superx-cli 0.2.0 → 0.4.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,39 @@ 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
+ // --- Engage ---
321
+ async listEngageFeeds(query = {}) {
322
+ return (await this.request("/engage/feeds", { query })).json;
323
+ }
324
+ async getEngageFeedPosts(feedId, query = {}) {
325
+ return (await this.request(`/engage/feeds/${encodeURIComponent(feedId)}/posts`, { query })).json;
326
+ }
327
+ async createEngageFeed(body) {
328
+ return (await this.request("/engage/feeds", { method: "POST", body })).json;
329
+ }
330
+ async updateEngageFeed(feedId, body) {
331
+ return (await this.request(`/engage/feeds/${encodeURIComponent(feedId)}`, { method: "PATCH", body })).json;
332
+ }
333
+ async deleteEngageFeed(feedId, query = {}) {
334
+ return (await this.request(`/engage/feeds/${encodeURIComponent(feedId)}`, { method: "DELETE", query })).json;
335
+ }
187
336
  // --- Scheduled posts ---
188
337
  async listScheduled(query = {}) {
189
338
  return (await this.request("/scheduled-posts", { query })).json;
@@ -194,6 +343,29 @@ var SuperXAPI = class {
194
343
  const res = await this.request("/scheduled-posts", { method: "POST", body, headers });
195
344
  return { json: res.json, replayed: res.headers.get("idempotency-replayed") === "true" };
196
345
  }
346
+ /**
347
+ * Publish immediately: the create endpoint with scheduled_for "now". The
348
+ * Idempotency-Key is REQUIRED by the API (a retry must never post twice),
349
+ * so it is a plain parameter here rather than an optional one.
350
+ */
351
+ async publishNow(body, idempotencyKey) {
352
+ const res = await this.request("/scheduled-posts", {
353
+ method: "POST",
354
+ body,
355
+ headers: { "Idempotency-Key": idempotencyKey }
356
+ });
357
+ return { json: res.json, replayed: res.headers.get("idempotency-replayed") === "true" };
358
+ }
359
+ // --- Scheduled posts: bulk queue operations ---
360
+ async bulkRetimeScheduled(body) {
361
+ return (await this.request("/scheduled-posts/bulk/retime", { method: "POST", body })).json;
362
+ }
363
+ async bulkEnableAutoRetweet(body) {
364
+ return (await this.request("/scheduled-posts/bulk/auto-retweet", { method: "POST", body })).json;
365
+ }
366
+ async bulkDeleteScheduled(body) {
367
+ return (await this.request("/scheduled-posts/bulk/delete", { method: "POST", body })).json;
368
+ }
197
369
  async updateScheduled(id, body) {
198
370
  return (await this.request(`/scheduled-posts/${encodeURIComponent(id)}`, { method: "PATCH", body })).json;
199
371
  }
@@ -217,6 +389,21 @@ var SuperXAPI = class {
217
389
  async deleteContextProduct(id, query = {}) {
218
390
  return (await this.request(`/context/products/${encodeURIComponent(id)}`, { method: "DELETE", query })).json;
219
391
  }
392
+ /** Full replace of the account's product list (max 5). */
393
+ async setContextProducts(body) {
394
+ return (await this.request("/context/products", { method: "PUT", body })).json;
395
+ }
396
+ /** Rebuild the generated style guide from recent posts. Free, once an hour. */
397
+ async regenerateStyleGuide(body) {
398
+ return (await this.request("/context/style-guide/regenerate", { method: "POST", body })).json;
399
+ }
400
+ /** Re-read a saved product's page and refresh its stored details. Free. */
401
+ async scrapeContextProduct(id, body) {
402
+ return (await this.request(`/context/products/${encodeURIComponent(id)}/scrape`, {
403
+ method: "POST",
404
+ body
405
+ })).json;
406
+ }
220
407
  // --- Queue settings ---
221
408
  async getQueueSettings(query = {}) {
222
409
  return (await this.request("/queue-settings", { query })).json;
@@ -269,6 +456,99 @@ var SuperXAPI = class {
269
456
  async generateArticleCover(id, body) {
270
457
  return (await this.request(`/articles/${encodeURIComponent(id)}/cover`, { method: "POST", body })).json;
271
458
  }
459
+ /** Saved article cover styles (pass an id as style_id to the cover call). */
460
+ async listCoverStyles(query = {}) {
461
+ return (await this.request("/cover-styles", { query })).json;
462
+ }
463
+ // --- Datasets (Ask SuperX collections) ---
464
+ async listDatasets(query = {}) {
465
+ return (await this.request("/datasets", { query })).json;
466
+ }
467
+ async getDataset(id) {
468
+ return (await this.request(`/datasets/${encodeURIComponent(id)}`)).json;
469
+ }
470
+ async getDatasetRows(id, query = {}) {
471
+ return (await this.request(`/datasets/${encodeURIComponent(id)}/rows`, { query })).json;
472
+ }
473
+ /** CSV export. Raw text, with the server's filename from Content-Disposition. */
474
+ async exportDatasetCsv(id) {
475
+ const { text, headers } = await this.requestText(
476
+ `/datasets/${encodeURIComponent(id)}/export`,
477
+ { format: "csv" }
478
+ );
479
+ const disposition = headers.get("content-disposition") || "";
480
+ const match = /filename="([^"]+)"/.exec(disposition);
481
+ return { filename: match ? match[1] : `superx-dataset-${id}.csv`, text };
482
+ }
483
+ async addDatasetToList(id, body) {
484
+ return (await this.request(`/datasets/${encodeURIComponent(id)}/contacts`, { method: "POST", body })).json;
485
+ }
486
+ /** Start a collection. Answers a ready dataset, or a collecting one (202). */
487
+ async createDataset(body) {
488
+ return (await this.request("/datasets", { method: "POST", body })).json;
489
+ }
490
+ /** Draft one outreach message per row of a research dataset. Text only. */
491
+ async draftOutreachDms(id, body) {
492
+ return (await this.request(`/datasets/${encodeURIComponent(id)}/outreach-drafts`, {
493
+ method: "POST",
494
+ body
495
+ })).json;
496
+ }
497
+ /** Filter a dataset by each row's text into a NEW dataset (200 or 202). */
498
+ async refineDataset(id, body) {
499
+ return (await this.request(`/datasets/${encodeURIComponent(id)}/refine`, {
500
+ method: "POST",
501
+ body
502
+ })).json;
503
+ }
504
+ // --- Audience (the four system people-lists) ---
505
+ /** kind: followers | following | repliers | reposters. Cursor paging. */
506
+ async getAudience(kind, query = {}) {
507
+ return (await this.request(`/audience/${encodeURIComponent(kind)}`, { query })).json;
508
+ }
509
+ /** Posts @-mentioning the account, read live. Costs 3 feed fetches. */
510
+ async getMentions(query = {}) {
511
+ return (await this.request("/engage/mentions", { query })).json;
512
+ }
513
+ // --- Live X lookups (read X now, not SuperX's stored data) ---
514
+ async lookupXPost(id, query = {}) {
515
+ return (await this.request(`/x/posts/${encodeURIComponent(id)}`, { query })).json;
516
+ }
517
+ async getXPostReplies(id, query = {}) {
518
+ return (await this.request(`/x/posts/${encodeURIComponent(id)}/replies`, { query })).json;
519
+ }
520
+ async lookupXUser(handle) {
521
+ return (await this.request(`/x/users/${encodeURIComponent(handle)}`)).json;
522
+ }
523
+ async getXUserPosts(handle, query = {}) {
524
+ return (await this.request(`/x/users/${encodeURIComponent(handle)}/posts`, { query })).json;
525
+ }
526
+ // --- Inspiration media (cross-platform media index) ---
527
+ async searchInspirationMedia(query = {}) {
528
+ return (await this.request("/inspiration/media", { query })).json;
529
+ }
530
+ // --- DM campaigns (enqueue only: the SuperX app sends) ---
531
+ async queueDmCampaign(body, idempotencyKey) {
532
+ const headers = {};
533
+ if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
534
+ const res = await this.request("/dm/campaigns", { method: "POST", body, headers });
535
+ return { json: res.json, replayed: res.headers.get("idempotency-replayed") === "true" };
536
+ }
537
+ async getDmCampaign(id, query = {}) {
538
+ return (await this.request(`/dm/campaigns/${encodeURIComponent(id)}`, { query })).json;
539
+ }
540
+ async cancelDmCampaign(id, query = {}) {
541
+ return (await this.request(`/dm/campaigns/${encodeURIComponent(id)}`, {
542
+ method: "DELETE",
543
+ query
544
+ })).json;
545
+ }
546
+ async listDmQueue(query = {}) {
547
+ return (await this.request("/dm/queue", { query })).json;
548
+ }
549
+ async getDmLimits(query = {}) {
550
+ return (await this.request("/dm/limits", { query })).json;
551
+ }
272
552
  // --- Docs (unauthenticated markdown) ---
273
553
  async docs() {
274
554
  const url = this.buildUrl("/docs");
@@ -405,6 +685,7 @@ async function status() {
405
685
  api_url: config.apiUrl,
406
686
  owner: me2?.data?.owner ?? null,
407
687
  plan: me2?.data?.plan ?? null,
688
+ credits: me2?.data?.credits ?? null,
408
689
  key: me2?.data?.key ?? null,
409
690
  rate_limit: api.lastRateLimit
410
691
  });
@@ -445,6 +726,24 @@ async function postsAnalytics(argv) {
445
726
  })
446
727
  );
447
728
  }
729
+ async function postsDraft(argv) {
730
+ if (!argv.brief || !argv.brief.trim()) {
731
+ note('Provide --brief "what the post should say".');
732
+ process.exit(1);
733
+ }
734
+ const body = { brief: argv.brief };
735
+ if (argv.count !== void 0) body.count = argv.count;
736
+ if (argv.voice) body.voice = argv.voice;
737
+ if (argv.creator) body.creator = argv.creator;
738
+ if (argv.mirror) body.mirror = argv.mirror;
739
+ if (argv.collection) body.collection = argv.collection;
740
+ if (argv.instructions) body.instructions = argv.instructions;
741
+ if (argv.account) body.account_id = argv.account;
742
+ const api = new SuperXAPI(getConfig());
743
+ const json = await api.draftPost(body);
744
+ note("Nothing was scheduled. Review the text, then pass it to scheduled:create.");
745
+ printJson(json);
746
+ }
448
747
  async function repliesList(argv) {
449
748
  const api = new SuperXAPI(getConfig());
450
749
  printJson(
@@ -470,6 +769,49 @@ async function repliesReceived(argv) {
470
769
  })
471
770
  );
472
771
  }
772
+ async function postsRemix(argv) {
773
+ const body = {
774
+ text: argv.text,
775
+ closeness: argv.closeness
776
+ };
777
+ if (argv.instructions) body.instructions = argv.instructions;
778
+ if (argv.account) body.account_id = argv.account;
779
+ const api = new SuperXAPI(getConfig());
780
+ printJson(await api.remixPost(body));
781
+ }
782
+
783
+ // src/commands/tools.ts
784
+ async function toolsInlineEdit(argv) {
785
+ if (!argv.instruction && !argv.type) {
786
+ note('Provide --instruction "..." or --type <preset> (or both).');
787
+ process.exit(1);
788
+ }
789
+ const body = { text: argv.text };
790
+ if (argv.full) body.full_text = argv.full;
791
+ if (argv.instruction) body.instruction = argv.instruction;
792
+ if (argv.type) body.edit_type = argv.type;
793
+ if (argv.account) body.account_id = argv.account;
794
+ const api = new SuperXAPI(getConfig());
795
+ printJson(await api.inlineEdit(body));
796
+ }
797
+ async function toolsRephrase(argv) {
798
+ const body = { type: argv.type, text: argv.text };
799
+ if (argv.account) body.account_id = argv.account;
800
+ const api = new SuperXAPI(getConfig());
801
+ printJson(await api.rephrase(body));
802
+ }
803
+ async function toolsFactcheck(argv) {
804
+ const body = { text: argv.text };
805
+ if (argv.account) body.account_id = argv.account;
806
+ const api = new SuperXAPI(getConfig());
807
+ printJson(await api.factCheck(body));
808
+ }
809
+ async function toolsPredict(argv) {
810
+ const body = { version_a: argv.a, version_b: argv.b };
811
+ if (argv.account) body.account_id = argv.account;
812
+ const api = new SuperXAPI(getConfig());
813
+ printJson(await api.predictAlgorithm(body));
814
+ }
473
815
 
