hookpost 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.
Files changed (3) hide show
  1. package/README.md +88 -0
  2. package/dist/index.js +241 -0
  3. package/package.json +38 -0
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # hookpost
2
+
3
+ Schedule and publish social media posts from your terminal, powered by [Hookpost](https://hookpost.hookstep.in).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g hookpost
9
+ ```
10
+
11
+ ## Sign in
12
+
13
+ ```bash
14
+ hookpost auth login
15
+ ```
16
+
17
+ It asks for your API key — find it in Hookpost under **Settings → Public API** — verifies it before saving, and stores it at `~/.hookpost/config.json` with owner-only permissions.
18
+
19
+ In CI, skip the login and set an environment variable instead:
20
+
21
+ ```bash
22
+ export HOOKPOST_API_KEY="your_key"
23
+ ```
24
+
25
+ ## Commands
26
+
27
+ ```
28
+ auth login Store your API key
29
+ auth status Show who you are signed in as
30
+ auth logout Remove the stored key
31
+
32
+ channels List your connected channels
33
+ posts [--days N] List upcoming posts (default 7 days)
34
+ post <text> Create a post
35
+ delete <postId> Delete a post
36
+ slot [channelId] Show the next free slot
37
+ ```
38
+
39
+ ### Creating posts
40
+
41
+ ```bash
42
+ # Schedule into your next free slot
43
+ hookpost post "Shipped something small today." --channel abc123
44
+
45
+ # Pick an exact time
46
+ hookpost post "Launch day" --channel abc123 --at 2026-09-10T09:00:00Z
47
+
48
+ # Publish immediately
49
+ hookpost post "Live now" --channel abc123 --now
50
+
51
+ # Save a draft
52
+ hookpost post "Rough idea" --channel abc123 --draft
53
+ ```
54
+
55
+ Run `hookpost channels` to get channel ids.
56
+
57
+ Omit `--at` on a scheduled post and the CLI asks Hookpost for the next free slot in your posting schedule — the same default the web app uses.
58
+
59
+ ### Channel settings
60
+
61
+ Some channels need extra fields. YouTube wants a title and visibility:
62
+
63
+ ```bash
64
+ hookpost post "New video" --channel abc123 --now \
65
+ --settings '{"__type":"youtube","title":"My video","type":"private","selfDeclaredMadeForKids":"no"}'
66
+ ```
67
+
68
+ Call `/public/v1/integration-settings/:id` to see what a given channel expects.
69
+
70
+ ## Environment
71
+
72
+ | Variable | Purpose |
73
+ |---|---|
74
+ | `HOOKPOST_API_KEY` | Use instead of `auth login` — handy in CI |
75
+ | `HOOKPOST_API_URL` | Override the API host when self-hosting |
76
+
77
+ ## Security
78
+
79
+ The API key grants full access to your organisation's channels and posts. Keep it out of shared shells and public repos; prefer `HOOKPOST_API_KEY` from a secret store in automation.
80
+
81
+ ## Documentation
82
+
83
+ - [Public API reference](https://hookpost.hookstep.in/docs/public-api)
84
+ - [OAuth apps](https://hookpost.hookstep.in/docs/oauth) — for acting on other people's accounts
85
+
86
+ ## Licence
87
+
88
+ AGPL-3.0
package/dist/index.js ADDED
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const fs_1 = require("fs");
5
+ const os_1 = require("os");
6
+ const path_1 = require("path");
7
+ const readline_1 = require("readline");
8
+ const CONFIG_DIR = (0, path_1.join)((0, os_1.homedir)(), '.hookpost');
9
+ const CONFIG_FILE = (0, path_1.join)(CONFIG_DIR, 'config.json');
10
+ const DEFAULT_HOST = 'https://hookpost.hookstep.in/api';
11
+ const c = {
12
+ dim: (s) => `\x1b[2m${s}\x1b[0m`,
13
+ bold: (s) => `\x1b[1m${s}\x1b[0m`,
14
+ red: (s) => `\x1b[31m${s}\x1b[0m`,
15
+ green: (s) => `\x1b[32m${s}\x1b[0m`,
16
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`,
17
+ };
18
+ async function readConfig() {
19
+ // Environment wins over the stored file so CI can run without `auth login`.
20
+ const fromEnv = {
21
+ apiKey: process.env.HOOKPOST_API_KEY,
22
+ host: process.env.HOOKPOST_API_URL,
23
+ };
24
+ let stored = {};
25
+ try {
26
+ stored = JSON.parse(await fs_1.promises.readFile(CONFIG_FILE, 'utf8'));
27
+ }
28
+ catch {
29
+ /* no config yet */
30
+ }
31
+ return {
32
+ apiKey: fromEnv.apiKey || stored.apiKey,
33
+ host: fromEnv.host || stored.host || DEFAULT_HOST,
34
+ };
35
+ }
36
+ async function writeConfig(cfg) {
37
+ await fs_1.promises.mkdir((0, path_1.dirname)(CONFIG_FILE), { recursive: true });
38
+ await fs_1.promises.writeFile(CONFIG_FILE, JSON.stringify(cfg, null, 2));
39
+ // The key is a full-access credential, so keep it owner-readable only.
40
+ await fs_1.promises.chmod(CONFIG_FILE, 0o600);
41
+ }
42
+ async function api(method, path, body) {
43
+ const { apiKey, host } = await readConfig();
44
+ if (!apiKey) {
45
+ throw new Error('Not signed in. Run `hookpost auth login`, or set HOOKPOST_API_KEY.');
46
+ }
47
+ const res = await fetch(`${String(host).replace(/\/+$/, '')}/public/v1${path}`, {
48
+ method,
49
+ headers: {
50
+ 'Content-Type': 'application/json',
51
+ // Hookpost takes the raw key - a Bearer prefix is rejected.
52
+ Authorization: apiKey,
53
+ },
54
+ body: body ? JSON.stringify(body) : undefined,
55
+ });
56
+ const text = await res.text();
57
+ let data;
58
+ try {
59
+ data = JSON.parse(text);
60
+ }
61
+ catch {
62
+ data = text;
63
+ }
64
+ if (!res.ok) {
65
+ const msg = data?.msg || data?.message || text || `HTTP ${res.status}`;
66
+ throw new Error(`${res.status} ${msg}`);
67
+ }
68
+ return data;
69
+ }
70
+ function ask(question) {
71
+ const rl = (0, readline_1.createInterface)({ input: process.stdin, output: process.stdout });
72
+ return new Promise((resolve) => rl.question(question, (a) => {
73
+ rl.close();
74
+ resolve(a.trim());
75
+ }));
76
+ }
77
+ function flag(args, name) {
78
+ const i = args.indexOf(`--${name}`);
79
+ return i !== -1 ? args[i + 1] : undefined;
80
+ }
81
+ const HELP = `
82
+ ${c.bold('hookpost')} - schedule and publish social media posts from your terminal
83
+
84
+ ${c.bold('Usage')}
85
+ hookpost <command> [options]
86
+
87
+ ${c.bold('Commands')}
88
+ auth login Store your API key
89
+ auth status Show who you are signed in as
90
+ auth logout Remove the stored key
91
+
92
+ channels List your connected channels
93
+ posts List upcoming posts
94
+ post <text> Create a post
95
+ delete <postId> Delete a post
96
+ slot [channelId] Show the next free slot
97
+
98
+ ${c.bold('post options')}
99
+ --channel <id> Channel to post to (required)
100
+ --at <ISO date> When to publish; omitted uses your next free slot
101
+ --now Publish immediately
102
+ --draft Save as a draft
103
+ --settings <json> Per-channel settings, e.g. YouTube title/visibility
104
+
105
+ ${c.bold('Examples')}
106
+ hookpost channels
107
+ hookpost post "Shipped something small today." --channel abc123
108
+ hookpost post "Live now" --channel abc123 --now
109
+ hookpost posts --days 14
110
+
111
+ ${c.bold('Environment')}
112
+ HOOKPOST_API_KEY Use instead of \`auth login\` (handy in CI)
113
+ HOOKPOST_API_URL Override the API host when self-hosting
114
+
115
+ Docs: https://hookpost.hookstep.in/docs/public-api
116
+ `;
117
+ async function main() {
118
+ const argv = process.argv.slice(2);
119
+ const [cmd, sub] = argv;
120
+ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
121
+ console.log(HELP);
122
+ return;
123
+ }
124
+ if (cmd === '--version' || cmd === '-v') {
125
+ console.log(require('../package.json').version);
126
+ return;
127
+ }
128
+ if (cmd === 'auth') {
129
+ if (sub === 'login') {
130
+ const key = await ask('API key (Settings -> Public API): ');
131
+ if (!key)
132
+ throw new Error('No key entered.');
133
+ const host = (await ask(`API host [${DEFAULT_HOST}]: `)) || DEFAULT_HOST;
134
+ await writeConfig({ apiKey: key, host });
135
+ // Prove the key works now rather than failing on first real use.
136
+ await api('GET', '/is-connected');
137
+ console.log(c.green(`Signed in. Key saved to ${CONFIG_FILE}`));
138
+ return;
139
+ }
140
+ if (sub === 'status') {
141
+ const { apiKey, host } = await readConfig();
142
+ if (!apiKey) {
143
+ console.log('Not signed in.');
144
+ return;
145
+ }
146
+ await api('GET', '/is-connected');
147
+ console.log(`${c.green('Signed in')} host=${host} key=${apiKey.slice(0, 8)}...`);
148
+ return;
149
+ }
150
+ if (sub === 'logout') {
151
+ await fs_1.promises.rm(CONFIG_FILE, { force: true });
152
+ console.log('Signed out.');
153
+ return;
154
+ }
155
+ throw new Error('Usage: hookpost auth <login|status|logout>');
156
+ }
157
+ if (cmd === 'channels') {
158
+ const list = await api('GET', '/integrations');
159
+ if (!Array.isArray(list) || !list.length) {
160
+ console.log('No channels connected yet.');
161
+ return;
162
+ }
163
+ for (const ch of list) {
164
+ console.log(`${c.cyan(ch.id)} ${String(ch.name).padEnd(24)} ${c.dim(ch.identifier || ch.providerIdentifier || '')}`);
165
+ }
166
+ return;
167
+ }
168
+ if (cmd === 'posts') {
169
+ const days = Number(flag(argv, 'days') || 7);
170
+ const startDate = new Date().toISOString();
171
+ const endDate = new Date(Date.now() + days * 864e5).toISOString();
172
+ const res = await api('GET', `/posts?startDate=${encodeURIComponent(startDate)}&endDate=${encodeURIComponent(endDate)}`);
173
+ const posts = res?.posts || res || [];
174
+ if (!posts.length) {
175
+ console.log(`No posts in the next ${days} days.`);
176
+ return;
177
+ }
178
+ for (const p of posts) {
179
+ const when = new Date(p.publishDate).toISOString().replace('T', ' ').slice(0, 16);
180
+ const text = String(p.content || '').replace(/<[^>]*>/g, '').slice(0, 50);
181
+ console.log(`${c.dim(when)} ${String(p.state).padEnd(9)} ${c.cyan(p.id)} ${text}`);
182
+ }
183
+ return;
184
+ }
185
+ if (cmd === 'post') {
186
+ const text = argv[1];
187
+ if (!text || text.startsWith('--')) {
188
+ throw new Error('Usage: hookpost post "your text" --channel <id>');
189
+ }
190
+ const channel = flag(argv, 'channel');
191
+ if (!channel)
192
+ throw new Error('--channel is required. Run `hookpost channels` to list them.');
193
+ const type = argv.includes('--now')
194
+ ? 'now'
195
+ : argv.includes('--draft')
196
+ ? 'draft'
197
+ : 'schedule';
198
+ let date = flag(argv, 'at');
199
+ if (type === 'schedule' && !date) {
200
+ const slot = await api('GET', `/find-slot/${channel}`);
201
+ date = slot?.date;
202
+ console.log(c.dim(`No --at given, using next free slot: ${date}`));
203
+ }
204
+ const settingsRaw = flag(argv, 'settings');
205
+ const settings = settingsRaw ? JSON.parse(settingsRaw) : undefined;
206
+ const res = await api('POST', '/posts', {
207
+ type,
208
+ date: date || new Date().toISOString(),
209
+ shortLink: false,
210
+ tags: [],
211
+ posts: [
212
+ {
213
+ integration: { id: channel },
214
+ value: [{ content: text, image: [] }],
215
+ ...(settings ? { settings } : {}),
216
+ },
217
+ ],
218
+ });
219
+ console.log(c.green('Created.'), JSON.stringify(res));
220
+ return;
221
+ }
222
+ if (cmd === 'delete') {
223
+ const id = argv[1];
224
+ if (!id)
225
+ throw new Error('Usage: hookpost delete <postId>');
226
+ await api('DELETE', `/posts/${id}`);
227
+ console.log(c.green(`Deleted ${id}`));
228
+ return;
229
+ }
230
+ if (cmd === 'slot') {
231
+ const id = argv[1];
232
+ const res = await api('GET', id ? `/find-slot/${id}` : '/is-connected');
233
+ console.log(JSON.stringify(res));
234
+ return;
235
+ }
236
+ throw new Error(`Unknown command: ${cmd}\nRun \`hookpost help\`.`);
237
+ }
238
+ main().catch((err) => {
239
+ console.error(c.red(`Error: ${err.message}`));
240
+ process.exit(1);
241
+ });
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "hookpost",
3
+ "version": "0.1.0",
4
+ "description": "Hookpost CLI - schedule and publish social media posts from your terminal",
5
+ "keywords": [
6
+ "hookpost",
7
+ "cli",
8
+ "social media",
9
+ "scheduling",
10
+ "social media scheduler"
11
+ ],
12
+ "license": "AGPL-3.0",
13
+ "author": "JR Consulting Co.",
14
+ "homepage": "https://hookpost.hookstep.in/docs/public-api",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/jatinder14/hookpost.git",
18
+ "directory": "apps/cli"
19
+ },
20
+ "bin": {
21
+ "hookpost": "dist/index.js"
22
+ },
23
+ "main": "dist/index.js",
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "scripts": {
28
+ "build": "tsc",
29
+ "prepublishOnly": "npm run build"
30
+ },
31
+ "devDependencies": {
32
+ "typescript": "^5.6.0",
33
+ "@types/node": "^22.0.0"
34
+ },
35
+ "engines": {
36
+ "node": ">=18"
37
+ }
38
+ }