superx-cli 0.7.1 → 0.8.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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.0 (2026-10-07)
4
+
5
+ - `posts:remix` gains `--author-handle <@handle>`, the handle that originally posted the text. Pass the account's own handle when remixing one of its own posts: the remix then keeps that post's subject and products instead of re-grounding them in the profile. The API (`author_handle` on `POST /v1/posts/remix`) and MCP `remix_post` take the same field
6
+ - Deleting a scheduled post (`scheduled:delete`, `scheduled:bulk-delete`, MCP `delete_scheduled_post` / `bulk_delete_scheduled_posts`) or moving one to draft (`scheduled:update --status draft`) now re-points scheduled posts that quote it to the post it quoted when that is one of your own posts (still scheduled earlier, or already published); otherwise they lose the quote
7
+ - Image-only posts: `scheduled:create` and `posts:publish` take `--media <key>` without `--text`, `scheduled:update` takes `--text "" --media <key>` (it replaces the whole post, so the empty text is explicit), and `--parts-json` parts may leave out `text` when they carry `media`. The API and the MCP `schedule_post`, `update_scheduled_post` and `publish_post` tools accept a part with media and no text the same way. Nothing accepted before changes; a post with no text and no media at all is still refused
8
+ - `inspiration:search` gains `--min-outlier-score <n>` (0-1000) and `--min-length <n>`. `outlier_score` is a post's engagement relative to what is expected for its author's follower count: 1.0 = expected, 3 = three times expected. `--min-outlier-score 3` keeps only posts at or above 3, and posts whose author's follower count is unknown are left out. `--min-length` keeps posts with at least that many characters
9
+ - MCP `find_inspiration` gained the same filters as the CLI and API: `min_followers` / `max_followers`, `min_outlier_score`, `sort` (including `outlier`), `min_reposts`, `min_replies`, `min_bookmarks`, `min_impressions`, `min_length`, `exclude_topics` and `page`. Each post now returns `outlier_score` and `bookmarks`, and the result carries `page` and `has_more`. Reconnect the server to see the new schema
10
+
3
11
  ## 0.7.1 (2026-09-18)
4
12
 
5
13
  - New skill Worth a Reply (worth-a-reply): `posts:triage` reads a topic, then the skill hands back the posts worth replying to, ordered by how much room there is to add something in a reply, each with its link, one line on why and the angle a reply could take. Skim posts come back as one-liners and the Pass pile stays out unless the person asks. It drafts nothing until they pick a post. Skills are now 32.
package/README.md CHANGED
@@ -126,9 +126,10 @@ Every stored reply your audience has sent you across all your posts: the reply t
126
126
  superx inspiration:search "build in public" --limit 10
127
127
  superx inspiration:search "indie hackers" --sort outlier --min-likes 500
128
128
  superx inspiration:search "AI tools" --min-followers 1000 --max-followers 50000
129
+ superx inspiration:search "SaaS pricing" --min-outlier-score 3 --min-length 100
129
130
  ```
130
131
 
131
- Searches a library of 50M+ real high-performing posts by topic. Options: `--sort relevant|recent|likes|reposts|impressions|outlier`, `--min-likes/--min-reposts/--min-replies/--min-bookmarks/--min-impressions`, `--min-followers/--max-followers` (author size), `--since/--until` (UTC ISO-8601), `--lang` (default en), `--exclude-topics "crypto,politics"`, `--limit` (max 50), `--page` (1-7). Each result carries an `outlier_score`: how far the post outperformed the norm for its author's follower tier. Results are relevance-ranked, strongest matches first, with weak and promotional matches filtered out, so a page may return fewer than `--limit` posts. Use them for structures and hooks to remix, never to copy.
132
+ Searches a library of 50M+ real high-performing posts by topic. Options: `--sort relevant|recent|likes|reposts|impressions|outlier`, `--min-likes/--min-reposts/--min-replies/--min-bookmarks/--min-impressions`, `--min-length` (characters), `--min-followers/--max-followers` (author size), `--min-outlier-score` (0-1000), `--since/--until` (UTC ISO-8601), `--lang` (default en), `--exclude-topics "crypto,politics"`, `--limit` (max 50), `--page` (1-7). Each result carries an `outlier_score`: how far the post outperformed the norm for its author's follower tier (1.0 = expected, 3 = three times expected). `--min-outlier-score` keeps only posts at or above that score. Results are relevance-ranked, strongest matches first, with weak and promotional matches filtered out, so a page may return fewer than `--limit` posts. Use them for structures and hooks to remix, never to copy.
132
133
 
133
134
  ### Contacts (who engages with you)
134
135
 
@@ -206,7 +207,7 @@ superx scheduled:create --text "Post" --at "2026-08-01T15:00:00Z" --idempotency-
206
207
 
207
208
  superx scheduled:list --status draft,scheduled # draft | scheduled | sent | error
208
209
  superx scheduled:list --from "2026-08-01T00:00:00Z" --to "2026-08-08T00:00:00Z"
209
- superx scheduled:delete <post-id>
210
+ superx scheduled:delete <post-id> # posts quoting it re-point to its quoted post if yours (scheduled earlier or published), else lose the quote
210
211
  ```
