@posteverywhere/cli 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +52 -13
  2. package/SKILL.md +5 -5
  3. package/dist/index.js +340 -74
  4. package/package.json +11 -4
package/README.md CHANGED
@@ -1,27 +1,66 @@
1
1
  # @posteverywhere/cli
2
2
 
3
- The PostEverywhere CLI for AI agents — schedule and publish to Instagram, TikTok, YouTube, LinkedIn, Facebook, X, Threads, Pinterest, Bluesky, Telegram and Discord from the command line. Every command outputs structured JSON for agents (Claude, Cursor, OpenClaw, …) to parse.
3
+ Post and schedule to **Instagram, TikTok, YouTube, LinkedIn, Facebook, X, Threads, Pinterest, Bluesky, Telegram and Discord** — all from your terminal. Log in once, connect accounts, publish. Built for humans *and* AI agents (`--json` output for Claude, Cursor, etc.).
4
4
 
5
- ## Quick start
5
+ ## Install
6
6
 
7
7
  ```bash
8
- export POSTEVERYWHERE_API_KEY=pe_live_... # Settings → Developers in the dashboard
8
+ npm install -g @posteverywhere/cli
9
+ # …or run without installing:
10
+ npx @posteverywhere/cli <command>
11
+ ```
12
+
13
+ ## 90-second setup
9
14
 
10
- npx @posteverywhere/cli whoami # verify the key
11
- npx @posteverywhere/cli accounts # list connected accounts (+ ids)
12
- npx @posteverywhere/cli post -c "Hello 🚀" -a 123,456 # publish now
13
- npx @posteverywhere/cli post -c "Later" -a 123 -s 2026-07-01T09:00:00Z # schedule (UTC)
15
+ ```bash
16
+ posteverywhere login # opens your browser, saves a key to ~/.posteverywhere
17
+ posteverywhere connect instagram # opens the OAuth flow; auto-detects the connected account
18
+ posteverywhere accounts # list connected accounts (+ ids & health)
19
+ posteverywhere post -c "Hello 🚀" -a 123,456
14
20
  ```
15
21
 
16
- Run `npx @posteverywhere/cli help` for the full command list.
22
+ `login` uses a device-grant flow (like the GitHub CLI): it prints a short code, you approve it in the browser, and a scoped API key is saved locally (`chmod 600`). Manage or revoke it anytime from **Settings → Developers**.
23
+
24
+ ## Connecting accounts
25
+
26
+ ```bash
27
+ posteverywhere connect <platform>
28
+ ```
29
+ - **Browser platforms** (`instagram`, `facebook`, `threads`, `x`, `linkedin`, `tiktok`, `youtube`, `pinterest`): opens the normal OAuth flow in your browser, then auto-detects the new account.
30
+ - **Bluesky**: prompts for your handle + an App Password (Settings → App Passwords).
31
+ - **Telegram**: prompts for a bot token (@BotFather) + your channel — add the bot as an admin first.
32
+ - **Discord**: prompts for a channel webhook URL (Server Settings → Integrations → Webhooks).
33
+
34
+ Re-authorize an expired account: `posteverywhere reconnect <accountId>`.
35
+
36
+ ## Commands
37
+
38
+ | Command | What it does |
39
+ |---|---|
40
+ | `login` / `logout` | Device-flow login / remove saved credentials |
41
+ | `whoami` | Show the authed account, plan & quota |
42
+ | `accounts` | List connected accounts (+ ids & health) |
43
+ | `connect <platform>` | Connect a new account |
44
+ | `reconnect <accountId>` | Re-authorize an account whose token expired |
45
+ | `account:health <id>` | Detailed health for one account |
46
+ | `post -c <text> -a <ids> [-s <iso>] [-m <mediaIds>]` | Publish now (omit `-s`) or schedule (`-s` ISO time) |
47
+ | `posts [--status x] [--platform y] [--limit n]` | List posts |
48
+ | `results <postId>` | Per-platform publish results |
49
+ | `retry <postId>` | Retry failed destinations |
50
+ | `upload <imageUrl>` | Import an image by URL → media_id |
51
+ | `caption -t <topic> [--platform x] [--tone y]` | AI captions |
52
+ | `analytics [--period week\|month\|all]` | Analytics summary |
53
+ | `campaigns` | List campaigns |
17
54
 
18
55
  ## For AI agents
19
56
 
