@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.
- package/README.md +52 -13
- package/SKILL.md +5 -5
- package/dist/index.js +340 -74
- package/package.json +11 -4
package/README.md
CHANGED
|
@@ -1,27 +1,66 @@
|
|
|
1
1
|
# @posteverywhere/cli
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
5
|
+
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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`
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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
|
|
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
|
-
*
|
|
3
|
+
* posteverywhere — the PostEverywhere CLI.
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
11
|
-
*
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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 <
|
|
74
|
-
|
|
75
|
-
posts [--status x] [--platform y] [--limit n]
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
case '
|
|
100
|
-
|
|
101
|
-
case 'accounts':
|
|
102
|
-
|
|
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
|
-
|
|
105
|
-
if (!id)
|
|
372
|
+
if (!positional[0])
|
|
106
373
|
fail('Usage: posteverywhere account:health <accountId>');
|
|
107
|
-
return
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
434
|
+
return outJson(await api('GET', '/campaigns'));
|
|
169
435
|
default:
|
|
170
|
-
fail(`Unknown command: ${cmd}. Run \`posteverywhere help
|
|
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.
|
|
4
|
-
"description": "
|
|
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": [
|
|
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
|
}
|