@letterstory/cli 0.177.16 → 0.177.18

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
@@ -88,7 +88,7 @@ import {
88
88
  import { fetchParkedSections, withoutParked } from "./parked.mjs";
89
89
 
90
90
  // Matches cli/package.json; publish-cli.yml rewrites both to the published version.
91
- export const VERSION = "0.177.16";
91
+ export const VERSION = "0.177.18";
92
92
 
93
93
  /** The exit code for a gate refusal (CARD_REQUIRED, PAYMENT_FAILED, OUT_OF_TOKENS): 1 is a plain error. */
94
94
  export const EXIT_PAYMENT_REQUIRED = 3;
@@ -106,6 +106,10 @@ export const BOOLEAN_FLAGS = new Set([
106
106
  "pull-requests",
107
107
  "make-orgs",
108
108
  "no-refit",
109
+ // build --check: read every gate a start meets and start nothing.
110
+ "check",
111
+ // admin invariants: press every startable row as a check too.
112
+ "presses",
109
113
  // admin targets check: check every target again, checked or not.
110
114
  "recheck",
111
115
  // build list: also move the builds' open changes along while polling.
@@ -1126,6 +1130,10 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
1126
1130
  admin recipes reread --org <id>,… Read these companies from their sites again: what was
1127
1131
  only ever inferred is replaced, what the company changed
1128
1132
  is kept. Refit them after. Needs recipes:write.
1133
+ admin invariants [--org <id>,…] [--limit 50] [--presses]
1134
+ The Ideas invariants over production, read only: rows a
1135
+ start would refuse, builds whose reading disagrees with
1136
+ what they hold, grouped with ids. Exits 1 on any.
1129
1137
  admin targets check [--org <id>,…] [--recheck] [--limit 10]
1130
1138
  Check ranking targets' phrases (buyer intent, self-named,
1131
1139
  search demand, fit) and store each verdict: --org those
@@ -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 endings [--days N] [--write-cases] (how turns and repo changes ended when not done, by owner)
25
25
  // admin letterpress settings|flip|set … (recipes:read / recipes:write)
26
+ // admin invariants [--org <id>,…] [--limit N] [--presses] (staff)
26
27
  // admin demo guide|list|show|set|reset|check|ideas … (demo:read / demo:write)
27
28
  // admin deployments [--health down|failing|blocked|setting_up|live|paused|archived] [--org <id>] [--q <text>] [--archived] [--test] [--no-host] (fleet:read)
28
29
  // admin models runs|run|star|unstar|scoreboard|requests|request|sampling … (models:read / models:write)
@@ -62,7 +63,42 @@ import {
62
63
  compact,
63
64
  ok,
64
65
  requirePositional,
66
+ argsFromFlags,
65
67
  } from "./shared.mjs";
68
+
69
+ // `admin invariants`: the one tool it calls, its flags declared (scripts/parity-check.ts reads this map).
70
+ export const ARGS = {
71
+ check_ideas_invariants: {
72
+ org_ids: { value: (flags) => (flags.org ? flagCsv(flags.org) : undefined) },
73
+ limit: { flag: "limit", type: "num" },
74
+ presses: { flag: "presses", type: "bool" },
75
+ },
76
+ };
77
+
78
+ const INVARIANT_LIMIT = 30;
79
+
80
+ // `admin invariants` — the Ideas invariants over production (docs/architecture/ideas-invariants.md), read only:
81
+ // check_ideas_invariants, grouped by invariant with ids. Exits 1 when any holds false.
82
+ async function cmdAdminInvariants(ctx) {
83
+ const { client, flags, io } = ctx;
84
+ const result = await client.callTool("check_ideas_invariants", argsFromFlags(ARGS.check_ideas_invariants, flags));
85
+ printResult(io, flags, result, (r) =>
86
+ [
87
+ `${r.total} violation(s) on ${r.accounts} account(s)${r.unread.length ? `, ${r.unread.length} not read` : ""}.`,
88
+ ...r.by_invariant.map((g) =>
89
+ [
90
+ `\n${g.invariant} ${g.count} (${g.owner})`,
91
+ ...g.violations
92
+ .slice(0, INVARIANT_LIMIT)
93
+ .map((v) => ` ${v.host} ${v.org_id} ${v.subject}\n ${v.evidence}`),
94
+ ...(g.count > INVARIANT_LIMIT ? [` … ${g.count - INVARIANT_LIMIT} more (--json for all)`] : []),
95
+ ].join("\n")
96
+ ),
97
+ ...r.unread.map((u) => `not read: ${u.org_id}: ${u.error}`),
98
+ ].join("\n")
99
+ );
100
+ return result.total > 0 ? 1 : 0;
101
+ }
66
102
  import { watchDomainJob } from "./onboard.mjs";
67
103
 
68
104
  const SEER_MODES = ["suggest", "skip"];
@@ -5141,12 +5177,14 @@ const RECIPE_FILE_FIELDS = [
5141
5177
  "tool_access",
5142
5178
  "primitives_access",
5143
5179
  "immediate",
5180
+ // Whether its row must name what it acts on (2026-11-11); left out here, an export would drop it.
5181
+ "names_subject",
5144
5182
  "status",
5145
5183
  "needs",
5146
5184
  "variations",
5147
5185
  ];
5148
5186
  const RECIPES_USAGE =
5149
- "Usage: admin recipes list [--kind recipe|known_issue] [--status live|parked] [--approach seo|geo|agent_conviction|inbound_conversion|outbound|other] | show <id|name> | runs <id|name> [--days 1..90] [--limit n] | vocab | stats | export [--out <file>] | import <file> [--dry-run] | refit [--org <id>,…] [--recipe <id|name>] [--limit n] | save [<id>] [--file <recipe.json>] [--kind recipe|known_issue] [--name …] [--description …] [--recipe …] [--when-to-use …] [--when-not-to-use …] [--weight 1..10] [--approaches seo,geo,…] [--reads llms_txt,github,…] [--runs <catalog action id>] [--primitives-access true|false] [--immediate true|false] [--status live|parked] [--needs <what a parked recipe is missing>] | delete <id> --yes";
5187
+ "Usage: admin recipes list [--kind recipe|known_issue] [--status live|parked] [--approach seo|geo|agent_conviction|inbound_conversion|outbound|other] | show <id|name> | runs <id|name> [--days 1..90] [--limit n] | vocab | stats | export [--out <file>] | import <file> [--dry-run] | refit [--org <id>,…] [--recipe <id|name>] [--limit n] | save [<id>] [--file <recipe.json>] [--kind recipe|known_issue] [--name …] [--description …] [--recipe …] [--when-to-use …] [--when-not-to-use …] [--weight 1..10] [--approaches seo,geo,…] [--reads llms_txt,github,…] [--runs <catalog action id>] [--primitives-access true|false] [--immediate true|false] [--names-subject true|false] [--status live|parked] [--needs <what a parked recipe is missing>] | delete <id> --yes";
5150
5188
 
5151
5189
  const recipeForFile = (r) => Object.fromEntries(RECIPE_FILE_FIELDS.map((k) => [k, r[k]]));
5152
5190
 
@@ -5196,6 +5234,7 @@ function recipeDetail(r) {
5196
5234
  : r.primitives_access
5197
5235
  ? [" letter's tools: its build may read the account through them (primitives_access)"]
5198
5236
  : []),
5237
+ ...(r.names_subject ? [" title: names what it acts on, or it is a no (names_subject)"] : []),
5199
5238
  ...(variations.length > 1
5200
5239
  ? variations.flatMap((v, i) => [` variation ${i + 1}: ${v.name}`, ...fields(v, " ")])
5201
5240
  : fields(r, " ")),
@@ -5250,6 +5289,8 @@ function recipeFromFlags(flags) {
5250
5289
  flags["primitives-access"] === undefined ? undefined : String(flags["primitives-access"]) === "true",
5251
5290
  // --immediate is accepted for old scripts and read by nothing: a quick row is one whose runs is empty.
5252
5291
  immediate: flags.immediate === undefined ? undefined : String(flags.immediate) === "true",
5292
+ // --names-subject true: its row names what it acts on, or is a no; false: it may keep its general name.
5293
+ names_subject: flags["names-subject"] === undefined ? undefined : String(flags["names-subject"]) === "true",
5253
5294
  // --reads llms_txt,github declares the facts its conditions rest on; --reads "" clears them.
5254
5295
  reads: flagStr(flags.reads)
5255
5296
  ?.split(",")
@@ -7344,10 +7385,11 @@ export async function cmdAdmin(ctx) {
7344
7385
  if (sub === "letterpress") return cmdAdminLetterpress({ ...ctx, positionals: ctx.positionals.slice(1) });
7345
7386
  if (sub === "demo") return cmdAdminDemo({ ...ctx, positionals: ctx.positionals.slice(1) });
7346
7387
  if (sub === "targets") return cmdAdminTargets({ ...ctx, positionals: ctx.positionals.slice(1) });
7388
+ if (sub === "invariants") return cmdAdminInvariants({ ...ctx, positionals: ctx.positionals.slice(1) });
7347
7389
  if (sub === "deployments" || sub === "deploys")
7348
7390
  return cmdAdminDeployments({ ...ctx, positionals: ctx.positionals.slice(1) });
7349
7391
  throw new CliError(
7350
- 'Usage: admin recipes … | admin letterpress … | admin demo … | admin targets … | 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)'
7392
+ 'Usage: admin recipes … | admin letterpress … | admin demo … | admin targets … | admin invariants … | 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)'
7351
7393
  );
7352
7394
  }
7353
7395
 
@@ -105,6 +105,8 @@ const ARGS = {
105
105
  site_name: "site-name",
106
106
  // --plan-only: answer what it would do and start nothing (--json for the plan to send back).
107
107
  plan_only: { flag: "plan-only", type: "bool" },
108
+ // --check: read every gate and say whether a press would start it, starting and recording nothing.
109
+ check: { flag: "check", type: "bool" },
108
110
  // --plan-only always plans what it's given, as /plan does: it was asked for by name.
109
111
  asked_to_plan: { value: (flags) => (flags["plan-only"] ? true : undefined) },
110
112
  // --plan-file <path>: build exactly the plan --plan-only answered.
@@ -281,6 +283,7 @@ async function buildStart(ctx) {
281
283
  "plan-only",
282
284
  "plan-file",
283
285
  "branch",
286
+ "check",
284
287
  ...FILE_FLAGS,
285
288
  ]);
286
289
  const files = await attachedFiles(ctx);
@@ -294,6 +297,7 @@ async function buildStart(ctx) {
294
297
  ...r.plan.changes.map((c) => ` ${c.title ?? c.instructions}`),
295
298
  ...r.plan.actions.map((a) => ` ${a.title}`),
296
299
  ].join("\n");
300
+ if (r.status === "would_start") return r.message ?? "This would start now.";
297
301
  if (r.status !== "started") return r.message ?? r.status;
298
302
  const steps = (r.build?.steps ?? []).map((s) => ` ${s.status} ${s.title}${s.note ? `: ${s.note}` : ""}`);
299
303
  return [`Started: ${r.build.title} (${r.build.id})`, ...steps].join("\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.177.16",
3
+ "version": "0.177.18",
4
4
  "description": "Spin up and manage Letter phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",