inklate
v0.1.0
Published
Inklate CLI — publish, schedule, preflight, and measure social posts from the terminal. `inklate mcp` runs the same binary as a local stdio MCP server.
Maintainers
Readme
Inklate
The Inklate CLI — create, schedule, validate, and measure social posts from the terminal. The same binary is a local stdio MCP server (inklate mcp).
npm i -g inklate
inklate auth login # device flow → org-pinned API key
inklate channels connect x # browser OAuth, destinations in the terminal
inklate posts create --text "hello" --channels x --scheduled-at now
inklate posts publish <post-id> --wait- Thin client, structurally. Every command names a REST operation and the client resolves its verb and path from a table generated off the server's endpoint descriptors — so a command cannot address a route that does not exist.
- Agent-grade output. Results as YAML on stdout (
--jsonfor JSON); progress, prompts, and errors on stderr; typed exit codes. - Interactive for humans, never a trap for agents. Prompts only on a TTY without
CI/--no-input; otherwise missing flags are named in a usage error and destructive operations require--yes. - Rate limits handled. A
429is retried after the server'sRetry-After; a limit that survives the retries exits7, the one code that means "this will work later".
Install
npm i -g inklate # or: bunx inklate, pnpm dlx inklate
inklate --helpRequires Node 20+.
Authentication
inklate auth login # device flow: approve a short code in the browser
inklate auth check # who am I, and which organizations can I act in?auth login prints a short user code, opens the browser to approve it, and stores an organization API key (prefix inklate_) in ~/.config/inklate/config.json at mode 0600. The key is org-pinned and revocable under Settings → API keys.
For CI and agents, skip the browser entirely:
export INKLATE_API_KEY=inklate_... # or: inklate auth set --token inklate_...Several credentials live side by side as named profiles: inklate --profile staging auth login, then --profile staging on any command.
Output contract
inklate posts list # YAML on stdout
inklate posts list --json | jq . # JSON on stdout
inklate posts list --quiet # no progress chatter on stderrstdout carries the result and nothing else. stderr carries progress, prompts, and errors. That split is what makes every command pipeable.
Exit codes
| Code | Name | Meaning |
| --- | --- | --- |
| 0 | ok | The command succeeded. |
| 1 | unexpected | An unhandled or transport-level failure — retry or report it. |
| 2 | usage | Bad or missing arguments. Nothing was sent. |
| 3 | auth | No stored credential, or it was rejected. Run inklate auth login. |
| 4 | refusal | The platform refused on purpose — plan limit, permission, or a preflight verdict. The machine-readable refusal is on stdout. |
| 5 | not found | The addressed resource does not exist. |
| 6 | conflict | The resource changed underneath you, or its state forbids the action. |
| 7 | rate limited | The organization's request budget is exhausted after the client already waited out Retry-After. Safe to re-run later — the message names the wait. |
Exit 4 is not a crash — it is the platform refusing on purpose (a plan limit, a permission, a post that is not ready), and the machine-readable refusal is on stdout for the caller to act on:
inklate posts validate --id <post-id> --json || echo "refused with $?"Global flags
Accepted by every command:
| Flag | Description |
| --- | --- |
| --org <value> | Organization slug to act in |
| --profile <value> | Named credentials profile (default: default) |
| --json | JSON output instead of YAML |
| --quiet | Suppress progress output on stderr |
| --yes | Skip confirmation prompts |
| --no-input | Never prompt (fail instead) |
| --base-url <value> | Override the API base URL |
Scheduling times
Scheduling flags take ISO-8601 with an offset, or a relative shorthand anchored to the server clock:
inklate posts update <id> --scheduled-at 2026-07-21T09:00:00-07:00
inklate posts update <id> --scheduled-at 2h # also: 30m, 3d, 1w, nowA naive local time (2026-07-21T09:00) is rejected with a corrective error — "9am" must never be guessed server-side.
Interactivity
On a TTY, missing arguments prompt (channel pickers come from your real channel list) and destructive commands confirm. In scripts, agents, and CI — no TTY, CI set, or --no-input — the CLI never prompts: it fails naming the exact flag, and destructive commands require --yes.
inklate mcp
Runs the same binary as a local stdio MCP server bridged to your organization. Every remote tool is forwarded unchanged, and files_upload additionally accepts local file paths (the bridge uploads the bytes and returns file ids):
{ "command": "inklate", "args": ["mcp"] }Point Claude Code, Cursor, or any stdio MCP client at that.
Escape hatch
Any REST endpoint is callable before it has a dedicated command:
inklate api GET /posts
inklate api POST /posts --field content=hello --field 'channel_ids=["<channel-id>"]'
inklate api POST /posts --input body.jsonCommand reference
auth — Log in and manage stored credentials
inklate auth login
Log in via the browser (device flow) and store an organization API key
inklate auth login [flags]| Name | Kind | Description |
| --- | --- | --- |
| --no-browser | flag | Print the URL without opening a browser |
REST: startDeviceAuthorization, pollDeviceAuthorization
inklate auth set
Store an organization API key (paste flow — works everywhere, incl. CI)
inklate auth set [flags]| Name | Kind | Description |
| --- | --- | --- |
| --token <value> | flag | The inklate_… API key; prompted for when omitted |
inklate auth check
Verify the stored credentials and show reachable organizations
inklate auth checkinklate auth logout
Remove the stored credentials for this profile
inklate auth logoutorganizations — Organizations you can act in
inklate organizations list
List organizations you can act in
inklate organizations listREST: organizationsList
channels — Connected social channels, their metrics, and growth
inklate channels list
List publishable channels + connection health
inklate channels listREST: channelsList
inklate channels creator-policy
What a channel's creator account currently allows (TikTok)
inklate channels creator-policy <channel>| Name | Kind | Description |
| --- | --- | --- |
| channel | argument | Channel id (or provider name when unambiguous) |
REST: channelsCreatorPolicy
inklate channels connect
Connect a channel: browser OAuth consent, then destination picking
inklate channels connect <provider> [flags]| Name | Kind | Description |
| --- | --- | --- |
| provider | argument | Provider key (the connectable set is listed by inklate capabilities list) |
| --destinations <value> | flag | Which destinations to connect: "all", or comma-separated external ids |
| --no-browser | flag | Print the URL without opening a browser |
| --timeout <value> | flag | Seconds to wait |
REST: connectionsCreate, connectionsCreateWithCredentials, connectionsGet, connectionsDestinations, connectionsSelect
inklate channels disconnect
Disconnect a channel (its scheduled placements stop publishing)
inklate channels disconnect <id>| Name | Kind | Description |
| --- | --- | --- |
| id | argument | Channel id |
REST: channelsDisconnect
inklate channels metrics
Aggregated per-day engagement for one channel
inklate channels metrics <id> [flags]| Name | Kind | Description |
| --- | --- | --- |
| id | argument | Channel id (or provider name when unambiguous) |
| --from <value> | flag | ISO-8601 with offset |
| --to <value> | flag | ISO-8601 with offset |
REST: channelsMetrics
inklate channels growth
Followers trajectory + per-day page flows for one channel
inklate channels growth <id> [flags]| Name | Kind | Description |
| --- | --- | --- |
| id | argument | Channel id (or provider name when unambiguous) |
| --from <value> | flag | ISO-8601 with offset |
| --to <value> | flag | ISO-8601 with offset |
REST: channelsGrowth
capabilities — Per-channel capabilities + provider manifests
inklate capabilities
Per-channel effective capabilities + provider manifests
inklate capabilitiesREST: capabilitiesList
posts — Create, validate, schedule, publish, and measure posts
inklate posts create
Create a post — draft, scheduled, or published now, in one call
inklate posts create [flags]| Name | Kind | Description |
| --- | --- | --- |
| --content <value> | flag | Post copy as markdown. Text with no markup posts exactly as typed; on a thread each PARAGRAPH becomes an item. Use * for a literal delimiter. |
| --channel <value> | flag | Repeatable: channel id or provider name (x, linkedin, …) (repeatable) |
| --scheduled-at <value> | flag | Schedule: "now", ISO-8601 with offset, or "30m"/"2h"/"3d"/"1w"; omit to save a draft |
| --per-channel <value> | flag | Per-channel overrides as JSON: {"x": {"content"?, "format"?, "options"?}}. content takes markdown: --per-channel '{"x": {"content": "shorter text"}}' |
| --autofix | flag | Apply suggested machine fixes server-side (--no-autofix to disable) |
| --media <value> | flag | Repeatable: file id (file_…), public https URL, or local file path (repeatable) |
| --label <value> | flag | Repeatable: label id to apply (repeatable) |
| --campaign <value> | flag | Campaign id to attach |
| --idempotency-key <value> | flag | Dedupe key: retries replay, not duplicate |
REST: postsCreate
inklate posts list
List posts with their per-channel status
inklate posts list [flags]| Name | Kind | Description |
| --- | --- | --- |
| --status <value> | flag | Repeatable status filter (repeatable) |
| --scheduled-from <value> | flag | ISO-8601 with offset |
| --scheduled-to <value> | flag | ISO-8601 with offset |
| --campaign <value> | flag | Only posts filed under this campaign id |
| --label <value> | flag | Only posts carrying this label id |
| --channel <value> | flag | Only posts targeting this channel — id or provider name (x, linkedin) |
| --include <value> | flag | Repeatable: extra per-row field to compute — ready fit-checks each row (repeatable) |
| --limit <value> | flag | Rows per page (default 100) |
| --cursor <value> | flag | next_cursor from a prior page |
REST: postsList
inklate posts get
The post, its per-channel validation, and live publish results
inklate posts get <id>| Name | Kind | Description |
| --- | --- | --- |
| id | argument | Post id |
REST: postsGet
inklate posts update
Change anything — content, channels, media, labels, campaign, or the schedule
inklate posts update <id> [flags]| Name | Kind | Description |
| --- | --- | --- |
| id | argument | Post id |
| --content <value> | flag | New copy as markdown (replaces the current copy) |
| --channel <value> | flag | Repeatable: channel id or provider name — REPLACES the target set (repeatable) |
| --scheduled-at <value> | flag | (Re)schedule: "now", ISO-8601 with offset, or "30m"/"2h"/"3d"/"1w" |
| --clear-schedule | flag | Return the post to draft |
| --per-channel <value> | flag | Per-channel overrides as JSON; content takes markdown; {"x": null} resets that channel to the shared content |
| --autofix | flag | Apply suggested machine fixes server-side (--no-autofix to disable) |
| --media <value> | flag | File ids, https URLs, or local paths (repeatable) |
| --label <value> | flag | Repeatable: replace the post's labels (repeatable) |
| --campaign <value> | flag | Campaign id to attach |
| --detach-campaign | flag | Detach the post from its campaign |
| --expected-updated-at <value> | flag | Optimistic-concurrency guard from the last read |
| --idempotency-key <value> | flag | Dedupe key: retries replay, not duplicate |
REST: postsUpdate
inklate posts validate
Dry-run validation — exit 4 when the post is not ready, nothing stored
inklate posts validate [id] [flags]| Name | Kind | Description |
| --- | --- | --- |
| id | argument | Validate a STORED post (media readiness included) (optional) |
| --content <value> | flag | Inline draft copy as markdown |
| --channel <value> | flag | Repeatable: channel id or provider name to fit-check against (repeatable) |
| --per-channel <value> | flag | Per-channel overrides as JSON |
| --media <value> | flag | File ids or https URLs (no local paths) (repeatable) |
REST: postsValidate
inklate posts delete
Delete a post (--takedown also removes it from the platforms)
inklate posts delete <id> [flags]| Name | Kind | Description |
| --- | --- | --- |
| id | argument | Post id |
| --takedown | flag | Best-effort delete the provider-side copies first |
REST: postsDelete
inklate posts publish
Publish an existing draft now (--wait polls to a terminal state)
inklate posts publish <id> [flags]| Name | Kind | Description |
| --- | --- | --- |
| id | argument | Post id |
| --wait | flag | Poll until published/failed |
| --expected-updated-at <value> | flag | Refuse (exit 6) if the post changed since this read, from posts get |
| --idempotency-key <value> | flag | Dedupe key: retries replay, not republish |
REST: postsPublish
inklate posts metrics
Per-channel totals + snapshot time series for one post
inklate posts metrics <id>| Name | Kind | Description |
| --- | --- | --- |
| id | argument | Post id |
REST: postsMetrics
files — Upload and manage the files a post attaches
inklate files upload
Upload local files (presign → PUT → poll) and print reusable file ids
inklate files upload <paths> [flags]| Name | Kind | Description |
| --- | --- | --- |
| paths | argument | 1–10 local files (jpg/jpeg/png/gif/webp/mp4/mov) |
| --idempotency-key <value> | flag | Dedupe key for the whole batch |
| --no-wait | flag | Return after the PUTs without polling readiness |
REST: filesUpload, filesList
inklate files get
Get a file (readiness, facts, variants; optionally a signed URL)
inklate files get <id> [flags]| Name | Kind | Description |
| --- | --- | --- |
| id | argument | File id (file_…) |
| --include-url | flag | Include a signed GET URL (600s) |
| --variant <value> | flag | Signed URL for a named variant |
REST: filesGet
inklate files list
List files (keyset-paginated, newest first)
inklate files list [flags]| Name | Kind | Description |
| --- | --- | --- |
| --readiness <value> | flag | Filter |
| --kind <value> | flag | Filter |
| --limit <value> | flag | |
| --cursor <value> | flag | next_cursor from the previous page |
REST: filesList
inklate files delete
Delete a file (refused while attached to a post)
inklate files delete <id>| Name | Kind | Description |
| --- | --- | --- |
| id | argument | File id (file_…) |
REST: filesDelete
search — Full-text search over your content
inklate search
Full-text search over your own content
inklate search <query> [flags]| Name | Kind | Description |
| --- | --- | --- |
| query | argument | Search query |
| --kinds <value> | flag | Comma-separated kinds filter |
| --limit <value> | flag | |
| --cursor <value> | flag | |
REST: searchContent
interactions — Ingested comments and mentions
inklate interactions list
Ingested comments and mentions across channels
inklate interactions list [flags]| Name | Kind | Description |
| --- | --- | --- |
| --kinds <value> | flag | Comma-separated kinds (comment, mention, …) |
| --channel <value> | flag | Channel id (or provider name when unambiguous) |
| --since <value> | flag | ISO-8601 with offset — only newer interactions |
| --limit <value> | flag | |
REST: interactionsList
inklate interactions reply
Reply to an ingested comment or mention (1 Ink)
inklate interactions reply <id> [flags]| Name | Kind | Description |
| --- | --- | --- |
| id | argument | Interaction id from inklate interactions list |
| --body <value> | flag | The reply text, posted as the channel (required) |
REST: interactionsReply
webhooks — Webhook endpoints and deliveries
inklate webhooks list
List webhook endpoints
inklate webhooks listREST: webhooksList
inklate webhooks create
Register a webhook endpoint (the signing secret is shown once)
inklate webhooks create <url> [flags]| Name | Kind | Description |
| --- | --- | --- |
| url | argument | HTTPS endpoint to deliver events to |
| --event-types <value> | flag | Comma-separated event types, e.g. "post.published,token.expired" (required) |
| --description <value> | flag | What this endpoint is for |
REST: webhooksCreate
inklate webhooks delete
Delete a webhook endpoint
inklate webhooks delete <id>| Name | Kind | Description |
| --- | --- | --- |
| id | argument | Webhook endpoint id |
REST: webhooksDelete
inklate webhooks rotate-secret
Rotate a webhook signing secret (the old secret stops verifying)
inklate webhooks rotate-secret <id>| Name | Kind | Description |
| --- | --- | --- |
| id | argument | Webhook endpoint id |
REST: webhooksRotateSecret
inklate webhooks deliveries
Recent deliveries for a webhook endpoint
inklate webhooks deliveries <id>| Name | Kind | Description |
| --- | --- | --- |
| id | argument | Webhook endpoint id |
REST: webhooksDeliveries
campaigns — Campaign grouping and rollups
inklate campaigns list
List campaigns
inklate campaigns listREST: campaignsList
inklate campaigns create
Create a campaign
inklate campaigns create <name> [flags]| Name | Kind | Description |
| --- | --- | --- |
| name | argument | Campaign name |
| --status <value> | flag | Initial status (default: server-side default) |
| --starts-at <value> | flag | ISO-8601 with offset |
| --ends-at <value> | flag | ISO-8601 with offset |
REST: campaignsCreate
inklate campaigns metrics
Roll up a campaign's engagement
inklate campaigns metrics <id>| Name | Kind | Description |
| --- | --- | --- |
| id | argument | Campaign id |
REST: campaignsMetrics
approvals — Approval rules, requests, and decisions
inklate approvals rule
Show the organization's approval rule
inklate approvals ruleREST: approvalsRule
inklate approvals set-rule
Set the organization's approval rule
inklate approvals set-rule <policy> [flags]| Name | Kind | Description |
| --- | --- | --- |
| policy | argument | Rule policy: single, sequential, or parallel |
| --approvers <value> | flag | Comma-separated approver user ids |
| --quorum <value> | flag | Approvals required |
| --disabled | flag | Turn the rule off |
REST: approvalsSetRule
inklate approvals requests
List approval requests
inklate approvals requestsREST: approvalsRequests
inklate approvals request
Submit a post for approval
inklate approvals request <post>| Name | Kind | Description |
| --- | --- | --- |
| post | argument | Post id |
REST: approvalsRequest
inklate approvals decide
Approve or reject an approval request
inklate approvals decide <id> [flags]| Name | Kind | Description |
| --- | --- | --- |
| id | argument | Approval request id |
| --approve | flag | Approve the request |
| --reject | flag | Reject the request |
| --comment <value> | flag | Reviewer comment |
REST: approvalsDecide
audit-logs — The organization's audit trail
inklate audit-logs list
Read the audit trail (admin)
inklate audit-logs list [flags]| Name | Kind | Description |
| --- | --- | --- |
| --action <value> | flag | Filter by action name |
| --resource-id <value> | flag | Filter by resource id |
| --limit <value> | flag | |
REST: auditLogsList
api — Call any REST endpoint directly
inklate api
Examples: inklate api GET /posts, inklate api POST /posts --field content=hello --field channel_ids='["<channel-id>"]', inklate api POST /posts --input body.json.
inklate api <method> <path> [flags]| Name | Kind | Description |
| --- | --- | --- |
| method | argument | HTTP method |
| path | argument | Endpoint path, e.g. /posts |
| --field <value> | flag | Body field as key=value (value parsed as JSON when it looks like it) (repeatable) |
| --query <value> | flag | Query param as key=value (repeatable) |
| --input <value> | flag | Read the JSON body from a file (- for stdin) |
mcp — Run a local stdio MCP server bridged to your organization
inklate mcp
Run a local stdio MCP server bridged to the remote Inklate MCP
inklate mcpcompletions — Shell completion scripts
inklate completions
bash: source <(inklate completions bash) · zsh: inklate completions zsh > "${fpath[1]}/_inklate" · fish: inklate completions fish > ~/.config/fish/completions/inklate.fish
inklate completions <shell>| Name | Kind | Description |
| --- | --- | --- |
| shell | argument | Target shell |
Development
Lives in the Inklate monorepo at packages/cli.
bun run --filter inklate build # bundle with tsdown (workspace deps inlined)
bun run --filter inklate docs:generate # regenerate this README + the docs reference
bun run --filter server sdk:generate # regenerate the SDK operation tableThis file and the per-command pages under apps/docs/content/docs/cli/ are generated from the command registry (src/registry.ts) and drift-tested byte for byte, so they cannot fall behind the binary. Releases ride changesets → .github/workflows/release-npm.yml with npm provenance; the procedure is docs/runbook.md § "Releasing the CLI to npm".
Full documentation: https://docs.inklate.com/cli
