@letterstory/cli 0.176.51 → 0.176.53

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
@@ -85,7 +85,7 @@ import {
85
85
  import { fetchParkedSections, withoutParked } from "./parked.mjs";
86
86
 
87
87
  // Matches cli/package.json; publish-cli.yml rewrites both to the published version.
88
- export const VERSION = "0.176.51";
88
+ export const VERSION = "0.176.53";
89
89
 
90
90
  /** The exit code for a gate refusal (CARD_REQUIRED, PAYMENT_FAILED, OUT_OF_TOKENS): 1 is a plain error. */
91
91
  export const EXIT_PAYMENT_REQUIRED = 3;
@@ -117,10 +117,12 @@ export const BOOLEAN_FLAGS = new Set([
117
117
  "keep-key",
118
118
  "delete-key",
119
119
  "keep",
120
- // automate add/set: on or off; only identified visitors marked hot leads.
120
+ // automate add/set: on or off; only identified visitors marked hot leads; run the prompt in a Build sandbox.
121
121
  "on",
122
122
  "off",
123
123
  "hot-leads-only",
124
+ "sandbox",
125
+ "no-sandbox",
124
126
  // admin crm prospect-org: start (or retry) the org prepared for the booked call.
125
127
  "start",
126
128
  // site research-builds: run now even when the stored pass is under an hour old.
@@ -136,6 +138,8 @@ export const BOOLEAN_FLAGS = new Set([
136
138
  // build: repeat an open build's ask; post its progress in the org's Slack channel.
137
139
  "again",
138
140
  "plan-only",
141
+ // build: every change reads Letter's tools from its sandbox.
142
+ "primitives-access",
139
143
  // build list: only Build's Archived tab (--open, already listed, is its Open tab).
140
144
  "archived",
141
145
  // plan pending --queued: only the rows a person queued for later.
@@ -177,6 +181,8 @@ export const BOOLEAN_FLAGS = new Set([
177
181
  "no-modifiers",
178
182
  // site map: do not start an audit for a host that has none.
179
183
  "no-audit",
184
+ // admin deployments: answer from our rows alone, without reading the host.
185
+ "no-host",
180
186
  // site metrics: skip the previous window (no deltas, no revamp indicator).
181
187
  "no-compare",
182
188
  // site refresh / observe: the loop's switches.
@@ -631,17 +637,22 @@ Reviewers:
631
637
  automate list Your automations: an event, which ones it acts on,
632
638
  and what Letterpress then does
633
639
  automate show <automation-id>
634
- automate add [--template post_new_issues|recommended_actions|competitor_pages|icp_visitors|vc_visitors|plan_updates]
635
- [--name <n>] [--trigger competitor_page|visitor_identified|issue_found|build_suggestion|plan_update|custom]
640
+ automate add [--template post_new_issues|fix_new_issues|recommended_actions|competitor_pages|competitor_takes|
641
+ icp_visitors|vc_visitors|plan_updates|weekly_release_notes|monthly_page_refresh]
642
+ [--name <n>] [--trigger competitor_page|visitor_identified|issue_found|build_suggestion|plan_update|custom|schedule]
636
643
  [--prompt <what Letterpress does with each event>] [--slack-channel <C…>] [--instruction <which events>]
637
644
  [--event <name>] [--kind post|landing] [--competitor <host>] [--site <host>]
645
+ [--every <n> --unit hours|days|weeks|months] [--sandbox]
638
646
  [--hot-leads-only] [--per-day <n>] [--backfill all|<YYYY-MM-DD>] [--off]
639
647
  Make one (a template fills what you leave out): it starts
640
648
  from now (--backfill also runs it on past events, oldest
641
649
  first), acts on its own, and posts each event it acts on
642
- to --slack-channel with its buttons
650
+ to --slack-channel with its buttons. --trigger schedule
651
+ --every 2 --unit weeks is a recurring one (on the calendar:
652
+ Monday, the 1st); --sandbox always runs its prompt in a Build
653
+ sandbox (off, Letterpress decides per event)
643
654
  automate set <automation-id> [--name <n>] [--instruction <text>] [--prompt <text>] [--slack-channel <C…>|none]
644
- [--backfill all|<YYYY-MM-DD>] [--on|--off] ...
655
+ [--sandbox|--no-sandbox] [--backfill all|<YYYY-MM-DD>] [--on|--off] ...
645
656
  automate try [<automation-id>] [--trigger <t> --prompt <text> --instruction <text> ...] [--limit <n>]
646
657
  What it would do with the latest events (acts on none)
647
658
  automate runs [--automation <id>] [--status acting|acted|skipped|failed|proposed|rejected] [--limit <n>]
@@ -1005,6 +1016,10 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
1005
1016
  read-only SQL across every org; --sql prints
1006
1017
  the queries behind the answer. Needs the
1007
1018
  fleet:query capability on the staff key.
1019
+ admin deployments [--health down|failing|…] [--org <id>] [--q <text>] [--archived] [--test] [--no-host] [--json]
1020
+ Every site Letter built and hosts, across every org,
1021
+ worst first: health, the problem, and what the host
1022
+ has that no app names. Needs fleet:read.
1008
1023
  admin recipes list | show <id|name> | vocab | stats Recipes: Letter's one library of direction for
1009
1024
  Letterpress on Plan, Build and Fix (/superadmin/recipes),
1010
1025
  each a name, a description, the recipe (what Letterpress
@@ -1720,6 +1735,7 @@ Build (a change to your site, or launching what isn't code; the Build box's own
1720
1735
  build <words…> [--prompt <text>] [--site <host>] [--subject <host/path>]... [--action build|build_pr|build_merge|slack|linear]
1721
1736
  [--attach <path>]... [--attach-url <name>=<https url>]... [--attach-ref <path>]... [--attach-url-ref <name>=<https url>]...
1722
1737
  [--harness letterpress-1|claude-code|codex] [--effort low|medium|high|xhigh] [--notify-slack] [--again] [--new-site]
1738
+ [--primitives-access]
1723
1739
  Build from your words: "add a pricing page" opens a preview or PR;
1724
1740
  "launch a phantom for us", "set up mailers" or "onboard my blog" is
1725
1741
  done by Build itself (a phantom site buys its own domain), and the
@@ -23,6 +23,7 @@
23
23
  // admin letterpress beyond [--limit N] [--before ISO] (what members asked that went past the library)
24
24
  // admin letterpress settings|flip|set … (recipes:read / recipes:write)
25
25
  // admin demo guide|list|show|set|reset|check … (demo:read / demo:write)
26
+ // admin deployments [--health down|failing|blocked|setting_up|live|paused|archived] [--org <id>] [--q <text>] [--archived] [--test] [--no-host] (fleet:read)
26
27
  // admin models runs|run|star|unstar|scoreboard|requests|request|sampling … (models:read / models:write)
27
28
  // admin replays [--org <id>] [--target <id>] [--status …] [--limit n] (fleet:read)
28
29
  // admin replay --org <id> [<response-id> | --target <id>] [--surface …] (fleet:replay)
@@ -6960,11 +6961,59 @@ export async function cmdAdmin(ctx) {
6960
6961
  if (sub === "recipes") return cmdAdminRecipes({ ...ctx, positionals: ctx.positionals.slice(1) });
6961
6962
  if (sub === "letterpress") return cmdAdminLetterpress({ ...ctx, positionals: ctx.positionals.slice(1) });
6962
6963
  if (sub === "demo") return cmdAdminDemo({ ...ctx, positionals: ctx.positionals.slice(1) });
6964
+ if (sub === "deployments" || sub === "deploys")
6965
+ return cmdAdminDeployments({ ...ctx, positionals: ctx.positionals.slice(1) });
6963
6966
  throw new CliError(
6964
- 'Usage: admin recipes … | admin letterpress … | admin demo … | admin republish … | admin create-org --domain <bare-domain> [--onboard] … | admin ask "<question>" | admin patterns … | admin experiments … | admin fleet-experiments … | admin crm … | admin spend … | admin evals … | admin brand-detection … | admin tuning … | admin tuning-context | admin replays … (see --help)'
6967
+ 'Usage: admin recipes … | admin letterpress … | admin demo … | admin deployments … | admin republish … | admin create-org --domain <bare-domain> [--onboard] … | admin ask "<question>" | admin patterns … | admin experiments … | admin fleet-experiments … | admin crm … | admin spend … | admin evals … | admin brand-detection … | admin tuning … | admin tuning-context | admin replays … (see --help)'
6965
6968
  );
6966
6969
  }
6967
6970
 
6971
+ const MANAGED_HEALTH = ["down", "failing", "blocked", "setting_up", "live", "paused", "archived"];
6972
+
6973
+ // `admin deployments` — every site Letter built and hosts, across every organization
6974
+ // (docs/architecture/deployments.md, "Every managed site"): worst first, each with its org, domain,
6975
+ // health and the problem in one sentence, then what the host has that no app of ours names. The
6976
+ // same read as Super Admin → Deployments and the staff-only `list_managed_deployments` tool.
6977
+ async function cmdAdminDeployments(ctx) {
6978
+ const { flags, io } = ctx;
6979
+ const health = flagStr(flags.health);
6980
+ if (health !== undefined && !MANAGED_HEALTH.includes(health))
6981
+ throw new CliError(`--health is one of ${MANAGED_HEALTH.join(", ")}.`);
6982
+ const params = new URLSearchParams();
6983
+ if (health) params.set("health", health);
6984
+ if (flagStr(flags.org)) params.set("org_id", flagStr(flags.org));
6985
+ if (flagStr(flags.q)) params.set("q", flagStr(flags.q));
6986
+ if (flagBool(flags.archived)) params.set("include_archived", "1");
6987
+ if (flagBool(flags.test)) params.set("include_test", "1");
6988
+ if (flagBool(flags["no-host"])) params.set("check_host", "0");
6989
+ const qs = params.toString();
6990
+ const result = await staffRequest(ctx, "GET", `/api/superadmin/deployments${qs ? `?${qs}` : ""}`);
6991
+ printResult(io, flags, result, (r) => {
6992
+ const c = r.counts;
6993
+ const lines = [
6994
+ `${c.total} managed site${c.total === 1 ? "" : "s"} across ${c.orgs} org${c.orgs === 1 ? "" : "s"}: ` +
6995
+ MANAGED_HEALTH.filter((h) => c[h] > 0)
6996
+ .map((h) => `${c[h]} ${h.replace("_", " ")}`)
6997
+ .join(", "),
6998
+ ];
6999
+ if (!r.apps.length) lines.push(" (none match)");
7000
+ for (const a of r.apps) {
7001
+ lines.push(
7002
+ `${a.id} ${a.health.padEnd(10)} ${a.name.slice(0, 26).padEnd(26)} ${(a.org.name ?? a.org.id).slice(0, 22).padEnd(22)} ${a.url ?? a.domain ?? "no domain yet"}`
7003
+ );
7004
+ if (a.problem) lines.push(` ${a.problem}`);
7005
+ else if (a.health === "setting_up" && a.stage_note) lines.push(` ${a.stage}: ${a.stage_note}`);
7006
+ }
7007
+ if (r.host.read) {
7008
+ lines.push("", `Host: ${r.host.apps} app${r.host.apps === 1 ? "" : "s"} on our account.`);
7009
+ for (const u of r.host.untracked)
7010
+ lines.push(` untracked ${u.name} (${u.status}) ${u.url ?? ""}`.trimEnd());
7011
+ } else lines.push("", `Host not read: ${r.host.note}`);
7012
+ return lines.join("\n");
7013
+ });
7014
+ return 0;
7015
+ }
7016
+
6968
7017
  // `admin brand-detection <org-id>` — how the client's brand is detected in AI answers;
6969
7018
  // with --term / --exact / --exclude it SETS the exact set (replacing the inferred one),
6970
7019
  // --clear goes back to inferred (recorded as a decision, with --note as the why). Every
@@ -7,15 +7,28 @@
7
7
  import { CliError } from "../client.mjs";
8
8
  import { printResult, ok, argsFromFlags, flagList, flagStr, refuseUnknownFlags } from "./shared.mjs";
9
9
 
10
- const TRIGGERS = ["competitor_page", "visitor_identified", "issue_found", "build_suggestion", "plan_update", "custom"];
10
+ const TRIGGERS = [
11
+ "competitor_page",
12
+ "visitor_identified",
13
+ "issue_found",
14
+ "build_suggestion",
15
+ "plan_update",
16
+ "custom",
17
+ "schedule",
18
+ ];
11
19
  const TEMPLATES = [
12
20
  "post_new_issues",
21
+ "fix_new_issues",
13
22
  "recommended_actions",
14
23
  "competitor_pages",
24
+ "competitor_takes",
15
25
  "icp_visitors",
16
26
  "vc_visitors",
17
27
  "plan_updates",
28
+ "weekly_release_notes",
29
+ "monthly_page_refresh",
18
30
  ];
31
+ const UNITS = ["hours", "days", "weeks", "months"];
19
32
  const STATUSES = ["pending", "proposed", "acting", "acted", "skipped", "failed", "rejected"];
20
33
 
21
34
  const triggerParams = {
@@ -26,8 +39,15 @@ const triggerParams = {
26
39
  hot_leads_only: { flag: "hot-leads-only", type: "bool" },
27
40
  event: { flag: "event" },
28
41
  per_day: { flag: "per-day", type: "num" },
42
+ // --trigger schedule --every 2 --unit weeks: a recurring automation, on the calendar (Monday, the 1st).
43
+ every: { flag: "every", type: "num" },
44
+ unit: { flag: "unit", enum: UNITS },
29
45
  },
30
46
  };
47
+ // --sandbox / --no-sandbox: always run the prompt in a Build sandbox (the repo and Letter's tools); off, Letterpress decides per event.
48
+ const sandbox = {
49
+ value: (flags) => (flags["no-sandbox"] === true ? false : flags.sandbox === true ? true : undefined),
50
+ };
31
51
  // --slack-channel C0123ABCD (repo slack-targets), or none to post nothing.
32
52
  const slackChannel = {
33
53
  value: (flags) =>
@@ -96,6 +116,7 @@ export const ARGS = {
96
116
  instruction: { flag: "instruction" },
97
117
  prompt: { flag: "prompt" },
98
118
  slack_channel: slackChannel,
119
+ sandbox,
99
120
  enabled,
100
121
  backfill,
101
122
  },
@@ -108,6 +129,7 @@ export const ARGS = {
108
129
  instruction: { flag: "instruction" },
109
130
  prompt: { flag: "prompt" },
110
131
  slack_channel: slackChannel,
132
+ sandbox,
111
133
  enabled,
112
134
  backfill,
113
135
  },
@@ -50,6 +50,8 @@ const ARGS = {
50
50
  files: { value: (_flags, _positionals, extra) => (extra.files?.length ? extra.files : undefined) },
51
51
  delivery: { flag: "action", enum: ["build", "build_pr", "build_merge", "slack", "linear"] },
52
52
  notify_slack: { flag: "notify-slack", type: "bool" },
53
+ // --primitives-access: every change reads Letter's tools from its sandbox (a read-only MCP key beside the repo).
54
+ primitives_access: { flag: "primitives-access", type: "bool" },
53
55
  harness: { flag: "harness", enum: ["letterpress-1", "claude-code", "codex"] },
54
56
  // --branch <name>: where its changes start and its pull requests go; omitted, the branch that builds the site.
55
57
  base_branch: "branch",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.176.51",
3
+ "version": "0.176.53",
4
4
  "description": "Spin up and manage Letter phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",