@letterstory/cli 0.90.0 → 0.92.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,7 +57,7 @@ import {
57
57
  } from "./commands.mjs";
58
58
 
59
59
  // Keep in sync with cli/package.json.
60
- export const VERSION = "0.90.0";
60
+ export const VERSION = "0.92.0";
61
61
 
62
62
  // Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
63
63
  // can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
@@ -121,6 +121,8 @@ export const BOOLEAN_FLAGS = new Set([
121
121
  // `phantom-onboard collection --enable-planner`, `strategy sitemap --posts-only`, and the
122
122
  // `strategy positioning set --clear-*` switches: value-less, and each sits next to a value.
123
123
  "enable-planner",
124
+ // `phantom-onboard primitives --retry [<domain>]` re-reads a domain whose read failed.
125
+ "retry",
124
126
  "posts-only",
125
127
  "clear-topics",
126
128
  "clear-stances",
@@ -135,9 +137,10 @@ export const BOOLEAN_FLAGS = new Set([
135
137
  "no-enrich",
136
138
  // `offerings set-collection <id> --none` (the org itself is the subject again).
137
139
  "none",
138
- // `prospects import|source --one-per-company`, `prospects source --enrich`.
140
+ // `prospects import|source --one-per-company`, `prospects source --enrich`, `prospects import --find-emails`.
139
141
  "one-per-company",
140
142
  "enrich",
143
+ "find-emails",
141
144
  // `targets update <id> --clear-framing`; `targets subqueries --retired`,
142
145
  // `targets subquery update <id> --retire` / `--restore`.
143
146
  "clear-framing",
@@ -580,6 +583,9 @@ Propose lane (the Fleet wizard's Plan and Name screens; persists nothing, spends
580
583
  (--plan '<json>' inline, or - for stdin).
581
584
 
582
585
  What does this org still need? (the router — reads only, decides nothing):
586
+ ${bin === "phantom" ? "onboard" : "phantom-onboard"} primitives [<domain>] [--retry] The primitives that fit a domain (default:
587
+ yours), each with the exact call that sets
588
+ it up — the plan Instances shows
583
589
  ${bin === "phantom" ? "onboard" : "phantom-onboard"} plan Every onboarding step (context, main blog,
584
590
  Planner, CMS, phantom) with its status:
585
591
  done / running / ready / blocked, and what
@@ -992,9 +998,10 @@ Sequences (multi-step cold email sent from your mailers; approving or enrolling
992
998
  [--[no-]opt-out-footer] [--[no-]postal-address-footer] [--[no-]unsubscribe-links] [--[no-]detect-reply-opt-outs]
993
999
  [--[no-]test-mode] [--default-lede <fallback opener line>]
994
1000
  [--send-window-start <HH:MM>] [--send-window-end <HH:MM>] [--send-window-timezone <IANA tz>]
995
- [--enroll-on-cake paid|scheduled|out_for_delivery|delivered|none]
1001
+ [--enroll-on-cake paid|scheduled|out_for_delivery|delivered|none] [--enroll-on-cake-list <list-id>|none]
996
1002
  --test-mode (internal accounts only): walks as live, sends nothing
997
1003
  --enroll-on-cake: start it when a recipient's Daymaker cake reaches that stage
1004
+ --enroll-on-cake-list: only for prospects in that list (none = any)
998
1005
  sequences pause|resume <sequence-id> Stop / restart sending
999
1006
  sequences enroll <sequence-id> (--list <list-id> | --email <addr>... | --recipients '<json>'|--recipients-file <path>)
1000
1007
  sequences stop <sequence-id> <enrollment-id> Stop one recipient
@@ -1023,6 +1030,9 @@ Prospects (who you write to):
1023
1030
  prospects get <prospect-id>
1024
1031
  prospects create-list --name <n>
1025
1032
  prospects import --csv-file <path|-> [--list <list-id>|--list-name <n>] [--one-per-company]
1033
+ [--find-emails [--max-email-lookups <1..100>]]
1034
+ --find-emails: look up the business email of rows with a
1035
+ LinkedIn URL and no email (paid; default 25 lookups)
1026
1036
  prospects source --description <who you want> [--list <list-id>|--list-name <n>] [--limit <1..100>]
1027
1037
  [--one-per-company] [--enrich] Fill a list from paid data providers (background);
1028
1038
  --enrich starts enrichment as soon as the pull lands
@@ -174,6 +174,12 @@ export const ARGS = {
174
174
  get_phantom_launch_status: {
175
175
  job_id: { internal: "the job id plan_phantom just returned" },
176
176
  },
177
+ // The primitive plan: a domain (default: the org's own) read into the calls that would
178
+ // stand each primitive up. Printed, never run — approving an item is running its call.
179
+ propose_primitive_plan: {
180
+ domain: { positional: 0, label: "domain" },
181
+ retry: { flag: "retry", type: "bool" },
182
+ },
177
183
  propose_phantom_plan: {
178
184
  client_domain: { positional: 0, label: "client-domain" },
179
185
  direction: ["about", "direction"],
@@ -752,8 +758,62 @@ async function cmdOnboardNext(ctx) {
752
758
  return 0;
753
759
  }
754
760
 
761
+ // `onboard primitives [<domain>]` — what Letterstory would stand up for this company: one
762
+ // read of propose_primitive_plan, printed as a table whose last column is the exact
763
+ // `call` that sets each item up. Nothing runs; approving an item is running its line.
764
+ // A first read crawls the site; `pending` means one is already in flight, so wait and ask.
765
+ function shellQuote(json) {
766
+ return `'${json.replace(/'/g, "'\\''")}'`;
767
+ }
768
+
769
+ function formatPrimitivePlan(ctx) {
770
+ return (res) => {
771
+ if (res.status !== "complete") {
772
+ return res.status === "pending"
773
+ ? `Still reading ${res.domain} — run this again in a few seconds.`
774
+ : `Couldn't read ${res.domain}: ${res.error ?? "unknown error"} (retry with --retry)`;
775
+ }
776
+ const { plan } = res;
777
+ const lines = [`${plan.company_name} (${plan.domain})`, ` ${plan.priority_topics.join(" · ")}`, ""];
778
+ for (const item of plan.items) {
779
+ // A part of its parent's setup prints indented under it.
780
+ const pad = item.parent ? " " : "";
781
+ const mark = item.status === "exists" ? "running " : "proposed";
782
+ lines.push(`${pad}${mark} ${item.title}`);
783
+ lines.push(
784
+ item.status === "exists"
785
+ ? `${pad} already: ${item.existing.map((e) => e.name).join(", ")}`
786
+ : `${pad} ${item.detail}`
787
+ );
788
+ if (item.call) {
789
+ lines.push(
790
+ `${pad} ${ctx.bin} call ${item.call.tool} --args ${shellQuote(JSON.stringify(item.call.args))}`
791
+ );
792
+ for (const [arg, ref] of Object.entries(item.refs ?? {})) {
793
+ lines.push(`${pad} with ${arg} = ${ref.field} from ${ref.item}`);
794
+ }
795
+ if (item.after?.length) lines.push(`${pad} after: ${item.after.join(", ")}`);
796
+ if (item.poll) lines.push(`${pad} then poll: ${item.poll.tool}`);
797
+ }
798
+ }
799
+ return lines.join("\n");
800
+ };
801
+ }
802
+
803
+ async function cmdOnboardPrimitives(ctx) {
804
+ const args = argsFromFlags(ARGS.propose_primitive_plan, ctx.flags, ctx.positionals);
805
+ ok(ctx, "Reading the domain…");
806
+ const res = await ctx.client.callTool("propose_primitive_plan", args);
807
+ printResult(ctx.io, ctx.flags, res, formatPrimitivePlan(ctx));
808
+ return 0;
809
+ }
810
+
755
811
  export async function cmdOnboard(ctx) {
756
812
  const { client, flags, io } = ctx;
813
+ // `onboard primitives [<domain>]` — the primitive plan and the call behind each item.
814
+ if (ctx.positionals[0] === "primitives") {
815
+ return cmdOnboardPrimitives({ ...ctx, positionals: ctx.positionals.slice(1) });
816
+ }
757
817
  // `onboard plan` / `onboard next` — the router: what does this org still need?
758
818
  if (ctx.positionals[0] === "plan") return cmdOnboardPlan(ctx);
759
819
  if (ctx.positionals[0] === "next") return cmdOnboardNext(ctx);
@@ -93,6 +93,13 @@ const SEQUENCE_WINDOW_ARGS = {
93
93
  // reaches it; `none` clears it.
94
94
  const CAKE_TRIGGER_STAGES = ["paid", "scheduled", "out_for_delivery", "delivered"];
95
95
  const CAKE_STAGES = ["draft", ...CAKE_TRIGGER_STAGES, "delivery_failed", "cancelled"];
96
+ const enrollOnCakeList = {
97
+ value: (flags) => {
98
+ const raw = flagStr(flags["enroll-on-cake-list"]);
99
+ if (raw === undefined) return undefined;
100
+ return raw === "none" ? null : raw;
101
+ },
102
+ };
96
103
  const enrollOnCake = {
97
104
  value: (flags) => {
98
105
  const raw = flagStr(flags["enroll-on-cake"]);
@@ -163,6 +170,7 @@ export const ARGS = {
163
170
  ...SEQUENCE_RAIL_ARGS,
164
171
  ...SEQUENCE_WINDOW_ARGS,
165
172
  enroll_on_cake: enrollOnCake,
173
+ enroll_on_cake_list_id: enrollOnCakeList,
166
174
  },
167
175
  update_sequence: {
168
176
  sequence_id: { positional: 0, label: "sequence-id", required: true },
@@ -173,6 +181,7 @@ export const ARGS = {
173
181
  ...SEQUENCE_RAIL_ARGS,
174
182
  ...SEQUENCE_WINDOW_ARGS,
175
183
  enroll_on_cake: enrollOnCake,
184
+ enroll_on_cake_list_id: enrollOnCakeList,
176
185
  },
177
186
  list_cakes: {
178
187
  stage: { flag: "stage", enum: CAKE_STAGES },
@@ -265,6 +274,8 @@ export const ARGS = {
265
274
  return csv;
266
275
  },
267
276
  },
277
+ find_emails: { flag: "find-emails", type: "bool" },
278
+ max_email_lookups: { flag: "max-email-lookups", type: "num" },
268
279
  one_per_company: { flag: "one-per-company", type: "bool" },
269
280
  list_id: "list",
270
281
  list_name: "list-name",
@@ -378,20 +389,8 @@ export const ARGS = {
378
389
  },
379
390
  };
380
391
 
381
- // Flags the computed `value` sources read (refuseUnknownFlags can't see them).
382
- const VALUE_FLAGS = {
383
- create_sequence: ["flow", "flow-file", "mailer", "send-window-start", "send-window-end"],
384
- update_sequence: ["flow", "flow-file", "mailer", "send-window-start", "send-window-end"],
385
- enroll_in_sequence: ["recipients", "recipients-file", "email"],
386
- update_review_item: ["body", "file", "personalize", "personalize-mode"],
387
- delete_prospects: ["id"],
388
- import_prospects_csv: ["csv", "csv-file"],
389
- update_prospect: ["custom"],
390
- set_mailer_forwarding: ["to", "clear"],
391
- };
392
-
393
392
  async function run(ctx, tool, message) {
394
- refuseUnknownFlags(ctx, ARGS[tool], VALUE_FLAGS[tool] ?? []);
393
+ refuseUnknownFlags(ctx, ARGS[tool]);
395
394
  const result = await ctx.client.callTool(tool, argsFromFlags(ARGS[tool], ctx.flags, ctx.positionals));
396
395
  if (message) ok(ctx, message);
397
396
  printResult(ctx.io, ctx.flags, result);
@@ -83,6 +83,35 @@ export const GLOBAL_FLAGS = new Set([
83
83
  "profile",
84
84
  ]);
85
85
 
86
+ /**
87
+ * The flags a computed `{ value }` source reads, found by running it against
88
+ * no flags and recording each key it looks up. A value source names its flags
89
+ * inside code the map can't see, so `sequences update --enroll-on-cake` was
90
+ * refused by a command whose help and parser both took it.
91
+ */
92
+ export function valueSourceFlags(value) {
93
+ const read = new Set();
94
+ const flags = new Proxy(
95
+ {},
96
+ {
97
+ get: (_target, key) => {
98
+ if (typeof key === "string") read.add(key);
99
+ return undefined;
100
+ },
101
+ has: (_target, key) => {
102
+ if (typeof key === "string") read.add(key);
103
+ return false;
104
+ },
105
+ }
106
+ );
107
+ try {
108
+ value(flags, []);
109
+ } catch {
110
+ // A required source throws when its flag is absent; the read was recorded first.
111
+ }
112
+ return read;
113
+ }
114
+
86
115
  /**
87
116
  * Refuse a flag this command does not understand.
88
117
  *
@@ -105,6 +134,8 @@ export function refuseUnknownFlags(ctx, argsMap, own = []) {
105
134
  else if (source && typeof source === "object" && source.flag) {
106
135
  allowed.add(source.flag);
107
136
  if (source.negate) allowed.add(source.negate);
137
+ } else if (source && typeof source === "object" && typeof source.value === "function") {
138
+ valueSourceFlags(source.value).forEach((f) => allowed.add(f));
108
139
  }
109
140
  }
110
141
  const unknown = Object.keys(ctx.flags).filter((f) => !allowed.has(f));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.90.0",
3
+ "version": "0.92.0",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",