{"id":"4d0c997a-36ab-4ae8-aff3-898a6a4030f6","entityType":"agent","slug":"clawhub-contentstudio-official-contentstudio","name":"ContentStudio","canonicalUrl":"https://www.xpersona.co/agent/clawhub-contentstudio-official-contentstudio","canonicalPath":"/agent/clawhub-contentstudio-official-contentstudio","generatedAt":"2026-10-11T14:13:06.447Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T10:33:47.704Z","emptyReason":null},"description":"ContentStudio is a tool to schedule social-media posts, manage the social inbox, and pull performance analytics across Facebook, LinkedIn, Twitter/X, Instagram, YouTube, TikTok, Pinterest, Threads, Tumblr, Bluesky, and Google Business Profile. Use when the user wants to list/create/delete/approve posts, find the best time to post, generate or edit images with AI, read and reply to DMs, comments and reviews, manage media, audit workspaces, accounts, campaigns, labels, categories, or team-members, or pull analytics reports (top posts, engagement, impressions, follower growth, AI insights, etc.) on their ContentStudio account.","descriptionLabel":"Source description","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1.1K downloads reported by the source. Last updated 10/11/2026.","installCommand":"clawhub skill install s173y8f4eqkny9nj3ecfc2mj3188g8pv:contentstudio","sourceUrl":"https://clawhub.ai/contentstudio-official/contentstudio","homepage":"https://clawhub.ai/contentstudio-official/skills/contentstudio","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/contentstudio-official/contentstudio","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/contentstudio-official/skills/contentstudio","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":61,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"ContentStudio technical dossier on Xpersona with agent coverage, OPENCLEW support, and live trust metadata."},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-11T10:33:47.704Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T10:33:47.704Z","emptyReason":null},"stars":null,"forks":null,"downloads":1086,"packageName":null,"latestVersion":"1.5.0","tractionLabel":"1.1K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T10:33:47.635Z","emptyReason":null},"lastUpdatedAt":"2026-10-11T10:33:47.704Z","lastCrawledAt":"2026-10-11T10:33:47.635Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-12T10:33:47.635Z","lastVerifiedAt":null,"highlights":[{"version":"1.5.0","createdAt":"2026-09-14T06:15:57.192Z","changelog":"Release 1.5.0","fileCount":6,"zipByteSize":64500},{"version":"1.4.1","createdAt":"2026-09-09T06:43:57.654Z","changelog":"Release 1.4.1","fileCount":6,"zipByteSize":54932},{"version":"1.4.0","createdAt":"2026-09-08T09:55:36.839Z","changelog":"Release 1.4.0","fileCount":6,"zipByteSize":54673},{"version":"1.3.0","createdAt":"2026-09-08T06:30:46.028Z","changelog":"Release 1.3.0","fileCount":6,"zipByteSize":53671},{"version":"1.2.0","createdAt":"2026-08-17T07:15:17.899Z","changelog":"Release 1.2.0","fileCount":6,"zipByteSize":43116},{"version":"1.1.1","createdAt":"2026-08-06T05:01:25.403Z","changelog":"Release 1.1.1","fileCount":6,"zipByteSize":38909},{"version":"1.1.0","createdAt":"2026-08-05T12:54:06.501Z","changelog":"Release 1.1.0","fileCount":6,"zipByteSize":39982},{"version":"1.0.12","createdAt":"2026-08-05T11:42:01.792Z","changelog":"Release 1.0.12","fileCount":6,"zipByteSize":30621}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s173y8f4eqkny9nj3ecfc2mj3188g8pv:contentstudio","setupComplexity":"low","setupSteps":["Install using `clawhub skill install s173y8f4eqkny9nj3ecfc2mj3188g8pv:contentstudio` in an isolated environment before connecting it to live workloads.","No published capability contract is available yet, so validate auth and request/response behavior manually.","Review the upstream CLAWHUB listing at https://clawhub.ai/contentstudio-official/contentstudio before using production credentials."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-contentstudio-official-contentstudio/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-contentstudio-official-contentstudio/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-contentstudio-official-contentstudio/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-contentstudio-official-contentstudio/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-contentstudio-official-contentstudio/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-contentstudio-official-contentstudio/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-11T14:13:06.440Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-contentstudio-official-contentstudio/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-contentstudio-official-contentstudio/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-contentstudio-official-contentstudio/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-contentstudio-official-contentstudio/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T10:33:47.704Z","emptyReason":null},"readme":"Skill: ContentStudio\n\nOwner: contentstudio-official\n\nSummary: ContentStudio is a tool to schedule social-media posts, manage the social inbox, and pull performance analytics across Facebook, LinkedIn, Twitter/X, Instagram, YouTube, TikTok, Pinterest, Threads, Tumblr, Bluesky, and Google Business Profile. Use when the user wants to list/create/delete/approve posts, find the best time to post, generate or edit images with AI, read and reply to DMs, comments and reviews, manage media, audit workspaces, accounts, campaigns, labels, categories, or team-members, or pull analytics reports (top posts, engagement, impressions, follower growth, AI insights, etc.) on their ContentStudio account.\n\nTags: latest:1.5.0\n\nVersion history:\n\nv1.5.0 | 2026-09-14T06:15:57.192Z | user\n\nRelease 1.5.0\n\nv1.4.1 | 2026-09-09T06:43:57.654Z | user\n\nRelease 1.4.1\n\nv1.4.0 | 2026-09-08T09:55:36.839Z | user\n\nRelease 1.4.0\n\nv1.3.0 | 2026-09-08T06:30:46.028Z | user\n\nRelease 1.3.0\n\nv1.2.0 | 2026-08-17T07:15:17.899Z | user\n\nRelease 1.2.0\n\nv1.1.1 | 2026-08-06T05:01:25.403Z | user\n\nRelease 1.1.1\n\nv1.1.0 | 2026-08-05T12:54:06.501Z | user\n\nRelease 1.1.0\n\nv1.0.12 | 2026-08-05T11:42:01.792Z | user\n\nRelease 1.0.12\n\nv1.0.10 | 2026-06-30T11:21:43.310Z | user\n\nVersion sync with npm release; description already includes Threads, Tumblr, Bluesky\n\nv1.0.9 | 2026-06-30T11:14:17.362Z | user\n\nAdd Threads, Tumblr, and Bluesky to the skill description (CLI already supported them)\n\nv1.0.8 | 2026-06-30T09:23:41.916Z | user\n\nDocument env-var auth (CONTENTSTUDIO_API_KEY) for headless/OpenClaw runtimes alongside auth:login\n\nv1.0.7 | 2026-06-30T05:59:41.027Z | user\n\nInitial ClawHub release\n\nArchive index:\n\nArchive v1.5.0: 6 files, 64500 bytes\n\nFiles: CHANGELOG.md (29986b), LICENSE (1070b), README.md (66269b), skill-card.md (2604b), SKILL.md (86291b), _meta.json (132b)\n\nFile v1.5.0:SKILL.md\n\n---\nname: contentstudio\ndescription: ContentStudio is a tool to schedule social-media posts, manage the social inbox, and pull performance analytics across Facebook, LinkedIn, Twitter/X, Instagram, YouTube, TikTok, Pinterest, Threads, Tumblr, Bluesky, and Google Business Profile. Use when the user wants to list/create/delete/approve posts, find the best time to post, generate or edit images with AI, read and reply to DMs, comments and reviews, manage media, audit workspaces, accounts, campaigns, labels, categories, or team-members, or pull analytics reports (top posts, engagement, impressions, follower growth, AI insights, etc.) on their ContentStudio account.\nversion: 1.5.0\nhomepage: https://api.contentstudio.io/guide\nmetadata: {\"openclaw\":{\"emoji\":\"📅\",\"requires\":{\"bins\":[\"contentstudio\"],\"env\":[\"CONTENTSTUDIO_API_KEY\"]}}}\n---\n\n## Install ContentStudio CLI if it doesn't exist\n\n```bash\nnpm install -g contentstudio-cli\n# or\npnpm install -g contentstudio-cli\n```\n\nnpm release: https://www.npmjs.com/package/contentstudio-cli\ncontentstudio-agent github: https://github.com/contentstudioio/contentstudio-agent\ncontentstudio API docs: https://api.contentstudio.io/api-docs\nofficial website: https://contentstudio.io\n\n---\n\n| Property | Value |\n|----------|-------|\n| **name** | contentstudio |\n| **description** | Social-media automation CLI for scheduling posts and managing media/accounts via the ContentStudio public API |\n| **allowed-tools** | Bash(contentstudio:*) |\n\n---\n\n## ⚠️ Authentication Required\n\n**You MUST authenticate before running any contentstudio CLI command.** All commands will fail without a valid API key.\n\nBefore doing anything else, check auth status:\n\n```bash\ncontentstudio auth:status\n```\n\nIf `has_api_key` is `false`, authenticate one of two ways. The user can generate a key from **ContentStudio Dashboard → Settings → API Keys**.\n\n1. **API key (interactive)** — stores the key in the CLI config file:\n\n```bash\ncontentstudio auth:login --api-key cs_...\n```\n\n2. **Environment variable (headless / agent runtimes)** — the CLI reads `CONTENTSTUDIO_API_KEY` from the environment and it takes precedence over the config file:\n\n```bash\nexport CONTENTSTUDIO_API_KEY=cs_...\n```\n\n> **Headless deployment note (OpenClaw, CI, daemons):** a shell `export` does **not** persist to a service process. Set `CONTENTSTUDIO_API_KEY` in the agent's actual environment — e.g. systemd `Environment=` (`systemctl edit`), an `EnvironmentFile=`, or Docker `-e` / compose `environment:` — then restart the service. Runtimes that gate on declared requirements (e.g. OpenClaw's `requires.env`) will stay blocked until this variable is present in the process environment.\n\nThen verify a workspace is selected:\n\n```bash\ncontentstudio --json workspaces:current\n```\n\nIf `active_workspace_id` is `null`, list workspaces and ask the user to pick one:\n\n```bash\ncontentstudio --json workspaces:list\ncontentstudio workspaces:use <workspace_id>\n```\n\n---\n\n## Invocation rules for agents\n\n- **Always pass `--json` before the subcommand** for stable, parseable output.\n- **Envelope shape**:\n  - Success: `{\"ok\": true, \"data\": <payload>, \"pagination\"?: {...}}`\n  - Error:   `{\"ok\": false, \"error\": {\"type\": \"<ErrorType>\", \"message\": \"...\", \"http_status\": <int>, \"hint\": \"...\"}}`\n- **Exit codes** are non-zero on error. Check both `returncode` and `ok`.\n- **Parse stdout only** — human messages go to stderr.\n- **Before any mutating action (posts/comments/media), run it with `--dry-run`** first to verify the payload is correct. `--dry-run` never touches the API.\n\n### Confirm the target workspace before mutating actions\n\nThe CLI silently defaults to the active workspace (whatever was set by `workspaces:use`). That default is fine for **read-only** calls (`workspaces:list`, `accounts:list`, `posts:list`, `media:list`, etc.) — just use the active workspace.\n\nBut for any **mutating** action — `accounts:connect`, `accounts:add-bluesky`, `accounts:add-facebook-group`, `accounts:remove`, `posts:create`, `posts:update`, `posts:delete`, `posts:approve`, `posts:reject`, `comments:add`, `media:upload`, `workspaces:update`, `workspaces:delete`, `labels:create`, `labels:update`, `labels:delete`, `campaigns:create`, `campaigns:update`, `campaigns:delete`, `team:add`, `team:update`, `team:remove`, and every `inbox:*` write (`inbox:send`, `inbox:comment-add`, `inbox:comment-delete`, `inbox:review-reply`, `inbox:update`, `inbox:tag-*`, …) — you MUST confirm the workspace with the user first, even if a workspace is already active. Don't assume the active workspace is the one they want to mutate.\n\n> **Inbox writes are customer-facing.** `inbox:send`, `inbox:comment-add`, and `inbox:review-reply` publish text to a real person on a real social platform, and there is no undo on the provider side. Always `--dry-run` first, show the exact message text to the user, and get explicit approval before sending. Never compose-and-send a reply to a customer in one step.\n\n(`workspaces:create` is the one write that is **not** workspace-scoped — it creates a brand-new workspace and ignores the active one.)\n\nPattern:\n\n1. Run `contentstudio --json workspaces:current` to see what's active.\n2. Tell the user: \"Your active workspace is **`<name>`** (`<id>`). Do you want to connect/post/delete in this workspace, or a different one?\"\n3. If they say a different one, run `workspaces:list`, let them pick, then either:\n   - Run `workspaces:use <id>` to switch the default, or\n   - Pass `--workspace <id>` on the single mutating call (preferred when it's a one-off — does not change the active workspace).\n4. Only then run the mutating command.\n\nThis is mandatory even when the user's request seems to imply the active workspace (\"connect a Facebook page\", \"create a draft post\") — they may have just switched contexts in their head and forgotten which workspace is active in the CLI.\n\n## Pagination — be proactive, don't silently truncate\n\n**All list commands return a `pagination` block** in JSON mode when more results exist than fit on one page:\n\n```json\n{\n  \"ok\": true,\n  \"data\": [ /* current page of items */ ],\n  \"pagination\": {\n    \"current_page\": 1,\n    \"per_page\": 10,\n    \"total\": 48,\n    \"last_page\": 5,\n    \"from\": 1,\n    \"to\": 10,\n    \"has_more\": true\n  }\n}\n```\n\n**Mandatory rule**: Whenever `pagination.has_more === true`, the user has more data than what was returned. **You MUST NOT silently treat the current page as \"all results\"**. Pick one of these three strategies:\n\n1. **Ask the user** (default for ambiguous requests):\n   > \"I retrieved 10 of your 48 workspaces. Do you want me to fetch the rest, or is the first 10 enough for what you're doing?\"\n\n2. **Auto-paginate** — if the user's request implies they want everything (e.g. \"list ALL my accounts\", \"show every draft post\", \"delete all queued posts\"):\n   - Call again with `--per-page <total>` to get everything in one round-trip:\n     ```bash\n     contentstudio --json workspaces:list --per-page 48\n     ```\n   - Or iterate `--page 2`, `--page 3`, … `--page <last_page>` if `total` is large (>200) and you want bounded pages.\n\n3. **Filter, don't paginate** — if the user asked for something specific (e.g. \"Facebook accounts only\"), use the relevant filter flag (`--platform facebook`, `--search \"...\"`, `--status draft`) instead of paginating. Smaller result set = no pagination needed.\n\n### Quick decision tree for the agent\n\n```\nDid the user say \"all\" / \"every\" / \"complete list\" / \"every single\"?\n  → YES: auto-paginate using --per-page <pagination.total>\n  → NO:\n      Did the user give a specific count? (\"show me top 5\", \"first 20 posts\")\n        → YES: respect that count; use --per-page accordingly\n        → NO:\n            pagination.has_more === true?\n              → YES: ASK the user before assuming you have everything\n              → NO: you have all the data; proceed\n```\n\n### Examples\n\n**User**: \"list my workspaces\"\n**Agent should**:\n1. Run `contentstudio --json workspaces:list --per-page 50` (high default to often avoid pagination)\n2. If `pagination.has_more` is still true, say: \"I see 50 of N workspaces. Want me to fetch all N?\"\n\n**User**: \"delete all my draft posts\"\n**Agent should**:\n1. Run `contentstudio --json posts:list --status draft --per-page 1` to peek at `total`\n2. Run `contentstudio --json posts:list --status draft --per-page <total>` to get them all\n3. Iterate over `data[]` and delete each\n4. Never delete just the first page and report \"done\"\n\n**User**: \"show me my Facebook accounts\"\n**Agent should**:\n1. Use `--platform facebook` filter — usually returns 0 or a handful, no pagination concern\n2. If `has_more` still true (>20 FB accounts), ask before auto-fetching\n\n### Endpoints that paginate\n\nAll `*:list` commands paginate:\n`workspaces:list`, `accounts:list`, `posts:list`, `comments:list`, `media:list`, `campaigns:list`, `categories:list`, `labels:list`, `team:list`, `approval-workflows:list`.\n\nNon-list commands (`auth:whoami`, `posts:create`, `posts:delete`, `media:upload`, etc.) never include `pagination` in their envelope.\n\n---\n\n## Command Reference\n\nAll commands are invoked as `contentstudio <group>:<command>`.\n\n### Authentication\n\n| Command | Purpose |\n|---------|---------|\n| `auth:login --api-key cs_...` | Store and verify API key |\n| `auth:logout` | Forget stored credentials |\n| `auth:whoami` | Hit `/me` and return user info |\n| `auth:status` | Show local config (key redacted) |\n\n### Workspaces\n\n| Command | Purpose |\n|---------|---------|\n| `workspaces:list` | List user's workspaces |\n| `workspaces:use <id>` | Set active workspace |\n| `workspaces:current` | Show active workspace |\n| `workspaces:create --name <n> --logo <url> --timezone <tz> [--super-admin-id <id>] [--note <t>] [--instagram-posting-method api\\|mobile] [--first-day-day <Day> --first-day-key <0-6>]` | Create a new workspace (NOT workspace-scoped) |\n| `workspaces:update [<id>] [--name] [--logo] [--timezone] [--note] [--instagram-posting-method] [--first-day-day --first-day-key]` | Update a workspace (defaults to active; ≥1 field required) |\n| `workspaces:delete <id>` | Delete a workspace |\n\n`workspaces:create` / `workspaces:update`:\n- `--name` ≤35 chars, letters/spaces/digits/period only.\n- `--logo` must be a URL; `--timezone` is an IANA string (e.g. `Asia/Karachi`).\n- `--super-admin-id` (create only) — account owner to create under; required when you manage multiple super admins.\n- First day of week is expressed as two paired flags: `--first-day-day <Sunday..Saturday>` + `--first-day-key <index>` where the key is the day's index (`Sunday=0 … Saturday=6`). Both build `first_day: {day, key}`.\n- `workspaces:update` defaults to the active workspace if `<id>` is omitted and requires at least one field.\n- Errors: `WORKSPACE_DELETE_FAILED` (422) on delete failure; 404 when the workspace doesn't exist.\n\n### Social accounts (read + connect)\n\n| Command | Purpose |\n|---------|---------|\n| `accounts:list [--platform <p>] [--search <q>]` | List connected social accounts |\n| `platforms:list` | List platforms supported for new account connections |\n| `accounts:connect <platform>` | Generate a one-time OAuth URL to connect a new account |\n| `accounts:connect <platform> --reconnect --account-id <id>` | Refresh an expired/invalid account |\n| `accounts:add-bluesky --handle <h> --app-password <p>` | Connect a Bluesky account (no browser — uses app password) |\n| `accounts:add-facebook-group --name <n> [--image <url>]` | Manually add a Facebook Group connection |\n| `accounts:remove <account_id>` | Remove (disconnect) a social account. `account_id` is the account's `id` from `accounts:list`. Requires the `save_social` permission (403 otherwise). |\n\n`--platform` values for `accounts:list` filter: `facebook`, `linkedin`, `twitter`, `instagram`, `youtube`, `tiktok`, `pinterest`, `gmb`.\n\n`<platform>` values for `accounts:connect`: `facebook`, `facebook-profile`, `instagram`, `instagram-via-facebook`, `twitter`, `linkedin`, `pinterest`, `tiktok`, `youtube`, `threads`, `gmb`, `tumblr`.\n\n**Account-connection flow for AI agents:**\n1. Run `platforms:list` to see what's supported and which method each uses (`oauth` / `credentials` / `manual`).\n2. For OAuth platforms (most), call `accounts:connect <platform>` and surface the returned URL to the user — they open it in their browser to authorize. The CLI itself never handles credentials.\n3. For Bluesky, ask the user for their handle + app-password (link them to <https://bsky.app/settings/app-passwords>) and call `accounts:add-bluesky`.\n4. For Facebook Groups, just call `accounts:add-facebook-group --name \"...\"`.\n\n### Posts\n\n| Command | Purpose |\n|---------|---------|\n| `posts:list [--status draft\\|scheduled\\|...] [--date-from] [--date-to]` | List posts |\n| `posts:create -c \"text\" -i <account> -t <publish_type> [-s \"YYYY-MM-DD HH:MM:SS\"] [-m <image_url>]` | Create a post (shortcut mode) |\n| `posts:create -c \"text\" -t content_category --content-category-id <cat_id>` | Create a content-category post (accounts come from the category) |\n| `posts:create -c \"text\" -i <fb_account> -t draft --facebook-carousel '<json>'` | Create a Facebook carousel post (2–10 cards) |\n| `posts:create -c \"text\" -i <threads_account> -t draft --threads '<json>'` | Create a Threads multi-thread (chained) post (max 10 items) |\n| `posts:create -c \"text\" -i <twitter_account> -t draft --twitter '<json>'` | Create a Twitter/X threaded-tweet post (max 10 tweets) |\n| `posts:create -c \"text\" -i <account> -t draft --first-comment \"...\" --first-comment-account <id>` | Create a post with a first comment |\n| `posts:create -c \"text\" -i <linkedin_account> -t draft --post-type poll --linkedin-options '<json>'` | Create a LinkedIn poll post (text-only) |\n| `posts:create -c \"text\" -i <ig_account> -t draft --post-type reel --video-url <url> --instagram-trial-reel` | Create an Instagram trial reel (shown to non-followers first) |\n| `posts:create -c \"common text\" -i <fb_account> -i <tiktok_account> -t draft -m <img_url> --platform-overrides '<json>'` | Same post to multiple platforms with a per-platform content override |\n| `posts:create --body /path/to/body.json` | Create a post with full JSON body |\n| `posts:update <post_id> [same flags as posts:create]` | Update an existing post (same body). Rejected (422) once the post is published/processing |\n| `posts:delete <post_id> [--delete-from-social]` | Delete a post |\n| `posts:approve <post_id> [--comment \"...\"]` | Approve a pending post |\n| `posts:reject <post_id> [--comment \"...\"]` | Reject a pending post |\n\n`-t / --publish-type` values: `scheduled`, `draft`, `queued`, `content_category`.\n\n`posts:update <post_id>` takes the **exact same flags and body** as `posts:create` (both `--body` and shortcut mode) — it PUTs to `/workspaces/{w}/posts/{post_id}`. The backend allows the update only while the post's status is **not** `published` or `processing` (otherwise it returns 422). Use `--approval-workflow-action` (below) on update to change an already-attached workflow.\n\n**`posts:create` / `posts:update` shortcut-mode flags:**\n- `-c / --content` (required) — post text.\n- `-i / --account <id>` (repeatable) — account ID(s) to post to. **Required UNLESS `--content-category-id` is given.**\n- `--content-category-id <id>` — sets top-level `content_category_id`. **Required by the backend when `--publish-type content_category`.** When set, accounts are derived from the category, so `--account` is not required (and may be omitted). Use this instead of `--account` for content-category posts.\n- `-s / --scheduled-at \"YYYY-MM-DD HH:MM:SS\"` — scheduling time. The CLI normalizes any parseable date to `YYYY-MM-DD HH:MM:SS` (the backend's required `date_format`) and sends it as a plain wall-clock string. **The API reads it in the workspace's timezone, not UTC** — so pass the local time the user wants the post to fire at, and get the zone from `workspaces:current` if you're unsure. `scheduling:best-times` already returns slots in that zone, so they can be passed straight through.\n- `-m / --image-url <url>` (repeatable), `--video-url <url>`, `--media-id <id>` (repeatable) — media.\n- `--post-type <type>` — e.g. `feed`, `reel`, `carousel`, `story`, `poll`. A **carousel** is auto-derived by the backend when `post_type=carousel` and 2+ images are attached. A **poll** requires `--post-type poll` **and** a text-only `--linkedin-options` poll block (no media).\n- `--label <id>` (repeatable, max 20) → `labels`.\n- `--campaign-id <id>` → `campaign_id`.\n- `--linkedin-options '<json>'` → `linkedin_options` (**LinkedIn accounts**). Pass a JSON **object**; the CLI parses it locally (invalid JSON → `ConfigError`) and sends it verbatim.\n  - Shape: `{ \"title\"?: <string>, \"poll\"?: { \"question\": <≤140>, \"options\": <string[2..4], each ≤30>, \"duration\": \"ONE_DAY\" | \"THREE_DAYS\" | \"SEVEN_DAYS\" | \"FOURTEEN_DAYS\" } }`\n  - A **poll** must be paired with `--post-type poll` and text-only content (no images/video). Backend validates and 422s on violations.\n- `--facebook-collaborator <user_id>` (repeatable, **max 10**) → `facebook_options.collaborators` (Facebook accounts). Merges with `--facebook-carousel` / `--facebook-background-id`.\n- `--instagram-collaborator <user_id>` (repeatable, **max 3**) → `instagram_options.collaborators` (Instagram accounts). Rejected (422) together with `--instagram-trial-reel`.\n- `--instagram-trial-reel` (boolean, default `false`) → `instagram_options.trial_reel.enabled`. Publishes an Instagram **trial reel** — shown to non-followers first, so it does not appear on the profile grid or in follower feeds.\n  - `--instagram-trial-reel-graduation SS_PERFORMANCE|MANUAL` (default `SS_PERFORMANCE`) → `instagram_options.trial_reel.graduation_strategy`. `SS_PERFORMANCE` lets Instagram auto-graduate it to followers if it performs well; `MANUAL` requires graduating it by hand in the Instagram app (Instagram has no API for that).\n  - Requires `--post-type reel` **exactly** (not `feed+reel`) and a video — feed/carousel/story are rejected. The CLI does not pre-validate this; the backend returns 422.\n  - **Rejected (422) together with `--instagram-collaborator`.** Share-to-story is silently dropped (not rejected) when combined with a trial reel.\n  - Not available when the workspace posts to Instagram via the mobile app (`instagram_posting_option=mobile`).\n- `--platform-overrides '<json>'` → `platform_overrides` (top-level, works across any platform in the post). Pass a JSON **object** keyed by platform (`facebook`, `instagram`, `twitter`, `linkedin`, `pinterest`, `youtube`, `tiktok`, `gmb`, `tumblr`, `threads`, `bluesky`, `telegram`); the CLI parses it locally (invalid JSON → `ConfigError`) and sends it verbatim.\n  - Shape per platform: `{ \"content\": { \"text\"?: <string>, \"post_type\"?: <string>, \"media\"?: { \"images\"?: <url[] ≤10>, \"video\"?: <url> } } }`.\n  - `text` and `post_type` each merge **independently** with the common top-level `content` — an override with only `media` still inherits the common `text`/`post_type`.\n  - `media` is **atomic**: if an override's `content` includes a `media` key at all, that platform's media is defined ENTIRELY by the override (no per-field fallback to the common media for whichever of `images`/`video` it omits). Omitting `media` entirely inherits the common `content.media` wholesale. This exists because some platforms (e.g. TikTok) can never support mixed images+video.\n  - Omitting `--platform-overrides` entirely publishes the same top-level `content` to every targeted platform.\n  - Override images are URLs only (no `media_ids`) and follow the same validation as the top-level media (max 10 images, no mixing images+video in one override).\n- **Approval — two mutually-exclusive systems (pass only one):**\n  - **Legacy** `--approver <user_id>` (repeatable) + `--approve-option anyone|everyone` (default `anyone`) + `--approval-notes \"...\"` → builds `approval: {approvers, approve_option, notes}` only when at least one approver is given. The post creator cannot be an approver. `anyone` = any single approver; `everyone` = all must approve.\n  - **Workflow** `--approval-workflow-id <id>` + `--approval-workflow-notes \"...\"` → `approval_workflow: {workflow_id, notes?}` — ATTACH a workflow (works on both create and update). Get the id from `approval-workflows:list` (its `id`).\n  - **Workflow (update only)** `--approval-workflow-action restart|resume|renotify_current|keep|remove` + `--approval-workflow-notes \"...\"` → `approval_workflow: {workflow_action, notes?}` — mutate the already-attached workflow. Only valid on `posts:update`.\n  - **Exactly one** of `--approval-workflow-id` / `--approval-workflow-action`, and `--approver` cannot be combined with either `--approval-workflow-*` flag. The CLI errors locally (`ConfigError`) if these rules are broken.\n- `--facebook-background-id <id>` → `facebook_options.facebook_background_id` (plain-text Facebook posts only; rejected if media is attached). Get a valid id from `facebook:text-backgrounds`.\n- `--facebook-carousel '<json>'` → `facebook_options.carousel` (**Facebook accounts only**). Pass a JSON **object**; the CLI parses it locally (invalid JSON → `ConfigError`) and adds `is_carousel_post: true`. It **merges** with `--facebook-background-id` (neither clobbers the other). The backend validates card counts/CTA/limits and returns a 422 if they're wrong.\n  - Shape: `{ \"cards\": [ { \"image\": <url, required>, \"link\": <url, required>, \"title\"?: <≤255>, \"description\"?: <≤1000> } ], \"call_to_action\"?, \"end_card\"?: <bool>, \"end_card_url\"?: <url>, \"accounts\"?: <string[]> }`\n  - **MIN 2, MAX 10 cards.** The Facebook account ID(s) still go in the top-level `-i / --account` (or in `carousel.accounts`).\n  - `call_to_action` is one of 33 values: `NO_BUTTON`, `ADD_TO_CART`, `APPLY_NOW`, `BET_NOW`, `BOOK_TRAVEL`, `BUY_NOW`, `BUY_TICKETS`, `CALL_NOW`, `CONTACT_US`, `DOWNLOAD`, `GET_DIRECTIONS`, `GET_OFFER`, `GET_QUOTE`, `GO_LIVE`, `INSTALL_MOBILE_APP`, `LEARN_MORE`, `LIKE_PAGE`, `LISTEN_MUSIC`, `OPEN_LINK`, `ORDER_NOW`, `PLAY_GAME`, `REGISTER_NOW`, `REQUEST_TIME`, `SAVE`, `MESSAGE_PAGE`, `WHATSAPP_MESSAGE`, `SHOP_NOW`, `SIGN_UP`, `SUBSCRIBE`, `USE_APP`, `WATCH_MORE`, `WATCH_VIDEO`.\n- `--threads '<json>'` → `threads_options` (**Threads accounts only**). Pass a JSON **array** of thread items; the CLI parses it locally (invalid JSON → `ConfigError`), sets `has_multi_threads: true` and `multi_threads: <array>`. The Threads account ID goes in the top-level `-i / --account`.\n  - Shape: `[ { \"message\": <string>, \"media\"?: <url[] ≤10>, \"media_ids\"?: <string[] ≤10> } ]`\n  - **MAX 10 items.** Each item needs `message` OR `media`. Threads allows mixed media. Backend validates limits and returns a 422 if exceeded.\n- `--twitter '<json>'` → `twitter_options` (**Twitter/X accounts only**). Pass a JSON **array** of tweet items; the CLI parses it locally (invalid JSON → `ConfigError`), sets `has_threaded_tweets: true` and `threaded_tweets: <array>`. The Twitter account ID goes in the top-level `-i / --account`. This mirrors `--threads` but for Twitter threaded tweets.\n  - Shape: `[ { \"message\": <string>, \"media\"?: <url[] ≤10>, \"media_ids\"?: <string[] ≤10> } ]`\n  - **MAX 10 tweets.** Each item needs `message` OR `media`. **Twitter does NOT allow mixed media in one tweet** (no images + video together) and **max 1 video per tweet**. The CLI does not validate tweet contents — the backend enforces these limits and returns a 422 if violated.\n- `--first-comment \"<message>\"` → `first_comment` (≤2000 chars). The CLI builds `first_comment: { message, accounts? }`. The accounts are supplied with `--first-comment-account <id>` (repeatable).\n  - `--first-comment-account <id>` (repeatable) → `first_comment.accounts`. **The backend REQUIRES at least one account when a `--first-comment` message is given, and the accounts must be a subset of the post's main `--account` IDs.** The CLI does not hard-block client-side — if you omit `--first-comment-account`, the backend returns a 422.\n\n(`--facebook-carousel`, `--facebook-collaborator`, `--instagram-collaborator`, `--instagram-trial-reel`, `--instagram-trial-reel-graduation`, `--linkedin-options`, `--platform-overrides`, `--threads`, and `--twitter` only apply in shortcut mode. The `--body` JSON mode already supports `facebook_options` (carousel + collaborators), `instagram_options` (`collaborators` + `trial_reel`), `linkedin_options`, `threads_options`, `twitter_options`, `first_comment`, `approval`, `approval_workflow`, and top-level `platform_overrides` natively — use it for posts that mix multiple platform option blocks.)\n\nThe `posts:list` payload now includes `linkedin_options` and `approval_workflow` per post (in addition to the existing fields) — they surface automatically in the `--json` output.\n\n### Scheduling — best time to post\n\n| Command | Purpose |\n|---------|---------|\n| `scheduling:best-times` | Ranked posting slots for the workspace, derived from the connected accounts' history |\n| `scheduling:best-times --account <platform>:<account_id>` | Restrict the analysis to specific accounts (repeatable) |\n| `scheduling:best-times --global-slots <n> --per-account-slots <n>` | How many recommendations to return (1–24 each) |\n| `scheduling:best-times --entities '<json>'` | Full entity array, for per-account slot counts |\n\nA **slot** is one recommended posting time: a weekday and an hour. Slots come back ranked best-first, so `--global-slots 3` means *the three best hours to post*.\n\n- **Times are always in the workspace timezone**, echoed as `meta.timezone`. There is no timezone parameter. That is the same clock `posts:create --scheduled-at` writes against, so a slot can be scheduled as-is — do **not** convert it to UTC first.\n- **Omit `--account` to analyse every connected account.** Otherwise pass `<platform>:<account_id>` where both halves come from one `accounts:list` row (its `platform` and `_id`), e.g. `--account facebook:<account_id>`. Supported platforms: `facebook`, `instagram`, `linkedin`, `twitter`, `tiktok`, `youtube`, `pinterest`, `threads`, `gmb`, `tumblr`, `bluesky`, `telegram`.\n- `--entities '[{\"id\":\"<account_id>\",\"type\":\"facebook\",\"slots\":3}]'` is the escape hatch for a **different slot count per account**; it cannot be combined with `--account`.\n- `--global-slots` (API default 5) sizes the pooled `global` view; `--per-account-slots` (API default 3) sizes each account's list. Both are 1–24 and are validated by the CLI before the call. Neither changes the underlying analysis or the `heatmap_matrix`, which always carries every hour that had signal.\n\n**Response shape** (`data` in the JSON envelope):\n\n- `meta` — `{generated_at, timezone, warnings[], missing_entities[], ai_fallback_entities[]}`.\n- `global` — pooled across analysed accounts: `top_recommendations[]` (each `{rank, day, date, time, score, platform_breakdown}`, where `time` is the hour as a bare string, e.g. `\"14\"` = 14:00), plus `heatmap_matrix.data` (sparse `[hour, day_index, score]` triples, `day_index` 0 = Monday) and `dates_key`. **`null` when no account had usable data.**\n- `individual` — the same breakdown keyed by account id, each with `platform` and `source` (`data_driven` or an AI fallback).\n\n**A thin workspace still returns HTTP 200.** Accounts with too little history come back in `meta.missing_entities` and `global` may be `null` — that is a successful read, not an error. Tell the user which accounts were skipped rather than reporting a failure. Accounts listed in `meta.ai_fallback_entities` are estimates, not measurements — say so when you present them.\n\nErrors: 422 for unknown accounts or a workspace with no connected accounts; 502 (`BackendError`) when the optimizer is temporarily unavailable — retry rather than reporting no data.\n\n**Reading is safe.** `scheduling:best-times` only reads, so it needs no `--dry-run` and no workspace confirmation. Scheduling a post from a slot is a mutation, so the usual `--dry-run` + workspace-confirmation rules apply to that step.\n\n### Comments / Internal notes\n\n| Command | Purpose |\n|---------|---------|\n| `comments:list <post_id>` | List comments on a post |\n| `comments:add <post_id> \"message\" [--note] [--mention <user_id>]` | Add public comment or internal note |\n\n### Media library\n\n| Command | Purpose |\n|---------|---------|\n| `media:list [--type images\\|videos] [--sort recent\\|...]` | List media assets |\n| `media:upload --file <local_path>` | Upload a local file |\n| `media:upload --url <external_url>` | Import from external URL |\n\n### AI images\n\n| Command | Purpose |\n|---------|---------|\n| `images:tools` | The image tools this API can invoke, with each tool's required inputs and control options |\n| `images:models` | Model identifiers `images:generate` accepts |\n| `images:brand` | `{configured, enabled}` — whether `--use-brand` will apply anything |\n| `images:generate -p \"<prompt>\"` | Prompt → image, saved to the media library |\n| `images:generate -p \"<edit>\" --image-url <url>` | Edit an existing image instead of generating from scratch |\n| `images:product-image --product-image-url <url>` | Restage a product photo |\n| `images:headshot --image-url <url>` | Professional headshot from a photo of a person |\n| `images:face-swap --target-image-url <url> --face-image-url <url>` | Put one image's face onto another's subject |\n| `images:outfit-swap --target-image-url <url> --outfit-image-url <url>` | Virtual try-on |\n| `images:upscale --image-url <url>` | Raise an image's resolution |\n| `images:remove-background --image-url <url>` | Cut the subject out of its background |\n| `images:tool <tool_key> --body '<json>'` | Any tool, with its full control set (this is how you reach `image-to-image`'s `style`, `aspect_ratio`, `image_resolution`, `image_quality`, multiple `attachments`, `reference_image_urls`) |\n\n**Every generation returns the same payload, and `data.media_id` is the handle you pass to `posts:create --media-id`.** That two-step is the normal way to publish an AI image — see the generate-then-publish recipe in the Examples section.\n\n```jsonc\n{ \"ok\": true, \"data\": {\n    \"media_id\": \"66f1a2b3c4d5e6f708192a3b\",   // → posts:create --media-id\n    \"url\": \"https://storage.googleapis.com/.../generated.png\",\n    \"width\": 1024, \"height\": 1024, \"mime_type\": \"image/png\",\n    \"model_used\": \"nano-banana-pro\",            // may differ from --model\n    \"brand_applied\": false,\n    \"credits\": { \"consumed\": 1, \"available\": 412 },\n    \"persist_error\": null } }\n```\n\n- **`media_id` is the durable handle; `url` is not.** Use `url` for a preview or as the input to the next tool. Do not store it — a `url` returned alongside a `persist_error` is a temporary provider link.\n- **Check `persist_error` (or `media_id !== null`) before calling a 200 done.** The image was generated *and charged* but could not be saved: `media_storage_full` means the workspace is out of media storage (retrying costs another credit and fails again), anything else is worth one retry. Tell the user to download the `url` now.\n- **Tools chain.** A media-library `url` from one call is valid input to the next (generate → upscale → remove-background). Each call is charged separately.\n- **Every image URL you pass in must be publicly fetchable over http(s)** by the image service — no auth, no expired signed URL, no private bucket, no local path. Upload a local file with `media:upload --file` first and pass the returned URL. A URL the service cannot download is `ValidationError` (`IMAGE_INPUT_REJECTED`), not a service outage.\n- **Generation is slow and billable.** The server's deadline is 120s; the CLI waits 150s (`--timeout <seconds>` to change it). These calls are **not retried** — the built-in 429/5xx retry is off for them, because re-running a generation can consume a second image credit. Retry deliberately, not in a loop.\n- **`--model` is optional.** Omit it for the service default. Costs differ (most models 1 image credit, `gpt-image-2` 5), so read `credits.consumed` rather than assuming.\n- **`model_used` is not one of the `images:models` values** — it comes back provider-prefixed (`fal-ai/nano-banana-pro`, `pixelcut/background-removal`) and names the model that actually ran after any fallback. Report it; never compare it for equality with `--model`.\n- **`images:tools` `controls` describe the underlying tool, not the public payload.** Take `--resolution` / `--aspect-ratio` values from there, but a control with no matching flag cannot be sent at all — `upscale` lists `model` and `upscale_factor`, and neither is in the API's tool payload. Likewise `accepts_instructions: true` on `headshot` and `face-swap` is not reachable: only `images:product-image` has `--instructions`. Sending an unsupported field is dropped in silence, so it will look like it worked.\n- **`--dimensions`** is `square`, `square_hd`, `portrait_4_5` or `landscape_16_9`, text→image only. Exact pixels are the model's choice — read `width`/`height` back. Anything else is rejected by the CLI before the call.\n- **Brand knowledge is a boolean, read-only.** `--use-brand` on `images:generate` only; it is resolved server-side and no brand ID or brand content is ever accepted or returned. `--use-brand` with no brand profile is `brand_applied: false`, not an error — `images:brand` tells you in advance. **The tool commands and `images:generate --image-url` always report `brand_applied: false`** — edits and tools do not apply brand knowledge.\n- **`--dry-run` on every generating command** prints the endpoint and body and calls nothing. Use it to show the user the prompt before spending a credit. The three discovery commands are reads and need no `--dry-run`.\n- **Rate limit: 30 requests/minute**, shared with the ContentStudio app's own AI usage on the same account. A `RateLimitError` here needs the full minute.\n- Video tools (`image-to-video`, `motion-control`, `lip-sync`, `talking-avatar`) are **not** on this API; asking for one is `NotFoundError` (`TOOL_NOT_FOUND`), same as an unknown key.\n- `images:tools` answering with an empty list means the catalogue is temporarily unreachable, not that the workspace has no tools. Retry rather than telling the user there are none.\n- Sample workspaces are read-only: the three discovery commands work, both generating paths return 403.\n\n### Lookup tables (read)\n\n| Command | Purpose |\n|---------|---------|\n| `campaigns:list` | List campaigns (folders) |\n| `categories:list` | List content categories |\n| `labels:list` | List labels |\n| `team:list` | List workspace team members |\n| `approval-workflows:list` | List approval workflows (use an item's `id` as `--approval-workflow-id`) |\n\nEach `approval-workflows:list` item is `{ id, name, is_default, levels: [{ level_number, title, rule, members: [{ user_id }] }] }`. Use `id` as `posts:create` / `posts:update`'s `--approval-workflow-id`.\n\n### Labels (write)\n\n| Command | Purpose |\n|---------|---------|\n| `labels:create --name <n> --color <color_N>` | Create a label |\n| `labels:update <label_id> [--name] [--color]` | Update a label |\n| `labels:delete <label_id>` | Delete a label |\n\n### Campaigns (write)\n\n| Command | Purpose |\n|---------|---------|\n| `campaigns:create --name <n> --color <color_N>` | Create a campaign |\n| `campaigns:update <campaign_id> [--name] [--color]` | Update a campaign |\n| `campaigns:delete <campaign_id>` | Delete a campaign |\n\nFor labels and campaigns: `--name` ≤100 chars; `--color` is one of the enum values `color_1` … `color_20`. On update, pass `--name` and/or `--color` (each is required-if-present).\n\n### Team members (write)\n\n| Command | Purpose |\n|---------|---------|\n| `team:add --email <e> --role <r> [--membership team\\|client] [--permissions '<json>']` | Invite a member |\n| `team:update <member_id> --role <r> --permissions '<json>' [--membership]` | Update a member's role/permissions |\n| `team:remove <member_id> [--confirmed]` | Remove a member |\n\n- `member_id` is the **membership id** — the `member_id` field from `team:list` (not the user's `id`, a distinct field).\n- `--role` (required): `admin`, `approver`, or `collaborator`.\n- `--email` (required for `team:add`): a single email address.\n- `--membership` (optional): `team` (internal) or `client` (external; hidden from internal notes). Default `team`.\n- `--permissions` (optional for `team:add`, **required for `team:update`**): a **role-aware** JSON object passed as a string (e.g. `--permissions '{\"addSocial\":true}'`). Invalid JSON → local `ConfigError`; invalid role/key combinations → backend 422. `team:update` is a partial merge — only the keys you send change; a role change drops boolean keys not valid for the new role.\n  - **Shared booleans** (any role): `accessSharedFolder`, `allow_workflow_management`.\n  - **admin**: full access — only the `hasBillingAccess` boolean applies.\n  - **collaborator** booleans: `addBlog`, `addSocial`, `addSource`, `addTopic`, `viewTeam`, `rescheduleQueue`, `postsReview`, `changeFBGroupPublishAs`, `hasListeningAccess`.\n  - **approver** booleans: `approverCanEditPost`, `approverCanAddNotes`, `approverCanCreatePost` (approvers can only approve/reject otherwise).\n  - **Account-access arrays** (any role; must be real connected account IDs in the workspace, else 422): `facebook`, `instagram`, `threads`, `twitter`, `linkedin`, `pinterest`, `telegram`, `youtube`, `tiktok`, `tumblr`, `tumblr_blogs`, `tumblr_profiles`, `bluesky`, `gmb`.\n  - **Blog arrays** (any role; not existence-validated): `wordpress`, `medium`, `shopify`, `webflow`.\n  - **content_categories** (any role; must be real category IDs in the workspace, else 422): array of content-category IDs.\n- `team:remove`: if the member is in approval workflows / in-flight posts, the backend returns error_code `REQUIRES_REMOVAL_CONFIRMATION` (422) — re-run with `--confirmed` (sends `?confirmed=true`) to proceed. 404 = `TEAM_MEMBER_NOT_FOUND`.\n\n### Social accounts (write)\n\n| Command | Purpose |\n|---------|---------|\n| `accounts:remove <account_id> [--dry-run]` | Remove (disconnect) a social account (`DELETE /workspaces/{w}/accounts/{account_id}`) |\n\n- `account_id` is the account's `id` from `accounts:list`.\n- Requires the `save_social` permission — callers without it get 403.\n- Errors: 401 (bad/missing API key), 403 (missing `save_social`), 404 (account not found in the workspace), 422 (removal failed). Success is 200 with an empty `data` array.\n- Mutating command — preview with `--dry-run` and confirm the workspace first.\n\n### Facebook helpers\n\n| Command | Purpose |\n|---------|---------|\n| `facebook:text-backgrounds` | List Facebook colored-background presets (use `id` as `facebook_options.facebook_background_id` on plain-text posts) |\n\n### Social Inbox\n\nThe inbox unifies three kinds of item into **elements**: `conversation` (DMs),\n`post` (a post with comments), and `review`. `inbox:list` is the entry point —\neverything else takes an id it returned.\n\n### Which id to pass\n\nInbox commands take their id from the `element_details` object on each\n`inbox:list` row. Use **`element_details.element_id`** — it is accepted by\nevery element-scoped command.\n\n| Command | Id to pass |\n|---------|------------|\n| `inbox:update` (`--element`) | `element_details.element_id` |\n| `inbox:tag-attach` / `inbox:tag-detach` | `element_details.element_id` |\n| `inbox:mark-read` | `element_details.element_id` |\n| `inbox:contact` / `inbox:contact-update` | `element_details.element_id` |\n| `inbox:messages` / `send` / `notes` / `note-add` / `bookmarks` | `element_details.element_id` (`t_…` form) |\n| `inbox:comments` / `inbox:comment-add` | `element_details.post_id` |\n\nValues look like:\n\n- `element_details.element_id` — `t_10000000000000001` (conversation) or\n  `100000000000000001_200000000000000002` (post)\n- `element_details.post_id` — `900000000000001_100000000000000001`\n\nThe row's top-level `element_ref` is an internal reference, not a command\nargument — always take the id from `element_details`.\n\nIf a command returns an empty list or reports the item as not found, confirm\nthe id against this table before describing the result to the user.\n\nAlso needed for most writes:\n\n- **`platform_id`** — the connected social account the item belongs to. The\n  backend replies through that account's token. It is on every `inbox:list`\n  row as `platform_id`, or from `accounts:list`.\n- The platform field on a list row is **`platform`** (not `platform_type`),\n  but the write commands take `--platform-type`.\n\n**Reading**\n\n| Command | Purpose |\n|---------|---------|\n| `inbox:list` | Search the inbox. `--type conversation\\|post\\|review` (repeatable), `--action all\\|marked_done\\|archived\\|assigned`, `--search`, `--tag`, `--channels '{\"facebook\":[\"<acct>\"]}'`, `--page`, `--limit` |\n| `inbox:summary` | Counts per bucket — cheap way to answer \"anything unread?\" |\n| `inbox:messages <conversation_id>` | Messages in a DM thread. Id = `element_details.element_id`. `--sort-order asc\\|desc` |\n| `inbox:comments <post_id>` | A post's comments (threaded). Id = `element_details.post_id` |\n| `inbox:notes <conversation_id>` | Internal notes (team-only). Id = `element_details.element_id`. Paginated |\n| `inbox:bookmarks <conversation_id>` | Starred messages. Id = `element_details.element_id`. Paginated |\n| `inbox:contact <element_ref>` | Contact profile behind an element |\n| `inbox:tags` | The workspace's inbox tag catalogue |\n\n**Replying — customer-facing, confirm before sending**\n\n| Command | Purpose |\n|---------|---------|\n| `inbox:send <conversation_id>` | Send a DM (id = `element_details.element_id`). Needs `--platform-type facebook\\|instagram`, `--platform-id`, and `--message` and/or `--file`. `--idempotency-key` de-dupes a retry |\n| `inbox:comment-add <post_id>` | Comment on a post. `--comment-id` makes it a threaded reply; `--private-reply` sends a Facebook DM instead; `--attachment <path>` attaches a file |\n| `inbox:review-reply <review_id>` | Add or replace a review reply (upsert). `--platform-id`, `--reply` |\n| `inbox:note-add <conversation_id>` | Add an internal note. `--mention <user_id>` (repeatable). Not customer-visible |\n\n**Triage and moderation**\n\n| Command | Purpose |\n|---------|---------|\n| `inbox:mark-read <element_ref>` | Mark read (idempotent) |\n| `inbox:update` | Bulk state change. `--element` (repeatable, **max 100**) plus **exactly one** of `--status done\\|pending`, `--archived`, `--assigned` (pair with `--assigned-to '{\"id\":\"<user>\"}'`) |\n| `inbox:comment-hide` / `inbox:comment-unhide <comment_id>` | Hide/unhide. Unhide needs `--platform-type` + `--platform-id` |\n| `inbox:comment-like` / `inbox:comment-unlike <comment_id>` | Facebook only |\n| `inbox:comment-delete <comment_id>` | Delete. Needs `--platform-type` + `--platform-id`; LinkedIn also needs `--comment-urn` |\n| `inbox:star` / `inbox:unstar <message_id>` | Star a message |\n| `inbox:message-delete <message_id>` | Soft-delete a message. `--platform-id` |\n| `inbox:review-reply-delete <review_id>` | Remove a review reply. `--platform-id` |\n| `inbox:contact-update <element_ref>` | `--platform-id` plus any of `--name`, `--email`, `--phone`, `--company` |\n\n**Tags**\n\n| Command | Purpose |\n|---------|---------|\n| `inbox:tag-create` | `--name` (≤50), `--color` — a **hex** value like `#33aa55`. (Older tags may display `color_1`, but the API now rejects that format.) |\n| `inbox:tag-update <tag_id>` | `--name` and/or `--color` |\n| `inbox:tag-delete` | `--tag <id>` (repeatable, bulk) |\n| `inbox:tag-merge` | Fold tags into a new one: `--name`, `--color`, `--tag` (repeatable) |\n| `inbox:tag-attach <element_ref>` | `--tag` (repeatable), `--platform-id`, `--inbox-type` |\n| `inbox:tag-detach <element_ref> <tag_id>` | `--platform-id`, `--inbox-type` |\n\n**Inbox pagination note.** Inbox list commands use `--limit` rather than\n`--per-page` (`--per-page` is accepted as an alias). The pagination rules in\nthe section above apply unchanged: if `pagination.has_more` is true, do not\nreport the first page as the whole inbox.\n\n> **Inbox page size is 200.** For inboxes larger than that, page through with\n> `--page 1`, `--page 2`, … up to `pagination.last_page` rather than raising\n> `--limit` past 200.\n\n**Inbox limits.** The CLI validates these locally, so they surface as a\n`ConfigError` before any request is sent:\n\n| Limit | Where |\n|-------|-------|\n| `--limit` ≤ 200 | `inbox:list`, `inbox:messages`, `inbox:comments` |\n| ≤ 100 `--element` refs per call | `inbox:update` |\n| Exactly **one** operation per call | `inbox:update` — `--status`, `--archived`, and `--assigned` are mutually exclusive; run separate commands |\n| Tag name ≤ 50 chars | `inbox:tag-create` |\n\n**Partial success on bulk updates.** `inbox:update` returns HTTP `207` when\nsome elements were updated and others were not, listing the remainder in\n`missing_ids`. The CLI reports this as a warning. When `missing_ids` is\nnon-empty, tell the user which elements did not change rather than reporting\nthe batch as fully applied.\n\n**`inbox:contact-update` updates the whole contact.** A contact is a person,\nnot a per-element attribute, so the change applies to every element for that\ncontact on that account in the workspace. The response's `updated_count` says\nhow many were updated. Mention this scope to the user before running it.\n\n**`inbox:contact` returns personal data.** Email and phone of an end customer.\nReturn only the fields the user actually asked for; don't dump the whole record\ninto a summary or paste it somewhere persistent without being asked.\n\n**`inbox:messages` includes activity events.** A thread contains both messages\nand a record of team activity. Activity entries have `message: null` and an\n`action` block (`MARKED_AS_DONE`, `PENDING`, `ARCHIVED`, …) naming the teammate\nwho performed it, and they count toward `total_messages` and pagination. Filter\non `action == null` when you mean customer messages — don't count activity\nentries as messages, quote them as customer text, or treat one as the latest\nreply. The CLI renders them as `— marked as done —` rows in human mode.\n\n**Replies are nested, not paginated.** In `inbox:comments`, replies live under\neach thread's `children` — they are not separate top-level rows. Paging counts\nthreads (`total_threads`), not individual comments, so \"12 comments\" from the\npagination block means 12 *threads* and there may be many more replies inside.\n\n**Handling a `409` on a send.** For `inbox:send` and `inbox:comment-add`, a\n`409` means the delivery outcome is undetermined — the message may or may not\nhave reached the customer. The CLI surfaces it as `ConflictError`. Do not retry\nautomatically: read the conversation back with `inbox:messages` to check\nwhether it landed, and tell the user what you found before sending again.\n\n**Confirming a send.** `inbox:send` returns `sent_message.id_status`. When it\nis `unavailable`, the platform accepted the message without returning an id, so\nthere is no id to reconcile against later — report it as sent, with delivery\nunconfirmed.\n\n**Inbox-specific responses.** A `502` from an `inbox:*` command indicates the\ninbox service is temporarily unreachable rather than a missing item — retry\nafter a short backoff. An empty `inbox:list` result is a successful empty\nread: report it as \"no matching conversations\", not \"not found\".\n\n### Analytics\n\nRead-only performance reports across Facebook, Instagram, YouTube, Pinterest,\nLinkedIn, Google Business Profile, TikTok, Twitter/X, **Meta Ads** and\n**Google Ads**, plus cross-network Campaigns & Labels reports (133 commands\ntotal, one per backend endpoint — no generic passthrough).\n\nMost commands need `--platform-id` (the connected account, from\n`accounts:list`) plus either a date range or a native post id. The ads and\ncampaign/label families are the exceptions — see below:\n\n- **Date-range reports** — `--start-date` / `--end-date` (`YYYY-MM-DD`,\n  both required). Optional on most: `--timezone` (IANA name, default UTC),\n  `--date` (alternative `'YYYY-MM-DD - YYYY-MM-DD'` form that overrides the\n  range), `--limit` / `--offset`, `--order-by` (choices vary per command —\n  check `--help`), and array filters like `--media-type`, `--hashtags`,\n  `--entity-type` (repeat the flag for multiple values).\n- **Single-item lookups** (`*-single-post`, `*-single-pin`, `*-single-tweet`,\n  `*-single-video`) — `--platform-id` + `--post-id` (the platform-native id,\n  not a ContentStudio internal id). No date range.\n- **AI insights** commands (`*-ai-insights`) additionally take `--type`\n  (`aiInsightsSummary` for the compact card, `aiInsightsDetailed` for the full\n  report) and `--language` (ISO 639-1, default `en`). Both ads platforms have\n  one too.\n- **Ads reports** (`analytics:meta-ads-*`, `analytics:google-ads-*`) take\n  `--account-id` — an *ad* account (`act_…` on Meta, a customer id on Google,\n  from `analytics:meta-ads-accounts` / `analytics:google-ads-accounts`) — not\n  `--platform-id`. Table commands add `--limit`/`--offset`, `--search`,\n  `--order-by`/`--order-dir` and id filters (`--campaign-id`, `--ad-set-id`,\n  `--ad-group-id`); chart commands add `--metric` and `--level`.\n  `analytics:*-ads-accounts` needs no account at all — it is how you find one.\n- **Campaigns & Labels** (`analytics:campaigns-labels-*`) are the only POST\n  reports: the filters are lists, so repeat the flag —\n  `--campaigns <id> --campaigns <id>`, `--labels <id>`, and one account list\n  per network (`--facebook-accounts`, `--instagram-accounts`, …). Only\n  `--start-date`/`--end-date` are required.\n\nRun `contentstudio analytics:<command> --help` to see the exact options for\nany one command — required vs. optional and enum choices differ per endpoint.\n\nEvery analytics command is read-only — the campaign/label ones are POSTs only\nbecause their filters are arrays — so none of them take `--dry-run` (that flag\nonly exists on mutating commands elsewhere in this CLI).\n\n**If a command returns `ANALYTICS_UPSTREAM_ERROR`** (HTTP 200 with\n`status: false`, often `upstream_status: 401`), that is the ContentStudio\nbackend's own analytics pipeline failing upstream — not a bad request. Report\nit as \"the analytics service is \n\nFile v1.5.0:README.md\n\n# contentstudio-cli\n\n[![npm version](https://img.shields.io/npm/v/contentstudio-cli.svg)](https://www.npmjs.com/package/contentstudio-cli)\n[![license](https://img.shields.io/npm/l/contentstudio-cli.svg)](./LICENSE)\n\n**Install as a skill:**\n```bash\nnpx skills add contentstudioio/contentstudio-agent\n```\n\nContentStudio CLI — schedule social-media posts, generate AI images, manage media, accounts, comments, approvals, the social inbox, and analytics across **Facebook, LinkedIn, Twitter/X, Instagram, YouTube, TikTok, Pinterest, and Google Business Profile** through the [ContentStudio](https://contentstudio.io) public API.\n\nThe `contentstudio` CLI provides a command-line interface for developers and AI agents to drive a ContentStudio workspace from the terminal — scheduling posts, generating and editing images with AI, uploading media, managing approvals, triaging the inbox, pulling analytics reports, and auditing accounts/campaigns/labels — using the same API your dashboard does.\n\n## Why use this CLI\n\n- **Drive ContentStudio from anywhere** — bash scripts, CI/CD pipelines, AI agents (Claude Code, Cursor, OpenCode, Codex), n8n workflows, custom automations.\n- **JSON output for agents** — every command supports `--json` returning a stable `{\"ok\": true, \"data\": ...}` envelope.\n- **Dry-run safety** — preview every mutating call before sending it, so AI agents (and humans) never publish by accident.\n- **No SaaS lock-in to your CLI tooling** — talks directly to the production ContentStudio API over HTTPS; no proxy, no extra service.\n\n## Installation\n\n### From npm (recommended)\n\n```bash\nnpm install -g contentstudio-cli\n# or\npnpm install -g contentstudio-cli\n```\n\nVerify:\n```bash\ncontentstudio --version\ncontentstudio --help\n```\n\n### Install the skill (for AI agents)\n\nIf you use an AI assistant (Claude Code, Cursor, OpenCode, Codex, Augment, IBM Bob, etc.), install the SKILL.md so the agent can drive this CLI on your behalf:\n\n```bash\nnpx skills add contentstudioio/contentstudio-agent\n```\n\nPick which agents to install into in the interactive prompt. The SKILL.md is dropped into each agent's skill directory (e.g. `~/.claude/skills/contentstudio/SKILL.md`).\n\n## Authentication\n\nAuthentication uses an **API key** issued from your ContentStudio dashboard.\n\n### Option 1: `auth:login` (persists to local config)\n\n```bash\ncontentstudio auth:login --api-key cs_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n```\n\nThis stores your key at `~/.config/contentstudio/config.json` (file mode `0600`, dir `0700`) and verifies it via a `/me` round-trip.\n\n```bash\n# Check current auth status (key redacted)\ncontentstudio auth:status\n\n# Verify the stored key is still valid\ncontentstudio --json auth:whoami\n\n# Remove stored credentials\ncontentstudio auth:logout\n```\n\n### Option 2: Environment variables\n\nFor CI/CD or one-off invocations, set the key in your environment instead of persisting:\n\n```bash\nexport CONTENTSTUDIO_API_KEY=cs_...\nexport CONTENTSTUDIO_WORKSPACE_ID=601b773d2149273f48039ec2     # optional\nexport CONTENTSTUDIO_BASE_URL=https://api.contentstudio.io/api/v1   # optional\n```\n\nEnv vars **take priority** over the persisted config when both are present.\n\n### Where to get an API key\n\nContentStudio Dashboard → **Settings → API Keys → Generate new key**.\n\n## Quick Start\n\n```bash\n# 1. Auth (once)\ncontentstudio auth:login --api-key cs_...\n\n# 2. Pick a workspace\ncontentstudio workspaces:list\ncontentstudio workspaces:use <workspace_id>\n\n# 3. List the social accounts connected to that workspace\ncontentstudio accounts:list --platform facebook\n\n# 4. Create a draft post (safe — won't publish to social)\ncontentstudio posts:create \\\n  --content \"Hello from contentstudio CLI\" \\\n  --account <account_id> \\\n  --publish-type draft\n\n# 5. Schedule a real post for 2 minutes from now\ncontentstudio posts:create \\\n  -c \"Hello from automation 👋\" \\\n  -i <account_id> \\\n  -t scheduled \\\n  -s \"$(date -d '+2 minutes' '+%F %T')\" \\\n  -m https://picsum.photos/400\n```\n\n## Discovery & Lookup\n\n### List your workspaces\n```bash\ncontentstudio --json workspaces:list\ncontentstudio --json workspaces:list --per-page 50\n```\nReturns workspace IDs, names, slugs, timezones.\n\n### Show / change the active workspace\n```bash\ncontentstudio workspaces:current\ncontentstudio workspaces:use <workspace_id>\n```\n\n### List connected social accounts\n```bash\ncontentstudio --json accounts:list                              # all accounts\ncontentstudio --json accounts:list --platform facebook          # filter\ncontentstudio --json accounts:list --search \"barcelona\"         # search by name\n```\n\n`--platform` values: `facebook`, `linkedin`, `twitter`, `instagram`, `youtube`, `tiktok`, `pinterest`, `gmb`.\n\n### Look up campaigns, labels, categories, team members\n```bash\ncontentstudio --json campaigns:list\ncontentstudio --json categories:list\ncontentstudio --json labels:list\ncontentstudio --json team:list\ncontentstudio --json approval-workflows:list   # use an item's id as --approval-workflow-id\n```\n\nAll support `--page` and `--per-page`; the campaigns/categories/labels/team lists also support `--search`.\n\n## Connecting Social Accounts\n\nThree ways to add new accounts to a workspace, depending on the platform.\n\n### List which platforms are connectable\n\n```bash\ncontentstudio --json platforms:list\n```\n\nReturns all 12+ supported platforms with their `connection_method` (`oauth`, `credentials`, or `manual`) and the endpoint to call.\n\n### OAuth platforms (Facebook, LinkedIn, Twitter, Instagram, YouTube, TikTok, Pinterest, GMB, Threads, Tumblr)\n\n```bash\ncontentstudio --json accounts:connect facebook\n# Returns a one-time authorization_url — open it in your browser to authorize.\n```\n\nTo **reconnect** an existing account that's expired or invalid:\n```bash\ncontentstudio --json accounts:connect facebook --reconnect --account-id <existing_account_id>\n```\n\nAvailable `<platform>` values: `facebook`, `facebook-profile`, `instagram`, `instagram-via-facebook`, `twitter`, `linkedin`, `pinterest`, `tiktok`, `youtube`, `threads`, `gmb`, `tumblr`.\n\n### Bluesky (credential-based, no browser)\n\nGenerate an app password at <https://bsky.app/settings/app-passwords> first, then:\n\n```bash\ncontentstudio --json accounts:add-bluesky \\\n  --handle yourname.bsky.social \\\n  --app-password xxxx-xxxx-xxxx-xxxx\n```\n\n⚠️ Use the Bluesky **app password**, NOT your main account password. The CLI redacts it from `--dry-run` output but it's still sent to ContentStudio's API over HTTPS.\n\n### Facebook Groups (manual)\n\n```bash\ncontentstudio --json accounts:add-facebook-group \\\n  --name \"My Community Group\" \\\n  --image https://example.com/group-cover.jpg\n```\n\nThe image URL is optional.\n\n### Remove (disconnect) an account\n\n```bash\n# preview first\ncontentstudio --json accounts:remove <account_id> --dry-run\n# then actually remove\ncontentstudio --json accounts:remove <account_id>\n```\n\n`account_id` is the account's `id` from `accounts:list`. Requires the `save_social` permission (403 otherwise); 404 if the account isn't in the workspace.\n\nAll three connect commands support `--dry-run` to preview the payload without calling the API.\n\n## Creating Posts\n\nThere are two ways to create a post: **shortcut flags** for simple cases, or **`--body <file.json>`** for the full schema.\n\n### Shortcut flags (simple posts)\n\n```bash\n# Scheduled post to a Facebook page with one image\ncontentstudio posts:create \\\n  -c \"Our latest blog post is live!\" \\\n  -i <account_id> \\\n  -t scheduled \\\n  -s \"2026-05-01 10:00:00\" \\\n  -m https://example.com/hero.jpg\n```\n\nOptions:\n\n| Flag | Purpose |\n|------|---------|\n| `-c`, `--content` | Post text |\n| `-i`, `--account` | Social account ID. Repeatable for multi-account posts. |\n| `-t`, `--publish-type` | `scheduled` \\| `draft` \\| `queued` \\| `content_category` |\n| `-s`, `--scheduled-at` | Schedule date `\"YYYY-MM-DD HH:MM:SS\"` |\n| `-m`, `--image-url` | External image URL. Repeatable. |\n| `--video-url` | External video URL |\n| `--media-id` | ID of media in your library (from `media:list`). Repeatable. |\n| `--post-type` | `feed` \\| `reel` \\| `story` \\| `feed+reel` \\| `feed+story` \\| `feed+reel+story` \\| `carousel` \\| `carousel+story` \\| `video` \\| `shorts` |\n| `--facebook-carousel '<json>'` | Facebook carousel (Facebook accounts only): JSON object `{cards:[{image,link,title?,description?}], call_to_action?, end_card?, end_card_url?, accounts?}`. 2–10 cards. CLI adds `is_carousel_post:true`. |\n| `--threads '<json>'` | Threads multi-thread (Threads accounts only): JSON array `[{message, media?, media_ids?}]`. Max 10 items. CLI adds `has_multi_threads:true`. |\n| `--twitter '<json>'` | Twitter/X threaded tweets (Twitter accounts only): JSON array `[{message, media?, media_ids?}]`. Max 10 tweets. CLI adds `has_threaded_tweets:true`. No mixed media per tweet (no images+video together), max 1 video per tweet. |\n| `--first-comment \"<message>\"` | First comment (≤2000 chars). Build `first_comment:{message, accounts?}`. Requires `--first-comment-account`. |\n| `--first-comment-account <id>` | Account for the first comment. Repeatable. Must be a subset of `--account`; backend 422s if omitted when `--first-comment` is set. |\n| `--dry-run` | Print the body that would be POSTed and exit (no API call) |\n\n### Multi-account post\n\nRepeat `-i` for each account:\n```bash\ncontentstudio posts:create \\\n  -c \"Cross-platform announcement 🚀\" \\\n  -i <facebook_id> \\\n  -i <linkedin_id> \\\n  -i <twitter_id> \\\n  -t scheduled \\\n  -s \"2026-05-01 09:00:00\"\n```\n\n### Use existing media library assets\n\n```bash\n# Find a media ID\ncontentstudio --json media:list --type images\n\n# Reference it by ID instead of URL\ncontentstudio posts:create \\\n  -c \"Post with library asset\" \\\n  -i <account_id> \\\n  -t draft \\\n  --media-id <media_library_id>\n```\n\n### Queued post (added to the publishing queue; no explicit time)\n\n```bash\ncontentstudio posts:create \\\n  -c \"Filler post for the queue\" \\\n  -i <account_id> \\\n  -t queued\n```\n\n`scheduled_at` is optional for `queued` — the backend slots it into the workspace queue automatically.\n\n### Content-category post (accounts come from the category — no `-i`)\n\n```bash\n# Find a category id first:\ncontentstudio --json categories:list\n\n# --content-category-id is required for -t content_category; accounts are derived from the category:\ncontentstudio posts:create \\\n  -c \"Evergreen tip of the day\" \\\n  -t content_category \\\n  --content-category-id <category_id>\n```\n\n### Facebook carousel (2–10 cards, Facebook accounts only)\n\n```bash\ncontentstudio --json posts:create --dry-run \\\n  -c \"Shop the new collection\" \\\n  -i <facebook_account_id> \\\n  -t scheduled \\\n  -s \"2026-07-01 10:00:00\" \\\n  --facebook-carousel '{\"cards\":[{\"image\":\"https://e.com/1.jpg\",\"link\":\"https://e.com/p1\",\"title\":\"Tee\"},{\"image\":\"https://e.com/2.jpg\",\"link\":\"https://e.com/p2\",\"title\":\"Hoodie\"}],\"call_to_action\":\"SHOP_NOW\",\"end_card\":true,\"end_card_url\":\"https://e.com/shop\"}'\n```\n\nThe CLI parses the JSON locally and adds `is_carousel_post: true`. A carousel and a colored-background text post (`--facebook-background-id`) are different Facebook formats — use one or the other, not both in the same post. CTA values (33 total) include `SHOP_NOW`, `LEARN_MORE`, `BUY_NOW`, `SIGN_UP`, … (see `SKILL.md` for the full list). The backend validates card counts and CTA values.\n\n### Threads multi-thread (chained, max 10 items, Threads accounts only)\n\n```bash\ncontentstudio --json posts:create --dry-run \\\n  -c \"🧵 A thread on shipping CLIs\" \\\n  -i <threads_account_id> \\\n  -t draft \\\n  --threads '[{\"message\":\"1/ Start small.\"},{\"message\":\"2/ Ship a demo.\",\"media\":[\"https://e.com/demo.mp4\"]},{\"message\":\"3/ Iterate in public.\"}]'\n```\n\nThe top-level `-c / --content` is the lead post; each `--threads` item is a chained reply, in order (don't repeat the lead text in the items). The CLI parses the JSON array locally and sets `has_multi_threads: true`. Each item needs `message` or `media`; Threads allows mixed media.\n\n### Twitter/X threaded tweets (chained, max 10 tweets, Twitter accounts only)\n\n```bash\ncontentstudio --json posts:create --dry-run \\\n  -c \"Why we built a CLI 🧵\" \\\n  -i <twitter_account_id> \\\n  -t draft \\\n  --twitter '[{\"message\":\"1/ Start with the contract.\"},{\"message\":\"2/ Show, don'\\''t tell.\",\"media\":[\"https://e.com/x.jpg\"]},{\"message\":\"3/ Ship it.\"}]'\n```\n\nThe top-level `-c / --content` is the lead tweet; each `--twitter` item is a follow-up tweet in the chain, in order (don't repeat the lead text in the items). The CLI parses the JSON array locally and sets `has_threaded_tweets: true`. Each item needs `message` or `media`. Unlike Threads, Twitter does **not** allow mixed media in one tweet (no images + video together) and allows **max 1 video per tweet** — the backend enforces this and returns a 422 if violated.\n\n### Per-platform content overrides (`--platform-overrides`)\n\nPublish the same post to several platforms but swap the caption, post type, or media for one of them:\n\n```bash\ncontentstudio --json posts:create --dry-run \\\n  -c \"Common caption\" \\\n  -i <facebook_id> -i <tiktok_id> \\\n  -t draft \\\n  -m https://example.com/common.jpg \\\n  --platform-overrides '{\"tiktok\":{\"content\":{\"media\":{\"video\":\"https://example.com/clip.mp4\"}}}}'\n```\n\nTikTok publishes with the *common* text (`\"Common caption\"`, inherited — the override didn't touch `text`) and its *own* video, with **no images at all** — because the override's `content` includes a `media` key, TikTok's media is defined entirely by the override (no per-field fallback to the common image). Facebook, which has no override entry, publishes the common text and image unchanged.\n\nKeyed platforms: `facebook`, `instagram`, `twitter`, `linkedin`, `pinterest`, `youtube`, `tiktok`, `gmb`, `tumblr`, `threads`, `bluesky`, `telegram`. Each value is `{\"content\":{\"text\"?,\"post_type\"?,\"media\"?:{\"images\"?,\"video\"?}}}`. `text` and `post_type` merge independently with the common `content` (an override can set one without the other); `media` is all-or-nothing per platform. Omit `--platform-overrides` to publish the same `content` everywhere.\n\n### Post with a first comment\n\n```bash\ncontentstudio --json posts:create --dry-run \\\n  -c \"New drop is live 🎉\" \\\n  -i <account_id> \\\n  -t draft \\\n  --first-comment \"🔗 link in bio\" \\\n  --first-comment-account <account_id>\n```\n\nThe CLI builds `first_comment: { message, accounts }`. `--first-comment-account` is **required** by the backend when `--first-comment` is set and must be a subset of the `-i / --account` IDs; otherwise the API returns a 422.\n\n### Full body via `--body <file.json>`\n\nFor platform-specific options (TikTok privacy, YouTube category, GMB topic, approval workflow, first-comment, labels, campaigns, etc.), write a JSON body and pass it via `--body`:\n\n```bash\ncontentstudio --json posts:create --body /tmp/post.json\n```\n\nBody schema:\n```jsonc\n{\n  \"content\": {\n    \"text\": \"Hello world\",\n    \"media\": {\n      \"images\": [\"https://example.com/img.jpg\"],\n      \"video\": \"https://example.com/clip.mp4\",\n      \"media_ids\": [\"<media_library_id>\"]\n    }\n  },\n  \"accounts\": [\"<account_id>\"],\n  \"post_type\": \"reel+story\",\n  \"post_video_title\": \"My Video Title\",\n  \"scheduling\": {\n    \"publish_type\": \"scheduled\",\n    \"scheduled_at\": \"2026-05-01 10:00:00\"\n  },\n  \"first_comment\": {\n    \"message\": \"🔗 link in bio\",\n    \"accounts\": [\"<account_id>\"]\n  },\n  \"labels\": [\"<label_id>\"],\n  \"campaign_id\": \"<campaign_id>\",\n  \"approval\": {\n    \"approvers\": [\"<user_id>\"],\n    \"approve_option\": \"anyone\",\n    \"notes\": \"please review\"\n  },\n  // facebook_options: use EITHER carousel OR facebook_background_id (different FB formats, not both):\n  \"facebook_options\":  { \"carousel\": { \"is_carousel_post\": true, \"cards\": [ {\"image\":\"https://...\",\"link\":\"https://...\",\"title\":\"...\",\"description\":\"...\"} ], \"call_to_action\": \"SHOP_NOW\", \"end_card\": true, \"end_card_url\": \"https://...\" } },\n  // colored-background text post instead: \"facebook_options\": { \"facebook_background_id\": \"<id>\" },\n  \"threads_options\":   { \"has_multi_threads\": true, \"multi_threads\": [ {\"message\":\"1/ ...\"}, {\"message\":\"2/ ...\",\"media\":[\"https://...mp4\"]} ] },\n  \"youtube_options\":   { \"title\": \"...\", \"privacy_status\": \"public\", \"category\": \"EDUCATION\", \"tags\": [\"tag1\"], \"license\": \"youtube\", \"made_for_kids\": false },\n  \"tiktok_options\":    { \"privacy_level\": \"PUBLIC_TO_EVERYONE\", \"disable_comment\": false, \"disable_duet\": false, \"disable_stitch\": false, \"auto_add_music\": false },\n  \"pinterest_options\": { \"title\": \"...\", \"link\": \"https://...\" },\n  \"gmb_options\":       { \"topic_type\": \"EVENT\", \"start_date\": \"2026-05-01\", \"end_date\": \"2026-05-02\", \"title\": \"...\", \"action_type\": \"BOOK\", \"cta_link\": \"https://...\" }\n}\n```\n\n### Always preview with `--dry-run` first\n\nFor agents (and cautious humans), every mutating command supports `--dry-run` — it prints the request body and exits **without** calling the API:\n\n```bash\ncontentstudio --json posts:create --dry-run \\\n  -c \"Test\" -i <account_id> -t scheduled -s \"2026-05-01 10:00\"\n# → {\"ok\": true, \"data\": {\"dry_run\": true, \"endpoint\": \"...\", \"body\": {...}}}\n```\n\n## Best Time to Post\n\n`scheduling:best-times` analyses the historical performance of the workspace's connected accounts and returns ranked posting **slots** — a weekday and an hour, best-first.\n\n```bash\n# Best times across every connected account\ncontentstudio --json scheduling:best-times\n\n# Just this Facebook page, 3 recommendations for it\ncontentstudio --json scheduling:best-times \\\n  --account facebook:<account_id> --per-account-slots 3\n\n# Several accounts, and a bigger pooled list\ncontentstudio --json scheduling:best-times \\\n  --account facebook:<account_id> \\\n  --account instagram:<account_id> \\\n  --global-slots 10\n\n# Per-account slot counts need the full entity array\ncontentstudio --json scheduling:best-times \\\n  --entities '[{\"id\":\"<account_id>\",\"type\":\"facebook\",\"slots\":5},\n               {\"id\":\"<account_id>\",\"type\":\"linkedin\",\"slots\":2}]'\n```\n\n`--account` takes `<platform>:<account_id>` — both halves come from a single `accounts:list` row (its `platform` and `_id`). Omit it to analyse everything connected. Supported platforms: `facebook`, `instagram`, `linkedin`, `twitter`, `tiktok`, `youtube`, `pinterest`, `threads`, `gmb`, `tumblr`, `bluesky`, `telegram`.\n\n`--global-slots` (API default 5) and `--per-account-slots` (API default 3) are 1–24 and only control how much of the ranking comes back — they never change the analysis, and they don't affect `heatmap_matrix`, which always carries every hour that had signal.\n\nThe `--json` payload:\n\n```json\n{\n  \"ok\": true,\n  \"data\": {\n    \"meta\": {\n      \"generated_at\": \"2026-08-17T09:00:00Z\",\n      \"timezone\": \"Asia/Karachi\",\n      \"warnings\": [],\n      \"missing_entities\": [],\n      \"ai_fallback_entities\": []\n    },\n    \"global\": {\n      \"top_recommendations\": [\n        { \"rank\": 1, \"day\": \"Wednesday\", \"date\": \"2026-08-19\", \"time\": \"14\",\n          \"score\": 100, \"platform_breakdown\": { \"facebook\": 60, \"instagram\": 40 } }\n      ],\n      \"heatmap_matrix\": { \"data\": [[14, 2, 100]] },\n      \"dates_key\": [\"2026-08-19\"]\n    },\n    \"individual\": {\n      \"<account_id>\": { \"platform\": \"facebook\", \"source\": \"data_driven\",\n                        \"top_recommendations\": [] }\n    }\n  }\n}\n```\n\n**Times are always in the workspace timezone** (echoed as `meta.timezone`); there is no timezone parameter. That is the same clock `posts:create --scheduled-at` writes against, so a slot goes in as-is — converting it to UTC first would move the post:\n\n```bash\n# rank 1 above → Wednesday 2026-08-19 at 14:00 workspace-local\ncontentstudio --json posts:create \\\n  -c \"Launch day is here.\" -i <account_id> -t scheduled \\\n  -s \"2026-08-19 14:00:00\" --dry-run\n```\n\nA workspace with too little history still returns HTTP 200: the accounts that could not be analysed are listed in `meta.missing_entities` and `global` may be `null`. Accounts in `meta.ai_fallback_entities` are estimates rather than measurements. Errors are 422 (unknown accounts, or no connected accounts) and 502 (`BackendError`) when the optimizer is temporarily unavailable.\n\n## Managing Posts\n\n### List posts (with filters)\n\n```bash\ncontentstudio --json posts:list                                          # all recent\ncontentstudio --json posts:list --status draft --per-page 5\ncontentstudio --json posts:list --status scheduled --status published\ncontentstudio --json posts:list --date-from 2026-04-01 --date-to 2026-04-30\n```\n\n### Update a post\n\n`posts:update <post_id>` takes the **same flags/body** as `posts:create` (both `--body` and shortcut mode) and PUTs to `/workspaces/{w}/posts/{post_id}`. The backend rejects the update (422) once the post is `published` or `processing`.\n\n```bash\n# Preview an edit (change text + reschedule)\ncontentstudio --json posts:update <post_id> -c \"Updated copy\" -i <account_id> -t scheduled -s \"2026-08-01 10:00:00\" --dry-run\n\n# Attach an approval workflow (get the id from approval-workflows:list)\ncontentstudio --json posts:update <post_id> -c \"Q3 launch\" -i <account_id> -t draft --approval-workflow-id <workflow_id>\n\n# Mutate the already-attached workflow (update only)\ncontentstudio --json posts:update <post_id> -c \"Q3 launch\" -i <account_id> -t draft --approval-workflow-action restart --approval-workflow-notes \"please re-review\"\n\n# LinkedIn poll (text-only; requires --post-type poll)\ncontentstudio --json posts:update <post_id> -c \"Vote!\" -i <linkedin_account_id> -t draft --post-type poll \\\n  --linkedin-options '{\"poll\":{\"question\":\"Best day to ship?\",\"options\":[\"Mon\",\"Fri\"],\"duration\":\"SEVEN_DAYS\"}}'\n```\n\n### Delete a post\n\n```bash\n# Just delete from ContentStudio\ncontentstudio --json posts:delete <post_id>\n\n# Also delete from the connected social platforms\ncontentstudio --json posts:delete <post_id> --delete-from-social\n\n# Limit the cross-platform delete to specific accounts\ncontentstudio --json posts:delete <post_id> --account <account_id> --delete-from-social\n\n# Preview without deleting\ncontentstudio --json posts:delete <post_id> --dry-run\n```\n\n### Approve / reject a post in an approval workflow\n\n```bash\ncontentstudio --json posts:approve <post_id> --comment \"LGTM, ship it\"\ncontentstudio --json posts:reject  <post_id> --comment \"fix the link first\"\n\n# Preview without acting\ncontentstudio --json posts:approve <post_id> --dry-run\n```\n\n## Comments & Internal Notes\n\n```bash\n# List all comments / notes on a post\ncontentstudio --json comments:list <post_id>\n\n# Add a public comment\ncontentstudio --json comments:add <post_id> \"Great work team!\"\n\n# Add an internal note (not visible to the public)\ncontentstudio --json comments:add <post_id> \"Double-check the link before publishing\" --note\n\n# Mention team members\ncontentstudio --json comments:add <post_id> \"Heads up\" --mention <user_id> --mention <user_id>\n\n# Preview\ncontentstudio --json comments:add <post_id> \"test\" --note --dry-run\n```\n\n> Note: `comments:*` are **ContentStudio-internal** comments on a *draft/scheduled\n> post* — collaboration between your team. To reply to a real comment left by a\n> real person on a published post, use the Social Inbox commands below.\n\n## Social Inbox\n\nThe inbox brings DMs, post comments, and reviews into one place. It models all\nthree as **elements**, each identified by an `element_ref`:\n\n**Ids come from `element_details`.** Use `element_details.element_id` — it is\naccepted by every element-scoped command:\n\n| Id from `inbox:list` | Used by |\n|----------------------|---------|\n| `element_details.element_id` | every element-scoped command, plus `messages` / `send` / `notes` / `bookmarks` |\n| `element_details.post_id` | `comments`, `comment-add` |\n\nThe row's top-level `element_ref` is an internal reference, not a command\nargument — take the id from `element_details`.\n\n| Inbox type | What it is |\n|------------|------------|\n| `conversation` | A DM thread (Facebook / Instagram) |\n| `post` | A published post with comments on it |\n| `review` | A review (e.g. Google Business Profile) |\n\nMost write commands also need `--platform-id` — the connected account the item\nbelongs to, since replies go out through that account. Find it with\n`accounts:list`.\n\n### Browse and search\n\n```bash\n# Counts per bucket — cheapest way to see if anything needs attention\ncontentstudio --json inbox:summary\n\n# Everything\ncontentstudio --json inbox:list\n\n# Just unanswered DMs, 50 at a time\ncontentstudio --json inbox:list --type conversation --action all --limit 50\n\n# Full-text search, restricted to one Facebook account\ncontentstudio --json inbox:list \\\n  --search \"refund\" \\\n  --channels '{\"facebook\":[\"<account_id>\"]}'\n\n# Filter by tag\ncontentstudio --json inbox:list --tag <tag_id> --tag <tag_id>\n```\n\nInbox lists use `--limit` (with `--per-page` accepted as an alias) and `--page`.\n\n**Limits the API enforces**, checked client-side before any request goes out:\n\n| Limit | Applies to |\n|-------|------------|\n| `--limit` ≤ 200 | `inbox:list`, `inbox:messages`, `inbox:comments` |\n| ≤ 100 `--element` refs | `inbox:update` |\n| Exactly one operation per call | `inbox:update` (`--status` / `--archived` / `--assigned` are mutually exclusive) |\n| Tag name ≤ 50 chars | `inbox:tag-create` |\n\n`inbox:update` may also come back as a **partial** success (HTTP `207`) when\nsome elements could not be updated. The CLI prints a warning listing the\nuntouched refs instead of reporting a clean pass.\n\n### Read a thread\n\n```bash\n# Messages, newest first\ncontentstudio --json inbox:messages <conversation_id> --sort-order desc --limit 20\n\n# A thread also contains team activity entries (marked done, archived, ...).\n# Those have `message: null` and an `action` block — filter on\n# `action == null` when you want customer messages only.\n\n# Comments on a published post\ncontentstudio --json inbox:comments <post_id>\n\n# Team-only notes attached to a conversation\ncontentstudio --json inbox:notes <conversation_id>\n\n# Starred messages\ncontentstudio --json inbox:bookmarks <conversation_id>\n\n# Who am I talking to?\ncontentstudio --json inbox:contact <element_ref>\n```\n\n### Reply\n\nThese reach real customers. Preview with `--dry-run` first.\n\n```bash\n# Send a DM\ncontentstudio --json inbox:send <conversation_id> \\\n  --platform-type facebook \\\n  --platform-id <account_id> \\\n  --message \"Thanks for reaching out — shipping today!\" \\\n  --dry-run\n\n# Send a DM with an image attached\ncontentstudio --json inbox:send <conversation_id> \\\n  --platform-type instagram --platform-id <account_id> \\\n  --message \"Here's the size chart\" \\\n  --file ./size-chart.png --file-type image\n\n# Comment on a post\ncontentstudio --json inbox:comment-add <post_id> \\\n  --platform-type facebook --platform-id <account_id> \\\n  --message \"Glad you like it!\"\n\n# Reply to a specific comment (threaded)\ncontentstudio --json inbox:comment-add <post_id> \\\n  --platform-type facebook --platform-id <account_id> \\\n  --comment-id <comment_id> --message \"DMing you the details.\"\n\n# Facebook private reply — answers a public comment via DM\ncontentstudio --json inbox:comment-add <post_id> \\\n  --platform-type facebook --platform-id <account_id> \\\n  --comment-id <comment_id> --private-reply \\\n  --message \"Sent you a DM with your order info.\"\n\n# Reply to a review (upsert — replaces an existing reply)\ncontentstudio --json inbox:review-reply <review_id> \\\n  --platform-id <account_id> --reply \"Thanks for the feedback!\"\n\n# Internal note — your team only, never shown to the customer\ncontentstudio --json inbox:note-add <conversation_id> \\\n  --platform-type facebook --platform-id <account_id> \\\n  --message \"Escalated to billing\" --mention <user_id>\n```\n\nRetrying a send? Pass `--idempotency-key <uuid>` so a repeated request isn't\ndelivered twice. It protects sequential retries, not concurrent ones.\n\n### Triage\n\n```bash\ncontentstudio --json inbox:mark-read <element_ref>\n\n# Bulk: close out several at once (max 100 refs per call)\ncontentstudio --json inbox:update \\\n  --element <ref_1> --element <ref_2> --status done\n\n# Archive / assign — exactly ONE operation per call\ncontentstudio --json inbox:update --element <ref> --archived\ncontentstudio --json inbox:update --element <ref> \\\n  --assigned --assigned-to '{\"id\":\"<user_id>\"}'\n\n# Star a message\ncontentstudio --json inbox:star <message_id>\ncontentstudio --json inbox:unstar <message_id>\n```\n\n### Moderate\n\n```bash\n# Hide is reversible — prefer it over delete\ncontentstudio --json inbox:comment-hide <comment_id>\ncontentstudio --json inbox:comment-unhide <comment_id> \\\n  --platform-type facebook --platform-id <account_id>\n\n# Like / unlike (Facebook)\ncontentstudio --json inbox:comment-like <comment_id>\ncontentstudio --json inbox:comment-unlike <comment_id>\n\n# Delete a comment (LinkedIn additionally needs --comment-urn)\ncontentstudio --json inbox:comment-delete <comment_id> \\\n  --platform-type facebook --platform-id <account_id>\n\n# Delete a message / a review reply\ncontentstudio --json inbox:message-delete <message_id> --platform-id <account_id>\ncontentstudio --json inbox:review-reply-delete <review_id> --platform-id <account_id>\n```\n\n### Tags\n\n```bash\ncontentstudio --json inbox:tags\ncontentstudio --json inbox:tag-create --name \"VIP\" --color \"#ff0055\"\ncontentstudio --json inbox:tag-update <tag_id> --name \"VIP customer\"\ncontentstudio --json inbox:tag-delete --tag <tag_id> --tag <tag_id>\n\n# Fold several tags into one new tag\ncontentstudio --json inbox:tag-merge --name \"Support\" --color \"#0088ff\" \\\n  --tag <tag_id> --tag <tag_id>\n\n# Attach / detach on an element\ncontentstudio --json inbox:tag-attach <element_ref> \\\n  --tag <tag_id> --platform-id <account_id> --inbox-type conversation\ncontentstudio --json inbox:tag-detach <element_ref> <tag_id> \\\n  --platform-id <account_id> --inbox-type conversation\n```\n\n### Updating contact details\n\n```bash\ncontentstudio --json inbox:contact-update <element_ref> \\\n  --platform-id <account_id> \\\n  --name \"Jane Doe\" --email jane@example.com --company \"Acme\"\n```\n\n## Media Library\n\n### List media assets\n\n```bash\ncontentstudio --json media:list                                          # all\ncontentstudio --json media:list --type images --sort recent --per-page 20\ncontentstudio --json media:list --type videos\ncontentstudio --json media:list --search \"campaign-2026\"\n```\n\n`--sort` values: `recent`, `oldest`, `size`, `a2z`, `z2a`.\n\n### Upload media\n\nUpload a local file:\n```bash\ncontentstudio --json media:upload --file ./hero.jpg\n```\n\nOr import from an external URL:\n```bash\ncontentstudio --json media:upload --url https://example.com/asset.mp4\n```\n\nOptionally place into a folder:\n```bash\ncontentstudio --json media:upload --file ./hero.jpg --folder-id <folder_id>\n```\n\nPreview (no upload):\n```bash\ncontentstudio --json media:upload --url https://example.com/img.jpg --dry-run\n```\n\nThe response includes an `id` you can pass as `--media-id` when creating posts.\n\n## Analytics\n\nRead-only performance reports across Facebook, Instagram, YouTube, Pinterest, LinkedIn, Google Business Profile, TikTok, Twitter/X, Meta Ads and Google Ads, plus cross-network Campaigns & Labels reports — 133 commands under the `analytics:` namespace, one per backend endpoint. Full per-platform command reference lives in [SKILL.md](./SKILL.md#analytics).\n\n```bash\n# Date-range report — most commands take --platform-id + --start-date/--end-date\ncontentstudio --json analytics:instagram-top-posts \\\n  --platform-id <account_id> --start-date 2026-08-01 --end-date 2026-08-12\n\n# With optional filters (order-by is an enum, media-type is repeatable)\ncontentstudio --json analytics:facebook-get-top-posts \\\n  --platform-id <account_id> --start-date 2026-08-01 --end-date 2026-08-12 \\\n  --order-by comments --media-type IMAGE --media-type VIDEO\n\n# Single-item lookup — platform-native id, not a ContentStudio id\ncontentstudio --json analytics:youtube-single-video --platform-id <account_id> --post-id <video_id>\n\n# AI-generated insights\ncontentstudio --json analytics:linkedin-ai-insights \\\n  --platform-id <account_id> --start-date 2026-08-01 --end-date 2026-08-12 --language en\n\n# See exactly which options a given command takes\ncontentstudio analytics:pinterest-top-pins --help\n```\n\nAll analytics commands are read-only GETs — none take `--dry-run`. A response with `\"status\": false` and `\"error_code\": \"ANALYTICS_UPSTREAM_ERROR\"` means ContentStudio's own analytics pipeline is temporarily unavailable, not a bad request.\n\n## AI Images\n\nGenerate images from a prompt, or run one of the dedicated image tools, and get back a `media_id` that `posts:create` accepts unchanged. Everything lands in the workspace media library.\n\n### Discover what is available\n\n```bash\ncontentstudio --json images:tools     # invocable tools, their required inputs and controls\ncontentstudio --json images:models    # model identifiers images:generate accepts\ncontentstudio --json images:brand     # {configured, enabled} — will --use-brand do anything?\n```\n\nThese three describe configuration rather than workspace state, so they are worth caching.\n\n### Generate\n\n```bash\n# Preview the request first — generating costs an image credit\ncontentstudio --json images:generate -p \"Flat-lay of autumn coffee beans on linen\" --dry-run\n\n# Generate\ncontentstudio --json images:generate \\\n  -p \"Flat-lay of autumn coffee beans on linen, warm daylight\" \\\n  --dimensions square_hd\n\n# Pick a model, and let the service refine the prompt (its default) or not\ncontentstudio --json images:generate -p \"...\" --model nano-banana-pro --no-enhance-prompt\n\n# Apply the workspace's brand knowledge (resolved server-side; no brand ID exists)\ncontentstudio --json images:generate -p \"...\" --use-brand\n\n# Edit an existing image — the prompt describes the change, not the whole picture\ncontentstudio --json images:generate \\\n  -p \"Make the background a snowy street at dusk\" \\\n  --image-url https://example.com/base.png\n```\n\n`--dimensions` is one of `square`, `square_hd`, `portrait_4_5`, `landscape_16_9`, and applies to text→image only — an edit keeps the source image's geometry. Exact pixels are the model's choice; read `width`/`height` back off the response.\n\n### Generate, then publish\n\n```bash\nMEDIA_ID=$(contentstudio --json images:generate \\\n  -p \"Flat-lay of autumn coffee beans on linen, warm daylight\" \\\n  --dimensions square_hd | jq -r '.data.media_id')\n\ncontentstudio --json posts:create \\\n  -c \"Autumn blend is back.\" -i <account_id> -t draft --media-id \"$MEDIA_ID\"\n```\n\n`-t draft` keeps it reviewable; `-t scheduled -s \"YYYY-MM-DD HH:MM:SS\"` sends it. There is\nno publish-now type.\n\n### The dedicated tools\n\n```bash\ncontentstudio --json images:product-image --product-image-url https://example.com/mug.png \\\n  --instructions \"on a marble kitchen counter, morning light\"\ncontentstudio --json images:headshot --image-url https://example.com/person.jpg --aspect-ratio 4:5\ncontentstudio --json images:face-swap \\\n  --target-image-url https://example.com/scene.png \\\n  --face-image-url https://example.com/face.jpg\ncontentstudio --json images:outfit-swap \\\n  --target-image-url https://example.com/model.jpg \\\n  --outfit-image-url https://example.com/jacket.png\ncontentstudio --json images:upscale --image-url https://example.com/small.png --resolution 2k\ncontentstudio --json images:remove-background --image-url https://example.com/mug.png\n```\n\nAllowed values for `--resolution` and `--aspect-ratio` come from that tool's `controls` in `images:tools` — they differ per tool, so the CLI forwards them rather than second-guessing the list.\n\nThose `controls` describe the **underlying** tool, though, not the public payload: a control with no matching flag cannot be sent, not even through `images:tool --body`. `upscale` advertises `model` and `upscale_factor` and the API accepts neither; `headshot` and `face-swap` report `accepts_instructions: true` but only `images:product-image` has `--instructions`. An unsupported field is dropped without an error, so it looks like it worked — the flags each command exposes are the real field set.\n\nEvery generating command takes `--dry-run`, `--timeout <seconds>` and `--json`.\n\n### Any tool, every control\n\n`images:tool <tool_key> --body '<json>'` posts a raw payload to any tool the API exposes. This is how you reach the controls the dedicated commands don't spell out — `image-to-image`'s `style`, `image_resolution`, `image_quality`, multiple `attachments`, `reference_image_urls` — and it keeps working when a tool is added upstream:\n\n```bash\ncontentstudio --json images:tool image-to-image --body '{\n  \"prompt\": \"same mug, editorial magazine styling\",\n  \"attachments\": [\"https://example.com/mug.png\"],\n  \"aspect_ratio\": \"4:5\"\n}'\n```\n\n### The response\n\n```json\n{\n  \"ok\": true,\n  \"data\": {\n    \"media_id\": \"66f1a2b3c4d5e6f708192a3b\",\n    \"url\": \"https://storage.googleapis.com/contentstudio/.../generated.png\",\n    \"width\": 1024,\n    \"height\": 1024,\n    \"mime_type\": \"image/png\",\n    \"model_used\": \"nano-banana-pro\",\n    \"brand_applied\": false,\n    \"credits\": { \"consumed\": 1, \"available\": 412 },\n    \"persist_error\": null\n  }\n}\n```\n\n- **`media_id` is the durable handle** — pass it to `posts:create --media-id`. `url` is for previews and for chaining one tool into the next; don't store it.\n- **Check `persist_error` before treating a success as done.** The image was generated *and charged* but could not be saved, so `media_id` is `null` and `url` is a temporary provider link. `media_storage_full` means the workspace is out of media storage and retrying will fail the same way; anything else is worth one retry.\n- **`model_used` names the model that actually ran and is not one of the `images:models` values** — it comes back provider-prefixed (`fal-ai/nano-banana-pro` for a generate, `pixelcut/background-removal` for a background removal). Don't compare it for equality with `--model`. Credit cost follows it (most 1, `gpt-image-2` 5), so read `credits.consumed` rather than assuming. `credits.available` is `null` when the balance could not be read — never `0` as a stand-in.\n- **`brand_applied` is always `false` for the tool commands and for `images:generate --image-url`.** Tools and edits do not apply brand knowledge; only text→image `--use-brand` does.\n\n### Input URLs\n\nEvery URL you pass in is downloaded by the image service, so it must be publicly reachable over `http`/`https` — no auth, no expired signature, no private bucket, and no local path. The CLI rejects a non-`http(s)` value before spending a request credit; a URL the service itself cannot fetch comes back as `ValidationError` / `IMAGE_INPUT_REJECTED` and costs no image credits.\n\nTo use a local file, put it in the media library first:\n\n```bash\nURL=$(contentstudio --json media:upload --file ./mug.png | jq -r '.data.url')\ncontentstudio --json images:upscale --image-url \"$URL\"\n```\n\nTools chain the same way — a media-library `url` from one call is valid input to the next (generate → upscale → remove-background). Each call is charged separately.\n\n### Timeouts and retries\n\nGeneration is synchronous and can take a while. The server's own deadline is **120 seconds** (past that it answers `504` / `AI_SERVICE_TIMEOUT`), and the CLI waits **150 seconds** by default so a server-side timeout surfaces as that error rather than an opaque local abort. Override with `--timeout <seconds>`; keep it above 120.\n\nUnlike the rest of the CLI, **the generating commands do not auto-retry** `429`/`5xx`. These POSTs are billable and not idempotent — an automatic retry can consume a second image credit — so retrying is left to you. The three discovery commands retry normally.\n\n### Errors\n\n| `error_code` | CLI error | What to do |\n|---|---|---|\n| `IMAGE_CREDIT_LIMIT_EXCEEDED` | `CreditLimitError` (exit 8) | Out of image credits; top up or wait for the cycle. Nothing was charged. The check is strict — a 5-credit model with 3 left is refused, not downgraded |\n| `CONTENT_BLOCKED` | `ValidationError` | The content policy refused the prompt; rephrase it. Retrying as-is fails again |\n| `IMAGE_INPUT_REJECTED` | `ValidationError` | Usually an image URL the service could not download; also a too-small or too-large source |\n| `TOOL_NOT_FOUND` | `NotFoundError` | Unknown, disabled, or a video tool. Re-read `images:tools` |\n| `RATE_LIMIT_EXCEEDED` | `RateLimitError` | 30 requests/minute, shared with the ContentStudio app's own AI usage on this account. Wait out the minute |\n| `AI_SERVICE_TIMEOUT` | `BackendError` | The service did not finish in 120s. Retry with backoff, or use a faster model |\n| `AI_SERVICE_UNAVAILABLE` | `BackendError` | Retry promptly. On a tool run this can arrive after the credit was taken |\n\nA `403` with no `error_code` is a membership or API-request-credit problem and stays an `AuthError`.\n\nVideo tools (`image-to-video`, `motion-control`, `lip-sync`, `talking-avatar`) are not exposed on this API — they answer `TOOL_NOT_FOUND` like an unknown key. Sample workspaces are read-only: the three discovery commands work, generation returns `403`.\n\n## Platform-Specific Examples\n\nThe full body schema accepts platform-specific options. These examples show the most common configurations.\n\n### Facebook Page\n\n```bash\ncontentstudio --json posts:create \\\n  -c \"Big news for our community 🎉\" \\\n  -i <facebook_page_id> \\\n  -t scheduled \\\n  -s \"2026-05-01 10:00:00\" \\\n  -m https://example.com/announcement.jpg\n```\n\nFor Facebook **Reels** or **Stories**, set `--post-type`:\n```bash\ncontentstudio posts:create \\\n  -c \"Behind-the-scenes\" \\\n  -i <facebook_id> \\\n  -t scheduled \\\n  -s \"2026-05-01 10:00:00\" \\\n  --video-url https://example.com/clip.mp4 \\\n  --post-type reel+story\n```\n\n### LinkedIn (personal or company page)\n\n```bash\ncontentstudio --json posts:create \\\n  -c \"Excited to share our Q2 roadmap\" \\\n  -i <linkedin_id> \\\n  -t scheduled \\\n  -s \"2026-05-01 09:00:00\" \\\n  -m https://example.com/roadmap.png\n```\n\n### Twitter / X\n\n```bash\n# Single tweet with image\ncontentstudio --json posts:create \\\n  -c \"New release shipped 🚀\" \\\n  -i <twitter_id> \\\n  -t scheduled \\\n  -s \"2026-05-01 10:00:00\" \\\n  -m https://example.com/preview.png\n```\n\n### Instagram (feed / reel / story)\n\nFor Instagram, control the post format with `--post-type`:\n\n```bash\n# Feed post\ncontentstudio posts:create \\\n  -c \"Caption with #hashtags\" \\\n  -i <instagram_id> \\\n  -t scheduled \\\n  -s \"2026-05-01 10:00:00\" \\\n  -m https://example.com/photo.jpg \\\n  --post-type feed\n\n# Reel\ncontentstudio posts:create \\\n  -c \"\" \\\n  -i <instagram_id> \\\n  -t scheduled \\\n  -s \"2026-05-01 10:00:00\" \\\n  --video-url https://example.com/reel.mp4 \\\n  --post-type reel\n\n# Trial reel — shown to non-followers first, not on the profile grid or\n# follower feeds. Requires --post-type reel exactly, plus a video.\n# Rejected (422) together with --instagram-collaborator.\ncontentstudio posts:create \\\n  -c \"\" \\\n  -i <instagram_id> \\\n  -t scheduled \\\n  -s \"2026-05-01 10:00:00\" \\\n  --video-url https://example.com/reel.mp4 \\\n  --post-type reel \\\n  --instagram-trial-reel \\\n  --instagram-trial-reel-graduation SS_PERFORMANCE\n\n# Story\ncontentstudio posts:create \\\n  -c \"\" \\\n  -i <instagram_id> \\\n  -t scheduled \\\n  -s \"2026-05-01 10:00:00\" \\\n  -m https://example.com/story.jpg \\\n  --post-type story\n```\n\n### YouTube (Shorts and Videos)\n\nYouTube needs `youtube_options` — use a `--body` file:\n\n```bash\ncat > /tmp/yt-post.json <<'JSON'\n{\n  \"content\": {\n    \"text\": \"Description shown under the video\",\n    \"media\": {\"video\": \"https://example.com/clip.mp4\"}\n  },\n  \"accounts\": [\"<youtube_id>\"],\n  \"post_type\": \"shorts\",\n  \"post_video_title\": \"How we built ContentStudio CLI\",\n  \"scheduling\": {\"publish_type\": \"scheduled\", \"scheduled_at\": \"2026-05-01 10:00:00\"},\n  \"youtube_options\": {\n    \"title\": \"How we built ContentStudio CLI\",\n    \"privacy_status\": \"public\",\n    \"category\": \"EDUCATION\",\n    \"tags\": [\"cli\", \"automation\", \"social-media\"],\n    \"license\": \"youtube\",\n    \"made_for_kids\": false\n  }\n}\nJSON\ncontentstudio --json posts:create --body /tmp/yt-post.json\n```\n\n### TikTok\n\n```bash\ncat > /tmp/tt-post.json <<'JSON'\n{\n  \"content\": {\n    \"text\": \"Quick demo #fyp #tutorial\",\n    \"media\": {\"video\": \"https://example.com/tiktok.mp4\"}\n  },\n  \"accounts\": [\"<tiktok_id>\"],\n  \"scheduling\": {\"publish_type\": \"scheduled\", \"scheduled_at\": \"2026-05-01 10:00:00\"},\n  \"tiktok_options\": {\n    \"privacy_level\": \"PUBLIC_TO_EVERYONE\",\n    \"disable_comment\": false,\n    \"disable_duet\": false,\n    \"disable_stitch\": false,\n    \"auto_add_music\": false,\n    \"brand_content_toggle\": false,\n    \"disclose_commercial_content\": false,\n    \"is_aigc\": false\n  }\n}\nJSON\ncontentstudio --json posts:create --body /tmp/tt-post.json\n```\n\n### Pinterest\n\n```bash\ncat > /tmp/pin-post.json <<'JSON'\n{\n  \"content\": {\n    \"text\": \"Check out our spring guide\",\n    \"media\": {\"images\": [\"https://example.com/pin.jpg\"]}\n  },\n  \"accounts\": [\"<pinterest_id>\"],\n  \"scheduling\": {\"publish_type\": \"scheduled\", \"scheduled_at\": \"2026-05-01 10:00:00\"},\n  \"pinterest_options\": {\n    \"title\": \"Spring 2026 Style Guide\",\n    \"link\": \"https://example.com/spring-guide\"\n  }\n}\nJSON\ncontentstudio --json posts:create --body /tmp/pin-post.json\n```\n\n### Google Business Profile\n\n```bash\ncat > /tmp/gmb-post.json <<'JSON'\n{\n  \"content\": {\n    \"text\": \"Join our grand opening event\",\n    \"media\": {\"images\": [\"https://example.com/event.jpg\"]}\n  },\n  \"accounts\": [\"<gmb_account_id>\"],\n  \"scheduling\": {\"publish_type\": \"scheduled\", \"scheduled_at\": \"2026-05-01 10:00:00\"},\n  \"gmb_options\": {\n    \"topic_type\": \"EVENT\",\n    \"start_date\": \"2026-05-15\",\n    \"end_date\": \"2026-05-16\",\n    \"title\": \"Grand Opening\",\n    \"action_type\": \"BOOK\",\n    \"cta_link\": \"https://example.com/rsvp\"\n  }\n}\nJSON\ncontentstudio --json posts:create --body /tmp/gmb-post.json\n```\n\n## Features for AI Agents\n\nThis CLI is designed to be driven by AI assistants. Three properties make it agent-friendly:\n\n### 1. Stable JSON envelope\n\nEvery command supports `--json` returning a predictable shape:\n\n```jsonc\n// Success\n{ \"ok\": true, \"data\": <payload> }\n\n// Error\n{\n  \"ok\": false,\n  \"error\": {\n    \"type\": \"AuthError\",\n    \"message\": \"Invalid or revoked API key\",\n    \"http_status\": 401,\n    \"hint\": \"Run `contentstudio auth:login --api-key cs_...` to set a valid API key.\"\n  }\n}\n```\n\nAgents check both `ok` and the process exit code (non-zero on error).\n\n### 2. Dry-run by default for safety\n\nEvery mutating command (`posts:create`, `posts:delete`, `posts:approve`, `posts:reject`, `comments:add`, `media:upload`, and every `images:*` command that generates) supports `--dry-run` — the agent can validate a payload before committing.\n\n### 3. Discoverable via `npx skills add`\n\nThe repo ships a `SKILL.md` agents can install with one command:\n```bash\nnpx skills add contentstudioio/contentstudio-agent\n```\nAfter this, the agent automatically knows when to use the `contentstudio` CLI without prompting.\n\n## Common Workflows\n\n### 1. Schedule a daily post for the next 7 days\n\n```bash\n#!/bin/bash\n# Daily content batch for a Facebook page\nACCOUNT=\"<facebook_page_id>\"\nCONTENT=(\n  \"Monday motivation 💪\"\n  \"Tuesday tips: keep it simple\"\n  \"Wednesday wisdom from the team\"\n  \"Throwback Thursday\"\n  \"Friday vibes 🎉\"\n  \"Weekend prep — try this\"\n  \"Sunday reflections\"\n)\n\nfor i in \"${!CONTENT[@]}\"; do\n  DATE=$(date -d \"+$((i+1)) day 09:00\" '+%F %T')\n  contentstudio --json posts:create \\\n    -c \"${CONTENT[$i]}\" \\\n    -i \"$ACCOUNT\" \\\n    -t scheduled \\\n    -s \"$DATE\"\ndone\n```\n\n### 2. Cross-platform campaign\n\n```bash\n#!/bin/bash\n# Same content to FB + LinkedIn + Twitter at the same time\nTIME=\"2026-05-01 10:00:00\"\n\n# List accounts and pick one per platform\nFB=$(contentstudio --json accounts:list --platform facebook | jq -r '.data[0].id')\nLI=$(contentstudio --json accounts:list --platform linkedin | jq -r '.data[0].id')\nTW=$(contentstudio --json accounts:list --platform twitter  | jq -r '.data[0].id')\n\ncontentstudio --json posts:create \\\n  -c \"Big launch today 🚀\" \\\n  -i \"$FB\" -i \"$LI\" -i \"$TW\" \\\n  -t scheduled \\\n  -s \"$TIME\" \\\n  -m https://example.com/launch.jpg\n```\n\n### 3. Bulk-delete drafts older than 30 days\n\n```bash\n#!/bin/bash\nCUTOFF=$(date -d '-30 days' '+%Y-%m-%d')\n\ncontentstudio --json posts:list --status draft --date-to \"$CUTOFF\" --per-page 100 \\\n  | jq -r '.data[].id' \\\n  | while read id; do\n      contentstudio --json posts:delete \"$id\"\n    done\n```\n\n### 4. Upload a folder of images and create one post per image\n\n```bash\n#!/bin/bash\nACCOUNT=\"<instagram_id>\"\n\nfor img in ./photos/*.jpg; do\n  # Upload first to get a media library ID\n  RESP=$(contentstudio --json media:upload --file \"$img\")\n  MEDIA_ID=$(echo \"$RESP\" | jq -r '.data.id')\n\n  # Schedule a post with the uploaded media\n  TIME=$(date -d \"+1 hour\" '+%F %T')\n  contentstudio --json posts:create \\\n    -c \"$(basename \"$img\" .jpg)\" \\\n    -i \"$ACCOUNT\" \\\n    -t scheduled \\\n    -s \"$TIME\" \\\n    --media-id \"$MEDIA_ID\" \\\n    --post-type feed\ndone\n```\n\n### 5. Approval pipeline — auto-approve posts from a trusted creator\n\n```bash\n#!/bin/bash\nTRUSTED_USER_ID=\"<user_id>\"\n\ncontentstudio --json posts:list --status pending_approval --per-page 50 \\\n  | jq -r --arg u \"$TRUSTED_USER_ID\" '.data[] | select(.created_by == $u) | .id' \\\n  | while read id; do\n      contentstudio --json posts:approve \"$id\" --comment \"auto-approved (trusted creator)\"\n    done\n```\n\n## API Endpoints\n\nThe CLI wraps these endpoints from the ContentStudio v1 public API (plus the workspace/label/campaign/team writes and the `inbox:*` surface documented above). Base URL: `https://api.contentstudio.io/api/v1`.\n\n| Method | Endpoint | CLI command |\n|--------|----------|-------------|\n| GET    | `/me` | `auth:whoami` |\n| GET    | `/platforms` | `platforms:list` |\n| GET    | `/facebook/text-backgrounds` | `facebook:text-backgrounds` |\n| GET    | `/workspaces` | `workspaces:list` |\n| GET    | `/workspaces/{w}/accounts` | `accounts:list` |\n| POST   | `/workspaces/{w}/connect/{platform}` | `accounts:connect <platform>` |\n| POST   | `/workspaces/{w}/add/bluesky` | `accounts:add-bluesky` |\n| POST   | `/workspaces/{w}/add/facebook-group` | `accounts:add-facebook-group` |\n| DELETE | `/workspaces/{w}/accounts/{account_id}` | `accounts:remove <account_id>` |\n| GET    | `/workspaces/{w}/campaigns` | `campaigns:list` \n\nFile v1.5.0:_meta.json\n\n{\n  \"ownerId\": \"kn7cvyshqjwfmhgyxnapwkqw4588habf\",\n  \"slug\": \"contentstudio\",\n  \"version\": \"1.5.0\",\n  \"publishedAt\": 1789366557192\n}\n\nFile v1.5.0:CHANGELOG.md\n\n# Changelog\n\n## 1.5.0 — AI image generation\n\nThe public API gained an AI image surface, so the CLI and the agent skill cover\nit: five endpoints under `/workspaces/{w}/ai/...`, wrapped as an `images:*`\ncommand group. Generation is synchronous and every success returns a `media_id`\nthat `posts:create --media-id` takes unchanged — prompt to scheduled post in two\ncommands.\n\n### Discovery\n\n- `images:tools` — the image tools this API can invoke, with each one's required\n  inputs and its control options. An empty list means the catalogue is\n  temporarily unreachable, not that the workspace has no tools.\n- `images:models` — the model identifiers `images:generate` accepts. A curated\n  list, narrower than the models the web app offers.\n- `images:brand` — `{configured, enabled}`: whether `--use-brand` will apply\n  anything. Status only; brand content is never returned, and there is no brand\n  CRUD on this API.\n\n### Generation\n\n- `images:generate -p \"<prompt>\"` — prompt → image, saved to the media library.\n  Flags: `--model`, `--dimensions` (`square`, `square_hd`, `portrait_4_5`,\n  `landscape_16_9`), `--use-brand`, `--enhance-prompt` / `--no-enhance-prompt`.\n- `images:generate -p \"<edit>\" --image-url <url>` — edit an existing image\n  instead. The prompt describes the change. `--dimensions` does not apply on this\n  path (the source geometry wins) and `brand_applied` is always `false`.\n- One command per dedicated tool: `images:product-image`, `images:headshot`,\n  `images:face-swap`, `images:outfit-swap`, `images:upscale`,\n  `images:remove-background`. Flag names mirror the API's field names\n  (`--target-image-url`, `--face-image-url`, `--product-image-url`, …).\n- `images:tool <tool_key> --body '<json>'` — any tool the API exposes, with its\n  full control set. This is how `image-to-image`'s `style`, `image_resolution`,\n  `image_quality`, multiple `attachments` and `reference_image_urls` are reached,\n  and it keeps working when a tool is added upstream without a CLI release.\n\nAll of them take `--dry-run`, `--json` and `--timeout <seconds>`.\n\n### Notes\n\n- **`media_id` is the durable handle; `url` is not.** `url` is for previews and\n  for chaining one tool into the next. A `url` returned alongside a\n  `persist_error` is a temporary provider link — the image was generated *and\n  charged* but not saved, so `media_id` is `null`. Human mode warns about this\n  explicitly; `media_storage_full` says retrying will fail the same way.\n- **The generating calls do not auto-retry.** Everywhere else the client retries\n  `429`/`5xx` twice; here it does not, because these POSTs are billable and not\n  idempotent and a retry can consume a second image credit. The three discovery\n  commands retry normally.\n- **Timeout is 150s, above the server's own 120s deadline**, so a slow model\n  surfaces as the API's `AI_SERVICE_TIMEOUT` (which names the cause and costs no\n  image credits) rather than an opaque local abort. `--timeout <seconds>`\n  overrides it.\n- **Error codes are mapped to typed errors with actionable hints**, because the\n  bare HTTP mapping was actively misleading: exhausted image credits are a `403`,\n  which would otherwise read as an `AuthError` and send an agent into an\n  `auth:login` loop that can never help. New `CreditLimitError` (exit code 8) for\n  `IMAGE_CREDIT_LIMIT_EXCEEDED`; `CONTENT_BLOCKED` and `IMAGE_INPUT_REJECTED`\n  render as `ValidationError` with distinct copy (rephrase the prompt vs. fix the\n  image URL); `TOOL_NOT_FOUND` points at `images:tools`; `RATE_LIMIT_EXCEEDED`\n  says the bucket is 30/min and shared with the app's own AI usage. A `403` with\n  no `error_code` is a membership / API-request-credit problem and stays an\n  `AuthError`.\n- **Input URLs are validated client-side** for the `http(s)` scheme and the 2048\n  character cap, as are the 1000-character `--prompt` / `--instructions` limits\n  and the `--dimensions` presets — a rejected call still costs an API request\n  credit, and a local path is a typo rather than a decision.\n- Every URL passed in is downloaded by the image service, so it must be publicly\n  reachable. The CLI's error text points at `media:upload --file` for local files.\n- Video tools (`image-to-video`, `motion-control`, `lip-sync`,\n  `talking-avatar`) are not exposed on this API and answer `TOOL_NOT_FOUND`.\n- `buildClient` now takes optional `ClientOpts`, which is how the image group\n  raises the timeout and switches retries off. No change for existing callers.\n- README gained an **AI Images** section; SKILL.md gained an **AI images**\n  command reference and a generate-then-publish recipe.\n- `images:tools` shows the command and flags to run each tool with, plus each\n  control's allowed options. It deliberately does **not** print the\n  descriptor's `inputs[].name`: those are the underlying tool's slot names\n  (`image`, `target_image`, `product`), neither wire fields nor flags, so\n  printing them pointed readers at flags that do not exist. The footer says\n  outright that a control with no matching flag cannot be sent — `upscale`\n  advertises `model` and `upscale_factor`, and the API's tool payload accepts\n  neither, the same way `accepts_instructions` on `headshot` / `face-swap` is\n  unreachable when only `product-image` declares `instructions`.\n\n## 1.4.1 — Clearer reporting help, and a guard on competitor ids\n\nNo behaviour change to any working command; this is about the two mistakes the\ncommand names invite.\n\n- **A \"competitor report\" is a saved set, not a document.** `competitor-reports:create`\n  now says so in `--help`, and points at the command that does produce a PDF.\n  `competitor-reports:get` says the same, and that `--wait` belongs to `reports:get`.\n- **`reports:generate --help`** names the command that yields the URL\n  (`reports:get <id> --wait`) instead of only saying \"poll\", and states that the\n  competitor types take `--competitor-report-id` rather than `--accounts`.\n- **`--competitors` now rejects a competitor-set id.** A set id is a 24-character\n  hex ObjectId; a real page id is numeric. Passing the former created a competitor\n  the network had never heard of, which surfaced minutes later as state `Failed`\n  with nothing explaining why. It is refused at entry, naming the fix.\n- **`share-links:create --help`** distinguishes a live shared dashboard from a\n  generated PDF.\n\n## 1.4.0 — Bluesky and Threads analytics, competitor reports are generatable\n\n### Bluesky and Threads analytics (26 commands)\n\nThe `analytics:` namespace covered eight networks and stopped there; both newer\nplatforms had none, despite the API serving them. Added one command per endpoint,\nsame shape as every other platform:\n\n- **`analytics:bluesky-*` (10)** — `summary`, `audience-growth`, `engagement`,\n  `publishing-behaviour`, `top-posts`, `sorted-top-posts`, `post`,\n  `posts-per-days`, `hashtags`, `capabilities`.\n- **`analytics:threads-*` (16)** — the same, plus `activity` (the true per-day\n  account series, distinct from engagement-by-publish-date), `posts-per-hours`,\n  `topic-tags` (Meta's curated tags, counted separately from hashtags),\n  `demographics`, `audience-location` and `ai-insights`.\n\nNeither network publishes impressions or reach, so neither has the exposure\nendpoints the older platforms do. That is the whole surface, not a subset.\n\n### Competitor reports are generatable\n\n`reports:generate` gains **`--competitor-report-id`**, required for the\n`facebook_competitor` and `instagram_competitor` types. Those are built from a\nsaved competitor set (see `competitor-reports:list`) rather than from connected\naccounts, and 1.3.0 shipped them as generatable types with no way to name the\nset — the id had to go somewhere, `--accounts` was the natural guess, and it was\ndropped silently: the call returned 202 and the report failed minutes later with\n\"Combined report generation failed\". The CLI now refuses that up front and names\nthe flag.\n\nRequires the matching API change (`competitor_report_id` on the report-generate\nrequest). Against an older API the field is ignored and competitor reports still\nfail, so upgrade the CLI only once that has shipped.\n\n## 1.3.0 — Analytics: reports, schedules, share links, competitors, ads\n\n### Reports, schedules and share links\n\nThree new command groups, all under the analytics umbrella:\n\n- **`reports:*` (6)** — `options`, `generate`, `get`, `list`, `retry`, `delete`.\n  Generation is asynchronous: `reports:generate` returns an id immediately and\n  `reports:get <id> --wait` polls until the download URL is ready.\n- **`report-schedules:*` (7)** — `create`, `list`, `get`, `pause`, `resume`,\n  `run`, `delete`. Recurring email delivery; `pause` is reversible and `run`\n  sends one immediately without disturbing the schedule.\n- **`share-links:*` (6)** — `create`, `list`, `get`, `enable`, `disable`,\n  `delete`. Client-facing links that need no ContentStudio account, optionally\n  password-protected and pinned to a fixed date range. `disable` revokes a link\n  without deleting it, so the URL can be restored rather than reissued.\n\n### Competitor benchmarking\n\n- **`competitors:search`** to find a page to track, **`competitor-reports:*`**\n  (`create`, `list`, `get`, `update`, `delete`) to manage a saved set, and\n  **`competitors:compare`** for the comparison numbers.\n  `competitor-reports:update` replaces the whole competitor set rather than\n  merging into it.\n\n### Ads analytics support\n\n34 more commands under the `analytics:` namespace, tracking the public API's ads\nsurface (added after the analytics work below):\n\n- **Meta Ads (11)** and **Google Ads (17)** — summary, performance over time /\n  by level / by placement / by type, campaigns, ad sets, ad groups, ads,\n  keywords, search terms, shopping, the four Google conversion reports,\n  demographics, ad-account listing, and `ai-insights` on both. These take\n  `--account-id` (an ad account) rather than `--platform-id`.\n- **Campaigns & Labels (5)** — summary, breakdown, insights-breakdown, posts and\n  top-posts. The only POST analytics commands: their filters are lists, so\n  `--campaigns`, `--labels` and the per-network `--*-accounts` flags repeat.\n- **YouTube publishing-behaviour** — the one social endpoint added since.\n\n`analytics:*-accounts` is how you find an ad account id; every other ads command\nneeds one. Still read-only, still one command per endpoint.\n\n### Analytics support\n\n99 new commands under the `analytics:` namespace, one per ContentStudio\npublic API v1 analytics endpoint, across Facebook, Instagram, YouTube,\nPinterest, LinkedIn, Google Business Profile, TikTok, and Twitter/X.\n\n- Date-range reports (`--platform-id`, `--start-date`, `--end-date`, plus\n  per-command optional filters like `--order-by`, `--media-type`,\n  `--hashtags`, `--limit`/`--offset`).\n- Single-item lookups (`*-single-post`, `*-single-pin`, `*-single-tweet`,\n  `*-single-video`) taking `--platform-id` + `--post-id`.\n- AI insights commands (`*-ai-insights`) taking `--type` and `--language`.\n- All read-only GETs — no `--dry-run` (mutating commands only).\n- SKILL.md documents the full command reference and the\n  `ANALYTICS_UPSTREAM_ERROR` response shape.\n\n### Instagram trial reels, per-platform overrides, `id` field rename, `platform_overrides` rename\n\n- `posts:create` / `posts:update`: added `--instagram-trial-reel` (boolean) and\n  `--instagram-trial-reel-graduation SS_PERFORMANCE|MANUAL` →\n  `instagram_options.trial_reel.{enabled,graduation_strategy}`. Publishes an\n  Instagram trial reel (shown to non-followers first). Requires\n  `--post-type reel` exactly plus a video; rejected (422) together with\n  `--instagram-collaborator` — the CLI now guards this locally as well.\n- `posts:create` / `posts:update`: added `--platform-overrides '<json>'` →\n  top-level `platform_overrides`, a per-platform content-override object\n  (`text`/`post_type` merge independently with the common content; `media`\n  is all-or-nothing per platform). Field was renamed from `overrides` to\n  `platform_overrides` to match a pre-release backend contract fix — no\n  compatibility shim needed since neither side has shipped yet.\n- **Breaking (mirrors a backend Public API v1 change):** all Public API v1\n  responses now return the primary identifier as `id` instead of `_id`\n  (accounts, media, workspaces, team members, campaigns, approval workflows,\n  labels, comments, content categories, posts and their nested\n  `labels[]`/`folder`/`accounts[]`). `member_id` on team members is unaffected\n  — it remains a distinct field. CLI output formatting and docs now read\n  `id` first, falling back to `_id` for compatibility with any cached/older\n  responses.\n\n## 1.2.0 — Best time to post\n\n### `scheduling:best-times`\n\nThe scheduling optimiser is now part of the ContentStudio public API, so the CLI\nand the agent skill cover it. One new command wrapping\n`POST /workspaces/{w}/scheduling/optimal-times`.\n\n`scheduling:best-times` analyses the historical performance of the workspace's\nconnected accounts and returns ranked posting **slots** — a weekday and an hour,\nbest-first — both pooled across accounts (`global`) and per account\n(`individual`).\n\nFlags:\n\n- `--account <platform>:<account_id>` (repeatable) — restrict the analysis.\n  Both halves come from a single `accounts:list` row (its `platform` and `_id`),\n  because the API needs the platform as the entity `type`. Omit the flag to\n  analyse every connected account.\n- `--entities '<json>'` — the full entity array, for a different slot count per\n  account: `[{\"id\":\"<id>\",\"type\":\"facebook\",\"slots\":3}]`. Mutually exclusive\n  with `--account`.\n- `--global-slots <n>` / `--per-account-slots <n>` — how many recommendations\n  come back (1–24; API defaults 5 and 3).\n\nNotes:\n\n- **Times are always in the workspace timezone**, echoed as `meta.timezone`;\n  the endpoint takes no timezone parameter. That is the same clock\n  `posts:create --scheduled-at` writes against, so a slot can be scheduled\n  as-is — converting it to UTC first would move the post.\n- The response is not the usual `{status, message, data}` envelope, so the API\n  wrapper normalises `{meta, global, individual}` into the CLI's standard\n  `{ok, data}` shape like every other command.\n- A workspace with too little history still returns HTTP 200: the accounts that\n  could not be analysed come back in `meta.missing_entities` and `global` may be\n  `null`. That is a successful read, not an error. Accounts in\n  `meta.ai_fallback_entities` are estimates rather than measurements, and the\n  human-mode output labels them as such.\n- Slot counts and the `<platform>:<account_id>` form are validated client-side,\n  so a bad call fails immediately with a `ConfigError` rather than a round-trip.\n- Read-only, so there is no `--dry-run` — matching `inbox:list`, the CLI's other\n  POST-with-a-body read.\n- Human mode renders the pooled and per-account recommendations as tables\n  (rank, day, date, time, score, platform breakdown), with the hour formatted as\n  a clock time (the API returns it as a bare string, e.g. `\"14\"`).\n\n### Corrected `--scheduled-at` timezone documentation\n\n`posts:create` / `posts:update` `-s / --scheduled-at` was documented as UTC. The\nAPI actually interprets the timestamp as **workspace-local wall-clock time**, so\nthe help text, SKILL.md and the README now say so. No behaviour change — the CLI\nsends the same string it always did; only the documentation was wrong, and it\nwould have caused posts scheduled from `scheduling:best-times` slots to land at\nthe wrong hour.\n\n## 1.1.1 — documentation wording\n\n- Reworded the Social Inbox sections of SKILL.md and the README, the inbox\n  `--help` text, and the `ConflictError` hint for clarity and consistency.\n- No CLI behaviour changes.\n\n## 1.1.0 — Social Inbox support, posts:update, and approval workflows\n\n### Social Inbox\n\nThe inbox endpoints are now part of the ContentStudio public API, so the CLI and\nthe agent skill cover them. 30 new commands under the `inbox:` namespace.\n\n**Reading** — `inbox:list` (search across conversations, commented posts, and\nreviews), `inbox:summary` (counts per bucket), `inbox:messages`, `inbox:comments`,\n`inbox:notes`, `inbox:bookmarks`, `inbox:contact`, `inbox:tags`.\n\n**Replying** — `inbox:send` (DM, with optional attachment), `inbox:comment-add`\n(comment, threaded reply, or Facebook private reply), `inbox:review-reply`,\n`inbox:note-add`. All accept `--idempotency-key` where the API supports it.\n\n**Triage** — `inbox:mark-read`, `inbox:update` (bulk done/pending, archive,\nassign), `inbox:star` / `inbox:unstar`, `inbox:contact-update`.\n\n**Moderation** — `inbox:comment-hide` / `-unhide`, `inbox:comment-like` /\n`-unlike`, `inbox:comment-delete`, `inbox:message-delete`,\n`inbox:review-reply-delete`.\n\n**Tags** — `inbox:tag-create`, `-update`, `-delete` (bulk), `-merge`, `-attach`,\n`-detach`.\n\n**Identifiers.** Inbox commands take their id from `element_details` on each\n`inbox:list` row: `element_details.element_id` for element-scoped commands,\nconversations, notes and bookmarks, and `element_details.post_id` for post\ncomments. SKILL.md and the README both lead with this.\n\nNotes:\n\n- Inbox responses are normalised into the CLI's standard envelope, so `--json`\n  output keeps the same `{ok, data, pagination}` shape as every other command.\n  Each endpoint's collection (`elements`, `messages`, `comments`, `tags`,\n  `contact`, `element_counts`) and its paginator fields are mapped onto that\n  envelope by the inbox wrappers.\n- Human-mode tables map to the inbox response fields: `platform` on a list row,\n  `last_message.message` for a conversation and `last_comment.message` for a\n  post, and the sender from `from[0]` (falling back to `first_name` /\n  `last_name` when `name` is empty). `inbox:list` also shows an UNREAD column.\n- `inbox:notes` and `inbox:bookmarks` accept `--page` / `--limit` and return a\n  `pagination` block.\n- Inbox tag colors are hex values, e.g. `#33aa55`.\n- Post-comment paging counts top-level threads (`total_threads`) rather than\n  every comment, so page counts reflect threads.\n- API limits are validated client-side so an invalid call fails immediately\n  with a `ConfigError` rather than a round-trip: `--limit` ≤ 200 on every inbox\n  list, ≤ 100 elements per `inbox:update`, tag names ≤ 50 chars, and exactly\n  one operation per `inbox:update` (`--status`, `--archived` and `--assigned`\n  are mutually exclusive).\n- `inbox:update` understands HTTP `207` partial success: when the response\n  carries `missing_ids`, the CLI warns and names the elements it could not\n  update rather than reporting the whole batch as applied.\n- New `ConflictError` (HTTP 409, exit code 7), used for duplicate resources and\n  for sends whose delivery outcome is undetermined. It carries a hint to verify\n  before retrying, and SKILL.md tells the agent to read the conversation back\n  rather than resending automatically.\n- `inbox:contact-update` documents its scope: a contact is a person, not a\n  per-element attribute, so the update applies to every element for that\n  contact on that account. The command reports `updated_count`.\n- `inbox:send` and `inbox:comment-add` report the outcome from the response:\n  `sent_comment.resource_type` distinguishes a comment from a private reply, and\n  `sent_message.id_status: \"unavailable\"` is surfaced as a note that delivery\n  could not be confirmed by id.\n- SKILL.md documents that `inbox:comments` nests replies under each thread's\n  `children` and pages by threads, and that `inbox:contact` returns end-customer\n  contact details (email, phone) that should be handled with care.\n- Note for agents: inbox pages are capped at 200 items, so SKILL.md directs\n  paging through `--page` rather than raising `--limit` past that.\n- Every inbox write supports `--dry-run`, and the preview now percent-encodes\n  path segments so it shows the URL that would actually be requested — element\n  refs can contain `:` and `/`.\n- Inbox lists page with `--limit` / `--page` (the API's own parameter names);\n  `--per-page` is accepted as an alias so the existing pagination guidance holds.\n- SKILL.md flags inbox writes as customer-facing: they publish to a real person\n  with no undo, so the agent must preview and get approval before sending.\n- Internal: `Client` gained a `patch()` method (three inbox endpoints use PATCH;\n  the client had no PATCH support at all). Inbox responses are unwrapped by\n  dedicated helpers rather than the generic `data` unwrapper. `emitDryRun` /\n  `parseJsonOption` moved from `commands/crud.ts` into `cliCtx.ts` so both\n  modules share one copy.\n- Removed SKILL.md's trailing `## Version` section; the frontmatter `version:`\n  field is the single source of truth.\n\n### posts:update, approval workflows, LinkedIn polls & collaborators\n\n- New `accounts:remove <account_id>` command — `DELETE /workspaces/{w}/accounts/{account_id}` disconnects a social account (`account_id` is the account's `_id` from `accounts:list`). Requires the `save_social` permission (403 otherwise); 404 when the account isn't found, 422 when removal fails. Carries `--dry-run` like the other mutating commands.\n- New `posts:update <post_id>` command — PUTs `/workspaces/{w}/posts/{post_id}` with the **same body/flags** as `posts:create` (shared option set + body builder). The backend rejects the update (422) once the post is `published` or `processing`.\n- New `approval-workflows:list` command (GET `/workspaces/{w}/approval-workflows`) — lists `{ _id, name, is_default, levels[] }`; use `_id` as `--approval-workflow-id`.\n- `posts:create` / `posts:update` new shortcut flags:\n  - `--linkedin-options '<json>'` → `linkedin_options` (title + poll; poll needs `--post-type poll` and text-only content).\n  - `--facebook-collaborator` (repeatable, max 10) → `facebook_options.collaborators`; `--instagram-collaborator` (repeatable, max 3) → `instagram_options.collaborators`.\n  - `--approval-workflow-id` + `--approval-workflow-notes` → attach a workflow (`approval_workflow.workflow_id`); `--approval-workflow-action restart|resume|renotify_current|keep|remove` → mutate an attached workflow (update only). Mutually exclusive with `--approver`, and exactly one of id/action.\n  - `--post-type` now documents `poll` (carousel is auto-derived by the backend from `post_type=carousel` + 2+ images).\n- `posts:list` now surfaces `linkedin_options` and `approval_workflow` per post in the `--json` output.\n\n## 1.0.10 — propagate platform-name metadata to the npm package\n\n- Release-only bump. The `package.json`/`plugin.json` description + keywords were updated to include Threads, Tumblr, and Bluesky *after* `1.0.9` had already been published to npm, so npm's `1.0.9` carried the old description and the follow-up deploy failed (`403`, can't republish an existing version). This bump ships the corrected package metadata to npm.\n- No CLI source or SKILL.md content changes beyond the version bump.\n\n## 1.0.9 — add Threads, Tumblr, and Bluesky to the skill description\n\n- SKILL.md: the `description` now lists Threads, Tumblr, and Bluesky alongside the existing platforms. The CLI already supports connecting these (`accounts:connect threads`, `accounts:connect tumblr`, `accounts:add-bluesky`), but the one-line summary had drifted and only advertised the original headline set.\n- No CLI source changes — platform support is unchanged; this only corrects the skill's discoverability/summary text.\n\n## 1.0.8 — document env-var authentication for headless/agent runtimes\n\n- SKILL.md: the Authentication section now documents **two** auth paths — `auth:login --api-key` (interactive, config file) and `export CONTENTSTUDIO_API_KEY` (headless / agent runtimes, env var). The env var takes precedence over the config file.\n- Added a headless-deployment note: a shell `export` does not persist to a service process; set `CONTENTSTUDIO_API_KEY` via systemd `Environment=`/`EnvironmentFile=`, Docker `-e`, etc., then restart. Runtimes that gate on `requires.env` (e.g. OpenClaw) stay blocked until the variable is present in the process environment.\n- Resolves a docs/metadata mismatch: the frontmatter already declared `requires.env: CONTENTSTUDIO_API_KEY`, but the body only documented `auth:login`, so OpenClaw operators were left blocked with no instruction on how to satisfy the gate.\n- No CLI source-code changes — the CLI already reads `CONTENTSTUDIO_API_KEY` from the environment (`src/config.ts`).\n\n### write commands for workspaces/labels/campaigns/team + posts:create fixes\n\n- Fixed `posts:create`: now emits top-level `content_category_id` and no longer forces `--account` when `--content-category-id` is supplied (content-category posts derive accounts from the category — previously 422'd). Added `--content-category-id`.\n- `posts:create` now normalizes `--scheduled-at` to the backend's `YYYY-MM-DD HH:MM:SS` (UTC) format, and gained parity flags `--label` (repeatable, max 20), `--campaign-id`, `--approver` (repeatable) + `--approve-option` + `--approval-notes`, and `--facebook-background-id`. `--publish-type` now also accepts `now`.\n- New workspace write commands: `workspaces:create`, `workspaces:update`, `workspaces:delete`.\n- New label write commands: `labels:create`, `labels:update`, `labels:delete`.\n- New campaign write commands: `campaigns:create`, `campaigns:update`, `campaigns:delete`.\n- New team-member write commands: `team:add`, `team:update`, `team:remove` (with `--confirmed` for guarded removals).\n- All new mutating commands support `--dry-run` and are added to the SKILL.md workspace-confirmation list. Added a `put` method to the API client and nock tests for every new wrapper.\n\n## 1.0.5 — workspace confirmation before mutations\n\n- SKILL.md: agents must now confirm the target workspace with the user before any mutating command (`accounts:connect`, `accounts:add-bluesky`, `accounts:add-facebook-group`, `posts:create`, `posts:delete`, `posts:approve`, `posts:reject`, `comments:add`, `media:upload`) instead of silently using whatever workspace is active in the CLI.\n- Read-only listings (`*:list`, `workspaces:current`, etc.) continue to use the active workspace silently — the rule only applies to mutations.\n- Documents the recommended pattern: run `workspaces:current`, surface the active workspace to the user, ask whether to proceed there or pick another, then either `workspaces:use <id>` or pass `--workspace <id>` for a one-off override.\n- No CLI source-code changes — the CLI's default-to-active-workspace behavior is unchanged.\n\n## 1.0.4 — update-check banner + version inlining\n\n- New: when a newer `contentstudio-cli` is published to npm, the CLI now prints a single-line \"update available\" banner to stderr on startup, with install + skill-refresh hints.\n- Banner is **suppressed** when:\n  - `--json`, `--version`, or `--help` is in argv (avoids corrupting machine-readable / metadata output)\n  - stderr isn't a TTY (avoids polluting log files / pipes)\n  - `CONTENTSTUDIO_NO_UPDATE_CHECK=1` is set\n- Update check is **fire-and-forget** (never blocks the command), result cached at `~/.config/contentstudio/.update-check.json` for 24h.\n- Fixed: `VERSION` constant now reads from `package.json` at build time via tsup `define` — older builds had `User-Agent` header reporting `1.0.0` regardless of actual version.\n- 22 new unit tests for the update checker (76/76 passing).\n\n## 1.0.3 — account connection commands\n\nAdded 5 new commands for managing social-account connections:\n\n- **`platforms:list`** — list all 12+ platforms available for connection, with their `connection_method` (`oauth` / `credentials` / `manual`).\n- **`accounts:connect <platform>`** — generate a one-time OAuth URL for connecting a new account; `--reconnect --account-id <id>` to refresh expired accounts.\n- **`accounts:add-bluesky --handle <h> --app-password <p>`** — credential-based Bluesky add (no browser). Password is redacted in `--dry-run` output.\n- **`accounts:add-facebook-group --name <n> [--image <url>]`** — manual Facebook Group connection.\n- **`facebook:text-backgrounds`** — list Facebook colored-background presets used in `facebook_options.facebook_background_id` on plain-text posts.\n\nAll new commands support `--json` (mutations also support `--dry-run`).\n\n## 1.0.2 — pagination metadata for AI agents\n\n- All `*:list` commands now surface Laravel pagination metadata (`current_page`, `per_page`, `total`, `last_page`, `from`, `to`, `has_more`) in the JSON envelope as a sibling of `data`.\n- Human-mode list commands now print a \"Showing X–Y of TOTAL (page N/M)\" footer with a hint to fetch more pages.\n- SKILL.md updated with mandatory pagination rules for AI agents — when `pagination.has_more` is true, the agent must either ask the user, auto-paginate, or filter; never silently truncate.\n- 4 new pagination unit tests; total now 47 unit + 9 E2E.\n\n## 1.0.1 — expanded README\n\n- Full README rewrite with platform-specific examples (FB / LinkedIn / Twitter / Instagram / YouTube / TikTok / Pinterest / GMB), common workflow scripts, API endpoints table, error handling table, quick reference, and development guide.\n- No code changes — docs only.\n\n## 1.0.0 — initial release\n\n- All 15 endpoints of the ContentStudio v1 public API exposed as `<group>:<verb>` commands.\n- Human + JSON output modes (`--json`).\n- `--dry-run` on every mutating command (posts:create, posts:delete, posts:approve/reject, comments:add, media:upload).\n- Persistent config at `~/.config/contentstudio/config.json` (0600 perms).\n- Env-var overrides: `CONTENTSTUDIO_API_KEY`, `CONTENTSTUDIO_BASE_URL`, `CONTENTSTUDIO_WORKSPACE_ID`, `CONTENTSTUDIO_CONFIG_PATH`.\n- Typed errors: `AuthError`, `NotFoundError`, `ValidationError`, `RateLimitError`, `BackendError`, `ConfigError`.\n- Retry on `429` and `5xx` with exponential backoff.\n- 52 tests passing — unit (errors, config, API, CLI) + real-API E2E.\n- SKILL.md + `.claude-plugin/` manifests for `npx skills add` and Claude Code marketplace.\n\nFile v1.5.0:skill-card.md\n\n## Description:\n\nContentStudio helps agents schedule social-media posts, manage social inboxes and media, generate or edit AI images, and retrieve analytics across supported social platforms through the ContentStudio public API.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[contentstudio-official](https://clawhub.ai/user/contentstudio-official)\n\n### License/Terms of Use:\n\nMIT\n\n## Use Case:\n\nDevelopers, operators, and external teams use this skill to automate ContentStudio workspace tasks such as scheduling posts, managing social inbox interactions and media, approving or deleting content, generating AI images, and pulling analytics reports.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The skill installs an unpinned external CLI that receives sensitive ContentStudio API credentials.\n\nMitigation: Review or pin the exact contentstudio-cli version and skill source before installation, and use a dedicated API key with the least permissions needed.\n\nRisk: Mutating commands can publish, delete, approve, or modify social content, team settings, and reporting assets.\n\nMitigation: Confirm the active workspace before writes and keep dry-run previews with explicit approval for posting, deletion, inbox replies, and report or share-link changes.\n\nRisk: Unattended bulk-delete or auto-approval automation can cause high-impact account changes.\n\nMitigation: Avoid unattended bulk writes unless separate review and rollback controls are in place.\n\n## Reference(s):\n\n- [ContentStudio API Guide](https://api.contentstudio.io/guide)\n- [ContentStudio API Docs](https://api.contentstudio.io/api-docs)\n- [contentstudio-cli npm package](https://www.npmjs.com/package/contentstudio-cli)\n- [ContentStudio website](https://contentstudio.io)\n- [contentstudio-agent GitHub repository](https://github.com/contentstudioio/contentstudio-agent)\n\n## Skill Output:\n\n**Output Type(s):** [text, markdown, shell commands, configuration, guidance]\n\n**Output Format:** [Markdown guidance with shell commands and JSON command-output expectations]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Uses the contentstudio CLI with --json for structured responses and --dry-run previews for mutating actions.]\n\n## Skill Version(s):\n\n1.5.0 (source: frontmatter and changelog)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nFile v1.5.0:LICENSE\n\nMIT License\n\nCopyright (c) 2026 ContentStudio\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\nArchive v1.4.1: 6 files, 54932 bytes\n\nFiles: CHANGELOG.md (24718b), LICENSE (1070b), README.md (55521b), skill-card.md (2801b), SKILL.md (77702b), _meta.json (132b)\n\nFile v1.4.1:SKILL.md\n\n---\nname: contentstudio\ndescription: ContentStudio is a tool to schedule social-media posts, manage the social inbox, and pull performance analytics across Facebook, LinkedIn, Twitter/X, Instagram, YouTube, TikTok, Pinterest, Threads, Tumblr, Bluesky, and Google Business Profile. Use when the user wants to list/create/delete/approve posts, find the best time to post, read and reply to DMs, comments and reviews, manage media, audit workspaces, accounts, campaigns, labels, categories, or team-members, or pull analytics reports (top posts, engagement, impressions, follower growth, AI insights, etc.) on their ContentStudio account.\nversion: 1.4.1\nhomepage: https://api.contentstudio.io/guide\nmetadata: {\"openclaw\":{\"emoji\":\"📅\",\"requires\":{\"bins\":[\"contentstudio\"],\"env\":[\"CONTENTSTUDIO_API_KEY\"]}}}\n---\n\n## Install ContentStudio CLI if it doesn't exist\n\n```bash\nnpm install -g contentstudio-cli\n# or\npnpm install -g contentstudio-cli\n```\n\nnpm release: https://www.npmjs.com/package/contentstudio-cli\ncontentstudio-agent github: https://github.com/contentstudioio/contentstudio-agent\ncontentstudio API docs: https://api.contentstudio.io/api-docs\nofficial website: https://contentstudio.io\n\n---\n\n| Property | Value |\n|----------|-------|\n| **name** | contentstudio |\n| **description** | Social-media automation CLI for scheduling posts and managing media/accounts via the ContentStudio public API |\n| **allowed-tools** | Bash(contentstudio:*) |\n\n---\n\n## ⚠️ Authentication Required\n\n**You MUST authenticate before running any contentstudio CLI command.** All commands will fail without a valid API key.\n\nBefore doing anything else, check auth status:\n\n```bash\ncontentstudio auth:status\n```\n\nIf `has_api_key` is `false`, authenticate one of two ways. The user can generate a key from **ContentStudio Dashboard → Settings → API Keys**.\n\n1. **API key (interactive)** — stores the key in the CLI config file:\n\n```bash\ncontentstudio auth:login --api-key cs_...\n```\n\n2. **Environment variable (headless / agent runtimes)** — the CLI reads `CONTENTSTUDIO_API_KEY` from the environment and it takes precedence over the config file:\n\n```bash\nexport CONTENTSTUDIO_API_KEY=cs_...\n```\n\n> **Headless deployment note (OpenClaw, CI, daemons):** a shell `export` does **not** persist to a service process. Set `CONTENTSTUDIO_API_KEY` in the agent's actual environment — e.g. systemd `Environment=` (`systemctl edit`), an `EnvironmentFile=`, or Docker `-e` / compose `environment:` — then restart the service. Runtimes that gate on declared requirements (e.g. OpenClaw's `requires.env`) will stay blocked until this variable is present in the process environment.\n\nThen verify a workspace is selected:\n\n```bash\ncontentstudio --json workspaces:current\n```\n\nIf `active_workspace_id` is `null`, list workspaces and ask the user to pick one:\n\n```bash\ncontentstudio --json workspaces:list\ncontentstudio workspaces:use <workspace_id>\n```\n\n---\n\n## Invocation rules for agents\n\n- **Always pass `--json` before the subcommand** for stable, parseable output.\n- **Envelope shape**:\n  - Success: `{\"ok\": true, \"data\": <payload>, \"pagination\"?: {...}}`\n  - Error:   `{\"ok\": false, \"error\": {\"type\": \"<ErrorType>\", \"message\": \"...\", \"http_status\": <int>, \"hint\": \"...\"}}`\n- **Exit codes** are non-zero on error. Check both `returncode` and `ok`.\n- **Parse stdout only** — human messages go to stderr.\n- **Before any mutating action (posts/comments/media), run it with `--dry-run`** first to verify the payload is correct. `--dry-run` never touches the API.\n\n### Confirm the target workspace before mutating actions\n\nThe CLI silently defaults to the active workspace (whatever was set by `workspaces:use`). That default is fine for **read-only** calls (`workspaces:list`, `accounts:list`, `posts:list`, `media:list`, etc.) — just use the active workspace.\n\nBut for any **mutating** action — `accounts:connect`, `accounts:add-bluesky`, `accounts:add-facebook-group`, `accounts:remove`, `posts:create`, `posts:update`, `posts:delete`, `posts:approve`, `posts:reject`, `comments:add`, `media:upload`, `workspaces:update`, `workspaces:delete`, `labels:create`, `labels:update`, `labels:delete`, `campaigns:create`, `campaigns:update`, `campaigns:delete`, `team:add`, `team:update`, `team:remove`, and every `inbox:*` write (`inbox:send`, `inbox:comment-add`, `inbox:comment-delete`, `inbox:review-reply`, `inbox:update`, `inbox:tag-*`, …) — you MUST confirm the workspace with the user first, even if a workspace is already active. Don't assume the active workspace is the one they want to mutate.\n\n> **Inbox writes are customer-facing.** `inbox:send`, `inbox:comment-add`, and `inbox:review-reply` publish text to a real person on a real social platform, and there is no undo on the provider side. Always `--dry-run` first, show the exact message text to the user, and get explicit approval before sending. Never compose-and-send a reply to a customer in one step.\n\n(`workspaces:create` is the one write that is **not** workspace-scoped — it creates a brand-new workspace and ignores the active one.)\n\nPattern:\n\n1. Run `contentstudio --json workspaces:current` to see what's active.\n2. Tell the user: \"Your active workspace is **`<name>`** (`<id>`). Do you want to connect/post/delete in this workspace, or a different one?\"\n3. If they say a different one, run `workspaces:list`, let them pick, then either:\n   - Run `workspaces:use <id>` to switch the default, or\n   - Pass `--workspace <id>` on the single mutating call (preferred when it's a one-off — does not change the active workspace).\n4. Only then run the mutating command.\n\nThis is mandatory even when the user's request seems to imply the active workspace (\"connect a Facebook page\", \"create a draft post\") — they may have just switched contexts in their head and forgotten which workspace is active in the CLI.\n\n## Pagination — be proactive, don't silently truncate\n\n**All list commands return a `pagination` block** in JSON mode when more results exist than fit on one page:\n\n```json\n{\n  \"ok\": true,\n  \"data\": [ /* current page of items */ ],\n  \"pagination\": {\n    \"current_page\": 1,\n    \"per_page\": 10,\n    \"total\": 48,\n    \"last_page\": 5,\n    \"from\": 1,\n    \"to\": 10,\n    \"has_more\": true\n  }\n}\n```\n\n**Mandatory rule**: Whenever `pagination.has_more === true`, the user has more data than what was returned. **You MUST NOT silently treat the current\n\nArchive v1.4.0: 6 files, 54673 bytes\n\nFiles: CHANGELOG.md (23656b), LICENSE (1070b), README.md (55521b), skill-card.md (2953b), SKILL.md (77702b), _meta.json (132b)\n\nArchive v1.3.0: 6 files, 53671 bytes\n\nFiles: CHANGELOG.md (21869b), LICENSE (1070b), README.md (54851b), skill-card.md (2957b), SKILL.md (77261b), _meta.json (132b)\n\nArchive v1.2.0: 6 files, 43116 bytes\n\nFiles: CHANGELOG.md (17363b), LICENSE (1070b), README.md (47963b), skill-card.md (2652b), SKILL.md (50966b), _meta.json (132b)\n\nArchive v1.1.1: 6 files, 38909 bytes\n\nFiles: CHANGELOG.md (14488b), LICENSE (1070b), README.md (44447b), skill-card.md (2312b), SKILL.md (46071b), _meta.json (132b)\n\nArchive v1.1.0: 6 files, 39982 bytes\n\nFiles: CHANGELOG.md (15485b), LICENSE (1070b), README.md (44680b), skill-card.md (2670b), SKILL.md (46565b), _meta.json (132b)\n\nArchive v1.0.12: 6 files, 30621 bytes\n\nFiles: CHANGELOG.md (9316b), LICENSE (1070b), README.md (37402b), skill-card.md (2434b), SKILL.md (34712b), _meta.json (133b)\n\nArchive v1.0.10: 6 files, 21257 bytes\n\nFiles: CHANGELOG.md (6348b), LICENSE (1070b), README.md (30640b), skill-card.md (2960b), SKILL.md (14624b), _meta.json (133b)\n\nArchive v1.0.9: 6 files, 20830 bytes\n\nFiles: CHANGELOG.md (5852b), LICENSE (1070b), README.md (30640b), skill-card.md (2404b), SKILL.md (14623b), _meta.json (132b)","readmeExcerpt":"Skill: ContentStudio Owner: contentstudio-official Summary: ContentStudio is a tool to schedule social-media posts, manage the social inbox, and pull performance analytics across Facebook, LinkedIn, Twitter/X, Instagram, YouTube, TikTok, Pinterest, Threads, Tumblr, Bluesky, and Google Business Profile. Use when the user wants to list/create/delete/approve posts, find the best time to post, generate or edit images wit","codeSnippets":[],"executableExamples":[{"language":"bash","snippet":"npm install -g contentstudio-cli\n# or\npnpm install -g contentstudio-cli"},{"language":"bash","snippet":"contentstudio auth:status"},{"language":"bash","snippet":"contentstudio auth:login --api-key cs_..."},{"language":"bash","snippet":"export CONTENTSTUDIO_API_KEY=cs_..."},{"language":"bash","snippet":"contentstudio --json workspaces:current"},{"language":"bash","snippet":"contentstudio --json workspaces:list\ncontentstudio workspaces:use <workspace_id>"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: contentstudio\ndescription: ContentStudio is a tool to schedule social-media posts, manage the social inbox, and pull performance analytics across Facebook, LinkedIn, Twitter/X, Instagram, YouTube, TikTok, Pinterest, Threads, Tumblr, Bluesky, and Google Business Profile. Use when the user wants to list/create/delete/approve posts, find the best time to post, generate or edit images with AI, read and reply to DMs, comments and reviews, manage media, audit workspaces, accounts, campaigns, labels, categories, or team-members, or pull analytics reports (top posts, engagement, impressions, follower growth, AI insights, etc.) on their ContentStudio account.\nversion: 1.5.0\nhomepage: https://api.contentstudio.io/guide\nmetadata: {\"openclaw\":{\"emoji\":\"📅\",\"requires\":{\"bins\":[\"contentstudio\"],\"env\":[\"CONTENTSTUDIO_API_KEY\"]}}}\n---\n\n## Install ContentStudio CLI if it doesn't exist\n\n```bash\nnpm install -g contentstudio-cli\n# or\npnpm install -g contentstudio-cli\n```\n\nnpm release: https://www.npmjs.com/package/contentstudio-cli\ncontentstudio-agent github: https://github.com/contentstudioio/contentstudio-agent\ncontentstudio API docs: https://api.contentstudio.io/api-docs\nofficial website: https://contentstudio.io\n\n---\n\n| Property | Value |\n|----------|-------|\n| **name** | contentstudio |\n| **description** | Social-media automation CLI for scheduling posts and managing media/accounts via the ContentStudio public API |\n| **allowed-tools** | Bash(contentstudio:*) |\n\n---\n\n## ⚠️ Authentication Required\n\n**You MUST authenticate before running any contentstudio CLI command.** All commands will fail without a valid API key.\n\nBefore doing anything else, check auth status:\n\n```bash\ncontentstudio auth:status\n```\n\nIf `has_api_key` is `false`, authenticate one of two ways. The user can generate a key from **ContentStudio Dashboard → Settings → API Keys**.\n\n1. **API key (interactive)** — stores the key in the CLI config file:\n\n```bash\ncontentstudio auth:login --api-key cs_...\n```\n\n2. **Environment variable (headless / agent runtimes)** — the CLI reads `CONTENTSTUDIO_API_KEY` from the environment and it takes precedence over the config file:\n\n```bash\nexport CONTENTSTUDIO_API_KEY=cs_...\n```\n\n> **Headless deployment note (OpenClaw, CI, daemons):** a shell `export` does **not** persist to a service process. Set `CONTENTSTUDIO_API_KEY` in the agent's actual environment — e.g. systemd `Environment=` (`systemctl edit`), an `EnvironmentFile=`, or Docker `-e` / compose `environment:` — then restart the service. Runtimes that gate on declared requirements (e.g. OpenClaw's `requires.env`) will stay blocked until this variable is present in the process environment.\n\nThen verify a workspace is selected:\n\n```bash\ncontentstudio --json workspaces:current\n```\n\nIf `active_workspace_id` is `null`, list workspaces and ask the user to pick one:\n\n```bash\ncontentstudio --json workspaces:list\ncontentstudio workspaces:use <workspace_id>\n```\n\n---\n\n## Invocation rules for agents\n\n- **Always p"},{"path":"README.md","content":"# contentstudio-cli\n\n[![npm version](https://img.shields.io/npm/v/contentstudio-cli.svg)](https://www.npmjs.com/package/contentstudio-cli)\n[![license](https://img.shields.io/npm/l/contentstudio-cli.svg)](./LICENSE)\n\n**Install as a skill:**\n```bash\nnpx skills add contentstudioio/contentstudio-agent\n```\n\nContentStudio CLI — schedule social-media posts, generate AI images, manage media, accounts, comments, approvals, the social inbox, and analytics across **Facebook, LinkedIn, Twitter/X, Instagram, YouTube, TikTok, Pinterest, and Google Business Profile** through the [ContentStudio](https://contentstudio.io) public API.\n\nThe `contentstudio` CLI provides a command-line interface for developers and AI agents to drive a ContentStudio workspace from the terminal — scheduling posts, generating and editing images with AI, uploading media, managing approvals, triaging the inbox, pulling analytics reports, and auditing accounts/campaigns/labels — using the same API your dashboard does.\n\n## Why use this CLI\n\n- **Drive ContentStudio from anywhere** — bash scripts, CI/CD pipelines, AI agents (Claude Code, Cursor, OpenCode, Codex), n8n workflows, custom automations.\n- **JSON output for agents** — every command supports `--json` returning a stable `{\"ok\": true, \"data\": ...}` envelope.\n- **Dry-run safety** — preview every mutating call before sending it, so AI agents (and humans) never publish by accident.\n- **No SaaS lock-in to your CLI tooling** — talks directly to the production ContentStudio API over HTTPS; no proxy, no extra service.\n\n## Installation\n\n### From npm (recommended)\n\n```bash\nnpm install -g contentstudio-cli\n# or\npnpm install -g contentstudio-cli\n```\n\nVerify:\n```bash\ncontentstudio --version\ncontentstudio --help\n```\n\n### Install the skill (for AI agents)\n\nIf you use an AI assistant (Claude Code, Cursor, OpenCode, Codex, Augment, IBM Bob, etc.), install the SKILL.md so the agent can drive this CLI on your behalf:\n\n```bash\nnpx skills add contentstudioio/contentstudio-agent\n```\n\nPick which agents to install into in the interactive prompt. The SKILL.md is dropped into each agent's skill directory (e.g. `~/.claude/skills/contentstudio/SKILL.md`).\n\n## Authentication\n\nAuthentication uses an **API key** issued from your ContentStudio dashboard.\n\n### Option 1: `auth:login` (persists to local config)\n\n```bash\ncontentstudio auth:login --api-key cs_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n```\n\nThis stores your key at `~/.config/contentstudio/config.json` (file mode `0600`, dir `0700`) and verifies it via a `/me` round-trip.\n\n```bash\n# Check current auth status (key redacted)\ncontentstudio auth:status\n\n# Verify the stored key is still valid\ncontentstudio --json auth:whoami\n\n# Remove stored credentials\ncontentstudio auth:logout\n```\n\n### Option 2: Environment variables\n\nFor CI/CD or one-off invocations, set the key in your environment instead of persisting:\n\n```bash\nexport CONTENTSTUDIO_API_KEY=cs_...\nexport CONTENTSTUDIO_WORKSPACE_ID=601b"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn7cvyshqjwfmhgyxnapwkqw4588habf\",\n  \"slug\": \"contentstudio\",\n  \"version\": \"1.5.0\",\n  \"publishedAt\": 1789366557192\n}"},{"path":"CHANGELOG.md","content":"# Changelog\n\n## 1.5.0 — AI image generation\n\nThe public API gained an AI image surface, so the CLI and the agent skill cover\nit: five endpoints under `/workspaces/{w}/ai/...`, wrapped as an `images:*`\ncommand group. Generation is synchronous and every success returns a `media_id`\nthat `posts:create --media-id` takes unchanged — prompt to scheduled post in two\ncommands.\n\n### Discovery\n\n- `images:tools` — the image tools this API can invoke, with each one's required\n  inputs and its control options. An empty list means the catalogue is\n  temporarily unreachable, not that the workspace has no tools.\n- `images:models` — the model identifiers `images:generate` accepts. A curated\n  list, narrower than the models the web app offers.\n- `images:brand` — `{configured, enabled}`: whether `--use-brand` will apply\n  anything. Status only; brand content is never returned, and there is no brand\n  CRUD on this API.\n\n### Generation\n\n- `images:generate -p \"<prompt>\"` — prompt → image, saved to the media library.\n  Flags: `--model`, `--dimensions` (`square`, `square_hd`, `portrait_4_5`,\n  `landscape_16_9`), `--use-brand`, `--enhance-prompt` / `--no-enhance-prompt`.\n- `images:generate -p \"<edit>\" --image-url <url>` — edit an existing image\n  instead. The prompt describes the change. `--dimensions` does not apply on this\n  path (the source geometry wins) and `brand_applied` is always `false`.\n- One command per dedicated tool: `images:product-image`, `images:headshot`,\n  `images:face-swap`, `images:outfit-swap`, `images:upscale`,\n  `images:remove-background`. Flag names mirror the API's field names\n  (`--target-image-url`, `--face-image-url`, `--product-image-url`, …).\n- `images:tool <tool_key> --body '<json>'` — any tool the API exposes, with its\n  full control set. This is how `image-to-image`'s `style`, `image_resolution`,\n  `image_quality`, multiple `attachments` and `reference_image_urls` are reached,\n  and it keeps working when a tool is added upstream without a CLI release.\n\nAll of them take `--dry-run`, `--json` and `--timeout <seconds>`.\n\n### Notes\n\n- **`media_id` is the durable handle; `url` is not.** `url` is for previews and\n  for chaining one tool into the next. A `url` returned alongside a\n  `persist_error` is a temporary provider link — the image was generated *and\n  charged* but not saved, so `media_id` is `null`. Human mode warns about this\n  explicitly; `media_storage_full` says retrying will fail the same way.\n- **The generating calls do not auto-retry.** Everywhere else the client retries\n  `429`/`5xx` twice; here it does not, because these POSTs are billable and not\n  idempotent and a retry can consume a second image credit. The three discovery\n  commands retry normally.\n- **Timeout is 150s, above the server's own 120s deadline**, so a slow model\n  surfaces as the API's `AI_SERVICE_TIMEOUT` (which names the cause and costs no\n  image credits) rather than an opaque local abort. `--timeout <seconds>`\n  overrides it.\n- **Error codes are mapped to typ"},{"path":"skill-card.md","content":"## Description:\n\nContentStudio helps agents schedule social-media posts, manage social inboxes and media, generate or edit AI images, and retrieve analytics across supported social platforms through the ContentStudio public API.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[contentstudio-official](https://clawhub.ai/user/contentstudio-official)\n\n### License/Terms of Use:\n\nMIT\n\n## Use Case:\n\nDevelopers, operators, and external teams use this skill to automate ContentStudio workspace tasks such as scheduling posts, managing social inbox interactions and media, approving or deleting content, generating AI images, and pulling analytics reports.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The skill installs an unpinned external CLI that receives sensitive ContentStudio API credentials.\n\nMitigation: Review or pin the exact contentstudio-cli version and skill source before installation, and use a dedicated API key with the least permissions needed.\n\nRisk: Mutating commands can publish, delete, approve, or modify social content, team settings, and reporting assets.\n\nMitigation: Confirm the active workspace before writes and keep dry-run previews with explicit approval for posting, deletion, inbox replies, and report or share-link changes.\n\nRisk: Unattended bulk-delete or auto-approval automation can cause high-impact account changes.\n\nMitigation: Avoid unattended bulk writes unless separate review and rollback controls are in place.\n\n## Reference(s):\n\n- [ContentStudio API Guide](https://api.contentstudio.io/guide)\n- [ContentStudio API Docs](https://api.contentstudio.io/api-docs)\n- [contentstudio-cli npm package](https://www.npmjs.com/package/contentstudio-cli)\n- [ContentStudio website](https://contentstudio.io)\n- [contentstudio-agent GitHub repository](https://github.com/contentstudioio/contentstudio-agent)\n\n## Skill Output:\n\n**Output Type(s):** [text, markdown, shell commands, configuration, guidance]\n\n**Output Format:** [Markdown guidance with shell commands and JSON command-output expectations]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Uses the contentstudio CLI with --json for structured responses and --dry-run previews for mutating actions.]\n\n## Skill Version(s):\n\n1.5.0 (source: frontmatter and changelog)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment."}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":null,"editorialQuality":{"score":100,"threshold":65,"status":"thin","wordCount":1844,"uniquenessScore":44,"reasons":["uniqueness-below-45"]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-11T10:33:47.704Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-11T10:33:47.704Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-11T14:13:06.447Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}