# contentstudio-cli

> ContentStudio CLI — schedule social-media posts, generate AI images, manage the social inbox (DMs, comments, reviews), media, accounts, approvals, and analytics reports across Facebook, LinkedIn, Twitter/X, Instagram, YouTube, TikTok, Pinterest, Threads,

Latest version **1.6.0** (published 2026-10-13) · MIT license · 0 weekly downloads

## Install

```sh
npm install contentstudio-cli
pnpm add contentstudio-cli
yarn add contentstudio-cli
bun add contentstudio-cli
```

Provides the command `contentstudio`.

## Health

**Score 55/100 (C)** — status: active.

Positive: no vulnerabilities; recently updated; high maintenance score.

Warnings: low downloads; no types; no esm support.

## Facts

| | |
|---|---|
| Version | 1.6.0 |
| Published | 2026-10-13 |
| First published | 2026-04-29 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | none |
| Module format | CommonJS |
| Node | >=18.0.0 |
| Dependencies | 4 |
| Unpacked size | 1.6 MB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| GitHub stars | 6 |
| Author | ContentStudio |
| Maintainers | contentstudio |
| Keywords | contentstudio, cli, social-media, scheduling, automation, ai-agent, command-line, facebook, linkedin, twitter, instagram, tiktok, youtube, pinterest, gmb, threads, tumblr, bluesky, inbox, social-inbox, dm, comments, ai-image-generation, image-generation |

## Links

- npm: https://www.npmjs.com/package/contentstudio-cli
- Repository: https://github.com/contentstudioio/contentstudio-agent
- Homepage: https://api.contentstudio.io/guide
- Issues: https://github.com/contentstudioio/contentstudio-agent/issues
- npm.io page: https://npm.io/package/contentstudio-cli

## Dependencies (4)

