@sprid/cli 1.0.5 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/src/commands/ads.mjs +24 -19
- package/src/docs/commands.mjs +2 -2
- package/src/docs/guides.generated.mjs +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
sprid follows semver: a breaking change to a command's arguments, its output
|
|
4
4
|
shape or its exit codes is a major release.
|
|
5
5
|
|
|
6
|
+
## 1.1.0 - 2026-10-04
|
|
7
|
+
|
|
8
|
+
- `sprid ads launch` and `sprid ads budget` print what the change would commit and open
|
|
9
|
+
the page in the Sprid app where you press Launch or set the budget. The server no
|
|
10
|
+
longer lets any token start spending, so this applies to every earlier version too;
|
|
11
|
+
`--yes` has nothing left to skip, and `launch` exits 0 after the hand-off.
|
|
12
|
+
|
|
6
13
|
## 1.0.5 - 2026-10-03
|
|
7
14
|
|
|
8
15
|
- `sprid docs posthog` and `sprid docs revenuecat` say how to fix each measurement check a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sprid/cli",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "The Sprid command line: connect your app, review results, prepare posts and store screenshots, and release mobile apps.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"type": "module",
|
package/src/commands/ads.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// sprid ads: paid campaigns on Meta through the same tools the ads skill
|
|
2
2
|
// calls. Everything is created paused. The two verbs that change what is spent
|
|
3
|
-
// (launch, budget)
|
|
4
|
-
//
|
|
3
|
+
// (launch, budget) print what it would commit and open the page in the Sprid
|
|
4
|
+
// app where you confirm it: no token can start spending.
|
|
5
5
|
|
|
6
6
|
import { UsageError } from "../args.mjs";
|
|
7
7
|
import { C, OK } from "../format.mjs";
|
|
@@ -12,8 +12,8 @@ const USAGE = [
|
|
|
12
12
|
"sprid ads [overview] --account <slug>",
|
|
13
13
|
"sprid ads status|council|metrics <adSetId> --account <slug> [--days N]",
|
|
14
14
|
"sprid ads create campaign|set|creative|ad|batch --file <args.json> --account <slug>",
|
|
15
|
-
"sprid ads launch <adSetId> --account <slug>
|
|
16
|
-
"sprid ads budget <adSetId> --daily-cents N --account <slug>
|
|
15
|
+
"sprid ads launch <adSetId> --account <slug>",
|
|
16
|
+
"sprid ads budget <adSetId> --daily-cents N --account <slug>",
|
|
17
17
|
"sprid ads pause <adSetId> --account <slug> [--yes]",
|
|
18
18
|
"sprid ads close <adSetId> --account <slug>",
|
|
19
19
|
].join("\n ");
|
|
@@ -48,8 +48,8 @@ export async function ads(ctx) {
|
|
|
48
48
|
show(ctx, await callTool(ctx, tool, { ...(await readJson(ctx, ctx.flags.file)), account }));
|
|
49
49
|
return 0;
|
|
50
50
|
}
|
|
51
|
-
if (sub === "launch") return launch(ctx, account, needId(arg, "sprid ads launch <adSetId> --account <slug>
|
|
52
|
-
if (sub === "budget") return budget(ctx, account, needId(arg, "sprid ads budget <adSetId> --daily-cents N --account <slug>
|
|
51
|
+
if (sub === "launch") return launch(ctx, account, needId(arg, "sprid ads launch <adSetId> --account <slug>"));
|
|
52
|
+
if (sub === "budget") return budget(ctx, account, needId(arg, "sprid ads budget <adSetId> --daily-cents N --account <slug>"));
|
|
53
53
|
if (sub === "pause") return pause(ctx, account, needId(arg, "sprid ads pause <adSetId> --account <slug> [--yes]"));
|
|
54
54
|
if (sub === "close") {
|
|
55
55
|
show(ctx, await callTool(ctx, "close_ad_set", { account, adSetId: needId(arg, "sprid ads close <adSetId> --account <slug>") }));
|
|
@@ -59,7 +59,22 @@ export async function ads(ctx) {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
|
-
*
|
|
62
|
+
* Spending is confirmed in the app. Print what it commits and open the page
|
|
63
|
+
* with the button on it.
|
|
64
|
+
*/
|
|
65
|
+
async function handOff(ctx, approval) {
|
|
66
|
+
const url = approval?.approvalUrl;
|
|
67
|
+
const opened = url ? await ctx.openBrowser(url) : false;
|
|
68
|
+
if (ctx.json) ctx.out({ ...approval, opened });
|
|
69
|
+
else {
|
|
70
|
+
ctx.print(` ${approval?.message ?? "Confirm it in the Sprid app."}`);
|
|
71
|
+
if (url) ctx.print(` ${opened ? "Opened" : "Open"} ${url}`);
|
|
72
|
+
}
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Ask before a call that resets learning. --yes skips the prompt.
|
|
63
78
|
* Without a terminal (or with --json) there is nobody to ask, so it stops
|
|
64
79
|
* with exit 2; a person who answers no gets exit 1.
|
|
65
80
|
*/
|
|
@@ -81,23 +96,13 @@ async function approve(ctx, summary, question) {
|
|
|
81
96
|
}
|
|
82
97
|
|
|
83
98
|
async function launch(ctx, account, adSetId) {
|
|
84
|
-
|
|
85
|
-
const stop = await approve(ctx, preview?.message ?? `Launch ad set ${adSetId}.`, "Launch and start spending?");
|
|
86
|
-
if (stop) return stop;
|
|
87
|
-
const res = await callTool(ctx, "launch_ads", { account, adSetId, confirmed: true });
|
|
88
|
-
if (ctx.json) ctx.out(res);
|
|
89
|
-
else ctx.print(` ${C.ok(OK)} Ad set ${adSetId} is live. Leave it alone for 3 days: every edit resets Meta's learning phase.`);
|
|
90
|
-
return 0;
|
|
99
|
+
return handOff(ctx, await callTool(ctx, "launch_ads", { account, adSetId }));
|
|
91
100
|
}
|
|
92
101
|
|
|
93
102
|
async function budget(ctx, account, adSetId) {
|
|
94
103
|
const cents = Number(ctx.flags["daily-cents"]);
|
|
95
104
|
if (!Number.isInteger(cents) || cents <= 0) throw new UsageError("Pass the new daily budget in cents: --daily-cents 1500 is 15.00 a day.");
|
|
96
|
-
|
|
97
|
-
const stop = await approve(ctx, summary, "Change the budget?");
|
|
98
|
-
if (stop) return stop;
|
|
99
|
-
show(ctx, await callTool(ctx, "update_ad_budget", { account, adSetId, dailyBudgetCents: cents }));
|
|
100
|
-
return 0;
|
|
105
|
+
return handOff(ctx, await callTool(ctx, "update_ad_budget", { account, adSetId, dailyBudgetCents: cents }));
|
|
101
106
|
}
|
|
102
107
|
|
|
103
108
|
async function pause(ctx, account, adSetId) {
|
package/src/docs/commands.mjs
CHANGED
|
@@ -118,8 +118,8 @@ export const COMMAND_GROUPS = [
|
|
|
118
118
|
['ads', 'sprid ads [overview] --account <slug>', 'Spend against the cap, what is live and what the connected Meta Ads channel can do.'],
|
|
119
119
|
['ads', 'sprid ads status|council|metrics <adSetId> --account <slug> [--days N]', 'Delivery and rejections from Meta, the weekly judgement, or the numbers for one ad set.'],
|
|
120
120
|
['ads', 'sprid ads create campaign|set|creative|ad|batch --file <args.json> --account <slug>', 'Create paused objects. The file holds the tool’s arguments; sprid tool describe create_ad_set shows them.'],
|
|
121
|
-
['ads', 'sprid ads launch <adSetId> --account <slug>
|
|
122
|
-
['ads', 'sprid ads budget <adSetId> --daily-cents N --account <slug>
|
|
121
|
+
['ads', 'sprid ads launch <adSetId> --account <slug>', 'Show what launching commits and open the page in Sprid where you press Launch. No token can start spending.'],
|
|
122
|
+
['ads', 'sprid ads budget <adSetId> --daily-cents N --account <slug>', 'Show the proposed daily budget and open the page in Sprid where you set it, checked against the account’s caps.'],
|
|
123
123
|
['ads', 'sprid ads pause <adSetId> --account <slug> [--yes]', 'Pause an ad set. Resuming restarts Meta’s learning phase.'],
|
|
124
124
|
['ads', 'sprid ads close <adSetId> --account <slug>', 'End an ad set and write its post-mortem.'],
|
|
125
125
|
] },
|
|
@@ -1652,8 +1652,8 @@ export const CONTENT_GUIDES = [
|
|
|
1652
1652
|
"title": "Ads: pay to show more people what already worked",
|
|
1653
1653
|
"summary": "Explains how Sprid thinks about paid ads on Instagram, Facebook, YouTube and TikTok: when to spend, how a promotion runs from draft to result, and which numbers to believe along the way.",
|
|
1654
1654
|
"url": "https://sprid.studio/docs/ads",
|
|
1655
|
-
"markdown": "# Ads: pay to show more people what already worked\n\n**What this guide does:** Explains how Sprid thinks about paid ads on\nInstagram, Facebook, YouTube and TikTok: when to spend, how a promotion runs\nfrom draft to result, and which numbers to believe along the way.\n\nAds don't rescue a post nobody wanted. They amplify, and they amplify\nwhatever you give them, including a weak idea. So the order Sprid follows is\norganic first: publish, see which posts people actually watched and saved, and\nput money behind the one that already beat your normal. That post has passed\nthe hardest test for free.\n\n## What we believe\n\n**Promote proof, not hope.** A post that did twice your median has evidence\nbehind it. A brand-new creative has only an opinion. Start with the post, and\nmake new creatives once you know what the winning one had.\n\n**Nothing spends without you.** Everything Sprid or your agent prepares is\npaused.
|
|
1656
|
-
"revision": "
|
|
1655
|
+
"markdown": "# Ads: pay to show more people what already worked\n\n**What this guide does:** Explains how Sprid thinks about paid ads on\nInstagram, Facebook, YouTube and TikTok: when to spend, how a promotion runs\nfrom draft to result, and which numbers to believe along the way.\n\nAds don't rescue a post nobody wanted. They amplify, and they amplify\nwhatever you give them, including a weak idea. So the order Sprid follows is\norganic first: publish, see which posts people actually watched and saved, and\nput money behind the one that already beat your normal. That post has passed\nthe hardest test for free.\n\n## What we believe\n\n**Promote proof, not hope.** A post that did twice your median has evidence\nbehind it. A brand-new creative has only an opinion. Start with the post, and\nmake new creatives once you know what the winning one had.\n\n**Nothing spends without you.** Everything Sprid or your agent prepares is\npaused. Spending starts only when you press Launch or Confirm in the Sprid app,\non a screen that shows what will run, what it costs a day and for how long. An\nagent can prepare a campaign and send you that link. It can't start one or\nraise a budget itself.\n\n**Keep the words, vary the picture.** We read 3,549 live ads across five\nmarkets. Among advertisers running eight or more, the median was 2.9 ads per\nline of copy, and the largest wellness advertiser ran 109 ads on essentially one\nline. The ad library counts placements separately, so the exact ratio overstates,\nbut the shape holds: the people who keep paying hold the copy and change the\ncreative. A new version changes one thing, or the result teaches nothing.\n\n**Give it time to be readable.** Meta's delivery settles after roughly 50\noptimisation events in a week, and Google and TikTok need a similar learning\nperiod. Before that, a result is noise, and the most\ncommon way to waste money on ads is to stop an ad set before it has said\nanything, then decide the idea doesn't work. Sprid says whether a result is\nreadable before it says whether it worked.\n\n**Judge by what happens in your product.** Cost per install or subscriber comes\nfirst and impressions come last, because the order a report puts numbers in is\nthe order people believe them in. When your app reports who became a paying\ncustomer, that outranks anything the platform can see.\n\n## Two ways to start\n\n**Promote a post.** Pick a post already live, set a daily budget and an end\ndate, and confirm. The ad runs only where that post lives, through the network\nbehind it:\n\n| Post live on | Runs as | Paid by |\n|---|---|---|\n| Instagram | a Meta ad on Instagram only | your Meta ad account |\n| Facebook | a Meta ad on Facebook only | your Meta ad account |\n| YouTube | a Google Ads video campaign (YouTube, Shorts, Discover, Gmail) | your Google Ads account |\n| TikTok | a Spark Ad on TikTok | your TikTok ad account |\n\nInstagram needs one more thing: the Instagram account has to be linked to the\nFacebook Page your Meta ad account runs as, because that is how Meta runs an\nInstagram ad. Until it is, the Ads page shows Instagram as **Not linked**, and\ntapping it says what to do.\n\nSprid checks the ad account can pay before you choose a budget, the network\nreviews the ad, and you get an email when it goes live, is rejected (with the\nnetwork's reason) or ends (with what the money bought). The budget and end date\ncan change while it runs, from Sprid. A TikTok post also needs the creator's\npermission for that one post: a code the TikTok app makes under the post's Ad\nsettings, pasted into Sprid.\n\n**Run a test.** For finding out what to say, not just saying it louder. Tests\nrun on Meta. Every\ncreative is built from a named shape, such as the screen that shows the app's\nverdict about the reader, two columns where the reader does the arithmetic, or\none sourced number. The shape is what makes a result transferable: \"showing the\nverdict beat listing features\" is something you can build again, \"ad 7 beat\nad 3\" is not.\n\n## The checks before money moves\n\n- **The copy check.** Banned words, your own never-say list, invented urgency,\n and openings that name an identity (\"for anxious people\") instead of a moment\n (\"you reread the message four times\"). Warnings pass, errors block the launch.\n- **Regulated categories.** Housing, credit, employment and political ads must\n be declared. An undeclared one isn't quietly rejected, it's pulled with the\n ad account attached. Anything about where people live, what they can borrow or\n who gets hired counts, even when the product doesn't look like it.\n- **The payment method**, before a budget is chosen rather than after Meta has\n accepted the campaign and refused the ad.\n\n## Reading the result\n\nAsk for an ad set's verdict once a week and it answers three questions: did it\nwork, why, and what next.\nWhen a test ends, it writes down what the hypothesis was, what happened and\nwhich shape carried it, so the next test starts from that instead of from zero.\nA losing set is data: close it and say what it ruled out.\n\nTwo rules for the numbers. A campaign's totals and its ad sets' totals are\nseparate readings of the same money, never added together. And a period that\nends today and one that ended last week are two different readings, so every\nnumber says which window it covers.\n\n## What competitors can and can't tell you\n\nMeta's Ad Library shows every active ad: the creative, the copy and the date it\nstarted. It shows no spend, no clicks and no results. Days live means somebody\nkeeps paying, not that it works, so a competitor's ad is \"longest-running\",\nnever \"best-performing\". Sprid's scout research reads the library for your own\nmarket, which is where the shapes above came from.\n\n## You need\n\n- An ad account with a card on it, connected to Sprid, for the network behind\n the post: [Meta Ads](https://sprid.studio/docs/connect/meta-ads) for\n Instagram and Facebook, [Google Ads](https://sprid.studio/docs/connect/google-ads)\n for YouTube, [TikTok Ads](https://sprid.studio/docs/connect/tiktok-ads) for\n TikTok.\n- A published post to promote, or, on Meta, an image or video to build a\n creative from.\n- You, signed in to the Sprid app, to press Launch. No agent or token can.\n",
|
|
1656
|
+
"revision": "89038f9560d6a887"
|
|
1657
1657
|
},
|
|
1658
1658
|
{
|
|
1659
1659
|
"id": "reel-first-seconds",
|