bama
BAMA: 모든 Mac 앱을 만들 수 있습니다
명령줄로 소셜 게시물 예약·AI 이미지 생성·주요 플랫폼 계정 관리합니다
Schedule social posts, generate AI images, and manage accounts across major platforms via the command line
AI가 요약 · README에서
스킬TypeScript · ShellGitHub @contentstudioio직접 설치운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
ContentStudio 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 public API.README 전문 보기 ↗Install as a skill:
npx skills add contentstudioio/contentstudio-agent
ContentStudio 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 public API.
The 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.
--json returning a stable {"ok": true, "data": ...} envelope.npm install -g contentstudio-cli
# or
pnpm install -g contentstudio-cli
Verify:
contentstudio --version
contentstudio --help
If 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:
npx skills add contentstudioio/contentstudio-agent
Pick 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).
This repo ships a Cursor plugin manifest at .cursor-plugin/plugin.json. Plugins listed in the Cursor marketplace are also available in Grok Bot.
The plugin installs the contentstudio skill and the hosted ContentStudio MCP server (https://mcp.contentstudio.io/mcp, from mcp.json). On first use, you sign in to ContentStudio in your browser through OAuth. No API key or local install is needed for the MCP tools.
Cursor: open Customize in the sidebar, find contentstudio, and select Install.
Grok Bot: add ContentStudio from the Plugins marketplace.
Local install (development):
git clone https://github.com/contentstudioio/contentstudio-agent.git
ln -s "$(pwd)/contentstudio-agent" ~/.cursor/plugins/local/contentstudio
Then restart Cursor or run Developer: Reload Window.
This repo also has a .grok-plugin/plugin.json manifest with the same skill and hosted MCP server, for the xAI plugin marketplace.
https://mcp.contentstudio.io/mcp: hosted MCP server (Streamable HTTP).https://api-prod.contentstudio.io/oauth/*: OAuth 2.1 sign-in (PKCE, dynamic client registration). Scopes: contentstudio:read, contentstudio:write.
skills/contentstudio/SKILL.mdis a copy of the rootSKILL.md(plugin marketplaces don't follow symlinks). After editingSKILL.md, runcp SKILL.md skills/contentstudio/SKILL.md. CI checks this.
Authentication uses an API key issued from your ContentStudio dashboard.
auth:login (persists to local config)contentstudio auth:login --api-key cs_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
This stores your key at ~/.config/contentstudio/config.json (file mode 0600, dir 0700) and verifies it via a /me round-trip.
# Check current auth status (key redacted)
contentstudio auth:status
# Verify the stored key is still valid
contentstudio --json auth:whoami
# Remove stored credentials
contentstudio auth:logout
For CI/CD or one-off invocations, set the key in your environment instead of persisting:
export CONTENTSTUDIO_API_KEY=cs_...
export CONTENTSTUDIO_WORKSPACE_ID=601b773d2149273f48039ec2 # optional
export CONTENTSTUDIO_BASE_URL=https://api.contentstudio.io/api/v1 # optional
Env vars take priority over the persisted config when both are present.
ContentStudio Dashboard → Settings → API Keys → Generate new key.
# 1. Auth (once)
contentstudio auth:login --api-key cs_...
# 2. Pick a workspace
contentstudio workspaces:list
contentstudio workspaces:use <workspace_id>
# 3. List the social accounts connected to that workspace
contentstudio accounts:list --platform facebook
# 4. Create a draft post (safe — won't publish to social)
contentstudio posts:create \
--content "Hello from contentstudio CLI" \
--account <account_id> \
--publish-type draft
# 5. Schedule a real post for 2 minutes from now
contentstudio posts:create \
-c "Hello from automation 👋" \
-i <account_id> \
-t scheduled \
-s "$(date -d '+2 minutes' '+%F %T')" \
-m https://picsum.photos/400
contentstudio --json workspaces:list
contentstudio --json workspaces:list --per-page 50
Returns workspace IDs, names, slugs, timezones.
contentstudio workspaces:current
contentstudio workspaces:use <workspace_id>
contentstudio --json accounts:list # all accounts
contentstudio --json accounts:list --platform facebook # filter
contentstudio --json accounts:list --search "barcelona" # search by name
--platform values: facebook, linkedin, twitter, instagram, youtube, tiktok, pinterest, gmb.
contentstudio --json campaigns:list
contentstudio --json categories:list
contentstudio --json labels:list
contentstudio --json team:list
contentstudio --json approval-workflows:list # use an item's id as --approval-workflow-id
All support --page and --per-page; the campaigns/categories/labels/team lists also support --search.
Three ways to add new accounts to a workspace, depending on the platform.
contentstudio --json platforms:list
Returns all 12+ supported platforms with their connection_method (oauth, credentials, or manual) and the endpoint to call.
contentstudio --json accounts:connect facebook
# Returns a one-time authorization_url — open it in your browser to authorize.
To reconnect an existing account that's expired or invalid:
contentstudio --json accounts:connect facebook --reconnect --account-id <existing_account_id>
Available <platform> values: facebook, facebook-profile, instagram, instagram-via-facebook, twitter, linkedin, pinterest, tiktok, youtube, threads, gmb, tumblr.
Generate an app password at https://bsky.app/settings/app-passwords first, then:
contentstudio --json accounts:add-bluesky \
--handle yourname.bsky.social \
--app-password xxxx-xxxx-xxxx-xxxx
⚠️ 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.
contentstudio --json accounts:add-facebook-group \
--name "My Community Group" \
--image https://example.com/group-cover.jpg
The image URL is optional.
# preview first
contentstudio --json accounts:remove <account_id> --dry-run
# then actually remove
contentstudio --json accounts:remove <account_id>
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.
All three connect commands support --dry-run to preview the payload without calling the API.
There are two ways to create a post: shortcut flags for simple cases, or --body <file.json> for the full schema.
# Scheduled post to a Facebook page with one image
contentstudio posts:create \
-c "Our latest blog post is live!" \
-i <account_id> \
-t scheduled \
-s "2026-05-01 10:00:00" \
-m https://example.com/hero.jpg
Options:
| Flag | Purpose |
|---|---|
-c, --content | Post text |
-i, --account | Social account ID. Repeatable for multi-account posts. |
-t, --publish-type | scheduled | draft | queued | content_category |
-s, --scheduled-at | Schedule date "YYYY-MM-DD HH:MM:SS" |
-m, --image-url | External image URL. Repeatable. |
--video-url | External video URL |
--media-id | ID of media in your library (from media:list). Repeatable. |
--post-type | feed | reel | story | feed+reel | feed+story | feed+reel+story | carousel | carousel+story | video | shorts |
--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. |
--threads '<json>' | Threads multi-thread (Threads accounts only): JSON array [{message, media?, media_ids?}]. Max 10 items. CLI adds has_multi_threads:true. |
--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. |
--first-comment "<message>" | First comment (≤2000 chars). Build first_comment:{message, accounts?}. Requires --first-comment-account. |
--first-comment-account <id> | Account for the first comment. Repeatable. Must be a subset of --account; backend 422s if omitted when is set. |
Repeat -i for each account:
contentstudio posts:create \
-c "Cross-platform announcement 🚀" \
-i <facebook_id> \
-i <linkedin_id> \
-i <twitter_id> \
-t scheduled \
-s "2026-05-01 09:00:00"
# Find a media ID
contentstudio --json media:list --type images
# Reference it by ID instead of URL
contentstudio posts:create \
-c "Post with library asset" \
-i <account_id> \
-t draft \
--media-id <media_library_id>
contentstudio posts:create \
-c "Filler post for the queue" \
-i <account_id> \
-t queued
scheduled_at is optional for queued — the backend slots it into the workspace queue automatically.
-i)# Find a category id first:
contentstudio --json categories:list
# --content-category-id is required for -t content_category; accounts are derived from the category:
contentstudio posts:create \
-c "Evergreen tip of the day" \
-t content_category \
--content-category-id <category_id>
contentstudio --json posts:create --dry-run \
-c "Shop the new collection" \
-i <facebook_account_id> \
-t scheduled \
-s "2026-07-01 10:00:00" \
--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"}'
The 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.
contentstudio --json posts:create --dry-run \
-c "🧵 A thread on shipping CLIs" \
-i <threads_account_id> \
-t draft \
--threads '[{"message":"1/ Start small."},{"message":"2/ Ship a demo.","media":["https://e.com/demo.mp4"]},{"message":"3/ Iterate in public."}]'
The 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.
contentstudio --json posts:create --dry-run \
-c "Why we built a CLI 🧵" \
-i <twitter_account_id> \
-t draft \
--twitter '[{"message":"1/ Start with the contract."},{"message":"2/ Show, don'\''t tell.","media":["https://e.com/x.jpg"]},{"message":"3/ Ship it."}]'
The 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.
--platform-overrides)Publish the same post to several platforms but swap the caption, post type, or media for one of them:
contentstudio --json posts:create --dry-run \
-c "Common caption" \
-i <facebook_id> -i <tiktok_id> \
-t draft \
-m https://example.com/common.jpg \
--platform-overrides '{"tiktok":{"content":{"media":{"video":"https://example.com/clip.mp4"}}}}'
TikTok 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.
Keyed 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.
contentstudio --json posts:create --dry-run \
-c "New drop is live 🎉" \
-i <account_id> \
-t draft \
--first-comment "🔗 link in bio" \
--first-comment-account <account_id>
The 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.
--body <file.json>For 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:
contentstudio --json posts:create --body /tmp/post.json
Body schema:
{
"content": {
"text": "Hello world",
"media": {
"images": ["https://example.com/img.jpg"],
"video": "https://example.com/clip.mp4",
"media_ids": ["<media_library_id>"]
}
},
"accounts": ["<account_id>"],
"post_type": "reel+story",
"post_video_title": "My Video Title",
"scheduling": {
"publish_type": "scheduled",
"scheduled_at": "2026-05-01 10:00:00"
},
"first_comment": {
"message": "🔗 link in bio",
"accounts": ["<account_id>"]
},
"labels": ["<label_id>"],
"campaign_id": "<campaign_id>",
"approval": {
"approvers": ["<user_id>"],
"approve_option": "anyone",
"notes": "please review"
},
// facebook_options: use EITHER carousel OR facebook_background_id (different FB formats, not both):
"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://..." } },
// colored-background text post instead: "facebook_options": { "facebook_background_id": "<id>" },
"threads_options": { "has_multi_threads": true, "multi_threads": [ {"message":"1/ ..."}, {"message":"2/ ...","media":["https://...mp4"]} ] },
"youtube_options": { "title": "...", "privacy_status": "public", "category": "EDUCATION", "tags": ["tag1"], "license": "youtube", "made_for_kids": false },
"tiktok_options": { "privacy_level": "PUBLIC_TO_EVERYONE", "disable_comment": false, "disable_duet": false, "disable_stitch": false, "auto_add_music": false },
"pinterest_options": { "title": "...", "link": "https://..." },
"gmb_options": { "topic_type": "EVENT", "start_date": "2026-05-01", "end_date": "2026-05-02", "title": "...", "action_type": "BOOK", "cta_link": "https://..." }
}
# Every week for 4 weeks, starting at the scheduled time
contentstudio --json posts:create -c "Weekly tip" -i <account_id> \
-t scheduled -s "2026-10-01 09:00:00" \
--repeat-type Week --repeat-times 4 --repeat-gap 1 --dry-run
All three flags are required together. --repeat-type is Day, Week or
Month; --repeat-times is 1–30; --repeat-gap is 1–99 and must be ≥3 for
Day. Repeat requires --publish-type scheduled — the CLI errors locally on
any other publish type.
This creates N independent child posts, not one recurring post — editing or
deleting the parent leaves the children alone, and children drop Twitter/X and
YouTube accounts. Repeat is never inherited on update, so re-sending a post
without the flags creates nothing new and a read-modify-write is safe. On the
read side scheduling.repeat is {is_parent, is_child, parent_id, type, times, gap} — no enabled key, which is request-only.
--dry-run firstFor agents (and cautious humans), every mutating command supports --dry-run — it prints the request body and exits without calling the API:
contentstudio --json posts:create --dry-run \
-c "Test" -i <account_id> -t scheduled -s "2026-05-01 10:00"
# → {"ok": true, "data": {"dry_run": true, "endpoint": "...", "body": {...}}}
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.
# Best times across every connected account
contentstudio --json scheduling:best-times
# Just this Facebook page, 3 recommendations for it
contentstudio --json scheduling:best-times \
--account facebook:<account_id> --per-account-slots 3
# Several accounts, and a bigger pooled list
contentstudio --json scheduling:best-times \
--account facebook:<account_id> \
--account instagram:<account_id> \
--global-slots 10
# Per-account slot counts need the full entity array
contentstudio --json scheduling:best-times \
--entities '[{"id":"<account_id>","type":"facebook","slots":5},
{"id":"<account_id>","type":"linkedin","slots":2}]'
--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.
--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.
The --json payload:
{
"ok": true,
"data": {
"meta": {
"generated_at": "2026-08-17T09:00:00Z",
"timezone": "Asia/Karachi",
"warnings": [],
"missing_entities": [],
"ai_fallback_entities": []
},
"global": {
"top_recommendations": [
{ "rank": 1, "day": "Wednesday", "date": "2026-08-19", "time": "14",
"score": 100, "platform_breakdown": { "facebook": 60, "instagram": 40 } }
],
"heatmap_matrix": { "data": [[14, 2, 100]] },
"dates_key": ["2026-08-19"]
},
"individual": {
"<account_id>": { "platform": "facebook", "source": "data_driven",
"top_recommendations": [] }
}
}
}
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:
# rank 1 above → Wednesday 2026-08-19 at 14:00 workspace-local
contentstudio --json posts:create \
-c "Launch day is here." -i <account_id> -t scheduled \
-s "2026-08-19 14:00:00" --dry-run
A 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.
contentstudio --json posts:list # all recent
contentstudio --json posts:list --status draft --per-page 5
contentstudio --json posts:list --status scheduled --status published
contentstudio --json posts:list --date-from 2026-04-01 --date-to 2026-04-30
contentstudio --json posts:get <post_id>
The payload is identical to that post's row in posts:list, so this is the
cheap way to re-read a single post after a write. A deleted post, a post in
another workspace, and a malformed id all answer 404.
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.
# Preview an edit (change text + reschedule)
contentstudio --json posts:update <post_id> -c "Updated copy" -i <account_id> -t scheduled -s "2026-08-01 10:00:00" --dry-run
# Attach an approval workflow (get the id from approval-workflows:list)
contentstudio --json posts:update <post_id> -c "Q3 launch" -i <account_id> -t draft --approval-workflow-id <workflow_id>
# Mutate the already-attached workflow (update only)
contentstudio --json posts:update <post_id> -c "Q3 launch" -i <account_id> -t draft --approval-workflow-action restart --approval-workflow-notes "please re-review"
# LinkedIn poll (text-only; requires --post-type poll)
contentstudio --json posts:update <post_id> -c "Vote!" -i <linkedin_account_id> -t draft --post-type poll \
--linkedin-options '{"poll":{"question":"Best day to ship?","options":["Mon","Fri"],"duration":"SEVEN_DAYS"}}'
# Just delete from ContentStudio
contentstudio --json posts:delete <post_id>
# Also delete from the connected social platforms
contentstudio --json posts:delete <post_id> --delete-from-social
# Limit the cross-platform delete to specific accounts
contentstudio --json posts:delete <post_id> --account <account_id> --delete-from-social
# Preview without deleting
contentstudio --json posts:delete <post_id> --dry-run
contentstudio --json posts:approve <post_id> --comment "LGTM, ship it"
contentstudio --json posts:reject <post_id> --comment "fix the link first"
# Preview without acting
contentstudio --json posts:approve <post_id> --dry-run
# List all comments / notes on a post
contentstudio --json comments:list <post_id>
# Add a public comment
contentstudio --json comments:add <post_id> "Great work team!"
# Add an internal note (not visible to the public)
contentstudio --json comments:add <post_id> "Double-check the link before publishing" --note
# Mention team members
contentstudio --json comments:add <post_id> "Heads up" --mention <user_id> --mention <user_id>
# Preview
contentstudio --json comments:add <post_id> "test" --note --dry-run
Note:
comments:*are ContentStudio-internal comments on a draft/scheduled post — collaboration between your team. To reply to a real comment left by a real person on a published post, use the Social Inbox commands below.
The inbox brings DMs, post comments, and reviews into one place. It models all
three as elements, each identified by an element_ref:
Ids come from element_details. Use element_details.element_id — it is
accepted by every element-scoped command:
Id from inbox:list | Used by |
|---|---|
element_details.element_id | every element-scoped command, plus messages / send / notes / bookmarks |
element_details.post_id | comments, comment-add |
The row's top-level element_ref is an internal reference, not a command
argument — take the id from element_details.
| Inbox type | What it is |
|---|---|
conversation | A DM thread (Facebook / Instagram) |
post | A published post with comments on it |
review | A review (e.g. Google Business Profile) |
Most write commands also need --platform-id — the connected account the item
belongs to, since replies go out through that account. Find it with
accounts:list.
# Counts per bucket — cheapest way to see if anything needs attention
contentstudio --json inbox:summary
# Everything
contentstudio --json inbox:list
# Just unanswered DMs, 50 at a time
contentstudio --json inbox:list --type conversation --action all --limit 50
# Full-text search, restricted to one Facebook account
contentstudio --json inbox:list \
--search "refund" \
--channels '{"facebook":["<account_id>"]}'
# Filter by tag
contentstudio --json inbox:list --tag <tag_id> --tag <tag_id>
Inbox lists use --limit (with --per-page accepted as an alias) and --page.
Limits the API enforces, checked client-side before any request goes out:
| Limit | Applies to |
|---|---|
--limit ≤ 200 | inbox:list, inbox:messages, inbox:comments |
≤ 100 --element refs | inbox:update |
| Exactly one operation per call | inbox:update (--status / --archived / --assigned are mutually exclusive) |
| Tag name ≤ 50 chars | inbox:tag-create |
inbox:update may also come back as a partial success (HTTP 207) when
some elements could not be updated. The CLI prints a warning listing the
untouched refs instead of reporting a clean pass.
# Messages, newest first
contentstudio --json inbox:messages <conversation_id> --sort-order desc --limit 20
# A thread also contains team activity entries (marked done, archived, ...).
# Those have `message: null` and an `action` block — filter on
# `action == null` when you want customer messages only.
# Comments on a published post
contentstudio --json inbox:comments <post_id>
# Team-only notes attached to a conversation
contentstudio --json inbox:notes <conversation_id>
# Starred messages
contentstudio --json inbox:bookmarks <conversation_id>
# Who am I talking to?
contentstudio --json inbox:contact <element_ref>
These reach real customers. Preview with --dry-run first.
# Send a DM
contentstudio --json inbox:send <conversation_id> \
--platform-type facebook \
--platform-id <account_id> \
--message "Thanks for reaching out — shipping today!" \
--dry-run
# Send a DM with an image attached
contentstudio --json inbox:send <conversation_id> \
--platform-type instagram --platform-id <account_id> \
--message "Here's the size chart" \
--file ./size-chart.png --file-type image
# Comment on a post
contentstudio --json inbox:comment-add <post_id> \
--platform-type facebook --platform-id <account_id> \
--message "Glad you like it!"
# Reply to a specific comment (threaded)
contentstudio --json inbox:comment-add <post_id> \
--platform-type facebook --platform-id <account_id> \
--comment-id <comment_id> --message "DMing you the details."
# Facebook private reply — answers a public comment via DM
contentstudio --json inbox:comment-add <post_id> \
--platform-type facebook --platform-id <account_id> \
--comment-id <comment_id> --private-reply \
--message "Sent you a DM with your order info."
# Reply to a review (upsert — replaces an existing reply)
contentstudio --json inbox:review-reply <review_id> \
--platform-id <account_id> --reply "Thanks for the feedback!"
# Internal note — your team only, never shown to the customer
contentstudio --json inbox:note-add <conversation_id> \
--platform-type facebook --platform-id <account_id> \
--message "Escalated to billing" --mention <user_id>
Retrying a send? Pass --idempotency-key <uuid> so a repeated request isn't
delivered twice. It protects sequential retries, not concurrent ones.
contentstudio --json inbox:mark-read <element_ref>
# Bulk: close out several at once (max 100 refs per call)
contentstudio --json inbox:update \
--element <ref_1> --element <ref_2> --status done
# Archive / assign — exactly ONE operation per call
contentstudio --json inbox:update --element <ref> --archived
contentstudio --json inbox:update --element <ref> \
--assigned --assigned-to '{"id":"<user_id>"}'
# Star a message
contentstudio --json inbox:star <message_id>
contentstudio --json inbox:unstar <message_id>
# Hide is reversible — prefer it over delete
contentstudio --json inbox:comment-hide <comment_id>
contentstudio --json inbox:comment-unhide <comment_id> \
--platform-type facebook --platform-id <account_id>
# Like / unlike (Facebook)
contentstudio --json inbox:comment-like <comment_id>
contentstudio --json inbox:comment-unlike <comment_id>
# Delete a comment (LinkedIn additionally needs --comment-urn)
contentstudio --json inbox:comment-delete <comment_id> \
--platform-type facebook --platform-id <account_id>
# Delete a message / a review reply
contentstudio --json inbox:message-delete <message_id> --platform-id <account_id>
contentstudio --json inbox:review-reply-delete <review_id> --platform-id <account_id>
contentstudio --json inbox:tags
contentstudio --json inbox:tag-create --name "VIP" --color "#ff0055"
contentstudio --json inbox:tag-update <tag_id> --name "VIP customer"
contentstudio --json inbox:tag-delete --tag <tag_id> --tag <tag_id>
# Fold several tags into one new tag
contentstudio --json inbox:tag-merge --name "Support" --color "#0088ff" \
--tag <tag_id> --tag <tag_id>
# Attach / detach on an element
contentstudio --json inbox:tag-attach <element_ref> \
--tag <tag_id> --platform-id <account_id> --inbox-type conversation
contentstudio --json inbox:tag-detach <element_ref> <tag_id> \
--platform-id <account_id> --inbox-type conversation
contentstudio --json inbox:contact-update <element_ref> \
--platform-id <account_id> \
--name "Jane Doe" --email [email protected] --company "Acme"
contentstudio --json media:list # all
contentstudio --json media:list --type images --sort recent --per-page 20
contentstudio --json media:list --type videos
contentstudio --json media:list --search "campaign-2026"
--sort values: recent, oldest, size, a2z, z2a.
Upload a local file:
contentstudio --json media:upload --file ./hero.jpg
Or import from an external URL:
contentstudio --json media:upload --url https://example.com/asset.mp4
Optionally place into a folder:
contentstudio --json media:upload --file ./hero.jpg --folder-id <folder_id>
Preview (no upload):
contentstudio --json media:upload --url https://example.com/img.jpg --dry-run
The response includes an id you can pass as --media-id when creating posts.
Read-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.
# Date-range report — most commands take --platform-id + --start-date/--end-date
contentstudio --json analytics:instagram-top-posts \
--platform-id <account_id> --start-date 2026-08-01 --end-date 2026-08-12
# With optional filters (order-by is an enum, media-type is repeatable)
contentstudio --json analytics:facebook-get-top-posts \
--platform-id <account_id> --start-date 2026-08-01 --end-date 2026-08-12 \
--order-by comments --media-type IMAGE --media-type VIDEO
# Single-item lookup — platform-native id, not a ContentStudio id
contentstudio --json analytics:youtube-single-video --platform-id <account_id> --post-id <video_id>
# AI-generated insights
contentstudio --json analytics:linkedin-ai-insights \
--platform-id <account_id> --start-date 2026-08-01 --end-date 2026-08-12 --language en
# See exactly which options a given command takes
contentstudio analytics:pinterest-top-pins --help
All 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.
Generate 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.
contentstudio --json images:tools # invocable tools, their required inputs and controls
contentstudio --json images:models # model identifiers images:generate accepts
contentstudio --json images:brand # {configured, enabled} — will --use-brand do anything?
These three describe configuration rather than workspace state, so they are worth caching.
# Preview the request first — generating costs an image credit
contentstudio --json images:generate -p "Flat-lay of autumn coffee beans on linen" --dry-run
# Generate
contentstudio --json images:generate \
-p "Flat-lay of autumn coffee beans on linen, warm daylight" \
--dimensions square_hd
# Pick a model, and let the service refine the prompt (its default) or not
contentstudio --json images:generate -p "..." --model nano-banana-pro --no-enhance-prompt
# Transparent background (only gpt-image-2.5-flare and gpt-image-2.5-sunburst honor --background)
contentstudio --json images:generate -p "Sticker of a coffee cup" --model gpt-image-2.5-flare --background transparent
# Apply the workspace's brand knowledge (resolved server-side; no brand ID exists)
contentstudio --json images:generate -p "..." --use-brand
# Edit an existing image — the prompt describes the change, not the whole picture
contentstudio --json images:generate \
-p "Make the background a snowy street at dusk" \
--image-url https://example.com/base.png
--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.
--background is auto, opaque or transparent (sent as image_background). It is honored only by the models that support it, currently gpt-image-2.5-flare and gpt-image-2.5-sunburst; other models ignore it.
Models and cost per image. images:models lists each model with its credits_per_image and supported backgrounds.
| Model | Credits per image | Generate | Edit (--image-url) |
|---|---|---|---|
most models (e.g. nano-banana-pro) | 1 | yes | yes |
gpt-image-2 | 5 | yes | yes |
gpt-image-2.5-flare | 3 | yes | yes |
gpt-image-2.5-sunburst | 5 | yes | yes |
MEDIA_ID=$(contentstudio --json images:generate \
-p "Flat-lay of autumn coffee beans on linen, warm daylight" \
--dimensions square_hd | jq -r '.data.media_id')
contentstudio --json posts:create \
-c "Autumn blend is back." -i <account_id> -t draft --media-id "$MEDIA_ID"
-t draft keeps it reviewable; -t scheduled -s "YYYY-MM-DD HH:MM:SS" sends it. There is
no publish-now type.
contentstudio --json images:product-image --product-image-url https://example.com/mug.png \
--instructions "on a marble kitchen counter, morning light"
contentstudio --json images:headshot --image-url https://example.com/person.jpg --aspect-ratio 4:5
contentstudio --json images:face-swap \
--target-image-url https://example.com/scene.png \
--face-image-url https://example.com/face.jpg
contentstudio --json images:outfit-swap \
--target-image-url https://example.com/model.jpg \
--outfit-image-url https://example.com/jacket.png
contentstudio --json images:upscale --image-url https://example.com/small.png --resolution 2k
contentstudio --json images:remove-background --image-url https://example.com/mug.png
Allowed 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.
Those 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.
Every generating command takes --dry-run, --timeout <seconds> and --json.
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:
contentstudio --json images:tool image-to-image --body '{
"prompt": "same mug, editorial magazine styling",
"attachments": ["https://example.com/mug.png"],
"aspect_ratio": "4:5"
}'
{
"ok": true,
"data": {
"media_id": "66f1a2b3c4d5e6f708192a3b",
"url": "https://storage.googleapis.com/contentstudio/.../generated.png",
"width": 1024,
"height": 1024,
"mime_type": "image/png",
"model_used": "nano-banana-pro",
"brand_applied": false,
"credits": { "consumed": 1, "available": 412 },
"persist_error": null
}
}
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.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.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, gpt-image-2.5-flare 3, gpt-image-2.5-sunburst 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.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.Every 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.
To use a local file, put it in the media library first:
URL=$(contentstudio --json media:upload --file ./mug.png | jq -r '.data.url')
contentstudio --json images:upscale --image-url "$URL"
Tools 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.
Generation 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.
Unlike 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.
error_code | CLI error | What to do |
|---|---|---|
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 |
CONTENT_BLOCKED | ValidationError | The content policy refused the prompt; rephrase it. Retrying as-is fails again |
IMAGE_INPUT_REJECTED | ValidationError | Usually an image URL the service could not download; also a too-small or too-large source |
TOOL_NOT_FOUND | NotFoundError | Unknown, disabled, or a video tool. Re-read images:tools |
RATE_LIMIT_EXCEEDED | RateLimitError | 30 requests/minute, shared with the ContentStudio app's own AI usage on this account. Wait out the minute |
AI_SERVICE_TIMEOUT | BackendError | The service did not finish in 120s. Retry with backoff, or use a faster model |
AI_SERVICE_UNAVAILABLE | BackendError | Retry promptly. On a tool run this can arrive after the credit was taken |
A 403 with no error_code is a membership or API-request-credit problem and stays an AuthError.
Video 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.
The full body schema accepts platform-specific options. These examples show the most common configurations.
contentstudio --json posts:create \
-c "Big news for our community 🎉" \
-i <facebook_page_id> \
-t scheduled \
-s "2026-05-01 10:00:00" \
-m https://example.com/announcement.jpg
For Facebook Reels or Stories, set --post-type:
contentstudio posts:create \
-c "Behind-the-scenes" \
-i <facebook_id> \
-t scheduled \
-s "2026-05-01 10:00:00" \
--video-url https://example.com/clip.mp4 \
--post-type reel+story
contentstudio --json posts:create \
-c "Excited to share our Q2 roadmap" \
-i <linkedin_id> \
-t scheduled \
-s "2026-05-01 09:00:00" \
-m https://example.com/roadmap.png
# Single tweet with image
contentstudio --json posts:create \
-c "New release shipped 🚀" \
-i <twitter_id> \
-t scheduled \
-s "2026-05-01 10:00:00" \
-m https://example.com/preview.png
For Instagram, control the post format with --post-type:
# Feed post
contentstudio posts:create \
-c "Caption with #hashtags" \
-i <instagram_id> \
-t scheduled \
-s "2026-05-01 10:00:00" \
-m https://example.com/photo.jpg \
--post-type feed
# Reel
contentstudio posts:create \
-c "" \
-i <instagram_id> \
-t scheduled \
-s "2026-05-01 10:00:00" \
--video-url https://example.com/reel.mp4 \
--post-type reel
# Trial reel — shown to non-followers first, not on the profile grid or
# follower feeds. Requires --post-type reel exactly, plus a video.
# Rejected (422) together with --instagram-collaborator.
contentstudio posts:create \
-c "" \
-i <instagram_id> \
-t scheduled \
-s "2026-05-01 10:00:00" \
--video-url https://example.com/reel.mp4 \
--post-type reel \
--instagram-trial-reel \
--instagram-trial-reel-graduation SS_PERFORMANCE
# Story
contentstudio posts:create \
-c "" \
-i <instagram_id> \
-t scheduled \
-s "2026-05-01 10:00:00" \
-m https://example.com/story.jpg \
--post-type story
YouTube needs youtube_options — use a --body file:
cat > /tmp/yt-post.json <<'JSON'
{
"content": {
"text": "Description shown under the video",
"media": {"video": "https://example.com/clip.mp4"}
},
"accounts": ["<youtube_id>"],
"post_type": "shorts",
"post_video_title": "How we built ContentStudio CLI",
"scheduling": {"publish_type": "scheduled", "scheduled_at": "2026-05-01 10:00:00"},
"youtube_options": {
"title": "How we built ContentStudio CLI",
"privacy_status": "public",
"category": "EDUCATION",
"tags": ["cli", "automation", "social-media"],
"license": "youtube",
"made_for_kids": false
}
}
JSON
contentstudio --json posts:create --body /tmp/yt-post.json
cat > /tmp/tt-post.json <<'JSON'
{
"content": {
"text": "Quick demo #fyp #tutorial",
"media": {"video": "https://example.com/tiktok.mp4"}
},
"accounts": ["<tiktok_id>"],
"scheduling": {"publish_type": "scheduled", "scheduled_at": "2026-05-01 10:00:00"},
"tiktok_options": {
"privacy_level": "PUBLIC_TO_EVERYONE",
"disable_comment": false,
"disable_duet": false,
"disable_stitch": false,
"auto_add_music": false,
"brand_content_toggle": false,
"disclose_commercial_content": false,
"is_aigc": false
}
}
JSON
contentstudio --json posts:create --body /tmp/tt-post.json
cat > /tmp/pin-post.json <<'JSON'
{
"content": {
"text": "Check out our spring guide",
"media": {"images": ["https://example.com/pin.jpg"]}
},
"accounts": ["<pinterest_id>"],
"scheduling": {"publish_type": "scheduled", "scheduled_at": "2026-05-01 10:00:00"},
"pinterest_options": {
"title": "Spring 2026 Style Guide",
"link": "https://example.com/spring-guide"
}
}
JSON
contentstudio --json posts:create --body /tmp/pin-post.json
cat > /tmp/gmb-post.json <<'JSON'
{
"content": {
"text": "Join our grand opening event",
"media": {"images": ["https://example.com/event.jpg"]}
},
"accounts": ["<gmb_account_id>"],
"scheduling": {"publish_type": "scheduled", "scheduled_at": "2026-05-01 10:00:00"},
"gmb_options": {
"topic_type": "EVENT",
"start_date": "2026-05-15",
"end_date": "2026-05-16",
"title": "Grand Opening",
"action_type": "BOOK",
"cta_link": "https://example.com/rsvp"
}
}
JSON
contentstudio --json posts:create --body /tmp/gmb-post.json
This CLI is designed to be driven by AI assistants. Three properties make it agent-friendly:
Every command supports --json returning a predictable shape:
// Success
{ "ok": true, "data": <payload> }
// Error
{
"ok": false,
"error": {
"type": "AuthError",
"message": "Invalid or revoked API key",
"http_status": 401,
"hint": "Run `contentstudio auth:login --api-key cs_...` to set a valid API key."
}
}
Agents check both ok and the process exit code (non-zero on error).
Every 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.
npx skills addThe repo ships a SKILL.md agents can install with one command:
npx skills add contentstudioio/contentstudio-agent
After this, the agent automatically knows when to use the contentstudio CLI without prompting.
#!/bin/bash
# Daily content batch for a Facebook page
ACCOUNT="<facebook_page_id>"
CONTENT=(
"Monday motivation 💪"
"Tuesday tips: keep it simple"
"Wednesday wisdom from the team"
"Throwback Thursday"
"Friday vibes 🎉"
"Weekend prep — try this"
"Sunday reflections"
)
for i in "${!CONTENT[@]}"; do
DATE=$(date -d "+$((i+1)) day 09:00" '+%F %T')
contentstudio --json posts:create \
-c "${CONTENT[$i]}" \
-i "$ACCOUNT" \
-t scheduled \
-s "$DATE"
done
#!/bin/bash
# Same content to FB + LinkedIn + Twitter at the same time
TIME="2026-05-01 10:00:00"
# List accounts and pick one per platform
FB=$(contentstudio --json accounts:list --platform facebook | jq -r '.data[0].id')
LI=$(contentstudio --json accounts:list --platform linkedin | jq -r '.data[0].id')
TW=$(contentstudio --json accounts:list --platform twitter | jq -r '.data[0].id')
contentstudio --json posts:create \
-c "Big launch today 🚀" \
-i "$FB" -i "$LI" -i "$TW" \
-t scheduled \
-s "$TIME" \
-m https://example.com/launch.jpg
#!/bin/bash
CUTOFF=$(date -d '-30 days' '+%Y-%m-%d')
contentstudio --json posts:list --status draft --date-to "$CUTOFF" --per-page 100 \
| jq -r '.data[].id' \
| while read id; do
contentstudio --json posts:delete "$id"
done
#!/bin/bash
ACCOUNT="<instagram_id>"
for img in ./photos/*.jpg; do
# Upload first to get a media library ID
RESP=$(contentstudio --json media:upload --file "$img")
MEDIA_ID=$(echo "$RESP" | jq -r '.data.id')
# Schedule a post with the uploaded media
TIME=$(date -d "+1 hour" '+%F %T')
contentstudio --json posts:create \
-c "$(basename "$img" .jpg)" \
-i "$ACCOUNT" \
-t scheduled \
-s "$TIME" \
--media-id "$MEDIA_ID" \
--post-type feed
done
#!/bin/bash
TRUSTED_USER_ID="<user_id>"
contentstudio --json posts:list --status pending_approval --per-page 50 \
| jq -r --arg u "$TRUSTED_USER_ID" '.data[] | select(.created_by == $u) | .id' \
| while read id; do
contentstudio --json posts:approve "$id" --comment "auto-approved (trusted creator)"
done
The 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.
| Method | Endpoint | CLI command |
|---|---|---|
| GET | /me | auth:whoami |
| GET | /platforms | platforms:list |
| GET | /facebook/text-backgrounds | facebook:text-backgrounds |
| GET | /workspaces | workspaces:list |
| GET | /workspaces/{w}/accounts | accounts:list |
| POST | /workspaces/{w}/connect/{platform} | accounts:connect <platform> |
| POST | /workspaces/{w}/add/bluesky | accounts:add-bluesky |
| POST | /workspaces/{w}/add/facebook-group | accounts:add-facebook-group |
| DELETE | /workspaces/{w}/accounts/{account_id} | accounts:remove <account_id> |
| GET | /workspaces/{w}/campaigns | campaigns:list |
| GET | /workspaces/{w}/limits | workspaces:limits |
| GET | /workspaces/{w}/content-categories | categories:list |
| POST | /workspaces/{w}/content-categories | categories:create |
| GET | /workspaces/{w}/content-categories/{c} | categories:get |
| PUT | /workspaces/{w}/content-categories/{c} | categories:update |
| DELETE | /workspaces/{w}/content-categories/{c} | categories:delete |
| POST | /workspaces/{w}/content-categories/{c}/shuffle | categories:shuffle |
| GET | /workspaces/{w}/content-categories/{c}/slots |
Full OpenAPI 3.0 spec: https://api.contentstudio.io/api-docs.json Human-readable docs: https://api.contentstudio.io/guide
Stored at:
$XDG_CONFIG_HOME/contentstudio/config.json
# falls back to ~/.config/contentstudio/config.json
File mode 0600, parent dir 0700 — never world-readable.
Format:
{
"api_key": "cs_...",
"base_url": "https://api.contentstudio.io/api/v1",
"active_workspace_id": "<workspace_id>",
"active_workspace_name": "...",
"user": { "id": "...", "email": "...", "full_name": "..." }
}
| Variable | Required | Default | Description |
|---|---|---|---|
CONTENTSTUDIO_API_KEY | No* | — | API key (overrides stored config) |
CONTENTSTUDIO_WORKSPACE_ID | No* | — | Active workspace (overrides stored config) |
CONTENTSTUDIO_BASE_URL | No | https://api.contentstudio.io/api/v1 | API base URL (override for staging) |
CONTENTSTUDIO_CONFIG_PATH | No | ~/.config/contentstudio/config.json | Custom config file path |
*Either run contentstudio auth:login once, or set CONTENTSTUDIO_API_KEY. Either run workspaces:use <id> once, set CONTENTSTUDIO_WORKSPACE_ID, or pass --workspace <id> per call.
The CLI provides typed errors with non-zero exit codes:
| Exit code | error.type | HTTP status | Typical cause / hint |
|---|---|---|---|
| 1 | ContentStudioError | varies | Generic — check message |
| 2 | AuthError | 401, 403 | Invalid/revoked key — run auth:login again |
| 3 | NotFoundError | 404 | Resource doesn't exist or wrong workspace |
| 4 | ValidationError | 422 | Malformed request — check message for field errors |
| 5 | RateLimitError | 429 | Too many calls — back off and retry |
| 6 | BackendError | 5xx / network | Upstream issue — retry with backoff |
| 7 | ConflictError | 409 | Resource already exists, or a send's delivery outcome is undetermined — verify before retrying |
| 8 | CreditLimitError | 403 | Out of AI image credits (images:*) — top up or wait for the cycle. Nothing was charged |
| 1 | ConfigError | — | Local config issue (no key/workspace set) — see hint |
The CLI auto-retries on 429 and 5xx (up to 2 attempts with exponential backoff). Connection timeouts also retry. The two AI image generation calls are the exception — they are billable and not idempotent, so they never auto-retry.
# Authentication
contentstudio auth:login --api-key cs_... # Persist key + verify
contentstudio auth:status # Show local config
contentstudio --json auth:whoami # Validate key against API
contentstudio auth:logout # Forget key
# Workspaces
contentstudio --json workspaces:list # List workspaces
contentstudio workspaces:use <workspace_id> # Set active workspace
contentstudio workspaces:current # Show active
# Discovery (workspace-scoped)
contentstudio --json accounts:list [--platform facebook] [--search "query"] # Connected accounts
contentstudio --json campaigns:list # Folders
contentstudio --json categories:list # Content categories
contentstudio --json labels:list # Labels
contentstudio --json team:list # Team members
contentstudio --json workspaces:limits # Plan limits + usage
# Content categories + posting slots
contentstudio --json categories:get <category_id> # Category (slots inlined)
contentstudio --json categories:create --name "Tips" --color color_3 --account <id> # Create
contentstudio --json categories:shuffle <category_id> # Re-deal upcoming posts
contentstudio --json category-slots:list <category_id> # Weekly slots
contentstudio --json category-slots:next <category_id> [--post-id <id>] # Next free slot
contentstudio --json category-slots:create <category_id> --day monday --hour 9 --minute 0 --period AM
# Approval workflows (all require the manage_workflow permission)
contentstudio --json approval-workflows:get <workflow_id> # Read one
contentstudio --json approval-workflows:create --name "Legal" --levels '<json>' # Create
contentstudio --json approval-workflows:update <id> --confirmed # Re-apply to in-flight posts
contentstudio --json approval-workflows:delete <id> [--force] # Delete
contentstudio --json approval-workflows:cascade-job <cascade_job_id> # Poll the cascade
# Planner share links (posts, not analytics)
contentstudio --json planner-share-links:list # List
contentstudio --json planner-share-links:create --name "Client Review" --plan <post_id>
contentstudio --json planner-share-links:send-invitations <id> --email [email protected] --approval-option anyone
contentstudio --json planner-share-links:activity <id> [--type comment] # Client comments/actions
# Posts
contentstudio --json posts:list [--status draft] [--date-from] [--date-to] # List posts
contentstudio --json posts:get <post_id> # Read one post
contentstudio --json posts:create -c "text" -i <account_id> -t draft # Create (shortcut)
contentstudio --json posts:create --body /path/to/post.json # Create (full body)
contentstudio --json posts:create [...] --dry-run # Preview, no API call
contentstudio --json posts:delete <post_id> [--delete-from-social] # Delete
contentstudio --json posts:approve <post_id> [--comment "..."] # Approve
contentstudio --json posts:reject <post_id> [--comment "..."] # Reject
# Best time to post
contentstudio --json scheduling:best-times # All connected accounts
contentstudio --json scheduling:best-times --account facebook:<account_id> # One account
contentstudio --json scheduling:best-times --global-slots 10 # More recommendations
# Comments / Notes
contentstudio --json comments:list <post_id> # List
contentstudio --json comments:add <post_id> "message" [--note] [--mention <id>] # Public comment / internal note
# Media
contentstudio --json media:list [--type images|videos] [--sort recent] # List
contentstudio --json media:upload --file <path> # Upload local file
contentstudio --json media:upload --url <url> # Import from URL
# AI images
contentstudio --json images:tools # Tools + their inputs
contentstudio --json images:models # Accepted models
contentstudio --json images:brand # Will --use-brand apply?
contentstudio --json images:generate -p "<prompt>" [--dimensions square_hd] # Prompt → image
contentstudio --json images:generate -p "<edit>" --image-url <url> # Edit an image
contentstudio --json images:product-image --product-image-url <url> # Restage a product
contentstudio --json images:headshot --image-url <url> # Headshot
contentstudio --json images:face-swap --target-image-url <url> --face-image-url <url>
contentstudio --json images:outfit-swap --target-image-url <url> --outfit-image-url <url>
contentstudio --json images:upscale --image-url <url> # Upscale
contentstudio --json images:remove-background --image-url <url> # Cut out subject
contentstudio --json images:tool <tool_key> --body '<json>' # Any tool, all controls
# Analytics (133 commands — one per endpoint; see SKILL.md#analytics for the full list)
contentstudio --json analytics:<platform>-<report> --platform-id <id> --start-date <d> --end-date <d>
contentstudio --json analytics:<platform>-single-post --platform-id <id> --post-id <native_id>
contentstudio --json analytics:meta-ads-summary --account-id <act_id> --start-date <d> --end-date <d>
contentstudio --json analytics:google-ads-ai-insights --account-id <id> --start-date <d> --end-date <d> --type aiInsightsDetailed
contentstudio analytics:<platform>-<report> --help # Exact options per command
# Analytics — Bluesky (10 commands) and Threads (16); --help lists each one's flags
contentstudio --json analytics:bluesky-summary --platform-id <did> \
--start-date 2026-08-01 --end-date 2026-08-31
contentstudio --json analytics:threads-top-posts --platform-id <id> \
--start-date 2026-08-01 --end-date 2026-08-31 --limit 10
contentstudio --json analytics:threads-demographics --platform-id <id> \
--start-date 2026-08-01 --end-date 2026-08-31 --breakdown age
# Analytics — reports (async: generate, then poll)
contentstudio --json reports:options # Report types + their sections
contentstudio reports:generate --name "Aug" --platform-type facebook \
--accounts <id> --date "2026-08-01 - 2026-08-31" # Returns an id immediately
contentstudio reports:generate --name "Rivals" --platform-type facebook_competitor \
--competitor-report-id <id> --date "2026-08-01 - 2026-08-31" # Competitor set, not accounts
contentstudio reports:get <report_id> --wait # Poll until ready, print download URL
contentstudio --json reports:list # Previously generated
contentstudio reports:retry <report_id> # Re-run a failed one
contentstudio reports:delete <report_id> # Remove
# Analytics — recurring schedules
contentstudio report-schedules:create --name "Monthly" --platform-type facebook \
--frequency monthly --accounts <id> --emails [email protected] # Provision once
contentstudio --json report-schedules:list # All schedules
contentstudio report-schedules:get <schedule_id> # Last run / next run
contentstudio report-schedules:pause <schedule_id> # Reversible
contentstudio report-schedules:resume <schedule_id>
contentstudio report-schedules:run <schedule_id> # Send one now
contentstudio report-schedules:delete <schedule_id>
# Analytics — client-facing share links
# NOTE: these currently 404 — there is no public-API route behind them (the
# endpoints are internal/JWT-only). Share an analytics dashboard from the web app.
contentstudio share-links:create --title "Q3" --platform instagram \
--account-id <id> --date-range "2026-07-01 - 2026-09-30" --password secret # Pinned + protected
contentstudio --json share-links:list
contentstudio share-links:get <id>
contentstudio share-links:disable <id> # Revoke without deleting
contentstudio share-links:enable <id>
contentstudio share-links:delete <id>
# Analytics — competitor benchmarking
contentstudio competitors:search "Nike" --platform-type facebook # Find a page to track
contentstudio competitor-reports:create --name "Rivals" --platform-type facebook \
--competitors "15087023444:Nike,763612290406925:Cheezious" # Saved set
contentstudio --json competitor-reports:list
contentstudio competitor-reports:get <report_id> # Per-competitor state
contentstudio competitor-reports:update <report_id> --name "Rivals" \
--platform-type facebook --competitors "..." # Replaces the whole set
contentstudio competitor-reports:delete <report_id>
contentstudio competitors:compare <report_id> --platform facebook \
--start-date 2026-08-01 --end-date 2026-08-31 # The comparison numbers
# AI Video
contentstudio --json ai-video:tools # Enabled tools
contentstudio --json ai-video:models # Selectable models
contentstudio --json ai-video:estimate [--duration 6] [--model <key>] # Credit/time estimate, no charge
contentstudio --json ai-video:generate --prompt "a cat riding a bike" --dry-run # Preview, no API call
contentstudio --json ai-video:generate --prompt "..." --image-url <url> # Image-to-video job
contentstudio --json ai-video:run-tool lip-sync --video-url <url> --audio-url <url> --dry-run
contentstudio --json ai-video:jobs [--status queued] # List submitted jobs
contentstudio --json ai-video:job <job_id> # Poll one job
contentstudio --json ai-video:cancel-job <job_id> # Cancel an active job
# Globals
contentstudio --version # Print version
contentstudio --help # Top-level help
contentstudio <group>:<verb> --help # Per-command help
contentstudio --json ... # JSON envelope output
contentstudio --workspace <id> ... # Per-call workspace override
contentstudio --base-url <url> ... # Per-call API base override
This package is built with TypeScript and bundled with tsup.
contentstudio-agent/
├── src/
│ ├── index.ts # CLI entry — yargs setup
│ ├── api.ts # HTTP client + endpoint wrappers
│ ├── config.ts # Persistent config (read/write/lock)
│ ├── errors.ts # Typed error hierarchy
│ ├── output.ts # JSON envelope + human renderer
│ ├── cliCtx.ts # Shared command glue (run, buildClient)
│ └── commands/
│ ├── auth.ts # auth:login, auth:logout, auth:whoami, auth:status
│ ├── workspaces.ts # workspaces:list, workspaces:use, workspaces:current
│ ├── lookups.ts # accounts/campaigns/categories/labels/team list commands
│ ├── contentCategories.ts # categories:* writes + category-slots:*
│ ├── approvalWorkflows.ts # approval-workflows:* writes + cascade-job polling
│ ├── plannerShareLinks.ts # planner-share-links:* (posts, not analytics)
│ ├── posts.ts # posts:list, posts:get, posts:create, posts:delete, posts:approve, posts:reject
│ ├── comments.ts # comments:list, comments:add
│ ├── media.ts # media:list, media:upload
│ └── images.ts # images:generate, images:tools/models/brand, one command per tool
├── tests/ # vitest + nock unit + real-API E2E
├── skills/contentstudio/SKILL.md # symlink → ../../SKILL.md
├── .claude-plugin/ # Claude Code plugin manifest
├── SKILL.md # AI-agent skill content
├── README.md # This file
├── CHANGELOG.md
├── LICENSE # MIT
├── package.json
├── tsconfig.json
└── tsup.config.ts
npm run dev # tsup --watch (rebuild on save)
npm run build # tsup → dist/index.js
npm run start # node dist/index.js
npm test # vitest run (unit + CLI subprocess; E2E auto-skipped)
npm run test:watch # vitest in watch mode
npm test. Uses nock to mock HTTPS. No network required.api.contentstudio.io for real:
CONTENTSTUDIO_API_KEY=cs_... \
CONTENTSTUDIO_WORKSPACE_ID=... \
npm test
E2E covers create-draft → delete cycles via both the API client and the installed CLI binary.tsup produces a single CommonJS bundle at dist/index.js (~43 KB) with #!/usr/bin/env node shebang. The npm package ships only dist/, README.md, SKILL.md, CHANGELOG.md, and LICENSE.
~/.config/contentstudio/config.json with mode 0600.auth:status).--json error envelope never includes the key.npm audit --omit=dev).mainnpm test — must stay green--first-comment--dry-run | Print the body that would be POSTed and exit (no API call) |
category-slots:list |
| POST | /workspaces/{w}/content-categories/{c}/slots | category-slots:create |
| GET | /workspaces/{w}/content-categories/{c}/slots/next | category-slots:next |
| PUT | /workspaces/{w}/content-categories/{c}/slots/{s} | category-slots:update |
| DELETE | /workspaces/{w}/content-categories/{c}/slots/{s} | category-slots:delete |
| GET | /workspaces/{w}/approval-workflows | approval-workflows:list |
| POST | /workspaces/{w}/approval-workflows | approval-workflows:create |
| GET | /workspaces/{w}/approval-workflows/{id} | approval-workflows:get |
| PUT | /workspaces/{w}/approval-workflows/{id} | approval-workflows:update |
| DELETE | /workspaces/{w}/approval-workflows/{id} | approval-workflows:delete |
| POST | /workspaces/{w}/approval-workflows/{id}/duplicate | approval-workflows:duplicate |
| PUT | /workspaces/{w}/approval-workflows/{id}/set-default | approval-workflows:set-default |
| PUT | /workspaces/{w}/approval-workflows/{id}/remove-default | approval-workflows:remove-default |
| GET | /workspaces/{w}/approval-workflows/cascade-jobs/{id} | approval-workflows:cascade-job |
| GET | /workspaces/{w}/share-links | planner-share-links:list |
| POST | /workspaces/{w}/share-links | planner-share-links:create |
| GET | /workspaces/{w}/share-links/{id} | planner-share-links:get |
| PUT | /workspaces/{w}/share-links/{id} | planner-share-links:update |
| DELETE | /workspaces/{w}/share-links/{id} | planner-share-links:delete |
| POST | /workspaces/{w}/share-links/{id}/send-invitations | planner-share-links:send-invitations |
| GET | /workspaces/{w}/share-links/{id}/activity | planner-share-links:activity |
| GET | /workspaces/{w}/labels | labels:list |
| GET | /workspaces/{w}/team-members | team:list |
| GET | /workspaces/{w}/media | media:list |
| POST | /workspaces/{w}/media | media:upload |
| GET | /workspaces/{w}/posts | posts:list |
| GET | /workspaces/{w}/posts/{p} | posts:get |
| POST | /workspaces/{w}/posts | posts:create |
| PUT | /workspaces/{w}/posts/{p} | posts:update |
| DELETE | /workspaces/{w}/posts/{p} | posts:delete |
| POST | /workspaces/{w}/posts/{p}/approval | posts:approve, posts:reject |
| POST | /workspaces/{w}/scheduling/optimal-times | scheduling:best-times |
| GET | /workspaces/{w}/ai/images/tools | images:tools |
| GET | /workspaces/{w}/ai/images/models | images:models |
| GET | /workspaces/{w}/ai/brand | images:brand |
| POST | /workspaces/{w}/ai/images/generate | images:generate |
| POST | /workspaces/{w}/ai/images/tools/{tool_key} | images:<tool>, images:tool <tool_key> |
| GET | /workspaces/{w}/posts/{p}/comments | comments:list |
| POST | /workspaces/{w}/posts/{p}/comments | comments:add |
| GET | /workspaces/{w}/ai/videos/tools | ai-video:tools |
| GET | /workspaces/{w}/ai/videos/models | ai-video:models |
| POST | /workspaces/{w}/ai/videos/estimate | ai-video:estimate |
| POST | /workspaces/{w}/ai/videos/generate | ai-video:generate |
| POST | /workspaces/{w}/ai/videos/tools/{tool_key} | ai-video:run-tool <tool_key> |
| GET | /workspaces/{w}/ai/jobs | ai-video:jobs |
| GET | /workspaces/{w}/ai/jobs/{job_id} | ai-video:job <job_id> |
| DELETE | /workspaces/{w}/ai/jobs/{job_id} | ai-video:cancel-job <job_id> |