- [chalk](https://npm.io/package/chalk.md) ^4.1.2
- [yargs](https://npm.io/package/yargs.md) ^17.7.2
- [form-data](https://npm.io/package/form-data.md) ^4.0.1
- [node-fetch](https://npm.io/package/node-fetch.md) ^2.7.0

## Alternatives

- [@salesforce/cli](https://npm.io/package/@salesforce/cli.md) — 389.7K weekly downloads
- [@mintlify/cli](https://npm.io/package/@mintlify/cli.md) — 208.9K weekly downloads
- [@grafana/e2e-selectors](https://npm.io/package/@grafana/e2e-selectors.md) — 128.7K weekly downloads
- [mintlify](https://npm.io/package/mintlify.md) — 112.0K weekly downloads
- [@intlayer/cli](https://npm.io/package/@intlayer/cli.md) — 22.8K weekly downloads

## Recent versions

- 1.6.0 (latest) — 2026-10-13
- 1.5.0 — 2026-09-14
- 1.4.1 — 2026-09-09
- 1.4.0 — 2026-09-08
- 1.3.0 — 2026-09-08
- 1.2.0 — 2026-08-17
- 1.1.1 — 2026-08-06
- 1.1.0 — 2026-08-05
- 1.0.12 — 2026-08-05
- 1.0.11 — 2026-07-20
- 1.0.10 — 2026-06-30
- 1.0.9 — 2026-06-30
- 1.0.8 — 2026-06-30
- 1.0.7 — 2026-05-15
- 1.0.6 — 2026-05-05
- … 6 more at https://npm.io/package/contentstudio-cli/versions

## README

# contentstudio-cli

[![npm version](https://img.shields.io/npm/v/contentstudio-cli.svg)](https://www.npmjs.com/package/contentstudio-cli)
[![license](https://img.shields.io/npm/l/contentstudio-cli.svg)](./LICENSE)

**Install as a skill:**
```bash
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](https://contentstudio.io) 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.

## Why use this CLI

- **Drive ContentStudio from anywhere** — bash scripts, CI/CD pipelines, AI agents (Claude Code, Cursor, OpenCode, Codex), n8n workflows, custom automations.
- **JSON output for agents** — every command supports `--json` returning a stable `{"ok": true, "data": ...}` envelope.
- **Dry-run safety** — preview every mutating call before sending it, so AI agents (and humans) never publish by accident.
- **No SaaS lock-in to your CLI tooling** — talks directly to the production ContentStudio API over HTTPS; no proxy, no extra service.

## Installation

### From npm (recommended)

```bash
npm install -g contentstudio-cli
# or
pnpm install -g contentstudio-cli
```

Verify:
```bash
contentstudio --version
contentstudio --help
```

### Install the skill (for AI agents)

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:

```bash
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`).

### Cursor and Grok Bot plugin

This repo ships a [Cursor plugin](https://cursor.com/docs/reference/plugins) 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):**

  ```bash
  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**.

### Grok Build plugin

This repo also has a `.grok-plugin/plugin.json` manifest with the same skill and hosted MCP server, for the [xAI plugin marketplace](https://github.com/xai-org/plugin-marketplace).

### Endpoints and permissions

- `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`.
- The plugin stores no API key. Some tools publish, delete, or approve posts, so review each write action before you approve it.

> `skills/contentstudio/SKILL.md` is a copy of the root `SKILL.md` (plugin marketplaces don't follow symlinks). After editing `SKILL.md`, run `cp SKILL.md skills/contentstudio/SKILL.md`. CI checks this.

## Authentication

Authentication uses an **API key** issued from your ContentStudio dashboard.

### Option 1: `auth:login` (persists to local config)

```bash
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.

```bash
# 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
```

### Option 2: Environment variables

For CI/CD or one-off invocations, set the key in your environment instead of persisting:

```bash
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.

### Where to get an API key

ContentStudio Dashboard → **Settings → API Keys → Generate new key**.

## Quick Start

```bash
# 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
```

## Discovery & Lookup

### List your workspaces
```bash
contentstudio --json workspaces:list
contentstudio --json workspaces:list --per-page 50
```
Returns workspace IDs, names, slugs, timezones.

### Show / change the active workspace
```bash
contentstudio workspaces:current
contentstudio workspaces:use <workspace_id>
```

### List connected social accounts
```bash
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`.

### Look up campaigns, labels, categories, team members
```bash
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`.

## Connecting Social Accounts

Three ways to add new accounts to a workspace, depending on the platform.

### List which platforms are connectable

```bash
contentstudio --json platforms:list
```

Returns all 12+ supported platforms with their `connection_method` (`oauth`, `credentials`, or `manual`) and the endpoint to call.

### OAuth platforms (Facebook, LinkedIn, Twitter, Instagram, YouTube, TikTok, Pinterest, GMB, Threads, Tumblr)

```bash
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:
```bash
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`.

### Bluesky (credential-based, no browser)

Generate an app password at <https://bsky.app/settings/app-passwords> first, then:

```bash
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.

### Facebook Groups (manual)

```bash
contentstudio --json accounts:add-facebook-group \
  --name "My Community Group" \
  --image https://example.com/group-cover.jpg
```

The image URL is optional.

### Remove (disconnect) an account

```bash
# 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.

## Creating Posts

There are two ways to create a post: **shortcut flags** for simple cases, or **`--body <file.json>`** for the full schema.

### Shortcut flags (simple posts)

```bash
# 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 `--first-comment` is set. |
| `--dry-run` | Print the body that would be POSTed and exit (no API call) |

### Multi-account post

Repeat `-i` for each account:
```bash
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"
```

### Use existing media library assets

```bash
# 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>
```

### Queued post (added to the publishing queue; no explicit time)

```bash
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.

### Content-category post (accounts come from the category — no `-i`)

```bash
# 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>
```

### Facebook carousel (2–10 cards, Facebook accounts only)

```bash
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.

### Threads multi-thread (chained, max 10 items, Threads accounts only)

```bash
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.

### Twitter/X threaded tweets (chained, max 10 tweets, Twitter accounts only)

```bash
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.

### Per-platform content overrides (`--platform-overrides`)

Publish the same post to several platforms but swap the caption, post type, or media for one of them:

```bash
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.

### Post with a first comment

```bash
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.

### Full body via `--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`:

```bash
contentstudio --json posts:create --body /tmp/post.json
```

Body schema:
```jsonc
{
  "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://..." }
}
```

### Repeating posts

```bash
# 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.

### Always preview with `--dry-run` first

For agents (and cautious humans), every mutating command supports `--dry-run` — it prints the request body and exits **without** calling the API:

```bash
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": {...}}}
```

## Best Time to Post

`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.

```bash
# 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:

```json
{
  "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:

```bash
# 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.

## Managing Posts

### List posts (with filters)

```bash
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
```

### Read one post

```bash
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**.

### Update a post

`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`.

```bash
# 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"}}'
```

### Delete a post

```bash
# 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
```

### Approve / reject a post in an approval workflow

```bash
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
```

## Comments & Internal Notes

```bash
# 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.

## Social Inbox

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`.

### Browse and search

```bash
# 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.

### Read a thread

```bash
# 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>
```

### Reply

These reach real customers. Preview with `--dry-run` first.

```bash
# 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.

### Triage

```bash
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>
```

### Moderate

```bash
# 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>
```

### Tags

```bash
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
```

### Updating contact details

```bash
contentstudio --json inbox:contact-update <element_ref> \
  --platform-id <account_id> \
  --name "Jane Doe" --email jane@example.com --company "Acme"
```

## Media Library

### List media assets

```bash
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 media

Upload a local file:
```bash
contentstudio --json media:upload --file ./hero.jpg
```

Or import from an external URL:
```bash
contentstudio --json media:upload --url https://example.com/asset.mp4
```

Optionally place into a folder:
```bash
contentstudio --json media:upload --file ./hero.jpg --folder-id <folder_id>
```

Preview (no upload):
```bash
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.

## Analytics

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](./SKILL.md#analytics).

```bash
# 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.

## AI Images

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.

### Discover what is available

```bash
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.

### Generate

```bash
# 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 |

### Generate, then publish

```bash
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.

### The dedicated tools

```bash
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`.

### Any tool, every control

`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:

```bash
contentstudio --json images:tool image-to-image --body '{
  "prompt": "same mug, editorial magazine styling",
  "attachments": ["https://example.com/mug.png"],
  "aspect_ratio": "4:5"
}'
```

### The response

```json
{
  "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.
- **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.
- **`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.

### Input URLs

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:

```bash
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.

### Timeouts and retries

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.

### Errors

| `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`.

## Platform-Specific Examples

The full body schema accepts platform-specific options. These examples show the most common configurations.

### Facebook Page

```bash
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`:
```bash
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
```

### LinkedIn (personal or company page)

```bash
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
```

### Twitter / X

```bash
# 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
```

### Instagram (feed / reel / story)

For Instagram, control the post format with `--post-type`:

```bash
# 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 (Shorts and Videos)

YouTube needs `youtube_options` — use a `--body` file:

```bash
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
```

### TikTok

```bash
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
```

### Pinterest

```bash
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
```

### Google Business Profile

```bash
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
```

## Features for AI Agents

This CLI is designed to be driven by AI assistants. Three properties make it agent-friendly:

### 1. Stable JSON envelope

Every command supports `--json` returning a predictable shape:

```jsonc
// 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).

### 2. Dry-run by default for safety

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.

### 3. Discoverable via `npx skills add`

The repo ships a `SKILL.md` agents can install with one command:
```bash
npx skills add contentstudioio/contentstudio-agent
```
After this, the agent automatically knows when to use the `contentstudio` CLI without prompting.

## Common Workflows

### 1. Schedule a daily post for the next 7 days

```bash
#!/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
```

### 2. Cross-platform campaign

```bash
#!/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
```

### 3. Bulk-delete drafts older than 30 days

```bash
#!/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
```

### 4. Upload a folder of images and create one post per image

```bash
#!/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
```

### 5. Approval pipeline — auto-approve posts from a trusted creator

```bash
#!/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
```

## API Endpoints

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` | `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>` |

Full OpenAPI 3.0 spec: <https://api.contentstudio.io/api-docs.json>
Human-readable docs: <https://api.contentstudio.io/guide>

## Configuration

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:
```jsonc
{
  "api_key": "cs_...",
  "base_url": "https://api.contentstudio.io/api/v1",
  "active_workspace_id": "<workspace_id>",
  "active_workspace_name": "...",
  "user": { "id": "...", "email": "...", "full_name": "..." }
}
```

## Environment Variables

| 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.

## Error Handling

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.

## Quick Reference

```bash
# 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 a@b.c --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 a@b.com                              # 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
```

## Development

This package is built with TypeScript and bundled with [tsup](https://tsup.egoist.dev/).

### Project structure

```
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
```

### Scripts

```bash
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
```

### Tests

- **Unit + CLI subprocess** — runs by default with `npm test`. Uses `nock` to mock HTTPS. **No network required.**
- **Real-API E2E** — gated on env vars. Hits `api.contentstudio.io` for real:
  ```bash
  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.

### Build output

`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`.

## Security

- API keys live in `~/.config/contentstudio/config.json` with mode `0600`.
- Keys are never echoed in CLI output (only a redacted prefix via `auth:status`).
- The `--json` error envelope never includes the key.
- All API traffic is HTTPS; the client validates TLS certificates.
- 0 production dependency vulnerabilities (`npm audit --omit=dev`).

## Contributing

1. Fork the repo at <https://github.com/contentstudioio/contentstudio-agent>
2. Branch off `main`
3. Make your changes; add/update tests
4. `npm test` — must stay green
5. Open a pull request

## Links

- **npm**: <https://www.npmjs.com/package/contentstudio-cli>
- **GitHub**: <https://github.com/contentstudioio/contentstudio-agent>
- **API guide**: <https://api.contentstudio.io/guide>
- **OpenAPI spec**: <https://api.contentstudio.io/api-docs>
- **ContentStudio**: <https://contentstudio.io>

## License

[MIT](./LICENSE)

---
_Source: https://npm.io/package/contentstudio-cli · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