20
- This package ships a `SKILL.md` describing the commands for agent auto-discovery. Point your agent at it, or connect via MCP instead:
57
+ Every command accepts `--json` (auto-on when piped) and emits structured JSON. This package ships a `SKILL.md` for agent auto-discovery point Claude/Cursor/etc. at it. Prefer MCP? Use the hosted endpoint `https://mcp.posteverywhere.ai/mcp` or `npx -y @posteverywhere/mcp`.
58
+
59
+ ## Auth precedence
21
60
 
22
- - **Hosted MCP (no install):** `https://mcp.posteverywhere.ai` see [docs](https://developers.posteverywhere.ai/integrations/mcp)
23
- - **Local MCP:** `npx -y @posteverywhere/mcp`
61
+ 1. `POSTEVERYWHERE_API_KEY` env var (great for CI / agents)
62
+ 2. the key saved by `posteverywhere login` (`~/.posteverywhere/config.json`)
24
63
 
25
- ## Auth & safety
64
+ Your key authenticates into **your** PostEverywhere account and acts only through it — the CLI never touches your social-platform credentials directly. Keep a human in the loop before publishing.
26
65
 
27
- Your API key authenticates into **your PostEverywhere account** and acts only through it — the CLI never touches your social-platform credentials directly. Keep a human in the loop before publishing.
66
+ Docs: https://developers.posteverywhere.ai/cli
package/SKILL.md CHANGED
@@ -8,15 +8,15 @@ description: Schedule and publish social media posts to Instagram, TikTok, YouTu
8
8
  Manage a user's social media through the PostEverywhere CLI. Every command prints JSON.
9
9
 
10
10
  ## Setup (once)
11
- The user must set their API key (from posteverywhere.ai → Settings → Developers):
12
- ```bash
13
- export POSTEVERYWHERE_API_KEY=pe_live_...
14
- ```
11
+ Authenticate one of two ways:
12
+ - **Interactive:** `posteverywhere login` opens the browser; the user approves a short code and a scoped key is saved locally. Then `posteverywhere connect <platform>` to add accounts (instagram, tiktok, youtube, linkedin, facebook, x, threads, pinterest = browser OAuth; bluesky/telegram/discord = the CLI prompts for credentials).
13
+ - **Non-interactive (CI / headless agents):** `export POSTEVERYWHERE_API_KEY=pe_live_...` (from posteverywhere.ai → Settings → Developers).
14
+
15
15
  Run commands with `npx @posteverywhere/cli <command>` (or `posteverywhere <command>` if installed).
16
16
 
17
17
  ## Always start here
18
18
  1. `posteverywhere whoami` — confirms the key works and shows the plan/quota.
19
- 2. `posteverywhere accounts` — lists connected accounts. **You need the numeric account `id`s to post.** If empty, tell the user to connect accounts in the dashboard first.
19
+ 2. `posteverywhere accounts` — lists connected accounts. **You need the numeric account `id`s to post.** If empty, run `posteverywhere connect <platform>` (or tell the user to connect in the dashboard).
20
20
 
21
21
  ## Core workflow
22
22
  **Publish now** to accounts 123 and 456:
package/dist/index.js CHANGED
@@ -1,30 +1,101 @@
1
1
  #!/usr/bin/env node
2
- "use strict";
3
2
  /**
4
- * PostEverywhere CLI for AI agents (task #245, "skill" distribution).
3
+ * posteverywhere the PostEverywhere CLI.
5
4
  *
6
- * Thin wrapper over the public v1 REST API. Every command prints structured
7
- * JSON to stdout so agents (Claude, Cursor, OpenClaw, etc.) can parse results.
8
- * Auth: set POSTEVERYWHERE_API_KEY=pe_live_... (Settings Developers).
5
+ * Log in once (`posteverywhere login`), connect accounts, and post/schedule to
6
+ * Instagram, TikTok, YouTube, LinkedIn, Facebook, X, Threads, Pinterest,
7
+ * Bluesky, Telegram & Discord all from your terminal.
9
8
  *
10
- * Same surface as the MCP server, for agents that run shell commands rather
11
- * than speak MCP. See SKILL.md for the agent-facing command reference.
9
+ * Agent-friendly: pass --json (or pipe to a non-TTY) and every command emits
10
+ * structured JSON so Claude, Cursor, OpenClaw etc. can parse results.
11
+ *
12
+ * Auth resolves from (1) POSTEVERYWHERE_API_KEY, else (2) the key saved by
13
+ * `posteverywhere login` at ~/.posteverywhere/config.json (chmod 600).
14
+ *
15
+ * Zero runtime dependencies — pure Node (>=18) so `npx posteverywhere-cli`
16
+ * is instant and adds no supply-chain surface.
12
17
  */
13
- const BASE = (process.env.POSTEVERYWHERE_API_URL || 'https://app.posteverywhere.ai').replace(/\/$/, '');
14
- const KEY = process.env.POSTEVERYWHERE_API_KEY || '';
15
- function out(data) {
16
- process.stdout.write(JSON.stringify(data, null, 2) + '\n');
17
- }
18
+ import fs from 'node:fs';
19
+ import os from 'node:os';
20
+ import path from 'node:path';
21
+ import readline from 'node:readline';
22
+ import { spawn } from 'node:child_process';
23
+ const BASE = (process.env.POSTEVERYWHERE_API_URL || process.env.POSTEVERYWHERE_BASE_URL || 'https://app.posteverywhere.ai').replace(/\/$/, '');
24
+ const CONFIG_DIR = path.join(os.homedir(), '.posteverywhere');
25
+ const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
26
+ const WANT_JSON = process.argv.includes('--json') || !process.stdout.isTTY;
27
+ const TTY = !!process.stdout.isTTY;
28
+ // ─── output ──────────────────────────────────────────────
29
+ const C = { reset: '\x1b[0m', dim: '\x1b[2m', bold: '\x1b[1m', green: '\x1b[32m', red: '\x1b[31m', yellow: '\x1b[33m', cyan: '\x1b[36m' };
30
+ const paint = (s, code) => (TTY ? `${code}${s}${C.reset}` : s);
31
+ function outJson(data) { process.stdout.write(JSON.stringify(data, null, 2) + '\n'); }
32
+ function say(msg = '') { process.stdout.write(msg + '\n'); }
18
33
  function fail(message, code = 1) {
19
- process.stderr.write(JSON.stringify({ error: message }) + '\n');
34
+ if (WANT_JSON)
35
+ process.stderr.write(JSON.stringify({ error: message }) + '\n');
36
+ else
37
+ process.stderr.write(paint('✖ ' + message, C.red) + '\n');
20
38
  process.exit(code);
21
39
  }
22
- // Parse "cmd positional --flag value -f value --bool" into { _: [...], flags }.
40
+ function loadConfig() { try {
41
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
42
+ }
43
+ catch {
44
+ return {};
45
+ } }
46
+ function saveConfig(cfg) {
47
+ try {
48
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
49
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2), { mode: 0o600 });
50
+ try {
51
+ fs.chmodSync(CONFIG_PATH, 0o600);
52
+ }
53
+ catch { /* best effort on non-POSIX */ }
54
+ }
55
+ catch (e) {
56
+ fail(`Could not save credentials to ${CONFIG_PATH}: ${e instanceof Error ? e.message : String(e)}`);
57
+ }
58
+ }
59
+ function resolveKey() { return process.env.POSTEVERYWHERE_API_KEY || loadConfig().api_key || ''; }
60
+ // ─── http ────────────────────────────────────────────────
61
+ async function raw(method, p, body, key) {
62
+ const headers = { Accept: 'application/json' };
63
+ if (key)
64
+ headers.Authorization = `Bearer ${key}`;
65
+ if (body)
66
+ headers['Content-Type'] = 'application/json';
67
+ let resp;
68
+ try {
69
+ resp = await fetch(`${BASE}${p}`, { method, headers, body: body ? JSON.stringify(body) : undefined });
70
+ }
71
+ catch (e) {
72
+ return fail(`Network error reaching ${BASE}: ${e instanceof Error ? e.message : String(e)}`);
73
+ }
74
+ const json = await resp.json().catch(() => ({}));
75
+ return { status: resp.status, json };
76
+ }
77
+ // Authenticated v1 call — unwraps {data,error}; fails cleanly on error.
78
+ async function api(method, p, body) {
79
+ const key = resolveKey();
80
+ if (!key || !key.startsWith('pe_live_')) {
81
+ fail('Not logged in. Run `posteverywhere login` (or set POSTEVERYWHERE_API_KEY=pe_live_...).');
82
+ }
83
+ const { json } = await raw(method, `/api/v1${p}`, body, key);
84
+ if (json?.error)
85
+ fail(typeof json.error === 'string' ? json.error : json.error.message || 'API error');
86
+ return json?.data;
87
+ }
88
+ // ─── small helpers ───────────────────────────────────────
89
+ const csv = (v) => (typeof v === 'string' ? v.split(',').map(s => s.trim()).filter(Boolean) : []);
90
+ const num = (v) => csv(v).map(Number).filter(n => !Number.isNaN(n));
91
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
23
92
  function parseArgs(argv) {
24
93
  const positional = [];
25
94
  const flags = {};
26
95
  for (let i = 0; i < argv.length; i++) {
27
96
  const a = argv[i];
97
+ if (a === '--json')
98
+ continue;
28
99
  if (a.startsWith('--') || a.startsWith('-')) {
29
100
  const key = a.replace(/^-+/, '');
30
101
  const next = argv[i + 1];
@@ -40,51 +111,247 @@ function parseArgs(argv) {
40
111
  }
41
112
  return { positional, flags };
42
113
  }
43
- async function api(method, path, body) {
44
- if (!KEY || !KEY.startsWith('pe_live_')) {
45
- fail('Missing or invalid POSTEVERYWHERE_API_KEY (must start with pe_live_). Create one at ' + BASE + ' Settings Developers.');
114
+ function openBrowser(url) {
115
+ try {
116
+ const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
117
+ const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
118
+ spawn(cmd, args, { stdio: 'ignore', detached: true }).unref();
46
119
  }
47
- let resp;
120
+ catch { /* best effort — the URL is always printed too */ }
121
+ }
122
+ function askLine(question) {
123
+ return new Promise((resolve) => {
124
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
125
+ rl.question(question, (answer) => { rl.close(); resolve(answer.trim()); });
126
+ });
127
+ }
128
+ // Masked secret prompt (app passwords / bot tokens). Standard readline with a
129
+ // muted output stream so the secret is never echoed to the screen; backspace,
130
+ // Enter and Ctrl-C are handled natively by readline. No raw-mode parsing.
131
+ function askSecret(question) {
132
+ return new Promise((resolve) => {
133
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
134
+ let muted = false;
135
+ const origWrite = typeof rl._writeToOutput === 'function' ? rl._writeToOutput.bind(rl) : null;
136
+ rl._writeToOutput = (str) => {
137
+ if (!muted) {
138
+ origWrite ? origWrite(str) : process.stdout.write(str);
139
+ return;
140
+ }
141
+ // While muted, only let newlines through so Enter still drops the cursor.
142
+ if (/[\r\n]/.test(str)) {
143
+ origWrite ? origWrite('\n') : process.stdout.write('\n');
144
+ }
145
+ };
146
+ rl.question(question, (answer) => { rl.close(); resolve(answer.trim()); });
147
+ muted = true; // everything typed after the prompt is shown is hidden
148
+ });
149
+ }
150
+ // ─── login (device-grant flow) ───────────────────────────
151
+ async function login() {
152
+ if (resolveKey() && !process.env.POSTEVERYWHERE_API_KEY) {
153
+ const who = loadConfig().email;
154
+ say(paint(`Already logged in${who ? ` as ${who}` : ''}. Run \`posteverywhere logout\` first to switch accounts.`, C.yellow));
155
+ return;
156
+ }
157
+ const start = await raw('POST', '/api/v1/auth/device', { client_name: `CLI on ${os.hostname()}` });
158
+ if (start.json?.error || !start.json?.data) {
159
+ fail(typeof start.json?.error === 'string' ? start.json.error : start.json?.error?.message || 'Could not start login.');
160
+ }
161
+ const { device_code, user_code, verification_uri, verification_uri_complete, interval } = start.json.data;
162
+ if (WANT_JSON) {
163
+ outJson({ status: 'pending', user_code, verification_uri, verification_uri_complete, device_code });
164
+ }
165
+ else {
166
+ say('');
167
+ say(` ${paint('Connect this CLI to PostEverywhere', C.bold)}`);
168
+ say('');
169
+ say(` 1. Open: ${paint(verification_uri, C.cyan)}`);
170
+ say(` 2. Enter code: ${paint(user_code, C.bold)}`);
171
+ say('');
172
+ say(paint(' Opening your browser…', C.dim));
173
+ openBrowser(verification_uri_complete);
174
+ say(paint(' Waiting for you to approve… (Ctrl-C to cancel)', C.dim));
175
+ }
176
+ const pollInterval = Math.max(2, Number(interval) || 5) * 1000;
177
+ const deadline = Date.now() + 15 * 60 * 1000;
178
+ let delay = pollInterval;
179
+ while (Date.now() < deadline) {
180
+ await sleep(delay);
181
+ const { json } = await raw('POST', '/api/v1/auth/device/token', { device_code });
182
+ if (json?.error)
183
+ fail(typeof json.error === 'string' ? json.error : json.error.message || 'Login failed.');
184
+ const data = json?.data || {};
185
+ switch (data.status) {
186
+ case 'pending': continue;
187
+ case 'slow_down':
188
+ delay += 2000;
189
+ continue;
190
+ case 'denied': fail('Login was denied in the browser.');
191
+ case 'expired': fail('The login code expired. Run `posteverywhere login` again.');
192
+ case 'authorized': {
193
+ saveConfig({ api_key: data.api_key, email: data.user?.email, organization_id: data.organization_id });
194
+ if (WANT_JSON)
195
+ return outJson({ status: 'authorized', email: data.user?.email, organization_id: data.organization_id });
196
+ say('');
197
+ say(paint(` ✓ Logged in as ${data.user?.email || 'your account'}`, C.green));
198
+ say(paint(` Credentials saved to ${CONFIG_PATH}`, C.dim));
199
+ say('');
200
+ say(' Next: `posteverywhere accounts` to see connected accounts, or');
201
+ say(' `posteverywhere connect instagram` to connect one.');
202
+ return;
203
+ }
204
+ default: continue;
205
+ }
206
+ }
207
+ fail('Timed out waiting for approval. Run `posteverywhere login` again.');
208
+ }
209
+ function logout() {
48
210
  try {
49
- resp = await fetch(`${BASE}/api/v1${path}`, {
50
- method,
51
- headers: { Authorization: `Bearer ${KEY}`, ...(body ? { 'Content-Type': 'application/json' } : {}) },
52
- body: body ? JSON.stringify(body) : undefined,
53
- });
211
+ fs.rmSync(CONFIG_PATH, { force: true });
54
212
  }
55
- catch (e) {
56
- fail(`Network error: ${e instanceof Error ? e.message : String(e)}`);
213
+ catch { }
214
+ if (WANT_JSON)
215
+ return outJson({ ok: true });
216
+ say(paint('✓ Logged out. Credentials removed.', C.green));
217
+ }
218
+ // ─── account connect / reconnect (browser bridge) ─────────
219
+ const HEADLESS_PLATFORMS = new Set(['bluesky', 'telegram', 'discord']);
220
+ async function connectHeadless(platform) {
221
+ if (platform === 'bluesky') {
222
+ say(paint('Connect Bluesky', C.bold));
223
+ const handle = await askLine(' Handle (e.g. you.bsky.social): ');
224
+ const appPassword = await askSecret(' App password (Settings → App Passwords): ');
225
+ if (!handle || !appPassword)
226
+ fail('Both handle and app password are required.');
227
+ const data = await api('POST', '/auth/social-connect/bluesky', { handle, app_password: appPassword });
228
+ if (WANT_JSON)
229
+ return outJson(data);
230
+ return say(paint(`✓ Connected Bluesky as ${data?.account?.account_name || handle}`, C.green));
57
231
  }
58
- const json = await resp.json().catch(() => ({ error: { message: `HTTP ${resp.status}` } }));
59
- if (json.error)
60
- fail(typeof json.error === 'string' ? json.error : json.error.message || 'API error');
61
- return json.data;
232
+ if (platform === 'telegram') {
233
+ say(paint('Connect Telegram', C.bold));
234
+ const botToken = await askSecret(' Bot token (from @BotFather): ');
235
+ if (!botToken)
236
+ fail('A bot token is required.');
237
+ const channel = await askLine(' Channel (e.g. @mychannel, or its numeric id): ');
238
+ if (!channel)
239
+ fail('A channel is required — add your bot to it as an admin first.');
240
+ const data = await api('POST', '/auth/social-connect/telegram', { bot_token: botToken, channel });
241
+ if (WANT_JSON)
242
+ return outJson(data);
243
+ return say(paint(`✓ Connected Telegram (${data?.account?.account_name || 'channel'})`, C.green));
244
+ }
245
+ if (platform === 'discord') {
246
+ say(paint('Connect Discord', C.bold));
247
+ say(paint(' Discord: Server Settings → Integrations → Webhooks → New Webhook → Copy URL', C.dim));
248
+ const webhookUrl = await askSecret(' Webhook URL: ');
249
+ if (!webhookUrl)
250
+ fail('A Discord webhook URL is required.');
251
+ const data = await api('POST', '/auth/social-connect/discord', { webhook_url: webhookUrl });
252
+ if (WANT_JSON)
253
+ return outJson(data);
254
+ return say(paint(`✓ Connected Discord (${data?.account?.account_name || 'channel'})`, C.green));
255
+ }
256
+ }
257
+ async function connectBrowser(platform, opts = {}) {
258
+ const body = { platform };
259
+ if (opts.mode)
260
+ body.mode = opts.mode;
261
+ if (opts.account_id)
262
+ body.account_id = opts.account_id;
263
+ const { session_id, authorize_url, interval } = await api('POST', '/auth/social-connect/start', body);
264
+ if (WANT_JSON) {
265
+ outJson({ status: 'pending', session_id, authorize_url });
266
+ }
267
+ else {
268
+ const verb = opts.mode === 'reconnect' ? 'Reconnect' : 'Connect';
269
+ say('');
270
+ say(` ${paint(`${verb} ${platform}`, C.bold)}`);
271
+ say(` Open: ${paint(authorize_url, C.cyan)}`);
272
+ say(paint(' Opening your browser…', C.dim));
273
+ openBrowser(authorize_url);
274
+ say(paint(' Waiting for you to finish in the browser… (Ctrl-C to cancel)', C.dim));
275
+ }
276
+ const pollInterval = Math.max(2, Number(interval) || 3) * 1000;
277
+ const deadline = Date.now() + 15 * 60 * 1000;
278
+ while (Date.now() < deadline) {
279
+ await sleep(pollInterval);
280
+ const data = await api('GET', `/auth/social-connect/poll/${session_id}`);
281
+ if (data.status === 'pending')
282
+ continue;
283
+ if (data.status === 'expired')
284
+ fail('The connection link expired. Try again.');
285
+ if (data.status === 'cancelled')
286
+ fail('Connection was cancelled.');
287
+ if (data.status === 'completed') {
288
+ if (WANT_JSON)
289
+ return outJson(data);
290
+ const a = data.account || {};
291
+ say('');
292
+ say(paint(` ✓ ${opts.mode === 'reconnect' ? 'Reconnected' : 'Connected'} ${a.platform || platform}${a.account_name ? ` as ${a.account_name}` : ''}`, C.green));
293
+ return;
294
+ }
295
+ }
296
+ fail('Timed out. Try again.');
297
+ }
298
+ async function connect(platform) {
299
+ if (!platform)
300
+ fail('Usage: posteverywhere connect <platform> (e.g. instagram, tiktok, bluesky, telegram)');
301
+ const p = platform.toLowerCase();
302
+ if (HEADLESS_PLATFORMS.has(p))
303
+ return connectHeadless(p);
304
+ return connectBrowser(p);
305
+ }
306
+ async function reconnect(arg) {
307
+ if (!arg)
308
+ fail('Usage: posteverywhere reconnect <accountId> (run `posteverywhere accounts` to find ids)');
309
+ const accountId = Number(arg);
310
+ if (Number.isNaN(accountId))
311
+ fail('Provide a numeric account id (from `posteverywhere accounts`).');
312
+ const acct = await api('GET', `/accounts/${accountId}`).catch(() => null);
313
+ const platform = acct?.platform;
314
+ if (!platform)
315
+ fail(`Could not find account ${accountId}. Run \`posteverywhere accounts\`.`);
316
+ if (HEADLESS_PLATFORMS.has(String(platform).toLowerCase()))
317
+ return connectHeadless(String(platform).toLowerCase());
318
+ return connectBrowser(String(platform).toLowerCase(), { mode: 'reconnect', account_id: accountId });
62
319
  }
63
- const csv = (v) => (typeof v === 'string' ? v.split(',').map((s) => s.trim()).filter(Boolean) : []);
64
- const num = (v) => csv(v).map(Number).filter((n) => !Number.isNaN(n));
65
- const HELP = `PostEverywhere CLI — manage social media from the command line / AI agents.
320
+ // ─── help ────────────────────────────────────────────────
321
+ const HELP = `${paint('posteverywhere', C.bold)} post & schedule to every social platform from your terminal.
66
322
 
67
- Auth: export POSTEVERYWHERE_API_KEY=pe_live_... (Settings Developers)
323
+ ${paint('Getting started', C.bold)}
324
+ posteverywhere login Log in (opens your browser, saves a key locally)
325
+ posteverywhere connect <platform> Connect an account (instagram, tiktok, youtube,
326
+ linkedin, facebook, x, threads, pinterest,
327
+ bluesky, telegram, discord)
328
+ posteverywhere accounts List connected accounts (+ ids & health)
329
+ posteverywhere post -c "Hello" -a 123,456
68
330
 
69
- Commands:
70
- whoami Show the authed account, quota, and scopes
71
- accounts List connected social accounts (+ their ids/health)
331
+ ${paint('Commands', C.bold)}
332
+ login Device-flow login (browser approval)
333
+ logout Remove saved credentials
334
+ whoami Show the authed account, plan & quota
335
+ accounts List connected social accounts
336
+ connect <platform> Connect a new account
337
+ reconnect <accountId> Re-authorize an account whose token expired
72
338
  account:health <id> Detailed health for one account
73
- post -c <text> -a <ids> [-s <iso>] [-m <media_ids>]
74
- Create/schedule a post (-a = comma account ids; omit -s = publish now)
75
- posts [--status x] [--platform y] [--limit n]
76
- List posts
77
- results <postId> Per-platform publish results for a post
78
- retry <postId> Retry failed destinations of a post
79
- upload <imageUrl> Import an image by URL (one-call), returns media_id
80
- caption -t <topic> [--platform x] [--tone y]
81
- Generate AI captions
82
- analytics [--period week|month|all]
83
- Account analytics summary
339
+ post -c <text> -a <ids> [-s <iso>] [-m <mediaIds>]
340
+ Publish now (omit -s) or schedule (-s ISO time)
341
+ posts [--status x] [--platform y] [--limit n] List posts
342
+ results <postId> Per-platform publish results
343
+ retry <postId> Retry failed destinations
344
+ upload <imageUrl> Import an image by URL -> media_id
345
+ caption -t <topic> [--platform x] [--tone y] AI captions
346
+ analytics [--period week|month|all] Analytics summary
84
347
  campaigns List campaigns
85
348
 
86
- All output is JSON on stdout. Errors are JSON on stderr with a non-zero exit.
87
- Docs: https://developers.posteverywhere.ai/integrations/mcp`;
349
+ ${paint('Flags', C.bold)}
350
+ --json Machine-readable JSON output (auto-on when piped). Great for agents.
351
+
352
+ Auth precedence: POSTEVERYWHERE_API_KEY env var, else the key from \`login\`.
353
+ Docs: https://developers.posteverywhere.ai`;
354
+ // ─── main ────────────────────────────────────────────────
88
355
  async function main() {
89
356
  const [, , cmd, ...rest] = process.argv;
90
357
  const { positional, flags } = parseArgs(rest);
@@ -94,17 +361,17 @@ async function main() {
94
361
  case 'help':
95
362
  case '--help':
96
363
  case '-h':
97
- process.stdout.write(HELP + '\n');
98
- return;
99
- case 'whoami':
100
- return out(await api('GET', '/me'));
101
- case 'accounts':
102
- return out(await api('GET', '/accounts'));
364
+ return say(HELP);
365
+ case 'login': return login();
366
+ case 'logout': return logout();
367
+ case 'whoami': return outJson(await api('GET', '/me'));
368
+ case 'accounts': return outJson(await api('GET', '/accounts'));
369
+ case 'connect': return connect(positional[0]);
370
+ case 'reconnect': return reconnect(positional[0]);
103
371
  case 'account:health': {
104
- const id = positional[0];
105
- if (!id)
372
+ if (!positional[0])
106
373
  fail('Usage: posteverywhere account:health <accountId>');
107
- return out(await api('GET', `/accounts/${id}/health`));
374
+ return outJson(await api('GET', `/accounts/${positional[0]}/health`));
108
375
  }
109
376
  case 'post': {
110
377
  const content = f('content', 'c');
@@ -122,7 +389,10 @@ async function main() {
122
389
  const media = csv(f('media', 'm'));
123
390
  if (media.length)
124
391
  body.media_ids = media;
125
- return out(await api('POST', '/posts', body));
392
+ const data = await api('POST', '/posts', body);
393
+ if (!WANT_JSON)
394
+ say(paint(`✓ ${body.scheduled_for ? 'Scheduled' : 'Publishing'} to ${accounts.length} account(s).`, C.green));
395
+ return outJson(data);
126
396
  }
127
397
  case 'posts': {
128
398
  const q = new URLSearchParams();
@@ -131,23 +401,21 @@ async function main() {
131
401
  if (typeof f('platform') === 'string')
132
402
  q.set('platform', f('platform'));
133
403
  q.set('limit', String(f('limit') || 20));
134
- return out(await api('GET', `/posts?${q.toString()}`));
404
+ return outJson(await api('GET', `/posts?${q.toString()}`));
135
405
  }
136
- case 'results': {
406
+ case 'results':
137
407
  if (!positional[0])
138
408
  fail('Usage: posteverywhere results <postId>');
139
- return out(await api('GET', `/posts/${positional[0]}/results`));
140
- }
141
- case 'retry': {
409
+ return outJson(await api('GET', `/posts/${positional[0]}/results`));
410
+ case 'retry':
142
411
  if (!positional[0])
143
412
  fail('Usage: posteverywhere retry <postId>');
144
- return out(await api('POST', `/posts/${positional[0]}/retry`));
145
- }
413
+ return outJson(await api('POST', `/posts/${positional[0]}/retry`));
146
414
  case 'upload': {
147
415
  const url = positional[0] || f('url');
148
416
  if (typeof url !== 'string')
149
417
  fail('Usage: posteverywhere upload <imageUrl>');
150
- return out(await api('POST', '/media/upload-from-url', { url }));
418
+ return outJson(await api('POST', '/media/upload-from-url', { url }));
151
419
  }
152
420
  case 'caption': {
153
421
  const topic = f('topic', 't');
@@ -158,16 +426,14 @@ async function main() {
158
426
  body.platform = f('platform');
159
427
  if (typeof f('tone') === 'string')
160
428
  body.tone = f('tone');
161
- return out(await api('POST', '/ai/generate-caption', body));
162
- }
163
- case 'analytics': {
164
- const period = f('period') || 'month';
165
- return out(await api('GET', `/analytics/summary?period=${encodeURIComponent(period)}`));
429
+ return outJson(await api('POST', '/ai/generate-caption', body));
166
430
  }
431
+ case 'analytics':
432
+ return outJson(await api('GET', `/analytics/summary?period=${encodeURIComponent(f('period') || 'month')}`));
167
433
  case 'campaigns':
168
- return out(await api('GET', '/campaigns'));
434
+ return outJson(await api('GET', '/campaigns'));
169
435
  default:
170
- fail(`Unknown command: ${cmd}. Run \`posteverywhere help\` for the command list.`);
436
+ fail(`Unknown command: ${cmd}. Run \`posteverywhere help\`.`);
171
437
  }
172
438
  }
173
439
  main().catch((e) => fail(e instanceof Error ? e.message : String(e)));
package/package.json CHANGED
@@ -1,19 +1,26 @@
1
1
  {
2
2
  "name": "@posteverywhere/cli",
3
- "version": "0.1.0",
4
- "description": "PostEverywhere CLI for AI agents — schedule and publish to Instagram, TikTok, YouTube, LinkedIn, Facebook, X, Threads, Pinterest, Bluesky, Telegram & Discord from the command line. Structured JSON output for Claude, Cursor, and other agents.",
3
+ "version": "0.2.0",
4
+ "description": "Post and schedule to Instagram, TikTok, YouTube, LinkedIn, Facebook, X, Threads, Pinterest, Bluesky, Telegram & Discord from your terminal. Log in once, connect accounts, publish. Also works as a tool for Claude, Cursor and other AI agents (--json output).",
5
5
  "type": "module",
6
6
  "bin": {
7
- "posteverywhere": "dist/index.js"
7
+ "posteverywhere": "dist/index.js",
8
+ "posteverywhere-cli": "dist/index.js"
8
9
  },
9
10
  "files": ["dist", "SKILL.md", "README.md"],
11
+ "publishConfig": { "access": "public" },
10
12
  "engines": { "node": ">=18.0.0" },
11
13
  "scripts": {
12
14
  "build": "tsc",
13
15
  "dev": "tsc --watch",
14
16
  "prepublishOnly": "npm run build"
15
17
  },
16
- "keywords": ["social media", "cli", "ai agent", "claude", "mcp", "scheduling", "automation"],
18
+ "keywords": [
19
+ "social media", "cli", "scheduling", "instagram", "tiktok", "youtube",
20
+ "linkedin", "bluesky", "ai agent", "claude", "mcp", "automation", "posteverywhere"
21
+ ],
17
22
  "license": "MIT",
23
+ "homepage": "https://posteverywhere.ai",
24
+ "bugs": { "url": "https://posteverywhere.ai" },
18
25
  "devDependencies": { "typescript": "^5.6.0", "@types/node": "^20.0.0" }
19
26
  }