superx-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/dist/index.js ADDED
@@ -0,0 +1,1280 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/index.ts
27
+ var import_yargs = __toESM(require("yargs"));
28
+ var import_helpers = require("yargs/helpers");
29
+
30
+ // src/api.ts
31
+ var ApiError = class extends Error {
32
+ constructor(status2, code, message, retryAfter = null) {
33
+ super(message);
34
+ this.name = "ApiError";
35
+ this.status = status2;
36
+ this.code = code;
37
+ this.retryAfter = retryAfter;
38
+ }
39
+ };
40
+ var SuperXAPI = class {
41
+ constructor(config) {
42
+ /** Rate-limit headers from the most recent authenticated response. */
43
+ this.lastRateLimit = null;
44
+ this.apiKey = config.apiKey;
45
+ this.apiUrl = config.apiUrl.replace(/\/+$/, "");
46
+ }
47
+ buildUrl(endpoint, query) {
48
+ let url = `${this.apiUrl}${endpoint}`;
49
+ if (query) {
50
+ const params = new URLSearchParams();
51
+ for (const [key, value] of Object.entries(query)) {
52
+ if (value !== void 0 && value !== null && value !== "") {
53
+ params.set(key, String(value));
54
+ }
55
+ }
56
+ const qs = params.toString();
57
+ if (qs) url += `?${qs}`;
58
+ }
59
+ return url;
60
+ }
61
+ captureRateLimit(headers) {
62
+ const num = (name) => {
63
+ const v = headers.get(name);
64
+ if (v === null) return null;
65
+ const n = Number(v);
66
+ return Number.isFinite(n) ? n : null;
67
+ };
68
+ if (headers.get("x-ratelimit-limit") !== null) {
69
+ this.lastRateLimit = {
70
+ limit: num("x-ratelimit-limit"),
71
+ remaining: num("x-ratelimit-remaining"),
72
+ reset: num("x-ratelimit-reset")
73
+ };
74
+ }
75
+ }
76
+ /** Perform a request. Non-2xx responses throw ApiError with the API's code. */
77
+ async request(endpoint, options = {}) {
78
+ const url = this.buildUrl(endpoint, options.query);
79
+ const headers = {
80
+ Authorization: `Bearer ${this.apiKey}`,
81
+ ...options.headers
82
+ };
83
+ if (options.body !== void 0) {
84
+ headers["Content-Type"] = "application/json";
85
+ }
86
+ let response;
87
+ try {
88
+ response = await fetch(url, {
89
+ method: options.method || "GET",
90
+ headers,
91
+ body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
92
+ });
93
+ } catch (err) {
94
+ throw new ApiError(0, "network_error", `Could not reach ${this.apiUrl} (${err?.message || err})`);
95
+ }
96
+ this.captureRateLimit(response.headers);
97
+ const text = await response.text();
98
+ let json = null;
99
+ try {
100
+ json = text ? JSON.parse(text) : null;
101
+ } catch {
102
+ json = null;
103
+ }
104
+ if (!response.ok) {
105
+ const retryHeader = response.headers.get("retry-after");
106
+ const retryAfter = retryHeader !== null && Number.isFinite(Number(retryHeader)) ? Number(retryHeader) : null;
107
+ let code = `http_${response.status}`;
108
+ let message = `Request failed with HTTP ${response.status}`;
109
+ const errField = json?.error;
110
+ if (errField && typeof errField === "object" && typeof errField.code === "string") {
111
+ code = errField.code;
112
+ if (typeof errField.message === "string") message = errField.message;
113
+ } else if (typeof errField === "string") {
114
+ code = errField.split(":")[0].trim() || code;
115
+ message = errField;
116
+ }
117
+ throw new ApiError(response.status, code, message, retryAfter);
118
+ }
119
+ return { status: response.status, headers: response.headers, json };
120
+ }
121
+ // --- Identity ---
122
+ async me() {
123
+ return (await this.request("/me")).json;
124
+ }
125
+ async accounts(query = {}) {
126
+ return (await this.request("/accounts", { query })).json;
127
+ }
128
+ // --- Posts and analytics ---
129
+ async listPosts(query = {}) {
130
+ return (await this.request("/posts", { query })).json;
131
+ }
132
+ async postsAnalytics(query = {}) {
133
+ return (await this.request("/posts/analytics", { query })).json;
134
+ }
135
+ async listReplies(query = {}) {
136
+ return (await this.request("/replies", { query })).json;
137
+ }
138
+ async receivedReplies(query = {}) {
139
+ return (await this.request("/replies/received", { query })).json;
140
+ }
141
+ // --- Inspiration ---
142
+ async searchInspiration(query = {}) {
143
+ return (await this.request("/inspiration", { query })).json;
144
+ }
145
+ // --- Contacts ---
146
+ async listContacts(query = {}) {
147
+ return (await this.request("/contacts", { query })).json;
148
+ }
149
+ async contactReplies(contactId, query = {}) {
150
+ return (await this.request(`/contacts/${encodeURIComponent(contactId)}/replies`, { query })).json;
151
+ }
152
+ // --- Contact lists ---
153
+ async listContactLists(query = {}) {
154
+ return (await this.request("/contact-lists", { query })).json;
155
+ }
156
+ async listListMembers(listId, query = {}) {
157
+ return (await this.request(`/contact-lists/${encodeURIComponent(listId)}/members`, { query })).json;
158
+ }
159
+ async addListMember(listId, body) {
160
+ return (await this.request(`/contact-lists/${encodeURIComponent(listId)}/members`, { method: "POST", body })).json;
161
+ }
162
+ async removeListMember(listId, memberId) {
163
+ return (await this.request(
164
+ `/contact-lists/${encodeURIComponent(listId)}/members/${encodeURIComponent(memberId)}`,
165
+ { method: "DELETE" }
166
+ )).json;
167
+ }
168
+ // --- Signals ---
169
+ async listSignalAgents(query = {}) {
170
+ return (await this.request("/signals/agents", { query })).json;
171
+ }
172
+ async listSignalLeads(query = {}) {
173
+ return (await this.request("/signals/leads", { query })).json;
174
+ }
175
+ async createSignalAgent(body, idempotencyKey) {
176
+ const headers = {};
177
+ if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
178
+ const res = await this.request("/signals/agents", { method: "POST", body, headers });
179
+ return { json: res.json, replayed: res.headers.get("idempotency-replayed") === "true" };
180
+ }
181
+ async setSignalAgentStatus(id, status2) {
182
+ return (await this.request(`/signals/agents/${id}`, { method: "PATCH", body: { status: status2 } })).json;
183
+ }
184
+ async deleteSignalAgent(id) {
185
+ return (await this.request(`/signals/agents/${id}`, { method: "DELETE" })).json;
186
+ }
187
+ // --- Scheduled posts ---
188
+ async listScheduled(query = {}) {
189
+ return (await this.request("/scheduled-posts", { query })).json;
190
+ }
191
+ async createScheduled(body, idempotencyKey) {
192
+ const headers = {};
193
+ if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
194
+ const res = await this.request("/scheduled-posts", { method: "POST", body, headers });
195
+ return { json: res.json, replayed: res.headers.get("idempotency-replayed") === "true" };
196
+ }
197
+ async updateScheduled(id, body) {
198
+ return (await this.request(`/scheduled-posts/${encodeURIComponent(id)}`, { method: "PATCH", body })).json;
199
+ }
200
+ async deleteScheduled(id) {
201
+ return (await this.request(`/scheduled-posts/${encodeURIComponent(id)}`, { method: "DELETE" })).json;
202
+ }
203
+ // --- Media ---
204
+ async createMediaUpload(body) {
205
+ return (await this.request("/media", { method: "POST", body })).json;
206
+ }
207
+ // --- Tags ---
208
+ async listTags() {
209
+ return (await this.request("/tags")).json;
210
+ }
211
+ async createTag(body) {
212
+ return (await this.request("/tags", { method: "POST", body })).json;
213
+ }
214
+ async updateTag(id, body) {
215
+ return (await this.request(`/tags/${encodeURIComponent(id)}`, { method: "PATCH", body })).json;
216
+ }
217
+ async deleteTag(id) {
218
+ return (await this.request(`/tags/${encodeURIComponent(id)}`, { method: "DELETE" })).json;
219
+ }
220
+ // --- Articles ---
221
+ async listArticles(query = {}) {
222
+ return (await this.request("/articles", { query })).json;
223
+ }
224
+ async getArticle(id) {
225
+ return (await this.request(`/articles/${encodeURIComponent(id)}`)).json;
226
+ }
227
+ async createArticle(body) {
228
+ return (await this.request("/articles", { method: "POST", body })).json;
229
+ }
230
+ async updateArticle(id, body) {
231
+ return (await this.request(`/articles/${encodeURIComponent(id)}`, { method: "PATCH", body })).json;
232
+ }
233
+ async deleteArticle(id) {
234
+ return (await this.request(`/articles/${encodeURIComponent(id)}`, { method: "DELETE" })).json;
235
+ }
236
+ async publishArticle(id) {
237
+ return (await this.request(`/articles/${encodeURIComponent(id)}/publish`, { method: "POST", body: {} })).json;
238
+ }
239
+ async scheduleArticle(id, body) {
240
+ return (await this.request(`/articles/${encodeURIComponent(id)}/schedule`, { method: "POST", body })).json;
241
+ }
242
+ async unscheduleArticle(id) {
243
+ return (await this.request(`/articles/${encodeURIComponent(id)}/unschedule`, { method: "POST", body: {} })).json;
244
+ }
245
+ async generateArticleCover(id, body) {
246
+ return (await this.request(`/articles/${encodeURIComponent(id)}/cover`, { method: "POST", body })).json;
247
+ }
248
+ // --- Docs (unauthenticated markdown) ---
249
+ async docs() {
250
+ const url = this.buildUrl("/docs");
251
+ let response;
252
+ try {
253
+ response = await fetch(url);
254
+ } catch (err) {
255
+ throw new ApiError(0, "network_error", `Could not reach ${this.apiUrl} (${err?.message || err})`);
256
+ }
257
+ if (!response.ok) {
258
+ throw new ApiError(response.status, `http_${response.status}`, `Docs request failed with HTTP ${response.status}`);
259
+ }
260
+ return response.text();
261
+ }
262
+ };
263
+ function printJson(value) {
264
+ process.stdout.write(JSON.stringify(value, null, 2) + "\n");
265
+ }
266
+ function note(message) {
267
+ process.stderr.write(message + "\n");
268
+ }
269
+
270
+ // src/commands/auth.ts
271
+ var readline = __toESM(require("readline"));
272
+
273
+ // src/config.ts
274
+ var fs = __toESM(require("fs"));
275
+ var os = __toESM(require("os"));
276
+ var path = __toESM(require("path"));
277
+ var DEFAULT_API_URL = "https://api.superx.so/v1";
278
+ var CONFIG_DIR = path.join(os.homedir(), ".superx");
279
+ var CREDENTIALS_PATH = path.join(CONFIG_DIR, "credentials.json");
280
+ function loadCredentials() {
281
+ try {
282
+ const raw = fs.readFileSync(CREDENTIALS_PATH, "utf8");
283
+ const parsed = JSON.parse(raw);
284
+ if (parsed && typeof parsed.apiKey === "string" && parsed.apiKey) {
285
+ return {
286
+ apiKey: parsed.apiKey,
287
+ apiUrl: typeof parsed.apiUrl === "string" && parsed.apiUrl ? parsed.apiUrl : DEFAULT_API_URL
288
+ };
289
+ }
290
+ return null;
291
+ } catch {
292
+ return null;
293
+ }
294
+ }
295
+ function saveCredentials(creds) {
296
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
297
+ fs.writeFileSync(CREDENTIALS_PATH, JSON.stringify(creds, null, 2) + "\n", { mode: 384 });
298
+ fs.chmodSync(CONFIG_DIR, 448);
299
+ fs.chmodSync(CREDENTIALS_PATH, 384);
300
+ }
301
+ function deleteCredentials() {
302
+ try {
303
+ fs.unlinkSync(CREDENTIALS_PATH);
304
+ return true;
305
+ } catch {
306
+ return false;
307
+ }
308
+ }
309
+ function resolveApiUrl(storedUrl) {
310
+ const envUrl = process.env.SUPERX_API_URL;
311
+ if (envUrl) return envUrl.replace(/\/+$/, "");
312
+ if (storedUrl) return storedUrl.replace(/\/+$/, "");
313
+ return DEFAULT_API_URL;
314
+ }
315
+ function getConfig() {
316
+ const creds = loadCredentials();
317
+ if (creds) {
318
+ return { apiKey: creds.apiKey, apiUrl: resolveApiUrl(creds.apiUrl), source: "credentials_file" };
319
+ }
320
+ const envKey = process.env.SUPERX_API_KEY;
321
+ if (envKey) {
322
+ return { apiKey: envKey, apiUrl: resolveApiUrl(), source: "env" };
323
+ }
324
+ process.stderr.write("Not authenticated. Either:\n");
325
+ process.stderr.write(" 1. Run: superx login\n");
326
+ process.stderr.write(" 2. Or set: export SUPERX_API_KEY=sxk_...\n");
327
+ process.stderr.write("Create an API key at https://app.superx.so/account?tab=developers\n");
328
+ process.exit(1);
329
+ }
330
+
331
+ // src/commands/auth.ts
332
+ function promptForKey() {
333
+ return new Promise((resolve) => {
334
+ const rl = readline.createInterface({
335
+ input: process.stdin,
336
+ output: process.stderr
337
+ // keep the prompt off stdout
338
+ });
339
+ rl.question("Paste your API key (sxk_...): ", (answer) => {
340
+ rl.close();
341
+ resolve(answer.trim());
342
+ });
343
+ });
344
+ }
345
+ async function login(argv) {
346
+ const apiUrl = resolveApiUrl(loadCredentials()?.apiUrl);
347
+ let key = (argv.key || "").trim();
348
+ if (!key) {
349
+ note("Create or copy an API key at:");
350
+ note(" https://app.superx.so/account?tab=developers");
351
+ note("");
352
+ key = await promptForKey();
353
+ }
354
+ if (!key) {
355
+ note("No API key provided.");
356
+ process.exit(1);
357
+ }
358
+ if (!key.startsWith("sxk_")) {
359
+ note("That does not look like a SuperX API key (expected an sxk_ prefix).");
360
+ process.exit(1);
361
+ }
362
+ const api = new SuperXAPI({ apiKey: key, apiUrl });
363
+ const me2 = await api.me();
364
+ saveCredentials({ apiKey: key, apiUrl });
365
+ note(`Logged in as ${me2?.data?.owner?.name || me2?.data?.owner?.id || "unknown"}.`);
366
+ note(`Credentials saved to ${CREDENTIALS_PATH}`);
367
+ printJson(me2);
368
+ }
369
+ async function logout() {
370
+ const removed = deleteCredentials();
371
+ note(removed ? `Removed ${CREDENTIALS_PATH}` : "No credentials file found; nothing to remove.");
372
+ printJson({ logged_out: true, credentials_removed: removed });
373
+ }
374
+ async function status() {
375
+ const config = getConfig();
376
+ const api = new SuperXAPI(config);
377
+ const me2 = await api.me();
378
+ printJson({
379
+ authenticated: true,
380
+ auth_source: config.source,
381
+ api_url: config.apiUrl,
382
+ owner: me2?.data?.owner ?? null,
383
+ plan: me2?.data?.plan ?? null,
384
+ key: me2?.data?.key ?? null,
385
+ rate_limit: api.lastRateLimit
386
+ });
387
+ }
388
+
389
+ // src/commands/accounts.ts
390
+ async function me() {
391
+ const api = new SuperXAPI(getConfig());
392
+ printJson(await api.me());
393
+ }
394
+ async function accounts(argv) {
395
+ const api = new SuperXAPI(getConfig());
396
+ printJson(await api.accounts({ limit: argv.limit, page: argv.page }));
397
+ }
398
+
399
+ // src/commands/posts.ts
400
+ async function postsList(argv) {
401
+ const api = new SuperXAPI(getConfig());
402
+ printJson(
403
+ await api.listPosts({
404
+ account_id: argv.account,
405
+ type: argv.type,
406
+ sort: argv.sort,
407
+ since: argv.since,
408
+ until: argv.until,
409
+ limit: argv.limit,
410
+ page: argv.page
411
+ })
412
+ );
413
+ }
414
+ async function postsAnalytics(argv) {
415
+ const api = new SuperXAPI(getConfig());
416
+ printJson(
417
+ await api.postsAnalytics({
418
+ account_id: argv.account,
419
+ since: argv.since,
420
+ until: argv.until
421
+ })
422
+ );
423
+ }
424
+ async function repliesList(argv) {
425
+ const api = new SuperXAPI(getConfig());
426
+ printJson(
427
+ await api.listReplies({
428
+ account_id: argv.account,
429
+ since: argv.since,
430
+ until: argv.until,
431
+ limit: argv.limit,
432
+ page: argv.page
433
+ })
434
+ );
435
+ }
436
+ async function repliesReceived(argv) {
437
+ const api = new SuperXAPI(getConfig());
438
+ printJson(
439
+ await api.receivedReplies({
440
+ account_id: argv.account,
441
+ sort: argv.sort,
442
+ since: argv.since,
443
+ until: argv.until,
444
+ limit: argv.limit,
445
+ page: argv.page
446
+ })
447
+ );
448
+ }
449
+
450
+ // src/commands/inspiration.ts
451
+ async function inspirationSearch(argv) {
452
+ const api = new SuperXAPI(getConfig());
453
+ printJson(
454
+ await api.searchInspiration({
455
+ q: argv.query,
456
+ sort: argv.sort,
457
+ min_likes: argv.minLikes,
458
+ min_reposts: argv.minReposts,
459
+ min_replies: argv.minReplies,
460
+ min_bookmarks: argv.minBookmarks,
461
+ min_impressions: argv.minImpressions,
462
+ min_followers: argv.minFollowers,
463
+ max_followers: argv.maxFollowers,
464
+ since: argv.since,
465
+ until: argv.until,
466
+ lang: argv.lang,
467
+ exclude_topics: argv.excludeTopics,
468
+ limit: argv.limit,
469
+ page: argv.page
470
+ })
471
+ );
472
+ }
473
+
474
+ // src/commands/contacts.ts
475
+ async function contactsList(argv) {
476
+ const api = new SuperXAPI(getConfig());
477
+ printJson(
478
+ await api.listContacts({
479
+ account_id: argv.account,
480
+ sort: argv.sort,
481
+ limit: argv.limit,
482
+ page: argv.page
483
+ })
484
+ );
485
+ }
486
+ async function contactsReplies(argv) {
487
+ const api = new SuperXAPI(getConfig());
488
+ printJson(
489
+ await api.contactReplies(argv.id, {
490
+ account_id: argv.account,
491
+ sort: argv.sort,
492
+ limit: argv.limit,
493
+ page: argv.page
494
+ })
495
+ );
496
+ }
497
+
498
+ // src/commands/lists.ts
499
+ async function listsList(argv) {
500
+ const api = new SuperXAPI(getConfig());
501
+ printJson(await api.listContactLists({ account_id: argv.account }));
502
+ }
503
+ async function listsMembers(argv) {
504
+ const api = new SuperXAPI(getConfig());
505
+ printJson(
506
+ await api.listListMembers(argv.id, {
507
+ account_id: argv.account,
508
+ q: argv.q,
509
+ limit: argv.limit,
510
+ page: argv.page
511
+ })
512
+ );
513
+ }
514
+ async function listsAddMember(argv) {
515
+ if (argv.handle && argv.xUserId || !argv.handle && !argv.xUserId) {
516
+ throw new Error("Provide exactly one of --handle or --x-user-id.");
517
+ }
518
+ const api = new SuperXAPI(getConfig());
519
+ const body = argv.handle ? { handle: argv.handle } : { x_user_id: argv.xUserId };
520
+ printJson(await api.addListMember(argv.id, body));
521
+ }
522
+ async function listsRemoveMember(argv) {
523
+ const api = new SuperXAPI(getConfig());
524
+ await api.removeListMember(argv.id, argv.memberId);
525
+ printJson({ data: { id: argv.memberId, removed: true } });
526
+ }
527
+
528
+ // src/commands/signals.ts
529
+ async function signalsAgents(argv) {
530
+ const api = new SuperXAPI(getConfig());
531
+ printJson(await api.listSignalAgents({ account_id: argv.account }));
532
+ }
533
+ async function signalsLeads(argv) {
534
+ const api = new SuperXAPI(getConfig());
535
+ printJson(
536
+ await api.listSignalLeads({
537
+ account_id: argv.account,
538
+ agent_id: argv.agent,
539
+ // yargs boolean: pass through only when the flag was given.
540
+ deposited: argv.deposited === void 0 ? void 0 : String(argv.deposited),
541
+ since: argv.since,
542
+ until: argv.until,
543
+ limit: argv.limit,
544
+ page: argv.page
545
+ })
546
+ );
547
+ }
548
+ async function signalsCreateAgent(argv) {
549
+ const body = {
550
+ name: argv.name,
551
+ icp_description: argv.icp
552
+ };
553
+ if (argv.precision) body.precision_mode = argv.precision;
554
+ if (argv["list-id"]) body.destination_list_id = argv["list-id"];
555
+ const keywords = (argv.keyword || []).filter((k) => typeof k === "string" && k.length > 0);
556
+ if (keywords.length > 0) body.keywords = keywords;
557
+ const api = new SuperXAPI(getConfig());
558
+ const { json, replayed } = await api.createSignalAgent(body, argv["idempotency-key"]);
559
+ if (replayed) {
560
+ note("Idempotency replay: this Idempotency-Key was already processed; returning the original response.");
561
+ printJson({ ...json, replayed: true });
562
+ return;
563
+ }
564
+ printJson(json);
565
+ }
566
+ async function signalsPauseAgent(argv) {
567
+ const api = new SuperXAPI(getConfig());
568
+ printJson(await api.setSignalAgentStatus(argv.id, "paused"));
569
+ }
570
+ async function signalsResumeAgent(argv) {
571
+ const api = new SuperXAPI(getConfig());
572
+ printJson(await api.setSignalAgentStatus(argv.id, "active"));
573
+ }
574
+ async function signalsDeleteAgent(argv) {
575
+ const api = new SuperXAPI(getConfig());
576
+ printJson(await api.deleteSignalAgent(argv.id));
577
+ }
578
+
579
+ // src/commands/scheduled.ts
580
+ async function scheduledList(argv) {
581
+ const api = new SuperXAPI(getConfig());
582
+ printJson(
583
+ await api.listScheduled({
584
+ account_id: argv.account,
585
+ status: argv.status,
586
+ tags: argv.tags,
587
+ from: argv.from,
588
+ to: argv.to,
589
+ limit: argv.limit,
590
+ page: argv.page
591
+ })
592
+ );
593
+ }
594
+ function mediaFromFlags(mediaFlag, altText) {
595
+ if (!mediaFlag) {
596
+ if (altText !== void 0) {
597
+ note("--alt-text requires --media.");
598
+ process.exit(1);
599
+ }
600
+ return null;
601
+ }
602
+ const keys = mediaFlag.split(",").map((k) => k.trim()).filter(Boolean);
603
+ if (keys.length === 0) {
604
+ note("--media must be a comma list of object_keys (from media:upload).");
605
+ process.exit(1);
606
+ }
607
+ if (altText !== void 0 && keys.length > 1) {
608
+ note("--alt-text works with a single --media key. For per-image alt text use --parts-json.");
609
+ process.exit(1);
610
+ }
611
+ return keys.map((object_key, i) => ({
612
+ object_key,
613
+ ...altText !== void 0 && i === 0 ? { alt_text: altText } : {}
614
+ }));
615
+ }
616
+ function parsePartsJson(raw) {
617
+ let parsed;
618
+ try {
619
+ parsed = JSON.parse(raw);
620
+ } catch {
621
+ note('--parts-json must be valid JSON, e.g. [{"text":"Hook","media":[{"object_key":"..."}]}]');
622
+ process.exit(1);
623
+ throw new Error("unreachable");
624
+ }
625
+ if (!Array.isArray(parsed) || parsed.some((p) => !p || typeof p !== "object" || typeof p.text !== "string")) {
626
+ note("--parts-json must be an array of { text, media? } objects.");
627
+ process.exit(1);
628
+ }
629
+ return parsed;
630
+ }
631
+ async function scheduledCreate(argv) {
632
+ const parts = (argv.part || []).filter((p) => typeof p === "string");
633
+ const sourceCount = [argv.text, parts.length > 0 ? "p" : void 0, argv["parts-json"]].filter(
634
+ (v) => v !== void 0
635
+ ).length;
636
+ if (sourceCount > 1) {
637
+ note("Use exactly one of --text (single post), --part (thread), or --parts-json.");
638
+ process.exit(1);
639
+ }
640
+ if (sourceCount === 0) {
641
+ note("Provide --text for a single post, --part flags for a thread, or --parts-json.");
642
+ process.exit(1);
643
+ }
644
+ if (argv.media !== void 0 && !argv.text) {
645
+ note("--media applies to the --text single-post form. For threads, put media in --parts-json.");
646
+ process.exit(1);
647
+ }
648
+ const body = {};
649
+ if (argv["parts-json"] !== void 0) {
650
+ body.parts = parsePartsJson(argv["parts-json"]);
651
+ } else if (argv.text) {
652
+ const media = mediaFromFlags(argv.media, argv["alt-text"]);
653
+ if (media) {
654
+ body.parts = [{ text: argv.text, media }];
655
+ } else {
656
+ body.text = argv.text;
657
+ }
658
+ } else {
659
+ body.parts = parts.map((text) => ({ text }));
660
+ }
661
+ if (argv.at) body.scheduled_for = argv.at;
662
+ if (argv.title !== void 0) body.title = argv.title;
663
+ if (argv.scratchpad !== void 0) body.scratchpad = argv.scratchpad;
664
+ const tags = (argv.tag || []).filter((t) => typeof t === "string" && t.length > 0);
665
+ if (tags.length > 0) body.tags = tags;
666
+ if (argv.account) body.account_id = argv.account;
667
+ const api = new SuperXAPI(getConfig());
668
+ const { json, replayed } = await api.createScheduled(body, argv["idempotency-key"]);
669
+ if (replayed) {
670
+ note("Idempotency replay: this Idempotency-Key was already processed; returning the original response.");
671
+ printJson({ ...json, replayed: true });
672
+ return;
673
+ }
674
+ printJson(json);
675
+ }
676
+ async function scheduledUpdate(argv) {
677
+ const parts = (argv.part || []).filter((p) => typeof p === "string");
678
+ const sourceCount = [argv.text, parts.length > 0 ? "p" : void 0, argv["parts-json"]].filter(
679
+ (v) => v !== void 0
680
+ ).length;
681
+ if (sourceCount > 1) {
682
+ note("Use exactly one of --text (single post), --part (thread), or --parts-json.");
683
+ process.exit(1);
684
+ }
685
+ if (argv.media !== void 0 && !argv.text) {
686
+ note("--media applies to the --text single-post form. For threads, put media in --parts-json.");
687
+ process.exit(1);
688
+ }
689
+ if (argv.title !== void 0 && argv["clear-title"]) {
690
+ note("Use either --title or --clear-title, not both.");
691
+ process.exit(1);
692
+ }
693
+ if (argv.scratchpad !== void 0 && argv["clear-scratchpad"]) {
694
+ note("Use either --scratchpad or --clear-scratchpad, not both.");
695
+ process.exit(1);
696
+ }
697
+ const tags = (argv.tag || []).filter((t) => typeof t === "string" && t.length > 0);
698
+ if (tags.length > 0 && argv["clear-tags"]) {
699
+ note("Use either --tag or --clear-tags, not both.");
700
+ process.exit(1);
701
+ }
702
+ const body = {};
703
+ if (argv["parts-json"] !== void 0) {
704
+ body.parts = parsePartsJson(argv["parts-json"]);
705
+ } else if (argv.text) {
706
+ const media = mediaFromFlags(argv.media, argv["alt-text"]);
707
+ if (media) {
708
+ body.parts = [{ text: argv.text, media }];
709
+ } else {
710
+ body.text = argv.text;
711
+ }
712
+ } else if (parts.length > 0) {
713
+ body.parts = parts.map((text) => ({ text }));
714
+ }
715
+ if (argv.at) body.scheduled_for = argv.at;
716
+ if (argv.status) body.status = argv.status;
717
+ if (argv.title !== void 0) body.title = argv.title;
718
+ if (argv["clear-title"]) body.title = null;
719
+ if (argv.scratchpad !== void 0) body.scratchpad = argv.scratchpad;
720
+ if (argv["clear-scratchpad"]) body.scratchpad = null;
721
+ if (tags.length > 0) body.tags = tags;
722
+ if (argv["clear-tags"]) body.tags = [];
723
+ if (argv.account) body.account_id = argv.account;
724
+ if (Object.keys(body).filter((k) => k !== "account_id").length === 0) {
725
+ note("Provide at least one field to update. Run: superx scheduled:update --help");
726
+ process.exit(1);
727
+ }
728
+ const api = new SuperXAPI(getConfig());
729
+ printJson(await api.updateScheduled(argv.id, body));
730
+ }
731
+ async function scheduledDelete(argv) {
732
+ const api = new SuperXAPI(getConfig());
733
+ printJson(await api.deleteScheduled(argv.id));
734
+ }
735
+
736
+ // src/commands/media.ts
737
+ var import_fs = require("fs");
738
+ var import_path = __toESM(require("path"));
739
+ function sniffImageType(bytes) {
740
+ if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) {
741
+ return "image/jpeg";
742
+ }
743
+ if (bytes.length >= 4 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) {
744
+ return "image/png";
745
+ }
746
+ if (bytes.length >= 4 && bytes.toString("latin1", 0, 4) === "GIF8") {
747
+ return "image/gif";
748
+ }
749
+ if (bytes.length >= 12 && bytes.toString("latin1", 0, 4) === "RIFF" && bytes.toString("latin1", 8, 12) === "WEBP") {
750
+ return "image/webp";
751
+ }
752
+ return null;
753
+ }
754
+ var EXT_TYPES = {
755
+ ".jpg": "image/jpeg",
756
+ ".jpeg": "image/jpeg",
757
+ ".png": "image/png",
758
+ ".webp": "image/webp",
759
+ ".gif": "image/gif"
760
+ };
761
+ async function mediaUpload(argv) {
762
+ let buffer;
763
+ try {
764
+ buffer = (0, import_fs.readFileSync)(argv.file);
765
+ } catch (err) {
766
+ note(`Error: could not read ${argv.file} (${err?.message || err})`);
767
+ process.exit(1);
768
+ return;
769
+ }
770
+ const fileType = sniffImageType(buffer) || EXT_TYPES[import_path.default.extname(argv.file).toLowerCase()];
771
+ if (!fileType) {
772
+ note("Error: unsupported file type. Supported images: JPG, PNG, WEBP, GIF.");
773
+ process.exit(1);
774
+ return;
775
+ }
776
+ const filename = import_path.default.basename(argv.file);
777
+ const api = new SuperXAPI(getConfig());
778
+ const created = await api.createMediaUpload({
779
+ filename,
780
+ file_type: fileType,
781
+ size: buffer.length
782
+ });
783
+ const data = created?.data || {};
784
+ if (!data.upload_url || !data.object_key) {
785
+ note("Error: the API did not return an upload URL.");
786
+ process.exit(1);
787
+ return;
788
+ }
789
+ note(`Uploading ${filename} (${fileType}, ${buffer.length} bytes)...`);
790
+ let putResponse;
791
+ try {
792
+ putResponse = await fetch(data.upload_url, {
793
+ method: "PUT",
794
+ // The presigned URL signs the Content-Type; it must match file_type.
795
+ headers: { "Content-Type": fileType },
796
+ body: buffer
797
+ });
798
+ } catch (err) {
799
+ throw new ApiError(0, "network_error", `Upload failed (${err?.message || err})`);
800
+ }
801
+ if (!putResponse.ok) {
802
+ throw new ApiError(putResponse.status, "upload_failed", `The storage upload returned HTTP ${putResponse.status}.`);
803
+ }
804
+ printJson({
805
+ object_key: data.object_key,
806
+ url: data.url,
807
+ file_type: fileType,
808
+ size: buffer.length
809
+ });
810
+ }
811
+
812
+ // src/commands/tags.ts
813
+ async function tagsList() {
814
+ const api = new SuperXAPI(getConfig());
815
+ printJson(await api.listTags());
816
+ }
817
+ async function tagsCreate(argv) {
818
+ const body = { name: argv.name };
819
+ if (argv.color) body.color = argv.color;
820
+ const api = new SuperXAPI(getConfig());
821
+ printJson(await api.createTag(body));
822
+ }
823
+ async function tagsUpdate(argv) {
824
+ if (argv.name === void 0 && argv.color === void 0) {
825
+ note("Provide --name and/or --color.");
826
+ process.exit(1);
827
+ }
828
+ const body = {};
829
+ if (argv.name !== void 0) body.name = argv.name;
830
+ if (argv.color !== void 0) body.color = argv.color;
831
+ const api = new SuperXAPI(getConfig());
832
+ printJson(await api.updateTag(argv.id, body));
833
+ }
834
+ async function tagsDelete(argv) {
835
+ const api = new SuperXAPI(getConfig());
836
+ await api.deleteTag(argv.id);
837
+ printJson({ id: argv.id, deleted: true });
838
+ }
839
+
840
+ // src/commands/articles.ts
841
+ var import_fs2 = require("fs");
842
+ function resolveContent(argv) {
843
+ if (argv.content !== void 0 && argv.file !== void 0) {
844
+ note("Use either --content or --file, not both.");
845
+ process.exit(1);
846
+ }
847
+ if (argv.content !== void 0) return argv.content;
848
+ if (argv.file !== void 0) {
849
+ try {
850
+ return (0, import_fs2.readFileSync)(argv.file, "utf8");
851
+ } catch (err) {
852
+ note(`Could not read ${argv.file}: ${err?.message || err}`);
853
+ process.exit(1);
854
+ }
855
+ }
856
+ if (!process.stdin.isTTY) {
857
+ try {
858
+ const piped = (0, import_fs2.readFileSync)(0, "utf8");
859
+ if (piped.length > 0) return piped;
860
+ } catch {
861
+ }
862
+ }
863
+ return void 0;
864
+ }
865
+ async function articlesList(argv) {
866
+ const api = new SuperXAPI(getConfig());
867
+ printJson(
868
+ await api.listArticles({
869
+ account_id: argv.account,
870
+ status: argv.status,
871
+ limit: argv.limit,
872
+ page: argv.page
873
+ })
874
+ );
875
+ }
876
+ async function articlesGet(argv) {
877
+ const api = new SuperXAPI(getConfig());
878
+ printJson(await api.getArticle(argv.id));
879
+ }
880
+ async function articlesCreate(argv) {
881
+ const content = resolveContent(argv);
882
+ const body = { title: argv.title };
883
+ if (content !== void 0) body.content_markdown = content;
884
+ if (argv.account) body.account_id = argv.account;
885
+ const api = new SuperXAPI(getConfig());
886
+ printJson(await api.createArticle(body));
887
+ }
888
+ async function articlesUpdate(argv) {
889
+ if (argv["cover-url"] !== void 0 && argv["clear-cover"]) {
890
+ note("Use either --cover-url or --clear-cover, not both.");
891
+ process.exit(1);
892
+ }
893
+ const body = {};
894
+ if (argv.title !== void 0) body.title = argv.title;
895
+ if (argv.content !== void 0 && argv.file !== void 0) {
896
+ note("Use either --content or --file, not both.");
897
+ process.exit(1);
898
+ }
899
+ if (argv.content !== void 0) body.content_markdown = argv.content;
900
+ if (argv.file !== void 0) {
901
+ try {
902
+ body.content_markdown = (0, import_fs2.readFileSync)(argv.file, "utf8");
903
+ } catch (err) {
904
+ note(`Could not read ${argv.file}: ${err?.message || err}`);
905
+ process.exit(1);
906
+ }
907
+ }
908
+ if (argv["cover-url"] !== void 0) body.cover_url = argv["cover-url"];
909
+ if (argv["clear-cover"]) body.cover_url = null;
910
+ if (Object.keys(body).length === 0) {
911
+ note("Provide at least one of: --title, --content, --file, --cover-url, --clear-cover.");
912
+ process.exit(1);
913
+ }
914
+ const api = new SuperXAPI(getConfig());
915
+ printJson(await api.updateArticle(argv.id, body));
916
+ }
917
+ async function articlesDelete(argv) {
918
+ const api = new SuperXAPI(getConfig());
919
+ await api.deleteArticle(argv.id);
920
+ printJson({ id: argv.id, deleted: true });
921
+ }
922
+ async function articlesPublish(argv) {
923
+ note("Publishing to X. This is live and irreversible; it can take up to 90 seconds...");
924
+ const api = new SuperXAPI(getConfig());
925
+ printJson(await api.publishArticle(argv.id));
926
+ }
927
+ async function articlesSchedule(argv) {
928
+ const api = new SuperXAPI(getConfig());
929
+ printJson(await api.scheduleArticle(argv.id, { scheduled_for: argv.at }));
930
+ }
931
+ async function articlesUnschedule(argv) {
932
+ const api = new SuperXAPI(getConfig());
933
+ printJson(await api.unscheduleArticle(argv.id));
934
+ }
935
+ async function articlesCover(argv) {
936
+ note("Generating a cover image. This spends AI credits and can take 60-100 seconds...");
937
+ const body = {};
938
+ if (argv.style !== void 0) body.style_text = argv.style;
939
+ if (argv.attach === false) body.attach = false;
940
+ const api = new SuperXAPI(getConfig());
941
+ printJson(await api.generateArticleCover(argv.id, body));
942
+ }
943
+
944
+ // src/commands/docs.ts
945
+ async function docs() {
946
+ const apiUrl = resolveApiUrl(loadCredentials()?.apiUrl || DEFAULT_API_URL);
947
+ const api = new SuperXAPI({ apiKey: "", apiUrl });
948
+ process.stdout.write(await api.docs());
949
+ }
950
+
951
+ // src/index.ts
952
+ function run(handler) {
953
+ return async (argv) => {
954
+ try {
955
+ await handler(argv);
956
+ } catch (err) {
957
+ if (err instanceof ApiError) {
958
+ const status2 = err.status ? ` (HTTP ${err.status})` : "";
959
+ note(`Error [${err.code}]${status2}: ${err.message}`);
960
+ if (err.retryAfter !== null) {
961
+ note(`Retry after ${err.retryAfter} seconds.`);
962
+ }
963
+ } else {
964
+ note(`Error: ${err?.message || err}`);
965
+ }
966
+ process.exit(1);
967
+ }
968
+ };
969
+ }
970
+ var paginationOptions = (y) => y.option("limit", { describe: "Items per page (max 100)", type: "number" }).option("page", { describe: "Page number (1-based)", type: "number" });
971
+ var accountOption = (y) => y.option("account", {
972
+ describe: "Account id (from `superx accounts`); defaults to your main account",
973
+ type: "string"
974
+ });
975
+ (0, import_yargs.default)((0, import_helpers.hideBin)(process.argv)).scriptName("superx").usage("$0 <command> [options]").command(
976
+ "login",
977
+ "Authenticate with a SuperX API key (guided paste or --key)",
978
+ (y) => y.option("key", {
979
+ describe: "API key (sxk_...); omit to be prompted",
980
+ type: "string"
981
+ }).example('$0 login --key "sxk_..."', "Non-interactive login").example("$0 login", "Guided login: prints the key page URL and prompts for a paste"),
982
+ run(login)
983
+ ).command("logout", "Remove stored credentials (~/.superx/credentials.json)", {}, run(logout)).command("status", "Check authentication, plan, and current rate-limit state", {}, run(status)).command("me", "Show the key owner, plan, and API key details", {}, run(me)).command(
984
+ "accounts",
985
+ "List the X accounts this key can read (main account first)",
986
+ (y) => paginationOptions(y),
987
+ run(accounts)
988
+ ).command(
989
+ "posts:list",
990
+ "List published posts with engagement metrics",
991
+ (y) => paginationOptions(accountOption(y)).option("type", {
992
+ describe: "Filter by post kind",
993
+ type: "string",
994
+ choices: ["posts", "replies", "all"]
995
+ }).option("sort", {
996
+ describe: "Sort order",
997
+ type: "string",
998
+ choices: ["posted_at", "likes", "impressions"]
999
+ }).option("since", { describe: "Start of range (UTC ISO-8601, e.g. 2026-06-01T00:00:00Z)", type: "string" }).option("until", { describe: "End of range (UTC ISO-8601)", type: "string" }).example("$0 posts:list --sort likes --limit 10", "Top 10 posts by likes").example('$0 posts:list --since "2026-06-01T00:00:00Z" --type posts', "Original posts since June"),
1000
+ run(postsList)
1001
+ ).command(
1002
+ "posts:analytics",
1003
+ "Account analytics: totals, daily series, follower change (default: last 30 days)",
1004
+ (y) => accountOption(y).option("since", { describe: "Start of range (UTC ISO-8601)", type: "string" }).option("until", { describe: "End of range (UTC ISO-8601)", type: "string" }).example('$0 posts:analytics --since "2026-06-01T00:00:00Z" --until "2026-07-01T00:00:00Z"', "June analytics"),
1005
+ run(postsAnalytics)
1006
+ ).command(
1007
+ "replies:list",
1008
+ "List replies the account has sent (newest first)",
1009
+ (y) => paginationOptions(accountOption(y)).option("since", { describe: "Start of range (UTC ISO-8601)", type: "string" }).option("until", { describe: "End of range (UTC ISO-8601)", type: "string" }),
1010
+ run(repliesList)
1011
+ ).command(
1012
+ "replies:received",
1013
+ "List replies your audience has sent you across all your posts",
1014
+ (y) => paginationOptions(accountOption(y)).option("sort", {
1015
+ describe: "Sort order",
1016
+ type: "string",
1017
+ choices: ["recent", "most_liked"]
1018
+ }).option("since", { describe: "Start of range (UTC ISO-8601)", type: "string" }).option("until", { describe: "End of range (UTC ISO-8601)", type: "string" }).example("$0 replies:received --limit 10", "Ten newest audience replies").example('$0 replies:received --sort most_liked --since "2026-06-01T00:00:00Z"', "Best-liked replies since June"),
1019
+ run(repliesReceived)
1020
+ ).command(
1021
+ "inspiration:search <query>",
1022
+ "Search a library of 50M+ high-performing posts for inspiration",
1023
+ (y) => paginationOptions(y).positional("query", { describe: "Topic or theme to search for", type: "string" }).option("sort", {
1024
+ describe: "Sort order",
1025
+ type: "string",
1026
+ choices: ["relevant", "recent", "likes", "reposts", "impressions", "outlier"]
1027
+ }).option("min-likes", { describe: "Only posts with at least this many likes", type: "number" }).option("min-reposts", { describe: "Only posts with at least this many reposts", type: "number" }).option("min-replies", { describe: "Only posts with at least this many replies", type: "number" }).option("min-bookmarks", { describe: "Only posts with at least this many bookmarks", type: "number" }).option("min-impressions", { describe: "Only posts with at least this many impressions", type: "number" }).option("min-followers", { describe: "Only posts from authors with at least this many followers", type: "number" }).option("max-followers", { describe: "Only posts from authors with at most this many followers", type: "number" }).option("since", { describe: "Only posts after this time (UTC ISO-8601)", type: "string" }).option("until", { describe: "Only posts before this time (UTC ISO-8601)", type: "string" }).option("lang", { describe: "Language code (default en)", type: "string" }).option("exclude-topics", { describe: "Comma-separated topics to exclude", type: "string" }).example('$0 inspiration:search "build in public" --limit 10', "Ten posts about building in public").example('$0 inspiration:search "indie hackers" --sort outlier --min-likes 500', "Overperformers with 500+ likes"),
1028
+ run(inspirationSearch)
1029
+ ).command(
1030
+ "contacts:list",
1031
+ "List the people who engage with you most",
1032
+ (y) => paginationOptions(accountOption(y)).option("sort", {
1033
+ describe: "Sort order",
1034
+ type: "string",
1035
+ choices: ["engagement", "replies", "reposts"]
1036
+ }),
1037
+ run(contactsList)
1038
+ ).command(
1039
+ "contacts:replies <id>",
1040
+ "List a contact's replies to you (their id from contacts:list)",
1041
+ (y) => paginationOptions(accountOption(y)).positional("id", { describe: "Contact id", type: "string" }).option("sort", {
1042
+ describe: "Sort order",
1043
+ type: "string",
1044
+ choices: ["recent", "most_liked"]
1045
+ }),
1046
+ run(contactsReplies)
1047
+ ).command(
1048
+ "lists:list",
1049
+ "List your contact lists (system lists are read-only)",
1050
+ (y) => accountOption(y),
1051
+ run(listsList)
1052
+ ).command(
1053
+ "lists:members <id>",
1054
+ "List the members of a contact list you created",
1055
+ (y) => paginationOptions(accountOption(y)).positional("id", { describe: "List id (from lists:list)", type: "string" }).option("q", { describe: "Filter members by handle or name substring", type: "string" }),
1056
+ run(listsMembers)
1057
+ ).command(
1058
+ "lists:add-member <id>",
1059
+ "Add a person to a contact list by handle or X user id",
1060
+ (y) => y.positional("id", { describe: "List id (from lists:list)", type: "string" }).option("handle", { describe: "X username, with or without the @", type: "string" }).option("x-user-id", { describe: "Numeric X user id", type: "string" }).example("$0 lists:add-member abc123 --handle levelsio", "Add by handle").example("$0 lists:add-member abc123 --x-user-id 44196397", "Add by X user id"),
1061
+ run(listsAddMember)
1062
+ ).command(
1063
+ "lists:remove-member <id> <memberId>",
1064
+ "Remove a member from a contact list (member id from lists:members)",
1065
+ (y) => y.positional("id", { describe: "List id (from lists:list)", type: "string" }).positional("memberId", { describe: "Member id (from lists:members)", type: "string" }),
1066
+ run(listsRemoveMember)
1067
+ ).command(
1068
+ "signals:agents",
1069
+ "List your signal agents (automated lead finders) with their watched signals",
1070
+ (y) => accountOption(y),
1071
+ run(signalsAgents)
1072
+ ).command(
1073
+ "signals:leads",
1074
+ "List the leads your signal agents have found (newest first)",
1075
+ (y) => paginationOptions(accountOption(y)).option("agent", { describe: "Narrow to one agent by its numeric id (from signals:agents)", type: "number" }).option("deposited", {
1076
+ describe: "true = only leads already saved to the agent's contact list, false = only new ones",
1077
+ type: "boolean"
1078
+ }).option("since", { describe: "Only leads discovered after this time (UTC ISO-8601)", type: "string" }).option("until", { describe: "Only leads discovered before this time (UTC ISO-8601)", type: "string" }).example("$0 signals:leads --limit 10", "Ten newest leads across all agents").example("$0 signals:leads --agent 3 --deposited false", "New leads from agent 3 not yet in its list"),
1079
+ run(signalsLeads)
1080
+ ).command(
1081
+ "signals:create-agent",
1082
+ "Create a signal agent (automated lead finder); leads land in Signals over the next minutes and days",
1083
+ (y) => y.option("name", { describe: "Agent name (max 80 chars)", type: "string", demandOption: true }).option("icp", {
1084
+ describe: "Who the ideal leads are: role, product, pains, buying intent (max 500 chars)",
1085
+ type: "string",
1086
+ demandOption: true
1087
+ }).option("precision", {
1088
+ describe: "high = fewer, stricter matches (default); discovery = broader net",
1089
+ type: "string",
1090
+ choices: ["high", "discovery"]
1091
+ }).option("list-id", {
1092
+ describe: "Contact list id (from lists:list) that receives the leads; omit to auto-create one",
1093
+ type: "string"
1094
+ }).option("keyword", {
1095
+ describe: "Plain-language search description to watch (repeat the flag, 1-5); omit to auto-suggest from --icp",
1096
+ type: "string",
1097
+ array: true
1098
+ }).option("idempotency-key", {
1099
+ describe: "Idempotency-Key header (max 64 chars); retries with the same key return the original result",
1100
+ type: "string"
1101
+ }).example(
1102
+ '$0 signals:create-agent --name "Build in public" --icp "Indie founders building SaaS in public" --keyword "building in public" --keyword "just shipped my MVP"',
1103
+ "Create an agent with explicit keywords"
1104
+ ).example(
1105
+ '$0 signals:create-agent --name "Agency leads" --icp "Marketing agency owners struggling with reporting"',
1106
+ "Create an agent with auto-suggested keywords and an auto-created list"
1107
+ ),
1108
+ run(signalsCreateAgent)
1109
+ ).command(
1110
+ "signals:pause-agent <id>",
1111
+ "Pause a signal agent (it stops finding leads until resumed)",
1112
+ (y) => y.positional("id", { describe: "Agent id (from signals:agents)", type: "number" }),
1113
+ run(signalsPauseAgent)
1114
+ ).command(
1115
+ "signals:resume-agent <id>",
1116
+ "Resume a paused signal agent",
1117
+ (y) => y.positional("id", { describe: "Agent id (from signals:agents)", type: "number" }),
1118
+ run(signalsResumeAgent)
1119
+ ).command(
1120
+ "signals:delete-agent <id>",
1121
+ "Delete a signal agent (its saved leads and contact list stay untouched)",
1122
+ (y) => y.positional("id", { describe: "Agent id (from signals:agents)", type: "number" }),
1123
+ run(signalsDeleteAgent)
1124
+ ).command(
1125
+ "scheduled:list",
1126
+ "List drafts and the scheduled queue",
1127
+ (y) => paginationOptions(accountOption(y)).option("status", {
1128
+ describe: "Comma list of: draft, scheduled, sent, error",
1129
+ type: "string"
1130
+ }).option("tags", {
1131
+ describe: "Comma list of tag ids (from tags:list); posts matching ANY of them",
1132
+ type: "string"
1133
+ }).option("from", { describe: "Scheduled-time lower bound (UTC ISO-8601)", type: "string" }).option("to", { describe: "Scheduled-time upper bound (UTC ISO-8601)", type: "string" }),
1134
+ run(scheduledList)
1135
+ ).command(
1136
+ "media:upload <file>",
1137
+ "Upload a local image (JPG, PNG, WEBP, GIF) and print its object_key for --media",
1138
+ (y) => y.positional("file", { describe: "Path to the image file (max 5MB; GIF 15MB)", type: "string" }).example("$0 media:upload ./chart.png", "Upload an image").example('KEY=$($0 media:upload ./chart.png | jq -r .object_key); $0 scheduled:create --text "Post" --media "$KEY"', "Upload, then attach"),
1139
+ run(mediaUpload)
1140
+ ).command(
1141
+ "scheduled:create",
1142
+ "Create a draft (no --at) or scheduled post; repeat --part for a thread",
1143
+ (y) => accountOption(y).option("text", { describe: "Text for a single post", type: "string" }).option("part", {
1144
+ describe: "Thread part text (repeat the flag, 1-25 parts, in order)",
1145
+ type: "string",
1146
+ array: true
1147
+ }).option("media", {
1148
+ describe: "Comma list of image object_keys (from media:upload) to attach; single-post form only (max 4 images or 1 GIF)",
1149
+ type: "string"
1150
+ }).option("alt-text", {
1151
+ describe: "Accessibility description for the attached image (single --media key only, max 1000 chars)",
1152
+ type: "string"
1153
+ }).option("parts-json", {
1154
+ describe: 'Full parts array as JSON for threads with media: [{"text":"...","media":[{"object_key":"...","alt_text":"..."}]}]',
1155
+ type: "string"
1156
+ }).option("at", {
1157
+ describe: "Schedule time: UTC ISO-8601 with explicit Z or offset, at least 60s in the future. Omit to create a draft.",
1158
+ type: "string"
1159
+ }).option("title", { describe: "Draft title (max 300 chars, shown in the SuperX app, never posted)", type: "string" }).option("scratchpad", { describe: "Private working notes (max 30000 chars, never posted)", type: "string" }).option("tag", {
1160
+ describe: "Tag id to assign (repeat the flag, max 20; ids from tags:list)",
1161
+ type: "string",
1162
+ array: true
1163
+ }).option("idempotency-key", {
1164
+ describe: "Idempotency-Key header (max 64 chars); retries with the same key return the original result",
1165
+ type: "string"
1166
+ }).example('$0 scheduled:create --text "Hello"', "Create a draft").example('$0 scheduled:create --text "Hello" --at "2026-08-01T15:00:00Z"', "Schedule a post").example('$0 scheduled:create --part "1/ Hook" --part "2/ Detail" --part "3/ CTA"', "Draft a 3-part thread").example('$0 scheduled:create --text "Hello" --title "Launch teaser" --tag abc123', "Draft with a title and a tag").example('$0 scheduled:create --text "Chart of the week" --media "<object_key>" --alt-text "Revenue chart"', "Draft with an image"),
1167
+ run(scheduledCreate)
1168
+ ).command(
1169
+ "scheduled:update <id>",
1170
+ "Edit a draft or scheduled post; only the flags you pass change",
1171
+ (y) => accountOption(y).positional("id", { describe: "Post id (from scheduled:list or scheduled:create)", type: "string" }).option("text", { describe: "Replacement text for a single post", type: "string" }).option("part", {
1172
+ describe: "Replacement thread part text (repeat the flag, 1-25 parts, in order)",
1173
+ type: "string",
1174
+ array: true
1175
+ }).option("media", {
1176
+ describe: "Comma list of image object_keys to attach with --text (full replace: re-list existing keys to keep them; --text without --media removes the post's media)",
1177
+ type: "string"
1178
+ }).option("alt-text", {
1179
+ describe: "Accessibility description for the attached image (single --media key only, max 1000 chars)",
1180
+ type: "string"
1181
+ }).option("parts-json", {
1182
+ describe: 'Full replacement parts array as JSON: [{"text":"...","media":[{"object_key":"..."}]}]',
1183
+ type: "string"
1184
+ }).option("at", {
1185
+ describe: "New schedule time (UTC ISO-8601 with explicit Z or offset). On its own it never schedules a draft; add --status scheduled.",
1186
+ type: "string"
1187
+ }).option("status", {
1188
+ describe: "Explicit transition; scheduled needs a future time (via --at or already set)",
1189
+ type: "string",
1190
+ choices: ["draft", "scheduled"]
1191
+ }).option("title", { describe: "New draft title (max 300 chars)", type: "string" }).option("clear-title", { describe: "Remove the title", type: "boolean" }).option("scratchpad", { describe: "New private notes (max 30000 chars)", type: "string" }).option("clear-scratchpad", { describe: "Remove the notes", type: "boolean" }).option("tag", {
1192
+ describe: "Replacement tag id set (repeat the flag, max 20; replaces ALL current tags)",
1193
+ type: "string",
1194
+ array: true
1195
+ }).option("clear-tags", { describe: "Remove all tags", type: "boolean" }).example('$0 scheduled:update abc123 --title "Better hook"', "Retitle a draft, everything else untouched").example('$0 scheduled:update abc123 --at "2026-08-01T15:00:00Z" --status scheduled', "Promote a draft to the queue").example("$0 scheduled:update abc123 --status draft", "Pull a post back to drafts (quota refunds)"),
1196
+ run(scheduledUpdate)
1197
+ ).command(
1198
+ "scheduled:delete <id>",
1199
+ "Delete a draft or scheduled post by id",
1200
+ (y) => y.positional("id", { describe: "Post id (from scheduled:list or scheduled:create)", type: "string" }),
1201
+ run(scheduledDelete)
1202
+ ).command("tags:list", "List your tags (id, name, color)", {}, run(tagsList)).command(
1203
+ "tags:create <name>",
1204
+ "Create a tag",
1205
+ (y) => y.positional("name", { describe: "Tag name (1-40 chars, unique)", type: "string" }).option("color", {
1206
+ describe: "Palette color name (default blue): rose, amber, lime, emerald, teal, cyan, blue, indigo, violet, fuchsia, slate, stone",
1207
+ type: "string"
1208
+ }).example('$0 tags:create "Launch week" --color amber', "Create an amber tag"),
1209
+ run(tagsCreate)
1210
+ ).command(
1211
+ "tags:update <id>",
1212
+ "Rename and/or recolor a tag",
1213
+ (y) => y.positional("id", { describe: "Tag id (from tags:list)", type: "string" }).option("name", { describe: "New name (1-40 chars, unique)", type: "string" }).option("color", { describe: "New palette color name", type: "string" }),
1214
+ run(tagsUpdate)
1215
+ ).command(
1216
+ "tags:delete <id>",
1217
+ "Delete a tag (it is removed from every post that carries it)",
1218
+ (y) => y.positional("id", { describe: "Tag id (from tags:list)", type: "string" }),
1219
+ run(tagsDelete)
1220
+ ).command(
1221
+ "articles:list",
1222
+ "List X Articles (long-form posts)",
1223
+ (y) => paginationOptions(accountOption(y)).option("status", {
1224
+ describe: "Comma list of: draft, scheduled, publishing, published, failed",
1225
+ type: "string"
1226
+ }),
1227
+ run(articlesList)
1228
+ ).command(
1229
+ "articles:get <id>",
1230
+ "Get one article with its body as markdown",
1231
+ (y) => y.positional("id", { describe: "Article id (from articles:list)", type: "string" }),
1232
+ run(articlesGet)
1233
+ ).command(
1234
+ "articles:create",
1235
+ "Create an article draft; body via --content, --file, or piped stdin (markdown)",
1236
+ (y) => accountOption(y).option("title", { describe: "Article title (1-300 chars)", type: "string", demandOption: true }).option("content", { describe: "Body as inline markdown", type: "string" }).option("file", { describe: "Path to a markdown file with the body", type: "string" }).example('$0 articles:create --title "My article" --file draft.md', "Draft from a markdown file").example('cat draft.md | $0 articles:create --title "My article"', "Draft from piped stdin"),
1237
+ run(articlesCreate)
1238
+ ).command(
1239
+ "articles:update <id>",
1240
+ "Edit an article; only the flags you pass change",
1241
+ (y) => y.positional("id", { describe: "Article id (from articles:list)", type: "string" }).option("title", { describe: "New title (1-300 chars)", type: "string" }).option("content", { describe: "Replacement body as inline markdown", type: "string" }).option("file", { describe: "Path to a markdown file with the replacement body", type: "string" }).option("cover-url", { describe: "http(s) image URL to set as the cover", type: "string" }).option("clear-cover", { describe: "Remove the cover image", type: "boolean" }),
1242
+ run(articlesUpdate)
1243
+ ).command(
1244
+ "articles:delete <id>",
1245
+ "Delete an article (scheduled articles refund their quota)",
1246
+ (y) => y.positional("id", { describe: "Article id", type: "string" }),
1247
+ run(articlesDelete)
1248
+ ).command(
1249
+ "articles:publish <id>",
1250
+ "Publish an article to X NOW (irreversible, spends quota, needs X Premium)",
1251
+ (y) => y.positional("id", { describe: "Article id", type: "string" }),
1252
+ run(articlesPublish)
1253
+ ).command(
1254
+ "articles:schedule <id>",
1255
+ "Schedule (or reschedule) an article",
1256
+ (y) => y.positional("id", { describe: "Article id", type: "string" }).option("at", {
1257
+ describe: "UTC ISO-8601 with explicit Z or offset, more than 2 minutes in the future",
1258
+ type: "string",
1259
+ demandOption: true
1260
+ }).example('$0 articles:schedule abc123 --at "2026-08-01T15:00:00Z"', "Queue an article"),
1261
+ run(articlesSchedule)
1262
+ ).command(
1263
+ "articles:unschedule <id>",
1264
+ "Pull a scheduled article back to draft (quota refunds)",
1265
+ (y) => y.positional("id", { describe: "Article id", type: "string" }),
1266
+ run(articlesUnschedule)
1267
+ ).command(
1268
+ "articles:cover <id>",
1269
+ "Generate an AI cover for an article (60-100s, spends AI credits)",
1270
+ (y) => y.positional("id", { describe: "Article id (needs a title)", type: "string" }).option("style", { describe: "Style description steering the artwork (max 8000 chars)", type: "string" }).option("attach", {
1271
+ describe: "Attach the result as the article's cover (use --no-attach to skip)",
1272
+ type: "boolean",
1273
+ default: true
1274
+ }),
1275
+ run(articlesCover)
1276
+ ).command("docs", "Print the SuperX API quickstart (markdown, no auth needed)", {}, run(docs)).demandCommand(1, "Specify a command. Run: superx --help").strict().help().alias("h", "help").version().wrap(Math.min(100, process.stdout.columns || 100)).fail((msg, err) => {
1277
+ if (err) throw err;
1278
+ note(msg || "Invalid usage. Run: superx --help");
1279
+ process.exit(1);
1280
+ }).parse();