superx-cli 0.3.0 → 0.5.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 CHANGED
@@ -118,6 +118,46 @@ var SuperXAPI = class {
118
118
  }
119
119
  return { status: response.status, headers: response.headers, json };
120
120
  }
121
+ /**
122
+ * Authenticated request whose 2xx body is NOT JSON (the CSV export). Same
123
+ * auth, rate-limit capture and error envelope as request(); the difference
124
+ * is that a successful body comes back as raw text.
125
+ */
126
+ async requestText(endpoint, query) {
127
+ const url = this.buildUrl(endpoint, query);
128
+ let response;
129
+ try {
130
+ response = await fetch(url, {
131
+ headers: { Authorization: `Bearer ${this.apiKey}` }
132
+ });
133
+ } catch (err) {
134
+ throw new ApiError(0, "network_error", `Could not reach ${this.apiUrl} (${err?.message || err})`);
135
+ }
136
+ this.captureRateLimit(response.headers);
137
+ const text = await response.text();
138
+ if (!response.ok) {
139
+ const retryHeader = response.headers.get("retry-after");
140
+ const retryAfter = retryHeader !== null && Number.isFinite(Number(retryHeader)) ? Number(retryHeader) : null;
141
+ let code = `http_${response.status}`;
142
+ let message = `Request failed with HTTP ${response.status}`;
143
+ let json = null;
144
+ try {
145
+ json = text ? JSON.parse(text) : null;
146
+ } catch {
147
+ json = null;
148
+ }
149
+ const errField = json?.error;
150
+ if (errField && typeof errField === "object" && typeof errField.code === "string") {
151
+ code = errField.code;
152
+ if (typeof errField.message === "string") message = errField.message;
153
+ } else if (typeof errField === "string") {
154
+ code = errField.split(":")[0].trim() || code;
155
+ message = errField;
156
+ }
157
+ throw new ApiError(response.status, code, message, retryAfter);
158
+ }
159
+ return { text, headers: response.headers };
160
+ }
121
161
  // --- Identity ---
