@letterstory/cli 0.136.0 → 0.137.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/lib/cli.mjs CHANGED
@@ -57,12 +57,13 @@ import {
57
57
  cmdEvals,
58
58
  cmdOfferings,
59
59
  cmdEnclosures,
60
+ cmdCampaigns,
60
61
  cmdTargets,
61
62
  cmdVisits,
62
63
  } from "./commands.mjs";
63
64
 
64
65
  // Keep in sync with cli/package.json.
65
- export const VERSION = "0.136.0";
66
+ export const VERSION = "0.137.0";
66
67
 
67
68
  // Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
68
69
  // can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
@@ -568,6 +569,11 @@ Enclosures (pictures, clips and sentences made for each prospect; each output is
568
569
  enclosures apps connect <sign-in-url> [--no-remember] A sign-in link a person opens (no password passes through)
569
570
  enclosures apps disconnect <app-id> --yes Remove it here and at the browser provider
570
571
 
572
+ Campaigns (one prompt, and Letter's agent builds the whole cold-email campaign; nothing sends until you approve):
573
+ campaigns build "<prompt>" [--limit <n>] [--watch] The list, the picture (tested on 3), the email, the review desk
574
+ campaigns show <build-id> [--watch] Its stage, what it built, and what it is doing
575
+ campaigns list | cancel <build-id> | suggest Its builds; stop one (what it built is kept); three to build
576
+
571
577
  Offerings (context layered org → offering → collection; a layer sets only what it overrides):
572
578
  offerings list The org's offerings (things you'd name when recommending them)
573
579
  offerings show <offering-id> One offering + its competitor overrides
@@ -1494,6 +1500,7 @@ const CLIENT_COMMANDS = {
1494
1500
  directives: cmdDirectives,
1495
1501
  offerings: cmdOfferings,
1496
1502
  enclosures: cmdEnclosures,
1503
+ campaigns: cmdCampaigns,
1497
1504
  targets: cmdTargets,
1498
1505
  probes: cmdProbes,
1499
1506
  suggestions: cmdSuggestions,
@@ -0,0 +1,159 @@
1
+ // `campaigns` — one prompt, and Letter's agent builds the whole cold-email campaign: the list, the
2
+ // enclosure (tested on 3 and chosen), the sequence email that carries each person's picture, and
3
+ // the launch into review. Thin wrappers over the campaign build tools, which call the same
4
+ // functions the app's /api/campaign-builds routes do.
5
+
6
+ import { CliError } from "../client.mjs";
7
+ import { argsFromFlags, flagNum, pollUntil, printResult, refuseUnknownFlags, ok } from "./shared.mjs";
8
+
9
+ // How every tool this module calls gets its arguments — see `argsFromFlags` in shared.mjs.
10
+ // `scripts/parity-check.ts` compares these keys against each tool's published input schema.
11
+ export const ARGS = {
12
+ build_campaign: {
13
+ prompt: {
14
+ value: (_flags, positionals) => {
15
+ const words = positionals.join(" ").trim();
16
+ if (!words)
17
+ throw new CliError(
18
+ 'Say what to build, e.g. campaigns build "Cold-email 10 SaaS founders with an audit of their own site"'
19
+ );
20
+ return words;
21
+ },
22
+ },
23
+ people_limit: { flag: "limit", type: "num" },
24
+ },
25
+ get_campaign_build: { build_id: { positional: 0, label: "build-id", required: true } },
26
+ list_campaign_builds: { limit: { flag: "limit", type: "num" } },
27
+ cancel_campaign_build: { build_id: { positional: 0, label: "build-id", required: true } },
28
+ suggest_campaigns: {},
29
+ };
30
+
31
+ const USAGE = `Usage:
32
+ campaigns build "<prompt>" [--limit <n>] [--watch] Build a whole campaign from one prompt (at most 50 people)
33
+ campaigns show <build-id> [--watch] Its stage, what it built, and what it is doing
34
+ campaigns list [--limit <n>] This org's builds, newest first
35
+ campaigns cancel <build-id> Stop it; what it built is kept
36
+ campaigns suggest Three campaigns to build in one step`;
37
+
38
+ const STAGES = { who: "Who", picture: "The picture", test: "Tested on 3", email: "The email", desk: "On the desk" };
39
+
40
+ export async function cmdCampaigns(ctx) {
41
+ const sub = ctx.positionals[0];
42
+ const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
43
+ switch (sub) {
44
+ case "build":
45
+ return campaignsBuild(rest);
46
+ case "show":
47
+ case "get":
48
+ return campaignsShow(rest);
49
+ case "list":
50
+ case "ls":
51
+ return campaignsList(rest);
52
+ case "cancel":
53
+ return campaignsCancel(rest);
54
+ case "suggest":
55
+ return campaignsSuggest(rest);
56
+ default:
57
+ throw new CliError(USAGE);
58
+ }
59
+ }
60
+
61
+ function formatBuild(b) {
62
+ const a = b.artifacts ?? {};
63
+ const lines = [`${b.id} [${b.status}, ${STAGES[b.stage] ?? b.stage}] ${b.prompt}`];
64
+ if (a.list)
65
+ lines.push(
66
+ ` who: ${a.list.people} people on "${a.list.name}"${a.list.pulling ? " (still finding them)" : ""}`
67
+ );
68
+ if (a.enclosure) {
69
+ const p = a.enclosure.prints ?? {};
70
+ lines.push(
71
+ ` picture: ${a.enclosure.name}${a.enclosure.current_version ? `, version ${a.enclosure.current_version}` : ", testing"}: ${p.done ?? 0} ready, ${p.making ?? 0} making, ${p.no_fit ?? 0} no fit, ${p.failed ?? 0} failed`
72
+ );
73
+ }
74
+ if (a.sequence) lines.push(` email: ${a.sequence.name} [${a.sequence.status}]`);
75
+ if (a.desk)
76
+ lines.push(
77
+ ` desk: ${a.desk.waiting} copies waiting (${a.desk.with_picture} with their picture), ${a.desk.on_the_way} on the way`
78
+ );
79
+ lines.push(` spent: $${Number(b.cost_usd).toFixed(2)} of $${Number(b.budget_usd).toFixed(2)}`);
80
+ if (b.summary) lines.push(` ${b.summary}`);
81
+ if (b.error) lines.push(` ${b.error}`);
82
+ return lines.join("\n");
83
+ }
84
+
85
+ const formatLine = (l) => `${l.at.slice(11, 19)} ${l.kind === "say" ? "" : `${l.kind}: `}${l.text}`;
86
+
87
+ // Follow a build: print each new activity line as it lands, until it stops.
88
+ async function follow(ctx, buildId) {
89
+ const { client, io } = ctx;
90
+ let shown = 0;
91
+ return pollUntil(
92
+ io,
93
+ async () => {
94
+ const { build } = await client.callTool("get_campaign_build", { build_id: buildId });
95
+ const lines = build.activity ?? [];
96
+ for (const line of lines.slice(shown)) io.log(formatLine(line));
97
+ shown = Math.max(shown, lines.length);
98
+ return { done: build.status !== "running", value: build };
99
+ },
100
+ { intervalMs: 5000, timeoutMs: 60 * 60_000, label: "the campaign build" }
101
+ );
102
+ }
103
+
104
+ async function campaignsBuild(ctx) {
105
+ const { client, flags, positionals, io } = ctx;
106
+ refuseUnknownFlags(ctx, ARGS.build_campaign, ["watch"]);
107
+ if (flags.limit !== undefined) flagNum(flags.limit);
108
+ const result = await client.callTool("build_campaign", argsFromFlags(ARGS.build_campaign, flags, positionals));
109
+ ok(ctx, `Building it (${result.build.id}). It takes 10 to 20 minutes; nothing sends until you approve a copy.`);
110
+ const build = flags.watch === true && !flags.json ? await follow(ctx, result.build.id) : result.build;
111
+ printResult(io, flags, flags.watch === true ? { build } : result, (r) => formatBuild(r.build));
112
+ return 0;
113
+ }
114
+
115
+ async function campaignsShow(ctx) {
116
+ const { client, flags, positionals, io } = ctx;
117
+ refuseUnknownFlags(ctx, ARGS.get_campaign_build, ["watch"]);
118
+ const args = argsFromFlags(ARGS.get_campaign_build, flags, positionals);
119
+ if (flags.watch === true && !flags.json) {
120
+ const build = await follow(ctx, args.build_id);
121
+ io.log(formatBuild(build));
122
+ return 0;
123
+ }
124
+ const result = await client.callTool("get_campaign_build", args);
125
+ printResult(io, flags, result, (r) =>
126
+ [formatBuild(r.build), ...(r.build.activity ?? []).slice(-15).map(formatLine)].join("\n")
127
+ );
128
+ return 0;
129
+ }
130
+
131
+ async function campaignsList(ctx) {
132
+ const { client, flags, io } = ctx;
133
+ if (flags.limit !== undefined) flagNum(flags.limit);
134
+ const result = await client.callTool("list_campaign_builds", argsFromFlags(ARGS.list_campaign_builds, flags));
135
+ printResult(io, flags, result, (r) =>
136
+ r.builds?.length ? r.builds.map(formatBuild).join("\n\n") : "No campaign builds yet."
137
+ );
138
+ return 0;
139
+ }
140
+
141
+ async function campaignsCancel(ctx) {
142
+ const { client, flags, positionals, io } = ctx;
143
+ const result = await client.callTool(
144
+ "cancel_campaign_build",
145
+ argsFromFlags(ARGS.cancel_campaign_build, flags, positionals)
146
+ );
147
+ ok(ctx, "Stopped. What it built is kept.");
148
+ printResult(io, flags, result, (r) => formatBuild(r.build));
149
+ return 0;
150
+ }
151
+
152
+ async function campaignsSuggest(ctx) {
153
+ const { client, flags, io } = ctx;
154
+ const result = await client.callTool("suggest_campaigns", {});
155
+ printResult(io, flags, result, (r) =>
156
+ (r.suggestions ?? []).map((s) => `${s.title}\n letterstory campaigns build "${s.prompt}"`).join("\n\n")
157
+ );
158
+ return 0;
159
+ }
package/lib/commands.mjs CHANGED
@@ -39,6 +39,7 @@ export * from "./commands/landings.mjs";
39
39
  export * from "./commands/outbound.mjs";
40
40
  export * from "./commands/offerings.mjs";
41
41
  export * from "./commands/enclosures.mjs";
42
+ export * from "./commands/campaigns.mjs";
42
43
  export * from "./commands/targets.mjs";
43
44
  export * from "./commands/admin.mjs";
44
45
  export * from "./commands/feedback.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.136.0",
3
+ "version": "0.137.0",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",