posthive-cli 0.1.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 ADDED
@@ -0,0 +1,81 @@
1
+ # posthive-cli
2
+
3
+ Posthive CLI — schedule posts to 11 social platforms from the command line or any AI agent that can run shell commands (Claude Code, OpenClaw, Cursor, custom pipelines).
4
+
5
+ **Supported platforms:** Bluesky, Threads, Instagram, LinkedIn, Mastodon, YouTube, Facebook Pages, Pinterest, Telegram, X (Twitter), Nostr
6
+
7
+ Every command outputs structured JSON, so LLMs and scripts can parse results directly.
8
+
9
+ ---
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm i -g posthive-cli
15
+ # or run without installing
16
+ npx posthive-cli help
17
+ ```
18
+
19
+ ## Setup
20
+
21
+ You need a Posthive account and an API key ([posthive.co](https://posthive.co) → Settings → API Keys, Pro/Team plan).
22
+
23
+ ```bash
24
+ export POSTHIVE_API_KEY=ph_your_key_here
25
+ export POSTHIVE_API_URL=https://api.posthive.co # or your self-hosted URL
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ```bash
31
+ # List connected accounts (start here — you need account IDs)
32
+ posthive accounts:list
33
+
34
+ # Create a draft post (default: drafts require approval before publishing)
35
+ posthive posts:create --content "Hello world" --accounts acc_1,acc_2
36
+
37
+ # Schedule directly
38
+ posthive posts:create --content "Launch day!" --accounts acc_1 --schedule 2026-07-10T09:00:00Z
39
+
40
+ # First comment automation
41
+ posthive posts:create --content "We shipped something big" --accounts acc_1 \
42
+ --first-comment "Full details: https://example.com"
43
+
44
+ # Upload media, then attach it
45
+ posthive upload ./screenshot.png
46
+ posthive posts:create --content "Sneak peek" --accounts acc_1 --media <returned_url>
47
+
48
+ # Manage the queue
49
+ posthive posts:list --status draft
50
+ posthive posts:get <post_id>
51
+ posthive posts:update <post_id> --content "Fixed typo"
52
+ posthive posts:approve <post_id> --schedule 2026-07-10T09:00:00Z
53
+ posthive posts:duplicate <post_id>
54
+ posthive posts:delete <post_id>
55
+
56
+ # Templates
57
+ posthive templates:list
58
+ posthive templates:use <template_id> --accounts acc_1
59
+
60
+ # Full command reference
61
+ posthive help
62
+ ```
63
+
64
+ ## Use with AI agents
65
+
66
+ The package ships with a [skill](./skills/posthive/SKILL.md) that teaches agents the full command set, platform character limits, and draft-first workflow. Works with Claude Code, OpenClaw, and any agent framework that supports skills or shell execution.
67
+
68
+ Posts created by agents default to **drafts** — nothing publishes without approval unless `--schedule` is passed explicitly. Human in the loop by design.
69
+
70
+ Prefer MCP? Use [`posthive-mcp`](https://www.npmjs.com/package/posthive-mcp) for Claude Desktop, Cursor, and other MCP clients.
71
+
72
+ ## Links
73
+
74
+ - [Posthive](https://posthive.co)
75
+ - [Documentation](https://posthive.co/docs)
76
+ - [For Developers](https://posthive.co/for-developers)
77
+ - [GitHub](https://github.com/AstaBlackClove/posthive)
78
+
79
+ ## License
80
+
81
+ AGPL-3.0
package/dist/index.js ADDED
@@ -0,0 +1,283 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Posthive CLI
4
+ *
5
+ * Schedule posts to 11 social platforms from the command line or any
6
+ * AI agent that can run shell commands (Claude Code, OpenClaw, Cursor).
7
+ *
8
+ * Every command outputs structured JSON for easy parsing by LLMs and scripts.
9
+ *
10
+ * Required env vars:
11
+ * POSTHIVE_API_KEY — API key from Posthive Settings → API Keys
12
+ * POSTHIVE_API_URL — Base URL of the Posthive API (default: https://api.posthive.co)
13
+ */
14
+ import { readFile } from "node:fs/promises";
15
+ import { basename } from "node:path";
16
+ const API_KEY = process.env.POSTHIVE_API_KEY;
17
+ const API_URL = (process.env.POSTHIVE_API_URL ?? "https://api.posthive.co").replace(/\/$/, "");
18
+ // ─── Output helpers ──────────────────────────────────────────────────────────
19
+ function out(data) {
20
+ process.stdout.write(JSON.stringify(data, null, 2) + "\n");
21
+ process.exit(0);
22
+ }
23
+ function fail(message, code = 1) {
24
+ process.stdout.write(JSON.stringify({ error: message }, null, 2) + "\n");
25
+ process.exit(code);
26
+ }
27
+ function parseArgs(argv) {
28
+ const [command = "help", ...rest] = argv;
29
+ const positional = [];
30
+ const flags = {};
31
+ for (let i = 0; i < rest.length; i++) {
32
+ const arg = rest[i];
33
+ if (arg.startsWith("--")) {
34
+ const key = arg.slice(2);
35
+ const next = rest[i + 1];
36
+ if (next !== undefined && !next.startsWith("--")) {
37
+ flags[key] = next;
38
+ i++;
39
+ }
40
+ else {
41
+ flags[key] = true;
42
+ }
43
+ }
44
+ else {
45
+ positional.push(arg);
46
+ }
47
+ }
48
+ return { command, positional, flags };
49
+ }
50
+ function str(flags, key) {
51
+ const v = flags[key];
52
+ return typeof v === "string" ? v : undefined;
53
+ }
54
+ function list(flags, key) {
55
+ const v = str(flags, key);
56
+ return v ? v.split(",").map(s => s.trim()).filter(Boolean) : undefined;
57
+ }
58
+ // ─── HTTP helper ─────────────────────────────────────────────────────────────
59
+ async function api(method, path, body) {
60
+ let res;
61
+ try {
62
+ res = await fetch(`${API_URL}${path}`, {
63
+ method,
64
+ headers: {
65
+ Authorization: `Bearer ${API_KEY}`,
66
+ ...(body !== undefined ? { "Content-Type": "application/json" } : {}),
67
+ },
68
+ body: body !== undefined ? JSON.stringify(body) : undefined,
69
+ });
70
+ }
71
+ catch (err) {
72
+ fail(`Cannot reach Posthive API at ${API_URL} — check POSTHIVE_API_URL and your network. (${err instanceof Error ? err.message : String(err)})`);
73
+ }
74
+ const text = await res.text();
75
+ let json;
76
+ try {
77
+ json = JSON.parse(text);
78
+ }
79
+ catch {
80
+ json = { raw: text };
81
+ }
82
+ if (!res.ok) {
83
+ const rec = json;
84
+ const msg = rec?.error ?? rec?.message ?? text;
85
+ fail(`API ${res.status}: ${typeof msg === "string" ? msg : JSON.stringify(msg)}`);
86
+ }
87
+ return json;
88
+ }
89
+ // ─── Help text ───────────────────────────────────────────────────────────────
90
+ const HELP = {
91
+ usage: "posthive <command> [args] [--flags]",
92
+ env: {
93
+ POSTHIVE_API_KEY: "required — API key from Posthive Settings → API Keys",
94
+ POSTHIVE_API_URL: "optional — API base URL (default: https://api.posthive.co)",
95
+ },
96
+ commands: {
97
+ "accounts:list": "List connected social accounts and their IDs",
98
+ "posts:create": "Create a post. --content <text> --accounts <id,id> [--schedule <ISO>] [--first-comment <text>] [--media <url,url>] [--media-type post|reel|story] [--youtube-type short|video] [--dry-run]. Saved as DRAFT unless --schedule is given.",
99
+ "posts:list": "List posts. [--status pending|draft|done|failed] [--limit <n>]",
100
+ "posts:get": "Get post details. posts:get <post_id>",
101
+ "posts:update": "Update a pending/draft post. posts:update <post_id> [--content <text>] [--schedule <ISO>] [--first-comment <text>]",
102
+ "posts:approve": "Approve a draft and schedule it. posts:approve <post_id> --schedule <ISO>",
103
+ "posts:duplicate": "Clone a post as a new draft. posts:duplicate <post_id>",
104
+ "posts:delete": "Delete a pending/draft post. posts:delete <post_id>",
105
+ "templates:list": "List saved post templates",
106
+ "templates:use": "Create a post from a template. templates:use <template_id> --accounts <id,id> [--content <override>] [--schedule <ISO>]",
107
+ "upload": "Upload an image or video. upload <file_path> — returns a media URL for posts:create --media",
108
+ "help": "Show this help",
109
+ },
110
+ notes: [
111
+ "All output is JSON.",
112
+ "Posts default to DRAFT status — review them in Posthive → Posts, or pass --schedule to schedule directly.",
113
+ "Get account IDs from accounts:list before creating posts.",
114
+ ],
115
+ };
116
+ // ─── Main ────────────────────────────────────────────────────────────────────
117
+ const { command, positional, flags } = parseArgs(process.argv.slice(2));
118
+ if (command === "help" || command === "--help" || command === "-h") {
119
+ out(HELP);
120
+ }
121
+ if (!API_KEY) {
122
+ fail("POSTHIVE_API_KEY environment variable is required. Get a key at Posthive → Settings → API Keys.");
123
+ }
124
+ switch (command) {
125
+ case "accounts:list": {
126
+ out(await api("GET", "/api/v1/accounts"));
127
+ break;
128
+ }
129
+ case "posts:create": {
130
+ const content = str(flags, "content");
131
+ const accounts = list(flags, "accounts");
132
+ if (!content)
133
+ fail("--content is required");
134
+ if (!accounts?.length)
135
+ fail("--accounts is required (comma-separated account IDs; see accounts:list)");
136
+ const schedule = str(flags, "schedule");
137
+ const body = {
138
+ content,
139
+ accountIds: accounts,
140
+ draft: !schedule,
141
+ ...(schedule ? { scheduledFor: schedule } : {}),
142
+ ...(list(flags, "media") ? { images: list(flags, "media") } : {}),
143
+ ...(str(flags, "media-type") ? { mediaType: str(flags, "media-type") } : {}),
144
+ ...(str(flags, "youtube-type") ? { youtubeType: str(flags, "youtube-type") } : {}),
145
+ ...(str(flags, "first-comment") ? { commentText: str(flags, "first-comment") } : {}),
146
+ ...(flags["dry-run"] === true ? { dryRun: true } : {}),
147
+ };
148
+ const data = await api("POST", "/api/v1/posts", body);
149
+ out({
150
+ ...data,
151
+ _note: schedule
152
+ ? "Post scheduled — will publish at the given time."
153
+ : "Post saved as DRAFT. Approve with posts:approve or review in Posthive → Posts.",
154
+ });
155
+ break;
156
+ }
157
+ case "posts:list": {
158
+ const params = new URLSearchParams();
159
+ const status = str(flags, "status");
160
+ const limit = str(flags, "limit");
161
+ if (status)
162
+ params.set("status", status);
163
+ if (limit)
164
+ params.set("limit", limit);
165
+ const qs = params.size ? `?${params}` : "";
166
+ out(await api("GET", `/api/v1/posts${qs}`));
167
+ break;
168
+ }
169
+ case "posts:get": {
170
+ const id = positional[0];
171
+ if (!id)
172
+ fail("Usage: posthive posts:get <post_id>");
173
+ out(await api("GET", `/api/v1/posts/${encodeURIComponent(id)}`));
174
+ break;
175
+ }
176
+ case "posts:update": {
177
+ const id = positional[0];
178
+ if (!id)
179
+ fail("Usage: posthive posts:update <post_id> [--content] [--schedule] [--first-comment]");
180
+ const body = {
181
+ ...(str(flags, "content") !== undefined ? { content: str(flags, "content") } : {}),
182
+ ...(str(flags, "schedule") !== undefined ? { scheduledFor: str(flags, "schedule") } : {}),
183
+ ...(str(flags, "first-comment") !== undefined ? { commentText: str(flags, "first-comment") } : {}),
184
+ };
185
+ if (Object.keys(body).length === 0) {
186
+ fail("Provide at least one of --content, --schedule, --first-comment");
187
+ }
188
+ out(await api("PATCH", `/api/v1/posts/${encodeURIComponent(id)}`, body));
189
+ break;
190
+ }
191
+ case "posts:approve": {
192
+ const id = positional[0];
193
+ const schedule = str(flags, "schedule");
194
+ if (!id)
195
+ fail("Usage: posthive posts:approve <post_id> --schedule <ISO datetime>");
196
+ if (!schedule)
197
+ fail("--schedule is required (ISO 8601 datetime, must be in the future)");
198
+ const data = await api("POST", `/api/v1/posts/${encodeURIComponent(id)}/approve`, { scheduledFor: schedule });
199
+ out({ ...data, _note: "Draft approved and scheduled." });
200
+ break;
201
+ }
202
+ case "posts:duplicate": {
203
+ const id = positional[0];
204
+ if (!id)
205
+ fail("Usage: posthive posts:duplicate <post_id>");
206
+ const data = await api("POST", `/api/v1/posts/${encodeURIComponent(id)}/duplicate`);
207
+ out({ ...data, _note: "Duplicated as a new draft." });
208
+ break;
209
+ }
210
+ case "posts:delete": {
211
+ const id = positional[0];
212
+ if (!id)
213
+ fail("Usage: posthive posts:delete <post_id>");
214
+ out(await api("DELETE", `/api/v1/posts/${encodeURIComponent(id)}`));
215
+ break;
216
+ }
217
+ case "templates:list": {
218
+ out(await api("GET", "/api/v1/templates"));
219
+ break;
220
+ }
221
+ case "templates:use": {
222
+ const id = positional[0];
223
+ const accounts = list(flags, "accounts");
224
+ if (!id)
225
+ fail("Usage: posthive templates:use <template_id> --accounts <id,id>");
226
+ if (!accounts?.length)
227
+ fail("--accounts is required (comma-separated account IDs)");
228
+ const schedule = str(flags, "schedule");
229
+ const body = {
230
+ accountIds: accounts,
231
+ draft: !schedule,
232
+ ...(schedule ? { scheduledFor: schedule } : {}),
233
+ ...(str(flags, "content") ? { contentOverride: str(flags, "content") } : {}),
234
+ ...(str(flags, "first-comment") ? { firstCommentOverride: str(flags, "first-comment") } : {}),
235
+ };
236
+ const data = await api("POST", `/api/v1/templates/${encodeURIComponent(id)}/use`, body);
237
+ out({
238
+ ...data,
239
+ _note: schedule ? "Post created from template and scheduled." : "Post created from template as DRAFT.",
240
+ });
241
+ break;
242
+ }
243
+ case "upload": {
244
+ const filePath = positional[0];
245
+ if (!filePath)
246
+ fail("Usage: posthive upload <file_path>");
247
+ let buf;
248
+ try {
249
+ buf = await readFile(filePath);
250
+ }
251
+ catch {
252
+ fail(`Cannot read file: ${filePath}`);
253
+ }
254
+ const form = new FormData();
255
+ form.append("file", new Blob([new Uint8Array(buf)]), basename(filePath));
256
+ let res;
257
+ try {
258
+ res = await fetch(`${API_URL}/upload`, {
259
+ method: "POST",
260
+ headers: { Authorization: `Bearer ${API_KEY}` },
261
+ body: form,
262
+ });
263
+ }
264
+ catch (err) {
265
+ fail(`Cannot reach Posthive API at ${API_URL} — check POSTHIVE_API_URL and your network. (${err instanceof Error ? err.message : String(err)})`);
266
+ }
267
+ const text = await res.text();
268
+ let json;
269
+ try {
270
+ json = JSON.parse(text);
271
+ }
272
+ catch {
273
+ json = { raw: text };
274
+ }
275
+ if (!res.ok) {
276
+ fail(`Upload failed (${res.status}): ${text}`);
277
+ }
278
+ out(json);
279
+ break;
280
+ }
281
+ default:
282
+ fail(`Unknown command: ${command}. Run "posthive help" for usage.`);
283
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "posthive-cli",
3
+ "version": "0.1.0",
4
+ "description": "Posthive CLI — schedule posts to 11 social platforms from the command line or any AI agent (Claude Code, OpenClaw, Cursor)",
5
+ "type": "module",
6
+ "bin": {
7
+ "posthive": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "skills"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "dev": "tsc --watch",
16
+ "start": "node dist/index.js",
17
+ "prepublishOnly": "tsc"
18
+ },
19
+ "keywords": [
20
+ "posthive",
21
+ "cli",
22
+ "social-media",
23
+ "scheduler",
24
+ "ai-agent",
25
+ "skill",
26
+ "claude",
27
+ "openclaw",
28
+ "bluesky",
29
+ "threads",
30
+ "linkedin",
31
+ "instagram",
32
+ "mastodon"
33
+ ],
34
+ "author": "Posthive",
35
+ "license": "AGPL-3.0",
36
+ "homepage": "https://posthive.co/for-developers",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/AstaBlackClove/posthive"
40
+ },
41
+ "bugs": {
42
+ "url": "https://github.com/AstaBlackClove/posthive/issues"
43
+ },
44
+ "devDependencies": {
45
+ "typescript": "^5.5.4",
46
+ "@types/node": "^20.14.12"
47
+ },
48
+ "engines": {
49
+ "node": ">=20"
50
+ }
51
+ }
@@ -0,0 +1,91 @@
1
+ ---
2
+ name: posthive
3
+ description: Schedule social media posts to 11 platforms (Bluesky, Threads, Instagram, LinkedIn, Mastodon, YouTube, Facebook Pages, Pinterest, Telegram, X/Twitter, Nostr) via the Posthive CLI. Use when the user asks to create, schedule, list, update, or delete social media posts, or manage their posting queue.
4
+ ---
5
+
6
+ # Posthive — social media scheduling
7
+
8
+ Schedule posts to 11 social platforms from the command line. Every command outputs structured JSON.
9
+
10
+ ## Setup
11
+
12
+ Requires two environment variables:
13
+
14
+ ```bash
15
+ export POSTHIVE_API_KEY=ph_xxx # Posthive → Settings → API Keys (Pro/Team plan)
16
+ export POSTHIVE_API_URL=https://api.posthive.co # or your self-hosted URL
17
+ ```
18
+
19
+ Run commands with `npx posthive-cli` or `posthive` if installed globally (`npm i -g posthive-cli`).
20
+
21
+ ## Workflow
22
+
23
+ 1. **Always start with `accounts:list`** to get valid account IDs — never guess IDs.
24
+ 2. Create posts with `posts:create`. **Posts are saved as DRAFTS by default** — the user reviews them in Posthive before anything publishes. Only pass `--schedule` when the user explicitly asks to schedule directly.
25
+ 3. Use `posts:approve` to promote a draft to the scheduled queue.
26
+
27
+ ## Commands
28
+
29
+ ```bash
30
+ # List connected accounts (do this first)
31
+ posthive accounts:list
32
+
33
+ # Create a draft post
34
+ posthive posts:create --content "Hello world" --accounts id1,id2
35
+
36
+ # Create with first comment (link/hashtags go here, not the caption)
37
+ posthive posts:create --content "Big launch today" --accounts id1 --first-comment "Details: https://example.com"
38
+
39
+ # Schedule directly (skips draft review — only when user explicitly asks)
40
+ posthive posts:create --content "Hello" --accounts id1 --schedule 2026-07-10T09:00:00Z
41
+
42
+ # With media (upload first, then reference the returned URL)
43
+ posthive upload ./image.png
44
+ posthive posts:create --content "Check this out" --accounts id1 --media https://...returned-url...
45
+
46
+ # Instagram Reel / Story (media required)
47
+ posthive posts:create --content "New reel" --accounts ig_id --media <video_url> --media-type reel
48
+
49
+ # YouTube Short
50
+ posthive posts:create --content "Title here" --accounts yt_id --media <video_url> --youtube-type short
51
+
52
+ # Queue management
53
+ posthive posts:list --status draft
54
+ posthive posts:get <post_id>
55
+ posthive posts:update <post_id> --content "Fixed typo"
56
+ posthive posts:approve <post_id> --schedule 2026-07-10T09:00:00Z
57
+ posthive posts:duplicate <post_id>
58
+ posthive posts:delete <post_id>
59
+
60
+ # Templates
61
+ posthive templates:list
62
+ posthive templates:use <template_id> --accounts id1 --content "Optional override"
63
+
64
+ # Dry run (full pipeline, no real API calls)
65
+ posthive posts:create --content "Test" --accounts id1 --dry-run
66
+ ```
67
+
68
+ ## Platform notes
69
+
70
+ | Platform | Char limit | Notes |
71
+ |----------|-----------|-------|
72
+ | Bluesky | 300 | |
73
+ | Threads | 500 | |
74
+ | Mastodon | 500 | |
75
+ | X (Twitter) | 280 | Pro/Team plan, no links |
76
+ | LinkedIn | 3,000 | Link in first comment performs better |
77
+ | Instagram | 2,200 | Image/video required; use --media-type for reel/story |
78
+ | YouTube | — | Video required; use --youtube-type short or video |
79
+ | Pinterest | 500 | Image required |
80
+ | Facebook Pages | 63,206 | |
81
+ | Telegram | 4,096 | |
82
+ | Nostr | — | |
83
+
84
+ When posting to multiple platforms at once, keep content within the smallest char limit of the selected platforms, or create separate posts per platform with tailored content.
85
+
86
+ ## Best practices
87
+
88
+ - Keep a human in the loop: default to drafts, let the user approve.
89
+ - Timestamps are ISO 8601 UTC (e.g. `2026-07-10T09:00:00Z`). Confirm the user's timezone before scheduling.
90
+ - Put links and hashtag walls in `--first-comment` rather than the main content on LinkedIn and Instagram.
91
+ - On errors, the CLI exits non-zero and prints `{"error": "..."}` to stdout.