474
816
  // src/commands/inspiration.ts
475
817
  async function inspirationSearch(argv) {
@@ -494,6 +836,70 @@ async function inspirationSearch(argv) {
494
836
  })
495
837
  );
496
838
  }
839
+ async function inspirationMedia(argv) {
840
+ const api = new SuperXAPI(getConfig());
841
+ printJson(
842
+ await api.searchInspirationMedia({
843
+ q: argv.query,
844
+ platforms: argv.platforms,
845
+ time_filter: argv.timeFilter,
846
+ media_type: argv.mediaType,
847
+ content_type: argv.contentType,
848
+ limit: argv.limit
849
+ })
850
+ );
851
+ }
852
+
853
+ // src/commands/x.ts
854
+ function toPostId(ref) {
855
+ const s = String(ref || "").trim();
856
+ if (/^\d{1,25}$/.test(s)) return s;
857
+ const m = s.match(
858
+ /^https?:\/\/(?:www\.|mobile\.)?(?:x\.com|twitter\.com)\/[A-Za-z0-9_]+\/status(?:es)?\/(\d+)/i
859
+ );
860
+ if (m) return m[1];
861
+ throw new ApiError(
862
+ 0,
863
+ "invalid_parameter",
864
+ "Provide an x.com/twitter.com post URL or a bare numeric post id."
865
+ );
866
+ }
867
+ function toHandle(raw) {
868
+ const s = String(raw || "").trim().replace(/^@/, "");
869
+ if (!/^[A-Za-z0-9_]{1,15}$/.test(s)) {
870
+ throw new ApiError(
871
+ 0,
872
+ "invalid_parameter",
873
+ "Provide a valid X handle: 1-15 letters, numbers or underscores."
874
+ );
875
+ }
876
+ return s;
877
+ }
878
+ async function xPost(argv) {
879
+ const api = new SuperXAPI(getConfig());
880
+ printJson(
881
+ await api.lookupXPost(toPostId(argv.id), {
882
+ include_quotes: argv.quotes ? "true" : void 0
883
+ })
884
+ );
885
+ }
886
+ async function xReplies(argv) {
887
+ const api = new SuperXAPI(getConfig());
888
+ printJson(await api.getXPostReplies(toPostId(argv.id), { limit: argv.limit }));
889
+ }
890
+ async function xUser(argv) {
891
+ const api = new SuperXAPI(getConfig());
892
+ printJson(await api.lookupXUser(toHandle(argv.handle)));
893
+ }
894
+ async function xUserPosts(argv) {
895
+ const api = new SuperXAPI(getConfig());
896
+ printJson(
897
+ await api.getXUserPosts(toHandle(argv.handle), {
898
+ limit: argv.limit,
899
+ exclude_reposts: argv.reposts === false ? "true" : void 0
900
+ })
901
+ );
902
+ }
497
903
 
498
904
  // src/commands/contacts.ts
499
905
  async function contactsList(argv) {
@@ -518,6 +924,38 @@ async function contactsReplies(argv) {
518
924
  })
519
925
  );
520
926
  }
927
+ async function contactsGet(argv) {
928
+ const api = new SuperXAPI(getConfig());
929
+ printJson(
930
+ await api.getContact(argv.id, {
931
+ account_id: argv.account,
932
+ // Only send the flag when asked: the default read is cache-only and
933
+ // costs no enrichment.
934
+ refresh: argv.refresh ? "true" : void 0
935
+ })
936
+ );
937
+ }
938
+ async function contactsNotes(argv) {
939
+ const api = new SuperXAPI(getConfig());
940
+ printJson(await api.listContactNotes(argv.id, { account_id: argv.account }));
941
+ }
942
+ async function contactsNotesAdd(argv) {
943
+ const body = { body: argv.body };
944
+ if (argv.account) body.account_id = argv.account;
945
+ const api = new SuperXAPI(getConfig());
946
+ printJson(await api.addContactNote(argv.id, body));
947
+ }
948
+ async function contactsNotesUpdate(argv) {
949
+ const body = { body: argv.body };
950
+ if (argv.account) body.account_id = argv.account;
951
+ const api = new SuperXAPI(getConfig());
952
+ printJson(await api.updateContactNote(argv.id, argv.noteId, body));
953
+ }
954
+ async function contactsNotesDelete(argv) {
955
+ const api = new SuperXAPI(getConfig());
956
+ await api.deleteContactNote(argv.id, argv.noteId, { account_id: argv.account });
957
+ printJson({ data: { id: argv.noteId, deleted: true } });
958
+ }
521
959
 
522
960
  // src/commands/lists.ts
523
961
  async function listsList(argv) {
@@ -548,6 +986,181 @@ async function listsRemoveMember(argv) {
548
986
  await api.removeListMember(argv.id, argv.memberId);
549
987
  printJson({ data: { id: argv.memberId, removed: true } });
550
988
  }
989
+ async function listsCreate(argv) {
990
+ const body = { name: argv.name };
991
+ if (argv.account) body.account_id = argv.account;
992
+ const api = new SuperXAPI(getConfig());
993
+ printJson(await api.createList(body));
994
+ }
995
+ async function listsRename(argv) {
996
+ const body = { name: argv.name };
997
+ if (argv.account) body.account_id = argv.account;
998
+ const api = new SuperXAPI(getConfig());
999
+ printJson(await api.renameList(argv.id, body));
1000
+ }
1001
+ async function listsDelete(argv) {
1002
+ const api = new SuperXAPI(getConfig());
1003
+ await api.deleteList(argv.id, { account_id: argv.account });
1004
+ printJson({ data: { id: argv.id, deleted: true } });
1005
+ }
1006
+ function commaList(value) {
1007
+ return value.split(",").map((s) => s.trim()).filter(Boolean);
1008
+ }
1009
+ async function listsAddMembers(argv) {
1010
+ const ids = commaList(argv.xUserIds || "");
1011
+ if (ids.length === 0) {
1012
+ note("Provide at least one id: --x-user-ids 44196397,1234567890");
1013
+ process.exit(1);
1014
+ }
1015
+ const body = { x_user_ids: ids };
1016
+ if (argv.account) body.account_id = argv.account;
1017
+ const api = new SuperXAPI(getConfig());
1018
+ printJson(await api.addListMembers(argv.id, body));
1019
+ }
1020
+ async function listsRemoveMembers(argv) {
1021
+ const ids = commaList(argv.memberIds || "");
1022
+ if (ids.length === 0) {
1023
+ note("Provide at least one id: --member-ids abc123,def456");
1024
+ process.exit(1);
1025
+ }
1026
+ const body = { member_ids: ids };
1027
+ if (argv.account) body.account_id = argv.account;
1028
+ const api = new SuperXAPI(getConfig());
1029
+ printJson(await api.removeListMembers(argv.id, body));
1030
+ }
1031
+
1032
+ // src/commands/datasets.ts
1033
+ var import_fs = __toESM(require("fs"));
1034
+ var import_path = __toESM(require("path"));
1035
+ async function datasetsList(argv) {
1036
+ const api = new SuperXAPI(getConfig());
1037
+ printJson(await api.listDatasets({ limit: argv.limit, page: argv.page }));
1038
+ }
1039
+ async function datasetsGet(argv) {
1040
+ const api = new SuperXAPI(getConfig());
1041
+ printJson(await api.getDataset(argv.id));
1042
+ }
1043
+ async function datasetsRows(argv) {
1044
+ const api = new SuperXAPI(getConfig());
1045
+ printJson(
1046
+ await api.getDatasetRows(argv.id, { limit: argv.limit, page: argv.page })
1047
+ );
1048
+ }
1049
+ async function datasetsExport(argv) {
1050
+ const api = new SuperXAPI(getConfig());
1051
+ const { filename, text } = await api.exportDatasetCsv(argv.id);
1052
+ if (argv.out === "-") {
1053
+ process.stdout.write(text);
1054
+ return;
1055
+ }
1056
+ const target = argv.out ? argv.out : import_path.default.join(process.cwd(), import_path.default.basename(filename));
1057
+ import_fs.default.writeFileSync(target, text, "utf8");
1058
+ note(`Wrote ${target}`);
1059
+ printJson({ file: target, bytes: Buffer.byteLength(text, "utf8") });
1060
+ }
1061
+ async function datasetsAddToList(argv) {
1062
+ const body = { list_id: argv.listId };
1063
+ if (argv.account) body.account_id = argv.account;
1064
+ const api = new SuperXAPI(getConfig());
1065
+ printJson(await api.addDatasetToList(argv.id, body));
1066
+ }
1067
+ var WAIT_POLL_MS = 5e3;
1068
+ var WAIT_TIMEOUT_MS = 15 * 60 * 1e3;
1069
+ async function waitForDataset(api, started) {
1070
+ const datasetId = started?.data?.id;
1071
+ if (!datasetId || started?.data?.status !== "collecting") return false;
1072
+ note(`Running ${datasetId}; polling every ${WAIT_POLL_MS / 1e3}s until it is ready.`);
1073
+ const deadline = Date.now() + WAIT_TIMEOUT_MS;
1074
+ let latest = started;
1075
+ while (Date.now() < deadline) {
1076
+ await new Promise((resolve) => setTimeout(resolve, WAIT_POLL_MS));
1077
+ latest = await api.getDataset(datasetId);
1078
+ if (latest?.data?.status !== "collecting") {
1079
+ printJson(latest);
1080
+ return true;
1081
+ }
1082
+ }
1083
+ note("Still running after 15 minutes; giving up on waiting (the job keeps going).");
1084
+ printJson(latest);
1085
+ return true;
1086
+ }
1087
+ async function datasetsCollect(argv) {
1088
+ const splitList = (raw) => raw === void 0 ? void 0 : raw.split(",").map((k) => k.trim()).filter(Boolean);
1089
+ const filters = {};
1090
+ const keywords = splitList(argv.keywords);
1091
+ if (keywords) filters.keywords = keywords;
1092
+ const bioKeywords = splitList(argv.bioKeywords);
1093
+ if (bioKeywords) filters.bio_keywords = bioKeywords;
1094
+ if (argv.minFollowers !== void 0) filters.min_followers = argv.minFollowers;
1095
+ if (argv.requireWebsite !== void 0) filters.require_website = argv.requireWebsite;
1096
+ if (argv.requireCanDm !== void 0) filters.require_can_dm = argv.requireCanDm;
1097
+ if (argv.sinceDays !== void 0) filters.since_days = argv.sinceDays;
1098
+ if (argv.sort !== void 0) filters.sort = argv.sort;
1099
+ const body = { source: argv.source };
1100
+ if (argv.target) body.target = argv.target;
1101
+ if (argv.title) body.title = argv.title;
1102
+ if (argv.maxRows !== void 0) body.max_rows = argv.maxRows;
1103
+ if (argv.account) body.account_id = argv.account;
1104
+ if (Object.keys(filters).length > 0) body.filters = filters;
1105
+ const api = new SuperXAPI(getConfig());
1106
+ const started = await api.createDataset(body);
1107
+ if (argv.wait && await waitForDataset(api, started)) return;
1108
+ printJson(started);
1109
+ }
1110
+ async function datasetsResearch(argv) {
1111
+ const body = { source: "research" };
1112
+ if (argv.handles !== void 0) {
1113
+ body.handles = argv.handles.split(",").map((h) => h.trim()).filter(Boolean);
1114
+ }
1115
+ if (argv.list) body.list_id = argv.list;
1116
+ if (argv.agent !== void 0) body.agent_id = argv.agent;
1117
+ if (argv.dataset) body.dataset_id = argv.dataset;
1118
+ if (argv.max !== void 0) body.max_rows = argv.max;
1119
+ if (argv.focus) body.focus = argv.focus;
1120
+ if (argv.title) body.title = argv.title;
1121
+ if (argv.account) body.account_id = argv.account;
1122
+ const api = new SuperXAPI(getConfig());
1123
+ const started = await api.createDataset(body);
1124
+ if (argv.wait && await waitForDataset(api, started)) return;
1125
+ printJson(started);
1126
+ }
1127
+ async function datasetsOutreachDrafts(argv) {
1128
+ const body = { format: argv.format };
1129
+ if (argv.instructions) body.instructions = argv.instructions;
1130
+ if (argv.account) body.account_id = argv.account;
1131
+ const api = new SuperXAPI(getConfig());
1132
+ printJson(await api.draftOutreachDms(argv.id, body));
1133
+ }
1134
+ async function datasetsRefine(argv) {
1135
+ const body = { criterion: argv.criterion };
1136
+ if (argv.keep !== void 0) body.keep_matching = argv.keep;
1137
+ if (argv.sort) body.sort_by = argv.sort;
1138
+ if (argv.limit !== void 0) body.limit = argv.limit;
1139
+ if (argv.title) body.title = argv.title;
1140
+ if (argv.account) body.account_id = argv.account;
1141
+ const api = new SuperXAPI(getConfig());
1142
+ const started = await api.refineDataset(argv.id, body);
1143
+ if (argv.wait && await waitForDataset(api, started)) return;
1144
+ printJson(started);
1145
+ }
1146
+
1147
+ // src/commands/audience.ts
1148
+ var KINDS = ["followers", "following", "repliers", "reposters"];
1149
+ async function audienceList(argv) {
1150
+ const kind = String(argv.kind || "").trim();
1151
+ if (!KINDS.includes(kind)) {
1152
+ note(`kind must be one of: ${KINDS.join(", ")}`);
1153
+ process.exit(1);
1154
+ }
1155
+ const api = new SuperXAPI(getConfig());
1156
+ printJson(
1157
+ await api.getAudience(kind, {
1158
+ account_id: argv.account,
1159
+ cursor: argv.cursor,
1160
+ limit: argv.limit
1161
+ })
1162
+ );
1163
+ }
551
1164
 
552
1165
  // src/commands/signals.ts
553
1166
  async function signalsAgents(argv) {
@@ -569,6 +1182,17 @@ async function signalsLeads(argv) {
569
1182
  })
570
1183
  );
