posthive-cli 0.1.2 → 0.1.4
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 +11 -1
- package/dist/credentials.js +48 -0
- package/dist/index.js +34 -7
- package/dist/login.js +131 -0
- package/package.json +1 -1
- package/skills/posthive/SKILL.md +10 -1
package/README.md
CHANGED
|
@@ -18,7 +18,17 @@ npx posthive-cli help
|
|
|
18
18
|
|
|
19
19
|
## Setup
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
Sign in once with your browser — no API key copy-pasting:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
posthive login
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
This opens your browser to sign in to Posthive, then stores credentials in `~/.posthive/config.json`. Run `posthive logout` to sign out, or `posthive whoami` to check who's currently logged in.
|
|
28
|
+
|
|
29
|
+
Self-hosted Posthive: `posthive login --api-url http://localhost:3001`
|
|
30
|
+
|
|
31
|
+
Prefer env vars (CI, scripts, or a manually generated key from Settings → API Keys)? They always take priority over the stored login:
|
|
22
32
|
|
|
23
33
|
```bash
|
|
24
34
|
export POSTHIVE_API_KEY=ph_your_key_here
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared credential store for posthive-cli.
|
|
3
|
+
*
|
|
4
|
+
* Stores the API key from `posthive login` in ~/.posthive/config.json
|
|
5
|
+
* (mode 0600 — owner read/write only). posthive-mcp reads the same file
|
|
6
|
+
* as a fallback when POSTHIVE_API_KEY isn't set in the environment.
|
|
7
|
+
*/
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { readFile, writeFile, mkdir, unlink, chmod } from "node:fs/promises";
|
|
11
|
+
function configDir() {
|
|
12
|
+
return join(homedir(), ".posthive");
|
|
13
|
+
}
|
|
14
|
+
function configPath() {
|
|
15
|
+
return join(configDir(), "config.json");
|
|
16
|
+
}
|
|
17
|
+
export async function readCredentials() {
|
|
18
|
+
try {
|
|
19
|
+
const raw = await readFile(configPath(), "utf-8");
|
|
20
|
+
const parsed = JSON.parse(raw);
|
|
21
|
+
if (typeof parsed?.apiKey === "string" && typeof parsed?.apiUrl === "string") {
|
|
22
|
+
return parsed;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export async function writeCredentials(creds) {
|
|
31
|
+
await mkdir(configDir(), { recursive: true });
|
|
32
|
+
await writeFile(configPath(), JSON.stringify(creds, null, 2), "utf-8");
|
|
33
|
+
try {
|
|
34
|
+
await chmod(configPath(), 0o600);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// chmod may not be meaningful on some filesystems (e.g. Windows) — best effort
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export async function clearCredentials() {
|
|
41
|
+
try {
|
|
42
|
+
await unlink(configPath());
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -7,15 +7,21 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Every command outputs structured JSON for easy parsing by LLMs and scripts.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Auth: run `posthive login` once to sign in via your browser (no API key
|
|
11
|
+
* copy-paste needed) — credentials are stored in ~/.posthive/config.json.
|
|
12
|
+
* Or set env vars directly, which always take priority:
|
|
13
|
+
* POSTHIVE_API_KEY — API key from Posthive Settings → API Keys
|
|
14
|
+
* POSTHIVE_API_URL — Base URL of the Posthive API (default: https://api.posthive.co).
|
|
13
15
|
* Set this for self-hosted deployments.
|
|
14
16
|
*/
|
|
15
17
|
import { readFile } from "node:fs/promises";
|
|
16
18
|
import { basename } from "node:path";
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
import { readCredentials } from "./credentials.js";
|
|
20
|
+
import { runLogin, runLogout } from "./login.js";
|
|
21
|
+
const DEFAULT_API_URL = "https://api.posthive.co";
|
|
22
|
+
const stored = process.env.POSTHIVE_API_KEY ? null : await readCredentials();
|
|
23
|
+
const API_KEY = process.env.POSTHIVE_API_KEY ?? stored?.apiKey;
|
|
24
|
+
const API_URL = (process.env.POSTHIVE_API_URL ?? stored?.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, "");
|
|
19
25
|
// ─── Output helpers ──────────────────────────────────────────────────────────
|
|
20
26
|
function out(data) {
|
|
21
27
|
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
|
|
@@ -90,11 +96,15 @@ async function api(method, path, body) {
|
|
|
90
96
|
// ─── Help text ───────────────────────────────────────────────────────────────
|
|
91
97
|
const HELP = {
|
|
92
98
|
usage: "posthive <command> [args] [--flags]",
|
|
99
|
+
auth: "Run `posthive login` once to sign in via your browser. No API key copy-paste needed.",
|
|
93
100
|
env: {
|
|
94
|
-
POSTHIVE_API_KEY: "
|
|
101
|
+
POSTHIVE_API_KEY: "optional — overrides stored login. API key from Posthive Settings → API Keys",
|
|
95
102
|
POSTHIVE_API_URL: "optional — API base URL (default: https://api.posthive.co)",
|
|
96
103
|
},
|
|
97
104
|
commands: {
|
|
105
|
+
"login": "Sign in via your browser. Stores credentials in ~/.posthive/config.json. [--api-url <url>] for self-hosted.",
|
|
106
|
+
"logout": "Clear stored login credentials",
|
|
107
|
+
"whoami": "Show the currently logged-in account",
|
|
98
108
|
"accounts:list": "List connected social accounts and their IDs",
|
|
99
109
|
"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.",
|
|
100
110
|
"posts:list": "List posts. [--status pending|draft|done|failed] [--limit <n>]",
|
|
@@ -119,8 +129,25 @@ const { command, positional, flags } = parseArgs(process.argv.slice(2));
|
|
|
119
129
|
if (command === "help" || command === "--help" || command === "-h") {
|
|
120
130
|
out(HELP);
|
|
121
131
|
}
|
|
132
|
+
if (command === "login") {
|
|
133
|
+
const loginUrl = (str(flags, "api-url") ?? process.env.POSTHIVE_API_URL ?? DEFAULT_API_URL).replace(/\/$/, "");
|
|
134
|
+
try {
|
|
135
|
+
await runLogin(loginUrl);
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (command === "logout") {
|
|
143
|
+
await runLogout();
|
|
144
|
+
process.exit(0);
|
|
145
|
+
}
|
|
122
146
|
if (!API_KEY) {
|
|
123
|
-
fail("
|
|
147
|
+
fail("Not logged in. Run `posthive login`, or set POSTHIVE_API_KEY for CI/scripts.");
|
|
148
|
+
}
|
|
149
|
+
if (command === "whoami") {
|
|
150
|
+
out(await api("GET", "/api/v1/me"));
|
|
124
151
|
}
|
|
125
152
|
switch (command) {
|
|
126
153
|
case "accounts:list": {
|
package/dist/login.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-based OAuth login for posthive-cli.
|
|
3
|
+
*
|
|
4
|
+
* Reuses the existing Posthive OAuth 2.0 Authorization Code + PKCE server
|
|
5
|
+
* (built for the Claude.ai MCP connector) via the standard CLI "loopback"
|
|
6
|
+
* pattern: spin up a local HTTP server, open the browser to /oauth/authorize
|
|
7
|
+
* with a 127.0.0.1 redirect_uri, and receive the code on that local server —
|
|
8
|
+
* same approach used by gh, vercel, and gcloud CLIs. No API key copy-paste.
|
|
9
|
+
*/
|
|
10
|
+
import { randomBytes, createHash } from "node:crypto";
|
|
11
|
+
import { createServer } from "node:http";
|
|
12
|
+
import { exec } from "node:child_process";
|
|
13
|
+
import { writeCredentials, clearCredentials, readCredentials } from "./credentials.js";
|
|
14
|
+
const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
15
|
+
function base64url(buf) {
|
|
16
|
+
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
17
|
+
}
|
|
18
|
+
function openBrowser(url) {
|
|
19
|
+
const cmd = process.platform === "darwin" ? `open "${url}"`
|
|
20
|
+
: process.platform === "win32" ? `start "" "${url}"`
|
|
21
|
+
: `xdg-open "${url}"`;
|
|
22
|
+
exec(cmd, () => { }); // best-effort; URL is always printed as a fallback
|
|
23
|
+
}
|
|
24
|
+
const SUCCESS_HTML = `<!DOCTYPE html><html><head><title>Posthive</title><style>
|
|
25
|
+
body{font-family:system-ui,-apple-system,sans-serif;background:#0a0a0a;color:#ededed;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}
|
|
26
|
+
.card{text-align:center}
|
|
27
|
+
h1{font-size:20px;margin-bottom:8px}
|
|
28
|
+
p{color:#888;font-size:14px}
|
|
29
|
+
</style></head><body><div class="card"><h1>✓ Logged in to Posthive</h1><p>You can close this window and return to your terminal.</p></div></body></html>`;
|
|
30
|
+
const DENIED_HTML = `<!DOCTYPE html><html><head><title>Posthive</title><style>
|
|
31
|
+
body{font-family:system-ui,-apple-system,sans-serif;background:#0a0a0a;color:#ededed;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}
|
|
32
|
+
.card{text-align:center}
|
|
33
|
+
h1{font-size:20px;margin-bottom:8px;color:#ef4444}
|
|
34
|
+
p{color:#888;font-size:14px}
|
|
35
|
+
</style></head><body><div class="card"><h1>Login cancelled</h1><p>You can close this window.</p></div></body></html>`;
|
|
36
|
+
export async function runLogin(apiUrl) {
|
|
37
|
+
const verifier = base64url(randomBytes(32));
|
|
38
|
+
const challenge = base64url(createHash("sha256").update(verifier).digest());
|
|
39
|
+
const state = base64url(randomBytes(16));
|
|
40
|
+
let redirectUri = "";
|
|
41
|
+
const code = await new Promise((resolve, reject) => {
|
|
42
|
+
const server = createServer((req, res) => {
|
|
43
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
44
|
+
if (url.pathname !== "/callback") {
|
|
45
|
+
res.writeHead(404).end();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const returnedState = url.searchParams.get("state");
|
|
49
|
+
const authCode = url.searchParams.get("code");
|
|
50
|
+
const error = url.searchParams.get("error");
|
|
51
|
+
if (error) {
|
|
52
|
+
res.writeHead(200, { "Content-Type": "text/html" }).end(DENIED_HTML);
|
|
53
|
+
server.close();
|
|
54
|
+
reject(new Error(url.searchParams.get("error_description") ?? error));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (!authCode || returnedState !== state) {
|
|
58
|
+
res.writeHead(400, { "Content-Type": "text/html" }).end(DENIED_HTML);
|
|
59
|
+
server.close();
|
|
60
|
+
reject(new Error("State mismatch or missing authorization code."));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
res.writeHead(200, { "Content-Type": "text/html" }).end(SUCCESS_HTML);
|
|
64
|
+
server.close();
|
|
65
|
+
resolve(authCode);
|
|
66
|
+
});
|
|
67
|
+
server.on("error", reject);
|
|
68
|
+
server.listen(0, "127.0.0.1", () => {
|
|
69
|
+
const address = server.address();
|
|
70
|
+
if (!address || typeof address === "string") {
|
|
71
|
+
reject(new Error("Failed to start local login server"));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
redirectUri = `http://127.0.0.1:${address.port}/callback`;
|
|
75
|
+
const authorizeUrl = new URL(`${apiUrl}/oauth/authorize`);
|
|
76
|
+
authorizeUrl.searchParams.set("redirect_uri", redirectUri);
|
|
77
|
+
authorizeUrl.searchParams.set("code_challenge", challenge);
|
|
78
|
+
authorizeUrl.searchParams.set("code_challenge_method", "S256");
|
|
79
|
+
authorizeUrl.searchParams.set("state", state);
|
|
80
|
+
authorizeUrl.searchParams.set("client_id", "Posthive CLI");
|
|
81
|
+
process.stdout.write("Opening browser to sign in...\n");
|
|
82
|
+
process.stdout.write(`If it doesn't open automatically, visit:\n${authorizeUrl.toString()}\n\n`);
|
|
83
|
+
openBrowser(authorizeUrl.toString());
|
|
84
|
+
const timer = setTimeout(() => {
|
|
85
|
+
server.close();
|
|
86
|
+
reject(new Error("Login timed out after 5 minutes. Run `posthive login` again."));
|
|
87
|
+
}, LOGIN_TIMEOUT_MS);
|
|
88
|
+
timer.unref();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
const tokenRes = await fetch(`${apiUrl}/oauth/token`, {
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
94
|
+
body: new URLSearchParams({
|
|
95
|
+
grant_type: "authorization_code",
|
|
96
|
+
code,
|
|
97
|
+
code_verifier: verifier,
|
|
98
|
+
redirect_uri: redirectUri,
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
if (!tokenRes.ok) {
|
|
102
|
+
const text = await tokenRes.text();
|
|
103
|
+
throw new Error(`Token exchange failed: ${text}`);
|
|
104
|
+
}
|
|
105
|
+
const { access_token: apiKey } = (await tokenRes.json());
|
|
106
|
+
let email;
|
|
107
|
+
try {
|
|
108
|
+
const meRes = await fetch(`${apiUrl}/api/v1/me`, {
|
|
109
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
110
|
+
});
|
|
111
|
+
if (meRes.ok) {
|
|
112
|
+
const { user } = (await meRes.json());
|
|
113
|
+
email = user.email;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Non-fatal — login still succeeds without the email confirmation
|
|
118
|
+
}
|
|
119
|
+
await writeCredentials({ apiKey, apiUrl, email });
|
|
120
|
+
process.stdout.write(`✓ Logged in${email ? ` as ${email}` : ""}\n`);
|
|
121
|
+
}
|
|
122
|
+
export async function runLogout() {
|
|
123
|
+
const existing = await readCredentials();
|
|
124
|
+
const cleared = await clearCredentials();
|
|
125
|
+
if (cleared) {
|
|
126
|
+
process.stdout.write(`✓ Logged out${existing?.email ? ` (${existing.email})` : ""}\n`);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
process.stdout.write("Not logged in.\n");
|
|
130
|
+
}
|
|
131
|
+
}
|
package/package.json
CHANGED
package/skills/posthive/SKILL.md
CHANGED
|
@@ -9,8 +9,12 @@ Schedule posts to 11 social platforms from the command line. Every command outpu
|
|
|
9
9
|
|
|
10
10
|
## Setup
|
|
11
11
|
|
|
12
|
+
If the user isn't already logged in, run `posthive login` — it opens their browser to sign in and stores credentials in `~/.posthive/config.json`. No API key needed. Check with `posthive whoami` first if unsure.
|
|
13
|
+
|
|
14
|
+
Alternatively, env vars always take priority over the stored login (useful for CI or scripts):
|
|
15
|
+
|
|
12
16
|
```bash
|
|
13
|
-
export POSTHIVE_API_KEY=ph_xxx #
|
|
17
|
+
export POSTHIVE_API_KEY=ph_xxx # Posthive → Settings → API Keys (Pro/Team plan)
|
|
14
18
|
export POSTHIVE_API_URL=http://localhost:3001 # optional — only for self-hosted Posthive, defaults to https://api.posthive.co
|
|
15
19
|
```
|
|
16
20
|
|
|
@@ -25,6 +29,11 @@ Run commands with `npx posthive-cli` or `posthive` if installed globally (`npm i
|
|
|
25
29
|
## Commands
|
|
26
30
|
|
|
27
31
|
```bash
|
|
32
|
+
# Auth (only if not already logged in)
|
|
33
|
+
posthive login
|
|
34
|
+
posthive whoami
|
|
35
|
+
posthive logout
|
|
36
|
+
|
|
28
37
|
# List connected accounts (do this first)
|
|
29
38
|
posthive accounts:list
|
|
30
39
|
|