@letterstory/cli 0.44.0 → 0.45.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/README.md CHANGED
@@ -386,8 +386,19 @@ letterstory admin crm accounts --stage demo_scheduled # with likelihood
386
386
  letterstory admin crm score <account-id> # recompute; prints the factors
387
387
  letterstory admin crm slack-channels [--all] # Watchdog channels + the account each is linked to
388
388
  letterstory admin crm link-slack <account-id> C0123ABC # the CRM's watch-only Slack link (`none` unlinks)
389
+
390
+ # One-click onboarding of a won client (key needs crm:write + fleet:onboard + org:create)
391
+ letterstory admin crm onboard <account-id> --domain acme.com \
392
+ --site "Where fintech compliance is heading" --site "" \
393
+ --slack jane@acme.com --invite jane@acme.com:admin --invite bob@acme.com
394
+ letterstory admin crm onboarding <account-id> # step-by-step progress (sites refreshed)
395
+ letterstory admin crm retry-onboarding <onboarding-id> # re-run only the failed steps
389
396
  ```
390
397
 
398
+ `onboard` runs org → link → phantom sites → Slack channel → checklist → app invites →
399
+ stage in one call. Each site is a real (not test) fleet build; `--site ""` lets the engine
400
+ infer its beat from the client's homepage.
401
+
391
402
  Every trigger — Cal.com bookings, Circleback recordings, Letterhome dinner RSVPs, RB2B
392
403
  visits, and what an agent records here — goes through one intake (`ingestSignal`,
393
404
  src/lib/crm/signals-intake.ts), so an agent's signal dedupes against a webhook's exactly
package/lib/cli.mjs CHANGED
@@ -52,7 +52,7 @@ import {
52
52
  } from "./commands.mjs";
53
53
 
54
54
  // Keep in sync with cli/package.json.
55
- export const VERSION = "0.44.0";
55
+ export const VERSION = "0.45.0";
56
56
 
57
57
  // Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
58
58
  // can't accidentally swallow the id as --json's value.
@@ -61,6 +61,7 @@ const BOOLEAN_FLAGS = new Set([
61
61
  "yes",
62
62
  "confirm-missing-target",
63
63
  "no-wait",
64
+ "no-checklist",
64
65
  "accept",
65
66
  "help",
66
67
  "version",
@@ -565,6 +566,9 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
565
566
  | white-glove [--suggested] | set-white-glove <account-id> on|off [--note "…"]
566
567
  | churn [--months 12] | record-churn <account-id> <reason> [--note "…"] [--at ISO]
567
568
  [--mrr-cents N] | undo-churn <account-id>
569
+ | onboard <account-id> --domain acme.com [--name "Acme"] [--site "<direction>"]… [--sites N]
570
+ [--slack a@acme.com,b@acme.com] [--invite a@acme.com[:admin]]… [--no-checklist]
571
+ | onboarding <account-id> | retry-onboarding <onboarding-id>
568
572
  The staff CRM's pipeline: the inbound signal
569
573
  feed, recording a signal with no webhook
570
574
  (Bookface, a tweet like), qualifying or
@@ -574,7 +578,14 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
574
578
  Churn: 30/90-day logo + revenue rates, who
575
579
  churned and why; Stripe cancellations are
576
580
  only suggestions until a person records one.
577
- Needs crm:read (reads) / crm:write (writes).
581
+ Onboard runs org → link → phantom sites →
582
+ Slack channel → checklist → app invites →
583
+ stage in one call (--site "" = infer the
584
+ beat from their homepage); retry re-runs
585
+ only what failed.
586
+ Needs crm:read (reads) / crm:write (writes);
587
+ onboard + retry also need fleet:onboard and
588
+ org:create.
578
589
  admin tuning-context [--q "<question>"] The experiment agent's context pack (JSON):
579
590
  the pipeline as deployed, levers, measures,
580
591
  goals, related history.
@@ -7,7 +7,7 @@
7
7
  // admin experiments list|show|new|update|note|started|spend|conclude|abandon … (tuning:experiment)
8
8
  // admin tuning-context [--q "<question>"] (tuning:experiment)
9
9
  // admin tuning-import <brief-results.json> [--experiment <id>] (tuning:experiment)
10
- // admin crm signals|record|signal|accounts|account|score|slack-channels|link-slack|white-glove|set-white-glove|churn|record-churn|undo-churn … (crm:read / crm:write)
10
+ // admin crm signals|record|signal|accounts|account|score|slack-channels|link-slack|white-glove|set-white-glove|churn|record-churn|undo-churn|onboard|onboarding|retry-onboarding … (crm:read / crm:write)
11
11
  // admin create-org --domain acme.com [--name "Acme"] [--test] [--no-key] [--key-name N]
12
12
  // [--capabilities a,b] [--onboard] [--about "…"] [--sitemap <url>]
13
13
  // [--cms <provider>] [--posts N] [--seers suggest|skip] [--no-visuals]
@@ -425,6 +425,57 @@ function accountLine(a) {
425
425
  return `${a.id} [${a.stage}] ${score} ${a.name}${a.domain ? ` (${a.domain})` : ""}${a.isYc ? " · YC" : ""}`;
426
426
  }
427
427
 
428
+ function onboardingLines(o) {
429
+ if (!o) return "(never onboarded from the CRM)";
430
+ const mark = { done: "✓", skipped: "–", failed: "✗", running: "…", pending: "·" };
431
+ return [
432
+ `${o.id} started ${o.startedAt.slice(0, 16).replace("T", " ")}${o.finishedAt ? " finished" : " in progress"}`,
433
+ ...o.steps.map(
434
+ (s) =>
435
+ ` ${mark[s.status] ?? "?"} ${s.key.padEnd(9)} ${s.status}${s.progress ? ` (${s.progress.done}/${s.progress.total})` : ""}${s.detail ? ` ${s.detail}` : ""}${s.error ? `\n error: ${s.error}` : ""}`
436
+ ),
437
+ ].join("\n");
438
+ }
439
+
440
+ /**
441
+ * `admin crm onboard` body from flags — the same CrmOnboardBodySchema the desk sends.
442
+ * --site "<direction>" per site (repeatable; "" = infer from the homepage) and/or
443
+ * --sites N (pads with inferred sites); --slack a@x,b@x (bare --slack = the channel, no
444
+ * invites yet); --invite email[:admin|editor] (repeatable); --no-checklist.
445
+ */
446
+ function onboardBody(flags) {
447
+ const domain = flagStr(flags.domain);
448
+ if (!domain) {
449
+ throw new CliError(
450
+ 'Usage: admin crm onboard <account-id> --domain acme.com [--name "Acme"] [--site "<direction>"]… [--sites N] [--slack a@x,b@x] [--invite a@x[:admin]]… [--no-checklist]'
451
+ );
452
+ }
453
+ const sites = flagList(flags.site).map((direction) => ({ direction: direction.trim() || null }));
454
+ const total = flagNum(flags.sites);
455
+ while (total !== undefined && sites.length < total) sites.push({ direction: null });
456
+ const slackEmails = flagList(flags.slack)
457
+ .flatMap((v) => v.split(","))
458
+ .map((e) => e.trim())
459
+ .filter(Boolean);
460
+ const slack = flags.slack === undefined ? null : { emails: slackEmails };
461
+ const invites = flagList(flags.invite)
462
+ .flatMap((v) => v.split(","))
463
+ .map((v) => v.trim())
464
+ .filter(Boolean)
465
+ .map((v) => {
466
+ const [email, role] = v.split(":");
467
+ return { email, role: role === "admin" ? "admin" : "editor" };
468
+ });
469
+ return compact({
470
+ domain,
471
+ name: flagStr(flags.name),
472
+ sites,
473
+ slack,
474
+ invites,
475
+ checklist: flagBool(flags["no-checklist"]) ? false : undefined,
476
+ });
477
+ }
478
+
428
479
  /**
429
480
  * `admin crm …` — the headless half of the staff CRM, so a background agent can watch the
430
481
  * pipeline and record the signals that have no webhook (Bookface, tweet likes, DMs).
@@ -623,9 +674,29 @@ async function cmdAdminCrm(ctx) {
623
674
  const r = await staffRequest(ctx, "DELETE", `${CRM}/accounts/${id}/churn`);
624
675
  return out(r, (x) => `churn undone: ${accountLine(x.account)}`);
625
676
  }
677
+ case "onboard": {
678
+ // admin crm onboard <account-id> --domain acme.com … — one-click onboarding of a won
679
+ // client. The key needs crm:write + fleet:onboard + org:create.
680
+ if (!id) throw new CliError("Usage: admin crm onboard <account-id> --domain acme.com … (see --help)");
681
+ const r = await staffPost(ctx, `${CRM}/accounts/${id}/onboard`, onboardBody(flags));
682
+ return out(r, (x) => onboardingLines(x.onboarding));
683
+ }
684
+ case "onboarding": {
685
+ // admin crm onboarding <account-id> — the latest onboarding, sites refreshed.
686
+ if (!id) throw new CliError("Usage: admin crm onboarding <account-id>");
687
+ return out(await staffRequest(ctx, "GET", `${CRM}/accounts/${id}/onboard`), (x) =>
688
+ onboardingLines(x.onboarding)
689
+ );
690
+ }
691
+ case "retry-onboarding": {
692
+ // admin crm retry-onboarding <onboarding-id> — re-run the failed/pending steps.
693
+ if (!id) throw new CliError("Usage: admin crm retry-onboarding <onboarding-id>");
694
+ const r = await staffPost(ctx, `${CRM}/onboardings/${id}/retry`, {});
695
+ return out(r, (x) => onboardingLines(x.onboarding));
696
+ }
626
697
  default:
627
698
  throw new CliError(
628
- "Usage: admin crm signals|record|signal|accounts|account|score|slack-channels|link-slack|white-glove|set-white-glove|churn|record-churn|undo-churn … (see --help)"
699
+ "Usage: admin crm signals|record|signal|accounts|account|score|slack-channels|link-slack|white-glove|set-white-glove|churn|record-churn|undo-churn|onboard|onboarding|retry-onboarding … (see --help)"
629
700
  );
630
701
  }
631
702
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.44.0",
3
+ "version": "0.45.0",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",