122
162
  async me() {
123
163
  return (await this.request("/me")).json;
@@ -138,6 +178,32 @@ var SuperXAPI = class {
138
178
  async receivedReplies(query = {}) {
139
179
  return (await this.request("/replies/received", { query })).json;
140
180
  }
181
+ // --- Drafting ---
182
+ /** Write post drafts in the account's voice. Nothing is scheduled. */
183
+ async draftPost(body) {
184
+ return (await this.request("/posts/draft", { method: "POST", body })).json;
185
+ }
186
+ /** Rewrite a post in the account's voice. Nothing is posted or scheduled. */
187
+ async remixPost(body) {
188
+ return (await this.request("/posts/remix", { method: "POST", body })).json;
189
+ }
190
+ // --- Composer tools (text in, text out; nothing is posted) ---
191
+ async inlineEdit(body) {
192
+ return (await this.request("/tools/inline-edit", { method: "POST", body })).json;
193
+ }
194
+ async rephrase(body) {
195
+ return (await this.request("/tools/rephrase", { method: "POST", body })).json;
196
+ }
197
+ async factCheck(body) {
198
+ return (await this.request("/tools/factcheck", { method: "POST", body })).json;
199
+ }
200
+ async predictAlgorithm(body) {
201
+ return (await this.request("/tools/algorithm-predict", { method: "POST", body })).json;
202
+ }
203
+ /** Draft ONE reply to a post. Text only: a person posts it. */
204
+ async draftReply(body) {
205
+ return (await this.request("/engage/reply-draft", { method: "POST", body })).json;
206
+ }
141
207
  // --- Inspiration ---
142
208
  async searchInspiration(query = {}) {
143
209
  return (await this.request("/inspiration", { query })).json;
@@ -149,6 +215,28 @@ var SuperXAPI = class {
149
215
  async contactReplies(contactId, query = {}) {
150
216
  return (await this.request(`/contacts/${encodeURIComponent(contactId)}/replies`, { query })).json;
151
217
  }
218
+ async getContact(contactId, query = {}) {
219
+ return (await this.request(`/contacts/${encodeURIComponent(contactId)}`, { query })).json;
220
+ }
221
+ // --- Contact notes ---
222
+ async listContactNotes(contactId, query = {}) {
223
+ return (await this.request(`/contacts/${encodeURIComponent(contactId)}/notes`, { query })).json;
224
+ }
225
+ async addContactNote(contactId, body) {
226
+ return (await this.request(`/contacts/${encodeURIComponent(contactId)}/notes`, { method: "POST", body })).json;
227
+ }
228
+ async updateContactNote(contactId, noteId, body) {
229
+ return (await this.request(
230
+ `/contacts/${encodeURIComponent(contactId)}/notes/${encodeURIComponent(noteId)}`,
231
+ { method: "PATCH", body }
232
+ )).json;
233
+ }
234
+ async deleteContactNote(contactId, noteId, query = {}) {
235
+ return (await this.request(
236
+ `/contacts/${encodeURIComponent(contactId)}/notes/${encodeURIComponent(noteId)}`,
237
+ { method: "DELETE", query }
238
+ )).json;
239
+ }
152
240
  // --- Contact lists ---
153
241
  async listContactLists(query = {}) {
154
242
  return (await this.request("/contact-lists", { query })).json;
@@ -165,13 +253,44 @@ var SuperXAPI = class {
165
253
  { method: "DELETE" }
166
254
  )).json;
167
255
  }
256
+ async createList(body) {
257
+ return (await this.request("/contact-lists", { method: "POST", body })).json;
258
+ }
259
+ async renameList(listId, body) {
260
+ return (await this.request(`/contact-lists/${encodeURIComponent(listId)}`, { method: "PATCH", body })).json;
261
+ }
262
+ async deleteList(listId, query = {}) {
263
+ return (await this.request(`/contact-lists/${encodeURIComponent(listId)}`, { method: "DELETE", query })).json;
264
+ }
265
+ async addListMembers(listId, body) {
266
+ return (await this.request(`/contact-lists/${encodeURIComponent(listId)}/members/bulk`, { method: "POST", body })).json;
267
+ }
268
+ async removeListMembers(listId, body) {
269
+ return (await this.request(`/contact-lists/${encodeURIComponent(listId)}/members/bulk-delete`, { method: "POST", body })).json;
270
+ }
168
271
  // --- Signals ---
169
272
  async listSignalAgents(query = {}) {
170
273
  return (await this.request("/signals/agents", { query })).json;
171
274
  }
275
+ /** One live keyword search for people on X now. Saves nothing. */
276
+ async searchLeads(body) {
277
+ return (await this.request("/signals/leads/search", { method: "POST", body })).json;
278
+ }
172
279
  async listSignalLeads(query = {}) {
173
280
  return (await this.request("/signals/leads", { query })).json;
174
281
  }
282
+ /** Audience description -> keyword-watch ideas. Free, creates nothing. */
283
+ async suggestKeywords(body) {
284
+ return (await this.request("/signals/keywords/suggest", { method: "POST", body })).json;
285
+ }
286
+ /** Audience description -> scoring rubric. Free, creates nothing. */
287
+ async expandIcp(body) {
288
+ return (await this.request("/signals/icp/expand", { method: "POST", body })).json;
289
+ }
290
+ /** Website -> audience description + rubric + keyword ideas. Free. */
291
+ async expandIcpFromUrl(body) {
292
+ return (await this.request("/signals/icp/expand-from-url", { method: "POST", body })).json;
293
+ }
175
294
  async createSignalAgent(body, idempotencyKey) {
176
295
  const headers = {};
177
296
  if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
@@ -181,9 +300,43 @@ var SuperXAPI = class {
181
300
  async setSignalAgentStatus(id, status2) {
182
301
  return (await this.request(`/signals/agents/${id}`, { method: "PATCH", body: { status: status2 } })).json;
183
302
  }
303
+ /** Edit an agent's name, ICP, precision mode, destination list or status. */
304
+ async updateSignalAgent(id, body) {
305
+ return (await this.request(`/signals/agents/${id}`, { method: "PATCH", body })).json;
306
+ }
184
307
  async deleteSignalAgent(id) {
185
308
  return (await this.request(`/signals/agents/${id}`, { method: "DELETE" })).json;
186
309
  }
310
+ async addSignalAgentSignal(id, body) {
311
+ return (await this.request(`/signals/agents/${id}/signals`, { method: "POST", body })).json;
312
+ }
313
+ async removeSignalAgentSignal(id, signalId) {
314
+ return (await this.request(`/signals/agents/${id}/signals/${signalId}`, { method: "DELETE" })).json;
315
+ }
316
+ /** Record (or clear, with null) the verdict on one lead. */
317
+ async setLeadFeedback(leadId, body) {
318
+ return (await this.request(`/signals/leads/${leadId}/feedback`, { method: "POST", body })).json;
319
+ }
320
+ // --- Workers ---
321
+ async listWorkers(query = {}) {
322
+ return (await this.request("/workers", { query })).json;
323
+ }
324
+ /** The posts your Workers have written, newest first. */
325
+ async listWorkerSuggestions(query = {}) {
326
+ return (await this.request("/workers/suggestions", { query })).json;
327
+ }
328
+ /** Save one suggestion as a draft. Nothing is posted. */
329
+ async draftWorkerSuggestion(id, body) {
330
+ return (await this.request(`/workers/suggestions/${id}/draft`, { method: "POST", body })).json;
331
+ }
332
+ /** Save one suggestion as a scheduled post. */
333
+ async scheduleWorkerSuggestion(id, body) {
334
+ return (await this.request(`/workers/suggestions/${id}/schedule`, { method: "POST", body })).json;
335
+ }
336
+ /** Clear one suggestion out of the To review list. */
337
+ async dismissWorkerSuggestion(id, body) {
338
+ return (await this.request(`/workers/suggestions/${id}/dismiss`, { method: "POST", body })).json;
339
+ }
187
340
  // --- Engage ---
188
341
  async listEngageFeeds(query = {}) {
189
342
  return (await this.request("/engage/feeds", { query })).json;
@@ -191,6 +344,15 @@ var SuperXAPI = class {
191
344
  async getEngageFeedPosts(feedId, query = {}) {
192
345
  return (await this.request(`/engage/feeds/${encodeURIComponent(feedId)}/posts`, { query })).json;
193
346
  }
347
+ async createEngageFeed(body) {
348
+ return (await this.request("/engage/feeds", { method: "POST", body })).json;
349
+ }
350
+ async updateEngageFeed(feedId, body) {
351
+ return (await this.request(`/engage/feeds/${encodeURIComponent(feedId)}`, { method: "PATCH", body })).json;
352
+ }
353
+ async deleteEngageFeed(feedId, query = {}) {
354
+ return (await this.request(`/engage/feeds/${encodeURIComponent(feedId)}`, { method: "DELETE", query })).json;
355
+ }
194
356
  // --- Scheduled posts ---
195
357
  async listScheduled(query = {}) {
196
358
  return (await this.request("/scheduled-posts", { query })).json;
@@ -201,6 +363,29 @@ var SuperXAPI = class {
201
363
  const res = await this.request("/scheduled-posts", { method: "POST", body, headers });
202
364
  return { json: res.json, replayed: res.headers.get("idempotency-replayed") === "true" };
203
365
  }
366
+ /**
367
+ * Publish immediately: the create endpoint with scheduled_for "now". The
368
+ * Idempotency-Key is REQUIRED by the API (a retry must never post twice),
369
+ * so it is a plain parameter here rather than an optional one.
370
+ */
371
+ async publishNow(body, idempotencyKey) {
372
+ const res = await this.request("/scheduled-posts", {
373
+ method: "POST",
374
+ body,
375
+ headers: { "Idempotency-Key": idempotencyKey }
376
+ });
377
+ return { json: res.json, replayed: res.headers.get("idempotency-replayed") === "true" };
378
+ }
379
+ // --- Scheduled posts: bulk queue operations ---
380
+ async bulkRetimeScheduled(body) {
381
+ return (await this.request("/scheduled-posts/bulk/retime", { method: "POST", body })).json;
382
+ }
383
+ async bulkEnableAutoRetweet(body) {
384
+ return (await this.request("/scheduled-posts/bulk/auto-retweet", { method: "POST", body })).json;
385
+ }
386
+ async bulkDeleteScheduled(body) {
387
+ return (await this.request("/scheduled-posts/bulk/delete", { method: "POST", body })).json;
388
+ }
204
389
  async updateScheduled(id, body) {
205
390
  return (await this.request(`/scheduled-posts/${encodeURIComponent(id)}`, { method: "PATCH", body })).json;
206
391
  }
@@ -224,6 +409,21 @@ var SuperXAPI = class {
224
409
  async deleteContextProduct(id, query = {}) {
225
410
  return (await this.request(`/context/products/${encodeURIComponent(id)}`, { method: "DELETE", query })).json;
226
411
  }
412
+ /** Full replace of the account's product list (max 5). */
413
+ async setContextProducts(body) {
414
+ return (await this.request("/context/products", { method: "PUT", body })).json;
415
+ }
416
+ /** Rebuild the generated style guide from recent posts. Free, once an hour. */
417
+ async regenerateStyleGuide(body) {
418
+ return (await this.request("/context/style-guide/regenerate", { method: "POST", body })).json;
419
+ }
420
+ /** Re-read a saved product's page and refresh its stored details. Free. */
421
+ async scrapeContextProduct(id, body) {
422
+ return (await this.request(`/context/products/${encodeURIComponent(id)}/scrape`, {
423
+ method: "POST",
424
+ body
425
+ })).json;
426
+ }
227
427
  // --- Queue settings ---
228
428
  async getQueueSettings(query = {}) {
229
429
  return (await this.request("/queue-settings", { query })).json;
@@ -276,6 +476,99 @@ var SuperXAPI = class {
276
476
  async generateArticleCover(id, body) {
277
477
  return (await this.request(`/articles/${encodeURIComponent(id)}/cover`, { method: "POST", body })).json;
278
478
  }
479
+ /** Saved article cover styles (pass an id as style_id to the cover call). */
480
+ async listCoverStyles(query = {}) {
481
+ return (await this.request("/cover-styles", { query })).json;
482
+ }
483
+ // --- Datasets (Ask SuperX collections) ---
484
+ async listDatasets(query = {}) {
485
+ return (await this.request("/datasets", { query })).json;
486
+ }
487
+ async getDataset(id) {
488
+ return (await this.request(`/datasets/${encodeURIComponent(id)}`)).json;
489
+ }
490
+ async getDatasetRows(id, query = {}) {
491
+ return (await this.request(`/datasets/${encodeURIComponent(id)}/rows`, { query })).json;
492
+ }
493
+ /** CSV export. Raw text, with the server's filename from Content-Disposition. */
494
+ async exportDatasetCsv(id) {
495
+ const { text, headers } = await this.requestText(
496
+ `/datasets/${encodeURIComponent(id)}/export`,
497
+ { format: "csv" }
498
+ );
499
+ const disposition = headers.get("content-disposition") || "";
500
+ const match = /filename="([^"]+)"/.exec(disposition);
501
+ return { filename: match ? match[1] : `superx-dataset-${id}.csv`, text };
502
+ }
503
+ async addDatasetToList(id, body) {
504
+ return (await this.request(`/datasets/${encodeURIComponent(id)}/contacts`, { method: "POST", body })).json;
505
+ }
506
+ /** Start a collection. Answers a ready dataset, or a collecting one (202). */
507
+ async createDataset(body) {
508
+ return (await this.request("/datasets", { method: "POST", body })).json;
509
+ }
510
+ /** Draft one outreach message per row of a research dataset. Text only. */
511
+ async draftOutreachDms(id, body) {
512
+ return (await this.request(`/datasets/${encodeURIComponent(id)}/outreach-drafts`, {
513
+ method: "POST",
514
+ body
515
+ })).json;
516
+ }
517
+ /** Filter a dataset by each row's text into a NEW dataset (200 or 202). */
518
+ async refineDataset(id, body) {
519
+ return (await this.request(`/datasets/${encodeURIComponent(id)}/refine`, {
520
+ method: "POST",
521
+ body
522
+ })).json;
523
+ }
524
+ // --- Audience (the four system people-lists) ---
525
+ /** kind: followers | following | repliers | reposters. Cursor paging. */
526
+ async getAudience(kind, query = {}) {
527
+ return (await this.request(`/audience/${encodeURIComponent(kind)}`, { query })).json;
528
+ }
529
+ /** Posts @-mentioning the account, read live. Costs 3 feed fetches. */
530
+ async getMentions(query = {}) {
531
+ return (await this.request("/engage/mentions", { query })).json;
532
+ }
533
+ // --- Live X lookups (read X now, not SuperX's stored data) ---
534
+ async lookupXPost(id, query = {}) {
535
+ return (await this.request(`/x/posts/${encodeURIComponent(id)}`, { query })).json;
536
+ }
537
+ async getXPostReplies(id, query = {}) {
538
+ return (await this.request(`/x/posts/${encodeURIComponent(id)}/replies`, { query })).json;
539
+ }
540
+ async lookupXUser(handle) {
541
+ return (await this.request(`/x/users/${encodeURIComponent(handle)}`)).json;
542
+ }
543
+ async getXUserPosts(handle, query = {}) {
544
+ return (await this.request(`/x/users/${encodeURIComponent(handle)}/posts`, { query })).json;
545
+ }
546
+ // --- Inspiration media (cross-platform media index) ---
547
+ async searchInspirationMedia(query = {}) {
548
+ return (await this.request("/inspiration/media", { query })).json;
549
+ }
550
+ // --- DM campaigns (enqueue only: the SuperX app sends) ---
551
+ async queueDmCampaign(body, idempotencyKey) {
552
+ const headers = {};
553
+ if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
554
+ const res = await this.request("/dm/campaigns", { method: "POST", body, headers });
555
+ return { json: res.json, replayed: res.headers.get("idempotency-replayed") === "true" };
556
+ }
557
+ async getDmCampaign(id, query = {}) {
558
+ return (await this.request(`/dm/campaigns/${encodeURIComponent(id)}`, { query })).json;
559
+ }
560
+ async cancelDmCampaign(id, query = {}) {
561
+ return (await this.request(`/dm/campaigns/${encodeURIComponent(id)}`, {
562
+ method: "DELETE",
563
+ query
564
+ })).json;
565
+ }
566
+ async listDmQueue(query = {}) {
567
+ return (await this.request("/dm/queue", { query })).json;
568
+ }
569
+ async getDmLimits(query = {}) {
570
+ return (await this.request("/dm/limits", { query })).json;
571
+ }
279
572
  // --- Docs (unauthenticated markdown) ---
280
573
  async docs() {
281
574
  const url = this.buildUrl("/docs");
@@ -412,6 +705,7 @@ async function status() {
412
705
  api_url: config.apiUrl,
413
706
  owner: me2?.data?.owner ?? null,
414
707
  plan: me2?.data?.plan ?? null,
708
+ credits: me2?.data?.credits ?? null,
415
709
  key: me2?.data?.key ?? null,
416
710
  rate_limit: api.lastRateLimit
417
711
  });
@@ -452,6 +746,24 @@ async function postsAnalytics(argv) {
452
746
  })
453
747
  );
454
748
  }
749
+ async function postsDraft(argv) {
750
+ if (!argv.brief || !argv.brief.trim()) {
751
+ note('Provide --brief "what the post should say".');
752
+ process.exit(1);
753
+ }
754
+ const body = { brief: argv.brief };
755
+ if (argv.count !== void 0) body.count = argv.count;
756
+ if (argv.voice) body.voice = argv.voice;
757
+ if (argv.creator) body.creator = argv.creator;
758
+ if (argv.mirror) body.mirror = argv.mirror;
759
+ if (argv.collection) body.collection = argv.collection;
760
+ if (argv.instructions) body.instructions = argv.instructions;
761
+ if (argv.account) body.account_id = argv.account;
762
+ const api = new SuperXAPI(getConfig());
763
+ const json = await api.draftPost(body);
764
+ note("Nothing was scheduled. Review the text, then pass it to scheduled:create.");
765
+ printJson(json);
766
+ }
455
767
  async function repliesList(argv) {
456
768
  const api = new SuperXAPI(getConfig());
457
769
  printJson(
@@ -477,6 +789,49 @@ async function repliesReceived(argv) {
477
789
  })
478
790
  );
479
791
  }
792
+ async function postsRemix(argv) {
793
+ const body = {
794
+ text: argv.text,
795
+ closeness: argv.closeness
796
+ };
797
+ if (argv.instructions) body.instructions = argv.instructions;
798
+ if (argv.account) body.account_id = argv.account;
799
+ const api = new SuperXAPI(getConfig());
800
+ printJson(await api.remixPost(body));
801
+ }
802
+
803
+ // src/commands/tools.ts
804
+ async function toolsInlineEdit(argv) {
805
+ if (!argv.instruction && !argv.type) {
806
+ note('Provide --instruction "..." or --type <preset> (or both).');
807
+ process.exit(1);
808
+ }
809
+ const body = { text: argv.text };
810
+ if (argv.full) body.full_text = argv.full;
811
+ if (argv.instruction) body.instruction = argv.instruction;
812
+ if (argv.type) body.edit_type = argv.type;
813
+ if (argv.account) body.account_id = argv.account;
814
+ const api = new SuperXAPI(getConfig());
815
+ printJson(await api.inlineEdit(body));
816
+ }
817
+ async function toolsRephrase(argv) {
818
+ const body = { type: argv.type, text: argv.text };
819
+ if (argv.account) body.account_id = argv.account;
820
+ const api = new SuperXAPI(getConfig());
821
+ printJson(await api.rephrase(body));
822
+ }
823
+ async function toolsFactcheck(argv) {
824
+ const body = { text: argv.text };
825
+ if (argv.account) body.account_id = argv.account;
826
+ const api = new SuperXAPI(getConfig());
827
+ printJson(await api.factCheck(body));
828
+ }
829
+ async function toolsPredict(argv) {
830
+ const body = { version_a: argv.a, version_b: argv.b };
831
+ if (argv.account) body.account_id = argv.account;
832
+ const api = new SuperXAPI(getConfig());
833
+ printJson(await api.predictAlgorithm(body));
834
+ }
480
835
 
481
836
  // src/commands/inspiration.ts
482
837
  async function inspirationSearch(argv) {
@@ -501,6 +856,70 @@ async function inspirationSearch(argv) {
501
856
  })
502
857
  );
503
858
  }
859
+ async function inspirationMedia(argv) {
860
+ const api = new SuperXAPI(getConfig());
861
+ printJson(
862
+ await api.searchInspirationMedia({
863
+ q: argv.query,
864
+ platforms: argv.platforms,
865
+ time_filter: argv.timeFilter,
866
+ media_type: argv.mediaType,
867
+ content_type: argv.contentType,
868
+ limit: argv.limit
869
+ })
870
+ );
871
+ }
872
+
873
+ // src/commands/x.ts
874
+ function toPostId(ref) {
875
+ const s = String(ref || "").trim();
876
+ if (/^\d{1,25}$/.test(s)) return s;
877
+ const m = s.match(
878
+ /^https?:\/\/(?:www\.|mobile\.)?(?:x\.com|twitter\.com)\/[A-Za-z0-9_]+\/status(?:es)?\/(\d+)/i
879
+ );
880
+ if (m) return m[1];
881
+ throw new ApiError(
882
+ 0,
883
+ "invalid_parameter",
884
+ "Provide an x.com/twitter.com post URL or a bare numeric post id."
885
+ );
886
+ }
887
+ function toHandle(raw) {
888
+ const s = String(raw || "").trim().replace(/^@/, "");
889
+ if (!/^[A-Za-z0-9_]{1,15}$/.test(s)) {
890
+ throw new ApiError(
891
+ 0,
892
+ "invalid_parameter",
893
+ "Provide a valid X handle: 1-15 letters, numbers or underscores."
894
+ );
895
+ }
896
+ return s;
897
+ }
898
+ async function xPost(argv) {
899
+ const api = new SuperXAPI(getConfig());
900
+ printJson(
901
+ await api.lookupXPost(toPostId(argv.id), {
902
+ include_quotes: argv.quotes ? "true" : void 0
903
+ })
904
+ );
905
+ }
906
+ async function xReplies(argv) {
907
+ const api = new SuperXAPI(getConfig());
908
+ printJson(await api.getXPostReplies(toPostId(argv.id), { limit: argv.limit }));
909
+ }
910
+ async function xUser(argv) {
911
+ const api = new SuperXAPI(getConfig());
912
+ printJson(await api.lookupXUser(toHandle(argv.handle)));
913
+ }
914
+ async function xUserPosts(argv) {
915
+ const api = new SuperXAPI(getConfig());
916
+ printJson(
917
+ await api.getXUserPosts(toHandle(argv.handle), {
918
+ limit: argv.limit,
919
+ exclude_reposts: argv.reposts === false ? "true" : void 0
920
+ })
921
+ );
922
+ }
504
923
 
505
924
  // src/commands/contacts.ts
506
925
  async function contactsList(argv) {
@@ -525,6 +944,38 @@ async function contactsReplies(argv) {
525
944
  })
526
945
  );
527
946
  }
947
+ async function contactsGet(argv) {
948
+ const api = new SuperXAPI(getConfig());
949
+ printJson(
950
+ await api.getContact(argv.id, {
951
+ account_id: argv.account,
952
+ // Only send the flag when asked: the default read is cache-only and
953
+ // costs no enrichment.
954
+ refresh: argv.refresh ? "true" : void 0
955
+ })
956
+ );
957
+ }
958
+ async function contactsNotes(argv) {
959
+ const api = new SuperXAPI(getConfig());
960
+ printJson(await api.listContactNotes(argv.id, { account_id: argv.account }));
961
+ }
962
+ async function contactsNotesAdd(argv) {
963
+ const body = { body: argv.body };
964
+ if (argv.account) body.account_id = argv.account;
965
+ const api = new SuperXAPI(getConfig());
966
+ printJson(await api.addContactNote(argv.id, body));
967
+ }
968
+ async function contactsNotesUpdate(argv) {
969
+ const body = { body: argv.body };
970
+ if (argv.account) body.account_id = argv.account;
971
+ const api = new SuperXAPI(getConfig());
972
+ printJson(await api.updateContactNote(argv.id, argv.noteId, body));
973
+ }
974
+ async function contactsNotesDelete(argv) {
975
+ const api = new SuperXAPI(getConfig());
976
+ await api.deleteContactNote(argv.id, argv.noteId, { account_id: argv.account });
977
+ printJson({ data: { id: argv.noteId, deleted: true } });
978
+ }
528
979
 
529
980
  // src/commands/lists.ts
530
981
  async function listsList(argv) {
@@ -555,6 +1006,181 @@ async function listsRemoveMember(argv) {
555
1006
  await api.removeListMember(argv.id, argv.memberId);
556
1007
  printJson({ data: { id: argv.memberId, removed: true } });
557
1008
  }
1009
+ async function listsCreate(argv) {
1010
+ const body = { name: argv.name };
1011
+ if (argv.account) body.account_id = argv.account;
1012
+ const api = new SuperXAPI(getConfig());
1013
+ printJson(await api.createList(body));
1014
+ }
1015
+ async function listsRename(argv) {
1016
+ const body = { name: argv.name };
1017
+ if (argv.account) body.account_id = argv.account;
1018
+ const api = new SuperXAPI(getConfig());
1019
+ printJson(await api.renameList(argv.id, body));
1020
+ }
1021
+ async function listsDelete(argv) {
1022
+ const api = new SuperXAPI(getConfig());
1023
+ await api.deleteList(argv.id, { account_id: argv.account });
1024
+ printJson({ data: { id: argv.id, deleted: true } });
1025
+ }
1026
+ function commaList(value) {
1027
+ return value.split(",").map((s) => s.trim()).filter(Boolean);
1028
+ }
1029
+ async function listsAddMembers(argv) {
1030
+ const ids = commaList(argv.xUserIds || "");
1031
+ if (ids.length === 0) {
1032
+ note("Provide at least one id: --x-user-ids 44196397,1234567890");
1033
+ process.exit(1);
1034
+ }
1035
+ const body = { x_user_ids: ids };
1036
+ if (argv.account) body.account_id = argv.account;
1037
+ const api = new SuperXAPI(getConfig());
1038
+ printJson(await api.addListMembers(argv.id, body));
1039
+ }
1040
+ async function listsRemoveMembers(argv) {
1041
+ const ids = commaList(argv.memberIds || "");
1042
+ if (ids.length === 0) {
1043
+ note("Provide at least one id: --member-ids abc123,def456");
1044
+ process.exit(1);
1045
+ }
1046
+ const body = { member_ids: ids };
1047
+ if (argv.account) body.account_id = argv.account;
1048
+ const api = new SuperXAPI(getConfig());
1049
+ printJson(await api.removeListMembers(argv.id, body));
1050
+ }
1051
+
1052
+ // src/commands/datasets.ts
1053
+ var import_fs = __toESM(require("fs"));
1054
+ var import_path = __toESM(require("path"));
1055
+ async function datasetsList(argv) {
1056
+ const api = new SuperXAPI(getConfig());
1057
+ printJson(await api.listDatasets({ limit: argv.limit, page: argv.page }));
1058
+ }
1059
+ async function datasetsGet(argv) {
1060
+ const api = new SuperXAPI(getConfig());
1061
+ printJson(await api.getDataset(argv.id));
1062
+ }
1063
+ async function datasetsRows(argv) {
1064
+ const api = new SuperXAPI(getConfig());
1065
+ printJson(
1066
+ await api.getDatasetRows(argv.id, { limit: argv.limit, page: argv.page })
1067
+ );
1068
+ }
1069
+ async function datasetsExport(argv) {
1070
+ const api = new SuperXAPI(getConfig());
1071
+ const { filename, text } = await api.exportDatasetCsv(argv.id);
1072
+ if (argv.out === "-") {
1073
+ process.stdout.write(text);
1074
+ return;
1075
+ }
1076
+ const target = argv.out ? argv.out : import_path.default.join(process.cwd(), import_path.default.basename(filename));
1077
+ import_fs.default.writeFileSync(target, text, "utf8");
1078
+ note(`Wrote ${target}`);
1079
+ printJson({ file: target, bytes: Buffer.byteLength(text, "utf8") });
1080
+ }
1081
+ async function datasetsAddToList(argv) {
1082
+ const body = { list_id: argv.listId };
1083
+ if (argv.account) body.account_id = argv.account;
1084
+ const api = new SuperXAPI(getConfig());
1085
+ printJson(await api.addDatasetToList(argv.id, body));
1086
+ }
1087
+ var WAIT_POLL_MS = 5e3;
1088
+ var WAIT_TIMEOUT_MS = 15 * 60 * 1e3;
1089
+ async function waitForDataset(api, started) {
1090
+ const datasetId = started?.data?.id;
1091
+ if (!datasetId || started?.data?.status !== "collecting") return false;
1092
+ note(`Running ${datasetId}; polling every ${WAIT_POLL_MS / 1e3}s until it is ready.`);
1093
+ const deadline = Date.now() + WAIT_TIMEOUT_MS;
1094
+ let latest = started;
1095
+ while (Date.now() < deadline) {
1096
+ await new Promise((resolve) => setTimeout(resolve, WAIT_POLL_MS));
1097
+ latest = await api.getDataset(datasetId);
1098
+ if (latest?.data?.status !== "collecting") {
1099
+ printJson(latest);
1100
+ return true;
1101
+ }
1102
+ }
1103
+ note("Still running after 15 minutes; giving up on waiting (the job keeps going).");
1104
+ printJson(latest);
1105
+ return true;
1106
+ }
1107
+ async function datasetsCollect(argv) {
1108
+ const splitList = (raw) => raw === void 0 ? void 0 : raw.split(",").map((k) => k.trim()).filter(Boolean);
1109
+ const filters = {};
1110
+ const keywords = splitList(argv.keywords);
1111
+ if (keywords) filters.keywords = keywords;
1112
+ const bioKeywords = splitList(argv.bioKeywords);
1113
+ if (bioKeywords) filters.bio_keywords = bioKeywords;
1114
+ if (argv.minFollowers !== void 0) filters.min_followers = argv.minFollowers;
1115
+ if (argv.requireWebsite !== void 0) filters.require_website = argv.requireWebsite;
1116
+ if (argv.requireCanDm !== void 0) filters.require_can_dm = argv.requireCanDm;
1117
+ if (argv.sinceDays !== void 0) filters.since_days = argv.sinceDays;
1118
+ if (argv.sort !== void 0) filters.sort = argv.sort;
1119
+ const body = { source: argv.source };
1120
+ if (argv.target) body.target = argv.target;
1121
+ if (argv.title) body.title = argv.title;
1122
+ if (argv.maxRows !== void 0) body.max_rows = argv.maxRows;
1123
+ if (argv.account) body.account_id = argv.account;
1124
+ if (Object.keys(filters).length > 0) body.filters = filters;
1125
+ const api = new SuperXAPI(getConfig());
1126
+ const started = await api.createDataset(body);
1127
+ if (argv.wait && await waitForDataset(api, started)) return;
1128
+ printJson(started);
1129
+ }
1130
+ async function datasetsResearch(argv) {
1131
+ const body = { source: "research" };
1132
+ if (argv.handles !== void 0) {
1133
+ body.handles = argv.handles.split(",").map((h) => h.trim()).filter(Boolean);
1134
+ }
1135
+ if (argv.list) body.list_id = argv.list;
1136
+ if (argv.agent !== void 0) body.agent_id = argv.agent;
1137
+ if (argv.dataset) body.dataset_id = argv.dataset;
1138
+ if (argv.max !== void 0) body.max_rows = argv.max;
1139
+ if (argv.focus) body.focus = argv.focus;
1140
+ if (argv.title) body.title = argv.title;
1141
+ if (argv.account) body.account_id = argv.account;
1142
+ const api = new SuperXAPI(getConfig());
1143
+ const started = await api.createDataset(body);
1144
+ if (argv.wait && await waitForDataset(api, started)) return;
1145
+ printJson(started);
1146
+ }
1147
+ async function datasetsOutreachDrafts(argv) {
1148
+ const body = { format: argv.format };
1149
+ if (argv.instructions) body.instructions = argv.instructions;
1150
+ if (argv.account) body.account_id = argv.account;
1151
+ const api = new SuperXAPI(getConfig());
1152
+ printJson(await api.draftOutreachDms(argv.id, body));
1153
+ }
1154
+ async function datasetsRefine(argv) {
1155
+ const body = { criterion: argv.criterion };
1156
+ if (argv.keep !== void 0) body.keep_matching = argv.keep;
1157
+ if (argv.sort) body.sort_by = argv.sort;
1158
+ if (argv.limit !== void 0) body.limit = argv.limit;
1159
+ if (argv.title) body.title = argv.title;
1160
+ if (argv.account) body.account_id = argv.account;
1161
+ const api = new SuperXAPI(getConfig());
1162
+ const started = await api.refineDataset(argv.id, body);
1163
+ if (argv.wait && await waitForDataset(api, started)) return;
1164
+ printJson(started);
1165
+ }
1166
+
1167
+ // src/commands/audience.ts
1168
+ var KINDS = ["followers", "following", "repliers", "reposters"];
1169
+ async function audienceList(argv) {
1170
+ const kind = String(argv.kind || "").trim();
1171
+ if (!KINDS.includes(kind)) {
1172
+ note(`kind must be one of: ${KINDS.join(", ")}`);
1173
+ process.exit(1);
1174
+ }
1175
+ const api = new SuperXAPI(getConfig());
1176
+ printJson(
1177
+ await api.getAudience(kind, {
1178
+ account_id: argv.account,
1179
+ cursor: argv.cursor,
1180
+ limit: argv.limit
1181
+ })
1182
+ );
1183
+ }
558
1184
 
559
1185
  // src/commands/signals.ts
560
1186
  async function signalsAgents(argv) {
@@ -576,6 +1202,17 @@ async function signalsLeads(argv) {
576
1202
  })
577
1203
  );
578
1204
  }
