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(
|
|
1563
|
-
|
|
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 &&
|
|
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 &&
|
|
1732
|
-
|
|
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 &&
|
|
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
|
-
}).
|
|
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("
|
|
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:
|
|
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.
|
|
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": {
|
package/skills/superx/SKILL.md
CHANGED
|
@@ -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.
|