211
212
 
212
213
  Images attach in two steps: upload, then reference the `object_key`.
@@ -217,7 +218,7 @@ superx scheduled:create --text "Chart of the week" --media "$KEY" --alt-text "We
217
218
  superx scheduled:create --parts-json '[{"text":"1/ Hook","media":[{"object_key":"'"$KEY"'"}]},{"text":"2/ Detail"}]'
218
219
  ```
219
220
 
220
- `media:upload` accepts JPG, PNG, and WEBP up to 5MB and GIF up to 15MB; a post part carries up to 4 images or exactly 1 GIF. `--media` takes a comma list of keys; `--alt-text` (max 1,000 chars) works with a single key, and `--parts-json` covers threads and per-image alt text. Uploads are capped at 100 per day and expire after 24 hours if never attached. Video is not supported yet.
221
+ `media:upload` accepts JPG, PNG, and WEBP up to 5MB and GIF up to 15MB; a post part carries up to 4 images or exactly 1 GIF. `--media` takes a comma list of keys; `--alt-text` (max 1,000 chars) works with a single key, and `--parts-json` covers threads and per-image alt text. A post can be image-only: `--media` without `--text` on `scheduled:create` and `posts:publish`, `--text "" --media <key>` on `scheduled:update`, or a `--parts-json` part with `media` and no `text`. Uploads are capped at 100 per day and expire after 24 hours if never attached. Video is not supported yet.
221
222
 
222
223
  Drafts can carry organizer fields: `--title` (max 300 chars) and `--scratchpad` (max 30,000 chars) are shown in the SuperX app and never posted; `--tag <id>` (repeatable, max 20) attaches tags from `tags:list`.
223
224
 
package/dist/index.js CHANGED
@@ -800,6 +800,7 @@ async function postsRemix(argv) {
800
800
  closeness: argv.closeness
801
801
  };
802
802
  if (argv.instructions) body.instructions = argv.instructions;
803
+ if (argv.authorHandle) body.author_handle = argv.authorHandle;
803
804
  if (argv.account) body.account_id = argv.account;
804
805
  const api = new SuperXAPI(getConfig());
805
806
  printJson(await api.remixPost(body));
@@ -874,6 +875,8 @@ async function inspirationSearch(argv) {
874
875
  min_impressions: argv.minImpressions,
875
876
  min_followers: argv.minFollowers,
876
877
  max_followers: argv.maxFollowers,
878
+ min_outlier_score: argv.minOutlierScore,
879
+ min_length: argv.minLength,
877
880
  since: argv.since,
878
881
  until: argv.until,
879
882
  lang: argv.lang,
@@ -1559,8 +1562,10 @@ function parsePartsJson(raw) {
1559
1562
  process.exit(1);
1560
1563
  throw new Error("unreachable");
1561
1564
  }
1562
- if (!Array.isArray(parsed) || parsed.some((p) => !p || typeof p !== "object" || typeof p.text !== "string")) {
1563
- note("--parts-json must be an array of { text, media? } objects.");
1565
+ if (!Array.isArray(parsed) || parsed.some(
1566
+ (p) => !p || typeof p !== "object" || typeof p.text !== "string" && !(p.text === void 0 && Array.isArray(p.media) && p.media.length > 0)
1567
+ )) {
1568
+ note("--parts-json must be an array of { text, media? } objects (text may be left out on a part with media).");
1564
1569
  process.exit(1);
1565
1570
  }
1566
1571
  return parsed;
@@ -1682,21 +1687,21 @@ async function scheduledCreate(argv) {
1682
1687
  note("Use exactly one of --text (single post), --part (thread), or --parts-json.");
1683
1688
  process.exit(1);
1684
1689
  }
1685
- if (sourceCount === 0) {
1690
+ if (sourceCount === 0 && argv.media === void 0) {
1686
1691
  note("Provide --text for a single post, --part flags for a thread, or --parts-json.");
1687
1692
  process.exit(1);
1688
1693
  }
1689
- if (argv.media !== void 0 && !argv.text) {
1694
+ if (argv.media !== void 0 && (parts.length > 0 || argv["parts-json"] !== void 0)) {
1690
1695
  note("--media applies to the --text single-post form. For threads, put media in --parts-json.");
1691
1696
  process.exit(1);
1692
1697
  }
1693
1698
  const body = {};
1694
1699
  if (argv["parts-json"] !== void 0) {
1695
1700
  body.parts = parsePartsJson(argv["parts-json"]);
1696
- } else if (argv.text) {
1701
+ } else if (argv.text || argv.media !== void 0) {
1697
1702
  const media = mediaFromFlags(argv.media, argv["alt-text"]);
1698
1703
  if (media) {
1699
- body.parts = [{ text: argv.text, media }];
1704
+ body.parts = [{ text: argv.text || "", media }];
1700
1705
  } else {
1701
1706
  body.text = argv.text;
1702
1707
  }
@@ -1728,8 +1733,12 @@ async function scheduledUpdate(argv) {
1728
1733
  note("Use exactly one of --text (single post), --part (thread), or --parts-json.");
1729
1734
  process.exit(1);
1730
1735
  }
1731
- if (argv.media !== void 0 && !argv.text) {
1732
- note("--media applies to the --text single-post form. For threads, put media in --parts-json.");
1736
+ if (argv.media !== void 0 && argv.text === void 0) {
1737
+ if (parts.length > 0 || argv["parts-json"] !== void 0) {
1738
+ note("--media applies to the --text single-post form. For threads, put media in --parts-json.");
1739
+ } else {
1740
+ note('--media replaces the whole post, text included. Pass --text with it, or --text "" for a media-only post.');
1741
+ }
1733
1742
  process.exit(1);
1734
1743
  }
1735
1744
  if (argv.title !== void 0 && argv["clear-title"]) {
@@ -1748,10 +1757,10 @@ async function scheduledUpdate(argv) {
1748
1757
  const body = {};
1749
1758
  if (argv["parts-json"] !== void 0) {
1750
1759
  body.parts = parsePartsJson(argv["parts-json"]);
1751
- } else if (argv.text) {
1760
+ } else if (argv.text || argv.media !== void 0) {
1752
1761
  const media = mediaFromFlags(argv.media, argv["alt-text"]);
1753
1762
  if (media) {
1754
- body.parts = [{ text: argv.text, media }];
1763
+ body.parts = [{ text: argv.text || "", media }];
1755
1764
  } else {
1756
1765
  body.text = argv.text;
1757
1766
  }
@@ -1794,21 +1803,21 @@ async function postsPublish(argv) {
1794
1803
  note("Use exactly one of --text (single post), --part (thread), or --parts-json.");
1795
1804
  process.exit(1);
1796
1805
  }
1797
- if (sourceCount === 0) {
1806
+ if (sourceCount === 0 && argv.media === void 0) {
1798
1807
  note("Provide --text for a single post, --part flags for a thread, or --parts-json.");
1799
1808
  process.exit(1);
1800
1809
  }
1801
- if (argv.media !== void 0 && !argv.text) {
1810
+ if (argv.media !== void 0 && (parts.length > 0 || argv["parts-json"] !== void 0)) {
1802
1811
  note("--media applies to the --text single-post form. For threads, put media in --parts-json.");
1803
1812
  process.exit(1);
1804
1813
  }
1805
1814
  const body = { scheduled_for: "now" };
1806
1815
  if (argv["parts-json"] !== void 0) {
1807
1816
  body.parts = parsePartsJson(argv["parts-json"]);
1808
- } else if (argv.text) {
1817
+ } else if (argv.text || argv.media !== void 0) {
1809
1818
  const media = mediaFromFlags(argv.media, argv["alt-text"]);
1810
1819
  if (media) {
1811
- body.parts = [{ text: argv.text, media }];
1820
+ body.parts = [{ text: argv.text || "", media }];
1812
1821
  } else {
1813
1822
  body.text = argv.text;
1814
1823
  }
@@ -2677,7 +2686,10 @@ var advancedSettingsOptions = (y) => y.option("auto-retweet", {
2677
2686
  }).option("instructions", {
2678
2687
  describe: "Extra direction for this remix (max 500 chars)",
2679
2688
  type: "string"
2680
- }).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(
2689
+ }).option("author-handle", {
2690
+ describe: "The @handle that originally posted the text. Pass this account's own handle when remixing one of its own posts so the subject stays",
2691
+ type: "string"
2692
+ }).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").example('$0 posts:remix --text "$(cat old-post.txt)" --closeness 60 --author-handle @you', "Say one of your own posts again, same subject").epilogue(
2681
2693
  "Returns TEXT ONLY. Nothing is posted or scheduled: save the result with posts:draft or scheduled:create once you are happy with it."
2682
2694
  ),
2683
2695
  run(postsRemix)
@@ -2783,7 +2795,10 @@ var advancedSettingsOptions = (y) => y.option("auto-retweet", {
2783
2795
  describe: "Sort order",
2784
2796
  type: "string",
2785
2797
  choices: ["relevant", "recent", "likes", "reposts", "impressions", "outlier"]
2786
- }).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"),
2798
+ }).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("min-outlier-score", {
2799
+ describe: "Only posts with at least this outlier score (1.0 = expected engagement for the author's size, max 1000)",
2800
+ type: "number"
2801
+ }).option("min-length", { describe: "Only posts with at least this many characters", 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"),
2787
2802
  run(inspirationSearch)
2788
2803
  ).command(
2789
2804
  "inspiration:media [query]",
@@ -3399,7 +3414,7 @@ var advancedSettingsOptions = (y) => y.option("auto-retweet", {
3399
3414
  type: "string",
3400
3415
  array: true
3401
3416
  }).option("media", {
3402
- describe: "Comma list of image object_keys (from media:upload) to attach; single-post form only (max 4 images or 1 GIF)",
3417
+ describe: "Comma list of image object_keys (from media:upload) to attach; single-post form only (max 4 images or 1 GIF). Without --text it makes a media-only post",
3403
3418
  type: "string"
3404
3419
  }).option("alt-text", {
3405
3420
  describe: "Accessibility description for the attached image (single --media key only, max 1000 chars)",
@@ -3417,7 +3432,7 @@ var advancedSettingsOptions = (y) => y.option("auto-retweet", {
3417
3432
  }).option("idempotency-key", {
3418
3433
  describe: "Idempotency-Key header (max 64 chars); retries with the same key return the original result",
3419
3434
  type: "string"
3420
- }).example('$0 scheduled:create --text "Hello"', "Create a draft").example('$0 scheduled:create --text "Hello" --at "2026-08-01T15:00:00Z"', "Schedule a post").example('$0 scheduled:create --part "1/ Hook" --part "2/ Detail" --part "3/ CTA"', "Draft a 3-part thread").example('$0 scheduled:create --text "Hello" --title "Launch teaser" --tag abc123', "Draft with a title and a tag").example('$0 scheduled:create --text "Chart of the week" --media "<object_key>" --alt-text "Revenue chart"', "Draft with an image").example('$0 scheduled:create --text "Hello" --at "2026-08-01T15:00:00Z" --auto-retweet 6 --auto-retweet-remove 4', "Schedule with an auto retweet").example('$0 scheduled:create --text "Hello" --at "2026-08-01T15:00:00Z" --no-auto-retweet --no-auto-plug', "Schedule with your defaults off for this post"),
3435
+ }).example('$0 scheduled:create --text "Hello"', "Create a draft").example('$0 scheduled:create --text "Hello" --at "2026-08-01T15:00:00Z"', "Schedule a post").example('$0 scheduled:create --part "1/ Hook" --part "2/ Detail" --part "3/ CTA"', "Draft a 3-part thread").example('$0 scheduled:create --text "Hello" --title "Launch teaser" --tag abc123', "Draft with a title and a tag").example('$0 scheduled:create --text "Chart of the week" --media "<object_key>" --alt-text "Revenue chart"', "Draft with an image").example('$0 scheduled:create --media "<object_key>"', "Draft an image-only post (no text)").example('$0 scheduled:create --text "Hello" --at "2026-08-01T15:00:00Z" --auto-retweet 6 --auto-retweet-remove 4', "Schedule with an auto retweet").example('$0 scheduled:create --text "Hello" --at "2026-08-01T15:00:00Z" --no-auto-retweet --no-auto-plug', "Schedule with your defaults off for this post"),
3421
3436
  run(scheduledCreate)
3422
3437
  ).command(
3423
3438
  "scheduled:update <id>",
@@ -3427,7 +3442,7 @@ var advancedSettingsOptions = (y) => y.option("auto-retweet", {
3427
3442
  type: "string",
3428
3443
  array: true
3429
3444
  }).option("media", {
3430
- describe: "Comma list of image object_keys to attach with --text (full replace: re-list existing keys to keep them; --text without --media removes the post's media)",
3445
+ describe: `Comma list of image object_keys to attach with --text (full replace: re-list existing keys to keep them; --text without --media removes the post's media; --text "" with --media makes the post media-only)`,
3431
3446
  type: "string"
3432
3447
  }).option("alt-text", {
3433
3448
  describe: "Accessibility description for the attached image (single --media key only, max 1000 chars)",
@@ -3439,18 +3454,18 @@ var advancedSettingsOptions = (y) => y.option("auto-retweet", {
3439
3454
  describe: "New schedule time (UTC ISO-8601 with explicit Z or offset). On its own it never schedules a draft; add --status scheduled.",
3440
3455
  type: "string"
3441
3456
  }).option("status", {
3442
- describe: "Explicit transition; scheduled needs a future time (via --at or already set)",
3457
+ describe: "Explicit transition; scheduled needs a future time (via --at or already set). Moving a scheduled post to draft re-points scheduled posts that quote it to the post it quoted when that is one of your own posts (still scheduled earlier, or already published); otherwise they lose the quote",
3443
3458
  type: "string",
3444
3459
  choices: ["draft", "scheduled"]
3445
3460
  }).option("title", { describe: "New draft title (max 300 chars)", type: "string" }).option("clear-title", { describe: "Remove the title", type: "boolean" }).option("scratchpad", { describe: "New private notes (max 30000 chars)", type: "string" }).option("clear-scratchpad", { describe: "Remove the notes", type: "boolean" }).option("tag", {
3446
3461
  describe: "Replacement tag id set (repeat the flag, max 20; replaces ALL current tags)",
3447
3462
  type: "string",
3448
3463
  array: true
3449
- }).option("clear-tags", { describe: "Remove all tags", type: "boolean" }).example('$0 scheduled:update abc123 --title "Better hook"', "Retitle a draft, everything else untouched").example('$0 scheduled:update abc123 --at "2026-08-01T15:00:00Z" --status scheduled', "Promote a draft to the queue").example("$0 scheduled:update abc123 --status draft", "Pull a post back to drafts (quota refunds)").example("$0 scheduled:update abc123 --auto-delete 8 --auto-delete-threshold 500", "Add an auto delete to the post").example("$0 scheduled:update abc123 --no-auto-retweet", "Remove the post's auto retweet"),
3464
+ }).option("clear-tags", { describe: "Remove all tags", type: "boolean" }).example('$0 scheduled:update abc123 --title "Better hook"', "Retitle a draft, everything else untouched").example('$0 scheduled:update abc123 --at "2026-08-01T15:00:00Z" --status scheduled', "Promote a draft to the queue").example("$0 scheduled:update abc123 --status draft", "Pull a post back to drafts (quota refunds)").example("$0 scheduled:update abc123 --auto-delete 8 --auto-delete-threshold 500", "Add an auto delete to the post").example("$0 scheduled:update abc123 --no-auto-retweet", "Remove the post's auto retweet").example('$0 scheduled:update abc123 --text "" --media "<object_key>"', "Make the post image-only (removes its text)"),
3450
3465
  run(scheduledUpdate)