1205
+ async function signalsSearch(argv) {
1206
+ const body = {
1207
+ keywords: argv.keywords,
1208
+ icp_description: argv.icp
1209
+ };
1210
+ if (argv.precision) body.precision = argv.precision;
1211
+ if (argv.max !== void 0) body.max_leads = argv.max;
1212
+ if (argv.account) body.account_id = argv.account;
1213
+ const api = new SuperXAPI(getConfig());
1214
+ printJson(await api.searchLeads(body));
1215
+ }
579
1216
  async function signalsCreateAgent(argv) {
580
1217
  const body = {
581
1218
  name: argv.name,
@@ -585,6 +1222,8 @@ async function signalsCreateAgent(argv) {
585
1222
  if (argv["list-id"]) body.destination_list_id = argv["list-id"];
586
1223
  const keywords = (argv.keyword || []).filter((k) => typeof k === "string" && k.length > 0);
587
1224
  if (keywords.length > 0) body.keywords = keywords;
1225
+ const specs = (argv.signal || []).filter((s) => typeof s === "string" && s.length > 0);
1226
+ if (specs.length > 0) body.signals = specs.map(parseSignalSpec);
588
1227
  const api = new SuperXAPI(getConfig());
589
1228
  const { json, replayed } = await api.createSignalAgent(body, argv["idempotency-key"]);
590
1229
  if (replayed) {
@@ -606,6 +1245,139 @@ async function signalsDeleteAgent(argv) {
606
1245
  const api = new SuperXAPI(getConfig());
607
1246
  printJson(await api.deleteSignalAgent(argv.id));
608
1247
  }
1248
+ var SIGNAL_TYPE_ALIASES = {
1249
+ keyword: "keyword_watch",
1250
+ keyword_watch: "keyword_watch",
1251
+ profile: "profile_watch",
1252
+ profile_watch: "profile_watch",
1253
+ follower: "follower_watch",
1254
+ follower_watch: "follower_watch",
1255
+ list: "list_watch",
1256
+ list_watch: "list_watch"
1257
+ };
1258
+ function parseSignalSpec(spec) {
1259
+ const at = spec.indexOf(":");
1260
+ const rawType = at === -1 ? "" : spec.slice(0, at).trim().toLowerCase();
1261
+ const target = at === -1 ? "" : spec.slice(at + 1).trim();
1262
+ const type = SIGNAL_TYPE_ALIASES[rawType];
1263
+ if (!type || !target) {
1264
+ note(
1265
+ `Could not read --signal "${spec}". Use type:target, for example keyword:"just shipped my MVP", profile:@naval, follower:@naval or list:https://x.com/i/lists/123.`
1266
+ );
1267
+ process.exit(1);
1268
+ }
1269
+ if (type === "keyword_watch") return { type, query: target };
1270
+ if (type === "list_watch") return { type, list: target };
1271
+ return { type, handle: target };
1272
+ }
1273
+ async function signalsUpdateAgent(argv) {
1274
+ const body = {};
1275
+ if (argv.name !== void 0) body.name = argv.name;
1276
+ if (argv.icp !== void 0) body.icp_description = argv.icp;
1277
+ if (argv.precision !== void 0) body.precision_mode = argv.precision;
1278
+ if (argv["list-id"] !== void 0) body.destination_list_id = argv["list-id"];
1279
+ if (argv.status !== void 0) body.status = argv.status;
1280
+ if (Object.keys(body).length === 0) {
1281
+ note("Provide at least one of: --name, --icp, --precision, --list-id, --status.");
1282
+ process.exit(1);
1283
+ }
1284
+ const api = new SuperXAPI(getConfig());
1285
+ printJson(await api.updateSignalAgent(argv.id, body));
1286
+ }
1287
+ async function signalsAddSignal(argv) {
1288
+ const type = SIGNAL_TYPE_ALIASES[String(argv.type || "").toLowerCase()];
1289
+ if (!type) {
1290
+ note("--type must be one of: keyword_watch, profile_watch, follower_watch, list_watch.");
1291
+ process.exit(1);
1292
+ }
1293
+ const body = { type };
1294
+ if (argv.query !== void 0) body.query = argv.query;
1295
+ if (argv.handle !== void 0) body.handle = argv.handle;
1296
+ if (argv.list !== void 0) body.list = argv.list;
1297
+ if (argv.account) body.account_id = argv.account;
1298
+ const api = new SuperXAPI(getConfig());
1299
+ printJson(await api.addSignalAgentSignal(argv.id, body));
1300
+ }
1301
+ async function signalsRemoveSignal(argv) {
1302
+ const api = new SuperXAPI(getConfig());
1303
+ await api.removeSignalAgentSignal(argv.id, argv.signalId);
1304
+ printJson({ agent_id: argv.id, signal_id: argv.signalId, deleted: true });
1305
+ }
1306
+ async function signalsFeedback(argv) {
1307
+ const chosen = [argv.fit, argv["not-fit"], argv.clear].filter(Boolean);
1308
+ if (chosen.length !== 1) {
1309
+ note("Pass exactly one of --fit, --not-fit or --clear.");
1310
+ process.exit(1);
1311
+ }
1312
+ const feedback = argv.fit ? "fit" : argv["not-fit"] ? "not_fit" : null;
1313
+ const body = { feedback };
1314
+ if (argv.account) body.account_id = argv.account;
1315
+ const api = new SuperXAPI(getConfig());
1316
+ printJson(await api.setLeadFeedback(argv.leadId, body));
1317
+ }
1318
+ async function signalsSuggestKeywords(argv) {
1319
+ const body = { icp_description: argv.icp };
1320
+ if (argv.account) body.account_id = argv.account;
1321
+ const api = new SuperXAPI(getConfig());
1322
+ printJson(await api.suggestKeywords(body));
1323
+ }
1324
+ async function signalsExpandIcp(argv) {
1325
+ const text = (argv.text || "").trim();
1326
+ const url = (argv.url || "").trim();
1327
+ if (!text && !url) {
1328
+ note("Provide --text with an audience description, or --url with a website.");
1329
+ process.exit(1);
1330
+ }
1331
+ if (text && url) {
1332
+ note("Use either --text or --url, not both.");
1333
+ process.exit(1);
1334
+ }
1335
+ const body = url ? { url } : { icp_description: text };
1336
+ if (argv.account) body.account_id = argv.account;
1337
+ const api = new SuperXAPI(getConfig());
1338
+ printJson(url ? await api.expandIcpFromUrl(body) : await api.expandIcp(body));
1339
+ }
1340
+
1341
+ // src/commands/workers.ts
1342
+ async function workersList(argv) {
1343
+ const api = new SuperXAPI(getConfig());
1344
+ printJson(await api.listWorkers({ account_id: argv.account }));
1345
+ }
1346
+ async function workersSuggestions(argv) {
1347
+ const api = new SuperXAPI(getConfig());
1348
+ printJson(
1349
+ await api.listWorkerSuggestions({
1350
+ account_id: argv.account,
1351
+ status: argv.status,
1352
+ worker_id: argv.worker,
1353
+ limit: argv.limit,
1354
+ page: argv.page
1355
+ })
1356
+ );
1357
+ }
1358
+ async function workersDraft(argv) {
1359
+ const body = {};
1360
+ if (argv.account) body.account_id = argv.account;
1361
+ const api = new SuperXAPI(getConfig());
1362
+ printJson(await api.draftWorkerSuggestion(argv.id, body));
1363
+ }
1364
+ async function workersSchedule(argv) {
1365
+ if (typeof argv.at !== "string" || argv.at.length === 0) {
1366
+ note("--at is required: the UTC ISO-8601 time to post, for example 2026-09-15T14:00:00Z.");
1367
+ process.exit(1);
1368
+ return;
1369
+ }
1370
+ const body = { scheduled_for: argv.at };
1371
+ if (argv.account) body.account_id = argv.account;
1372
+ const api = new SuperXAPI(getConfig());
1373
+ printJson(await api.scheduleWorkerSuggestion(argv.id, body));
1374
+ }
1375
+ async function workersDismiss(argv) {
1376
+ const body = {};
1377
+ if (argv.account) body.account_id = argv.account;
1378
+ const api = new SuperXAPI(getConfig());
1379
+ printJson(await api.dismissWorkerSuggestion(argv.id, body));
1380
+ }
609
1381
 
610
1382
  // src/commands/engage.ts
611
1383
  async function engageFeeds(argv) {
@@ -626,6 +1398,89 @@ async function engagePosts(argv) {
626
1398
  })
627
1399
  );
628
1400
  }
1401
+ function feedSourceBody(argv) {
1402
+ const keywords = (argv.keyword || []).filter(
1403
+ (k) => typeof k === "string" && k.length > 0
1404
+ );
1405
+ const families = [
1406
+ keywords.length > 0 ? "keywords" : null,
1407
+ argv["x-list"] ? "x_list" : null,
1408
+ argv["list-id"] ? "list" : null
1409
+ ].filter(Boolean);
1410
+ if (families.length === 0) return null;
1411
+ if (families.length > 1) {
1412
+ note("Use exactly one source: --keyword, --x-list or --list-id.");
1413
+ process.exit(1);
1414
+ }
1415
+ if (keywords.length > 0) return { type: "keywords", keywords };
1416
+ if (argv["x-list"]) {
1417
+ const raw = String(argv["x-list"]).trim();
1418
+ return /^\d{1,32}$/.test(raw) ? { type: "x_list", x_list_id: raw } : { type: "x_list", x_list_url: raw };
1419
+ }
1420
+ return { type: "list", list_id: argv["list-id"] };
1421
+ }
1422
+ async function engageFeedsCreate(argv) {
1423
+ const source = feedSourceBody(argv);
1424
+ if (!source) {
1425
+ note("Provide a source: --keyword (repeatable), --x-list or --list-id.");
1426
+ process.exit(1);
1427
+ }
1428
+ const body = { name: argv.name, ...source };
1429
+ if (argv.account) body.account_id = argv.account;
1430
+ const api = new SuperXAPI(getConfig());
1431
+ printJson(await api.createEngageFeed(body));
1432
+ }
1433
+ async function engageFeedsUpdate(argv) {
1434
+ const source = feedSourceBody(argv);
1435
+ const body = { ...source || {} };
1436
+ delete body.type;
1437
+ if (argv.name !== void 0) body.name = argv.name;
1438
+ if (Object.keys(body).length === 0) {
1439
+ note("Provide --name, or a new source: --keyword, --x-list or --list-id.");
1440
+ process.exit(1);
1441
+ }
1442
+ if (argv.account) body.account_id = argv.account;
1443
+ const api = new SuperXAPI(getConfig());
1444
+ printJson(await api.updateEngageFeed(argv.feedId, body));
1445
+ }
1446
+ async function engageFeedsDelete(argv) {
1447
+ const api = new SuperXAPI(getConfig());
1448
+ await api.deleteEngageFeed(argv.feedId, { account_id: argv.account });
1449
+ printJson({ id: argv.feedId, deleted: true });
1450
+ }
1451
+ async function engageMentions(argv) {
1452
+ const api = new SuperXAPI(getConfig());
1453
+ printJson(
1454
+ await api.getMentions({
1455
+ account_id: argv.account,
1456
+ sort: argv.sort,
1457
+ // yargs boolean: pass through only when the flag was given.
1458
+ include_replied: argv.includeReplied === void 0 ? void 0 : String(argv.includeReplied),
1459
+ cursor: argv.cursor
1460
+ })
1461
+ );
1462
+ }
1463
+ async function engageReplyDraft(argv) {
1464
+ const hasPost = !!argv.post;
1465
+ const hasText = !!argv.text;
1466
+ if (hasPost === hasText) {
1467
+ note('Provide exactly one of --post <id> (read live) or --text "the post".');
1468
+ process.exit(1);
1469
+ }
1470
+ const body = {};
1471
+ if (hasPost) body.post_id = argv.post;
1472
+ else {
1473
+ const post = { text: argv.text };
1474
+ if (argv.author) post.author_name = argv.author;
1475
+ if (argv.handle) post.author_handle = argv.handle;
1476
+ body.post = post;
1477
+ }
1478
+ if (argv.thoughts) body.thoughts = argv.thoughts;
1479
+ if (argv.tone) body.tone = argv.tone;
1480
+ if (argv.account) body.account_id = argv.account;
1481
+ const api = new SuperXAPI(getConfig());
1482
+ printJson(await api.draftReply(body));
1483
+ }
629
1484
 
630
1485
  // src/commands/scheduled.ts
631
1486
  async function scheduledList(argv) {
@@ -745,6 +1600,40 @@ function applyAdvancedFlags(argv, body) {
745
1600
  note("--auto-plug-threshold requires --auto-plug <templateId>.");
746
1601
  process.exit(1);
747
1602
  }
1603
+ const dmMessage = argv["auto-dm-message"];
1604
+ const dmTriggers = argv["auto-dm-triggers"];
1605
+ const dmMax = numericFlag("auto-dm-max", argv["auto-dm-max"]);
1606
+ const dmBatch = argv["auto-dm-batch"];
1607
+ const dmOff = argv["auto-dm"] === false;
1608
+ if (dmOff) {
1609
+ if (dmMessage !== void 0 || dmTriggers !== void 0 || typeof dmMax === "number" || dmBatch !== void 0) {
1610
+ note("--auto-dm-* flags cannot be combined with --no-auto-dm.");
1611
+ process.exit(1);
1612
+ }
1613
+ body.auto_dm = null;
1614
+ } else if (typeof dmMessage === "string" && dmMessage.length > 0) {
1615
+ const autoDm = { message: dmMessage };
1616
+ if (typeof dmTriggers === "string") {
1617
+ const wanted = dmTriggers.split(",").map((t) => t.trim().toLowerCase()).filter(Boolean);
1618
+ const unknown = wanted.filter(
1619
+ (t) => t !== "reply" && t !== "repost" && t !== "retweet"
1620
+ );
1621
+ if (unknown.length > 0 || wanted.length === 0) {
1622
+ note("--auto-dm-triggers takes a comma list of reply,repost (retweet is accepted as an alias of repost).");
1623
+ process.exit(1);
1624
+ }
1625
+ autoDm.triggers = {
1626
+ reply: wanted.includes("reply"),
1627
+ repost: wanted.includes("repost") || wanted.includes("retweet")
1628
+ };
1629
+ }
1630
+ if (typeof dmMax === "number") autoDm.max_dms = dmMax;
1631
+ if (typeof dmBatch === "boolean") autoDm.batch_mode = dmBatch;
1632
+ body.auto_dm = autoDm;
1633
+ } else if (dmTriggers !== void 0 || typeof dmMax === "number" || dmBatch !== void 0) {
1634
+ note("--auto-dm-triggers, --auto-dm-max and --auto-dm-batch require --auto-dm-message <text>.");
1635
+ process.exit(1);
1636
+ }
748
1637
  if (typeof argv["super-followers"] === "boolean") {
749
1638
  body.super_followers_only = argv["super-followers"];
750
1639
  }
@@ -859,10 +1748,118 @@ async function scheduledDelete(argv) {
859
1748
  const api = new SuperXAPI(getConfig());
860
1749
  printJson(await api.deleteScheduled(argv.id));
861
1750
  }
1751
+ async function postsPublish(argv) {
1752
+ const idempotencyKey = argv["idempotency-key"];
1753
+ if (typeof idempotencyKey !== "string" || idempotencyKey.length === 0) {
1754
+ note("--idempotency-key is required for posts:publish. Reuse the SAME key when retrying; only use a new key for new content.");
1755
+ process.exit(1);
1756
+ return;
1757
+ }
1758
+ const parts = (argv.part || []).filter((p) => typeof p === "string");
1759
+ const sourceCount = [argv.text, parts.length > 0 ? "p" : void 0, argv["parts-json"]].filter(
1760
+ (v) => v !== void 0
1761
+ ).length;
1762
+ if (sourceCount > 1) {
1763
+ note("Use exactly one of --text (single post), --part (thread), or --parts-json.");
1764
+ process.exit(1);
1765
+ }
1766
+ if (sourceCount === 0) {
1767
+ note("Provide --text for a single post, --part flags for a thread, or --parts-json.");
1768
+ process.exit(1);
1769
+ }
1770
+ if (argv.media !== void 0 && !argv.text) {
1771
+ note("--media applies to the --text single-post form. For threads, put media in --parts-json.");
1772
+ process.exit(1);
1773
+ }
1774
+ const body = { scheduled_for: "now" };
1775
+ if (argv["parts-json"] !== void 0) {
1776
+ body.parts = parsePartsJson(argv["parts-json"]);
1777
+ } else if (argv.text) {
1778
+ const media = mediaFromFlags(argv.media, argv["alt-text"]);
1779
+ if (media) {
1780
+ body.parts = [{ text: argv.text, media }];
1781
+ } else {
1782
+ body.text = argv.text;
1783
+ }
1784
+ } else {
1785
+ body.parts = parts.map((text) => ({ text }));
1786
+ }
1787
+ const tags = (argv.tag || []).filter((t) => typeof t === "string" && t.length > 0);
1788
+ if (tags.length > 0) body.tags = tags;
1789
+ applyAdvancedFlags(argv, body);
1790
+ if (argv.account) body.account_id = argv.account;
1791
+ const api = new SuperXAPI(getConfig());
1792
+ const { json, replayed } = await api.publishNow(body, idempotencyKey);
1793
+ if (replayed) {
1794
+ note("Idempotency replay: this key was already published; returning the original result. Nothing was posted twice.");
1795
+ printJson({ ...json, replayed: true });
1796
+ return;
1797
+ }
1798
+ printJson(json);
1799
+ }
1800
+ function idsFromFlag(name, raw) {
1801
+ const ids = (raw || "").split(",").map((id) => id.trim()).filter(Boolean);
1802
+ if (ids.length === 0) {
1803
+ note(`--${name} must be a comma list of post ids (from scheduled:list).`);
1804
+ process.exit(1);
1805
+ }
1806
+ return Array.from(new Set(ids));
1807
+ }
1808
+ async function scheduledBulkRetime(argv) {
1809
+ const raw = argv["moves-json"];
1810
+ if (typeof raw !== "string" || raw.length === 0) {
1811
+ note(`--moves-json is required, e.g. '[{"id":"abc","scheduled_for":"2026-09-08T15:00:00Z"}]'`);
1812
+ process.exit(1);
1813
+ return;
1814
+ }
1815
+ let moves;
1816
+ try {
1817
+ moves = JSON.parse(raw);
1818
+ } catch {
1819
+ note(`--moves-json must be valid JSON, e.g. '[{"id":"abc","scheduled_for":"2026-09-08T15:00:00Z"}]'`);
1820
+ process.exit(1);
1821
+ return;
1822
+ }
1823
+ if (!Array.isArray(moves) || moves.length === 0 || moves.some((m) => !m || typeof m !== "object" || typeof m.id !== "string" || typeof m.scheduled_for !== "string")) {
1824
+ note("--moves-json must be a non-empty array of { id, scheduled_for } objects.");
1825
+ process.exit(1);
1826
+ }
1827
+ const body = { moves };
1828
+ if (argv.account) body.account_id = argv.account;
1829
+ const api = new SuperXAPI(getConfig());
1830
+ printJson(await api.bulkRetimeScheduled(body));
1831
+ }
1832
+ async function scheduledBulkAutoRetweet(argv) {
1833
+ const ids = idsFromFlag("ids", argv.ids);
1834
+ const afterHours = numericFlag("auto-retweet", argv["auto-retweet"]);
1835
+ if (typeof afterHours !== "number") {
1836
+ note("--auto-retweet <hours> is required (1-12).");
1837
+ process.exit(1);
1838
+ return;
1839
+ }
1840
+ const removeAfterHours = numericFlag("auto-retweet-remove", argv["auto-retweet-remove"]);
1841
+ const body = {
1842
+ ids,
1843
+ auto_retweet: {
1844
+ after_hours: afterHours,
1845
+ ...typeof removeAfterHours === "number" ? { remove_after_hours: removeAfterHours } : {}
1846
+ }
1847
+ };
1848
+ if (argv.account) body.account_id = argv.account;
1849
+ const api = new SuperXAPI(getConfig());
1850
+ printJson(await api.bulkEnableAutoRetweet(body));
1851
+ }
1852
+ async function scheduledBulkDelete(argv) {
1853
+ const ids = idsFromFlag("ids", argv.ids);
1854
+ const body = { ids };
1855
+ if (argv.account) body.account_id = argv.account;
1856
+ const api = new SuperXAPI(getConfig());
1857
+ printJson(await api.bulkDeleteScheduled(body));
1858
+ }
862
1859
 
863
1860
  // src/commands/media.ts
864
- var import_fs = require("fs");
865
- var import_path = __toESM(require("path"));
1861
+ var import_fs2 = require("fs");
1862
+ var import_path2 = __toESM(require("path"));
866
1863
  function sniffImageType(bytes) {
867
1864
  if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) {
868
1865
  return "image/jpeg";
@@ -888,19 +1885,19 @@ var EXT_TYPES = {
888
1885
  async function mediaUpload(argv) {
889
1886
  let buffer;
890
1887
  try {
891
- buffer = (0, import_fs.readFileSync)(argv.file);
1888
+ buffer = (0, import_fs2.readFileSync)(argv.file);
892
1889
  } catch (err) {
893
1890
  note(`Error: could not read ${argv.file} (${err?.message || err})`);
894
1891
  process.exit(1);
895
1892
  return;
896
1893
  }
897
- const fileType = sniffImageType(buffer) || EXT_TYPES[import_path.default.extname(argv.file).toLowerCase()];
1894
+ const fileType = sniffImageType(buffer) || EXT_TYPES[import_path2.default.extname(argv.file).toLowerCase()];
898
1895
  if (!fileType) {
899
1896
  note("Error: unsupported file type. Supported images: JPG, PNG, WEBP, GIF.");
900
1897
  process.exit(1);
901
1898
  return;
902
1899
  }
903
- const filename = import_path.default.basename(argv.file);
1900
+ const filename = import_path2.default.basename(argv.file);
904
1901
  const api = new SuperXAPI(getConfig());
905
1902
  const created = await api.createMediaUpload({
906
1903
  filename,
@@ -965,7 +1962,7 @@ async function tagsDelete(argv) {
965
1962
  }
966
1963
 
967
1964
  // src/commands/articles.ts
968
- var import_fs2 = require("fs");
1965
+ var import_fs3 = require("fs");
969
1966
  function resolveContent(argv) {
970
1967
  if (argv.content !== void 0 && argv.file !== void 0) {
971
1968
  note("Use either --content or --file, not both.");
@@ -974,7 +1971,7 @@ function resolveContent(argv) {
974
1971
  if (argv.content !== void 0) return argv.content;
975
1972
  if (argv.file !== void 0) {
976
1973
  try {
977
- return (0, import_fs2.readFileSync)(argv.file, "utf8");
1974
+ return (0, import_fs3.readFileSync)(argv.file, "utf8");
978
1975
  } catch (err) {
979
1976
  note(`Could not read ${argv.file}: ${err?.message || err}`);
980
1977
  process.exit(1);
@@ -982,7 +1979,7 @@ function resolveContent(argv) {
982
1979
  }
983
1980
  if (!process.stdin.isTTY) {
984
1981
  try {
985
- const piped = (0, import_fs2.readFileSync)(0, "utf8");
1982
+ const piped = (0, import_fs3.readFileSync)(0, "utf8");
986
1983
  if (piped.length > 0) return piped;
987
1984
  } catch {
988
1985
  }
@@ -1026,7 +2023,7 @@ async function articlesUpdate(argv) {
1026
2023
  if (argv.content !== void 0) body.content_markdown = argv.content;
1027
2024
  if (argv.file !== void 0) {
1028
2025
  try {
1029
- body.content_markdown = (0, import_fs2.readFileSync)(argv.file, "utf8");
2026
+ body.content_markdown = (0, import_fs3.readFileSync)(argv.file, "utf8");
1030
2027
  } catch (err) {
1031
2028
  note(`Could not read ${argv.file}: ${err?.message || err}`);
1032
2029
  process.exit(1);
@@ -1059,10 +2056,19 @@ async function articlesUnschedule(argv) {
1059
2056
  const api = new SuperXAPI(getConfig());
1060
2057
  printJson(await api.unscheduleArticle(argv.id));
1061
2058
  }
2059
+ async function articlesCoverStyles(argv) {
2060
+ const api = new SuperXAPI(getConfig());
2061
+ printJson(await api.listCoverStyles({ account_id: argv.account }));
2062
+ }
1062
2063
  async function articlesCover(argv) {
2064
+ if (argv.style !== void 0 && argv["style-id"] !== void 0) {
2065
+ note("Use either --style or --style-id, not both.");
2066
+ process.exit(1);
2067
+ }
1063
2068
  note("Generating a cover image. This spends AI credits and can take 60-100 seconds...");
1064
2069
  const body = {};
1065
2070
  if (argv.style !== void 0) body.style_text = argv.style;
2071
+ if (argv["style-id"] !== void 0) body.style_id = argv["style-id"];
1066
2072
  if (argv.attach === false) body.attach = false;
1067
2073
  const api = new SuperXAPI(getConfig());
1068
2074
  printJson(await api.generateArticleCover(argv.id, body));
@@ -1073,7 +2079,7 @@ async function contextGet(argv) {
1073
2079
  const api = new SuperXAPI(getConfig());
1074
2080
  printJson(await api.getContext({ account_id: argv.account }));
1075
2081
  }
1076
- function commaList(value) {
2082
+ function commaList2(value) {
1077
2083
  return value.split(",").map((s) => s.trim()).filter(Boolean);
1078
2084
  }
1079
2085
  function stringOrClear(value) {
@@ -1092,7 +2098,7 @@ async function contextSet(argv) {
1092
2098
  body.profile_description = profileDescription;
1093
2099
  }
1094
2100
  if (argv.interests !== void 0) {
1095
- body.interests = commaList(argv.interests);
2101
+ body.interests = commaList2(argv.interests);
1096
2102
  }
1097
2103
  if (argv.rules !== void 0) {
1098
2104
  body.rules = stringOrClear(argv.rules);
@@ -1109,7 +2115,7 @@ async function contextSet(argv) {
1109
2115
  }
1110
2116
  const voice = {};
1111
2117
  if (argv["favorite-creators"] !== void 0) {
1112
- voice.favorite_creators = commaList(argv["favorite-creators"]);
2118
+ voice.favorite_creators = commaList2(argv["favorite-creators"]);
1113
2119
  }
1114
2120
  if (typeof argv["own-posts-as-examples"] === "boolean") {
1115
2121
  voice.use_own_posts_as_examples = argv["own-posts-as-examples"];
@@ -1145,28 +2151,132 @@ async function contextProductsSet(argv) {
1145
2151
  note("Provide --id (from context:products) to edit, or --url to add or edit by url.");
1146
2152
  process.exit(1);
1147
2153
  }
1148
- if (argv.id && argv.url) {
1149
- note("Use either --id or --url, not both.");
2154
+ if (argv.id && argv.url) {
2155
+ note("Use either --id or --url, not both.");
2156
+ process.exit(1);
2157
+ }
2158
+ const body = {};
2159
+ if (argv.name !== void 0) body.name = stringOrClear(argv.name);
2160
+ if (argv.description !== void 0) body.description = stringOrClear(argv.description);
2161
+ if (argv.positioning !== void 0) body.positioning = stringOrClear(argv.positioning);
2162
+ if (argv.features !== void 0) body.features = stringOrClear(argv.features);
2163
+ if (argv.updates !== void 0) body.updates = stringOrClear(argv.updates);
2164
+ if (argv.account) body.account_id = argv.account;
2165
+ const api = new SuperXAPI(getConfig());
2166
+ printJson(await api.updateContextProduct(argv.id || argv.url, body));
2167
+ }
2168
+ async function contextProductsDelete(argv) {
2169
+ const id = String(argv.id || "").trim();
2170
+ if (!id) {
2171
+ note("Provide the product id. Run: superx context:products:delete --help");
2172
+ process.exit(1);
2173
+ }
2174
+ const api = new SuperXAPI(getConfig());
2175
+ printJson(await api.deleteContextProduct(id, { account_id: argv.account }));
2176
+ }
2177
+ async function contextProductsReplace(argv) {
2178
+ let products;
2179
+ try {
2180
+ products = JSON.parse(argv.json);
2181
+ } catch (err) {
2182
+ note(`--json is not valid JSON: ${err?.message || err}`);
2183
+ process.exit(1);
2184
+ }
2185
+ if (!Array.isArray(products)) {
2186
+ note(`--json must be a JSON array, e.g. '[{"url":"https://superx.so","name":"SuperX"}]' ('[]' removes every product).`);
2187
+ process.exit(1);
2188
+ }
2189
+ const body = { products };
2190
+ if (argv.account) body.account_id = argv.account;
2191
+ const api = new SuperXAPI(getConfig());
2192
+ printJson(await api.setContextProducts(body));
2193
+ }
2194
+ async function contextRegenerateStyleGuide(argv) {
2195
+ const body = {};
2196
+ if (argv.account) body.account_id = argv.account;
2197
+ const api = new SuperXAPI(getConfig());
2198
+ printJson(await api.regenerateStyleGuide(body));
2199
+ }
2200
+ async function contextScrapeProduct(argv) {
2201
+ const id = String(argv.id || "").trim();
2202
+ if (!id) {
2203
+ note("Provide the product id (from context:products). Run: superx context:scrape-product --help");
2204
+ process.exit(1);
2205
+ }
2206
+ const body = {};
2207
+ if (argv.account) body.account_id = argv.account;
2208
+ const api = new SuperXAPI(getConfig());
2209
+ printJson(await api.scrapeContextProduct(id, body));
2210
+ }
2211
+
2212
+ // src/commands/dm.ts
2213
+ var import_fs4 = __toESM(require("fs"));
2214
+ function readRecipients(source) {
2215
+ let raw;
2216
+ if (source === "-") {
2217
+ try {
2218
+ raw = import_fs4.default.readFileSync(0, "utf8");
2219
+ } catch (err) {
2220
+ note(`Could not read recipients from stdin: ${err?.message || err}`);
2221
+ process.exit(1);
2222
+ }
2223
+ } else {
2224
+ try {
2225
+ raw = import_fs4.default.readFileSync(source, "utf8");
2226
+ } catch (err) {
2227
+ note(`Could not read ${source}: ${err?.message || err}`);
2228
+ process.exit(1);
2229
+ }
2230
+ }
2231
+ let parsed;
2232
+ try {
2233
+ parsed = JSON.parse(raw);
2234
+ } catch (err) {
2235
+ note(`--recipients must be JSON: ${err?.message || err}`);
2236
+ process.exit(1);
2237
+ }
2238
+ const list = Array.isArray(parsed) ? parsed : parsed?.recipients;
2239
+ if (!Array.isArray(list) || list.length === 0) {
2240
+ note("--recipients must be a non-empty JSON array of { x_user_id, handle?, name?, message? }.");
1150
2241
  process.exit(1);
1151
2242
  }
1152
- const body = {};
1153
- if (argv.name !== void 0) body.name = stringOrClear(argv.name);
1154
- if (argv.description !== void 0) body.description = stringOrClear(argv.description);
1155
- if (argv.positioning !== void 0) body.positioning = stringOrClear(argv.positioning);
1156
- if (argv.features !== void 0) body.features = stringOrClear(argv.features);
1157
- if (argv.updates !== void 0) body.updates = stringOrClear(argv.updates);
2243
+ return list;
2244
+ }
2245
+ async function dmCampaign(argv) {
2246
+ const api = new SuperXAPI(getConfig());
2247
+ const recipients = readRecipients(argv.recipients);
2248
+ const body = { recipients };
2249
+ if (argv.message !== void 0) body.message = argv.message;
2250
+ if (argv.spread === true) body.spread = true;
1158
2251
  if (argv.account) body.account_id = argv.account;
2252
+ const { json, replayed } = await api.queueDmCampaign(body, argv["idempotency-key"]);
2253
+ if (replayed) note("Replayed a previous response for this Idempotency-Key. Nothing new was queued.");
2254
+ printJson(json);
2255
+ note("Queued only. The SuperX app sends these within your DM limits; cancel the unsent ones with dm:cancel.");
2256
+ }
2257
+ async function dmCampaignStatus(argv) {
1159
2258
  const api = new SuperXAPI(getConfig());
1160
- printJson(await api.updateContextProduct(argv.id || argv.url, body));
2259
+ printJson(await api.getDmCampaign(argv.id, { account_id: argv.account }));
1161
2260
  }
1162
- async function contextProductsDelete(argv) {
1163
- const id = String(argv.id || "").trim();
1164
- if (!id) {
1165
- note("Provide the product id. Run: superx context:products:delete --help");
1166
- process.exit(1);
1167
- }
2261
+ async function dmCancel(argv) {
1168
2262
  const api = new SuperXAPI(getConfig());
1169
- printJson(await api.deleteContextProduct(id, { account_id: argv.account }));
2263
+ printJson(await api.cancelDmCampaign(argv.id, { account_id: argv.account }));
2264
+ }
2265
+ async function dmQueue(argv) {
2266
+ const api = new SuperXAPI(getConfig());
2267
+ printJson(
2268
+ await api.listDmQueue({
2269
+ limit: argv.limit,
2270
+ offset: argv.offset,
2271
+ status: argv.status,
2272
+ campaign_id: argv.campaign,
2273
+ account_id: argv.account
2274
+ })
2275
+ );
2276
+ }
2277
+ async function dmLimits(argv) {
2278
+ const api = new SuperXAPI(getConfig());
2279
+ printJson(await api.getDmLimits({ account_id: argv.account }));
1170
2280
  }
1171
2281
 
1172
2282
  // src/commands/queue.ts
@@ -1246,6 +2356,20 @@ var advancedSettingsOptions = (y) => y.option("auto-retweet", {
1246
2356
  type: "string"
1247
2357
  }).option("auto-plug-threshold", {
1248
2358
  describe: "Likes threshold for --auto-plug: the reply posts once the post hits this many likes"
2359
+ }).option("auto-dm-message", {
2360
+ describe: "Auto DM text (1-1000) sent to people who engage with the post; --no-auto-dm turns it off",
2361
+ type: "string"
2362
+ }).option("auto-dm-triggers", {
2363
+ describe: "Who gets the auto DM: comma list of reply,repost (retweet = repost). Default reply",
2364
+ type: "string"
2365
+ }).option("auto-dm-max", {
2366
+ describe: "Most people to auto DM for this post (1-100, default 100)"
2367
+ }).option("auto-dm-batch", {
2368
+ describe: "Send the auto DMs in one batch instead of as engagement arrives",
2369
+ type: "boolean"
2370
+ }).option("auto-dm", {
2371
+ describe: "Only the negated form is used: --no-auto-dm turns Auto DM off for this post",
2372
+ type: "boolean"
1249
2373
  }).option("super-followers", {
1250
2374
  describe: "Post to Super Followers only (--no-super-followers turns it off)",
1251
2375
  type: "boolean"
@@ -1281,6 +2405,116 @@ var advancedSettingsOptions = (y) => y.option("auto-retweet", {
1281
2405
  "Account analytics: totals, daily series, follower change (default: last 30 days)",
1282
2406
  (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"),
1283
2407
  run(postsAnalytics)
2408
+ ).command(
2409
+ "posts:draft",
2410
+ "Write post drafts in your voice from a brief (nothing is scheduled; costs AI credits)",
2411
+ (y) => accountOption(y).option("brief", {
2412
+ describe: "What the post should say: the data, angle, or notes to write from (required, max 2000 chars)",
2413
+ type: "string"
2414
+ }).option("count", {
2415
+ describe: "How many drafts to write, 1 to 3 (default 1). Each one costs credits",
2416
+ type: "number"
2417
+ }).option("voice", {
2418
+ describe: "Whose voice to write in; mine is the voice of the account passed in --account, or your main account if --account is omitted",
2419
+ type: "string",
2420
+ choices: ["mine", "creator", "hybrid"]
2421
+ }).option("creator", {
2422
+ describe: "X handle to borrow style from (needed for --voice creator or hybrid)",
2423
+ type: "string"
2424
+ }).option("mirror", {
2425
+ describe: "Text of a proven post whose SHAPE to copy (50-1500 chars); pick one with room for your data, a two-line aphorism squeezes the facts out; omit to have one picked",
2426
+ type: "string"
2427
+ }).option("collection", {
2428
+ describe: "Format collection id to bias the picked shape toward; used only without --mirror",
2429
+ type: "string"
2430
+ }).option("instructions", {
2431
+ describe: "Extra style instructions for this batch (max 500 chars)",
2432
+ type: "string"
2433
+ }).example('$0 posts:draft --brief "We cut churn from 6.2% to 3.8% by replacing the onboarding video with a checklist"', "One draft in your voice").example('$0 posts:draft --brief "..." --count 3 --mirror "$(cat proven-post.txt)"', "Three drafts copying a proven post shape").example('$0 posts:draft --brief "..." --voice hybrid --creator @naval', "Your substance, a creator's flavor"),
2434
+ run(postsDraft)
2435
+ ).command(
2436
+ "posts:remix",
2437
+ "Rewrite a post in your voice, near or far from the original (nothing is posted; costs AI credits)",
2438
+ (y) => accountOption(y).option("text", {
2439
+ describe: "The post to remix (required, max 4000 chars)",
2440
+ type: "string",
2441
+ demandOption: true
2442
+ }).option("closeness", {
2443
+ describe: "0 keeps only the idea, 100 stays very close to the original wording (required)",
2444
+ type: "number",
2445
+ demandOption: true
2446
+ }).option("instructions", {
2447
+ describe: "Extra direction for this remix (max 500 chars)",
2448
+ type: "string"
2449
+ }).example('$0 posts:remix --text "$(cat post.txt)" --closeness 70', "A close rewrite in your voice").example('$0 posts:remix --text "..." --closeness 20 --instructions "make it a question"', "A loose reinterpretation").epilogue(
2450
+ "Returns TEXT ONLY. Nothing is posted or scheduled: save the result with posts:draft or scheduled:create once you are happy with it."
2451
+ ),
2452
+ run(postsRemix)
2453
+ ).command(
2454
+ "tools:inline-edit",
2455
+ "Edit one selected piece of a post, keeping the surrounding style (costs AI credits)",
2456
+ (y) => accountOption(y).option("text", {
2457
+ describe: "The selected piece to edit (required, max 4000 chars)",
2458
+ type: "string",
2459
+ demandOption: true
2460
+ }).option("full", {
2461
+ describe: "The whole post the selection sits in, so the edit matches its style",
2462
+ type: "string"
2463
+ }).option("instruction", { describe: "Free-text direction, e.g. 'make this one line'", type: "string" }).option("type", {
2464
+ describe: "A preset edit instead of, or alongside, --instruction",
2465
+ type: "string",
2466
+ choices: [
2467
+ "grammar",
2468
+ "translate",
2469
+ "hook",
2470
+ "details",
2471
+ "concise",
2472
+ "engaging",
2473
+ "humorous",
2474
+ "creative",
2475
+ "sarcastic",
2476
+ "inspirational"
2477
+ ]
2478
+ }).example('$0 tools:inline-edit --text "the hook line" --full "$(cat post.txt)" --type hook', "Sharpen the hook in place").epilogue("Provide --instruction, --type, or both. Returns TEXT ONLY; nothing is posted."),
2479
+ run(toolsInlineEdit)
2480
+ ).command(
2481
+ "tools:rephrase",
2482
+ "Rewrite a post one preset way (costs AI credits)",
2483
+ (y) => accountOption(y).option("type", {
2484
+ describe: "Which rewrite to apply. The style presets use your voice; the mechanical ones do not",
2485
+ type: "string",
2486
+ demandOption: true,
2487
+ choices: [
2488
+ "improve",
2489
+ "grammar",
2490
+ "translate",
2491
+ "hook",
2492
+ "details",
2493
+ "clarity",
2494
+ "engaging",
2495
+ "humorous",
2496
+ "positive",
2497
+ "creative",
2498
+ "sarcastic",
2499
+ "inspirational",
2500
+ "concise"
2501
+ ]
2502
+ }).option("text", { describe: "The post to rewrite (required)", type: "string", demandOption: true }).example('$0 tools:rephrase --type concise --text "$(cat post.txt)"', "Tighten a post").epilogue("Returns TEXT ONLY; nothing is posted."),
2503
+ run(toolsRephrase)
2504
+ ).command(
2505
+ "tools:factcheck",
2506
+ "Check a statement against a web search and report true, false or unknown (costs AI credits)",
2507
+ (y) => accountOption(y).option("text", { describe: "The statement to check (required)", type: "string", demandOption: true }).example('$0 tools:factcheck --text "X has 600M daily active users"', "Check a claim before posting it").epilogue(
2508
+ "The verdict is a model's reading of a couple of search results, not a guarantee. Read the sources it returns before acting on it."
2509
+ ),
2510
+ run(toolsFactcheck)
2511
+ ).command(
2512
+ "tools:predict",
2513
+ "Score two versions of a post against what the timeline rewards (costs AI credits)",
2514
+ (y) => accountOption(y).option("a", { describe: "The first version (required)", type: "string", demandOption: true }).option("b", { describe: "The second version (required)", type: "string", demandOption: true }).example('$0 tools:predict --a "$(cat v1.txt)" --b "$(cat v2.txt)"', "Compare two drafts").epilogue(
2515
+ "The scores are a model's opinion, useful for comparing two drafts against each other, not a prediction of real reach."
2516
+ ),
2517
+ run(toolsPredict)
1284
2518
  ).command(
1285
2519
  "replies:list",
1286
2520
  "List replies the account has sent (newest first)",
@@ -1304,6 +2538,31 @@ var advancedSettingsOptions = (y) => y.option("auto-retweet", {
1304
2538
  choices: ["relevant", "recent", "likes", "reposts", "impressions", "outlier"]
1305
2539
  }).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"),
1306
2540
  run(inspirationSearch)
2541
+ ).command(
2542
+ "inspiration:media [query]",
2543
+ "Search the cross-platform media index behind the app's Inspiration > Media tab",
2544
+ (y) => y.positional("query", {
2545
+ describe: "What to search for. Omit to browse the newest media",
2546
+ type: "string"
2547
+ }).option("platforms", {
2548
+ describe: "Comma-separated platforms: x, instagram, youtube, threads, reddit, linkedin",
2549
+ type: "string"
2550
+ }).option("time-filter", {
2551
+ describe: "How recent the media must be",
2552
+ type: "string",
2553
+ choices: ["all", "24h", "7d", "30d"]
2554
+ }).option("media-type", {
2555
+ describe: "Media kind",
2556
+ type: "string",
2557
+ choices: ["all", "video", "image"]
2558
+ }).option("content-type", {
2559
+ describe: "Free-text content-type label as stored in the index; an unknown label returns nothing and still costs a search",
2560
+ type: "string"
2561
+ }).option("limit", {
2562
+ describe: "Items to return (1-120, default 20). No pagination: 120 is one query's maximum",
2563
+ type: "number"
2564
+ }).example('$0 inspiration:media "founder morning routine" --limit 10', "Ten media posts on that theme").example("$0 inspiration:media --platforms youtube,instagram", "Browse the newest video-platform media"),
2565
+ run(inspirationMedia)
1307
2566
  ).command(
1308
2567
  "contacts:list",
1309
2568
  "List the people who engage with you most",
@@ -1322,6 +2581,34 @@ var advancedSettingsOptions = (y) => y.option("auto-retweet", {
1322
2581
  choices: ["recent", "most_liked"]
1323
2582
  }),
1324
2583
  run(contactsReplies)
2584
+ ).command(
2585
+ "contacts:get <id>",
2586
+ "Show one person: profile, follower counts and the lists they are in (your known contacts only: engagers, contact-list members, signal leads)",
2587
+ (y) => accountOption(y).positional("id", { describe: "Numeric X user id (from contacts:list, lists:members or signals:leads)", type: "string" }).option("refresh", {
2588
+ describe: "Refresh the profile from X when the stored copy is stale (counts against the enrichment limit)",
2589
+ type: "boolean"
2590
+ }),
2591
+ run(contactsGet)
2592
+ ).command(
2593
+ "contacts:notes <id>",
2594
+ "List your private notes about one person (newest first)",
2595
+ (y) => accountOption(y).positional("id", { describe: "Numeric X user id", type: "string" }),
2596
+ run(contactsNotes)
2597
+ ).command(
2598
+ "contacts:notes:add <id>",
2599
+ "Write a private note about one person (never posted anywhere; known contacts only: engagers, contact-list members, signal leads)",
2600
+ (y) => accountOption(y).positional("id", { describe: "Numeric X user id", type: "string" }).option("body", { describe: "Note text (1-5000 chars)", type: "string", demandOption: true }).example('$0 contacts:notes:add 44196397 --body "Met at the SaaS meetup, wants a demo"', "Add a note"),
2601
+ run(contactsNotesAdd)
2602
+ ).command(
2603
+ "contacts:notes:update <id> <noteId>",
2604
+ "Rewrite one note (the new body fully replaces the old one)",
2605
+ (y) => accountOption(y).positional("id", { describe: "Numeric X user id", type: "string" }).positional("noteId", { describe: "Note id (from contacts:notes)", type: "string" }).option("body", { describe: "Replacement note text (1-5000 chars)", type: "string", demandOption: true }),
2606
+ run(contactsNotesUpdate)
2607
+ ).command(
2608
+ "contacts:notes:delete <id> <noteId>",
2609
+ "Delete one note (note id from contacts:notes)",
2610
+ (y) => accountOption(y).positional("id", { describe: "Numeric X user id", type: "string" }).positional("noteId", { describe: "Note id (from contacts:notes)", type: "string" }),
2611
+ run(contactsNotesDelete)
1325
2612
  ).command(
1326
2613
  "lists:list",
1327
2614
  "List your contact lists (system lists are read-only)",
@@ -1342,11 +2629,229 @@ var advancedSettingsOptions = (y) => y.option("auto-retweet", {
1342
2629
  "Remove a member from a contact list (member id from lists:members)",
1343
2630
  (y) => y.positional("id", { describe: "List id (from lists:list)", type: "string" }).positional("memberId", { describe: "Member id (from lists:members)", type: "string" }),
1344
2631
  run(listsRemoveMember)
2632
+ ).command(
2633
+ "lists:create",
2634
+ "Create a contact list (names are not unique)",
2635
+ (y) => accountOption(y).option("name", { describe: "List name (1-120 chars)", type: "string", demandOption: true }).example('$0 lists:create --name "Founder prospects"', "Create a list"),
2636
+ run(listsCreate)
2637
+ ).command(
2638
+ "lists:rename <id>",
2639
+ "Rename a contact list you created (system lists are read-only)",
2640
+ (y) => accountOption(y).positional("id", { describe: "List id (from lists:list)", type: "string" }).option("name", { describe: "New name (1-120 chars)", type: "string", demandOption: true }),
2641
+ run(listsRename)
2642
+ ).command(
2643
+ "lists:delete <id>",
2644
+ "Delete a contact list you created and its membership (the people stay)",
2645
+ (y) => accountOption(y).positional("id", { describe: "List id (from lists:list)", type: "string" }),
2646
+ run(listsDelete)
2647
+ ).command(
2648
+ "lists:add-members <id>",
2649
+ "Add up to 500 people at once by X user id (ids SuperX already knows)",
2650
+ (y) => accountOption(y).positional("id", { describe: "List id (from lists:list)", type: "string" }).option("x-user-ids", {
2651
+ describe: "Comma list of numeric X user ids (max 500); ids SuperX has never seen come back in not_found",
2652
+ type: "string",
2653
+ demandOption: true
2654
+ }).example("$0 lists:add-members abc123 --x-user-ids 44196397,1234567890", "Add two people by id"),
2655
+ run(listsAddMembers)
2656
+ ).command(
2657
+ "lists:remove-members <id>",
2658
+ "Remove up to 500 members at once by member id (from lists:members)",
2659
+ (y) => accountOption(y).positional("id", { describe: "List id (from lists:list)", type: "string" }).option("member-ids", {
2660
+ describe: "Comma list of member ids (max 500, from lists:members)",
2661
+ type: "string",
2662
+ demandOption: true
2663
+ }),
2664
+ run(listsRemoveMembers)
2665
+ ).command(
2666
+ "datasets:list",
2667
+ "List the audience collections Ask SuperX built in the app (kept for 30 days)",
2668
+ (y) => paginationOptions(y),
2669
+ run(datasetsList)
2670
+ ).command(
2671
+ "datasets:get <id>",
2672
+ "Read one dataset: status, counts, coverage (poll this while it collects)",
2673
+ (y) => y.positional("id", { describe: "Dataset id (from datasets:list)", type: "string" }),
2674
+ run(datasetsGet)
2675
+ ).command(
2676
+ "datasets:rows <id>",
2677
+ "Read a page of a ready dataset's rows, exactly as collected",
2678
+ (y) => paginationOptions(y).positional("id", {
2679
+ describe: "Dataset id (from datasets:list)",
2680
+ type: "string"
2681
+ }),
2682
+ run(datasetsRows)
2683
+ ).command(
2684
+ "datasets:export <id>",
2685
+ "Download a ready dataset as CSV (XLSX stays in the SuperX app)",
2686
+ (y) => y.positional("id", { describe: "Dataset id (from datasets:list)", type: "string" }).option("out", {
2687
+ describe: "File to write; defaults to the server's filename in the current directory. Use - to stream the CSV to stdout",
2688
+ type: "string",
2689
+ // Without requiresArg, yargs drops a lone `-` and strict mode
2690
+ // rejects it as an unknown argument, so `--out -` would fail.
2691
+ requiresArg: true
2692
+ }).example("$0 datasets:export abc123", "Write superx-dataset-<title>-<date>.csv here").example("$0 datasets:export abc123 --out - | head", "Stream the CSV to stdout"),
2693
+ run(datasetsExport)
2694
+ ).command(
2695
+ "datasets:add-to-list <id>",
2696
+ "Copy the people in a ready dataset into a contact list you created",
2697
+ (y) => accountOption(y).positional("id", { describe: "Dataset id (from datasets:list)", type: "string" }).option("list-id", {
2698
+ describe: "Contact list id (from lists:list) that receives the people",
2699
+ type: "string",
2700
+ demandOption: true
2701
+ }).example("$0 datasets:add-to-list abc123 --list-id def456", "Add the dataset's people to a list"),
2702
+ run(datasetsAddToList)
2703
+ ).command(
2704
+ "datasets:collect",
2705
+ "Collect an audience (or your own posts) into a new dataset",
2706
+ (y) => accountOption(y).option("source", {
2707
+ describe: "Who to collect",
2708
+ type: "string",
2709
+ choices: [
2710
+ "repliers",
2711
+ "quoters",
2712
+ "reposters",
2713
+ "list_members",
2714
+ "my_replies",
2715
+ "my_posts"
2716
+ ],
2717
+ demandOption: true
2718
+ }).option("target", {
2719
+ describe: "Post URL or id (repliers, quoters, reposters), or X list URL or id (list_members). Not used for my_posts / my_replies",
2720
+ type: "string"
2721
+ }).option("title", { describe: "Title for the dataset", type: "string" }).option("max-rows", { describe: "Rows to collect at most (1-1000, default 500)", type: "number" }).option("keywords", {
2722
+ describe: "Comma list: keep only rows whose reply, quote or post text contains one of these",
2723
+ type: "string"
2724
+ }).option("bio-keywords", {
2725
+ describe: "Comma list: keep only people whose X bio contains one of these",
2726
+ type: "string"
2727
+ }).option("min-followers", { describe: "Keep only people with at least this many followers", type: "number" }).option("require-website", { describe: "Keep only people with a website in their profile", type: "boolean" }).option("require-can-dm", { describe: "Keep only people whose DMs look open", type: "boolean" }).option("since-days", { describe: "Own posts only: keep posts from the last N days", type: "number" }).option("sort", {
2728
+ describe: "Own posts only: which posts to keep when max-rows cuts the list",
2729
+ type: "string",
2730
+ choices: ["recent", "likes", "impressions"]
2731
+ }).option("wait", {
2732
+ describe: "Poll until a background collection is ready (up to 15 minutes)",
2733
+ type: "boolean"
2734
+ }).example(
2735
+ "$0 datasets:collect --source repliers --target https://x.com/me/status/123 --wait",
2736
+ "Collect everyone who replied and wait for it"
2737
+ ).epilogue(
2738
+ "Costs one of 10 collections a day, shared with the collections Ask SuperX runs in the app. A big collection answers with status collecting and keeps running in the background: poll it with datasets:get, or pass --wait."
2739
+ ),
2740
+ run(datasetsCollect)
2741
+ ).command(
2742
+ "datasets:research",
2743
+ "Research people into outreach briefs saved as a dataset",
2744
+ (y) => accountOption(y).option("handles", {
2745
+ describe: "Comma list of X handles to research (with or without the @). Max 25",
2746
+ type: "string"
2747
+ }).option("list", { describe: "Research the members of this contact list id", type: "string" }).option("agent", { describe: "Research this signal agent's leads (numeric id)", type: "number" }).option("dataset", { describe: "Research the people in this dataset id", type: "string" }).option("max", { describe: "Profiles to research, first N from the source (1-25, default 10)", type: "number" }).option("focus", {
2748
+ describe: "Optional steer, e.g. 'founders who might need audience-growth tooling'",
2749
+ type: "string"
2750
+ }).option("title", { describe: "Title for the briefs dataset", type: "string" }).option("wait", {
2751
+ describe: "Poll until a background research run is ready (up to 15 minutes)",
2752
+ type: "boolean"
2753
+ }).example(
2754
+ "$0 datasets:research --handles levelsio,naval --focus 'audience-growth tooling'",
2755
+ "Research two handles into briefs"
2756
+ ).epilogue(
2757
+ "Give exactly one source: --handles, --list, --agent or --dataset. Costs 1 AI credit per profile actually researched (the rest are returned) plus one of the plan's daily research runs. More than 5 profiles run in the background: the result says status collecting, so poll with datasets:get or pass --wait. Live-data actions also draw on a platform-wide fair-use ceiling shared by every account."
2758
+ ),
2759
+ run(datasetsResearch)
2760
+ ).command(
2761
+ "datasets:outreach-drafts <id>",
2762
+ "Draft one personalized message per person in a research dataset (text only, nothing is sent)",
2763
+ (y) => accountOption(y).positional("id", { describe: "Research dataset id (from datasets:research)", type: "string" }).option("format", {
2764
+ describe: "The template or example message every draft should follow (10-1000 chars)",
2765
+ type: "string",
2766
+ demandOption: true
2767
+ }).option("instructions", {
2768
+ describe: "Optional extra steer: tone, what to emphasize, what to avoid",
2769
+ type: "string"
2770
+ }).example(
2771
+ '$0 datasets:outreach-drafts abc123 --format "hey [first]! <personalization>. would love to trade notes"',
2772
+ "Draft messages onto the dataset"
2773
+ ).epilogue(
2774
+ "The drafts are TEXT: they are stored on the dataset (read them with datasets:rows) and a person sends them from the SuperX app. Nothing here sends a DM. Re-running overwrites every draft. [name], [first] and [handle] tokens are kept for per-recipient fill-in at send time."
2775
+ ),
2776
+ run(datasetsOutreachDrafts)
2777
+ ).command(
2778
+ "datasets:refine <id>",
2779
+ "Filter a dataset by what each person wrote, into a new dataset",
2780
+ (y) => accountOption(y).positional("id", { describe: "Source dataset id (from datasets:list)", type: "string" }).option("criterion", {
2781
+ describe: "What the rows to match look like, judged on each row's own text",
2782
+ type: "string",
2783
+ demandOption: true
2784
+ }).option("keep", {
2785
+ describe: "Keep the rows that MATCH (default). --no-keep keeps the rows that do not",
2786
+ type: "boolean"
2787
+ }).option("sort", {
2788
+ describe: "Sort the kept rows descending before the limit",
2789
+ type: "string",
2790
+ choices: ["followers", "likes", "none"]
2791
+ }).option("limit", { describe: "Keep at most this many rows after filtering and sorting", type: "number" }).option("title", { describe: "Title for the new dataset", type: "string" }).option("wait", {
2792
+ describe: "Poll until a background refinement is ready (up to 15 minutes)",
2793
+ type: "boolean"
2794
+ }).example(
2795
+ "$0 datasets:refine abc123 --criterion 'supportive or neutral, not hostile' --sort followers",
2796
+ "Keep the friendly repliers, best-followed first"
2797
+ ).epilogue(
2798
+ "The source dataset is untouched. Only datasets whose rows carry text (repliers, quoters) can be refined this way. Rows the classifier cannot judge are KEPT and counted as unclear. A refinement creates a dataset, so it counts against the same 10 collections a day, and it costs AI credits."
2799
+ ),
2800
+ run(datasetsRefine)
2801
+ ).command(
2802
+ "x:post <id|url>",
2803
+ "Look up one public X post live (URL or numeric id)",
2804
+ (y) => y.positional("id", { describe: "Post URL or bare numeric post id", type: "string" }).option("quotes", {
2805
+ describe: "Also fetch a page of the posts quoting it (costs a second unit)",
2806
+ type: "boolean"
2807
+ }).example("$0 x:post https://x.com/levelsio/status/1938765432109876543", "Read that post"),
2808
+ run(xPost)
2809
+ ).command(
2810
+ "x:replies <id|url>",
2811
+ "Top replies by likes to a public X post, live (a sample, not every reply)",
2812
+ (y) => y.positional("id", { describe: "Post URL or bare numeric post id", type: "string" }).option("limit", { describe: "Replies to return (1-20, default 10)", type: "number" }).example("$0 x:replies 1938765432109876543 --limit 20", "The twenty best-liked replies"),
2813
+ run(xReplies)
2814
+ ).command(
2815
+ "x:user <handle>",
2816
+ "Look up one public X profile live (@ optional)",
2817
+ (y) => y.positional("handle", { describe: "The account's @handle", type: "string" }).example("$0 x:user @levelsio", "Read that profile"),
2818
+ run(xUser)
2819
+ ).command(
2820
+ "x:user-posts <handle>",
2821
+ "One live page of an account's latest posts, newest first",
2822
+ (y) => y.positional("handle", { describe: "The account's @handle", type: "string" }).option("limit", { describe: "Posts to return (1-20, default 10)", type: "number" }).option("reposts", {
2823
+ describe: "Include reposts (default). Use --no-reposts for own posts only",
2824
+ type: "boolean",
2825
+ default: true
2826
+ }).example("$0 x:user-posts levelsio --no-reposts", "Their own recent posts, no reposts"),
2827
+ run(xUserPosts)
1345
2828
  ).command(
1346
2829
  "signals:agents",
1347
2830
  "List your signal agents (automated lead finders) with their watched signals",
1348
2831
  (y) => accountOption(y),
1349
2832
  run(signalsAgents)
2833
+ ).command(
2834
+ "signals:search",
2835
+ "Search X now for people matching an audience description (saves nothing)",
2836
+ (y) => accountOption(y).option("keywords", {
2837
+ describe: "Plain-language phrases these people would post, comma-separated for alternatives. No search operators",
2838
+ type: "string",
2839
+ demandOption: true
2840
+ }).option("icp", {
2841
+ describe: "Who counts as a good lead, in 1-2 sentences: role, domain, and the intent that qualifies them",
2842
+ type: "string",
2843
+ demandOption: true
2844
+ }).option("precision", {
2845
+ describe: "high = only confident matches; discovery (default) = broader adjacent matches",
2846
+ type: "string",
2847
+ choices: ["high", "discovery"]
2848
+ }).option("max", { describe: "Leads to return at most (1-30, default 10)", type: "number" }).example(
2849
+ "$0 signals:search --keywords 'losing customers to churn' --icp 'B2B SaaS founders worried about retention'",
2850
+ "Find people posting about churn right now"
2851
+ ).epilogue(
2852
+ "This CREATES NOTHING: no signal agent, no saved leads. Use signals:create-agent for an audience that keeps filling up. Reading X live costs AI credits (at least 1) plus one of the plan's daily lead searches, and draws on a platform-wide fair-use ceiling shared by every account. Takes up to a minute."
2853
+ ),
2854
+ run(signalsSearch)
1350
2855
  ).command(
1351
2856
  "signals:leads",
1352
2857
  "List the leads your signal agents have found (newest first)",
@@ -1373,6 +2878,10 @@ var advancedSettingsOptions = (y) => y.option("auto-retweet", {
1373
2878
  describe: "Plain-language search description to watch (repeat the flag, 1-5); omit to auto-suggest from --icp",
1374
2879
  type: "string",
1375
2880
  array: true
2881
+ }).option("signal", {
2882
+ describe: 'Non-keyword signal as "type:target" (repeat the flag). Types: keyword, profile, follower, list. Combined with --keyword, at most 5',
2883
+ type: "string",
2884
+ array: true
1376
2885
  }).option("idempotency-key", {
1377
2886
  describe: "Idempotency-Key header (max 64 chars); retries with the same key return the original result",
1378
2887
  type: "string"
@@ -1382,8 +2891,77 @@ var advancedSettingsOptions = (y) => y.option("auto-retweet", {
1382
2891
  ).example(
1383
2892
  '$0 signals:create-agent --name "Agency leads" --icp "Marketing agency owners struggling with reporting"',
1384
2893
  "Create an agent with auto-suggested keywords and an auto-created list"
2894
+ ).example(
2895
+ '$0 signals:create-agent --name "Naval orbit" --icp "..." --signal "profile:@naval" --signal "follower:@naval"',
2896
+ "Create an agent watching an account and its followers"
1385
2897
  ),
1386
2898
  run(signalsCreateAgent)
2899
+ ).command(
2900
+ "signals:update-agent <id>",
2901
+ "Edit a signal agent's name, ICP, precision mode, destination list or status",
2902
+ (y) => y.positional("id", { describe: "Agent id (from signals:agents)", type: "number" }).option("name", { describe: "New agent name (max 80 chars)", type: "string" }).option("icp", { describe: "New ideal-customer description (max 500 chars)", type: "string" }).option("precision", {
2903
+ describe: "high = fewer, stricter matches; discovery = broader net",
2904
+ type: "string",
2905
+ choices: ["high", "discovery"]
2906
+ }).option("list-id", {
2907
+ describe: "Contact list id (from lists:list) that receives the leads",
2908
+ type: "string"
2909
+ }).option("status", {
2910
+ describe: "active = resume finding leads; paused = stop",
2911
+ type: "string",
2912
+ choices: ["active", "paused"]
2913
+ }).example('$0 signals:update-agent 3 --icp "Series A founders hiring their first RevOps lead"', "Retune the scoring").example("$0 signals:update-agent 3 --list-id abc123", "Send new leads to a different list"),
2914
+ run(signalsUpdateAgent)
2915
+ ).command(
2916
+ "signals:add-signal <id>",
2917
+ "Add one thing for an agent to watch (a search, an account, its followers, or an X list)",
2918
+ (y) => accountOption(y).positional("id", { describe: "Agent id (from signals:agents)", type: "number" }).option("type", {
2919
+ describe: "What to watch",
2920
+ type: "string",
2921
+ choices: ["keyword_watch", "profile_watch", "follower_watch", "list_watch"],
2922
+ demandOption: true
2923
+ }).option("query", { describe: "For keyword_watch: the search description (max 180 chars)", type: "string" }).option("handle", { describe: "For profile_watch / follower_watch: an X username", type: "string" }).option("list", { describe: "For list_watch: a public X list id or x.com/i/lists link", type: "string" }).example('$0 signals:add-signal 3 --type keyword_watch --query "just raised a seed round"', "Watch a search").example("$0 signals:add-signal 3 --type follower_watch --handle naval", "Watch who an account follows"),
2924
+ run(signalsAddSignal)
2925
+ ).command(
2926
+ "signals:remove-signal <id> <signalId>",
2927
+ "Remove one signal from an agent (leads it already found stay)",
2928
+ (y) => y.positional("id", { describe: "Agent id (from signals:agents)", type: "number" }).positional("signalId", { describe: "Signal id (from the agent's signals in signals:agents)", type: "number" }),
2929
+ run(signalsRemoveSignal)
2930
+ ).command(
2931
+ "signals:feedback <leadId>",
2932
+ "Record your verdict on one lead: --fit, --not-fit or --clear",
2933
+ (y) => accountOption(y).positional("leadId", { describe: "Numeric lead id (from signals:leads)", type: "number" }).option("fit", { describe: "Mark the lead a good match", type: "boolean" }).option("not-fit", { describe: "Mark the lead a bad match", type: "boolean" }).option("clear", { describe: "Remove any verdict on the lead", type: "boolean" }).example("$0 signals:feedback 4821 --fit", "Teach the scorer this lead was right").epilogue("The verdict trains the scorer, so record it on leads you actually reviewed."),
2934
+ run(signalsFeedback)
2935
+ ).command(
2936
+ "signals:suggest-keywords",
2937
+ "Turn an audience description into 2 or 3 keyword-watch ideas (free, creates nothing)",
2938
+ (y) => accountOption(y).option("icp", {
2939
+ describe: "Who the ideal customer is, in plain language (3 to 500 chars)",
2940
+ type: "string",
2941
+ demandOption: true
2942
+ }).example(
2943
+ `$0 signals:suggest-keywords --icp "B2B SaaS founders worried about churn"`,
2944
+ "Ideas to watch for that audience"
2945
+ ).epilogue(
2946
+ "Saves nothing and costs no AI credits. Pass a suggestion you like to signals:create-agent --keyword, or signals:add-signal --type keyword_watch."
2947
+ ),
2948
+ run(signalsSuggestKeywords)
2949
+ ).command(
2950
+ "signals:expand-icp",
2951
+ "Build the scoring rubric a signal agent qualifies people with, from a description or a website (free)",
2952
+ (y) => accountOption(y).option("text", {
2953
+ describe: "Who the ideal customer is, in plain language (3 to 500 chars)",
2954
+ type: "string"
2955
+ }).option("url", {
2956
+ describe: "A product or company website to read instead; also returns a description and keyword ideas",
2957
+ type: "string"
2958
+ }).example(
2959
+ `$0 signals:expand-icp --text "Indie founders building SaaS in public"`,
2960
+ "Rubric from a description"
2961
+ ).example("$0 signals:expand-icp --url superx.so", "Rubric, description and keyword ideas from a site").epilogue(
2962
+ "Provide --text or --url, not both. Saves nothing and costs no AI credits; --url reads a page on the account's allowance of 20 page reads a day, shared with the SuperX app, and takes up to a minute. The rubric is a reading of the description, not a field you can store: agents are created with --icp, so use it to sharpen that text first. With --url, the icp_description it returns is what you pass to signals:create-agent --icp."
2963
+ ),
2964
+ run(signalsExpandIcp)
1387
2965
  ).command(
1388
2966
  "signals:pause-agent <id>",
1389
2967
  "Pause a signal agent (it stops finding leads until resumed)",
@@ -1399,6 +2977,49 @@ var advancedSettingsOptions = (y) => y.option("auto-retweet", {
1399
2977
  "Delete a signal agent (its saved leads and contact list stay untouched)",
1400
2978
  (y) => y.positional("id", { describe: "Agent id (from signals:agents)", type: "number" }),
1401
2979
  run(signalsDeleteAgent)
2980
+ ).command(
2981
+ "workers:list",
2982
+ "List your Workers (the agents that write posts for you on a schedule)",
2983
+ (y) => accountOption(y).example("$0 workers:list", "Your Workers, their schedules and next run times").epilogue(
2984
+ "Workers are created, edited and run in the SuperX app. This reads them so you can tell which Worker a suggestion came from."
2985
+ ),
2986
+ run(workersList)
2987
+ ).command(
2988
+ "workers:suggestions",
2989
+ "List the posts your Workers have written, newest first",
2990
+ (y) => paginationOptions(accountOption(y)).option("status", {
2991
+ describe: "to_review (default) = waiting on you; drafted / scheduled = already saved; dismissed = cleared; all = everything",
2992
+ type: "string",
2993
+ choices: ["to_review", "drafted", "scheduled", "dismissed", "all"]
2994
+ }).option("worker", { describe: "Narrow to one Worker by its numeric id (from workers:list)", type: "number" }).example("$0 workers:suggestions --limit 10", "Ten newest posts waiting for review").example("$0 workers:suggestions --worker 3 --status all", "Everything Worker 3 has written").epilogue(
2995
+ "Reading suggestions costs nothing and changes nothing. Act on one with workers:draft, workers:schedule or workers:dismiss."
2996
+ ),
2997
+ run(workersSuggestions)
2998
+ ).command(
2999
+ "workers:draft <id>",
3000
+ "Save one Worker suggestion as a draft (nothing is posted)",
3001
+ (y) => accountOption(y).positional("id", { describe: "Suggestion id (from workers:suggestions)", type: "number" }).example("$0 workers:draft 4821", "Move it into Drafts, where scheduled:list --status draft finds it").epilogue(
3002
+ "The draft keeps the Worker's text as written. Edit it with scheduled:update, or rewrite it first with posts:remix and then save your own version."
3003
+ ),
3004
+ run(workersDraft)
3005
+ ).command(
3006
+ "workers:schedule <id>",
3007
+ "Schedule one Worker suggestion to post at a given time",
3008
+ (y) => accountOption(y).positional("id", { describe: "Suggestion id (from workers:suggestions)", type: "number" }).option("at", {
3009
+ describe: "When to post, UTC ISO-8601 (for example 2026-09-15T14:00:00Z)",
3010
+ type: "string",
3011
+ demandOption: true
3012
+ }).example("$0 workers:schedule 4821 --at 2026-09-15T14:00:00Z", "Queue it for Tuesday afternoon").epilogue(
3013
+ "The post does not inherit your Default Post Settings: it carries only what this call passes, which is nothing beyond the time. Add auto retweet, auto plug or auto delete afterwards with scheduled:update, which also retimes it; scheduled:delete cancels it."
3014
+ ),
3015
+ run(workersSchedule)
3016
+ ).command(
3017
+ "workers:dismiss <id>",
3018
+ "Clear one Worker suggestion out of the To review list",
3019
+ (y) => accountOption(y).positional("id", { describe: "Suggestion id (from workers:suggestions)", type: "number" }).example("$0 workers:dismiss 4821", "Skip this one").epilogue(
3020
+ "Dismissing drops it from the default to_review list. It stays readable with --status dismissed, and nothing already drafted or scheduled is affected."
3021
+ ),
3022
+ run(workersDismiss)
1402
3023
  ).command(
1403
3024
  "engage:feeds",
1404
3025
  "List the Engage feeds set up in the app",
@@ -1425,6 +3046,82 @@ var advancedSettingsOptions = (y) => y.option("auto-retweet", {
1425
3046
  type: "string"
1426
3047
  }).example("$0 engage:posts <feed-id> --limit 50", "One big page from a keyword feed").example("$0 engage:posts <feed-id> --mode latest --fresh true", "Newest posts, skipping the cache").example("$0 engage:posts <feed-id> --exclude 1234567890,1234567891", "Next batch, minus the posts you have"),
1427
3048
  run(engagePosts)
3049
+ ).command(
3050
+ "engage:feeds:create",
3051
+ "Save a new Engage feed (keywords, a public X list, or one of your contact lists)",
3052
+ (y) => accountOption(y).option("name", { describe: "Feed name (1-40 chars)", type: "string", demandOption: true }).option("keyword", {
3053
+ describe: "Search term for a keyword feed (repeat the flag, 1-5)",
3054
+ type: "string",
3055
+ array: true
3056
+ }).option("x-list", {
3057
+ describe: "Public X list id or x.com/i/lists link for an X list feed",
3058
+ type: "string"
3059
+ }).option("list-id", { describe: "Contact list id (from lists:list) for a list feed", type: "string" }).example('$0 engage:feeds:create --name "AI builders" --keyword "shipping with LLMs" --keyword "eval harness"', "A keyword feed").example('$0 engage:feeds:create --name "Founders" --x-list https://x.com/i/lists/1234567890', "A public X list feed").epilogue(
3060
+ "Use exactly one source. A new feed does not become the feed the SuperX app has open. Up to 8 feeds per account."
3061
+ ),
3062
+ run(engageFeedsCreate)
3063
+ ).command(
3064
+ "engage:feeds:update <feedId>",
3065
+ "Rename an Engage feed, replace what it watches, or both",
3066
+ (y) => accountOption(y).positional("feedId", { describe: "Feed id (from engage:feeds)", type: "string" }).option("name", { describe: "New feed name (1-40 chars)", type: "string" }).option("keyword", {
3067
+ describe: "Replace the feed's keywords (repeat the flag, 1-5)",
3068
+ type: "string",
3069
+ array: true
3070
+ }).option("x-list", { describe: "Point the feed at this X list id or link", type: "string" }).option("list-id", { describe: "Point the feed at this contact list (from lists:list)", type: "string" }).example('$0 engage:feeds:update <feed-id> --name "AI builders"', "Rename a feed").epilogue("Change one source at a time; a feed may change type and keeps its id."),
3071
+ run(engageFeedsUpdate)
3072
+ ).command(
3073
+ "engage:feeds:delete <feedId>",
3074
+ "Delete an Engage feed (if it was the open one, the first remaining feed takes over)",
3075
+ (y) => accountOption(y).positional("feedId", { describe: "Feed id (from engage:feeds)", type: "string" }),
3076
+ run(engageFeedsDelete)
3077
+ ).command(
3078
+ "engage:reply-draft",
3079
+ "Draft one reply to a post, in your voice (nothing is posted; costs AI credits)",
3080
+ (y) => accountOption(y).option("post", {
3081
+ describe: "X post id to reply to; the API reads it live (costs one live lookup). Use instead of --text",
3082
+ type: "string"
3083
+ }).option("text", { describe: "The post's text, supplied by you. Use instead of --post", type: "string" }).option("author", { describe: "The author's display name, with --text", type: "string" }).option("handle", { describe: "The author's @handle without the @, with --text", type: "string" }).option("thoughts", {
3084
+ describe: "What you want the reply to convey (max 2000 chars)",
3085
+ type: "string"
3086
+ }).option("tone", {
3087
+ describe: "Register for the reply",
3088
+ type: "string",
3089
+ choices: ["engaging", "humorous", "creative", "sarcastic", "inspirational", "concise"]
3090
+ }).example('$0 engage:reply-draft --post 1234567890 --thoughts "agree, and add that we saw the same thing"', "Draft a reply to a real post").example('$0 engage:reply-draft --text "hot take about pricing" --handle levelsio --tone concise', "Draft from text you paste in").epilogue(
3091
+ "Provide exactly one of --post or --text. The draft is TEXT: nothing is posted or sent, a person reviews it and posts it."
3092
+ ),
3093
+ run(engageReplyDraft)
3094
+ ).command(
3095
+ "engage:mentions",
3096
+ "The posts @-mentioning you right now, each with the post it replies to",
3097
+ (y) => accountOption(y).option("sort", {
3098
+ describe: "latest (default, newest first) or top (most engaged first)",
3099
+ type: "string",
3100
+ choices: ["latest", "top"]
3101
+ }).option("include-replied", {
3102
+ describe: "Keep mentions you already replied to on X, flagged replied",
3103
+ type: "boolean"
3104
+ }).option("cursor", {
3105
+ describe: "next_cursor from the previous call, to read the next page",
3106
+ type: "string"
3107
+ }).example("$0 engage:mentions --sort top", "The most engaged mentions first").epilogue(
3108
+ "Reads X live and costs 3 of your daily feed fetches per call, so read a page and work from it rather than polling. Page with --cursor."
3109
+ ),
3110
+ run(engageMentions)
3111
+ ).command(
3112
+ "audience:list <kind>",
3113
+ "A page of your followers, following, repliers or reposters",
3114
+ (y) => accountOption(y).positional("kind", {
3115
+ describe: "followers, following, repliers or reposters",
3116
+ type: "string",
3117
+ choices: ["followers", "following", "repliers", "reposters"]
3118
+ }).option("cursor", {
3119
+ describe: "next_cursor from the previous call, to read the next page",
3120
+ type: "string"
3121
+ }).option("limit", { describe: "People per page (1-100, default 50)", type: "number" }).example("$0 audience:list followers --limit 100", "The first 100 followers").epilogue(
3122
+ "These are the four system lists in the app's Contacts tab; lists:members does not serve them. Paging is by cursor, not page number, and meta.synced_count is the size of the whole list. Repliers and reposters cover a rolling 90 days."
3123
+ ),
3124
+ run(audienceList)
1428
3125
  ).command(
1429
3126
  "scheduled:list",
1430
3127
  "List drafts and the scheduled queue",
@@ -1503,6 +3200,49 @@ var advancedSettingsOptions = (y) => y.option("auto-retweet", {
1503
3200
  "Delete a draft or scheduled post by id",
1504
3201
  (y) => y.positional("id", { describe: "Post id (from scheduled:list or scheduled:create)", type: "string" }),
1505
3202
  run(scheduledDelete)
3203
+ ).command(
3204
+ "posts:publish",
3205
+ "Publish a post or thread to X RIGHT NOW (irreversible; --idempotency-key required)",
3206
+ (y) => advancedSettingsOptions(accountOption(y)).option("text", { describe: "Text for a single post", type: "string" }).option("part", {
3207
+ describe: "Thread part text (repeat the flag, 1-25 parts, in order)",
3208
+ type: "string",
3209
+ array: true
3210
+ }).option("media", {
3211
+ describe: "Comma list of image object_keys (from media:upload) to attach; single-post form only (max 4 images or 1 GIF)",
3212
+ type: "string"
3213
+ }).option("alt-text", {
3214
+ describe: "Accessibility description for the attached image (single --media key only, max 1000 chars)",
3215
+ type: "string"
3216
+ }).option("parts-json", {
3217
+ describe: 'Full parts array as JSON for threads with media: [{"text":"...","media":[{"object_key":"...","alt_text":"..."}]}]',
3218
+ type: "string"
3219
+ }).option("tag", {
3220
+ describe: "Tag id to assign (repeat the flag, max 20; ids from tags:list)",
3221
+ type: "string",
3222
+ array: true
3223
+ }).option("idempotency-key", {
3224
+ describe: "REQUIRED (max 64 chars). Reuse the SAME key when retrying so a timed-out call cannot post twice; use a new key only for new content",
3225
+ type: "string"
3226
+ }).example('$0 posts:publish --text "Shipping now." --idempotency-key launch-2026-09-07', "Publish a single post").example('$0 posts:publish --part "1/ Hook" --part "2/ Detail" --idempotency-key thread-42', "Publish a thread").example('$0 posts:publish --text "Shipping now." --auto-retweet 6 --idempotency-key launch-2026-09-07', "Publish with an auto retweet"),
3227
+ run(postsPublish)
3228
+ ).command(
3229
+ "scheduled:bulk-retime",
3230
+ "Move up to 500 queued posts to new times in one transaction",
3231
+ (y) => accountOption(y).option("moves-json", {
3232
+ describe: 'Moves as JSON: [{"id":"abc","scheduled_for":"2026-09-08T15:00:00Z"}] (max 500, each time 60s+ ahead)',
3233
+ type: "string"
3234
+ }).example(`$0 scheduled:bulk-retime --moves-json '[{"id":"abc","scheduled_for":"2026-09-08T15:00:00Z"}]'`, "Retime one queued post"),
3235
+ run(scheduledBulkRetime)
3236
+ ).command(
3237
+ "scheduled:bulk-auto-retweet",
3238
+ "Turn Auto Retweet on for up to 100 queued posts that do not have it yet",
3239
+ (y) => accountOption(y).option("ids", { describe: "Comma list of post ids (max 100, from scheduled:list)", type: "string" }).option("auto-retweet", { describe: "Retweet each post this many hours after it goes live (1-12), required" }).option("auto-retweet-remove", { describe: "Remove the retweet this many hours later (1-12)" }).example("$0 scheduled:bulk-auto-retweet --ids abc,def --auto-retweet 6", "Auto retweet two queued posts after 6 hours"),
3240
+ run(scheduledBulkAutoRetweet)
3241
+ ).command(
3242
+ "scheduled:bulk-delete",
3243
+ "Delete up to 100 QUEUED posts and refund their post quota (sent posts and drafts are left alone)",
3244
+ (y) => accountOption(y).option("ids", { describe: "Comma list of post ids (max 100, from scheduled:list)", type: "string" }).example("$0 scheduled:bulk-delete --ids abc,def", "Delete two queued posts"),
3245
+ run(scheduledBulkDelete)
1506
3246
  ).command(
1507
3247
  "plug-templates:list",
1508
3248
  "List your auto-plug reply templates (id, text, has_media) for --auto-plug",
@@ -1563,6 +3303,34 @@ var advancedSettingsOptions = (y) => y.option("auto-retweet", {
1563
3303
  "Remove a product by id (reversible by re-adding the same url)",
1564
3304
  (y) => accountOption(y).positional("id", { describe: "Product id (from context:products)", type: "string" }),
1565
3305
  run(contextProductsDelete)
3306
+ ).command(
3307
+ "context:products:replace",
3308
+ "Replace the WHOLE product list with a JSON array (max 5); products missing from it are removed",
3309
+ (y) => accountOption(y).option("json", {
3310
+ describe: `JSON array of products, e.g. '[{"url":"https://superx.so","name":"SuperX"}]' ('[]' removes every product)`,
3311
+ type: "string",
3312
+ demandOption: true
3313
+ }).example(
3314
+ `$0 context:products:replace --json '[{"url":"https://superx.so","name":"SuperX"}]'`,
3315
+ "Make SuperX the only product"
3316
+ ).epilogue(
3317
+ "CAUTION: this is a full replace by url. Read the current list with context:products first and send every product the account should keep; to change one product without touching the others, use context:products:set."
3318
+ ),
3319
+ run(contextProductsReplace)
3320
+ ).command(
3321
+ "context:regenerate-style-guide",
3322
+ "Rebuild the generated style guide from the account's recent posts (free, once an hour)",
3323
+ (y) => accountOption(y).example("$0 context:regenerate-style-guide", "Rewrite the guide for the main account").epilogue(
3324
+ "Costs no AI credits. Your manual style-guide overrides (context:set --style-audience / --style-vocabulary) are left alone and keep outranking the generated guide. An account with fewer than 5 recent posts stored is read live, which draws on a platform-wide fair-use ceiling."
3325
+ ),
3326
+ run(contextRegenerateStyleGuide)
3327
+ ).command(
3328
+ "context:scrape-product <id>",
3329
+ "Re-read a saved product's page and refresh its stored details (free)",
3330
+ (y) => accountOption(y).positional("id", { describe: "Product id (from context:products)", type: "string" }).example("$0 context:scrape-product 3", "Refresh product 3 from its own url").epilogue(
3331
+ "The url comes from the saved product, so change it with context:products:set first if it moved. Costs no AI credits, and runs on the account's allowance of 20 page reads a day, shared with the SuperX app."
3332
+ ),
3333
+ run(contextScrapeProduct)
1566
3334
  ).command(
1567
3335
  "queue:get",
1568
3336
  "Show the account's posting schedule: predefined time slots and the timezone they run in",
@@ -1649,15 +3417,74 @@ var advancedSettingsOptions = (y) => y.option("auto-retweet", {
1649
3417
  "Pull a scheduled article back to draft (quota refunds)",
1650
3418
  (y) => y.positional("id", { describe: "Article id", type: "string" }),
1651
3419
  run(articlesUnschedule)
3420
+ ).command(
3421
+ "articles:cover-styles",
3422
+ "List the article cover styles saved in the SuperX app (ids for --style-id)",
3423
+ (y) => accountOption(y),
3424
+ run(articlesCoverStyles)
1652
3425
  ).command(
1653
3426
  "articles:cover <id>",
1654
3427
  "Generate an AI cover for an article (60-100s, spends AI credits)",
1655
- (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", {
3428
+ (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("style-id", {
3429
+ describe: "Saved cover style id (from articles:cover-styles); not with --style",
3430
+ type: "string"
3431
+ }).option("attach", {
1656
3432
  describe: "Attach the result as the article's cover (use --no-attach to skip)",
1657
3433
  type: "boolean",
1658
3434
  default: true
1659
- }),
3435
+ }).example("$0 articles:cover abc123 --style-id sty_9f2", "Generate in a saved style"),
1660
3436
  run(articlesCover)
3437
+ ).command(
3438
+ "dm:campaign",
3439
+ "Queue direct messages to a list of X users (nothing is sent by this command)",
3440
+ (y) => accountOption(y).option("recipients", {
3441
+ describe: 'JSON file of [{ "x_user_id", "handle"?, "name"?, "message"? }]; --recipients=- reads stdin',
3442
+ type: "string",
3443
+ demandOption: true
3444
+ }).option("message", {
3445
+ describe: "The shared message (1-1000). [name], [first] and [handle] are filled per recipient",
3446
+ type: "string"
3447
+ }).option("spread", {
3448
+ describe: "Spread what today's allowance cannot hold over the coming days instead of skipping it",
3449
+ type: "boolean"
3450
+ }).option("idempotency-key", {
3451
+ describe: "Reuse the same key on a retry so one campaign is never queued twice",
3452
+ type: "string"
3453
+ }).example(
3454
+ '$0 dm:campaign --recipients people.json --message "Hey [first], loved your post"',
3455
+ "Queue a campaign from a file"
3456
+ ).epilogue(
3457
+ "NOTHING IS SENT BY THIS COMMAND. The messages go into your DM queue and the SuperX app sends them within your daily and monthly DM limits, so the reply is counts, not deliveries. People you messaged in the last 24 hours are skipped and your own account is never messaged. You are responsible for these messages under X's automation rules. Check your allowances with dm:limits and cancel the unsent ones with dm:cancel."
3458
+ ),
3459
+ run(dmCampaign)
3460
+ ).command(
3461
+ "dm:campaign-status <id>",
3462
+ "Show one campaign's status counts and its individual messages",
3463
+ (y) => accountOption(
3464
+ y.positional("id", { describe: "Campaign id (from dm:campaign)", type: "string" })
3465
+ ),
3466
+ run(dmCampaignStatus)
3467
+ ).command(
3468
+ "dm:cancel <id>",
3469
+ "Cancel a campaign's unsent messages (sent ones cannot be recalled)",
3470
+ (y) => accountOption(
3471
+ y.positional("id", { describe: "Campaign id (from dm:campaign)", type: "string" })
3472
+ ),
3473
+ run(dmCancel)
3474
+ ).command(
3475
+ "dm:queue",
3476
+ "List the account's queued and recently sent direct messages",
3477
+ (y) => accountOption(y).option("limit", { describe: "Rows per page (max 200, default 50)", type: "number" }).option("offset", { describe: "Rows to skip", type: "number" }).option("status", {
3478
+ describe: "Only rows in this state",
3479
+ type: "string",
3480
+ choices: ["pending", "sending", "sent", "failed", "skipped"]
3481
+ }).option("campaign", { describe: "Only rows from this campaign id", type: "string" }).example("$0 dm:queue --status pending", "What is still waiting to go out"),
3482
+ run(dmQueue)
3483
+ ).command(
3484
+ "dm:limits",
3485
+ "Show the account's DM allowances and how much of each is used (free)",
3486
+ (y) => accountOption(y),
3487
+ run(dmLimits)
1661
3488
  ).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) => {
1662
3489
  if (err) throw err;
1663
3490
  note(msg || "Invalid usage. Run: superx --help");