571
1184
  }
1185
+ async function signalsSearch(argv) {
1186
+ const body = {
1187
+ keywords: argv.keywords,
1188
+ icp_description: argv.icp
1189
+ };
1190
+ if (argv.precision) body.precision = argv.precision;
1191
+ if (argv.max !== void 0) body.max_leads = argv.max;
1192
+ if (argv.account) body.account_id = argv.account;
1193
+ const api = new SuperXAPI(getConfig());
1194
+ printJson(await api.searchLeads(body));
1195
+ }
572
1196
  async function signalsCreateAgent(argv) {
573
1197
  const body = {
574
1198
  name: argv.name,
@@ -578,6 +1202,8 @@ async function signalsCreateAgent(argv) {
578
1202
  if (argv["list-id"]) body.destination_list_id = argv["list-id"];
579
1203
  const keywords = (argv.keyword || []).filter((k) => typeof k === "string" && k.length > 0);
580
1204
  if (keywords.length > 0) body.keywords = keywords;
1205
+ const specs = (argv.signal || []).filter((s) => typeof s === "string" && s.length > 0);
1206
+ if (specs.length > 0) body.signals = specs.map(parseSignalSpec);
581
1207
  const api = new SuperXAPI(getConfig());
582
1208
  const { json, replayed } = await api.createSignalAgent(body, argv["idempotency-key"]);
583
1209
  if (replayed) {
@@ -599,19 +1225,214 @@ async function signalsDeleteAgent(argv) {
599
1225
  const api = new SuperXAPI(getConfig());
600
1226
  printJson(await api.deleteSignalAgent(argv.id));
601
1227
  }
1228
+ var SIGNAL_TYPE_ALIASES = {
1229
+ keyword: "keyword_watch",
1230
+ keyword_watch: "keyword_watch",
1231
+ profile: "profile_watch",
1232
+ profile_watch: "profile_watch",
1233
+ follower: "follower_watch",
1234
+ follower_watch: "follower_watch",
1235
+ list: "list_watch",
1236
+ list_watch: "list_watch"
1237
+ };
1238
+ function parseSignalSpec(spec) {
1239
+ const at = spec.indexOf(":");
1240
+ const rawType = at === -1 ? "" : spec.slice(0, at).trim().toLowerCase();
1241
+ const target = at === -1 ? "" : spec.slice(at + 1).trim();
1242
+ const type = SIGNAL_TYPE_ALIASES[rawType];
1243
+ if (!type || !target) {
1244
+ note(
1245
+ `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.`
1246
+ );
1247
+ process.exit(1);
1248
+ }
1249
+ if (type === "keyword_watch") return { type, query: target };
1250
+ if (type === "list_watch") return { type, list: target };
1251
+ return { type, handle: target };
1252
+ }
1253
+ async function signalsUpdateAgent(argv) {
1254
+ const body = {};
1255
+ if (argv.name !== void 0) body.name = argv.name;
1256
+ if (argv.icp !== void 0) body.icp_description = argv.icp;
1257
+ if (argv.precision !== void 0) body.precision_mode = argv.precision;
1258
+ if (argv["list-id"] !== void 0) body.destination_list_id = argv["list-id"];
1259
+ if (argv.status !== void 0) body.status = argv.status;
1260
+ if (Object.keys(body).length === 0) {
1261
+ note("Provide at least one of: --name, --icp, --precision, --list-id, --status.");
1262
+ process.exit(1);
1263
+ }
1264
+ const api = new SuperXAPI(getConfig());
1265
+ printJson(await api.updateSignalAgent(argv.id, body));
1266
+ }
1267
+ async function signalsAddSignal(argv) {
1268
+ const type = SIGNAL_TYPE_ALIASES[String(argv.type || "").toLowerCase()];
1269
+ if (!type) {
1270
+ note("--type must be one of: keyword_watch, profile_watch, follower_watch, list_watch.");
1271
+ process.exit(1);
1272
+ }
1273
+ const body = { type };
1274
+ if (argv.query !== void 0) body.query = argv.query;
1275
+ if (argv.handle !== void 0) body.handle = argv.handle;
1276
+ if (argv.list !== void 0) body.list = argv.list;
1277
+ if (argv.account) body.account_id = argv.account;
1278
+ const api = new SuperXAPI(getConfig());
1279
+ printJson(await api.addSignalAgentSignal(argv.id, body));
1280
+ }
1281
+ async function signalsRemoveSignal(argv) {
1282
+ const api = new SuperXAPI(getConfig());
1283
+ await api.removeSignalAgentSignal(argv.id, argv.signalId);
1284
+ printJson({ agent_id: argv.id, signal_id: argv.signalId, deleted: true });
1285
+ }
1286
+ async function signalsFeedback(argv) {
1287
+ const chosen = [argv.fit, argv["not-fit"], argv.clear].filter(Boolean);
1288
+ if (chosen.length !== 1) {
1289
+ note("Pass exactly one of --fit, --not-fit or --clear.");
1290
+ process.exit(1);
1291
+ }
1292
+ const feedback = argv.fit ? "fit" : argv["not-fit"] ? "not_fit" : null;
1293
+ const body = { feedback };
1294
+ if (argv.account) body.account_id = argv.account;
1295
+ const api = new SuperXAPI(getConfig());
1296
+ printJson(await api.setLeadFeedback(argv.leadId, body));
1297
+ }
1298
+ async function signalsSuggestKeywords(argv) {
1299
+ const body = { icp_description: argv.icp };
1300
+ if (argv.account) body.account_id = argv.account;
1301
+ const api = new SuperXAPI(getConfig());
1302
+ printJson(await api.suggestKeywords(body));
1303
+ }
1304
+ async function signalsExpandIcp(argv) {
1305
+ const text = (argv.text || "").trim();
1306
+ const url = (argv.url || "").trim();
1307
+ if (!text && !url) {
1308
+ note("Provide --text with an audience description, or --url with a website.");
1309
+ process.exit(1);
1310
+ }
1311
+ if (text && url) {
1312
+ note("Use either --text or --url, not both.");
1313
+ process.exit(1);
1314
+ }
1315
+ const body = url ? { url } : { icp_description: text };
1316
+ if (argv.account) body.account_id = argv.account;
1317
+ const api = new SuperXAPI(getConfig());
1318
+ printJson(url ? await api.expandIcpFromUrl(body) : await api.expandIcp(body));
1319
+ }
602
1320
 
603
- // src/commands/scheduled.ts
604
- async function scheduledList(argv) {
1321
+ // src/commands/engage.ts
1322
+ async function engageFeeds(argv) {
1323
+ const api = new SuperXAPI(getConfig());
1324
+ printJson(await api.listEngageFeeds({ account_id: argv.account }));
1325
+ }
1326
+ async function engagePosts(argv) {
605
1327
  const api = new SuperXAPI(getConfig());
606
1328
  printJson(
607
- await api.listScheduled({
1329
+ await api.getEngageFeedPosts(argv.feedId, {
608
1330
  account_id: argv.account,
609
- status: argv.status,
610
- tags: argv.tags,
611
- from: argv.from,
612
- to: argv.to,
613
1331
  limit: argv.limit,
614
- page: argv.page
1332
+ mode: argv.mode,
1333
+ // yargs boolean: pass through only when the flag was given.
1334
+ fresh: argv.fresh === void 0 ? void 0 : String(argv.fresh),
1335
+ include_replied: argv.includeReplied === void 0 ? void 0 : String(argv.includeReplied),
1336
+ exclude_post_ids: argv.exclude
1337
+ })
1338
+ );
1339
+ }
1340
+ function feedSourceBody(argv) {
1341
+ const keywords = (argv.keyword || []).filter(
1342
+ (k) => typeof k === "string" && k.length > 0
1343
+ );
1344
+ const families = [
1345
+ keywords.length > 0 ? "keywords" : null,
1346
+ argv["x-list"] ? "x_list" : null,
1347
+ argv["list-id"] ? "list" : null
1348
+ ].filter(Boolean);
1349
+ if (families.length === 0) return null;
1350
+ if (families.length > 1) {
1351
+ note("Use exactly one source: --keyword, --x-list or --list-id.");
1352
+ process.exit(1);
1353
+ }
1354
+ if (keywords.length > 0) return { type: "keywords", keywords };
1355
+ if (argv["x-list"]) {
1356
+ const raw = String(argv["x-list"]).trim();
1357
+ return /^\d{1,32}$/.test(raw) ? { type: "x_list", x_list_id: raw } : { type: "x_list", x_list_url: raw };
1358
+ }
1359
+ return { type: "list", list_id: argv["list-id"] };
1360
+ }
1361
+ async function engageFeedsCreate(argv) {
1362
+ const source = feedSourceBody(argv);
1363
+ if (!source) {
1364
+ note("Provide a source: --keyword (repeatable), --x-list or --list-id.");
1365
+ process.exit(1);
1366
+ }
1367
+ const body = { name: argv.name, ...source };
1368
+ if (argv.account) body.account_id = argv.account;
1369
+ const api = new SuperXAPI(getConfig());
1370
+ printJson(await api.createEngageFeed(body));
1371
+ }
1372
+ async function engageFeedsUpdate(argv) {
1373
+ const source = feedSourceBody(argv);
1374
+ const body = { ...source || {} };
1375
+ delete body.type;
1376
+ if (argv.name !== void 0) body.name = argv.name;
1377
+ if (Object.keys(body).length === 0) {
1378
+ note("Provide --name, or a new source: --keyword, --x-list or --list-id.");
1379
+ process.exit(1);
1380
+ }
1381
+ if (argv.account) body.account_id = argv.account;
1382
+ const api = new SuperXAPI(getConfig());
1383
+ printJson(await api.updateEngageFeed(argv.feedId, body));
1384
+ }
1385
+ async function engageFeedsDelete(argv) {
1386
+ const api = new SuperXAPI(getConfig());
1387
+ await api.deleteEngageFeed(argv.feedId, { account_id: argv.account });
1388
+ printJson({ id: argv.feedId, deleted: true });
1389
+ }
1390
+ async function engageMentions(argv) {
1391
+ const api = new SuperXAPI(getConfig());
1392
+ printJson(
1393
+ await api.getMentions({
1394
+ account_id: argv.account,
1395
+ sort: argv.sort,
1396
+ // yargs boolean: pass through only when the flag was given.
1397
+ include_replied: argv.includeReplied === void 0 ? void 0 : String(argv.includeReplied),
1398
+ cursor: argv.cursor
1399
+ })
1400
+ );
1401
+ }
1402
+ async function engageReplyDraft(argv) {
1403
+ const hasPost = !!argv.post;
1404
+ const hasText = !!argv.text;
1405
+ if (hasPost === hasText) {
1406
+ note('Provide exactly one of --post <id> (read live) or --text "the post".');
1407
+ process.exit(1);
1408
+ }
1409
+ const body = {};
1410
+ if (hasPost) body.post_id = argv.post;
1411
+ else {
1412
+ const post = { text: argv.text };
1413
+ if (argv.author) post.author_name = argv.author;
1414
+ if (argv.handle) post.author_handle = argv.handle;
1415
+ body.post = post;
1416
+ }
1417
+ if (argv.thoughts) body.thoughts = argv.thoughts;
1418
+ if (argv.tone) body.tone = argv.tone;
1419
+ if (argv.account) body.account_id = argv.account;
1420
+ const api = new SuperXAPI(getConfig());
1421
+ printJson(await api.draftReply(body));
1422
+ }
1423
+
1424
+ // src/commands/scheduled.ts
1425
+ async function scheduledList(argv) {
1426
+ const api = new SuperXAPI(getConfig());
1427
+ printJson(
1428
+ await api.listScheduled({
1429
+ account_id: argv.account,
1430
+ status: argv.status,
1431
+ tags: argv.tags,
1432
+ from: argv.from,
1433
+ to: argv.to,
1434
+ limit: argv.limit,
1435
+ page: argv.page
615
1436
  })
616
1437
  );
617
1438
  }
@@ -718,6 +1539,40 @@ function applyAdvancedFlags(argv, body) {
718
1539
  note("--auto-plug-threshold requires --auto-plug <templateId>.");
719
1540
  process.exit(1);
720
1541
  }
1542
+ const dmMessage = argv["auto-dm-message"];
1543
+ const dmTriggers = argv["auto-dm-triggers"];
1544
+ const dmMax = numericFlag("auto-dm-max", argv["auto-dm-max"]);
1545
+ const dmBatch = argv["auto-dm-batch"];
1546
+ const dmOff = argv["auto-dm"] === false;
1547
+ if (dmOff) {
1548
+ if (dmMessage !== void 0 || dmTriggers !== void 0 || typeof dmMax === "number" || dmBatch !== void 0) {
1549
+ note("--auto-dm-* flags cannot be combined with --no-auto-dm.");
1550
+ process.exit(1);
1551
+ }
1552
+ body.auto_dm = null;
1553
+ } else if (typeof dmMessage === "string" && dmMessage.length > 0) {
1554
+ const autoDm = { message: dmMessage };
1555
+ if (typeof dmTriggers === "string") {
1556
+ const wanted = dmTriggers.split(",").map((t) => t.trim().toLowerCase()).filter(Boolean);
1557
+ const unknown = wanted.filter(
1558
+ (t) => t !== "reply" && t !== "repost" && t !== "retweet"
1559
+ );
1560
+ if (unknown.length > 0 || wanted.length === 0) {
1561
+ note("--auto-dm-triggers takes a comma list of reply,repost (retweet is accepted as an alias of repost).");
1562
+ process.exit(1);
1563
+ }
1564
+ autoDm.triggers = {
1565
+ reply: wanted.includes("reply"),
1566
+ repost: wanted.includes("repost") || wanted.includes("retweet")
1567
+ };
1568
+ }
1569
+ if (typeof dmMax === "number") autoDm.max_dms = dmMax;
1570
+ if (typeof dmBatch === "boolean") autoDm.batch_mode = dmBatch;
1571
+ body.auto_dm = autoDm;
1572
+ } else if (dmTriggers !== void 0 || typeof dmMax === "number" || dmBatch !== void 0) {
1573
+ note("--auto-dm-triggers, --auto-dm-max and --auto-dm-batch require --auto-dm-message <text>.");
1574
+ process.exit(1);
1575
+ }
721
1576
  if (typeof argv["super-followers"] === "boolean") {
722
1577
  body.super_followers_only = argv["super-followers"];
723
1578
  }
@@ -832,10 +1687,118 @@ async function scheduledDelete(argv) {
832
1687
  const api = new SuperXAPI(getConfig());
833
1688
  printJson(await api.deleteScheduled(argv.id));
834
1689
  }
1690
+ async function postsPublish(argv) {
1691
+ const idempotencyKey = argv["idempotency-key"];
1692
+ if (typeof idempotencyKey !== "string" || idempotencyKey.length === 0) {
1693
+ note("--idempotency-key is required for posts:publish. Reuse the SAME key when retrying; only use a new key for new content.");
1694
+ process.exit(1);
1695
+ return;
1696
+ }
1697
+ const parts = (argv.part || []).filter((p) => typeof p === "string");
1698
+ const sourceCount = [argv.text, parts.length > 0 ? "p" : void 0, argv["parts-json"]].filter(
1699
+ (v) => v !== void 0
1700
+ ).length;
1701
+ if (sourceCount > 1) {
1702
+ note("Use exactly one of --text (single post), --part (thread), or --parts-json.");
1703
+ process.exit(1);
1704
+ }
1705
+ if (sourceCount === 0) {
1706
+ note("Provide --text for a single post, --part flags for a thread, or --parts-json.");
1707
+ process.exit(1);
1708
+ }
1709
+ if (argv.media !== void 0 && !argv.text) {
1710
+ note("--media applies to the --text single-post form. For threads, put media in --parts-json.");
1711
+ process.exit(1);
1712
+ }
1713
+ const body = { scheduled_for: "now" };
1714
+ if (argv["parts-json"] !== void 0) {
1715
+ body.parts = parsePartsJson(argv["parts-json"]);
1716
+ } else if (argv.text) {
1717
+ const media = mediaFromFlags(argv.media, argv["alt-text"]);
1718
+ if (media) {
1719
+ body.parts = [{ text: argv.text, media }];
1720
+ } else {
1721
+ body.text = argv.text;
1722
+ }
1723
+ } else {
1724
+ body.parts = parts.map((text) => ({ text }));
1725
+ }
1726
+ const tags = (argv.tag || []).filter((t) => typeof t === "string" && t.length > 0);
1727
+ if (tags.length > 0) body.tags = tags;
1728
+ applyAdvancedFlags(argv, body);
1729
+ if (argv.account) body.account_id = argv.account;
1730
+ const api = new SuperXAPI(getConfig());
1731
+ const { json, replayed } = await api.publishNow(body, idempotencyKey);
1732
+ if (replayed) {
1733
+ note("Idempotency replay: this key was already published; returning the original result. Nothing was posted twice.");
1734
+ printJson({ ...json, replayed: true });
1735
+ return;
1736
+ }
1737
+ printJson(json);
1738
+ }
1739
+ function idsFromFlag(name, raw) {
1740
+ const ids = (raw || "").split(",").map((id) => id.trim()).filter(Boolean);
1741
+ if (ids.length === 0) {
1742
+ note(`--${name} must be a comma list of post ids (from scheduled:list).`);
1743
+ process.exit(1);
1744
+ }
1745
+ return Array.from(new Set(ids));
1746
+ }
1747
+ async function scheduledBulkRetime(argv) {
1748
+ const raw = argv["moves-json"];
1749
+ if (typeof raw !== "string" || raw.length === 0) {
1750
+ note(`--moves-json is required, e.g. '[{"id":"abc","scheduled_for":"2026-09-08T15:00:00Z"}]'`);
1751
+ process.exit(1);
1752
+ return;
1753
+ }
1754
+ let moves;
1755
+ try {
1756
+ moves = JSON.parse(raw);
1757
+ } catch {
1758
+ note(`--moves-json must be valid JSON, e.g. '[{"id":"abc","scheduled_for":"2026-09-08T15:00:00Z"}]'`);
1759
+ process.exit(1);
1760
+ return;
1761
+ }
1762
+ if (!Array.isArray(moves) || moves.length === 0 || moves.some((m) => !m || typeof m !== "object" || typeof m.id !== "string" || typeof m.scheduled_for !== "string")) {
1763
+ note("--moves-json must be a non-empty array of { id, scheduled_for } objects.");
1764
+ process.exit(1);
1765
+ }
1766
+ const body = { moves };
1767
+ if (argv.account) body.account_id = argv.account;
1768
+ const api = new SuperXAPI(getConfig());
1769
+ printJson(await api.bulkRetimeScheduled(body));
1770
+ }
1771
+ async function scheduledBulkAutoRetweet(argv) {
1772
+ const ids = idsFromFlag("ids", argv.ids);
1773
+ const afterHours = numericFlag("auto-retweet", argv["auto-retweet"]);
1774
+ if (typeof afterHours !== "number") {
1775
+ note("--auto-retweet <hours> is required (1-12).");
1776
+ process.exit(1);
1777
+ return;
1778
+ }
1779
+ const removeAfterHours = numericFlag("auto-retweet-remove", argv["auto-retweet-remove"]);
1780
+ const body = {
1781
+ ids,
1782
+ auto_retweet: {
1783
+ after_hours: afterHours,
1784
+ ...typeof removeAfterHours === "number" ? { remove_after_hours: removeAfterHours } : {}
1785
+ }
1786
+ };
1787
+ if (argv.account) body.account_id = argv.account;
1788
+ const api = new SuperXAPI(getConfig());
1789
+ printJson(await api.bulkEnableAutoRetweet(body));
1790
+ }
1791
+ async function scheduledBulkDelete(argv) {
1792
+ const ids = idsFromFlag("ids", argv.ids);
1793
+ const body = { ids };
1794
+ if (argv.account) body.account_id = argv.account;
1795
+ const api = new SuperXAPI(getConfig());
1796
+ printJson(await api.bulkDeleteScheduled(body));
1797
+ }
835
1798
 
836
1799
  // src/commands/media.ts
837
- var import_fs = require("fs");
838
- var import_path = __toESM(require("path"));
1800
+ var import_fs2 = require("fs");
1801
+ var import_path2 = __toESM(require("path"));
839
1802
  function sniffImageType(bytes) {
840
1803
  if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) {
841
1804
  return "image/jpeg";
@@ -861,19 +1824,19 @@ var EXT_TYPES = {
861
1824
  async function mediaUpload(argv) {
862
1825
  let buffer;
863
1826
  try {
864
- buffer = (0, import_fs.readFileSync)(argv.file);
1827
+ buffer = (0, import_fs2.readFileSync)(argv.file);
865
1828
  } catch (err) {
866
1829
  note(`Error: could not read ${argv.file} (${err?.message || err})`);
867
1830
  process.exit(1);
868
1831
  return;
869
1832
  }
870
- const fileType = sniffImageType(buffer) || EXT_TYPES[import_path.default.extname(argv.file).toLowerCase()];
1833
+ const fileType = sniffImageType(buffer) || EXT_TYPES[import_path2.default.extname(argv.file).toLowerCase()];
871
1834
  if (!fileType) {
872
1835
  note("Error: unsupported file type. Supported images: JPG, PNG, WEBP, GIF.");
873
1836
  process.exit(1);
874
1837
  return;
875
1838
  }
876
- const filename = import_path.default.basename(argv.file);
1839
+ const filename = import_path2.default.basename(argv.file);
877
1840
  const api = new SuperXAPI(getConfig());
878
1841
  const created = await api.createMediaUpload({
879
1842
  filename,
@@ -938,7 +1901,7 @@ async function tagsDelete(argv) {
938
1901
  }
939
1902
 
940
1903
  // src/commands/articles.ts
941
- var import_fs2 = require("fs");
1904
+ var import_fs3 = require("fs");
942
1905
  function resolveContent(argv) {
943
1906
  if (argv.content !== void 0 && argv.file !== void 0) {
944
1907
  note("Use either --content or --file, not both.");
@@ -947,7 +1910,7 @@ function resolveContent(argv) {
947
1910
  if (argv.content !== void 0) return argv.content;
948
1911
  if (argv.file !== void 0) {
949
1912
  try {
950
- return (0, import_fs2.readFileSync)(argv.file, "utf8");
1913
+ return (0, import_fs3.readFileSync)(argv.file, "utf8");
951
1914
  } catch (err) {
952
1915
  note(`Could not read ${argv.file}: ${err?.message || err}`);
953
1916
  process.exit(1);
@@ -955,7 +1918,7 @@ function resolveContent(argv) {
955
1918
  }
956
1919
  if (!process.stdin.isTTY) {
957
1920
  try {
958
- const piped = (0, import_fs2.readFileSync)(0, "utf8");
1921
+ const piped = (0, import_fs3.readFileSync)(0, "utf8");
959
1922
  if (piped.length > 0) return piped;
960
1923
  } catch {
961
1924
  }
@@ -999,7 +1962,7 @@ async function articlesUpdate(argv) {
999
1962
  if (argv.content !== void 0) body.content_markdown = argv.content;
1000
1963
  if (argv.file !== void 0) {
1001
1964
  try {
1002
- body.content_markdown = (0, import_fs2.readFileSync)(argv.file, "utf8");
1965
+ body.content_markdown = (0, import_fs3.readFileSync)(argv.file, "utf8");
1003
1966
  } catch (err) {
1004
1967
  note(`Could not read ${argv.file}: ${err?.message || err}`);
1005
1968
  process.exit(1);
@@ -1032,10 +1995,19 @@ async function articlesUnschedule(argv) {
1032
1995
  const api = new SuperXAPI(getConfig());
1033
1996
  printJson(await api.unscheduleArticle(argv.id));
1034
1997
  }
1998
+ async function articlesCoverStyles(argv) {
1999
+ const api = new SuperXAPI(getConfig());
2000
+ printJson(await api.listCoverStyles({ account_id: argv.account }));
2001
+ }
1035
2002
  async function articlesCover(argv) {
2003
+ if (argv.style !== void 0 && argv["style-id"] !== void 0) {
2004
+ note("Use either --style or --style-id, not both.");
2005
+ process.exit(1);
2006
+ }
1036
2007
  note("Generating a cover image. This spends AI credits and can take 60-100 seconds...");
1037
2008
  const body = {};
1038
2009
  if (argv.style !== void 0) body.style_text = argv.style;
2010
+ if (argv["style-id"] !== void 0) body.style_id = argv["style-id"];
1039
2011
  if (argv.attach === false) body.attach = false;
1040
2012
  const api = new SuperXAPI(getConfig());
1041
2013
  printJson(await api.generateArticleCover(argv.id, body));
@@ -1046,7 +2018,7 @@ async function contextGet(argv) {
1046
2018
  const api = new SuperXAPI(getConfig());
1047
2019
  printJson(await api.getContext({ account_id: argv.account }));
1048
2020
  }
1049
- function commaList(value) {
2021
+ function commaList2(value) {
1050
2022
  return value.split(",").map((s) => s.trim()).filter(Boolean);
1051
2023
  }
1052
2024
  function stringOrClear(value) {
@@ -1065,7 +2037,7 @@ async function contextSet(argv) {
1065
2037
  body.profile_description = profileDescription;
1066
2038
  }
1067
2039
  if (argv.interests !== void 0) {
1068
- body.interests = commaList(argv.interests);
2040
+ body.interests = commaList2(argv.interests);
1069
2041
  }
1070
2042
  if (argv.rules !== void 0) {
1071
2043
  body.rules = stringOrClear(argv.rules);
@@ -1082,7 +2054,7 @@ async function contextSet(argv) {
1082
2054
  }
1083
2055
  const voice = {};
1084
2056
  if (argv["favorite-creators"] !== void 0) {
1085
- voice.favorite_creators = commaList(argv["favorite-creators"]);
2057
+ voice.favorite_creators = commaList2(argv["favorite-creators"]);
1086
2058
  }
1087
2059
  if (typeof argv["own-posts-as-examples"] === "boolean") {
1088
2060
  voice.use_own_posts_as_examples = argv["own-posts-as-examples"];
@@ -1141,6 +2113,110 @@ async function contextProductsDelete(argv) {
1141
2113
  const api = new SuperXAPI(getConfig());
1142
2114
  printJson(await api.deleteContextProduct(id, { account_id: argv.account }));
1143
2115
  }
2116
+ async function contextProductsReplace(argv) {
2117
+ let products;
2118
+ try {
2119
+ products = JSON.parse(argv.json);
2120
+ } catch (err) {
2121
+ note(`--json is not valid JSON: ${err?.message || err}`);
2122
+ process.exit(1);
2123
+ }
2124
+ if (!Array.isArray(products)) {
2125
+ note(`--json must be a JSON array, e.g. '[{"url":"https://superx.so","name":"SuperX"}]' ('[]' removes every product).`);
2126
+ process.exit(1);
2127
+ }
2128
+ const body = { products };
2129
+ if (argv.account) body.account_id = argv.account;
2130
+ const api = new SuperXAPI(getConfig());
2131
+ printJson(await api.setContextProducts(body));
2132
+ }
2133
+ async function contextRegenerateStyleGuide(argv) {
2134
+ const body = {};
2135
+ if (argv.account) body.account_id = argv.account;
2136
+ const api = new SuperXAPI(getConfig());
2137
+ printJson(await api.regenerateStyleGuide(body));
2138
+ }
2139
+ async function contextScrapeProduct(argv) {
2140
+ const id = String(argv.id || "").trim();
2141
+ if (!id) {
2142
+ note("Provide the product id (from context:products). Run: superx context:scrape-product --help");
2143
+ process.exit(1);
2144
+ }
2145
+ const body = {};
2146
+ if (argv.account) body.account_id = argv.account;
2147
+ const api = new SuperXAPI(getConfig());
2148
+ printJson(await api.scrapeContextProduct(id, body));
2149
+ }
2150
+
2151
+ // src/commands/dm.ts
2152
+ var import_fs4 = __toESM(require("fs"));
2153
+ function readRecipients(source) {
2154
+ let raw;
2155
+ if (source === "-") {
2156
+ try {
2157
+ raw = import_fs4.default.readFileSync(0, "utf8");
2158
+ } catch (err) {
2159
+ note(`Could not read recipients from stdin: ${err?.message || err}`);
2160
+ process.exit(1);
2161
+ }
2162
+ } else {
2163
+ try {
2164
+ raw = import_fs4.default.readFileSync(source, "utf8");
2165
+ } catch (err) {
2166
+ note(`Could not read ${source}: ${err?.message || err}`);
2167
+ process.exit(1);
2168
+ }
2169
+ }
2170
+ let parsed;
2171
+ try {
2172
+ parsed = JSON.parse(raw);
2173
+ } catch (err) {
2174
+ note(`--recipients must be JSON: ${err?.message || err}`);
2175
+ process.exit(1);
2176
+ }
2177
+ const list = Array.isArray(parsed) ? parsed : parsed?.recipients;
2178
+ if (!Array.isArray(list) || list.length === 0) {
2179
+ note("--recipients must be a non-empty JSON array of { x_user_id, handle?, name?, message? }.");
2180
+ process.exit(1);
2181
+ }
2182
+ return list;
2183
+ }
2184
+ async function dmCampaign(argv) {
2185
+ const api = new SuperXAPI(getConfig());
2186
+ const recipients = readRecipients(argv.recipients);
2187
+ const body = { recipients };
2188
+ if (argv.message !== void 0) body.message = argv.message;
2189
+ if (argv.spread === true) body.spread = true;
2190
+ if (argv.account) body.account_id = argv.account;
2191
+ const { json, replayed } = await api.queueDmCampaign(body, argv["idempotency-key"]);
2192
+ if (replayed) note("Replayed a previous response for this Idempotency-Key. Nothing new was queued.");
2193
+ printJson(json);
2194
+ note("Queued only. The SuperX app sends these within your DM limits; cancel the unsent ones with dm:cancel.");
2195
+ }
2196
+ async function dmCampaignStatus(argv) {
2197
+ const api = new SuperXAPI(getConfig());
2198
+ printJson(await api.getDmCampaign(argv.id, { account_id: argv.account }));
2199
+ }
2200
+ async function dmCancel(argv) {
2201
+ const api = new SuperXAPI(getConfig());
2202
+ printJson(await api.cancelDmCampaign(argv.id, { account_id: argv.account }));
2203
+ }
2204
+ async function dmQueue(argv) {
2205
+ const api = new SuperXAPI(getConfig());
2206
+ printJson(
2207
+ await api.listDmQueue({
2208
+ limit: argv.limit,
2209
+ offset: argv.offset,
2210
+ status: argv.status,
2211
+ campaign_id: argv.campaign,
2212
+ account_id: argv.account
2213
+ })
2214
+ );
2215
+ }
2216
+ async function dmLimits(argv) {
2217
+ const api = new SuperXAPI(getConfig());
2218
+ printJson(await api.getDmLimits({ account_id: argv.account }));
2219
+ }
1144
2220
 
1145
2221
  // src/commands/queue.ts
1146
2222
  async function queueGet(argv) {
@@ -1219,6 +2295,20 @@ var advancedSettingsOptions = (y) => y.option("auto-retweet", {
1219
2295
  type: "string"
1220
2296
  }).option("auto-plug-threshold", {
1221
2297
  describe: "Likes threshold for --auto-plug: the reply posts once the post hits this many likes"
2298
+ }).option("auto-dm-message", {
2299
+ describe: "Auto DM text (1-1000) sent to people who engage with the post; --no-auto-dm turns it off",
2300
+ type: "string"
2301
+ }).option("auto-dm-triggers", {
2302
+ describe: "Who gets the auto DM: comma list of reply,repost (retweet = repost). Default reply",
2303
+ type: "string"
2304
+ }).option("auto-dm-max", {
2305
+ describe: "Most people to auto DM for this post (1-100, default 100)"
2306
+ }).option("auto-dm-batch", {
2307
+ describe: "Send the auto DMs in one batch instead of as engagement arrives",
2308
+ type: "boolean"
2309
+ }).option("auto-dm", {
2310
+ describe: "Only the negated form is used: --no-auto-dm turns Auto DM off for this post",
2311
+ type: "boolean"
1222
2312
  }).option("super-followers", {
1223
2313
  describe: "Post to Super Followers only (--no-super-followers turns it off)",
1224
2314
  type: "boolean"
@@ -1254,6 +2344,116 @@ var advancedSettingsOptions = (y) => y.option("auto-retweet", {
1254
2344
  "Account analytics: totals, daily series, follower change (default: last 30 days)",
1255
2345
  (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"),
1256
2346
  run(postsAnalytics)
2347
+ ).command(
2348
+ "posts:draft",
2349
+ "Write post drafts in your voice from a brief (nothing is scheduled; costs AI credits)",
2350
+ (y) => accountOption(y).option("brief", {
2351
+ describe: "What the post should say: the data, angle, or notes to write from (required, max 2000 chars)",
2352
+ type: "string"
2353
+ }).option("count", {
2354
+ describe: "How many drafts to write, 1 to 3 (default 1). Each one costs credits",
2355
+ type: "number"
2356
+ }).option("voice", {
2357
+ describe: "Whose voice to write in",
2358
+ type: "string",
2359
+ choices: ["mine", "creator", "hybrid"]
2360
+ }).option("creator", {
2361
+ describe: "X handle to borrow style from (needed for --voice creator or hybrid)",
2362
+ type: "string"
2363
+ }).option("mirror", {
2364
+ 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",
2365
+ type: "string"
2366
+ }).option("collection", {
2367
+ describe: "Format collection id to bias the picked shape toward; used only without --mirror",
2368
+ type: "string"
2369
+ }).option("instructions", {
2370
+ describe: "Extra style instructions for this batch (max 500 chars)",
2371
+ type: "string"
2372
+ }).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"),
2373
+ run(postsDraft)
2374
+ ).command(
2375
+ "posts:remix",
2376
+ "Rewrite a post in your voice, near or far from the original (nothing is posted; costs AI credits)",
2377
+ (y) => accountOption(y).option("text", {
2378
+ describe: "The post to remix (required, max 4000 chars)",
2379
+ type: "string",
2380
+ demandOption: true
2381
+ }).option("closeness", {
2382
+ describe: "0 keeps only the idea, 100 stays very close to the original wording (required)",
2383
+ type: "number",
2384
+ demandOption: true
2385
+ }).option("instructions", {
2386
+ describe: "Extra direction for this remix (max 500 chars)",
2387
+ type: "string"
2388
+ }).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(
2389
+ "Returns TEXT ONLY. Nothing is posted or scheduled: save the result with posts:draft or scheduled:create once you are happy with it."
2390
+ ),
2391
+ run(postsRemix)
2392
+ ).command(
2393
+ "tools:inline-edit",
2394
+ "Edit one selected piece of a post, keeping the surrounding style (costs AI credits)",
2395
+ (y) => accountOption(y).option("text", {
2396
+ describe: "The selected piece to edit (required, max 4000 chars)",
2397
+ type: "string",
2398
+ demandOption: true
2399
+ }).option("full", {
2400
+ describe: "The whole post the selection sits in, so the edit matches its style",
2401
+ type: "string"
2402
+ }).option("instruction", { describe: "Free-text direction, e.g. 'make this one line'", type: "string" }).option("type", {
2403
+ describe: "A preset edit instead of, or alongside, --instruction",
2404
+ type: "string",
2405
+ choices: [
2406
+ "grammar",
2407
+ "translate",
2408
+ "hook",
2409
+ "details",
2410
+ "concise",
2411
+ "engaging",
2412
+ "humorous",
2413
+ "creative",
2414
+ "sarcastic",
2415
+ "inspirational"
2416
+ ]
2417
+ }).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."),
2418
+ run(toolsInlineEdit)
2419
+ ).command(
2420
+ "tools:rephrase",
2421
+ "Rewrite a post one preset way (costs AI credits)",
2422
+ (y) => accountOption(y).option("type", {
2423
+ describe: "Which rewrite to apply. The style presets use your voice; the mechanical ones do not",
2424
+ type: "string",
2425
+ demandOption: true,
2426
+ choices: [
2427
+ "improve",
2428
+ "grammar",
2429
+ "translate",
2430
+ "hook",
2431
+ "details",
2432
+ "clarity",
2433
+ "engaging",
2434
+ "humorous",
2435
+ "positive",
2436
+ "creative",
2437
+ "sarcastic",
2438
+ "inspirational",
2439
+ "concise"
2440
+ ]
2441
+ }).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."),
2442
+ run(toolsRephrase)
2443
+ ).command(
2444
+ "tools:factcheck",
2445
+ "Check a statement against a web search and report true, false or unknown (costs AI credits)",
2446
+ (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(
2447
+ "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."
2448
+ ),
2449
+ run(toolsFactcheck)
2450
+ ).command(
2451
+ "tools:predict",
2452
+ "Score two versions of a post against what the timeline rewards (costs AI credits)",
2453
+ (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(
2454
+ "The scores are a model's opinion, useful for comparing two drafts against each other, not a prediction of real reach."
2455
+ ),
2456
+ run(toolsPredict)
1257
2457
  ).command(
1258
2458
  "replies:list",
1259
2459
  "List replies the account has sent (newest first)",
@@ -1277,6 +2477,31 @@ var advancedSettingsOptions = (y) => y.option("auto-retweet", {
1277
2477
  choices: ["relevant", "recent", "likes", "reposts", "impressions", "outlier"]
1278
2478
  }).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"),
1279
2479
  run(inspirationSearch)
2480
+ ).command(
2481
+ "inspiration:media [query]",
2482
+ "Search the cross-platform media index behind the app's Inspiration > Media tab",
2483
+ (y) => y.positional("query", {
2484
+ describe: "What to search for. Omit to browse the newest media",
2485
+ type: "string"
2486
+ }).option("platforms", {
2487
+ describe: "Comma-separated platforms: x, instagram, youtube, threads, reddit, linkedin",
2488
+ type: "string"
2489
+ }).option("time-filter", {
2490
+ describe: "How recent the media must be",
2491
+ type: "string",
2492
+ choices: ["all", "24h", "7d", "30d"]
2493
+ }).option("media-type", {
2494
+ describe: "Media kind",
2495
+ type: "string",
2496
+ choices: ["all", "video", "image"]
2497
+ }).option("content-type", {
2498
+ describe: "Free-text content-type label as stored in the index; an unknown label returns nothing and still costs a search",
2499
+ type: "string"
2500
+ }).option("limit", {
2501
+ describe: "Items to return (1-120, default 20). No pagination: 120 is one query's maximum",
2502
+ type: "number"
2503
+ }).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"),
2504
+ run(inspirationMedia)
1280
2505
  ).command(
1281
2506
  "contacts:list",
1282
2507
  "List the people who engage with you most",
@@ -1295,6 +2520,34 @@ var advancedSettingsOptions = (y) => y.option("auto-retweet", {
1295
2520
  choices: ["recent", "most_liked"]
1296
2521
  }),
1297
2522
  run(contactsReplies)
2523
+ ).command(
2524
+ "contacts:get <id>",
2525
+ "Show one person: profile, follower counts and the lists they are in (your known contacts only: engagers, contact-list members, signal leads)",
2526
+ (y) => accountOption(y).positional("id", { describe: "Numeric X user id (from contacts:list, lists:members or signals:leads)", type: "string" }).option("refresh", {
2527
+ describe: "Refresh the profile from X when the stored copy is stale (counts against the enrichment limit)",
2528
+ type: "boolean"
2529
+ }),
2530
+ run(contactsGet)
2531
+ ).command(
2532
+ "contacts:notes <id>",
2533
+ "List your private notes about one person (newest first)",
2534
+ (y) => accountOption(y).positional("id", { describe: "Numeric X user id", type: "string" }),
2535
+ run(contactsNotes)
2536
+ ).command(
2537
+ "contacts:notes:add <id>",
2538
+ "Write a private note about one person (never posted anywhere; known contacts only: engagers, contact-list members, signal leads)",
2539
+ (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"),
2540
+ run(contactsNotesAdd)
2541
+ ).command(
2542
+ "contacts:notes:update <id> <noteId>",
2543
+ "Rewrite one note (the new body fully replaces the old one)",
2544
+ (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 }),
2545
+ run(contactsNotesUpdate)
2546
+ ).command(
2547
+ "contacts:notes:delete <id> <noteId>",
2548
+ "Delete one note (note id from contacts:notes)",
2549
+ (y) => accountOption(y).positional("id", { describe: "Numeric X user id", type: "string" }).positional("noteId", { describe: "Note id (from contacts:notes)", type: "string" }),
2550
+ run(contactsNotesDelete)
1298
2551
  ).command(
1299
2552
  "lists:list",
1300
2553
  "List your contact lists (system lists are read-only)",
@@ -1315,11 +2568,229 @@ var advancedSettingsOptions = (y) => y.option("auto-retweet", {
1315
2568
  "Remove a member from a contact list (member id from lists:members)",
1316
2569
  (y) => y.positional("id", { describe: "List id (from lists:list)", type: "string" }).positional("memberId", { describe: "Member id (from lists:members)", type: "string" }),
1317
2570
  run(listsRemoveMember)
2571
+ ).command(
2572
+ "lists:create",
2573
+ "Create a contact list (names are not unique)",
2574
+ (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"),
2575
+ run(listsCreate)
2576
+ ).command(
2577
+ "lists:rename <id>",
2578
+ "Rename a contact list you created (system lists are read-only)",
2579
+ (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 }),
2580
+ run(listsRename)
2581
+ ).command(
2582
+ "lists:delete <id>",
2583
+ "Delete a contact list you created and its membership (the people stay)",
2584
+ (y) => accountOption(y).positional("id", { describe: "List id (from lists:list)", type: "string" }),
2585
+ run(listsDelete)
2586
+ ).command(
2587
+ "lists:add-members <id>",
2588
+ "Add up to 500 people at once by X user id (ids SuperX already knows)",
2589
+ (y) => accountOption(y).positional("id", { describe: "List id (from lists:list)", type: "string" }).option("x-user-ids", {
2590
+ describe: "Comma list of numeric X user ids (max 500); ids SuperX has never seen come back in not_found",
2591
+ type: "string",
2592
+ demandOption: true
2593
+ }).example("$0 lists:add-members abc123 --x-user-ids 44196397,1234567890", "Add two people by id"),
2594
+ run(listsAddMembers)
2595
+ ).command(
2596
+ "lists:remove-members <id>",
2597
+ "Remove up to 500 members at once by member id (from lists:members)",
2598
+ (y) => accountOption(y).positional("id", { describe: "List id (from lists:list)", type: "string" }).option("member-ids", {
2599
+ describe: "Comma list of member ids (max 500, from lists:members)",
2600
+ type: "string",
2601
+ demandOption: true
2602
+ }),
2603
+ run(listsRemoveMembers)
2604
+ ).command(
2605
+ "datasets:list",
2606
+ "List the audience collections Ask SuperX built in the app (kept for 30 days)",
2607
+ (y) => paginationOptions(y),
2608
+ run(datasetsList)
2609
+ ).command(
2610
+ "datasets:get <id>",
2611
+ "Read one dataset: status, counts, coverage (poll this while it collects)",
2612
+ (y) => y.positional("id", { describe: "Dataset id (from datasets:list)", type: "string" }),
2613
+ run(datasetsGet)
2614
+ ).command(
2615
+ "datasets:rows <id>",
2616
+ "Read a page of a ready dataset's rows, exactly as collected",
2617
+ (y) => paginationOptions(y).positional("id", {
2618
+ describe: "Dataset id (from datasets:list)",
2619
+ type: "string"
2620
+ }),
2621
+ run(datasetsRows)
2622
+ ).command(
2623
+ "datasets:export <id>",
2624
+ "Download a ready dataset as CSV (XLSX stays in the SuperX app)",
2625
+ (y) => y.positional("id", { describe: "Dataset id (from datasets:list)", type: "string" }).option("out", {
2626
+ describe: "File to write; defaults to the server's filename in the current directory. Use - to stream the CSV to stdout",
2627
+ type: "string",
2628
+ // Without requiresArg, yargs drops a lone `-` and strict mode
2629
+ // rejects it as an unknown argument, so `--out -` would fail.
2630
+ requiresArg: true
2631
+ }).example("$0 datasets:export abc123", "Write superx-dataset-<title>-<date>.csv here").example("$0 datasets:export abc123 --out - | head", "Stream the CSV to stdout"),
2632
+ run(datasetsExport)
2633
+ ).command(
2634
+ "datasets:add-to-list <id>",
2635
+ "Copy the people in a ready dataset into a contact list you created",
2636
+ (y) => accountOption(y).positional("id", { describe: "Dataset id (from datasets:list)", type: "string" }).option("list-id", {
2637
+ describe: "Contact list id (from lists:list) that receives the people",
2638
+ type: "string",
2639
+ demandOption: true
2640
+ }).example("$0 datasets:add-to-list abc123 --list-id def456", "Add the dataset's people to a list"),
2641
+ run(datasetsAddToList)
2642
+ ).command(
2643
+ "datasets:collect",
2644
+ "Collect an audience (or your own posts) into a new dataset",
2645
+ (y) => accountOption(y).option("source", {
2646
+ describe: "Who to collect",
2647
+ type: "string",
2648
+ choices: [
2649
+ "repliers",
2650
+ "quoters",
2651
+ "reposters",
2652
+ "list_members",
2653
+ "my_replies",
2654
+ "my_posts"
2655
+ ],
2656
+ demandOption: true
2657
+ }).option("target", {
2658
+ describe: "Post URL or id (repliers, quoters, reposters), or X list URL or id (list_members). Not used for my_posts / my_replies",
2659
+ type: "string"
2660
+ }).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", {
2661
+ describe: "Comma list: keep only rows whose reply, quote or post text contains one of these",
2662
+ type: "string"
2663
+ }).option("bio-keywords", {
2664
+ describe: "Comma list: keep only people whose X bio contains one of these",
2665
+ type: "string"
2666
+ }).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", {
2667
+ describe: "Own posts only: which posts to keep when max-rows cuts the list",
2668
+ type: "string",
2669
+ choices: ["recent", "likes", "impressions"]
2670
+ }).option("wait", {
2671
+ describe: "Poll until a background collection is ready (up to 15 minutes)",
2672
+ type: "boolean"
2673
+ }).example(
2674
+ "$0 datasets:collect --source repliers --target https://x.com/me/status/123 --wait",
2675
+ "Collect everyone who replied and wait for it"
2676
+ ).epilogue(
2677
+ "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."
2678
+ ),
2679
+ run(datasetsCollect)
2680
+ ).command(
2681
+ "datasets:research",
2682
+ "Research people into outreach briefs saved as a dataset",
2683
+ (y) => accountOption(y).option("handles", {
2684
+ describe: "Comma list of X handles to research (with or without the @). Max 25",
2685
+ type: "string"
2686
+ }).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", {
2687
+ describe: "Optional steer, e.g. 'founders who might need audience-growth tooling'",
2688
+ type: "string"
2689
+ }).option("title", { describe: "Title for the briefs dataset", type: "string" }).option("wait", {
2690
+ describe: "Poll until a background research run is ready (up to 15 minutes)",
2691
+ type: "boolean"
2692
+ }).example(
2693
+ "$0 datasets:research --handles levelsio,naval --focus 'audience-growth tooling'",
2694
+ "Research two handles into briefs"
2695
+ ).epilogue(
2696
+ "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."
2697
+ ),
2698
+ run(datasetsResearch)
2699
+ ).command(
2700
+ "datasets:outreach-drafts <id>",
2701
+ "Draft one personalized message per person in a research dataset (text only, nothing is sent)",
2702
+ (y) => accountOption(y).positional("id", { describe: "Research dataset id (from datasets:research)", type: "string" }).option("format", {
2703
+ describe: "The template or example message every draft should follow (10-1000 chars)",
2704
+ type: "string",
2705
+ demandOption: true
2706
+ }).option("instructions", {
2707
+ describe: "Optional extra steer: tone, what to emphasize, what to avoid",
2708
+ type: "string"
2709
+ }).example(
2710
+ '$0 datasets:outreach-drafts abc123 --format "hey [first]! <personalization>. would love to trade notes"',
2711
+ "Draft messages onto the dataset"
2712
+ ).epilogue(
2713
+ "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."
2714
+ ),
2715
+ run(datasetsOutreachDrafts)
2716
+ ).command(
2717
+ "datasets:refine <id>",
2718
+ "Filter a dataset by what each person wrote, into a new dataset",
2719
+ (y) => accountOption(y).positional("id", { describe: "Source dataset id (from datasets:list)", type: "string" }).option("criterion", {
2720
+ describe: "What the rows to match look like, judged on each row's own text",
2721
+ type: "string",
2722
+ demandOption: true
2723
+ }).option("keep", {
2724
+ describe: "Keep the rows that MATCH (default). --no-keep keeps the rows that do not",
2725
+ type: "boolean"
2726
+ }).option("sort", {
2727
+ describe: "Sort the kept rows descending before the limit",
2728
+ type: "string",
2729
+ choices: ["followers", "likes", "none"]
2730
+ }).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", {
2731
+ describe: "Poll until a background refinement is ready (up to 15 minutes)",
2732
+ type: "boolean"
2733
+ }).example(
2734
+ "$0 datasets:refine abc123 --criterion 'supportive or neutral, not hostile' --sort followers",
2735
+ "Keep the friendly repliers, best-followed first"
2736
+ ).epilogue(
2737
+ "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."
2738
+ ),
2739
+ run(datasetsRefine)
2740
+ ).command(
2741
+ "x:post <id|url>",
2742
+ "Look up one public X post live (URL or numeric id)",
2743
+ (y) => y.positional("id", { describe: "Post URL or bare numeric post id", type: "string" }).option("quotes", {
2744
+ describe: "Also fetch a page of the posts quoting it (costs a second unit)",
2745
+ type: "boolean"
2746
+ }).example("$0 x:post https://x.com/levelsio/status/1938765432109876543", "Read that post"),
2747
+ run(xPost)
2748
+ ).command(
2749
+ "x:replies <id|url>",
2750
+ "Top replies by likes to a public X post, live (a sample, not every reply)",
2751
+ (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"),
2752
+ run(xReplies)
2753
+ ).command(
2754
+ "x:user <handle>",
2755
+ "Look up one public X profile live (@ optional)",
2756
+ (y) => y.positional("handle", { describe: "The account's @handle", type: "string" }).example("$0 x:user @levelsio", "Read that profile"),
2757
+ run(xUser)
2758
+ ).command(
2759
+ "x:user-posts <handle>",
2760
+ "One live page of an account's latest posts, newest first",
2761
+ (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", {
2762
+ describe: "Include reposts (default). Use --no-reposts for own posts only",
2763
+ type: "boolean",
2764
+ default: true
2765
+ }).example("$0 x:user-posts levelsio --no-reposts", "Their own recent posts, no reposts"),
2766
+ run(xUserPosts)
1318
2767
  ).command(
1319
2768
  "signals:agents",
1320
2769
  "List your signal agents (automated lead finders) with their watched signals",
1321
2770
  (y) => accountOption(y),
1322
2771
  run(signalsAgents)
2772
+ ).command(
2773
+ "signals:search",
2774
+ "Search X now for people matching an audience description (saves nothing)",
2775
+ (y) => accountOption(y).option("keywords", {
2776
+ describe: "Plain-language phrases these people would post, comma-separated for alternatives. No search operators",
2777
+ type: "string",
2778
+ demandOption: true
2779
+ }).option("icp", {
2780
+ describe: "Who counts as a good lead, in 1-2 sentences: role, domain, and the intent that qualifies them",
2781
+ type: "string",
2782
+ demandOption: true
2783
+ }).option("precision", {
2784
+ describe: "high = only confident matches; discovery (default) = broader adjacent matches",
2785
+ type: "string",
2786
+ choices: ["high", "discovery"]
2787
+ }).option("max", { describe: "Leads to return at most (1-30, default 10)", type: "number" }).example(
2788
+ "$0 signals:search --keywords 'losing customers to churn' --icp 'B2B SaaS founders worried about retention'",
2789
+ "Find people posting about churn right now"
2790
+ ).epilogue(
2791
+ "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."
2792
+ ),
2793
+ run(signalsSearch)
1323
2794
  ).command(
1324
2795
  "signals:leads",
1325
2796
  "List the leads your signal agents have found (newest first)",
@@ -1346,6 +2817,10 @@ var advancedSettingsOptions = (y) => y.option("auto-retweet", {
1346
2817
  describe: "Plain-language search description to watch (repeat the flag, 1-5); omit to auto-suggest from --icp",
1347
2818
  type: "string",
1348
2819
  array: true
2820
+ }).option("signal", {
2821
+ describe: 'Non-keyword signal as "type:target" (repeat the flag). Types: keyword, profile, follower, list. Combined with --keyword, at most 5',
2822
+ type: "string",
2823
+ array: true
1349
2824
  }).option("idempotency-key", {
1350
2825
  describe: "Idempotency-Key header (max 64 chars); retries with the same key return the original result",
1351
2826
  type: "string"
@@ -1355,8 +2830,77 @@ var advancedSettingsOptions = (y) => y.option("auto-retweet", {
1355
2830
  ).example(
1356
2831
  '$0 signals:create-agent --name "Agency leads" --icp "Marketing agency owners struggling with reporting"',
1357
2832
  "Create an agent with auto-suggested keywords and an auto-created list"
2833
+ ).example(
2834
+ '$0 signals:create-agent --name "Naval orbit" --icp "..." --signal "profile:@naval" --signal "follower:@naval"',
2835
+ "Create an agent watching an account and its followers"
1358
2836
  ),
1359
2837
  run(signalsCreateAgent)
2838
+ ).command(
2839
+ "signals:update-agent <id>",
2840
+ "Edit a signal agent's name, ICP, precision mode, destination list or status",
2841
+ (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", {
2842
+ describe: "high = fewer, stricter matches; discovery = broader net",
2843
+ type: "string",
2844
+ choices: ["high", "discovery"]
2845
+ }).option("list-id", {
2846
+ describe: "Contact list id (from lists:list) that receives the leads",
2847
+ type: "string"
2848
+ }).option("status", {
2849
+ describe: "active = resume finding leads; paused = stop",
2850
+ type: "string",
2851
+ choices: ["active", "paused"]
2852
+ }).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"),
2853
+ run(signalsUpdateAgent)
2854
+ ).command(
2855
+ "signals:add-signal <id>",
2856
+ "Add one thing for an agent to watch (a search, an account, its followers, or an X list)",
2857
+ (y) => accountOption(y).positional("id", { describe: "Agent id (from signals:agents)", type: "number" }).option("type", {
2858
+ describe: "What to watch",
2859
+ type: "string",
2860
+ choices: ["keyword_watch", "profile_watch", "follower_watch", "list_watch"],
2861
+ demandOption: true
2862
+ }).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"),
2863
+ run(signalsAddSignal)
2864
+ ).command(
2865
+ "signals:remove-signal <id> <signalId>",
2866
+ "Remove one signal from an agent (leads it already found stay)",
2867
+ (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" }),
2868
+ run(signalsRemoveSignal)
2869
+ ).command(
2870
+ "signals:feedback <leadId>",
2871
+ "Record your verdict on one lead: --fit, --not-fit or --clear",
2872
+ (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."),
2873
+ run(signalsFeedback)
2874
+ ).command(
2875
+ "signals:suggest-keywords",
2876
+ "Turn an audience description into 2 or 3 keyword-watch ideas (free, creates nothing)",
2877
+ (y) => accountOption(y).option("icp", {
2878
+ describe: "Who the ideal customer is, in plain language (3 to 500 chars)",
2879
+ type: "string",
2880
+ demandOption: true
2881
+ }).example(
2882
+ `$0 signals:suggest-keywords --icp "B2B SaaS founders worried about churn"`,
2883
+ "Ideas to watch for that audience"
2884
+ ).epilogue(
2885
+ "Saves nothing and costs no AI credits. Pass a suggestion you like to signals:create-agent --keyword, or signals:add-signal --type keyword_watch."
2886
+ ),
2887
+ run(signalsSuggestKeywords)
2888
+ ).command(
2889
+ "signals:expand-icp",
2890
+ "Build the scoring rubric a signal agent qualifies people with, from a description or a website (free)",
2891
+ (y) => accountOption(y).option("text", {
2892
+ describe: "Who the ideal customer is, in plain language (3 to 500 chars)",
2893
+ type: "string"
2894
+ }).option("url", {
2895
+ describe: "A product or company website to read instead; also returns a description and keyword ideas",
2896
+ type: "string"
2897
+ }).example(
2898
+ `$0 signals:expand-icp --text "Indie founders building SaaS in public"`,
2899
+ "Rubric from a description"
2900
+ ).example("$0 signals:expand-icp --url superx.so", "Rubric, description and keyword ideas from a site").epilogue(
2901
+ "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."
2902
+ ),
2903
+ run(signalsExpandIcp)
1360
2904
  ).command(
1361
2905
  "signals:pause-agent <id>",
1362
2906
  "Pause a signal agent (it stops finding leads until resumed)",
@@ -1372,6 +2916,108 @@ var advancedSettingsOptions = (y) => y.option("auto-retweet", {
1372
2916
  "Delete a signal agent (its saved leads and contact list stay untouched)",
1373
2917
  (y) => y.positional("id", { describe: "Agent id (from signals:agents)", type: "number" }),
1374
2918
  run(signalsDeleteAgent)
2919
+ ).command(
2920
+ "engage:feeds",
2921
+ "List the Engage feeds set up in the app",
2922
+ (y) => accountOption(y).example("$0 engage:feeds", "Feed ids, types, and what one fetch costs").example("$0 engage:feeds --account <account-id>", "Feeds saved on a linked account"),
2923
+ run(engageFeeds)
2924
+ ).command(
2925
+ "engage:posts <feedId>",
2926
+ "Fetch the posts an Engage feed surfaces (for review; replies are sent by a person in the app)",
2927
+ (y) => accountOption(y).positional("feedId", { describe: "Feed id (from engage:feeds)", type: "string" }).option("limit", {
2928
+ describe: "Posts to return, 1-50 (default 20). Applies to keyword feeds; list feeds return one page (about 10 to 25 posts) and --limit only trims it",
2929
+ type: "number"
2930
+ }).option("mode", {
2931
+ describe: "top = highest-signal posts first (default); latest = newest first",
2932
+ type: "string",
2933
+ choices: ["top", "latest"]
2934
+ }).option("fresh", {
2935
+ describe: "true = skip the cache and fetch new posts; false = allow cached posts (default)",
2936
+ type: "boolean"
2937
+ }).option("include-replied", {
2938
+ describe: "true = keep posts already replied to, skipped, or blocked and flag them with `replied`; false = leave them out (default)",
2939
+ type: "boolean"
2940
+ }).option("exclude", {
2941
+ describe: "Comma list of post ids to leave out (max 100); this is how you page: pass the ids you already have to get the next batch",
2942
+ type: "string"
2943
+ }).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"),
2944
+ run(engagePosts)
2945
+ ).command(
2946
+ "engage:feeds:create",
2947
+ "Save a new Engage feed (keywords, a public X list, or one of your contact lists)",
2948
+ (y) => accountOption(y).option("name", { describe: "Feed name (1-40 chars)", type: "string", demandOption: true }).option("keyword", {
2949
+ describe: "Search term for a keyword feed (repeat the flag, 1-5)",
2950
+ type: "string",
2951
+ array: true
2952
+ }).option("x-list", {
2953
+ describe: "Public X list id or x.com/i/lists link for an X list feed",
2954
+ type: "string"
2955
+ }).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(
2956
+ "Use exactly one source. A new feed does not become the feed the SuperX app has open. Up to 8 feeds per account."
2957
+ ),
2958
+ run(engageFeedsCreate)
2959
+ ).command(
2960
+ "engage:feeds:update <feedId>",
2961
+ "Rename an Engage feed, replace what it watches, or both",
2962
+ (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", {
2963
+ describe: "Replace the feed's keywords (repeat the flag, 1-5)",
2964
+ type: "string",
2965
+ array: true
2966
+ }).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."),
2967
+ run(engageFeedsUpdate)
2968
+ ).command(
2969
+ "engage:feeds:delete <feedId>",
2970
+ "Delete an Engage feed (if it was the open one, the first remaining feed takes over)",
2971
+ (y) => accountOption(y).positional("feedId", { describe: "Feed id (from engage:feeds)", type: "string" }),
2972
+ run(engageFeedsDelete)
2973
+ ).command(
2974
+ "engage:reply-draft",
2975
+ "Draft one reply to a post, in your voice (nothing is posted; costs AI credits)",
2976
+ (y) => accountOption(y).option("post", {
2977
+ describe: "X post id to reply to; the API reads it live (costs one live lookup). Use instead of --text",
2978
+ type: "string"
2979
+ }).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", {
2980
+ describe: "What you want the reply to convey (max 2000 chars)",
2981
+ type: "string"
2982
+ }).option("tone", {
2983
+ describe: "Register for the reply",
2984
+ type: "string",
2985
+ choices: ["engaging", "humorous", "creative", "sarcastic", "inspirational", "concise"]
2986
+ }).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(
2987
+ "Provide exactly one of --post or --text. The draft is TEXT: nothing is posted or sent, a person reviews it and posts it."
2988
+ ),
2989
+ run(engageReplyDraft)
2990
+ ).command(
2991
+ "engage:mentions",
2992
+ "The posts @-mentioning you right now, each with the post it replies to",
2993
+ (y) => accountOption(y).option("sort", {
2994
+ describe: "latest (default, newest first) or top (most engaged first)",
2995
+ type: "string",
2996
+ choices: ["latest", "top"]
2997
+ }).option("include-replied", {
2998
+ describe: "Keep mentions you already replied to on X, flagged replied",
2999
+ type: "boolean"
3000
+ }).option("cursor", {
3001
+ describe: "next_cursor from the previous call, to read the next page",
3002
+ type: "string"
3003
+ }).example("$0 engage:mentions --sort top", "The most engaged mentions first").epilogue(
3004
+ "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."
3005
+ ),
3006
+ run(engageMentions)
3007
+ ).command(
3008
+ "audience:list <kind>",
3009
+ "A page of your followers, following, repliers or reposters",
3010
+ (y) => accountOption(y).positional("kind", {
3011
+ describe: "followers, following, repliers or reposters",
3012
+ type: "string",
3013
+ choices: ["followers", "following", "repliers", "reposters"]
3014
+ }).option("cursor", {
3015
+ describe: "next_cursor from the previous call, to read the next page",
3016
+ type: "string"
3017
+ }).option("limit", { describe: "People per page (1-100, default 50)", type: "number" }).example("$0 audience:list followers --limit 100", "The first 100 followers").epilogue(
3018
+ "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."
3019
+ ),
3020
+ run(audienceList)
1375
3021
  ).command(
1376
3022
  "scheduled:list",
1377
3023
  "List drafts and the scheduled queue",
@@ -1450,6 +3096,49 @@ var advancedSettingsOptions = (y) => y.option("auto-retweet", {
1450
3096
  "Delete a draft or scheduled post by id",
1451
3097
  (y) => y.positional("id", { describe: "Post id (from scheduled:list or scheduled:create)", type: "string" }),
1452
3098
  run(scheduledDelete)
3099
+ ).command(
3100
+ "posts:publish",
3101
+ "Publish a post or thread to X RIGHT NOW (irreversible; --idempotency-key required)",
3102
+ (y) => advancedSettingsOptions(accountOption(y)).option("text", { describe: "Text for a single post", type: "string" }).option("part", {
3103
+ describe: "Thread part text (repeat the flag, 1-25 parts, in order)",
3104
+ type: "string",
3105
+ array: true
3106
+ }).option("media", {
3107
+ describe: "Comma list of image object_keys (from media:upload) to attach; single-post form only (max 4 images or 1 GIF)",
3108
+ type: "string"
3109
+ }).option("alt-text", {
3110
+ describe: "Accessibility description for the attached image (single --media key only, max 1000 chars)",
3111
+ type: "string"
3112
+ }).option("parts-json", {
3113
+ describe: 'Full parts array as JSON for threads with media: [{"text":"...","media":[{"object_key":"...","alt_text":"..."}]}]',
3114
+ type: "string"
3115
+ }).option("tag", {
3116
+ describe: "Tag id to assign (repeat the flag, max 20; ids from tags:list)",
3117
+ type: "string",
3118
+ array: true
3119
+ }).option("idempotency-key", {
3120
+ 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",
3121
+ type: "string"
3122
+ }).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"),
3123
+ run(postsPublish)
3124
+ ).command(
3125
+ "scheduled:bulk-retime",
3126
+ "Move up to 500 queued posts to new times in one transaction",
3127
+ (y) => accountOption(y).option("moves-json", {
3128
+ describe: 'Moves as JSON: [{"id":"abc","scheduled_for":"2026-09-08T15:00:00Z"}] (max 500, each time 60s+ ahead)',
3129
+ type: "string"
3130
+ }).example(`$0 scheduled:bulk-retime --moves-json '[{"id":"abc","scheduled_for":"2026-09-08T15:00:00Z"}]'`, "Retime one queued post"),
3131
+ run(scheduledBulkRetime)
3132
+ ).command(
3133
+ "scheduled:bulk-auto-retweet",
3134
+ "Turn Auto Retweet on for up to 100 queued posts that do not have it yet",
3135
+ (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"),
3136
+ run(scheduledBulkAutoRetweet)
3137
+ ).command(
3138
+ "scheduled:bulk-delete",
3139
+ "Delete up to 100 QUEUED posts and refund their post quota (sent posts and drafts are left alone)",
3140
+ (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"),
3141
+ run(scheduledBulkDelete)
1453
3142
  ).command(
1454
3143
  "plug-templates:list",
1455
3144
  "List your auto-plug reply templates (id, text, has_media) for --auto-plug",
@@ -1510,6 +3199,34 @@ var advancedSettingsOptions = (y) => y.option("auto-retweet", {
1510
3199
  "Remove a product by id (reversible by re-adding the same url)",
1511
3200
  (y) => accountOption(y).positional("id", { describe: "Product id (from context:products)", type: "string" }),
1512
3201
  run(contextProductsDelete)
3202
+ ).command(
3203
+ "context:products:replace",
3204
+ "Replace the WHOLE product list with a JSON array (max 5); products missing from it are removed",
3205
+ (y) => accountOption(y).option("json", {
3206
+ describe: `JSON array of products, e.g. '[{"url":"https://superx.so","name":"SuperX"}]' ('[]' removes every product)`,
3207
+ type: "string",
3208
+ demandOption: true
3209
+ }).example(
3210
+ `$0 context:products:replace --json '[{"url":"https://superx.so","name":"SuperX"}]'`,
3211
+ "Make SuperX the only product"
3212
+ ).epilogue(
3213
+ "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."
3214
+ ),
3215
+ run(contextProductsReplace)
3216
+ ).command(
3217
+ "context:regenerate-style-guide",
3218
+ "Rebuild the generated style guide from the account's recent posts (free, once an hour)",
3219
+ (y) => accountOption(y).example("$0 context:regenerate-style-guide", "Rewrite the guide for the main account").epilogue(
3220
+ "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."
3221
+ ),
3222
+ run(contextRegenerateStyleGuide)
3223
+ ).command(
3224
+ "context:scrape-product <id>",
3225
+ "Re-read a saved product's page and refresh its stored details (free)",
3226
+ (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(
3227
+ "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."
3228
+ ),
3229
+ run(contextScrapeProduct)
1513
3230
  ).command(
1514
3231
  "queue:get",
1515
3232
  "Show the account's posting schedule: predefined time slots and the timezone they run in",
@@ -1596,15 +3313,74 @@ var advancedSettingsOptions = (y) => y.option("auto-retweet", {
1596
3313
  "Pull a scheduled article back to draft (quota refunds)",
1597
3314
  (y) => y.positional("id", { describe: "Article id", type: "string" }),
1598
3315
  run(articlesUnschedule)
3316
+ ).command(
3317
+ "articles:cover-styles",
3318
+ "List the article cover styles saved in the SuperX app (ids for --style-id)",
3319
+ (y) => accountOption(y),
3320
+ run(articlesCoverStyles)
1599
3321
  ).command(
1600
3322
  "articles:cover <id>",
1601
3323
  "Generate an AI cover for an article (60-100s, spends AI credits)",
1602
- (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", {
3324
+ (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", {
3325
+ describe: "Saved cover style id (from articles:cover-styles); not with --style",
3326
+ type: "string"
3327
+ }).option("attach", {
1603
3328
  describe: "Attach the result as the article's cover (use --no-attach to skip)",
1604
3329
  type: "boolean",
1605
3330
  default: true
1606
- }),
3331
+ }).example("$0 articles:cover abc123 --style-id sty_9f2", "Generate in a saved style"),
1607
3332
  run(articlesCover)
3333
+ ).command(
3334
+ "dm:campaign",
3335
+ "Queue direct messages to a list of X users (nothing is sent by this command)",
3336
+ (y) => accountOption(y).option("recipients", {
3337
+ describe: 'JSON file of [{ "x_user_id", "handle"?, "name"?, "message"? }]; --recipients=- reads stdin',
3338
+ type: "string",
3339
+ demandOption: true
3340
+ }).option("message", {
3341
+ describe: "The shared message (1-1000). [name], [first] and [handle] are filled per recipient",
3342
+ type: "string"
3343
+ }).option("spread", {
3344
+ describe: "Spread what today's allowance cannot hold over the coming days instead of skipping it",
3345
+ type: "boolean"
3346
+ }).option("idempotency-key", {
3347
+ describe: "Reuse the same key on a retry so one campaign is never queued twice",
3348
+ type: "string"
3349
+ }).example(
3350
+ '$0 dm:campaign --recipients people.json --message "Hey [first], loved your post"',
3351
+ "Queue a campaign from a file"
3352
+ ).epilogue(
3353
+ "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."
3354
+ ),
3355
+ run(dmCampaign)
3356
+ ).command(
3357
+ "dm:campaign-status <id>",
3358
+ "Show one campaign's status counts and its individual messages",
3359
+ (y) => accountOption(
3360
+ y.positional("id", { describe: "Campaign id (from dm:campaign)", type: "string" })
3361
+ ),
3362
+ run(dmCampaignStatus)
3363
+ ).command(
3364
+ "dm:cancel <id>",
3365
+ "Cancel a campaign's unsent messages (sent ones cannot be recalled)",
3366
+ (y) => accountOption(
3367
+ y.positional("id", { describe: "Campaign id (from dm:campaign)", type: "string" })
3368
+ ),
3369
+ run(dmCancel)
3370
+ ).command(
3371
+ "dm:queue",
3372
+ "List the account's queued and recently sent direct messages",
3373
+ (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", {
3374
+ describe: "Only rows in this state",
3375
+ type: "string",
3376
+ choices: ["pending", "sending", "sent", "failed", "skipped"]
3377
+ }).option("campaign", { describe: "Only rows from this campaign id", type: "string" }).example("$0 dm:queue --status pending", "What is still waiting to go out"),
3378
+ run(dmQueue)
3379
+ ).command(
3380
+ "dm:limits",
3381
+ "Show the account's DM allowances and how much of each is used (free)",
3382
+ (y) => accountOption(y),
3383
+ run(dmLimits)
1608
3384
  ).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) => {
1609
3385
  if (err) throw err;
1610
3386
  note(msg || "Invalid usage. Run: superx --help");