3451
3466
  ).command(
3452
3467
  "scheduled:delete <id>",
3453
- "Delete a draft or scheduled post by id",
3468
+ "Delete a draft or scheduled post by id. Scheduled posts that quote it are re-pointed to the post it quoted when that is one of your own posts (still scheduled earlier, or already published); otherwise they lose the quote",
3454
3469
  (y) => y.positional("id", { describe: "Post id (from scheduled:list or scheduled:create)", type: "string" }),
3455
3470
  run(scheduledDelete)
3456
3471
  ).command(
@@ -3461,7 +3476,7 @@ var advancedSettingsOptions = (y) => y.option("auto-retweet", {
3461
3476
  type: "string",
3462
3477
  array: true
3463
3478
  }).option("media", {
3464
- describe: "Comma list of image object_keys (from media:upload) to attach; single-post form only (max 4 images or 1 GIF)",
3479
+ describe: "Comma list of image object_keys (from media:upload) to attach; single-post form only (max 4 images or 1 GIF). Without --text it makes a media-only post",
3465
3480
  type: "string"
3466
3481
  }).option("alt-text", {
3467
3482
  describe: "Accessibility description for the attached image (single --media key only, max 1000 chars)",
@@ -3476,7 +3491,7 @@ var advancedSettingsOptions = (y) => y.option("auto-retweet", {
3476
3491
  }).option("idempotency-key", {
3477
3492
  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",
3478
3493
  type: "string"
3479
- }).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"),
3494
+ }).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 --media "<object_key>" --idempotency-key gif-2026-10-03', "Publish an image-only post").example('$0 posts:publish --text "Shipping now." --auto-retweet 6 --idempotency-key launch-2026-09-07', "Publish with an auto retweet"),
3480
3495
  run(postsPublish)
3481
3496
  ).command(
3482
3497
  "scheduled:bulk-retime",
@@ -3493,7 +3508,7 @@ var advancedSettingsOptions = (y) => y.option("auto-retweet", {
3493
3508
  run(scheduledBulkAutoRetweet)
3494
3509
  ).command(
3495
3510
  "scheduled:bulk-delete",
3496
- "Delete up to 100 QUEUED posts and refund their post quota (sent posts and drafts are left alone)",
3511
+ "Delete up to 100 QUEUED posts and refund their post quota (sent posts and drafts are left alone). Scheduled posts that quote them are re-pointed to the post each one quoted when that is one of your own posts (still scheduled earlier, or already published); otherwise they lose the quote",
3497
3512
  (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"),
3498
3513
  run(scheduledBulkDelete)
3499
3514
  ).command(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superx-cli",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "SuperX CLI - command line interface to the SuperX API for Twitter/X growth: read posts and analytics, find engaged contacts, and schedule posts and threads",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -32,7 +32,7 @@ official website: https://superx.so
32
32
 
33
33
  **Rule 2: Read `references/growth-strategy.md` before creating any content.** This skill ships a growth strategy guide (`references/growth-strategy.md`, also inside the installed npm package). It tells you WHAT to post, WHEN, and WHY: the action hierarchy, out-of-network discovery, the engagement loop, and the failure modes that kill reach. The CLI gives you data and actions; the strategy guide gives you judgment. Do not schedule content without it. For a goal-shaped task ("give me my weekly recap", "DM the people who replied to this post"), open the matching skill under `references/skills/`: the named SuperX skills, each with its exact command chain, MCP tool names and stopping point.
34
34
 
35
- **Rule 3: Know the write constraints.** `scheduled:create` without `--at` creates a DRAFT (nothing publishes). With `--at` it schedules for that time. `scheduled:update` changes only the flags you pass, and a new `--at` alone never schedules a draft; add `--status scheduled` to promote. Writes work on your main account or any linked account (pass the same `--account` you used to read it); accounts shared with you by other people are read-only, and tags are workspace-wide. Images attach via `media:upload` then `--media` (JPG/PNG/WEBP up to 5MB, GIF up to 15MB; max 4 images or 1 GIF per post); video is not supported. Timestamps MUST be UTC ISO-8601 with an explicit `Z` or offset; naive timestamps are rejected with 400. `posts:publish` and `articles:publish` post to X IMMEDIATELY and irreversibly; treat them like hitting Publish in public and get human confirmation of the exact text unless the user already gave it. `posts:publish` also requires `--idempotency-key`, which you reuse verbatim on any retry. `posts:draft` writes post text in the user's voice and saves NOTHING: show the drafts, let the user pick and edit one, then pass the final text to `scheduled:create` yourself; it costs AI credits per draft, so ask for the count the user actually wants. `--voice mine` is the voice of the `--account` you pass (its own posts and style guide); shared accounts are refused.
35
+ **Rule 3: Know the write constraints.** `scheduled:create` without `--at` creates a DRAFT (nothing publishes). With `--at` it schedules for that time. `scheduled:update` changes only the flags you pass, and a new `--at` alone never schedules a draft; add `--status scheduled` to promote. Writes work on your main account or any linked account (pass the same `--account` you used to read it); accounts shared with you by other people are read-only, and tags are workspace-wide. Images attach via `media:upload` then `--media` (JPG/PNG/WEBP up to 5MB, GIF up to 15MB; max 4 images or 1 GIF per post), and a post can be image-only (`--media` with no `--text`; on `scheduled:update` use `--text "" --media KEY`, since an update replaces the whole post); video is not supported. Timestamps MUST be UTC ISO-8601 with an explicit `Z` or offset; naive timestamps are rejected with 400. `posts:publish` and `articles:publish` post to X IMMEDIATELY and irreversibly; treat them like hitting Publish in public and get human confirmation of the exact text unless the user already gave it. `posts:publish` also requires `--idempotency-key`, which you reuse verbatim on any retry. `posts:draft` writes post text in the user's voice and saves NOTHING: show the drafts, let the user pick and edit one, then pass the final text to `scheduled:create` yourself; it costs AI credits per draft, so ask for the count the user actually wants. `--voice mine` is the voice of the `--account` you pass (its own posts and style guide); shared accounts are refused.
36
36
 
37
37
  ---
38
38
 
@@ -81,11 +81,12 @@ superx replies:received --limit 20 # Replies the audience has se
81
81
  superx inspiration:search "build in public" --limit 10 # Topic search
82
82
  superx inspiration:search "indie hackers" --sort outlier # Biggest overperformers
83
83
  superx inspiration:search "AI tools" --min-likes 500 --min-followers 1000 --max-followers 50000
84
+ superx inspiration:search "SaaS pricing" --min-outlier-score 3 --min-length 100 # 3x+ expected engagement, 100+ chars
84
85
  ```
85
86
 
86
87
  - Searches a library of 50M+ real high-performing posts. Use results for structures, hooks, and angles to remix. Never copy them.
87
- - Flags: `--sort relevant|recent|likes|reposts|impressions|outlier`, `--min-likes/--min-reposts/--min-replies/--min-bookmarks/--min-impressions`, `--min-followers/--max-followers` (author size), `--since/--until`, `--lang` (default en), `--exclude-topics "crypto,politics"`, `--limit` (max 50), `--page` (1-7).
88
- - `outlier_score` on each result = how far the post outperformed the norm for its author's follower tier. Sorting by `outlier` surfaces content that won on substance, not audience size.
88
+ - Flags: `--sort relevant|recent|likes|reposts|impressions|outlier`, `--min-likes/--min-reposts/--min-replies/--min-bookmarks/--min-impressions`, `--min-length` (characters), `--min-followers/--max-followers` (author size), `--min-outlier-score` (0-1000), `--since/--until`, `--lang` (default en), `--exclude-topics "crypto,politics"`, `--limit` (max 50), `--page` (1-7).
89
+ - `outlier_score` on each result = how far the post outperformed the norm for its author's follower tier (1.0 = expected, 3 = three times expected). Sorting by `outlier` surfaces content that won on substance, not audience size; `--min-outlier-score` drops everything below a floor.
89
90
  - Results are relevance-ranked, strongest matches first. Weak and promotional matches are filtered out, so a page may return fewer than `--limit` posts.
90
91
 
91
92
  ### Live X lookups
@@ -358,6 +359,7 @@ superx engage:reply-draft --text "hot take about pricing" --handle levelsio --au
358
359
  # Rewrite a post, near or far from the original
359
360
  superx posts:remix --text "$(cat post.txt)" --closeness 70
360
361
  superx posts:remix --text "..." --closeness 20 --instructions "make it a question"
362
+ superx posts:remix --text "$(cat old-post.txt)" --closeness 60 --author-handle @you
361
363
 
362
364
  # Change one selected piece, keeping the surrounding style
363
365
  superx tools:inline-edit --text "the hook line" --full "$(cat post.txt)" --type hook
@@ -380,7 +382,7 @@ superx posts:triage "indie SaaS" --days 7
380
382
 
381
383
  - **Every one of these returns TEXT and posts NOTHING.** `engage:reply-draft` writes a reply for a person to review and post; there is still no reply-sending command anywhere in the CLI. Show the draft, let the user edit it, and never say a reply went out.
382
384
  - `engage:reply-draft` takes exactly one of `--post <id>` (the API reads the post live, so the draft sees the real text, author and any quoted post) or `--text` with optional `--author` and `--handle`. Add `--thoughts` with what the USER wants to say - ask them, never invent an opinion for them - and `--tone engaging|humorous|creative|sarcastic|inspirational|concise`. `--post` also spends one live X lookup on top of the credit.
383
- - `posts:remix` needs `--closeness` 0-100: 0 keeps only the idea, 100 stays very close to the original wording. Use it on a proven post the user wants to say again in their own words, then save the result with `posts:draft` or `scheduled:create`.
385
+ - `posts:remix` needs `--closeness` 0-100: 0 keeps only the idea, 100 stays very close to the original wording. Use it on a proven post the user wants to say again in their own words, then save the result with `posts:draft` or `scheduled:create`. When the post is one of the account's OWN (from `posts:list`, analytics or an old post the user pastes), add `--author-handle` with that account's handle so the remix keeps its subject instead of re-grounding it in the profile.
384
386
  - `tools:inline-edit` needs `--instruction`, `--type`, or both, and works best with `--full` so the edit blends into the post around it. `--type` presets: grammar, translate, hook, details, concise, engaging, humorous, creative, sarcastic, inspirational.
385
387
  - `tools:rephrase` presets: improve, grammar, translate, hook, details, clarity, engaging, humorous, positive, creative, sarcastic, inspirational, concise. The style ones write in the user's voice; grammar, translate, clarity, details and concise stay mechanical.
386
388
  - `tools:factcheck` reports `result` (true, false or unknown), a one-sentence `comment` and the `sources` it read. It is a model's reading of a couple of search results, NOT a guarantee: show the sources and never present the verdict as settled.
@@ -452,6 +454,7 @@ superx scheduled:delete <post-id>
452
454
  - Replays add `"replayed": true` to the JSON output and print a stderr note.
453
455
  - `scheduled:list` filters: `--status draft,scheduled,sent,error` (comma list), `--tags id,id` (any-of), `--from/--to` bounds on the scheduled time.
454
456
  - `media:upload` accepts JPG/PNG/WEBP (5MB) and GIF (15MB); a post part carries up to 4 images OR exactly 1 GIF. Uploads are capped at 100/day and expire after 24h if never attached.
457
+ - Image-only posts work: `--media <key>` with no `--text` (on `scheduled:update` pass `--text "" --media <key>`, since it replaces the whole post), or a `--parts-json` part with `media` and no `text`.
455
458
 
456
459
  ### Publishing now (irreversible)
457
460
 
@@ -10,6 +10,6 @@ superx scheduled:create --text "<approved text>" --idempotency-key "remix-<slug>
10
10
 
11
11
  `--mirror` copies the SHAPE of a proven post, not its words. Pick a reference with room for the account's own facts.
12
12
 
13
- MCP: `find_inspiration` -> `draft_post` -> `schedule_post`
13
+ MCP: `find_inspiration` (`sort: "outlier"`, optionally `min_outlier_score: 3` for posts with 3x the expected engagement for the author's size) -> `draft_post` -> `schedule_post`
14
14
 
15
15
  Stops at: reference posts plus drafts, for the person to pick from. 3 AI credits per draft written.