# CLI reference

Inspect destination contracts, manage drafts and media, and control publication state.

URL: /docs/cli-reference

The Bun CLI uses the existing `@postdot/client` Eden Treaty factory. Request types
come from the Elysia application; no separate REST schema or generated SDK is
maintained. Eden Fetch offers a different typed calling style, and OpenAPI remains
available to external consumers. Neither requires a second transport in this CLI.
See [Eden Treaty](https://elysiajs.com/eden/treaty/overview),
[Eden Fetch](https://elysiajs.com/eden/fetch), and
[OpenAPI](https://elysiajs.com/patterns/openapi).

Use `POSTBOT_URL` and `POSTBOT_API_KEY` for machine authentication. Connections and
provider credentials stay in Postdot. Existing project roles and API-key action
ceilings apply; scheduling requires the corresponding dispatch permission.

## Create a source and its destinations [#create-a-source-and-its-destinations]

```json
{
    "title": "Release announcement",
    "sourceContent": {
        "body": { "text": "The release is ready.", "format": "plain_text" },
        "media": []
    },
    "publications": [
        {
            "channelId": "YOUR_CHANNEL_ID",
            "deliveryMode": "manual",
            "targetContent": { "body": "The release is ready.", "media": [] }
        }
    ]
}
```

```sh
bun run postdot posts create --project "$PROJECT_ID" --input post.json --json
```

`--input -` reads stdin. Inputs are bounded to 1 MiB and must contain a JSON object.
Elysia performs authoritative schema and domain validation. Destination content
must match the channel profile, which can be inspected with `channels list` or the
existing API. The example is for a manual message channel, not a universal payload
for every platform. `scheduledAt`, when supplied per destination, is an epoch
millisecond timestamp; omission creates an unscheduled publication.

## Inspect and schedule [#inspect-and-schedule]

```sh
bun run postdot publications list --project "$PROJECT_ID" --json
bun run postdot publications show --project "$PROJECT_ID" --publication "$PUBLICATION_ID" --json
bun run postdot publications promote --project "$PROJECT_ID" --publication "$PUBLICATION_ID" --expected-revision 1 --at "$EPOCH_MS" --json
bun run postdot publications schedule --project "$PROJECT_ID" --publications "$PUBLICATION_ID" --at "$LATER_EPOCH_MS" --json
bun run postdot publications cancel --project "$PROJECT_ID" --publications "$PUBLICATION_ID" --json
```

`list` returns one page of at most 100 publications plus `nextCursor`; pass
`--cursor` for another page. It never silently loops over the entire project.
`show` returns the editor state including source, draft, target, and lifecycle.
`promote` requires the inspected draft revision and creates the execution target.
`schedule` changes queued publications; it does not substitute for draft promotion.
`cancel` and `schedule` accept comma-separated publication IDs under the existing
server batch limit. `publications save --input draft.json` forwards the draft PATCH
contract, including `expectedRevision`, optional `sourceContent`, and optional
`targetContent`. A stale revision is refused rather than overwritten.

`publications retry --publication <id>` requests the existing retry of a failed
automatic publication. It cannot retry arbitrary states. The existing
`publications publish` command still marks a due **manual** publication as
published; it does not mean automatic dispatch. Existing reconciliation commands
remain available for ambiguous outcomes.

## Output and failures [#output-and-failures]

With `--json`, success writes the API JSON to stdout. Failure writes one
`{"error":{"code":"...","message":"...","status":409}}` object to stderr;
`status` is present only when an HTTP response was received. Exit 0 is success,
1 is an API/transport/command failure, and 2 is invalid local input. Human mode
uses concise confirmations; inspection commands display formatted JSON.

Requests have a 30-second transport deadline by default; `--timeout-ms` sets a different positive per-request deadline (useful for larger uploads). The CLI never automatically retries
writes. A transport failure has code `transport_error`; the write may already
have succeeded on the server. Inspect state before submitting it again. This
first tranche does not add idempotency keys or promise exactly-once submission.

## Import and inspect media [#import-and-inspect-media]

```sh
bun run postdot media policy --project "$PROJECT_ID" --json
bun run postdot media upload --project "$PROJECT_ID" --file image.png --alt "Release dashboard" --json
bun run postdot media import-url --project "$PROJECT_ID" --url "https://YOUR_HOST/image.png" --json
bun run postdot media show --project "$PROJECT_ID" --asset "$ASSET_ID" --json
bun run postdot media list --project "$PROJECT_ID" --json
bun run postdot media finalize --project "$PROJECT_ID" --intent "$INTENT_ID" --json
```

File upload checks the current server policy, reserves an asset, streams the raw
file bytes, and asks the server to inspect/finalize them. `--content-type` overrides
the filename-derived hint when necessary; it never bypasses inspection or converts
the file. The upload size limit comes from Postdot, not the 1 MiB JSON-input limit.
Use `--timeout-ms 120000` when a transfer needs a longer deadline.

URL import uses Postdot's existing server importer: it downloads and stores a copy,
not a permanent external reference. URL/network restrictions, supported formats,
and image/video inspection remain server-owned. MP4 files must satisfy Postdot's
existing ready-to-publish video policy; no conversion is added.

Exit 0 acknowledges a successful API operation, not necessarily a ready asset.
Check `status`: `ready` includes `media.asset.id` and representations; `pending`
includes the reservation and intent ID. `failed` exits 1 with
`media_inspection_failed`. To attach a ready asset, use its ID in source media and
the destination's declared media field, for example `[{"assetId":"..."}]` for a
manual message channel. Do not attach pending or failed assets.

The CLI does not poll. Inspect with `media show`, or explicitly finalize a known
intent again when appropriate. A transfer failure after reservation includes
`error.media.assetId` and `error.media.intentId` in JSON stderr. Use them to inspect
state before retrying; an unknown result is not proof that the bytes were lost.
No automatic repeat or deletion is performed. A URL request whose response was
lost may not expose an asset ID; inspect the library before importing it again.
`media list` exposes the server cursor rather than silently downloading all pages.

## Discover a destination contract [#discover-a-destination-contract]

Before constructing a destination payload, inspect its channel profile:

```sh
bun scripts/postbot.ts channels list --project <project-id> --json
bun scripts/postbot.ts channels profile --project <project-id> --channel <channel-id> --json
```

The response is the existing API contract: `channel`, `active`, and nullable
`draft`. Read the definition under `active.profile` for schema, declared bindings,
text capabilities, and media requirements. The active version is distinct from an
profile draft awaiting activation. Keep the version reference when preparing a payload;
server validation and promotion remain authoritative if the channel changes.

Both output modes display the full profile response, rather than guessing field
names or flattening away constraints. This command neither activates a profile nor
connects a publisher. A valid content contract does not establish provider
connection readiness or guarantee publication success.

## Inspect publisher connections [#inspect-publisher-connections]

```sh
bun scripts/postbot.ts publishers list --project <project-id> --json
```

The response preserves the existing `connections` and `oauthReadiness` fields.
Each connection identifies its channel, provider, account, scopes, and recorded
state (`active`, `reconnect_required`, or `revoked`). Credentials are referenced
by ID; access tokens and refresh tokens are not returned. An empty connection
list is valid, including for projects using manual channels.

`oauthReadiness` describes application configuration for LinkedIn, Bluesky, and X:
`ready`, `not_configured`, or `unavailable`. It is not a live provider health check,
and does not describe every provider. An active connection plus ready application
configuration does not guarantee a successful delivery. This read command neither
connects a provider nor sends a test publication.

