@letterstory/cli 0.177.22 → 0.177.24

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.22";
91
+ export const VERSION = "0.177.24";
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;
@@ -97,6 +97,8 @@ export const EXIT_PAYMENT_REQUIRED = 3;
97
97
  // can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
98
98
  // `negate` spelling) in a command's ARGS map must be listed; boolean-flags.test.ts enforces it.
99
99
  export const BOOLEAN_FLAGS = new Set([
100
+ // org mcp add: the server signs in with OAuth instead of a header value.
101
+ "sign-in",
100
102
  // collection onboard: connect without drafting or scheduling.
101
103
  "connect-only",
102
104
  // admin demo ideas: every live recipe, not only the Ideas that fit.
@@ -106,6 +108,8 @@ export const BOOLEAN_FLAGS = new Set([
106
108
  "pull-requests",
107
109
  "make-orgs",
108
110
  "no-refit",
111
+ // admin demo sweep --new-org: one fresh test org for exactly the picks.
112
+ "new-org",
109
113
  // build --check: read every gate a start meets and start nothing.
110
114
  "check",
111
115
  // admin invariants: press every startable row as a check too.
@@ -1226,11 +1230,12 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
1226
1230
  admin demo ideas <org-id> [--library] [--preview] [--force] | --status [--id <run>]
1227
1231
  Start every Idea on a demo org for real (pull requests, or
1228
1232
  previews), or read how the latest run went.
1229
- admin demo sweep [--prepare] [--pull-requests] [--recipes a,b] [--variation <n|name>] [--orgs a,b] [--exclude a,b] [--buy a,b] [--make-orgs] [--no-refit] | --status [--id <sweep>] | --reset <recipe> [--id <sweep>]
1233
+ admin demo sweep [--prepare] [--pull-requests] [--recipes a,b] [--variation <n|name>] [--pick <recipe>[:<variation>]]... [--new-org] [--orgs a,b] [--exclude a,b] [--buy a,b] [--make-orgs] [--no-refit] | --status [--id <sweep>] | --reset <recipe> [--id <sweep>]
1230
1234
  Every live recipe end to end: each gets a test org whose Ideas
1231
1235
  offers it, its Start pressed (or, with --prepare, left for you
1232
1236
  to press in the app), and its outcome judged, with who owns a
1233
- failure. Exits 1 when the sweep failed.
1237
+ failure. --new-org --pick a --pick b:2 makes one fresh test org
1238
+ for exactly those recipes. Exits 1 when the sweep failed.
1234
1239
  admin brand-detection <org-id> [--term <t>]... [--exact <t>]... [--exclude <phrase>]... [--note "…"] | --clear [--note "…"]
1235
1240
  How a client's brand is detected in AI answers, with
1236
1241
  every earlier setting; with flags, set the exact set
@@ -7007,7 +7007,7 @@ const DEMO_ITEM_FLAGS = [
7007
7007
  "setup",
7008
7008
  ];
7009
7009
  const DEMO_USAGE =
7010
- "Usage: admin demo guide | list [--enrollable] | show <org-id> [--moves] | set <org-id> [--enroll] [--unenroll] [--file <state.json>] [--phantom parked|shown] [--main-blog …] [--visitors …] [--probes not_run|shown] [--cms hidden|shown] [--publish-target …] [--search-console …] [--github …] [--domain …] [--setup fresh|finished] [--domain-cap n] [--inject <key>]... [--hide <key>]... [--save-as <name>] [--baseline <owner/repo>[@sha] | --clear-baseline] [--copy-from <org-id> [--copy-scenarios a,b] [--overwrite]] [--auto-reset <scenario>|off] [--every <minutes>] [--release] | domains | new <domain> [--name <n>] [--brief <t>] [--full] [--host vercel|pages] [--no-wait] | teardown <org-id> [--keep-repo] | bootstrap <org-id>... --from <org-id> [--scenarios a,b] [--overwrite] | reset <org-id> <scenario> [--clean-slate] [--close-prs since_applied|all_open|none] [--hold <minutes>] [--restore-repo revert|force] [--rebuild] [--reread-company] [--forget-company] [--forget-site] | check <org-id> | ideas <org-id> [--library] [--preview] [--force] | ideas <org-id> --status [--id <run-id>] | sweep [--prepare] [--pull-requests] [--recipes <id>,…] [--variation <n|name>] [--orgs <id>,…] [--exclude <id>,…] [--buy <recipe-id>,…] [--make-orgs] [--no-refit] | sweep --status [--id <sweep-id>] | sweep --reset <recipe-id> [--id <sweep-id>]";
7010
+ "Usage: admin demo guide | list [--enrollable] | show <org-id> [--moves] | set <org-id> [--enroll] [--unenroll] [--file <state.json>] [--phantom parked|shown] [--main-blog …] [--visitors …] [--probes not_run|shown] [--cms hidden|shown] [--publish-target …] [--search-console …] [--github …] [--domain …] [--setup fresh|finished] [--domain-cap n] [--inject <key>]... [--hide <key>]... [--save-as <name>] [--baseline <owner/repo>[@sha] | --clear-baseline] [--copy-from <org-id> [--copy-scenarios a,b] [--overwrite]] [--auto-reset <scenario>|off] [--every <minutes>] [--release] | domains | new <domain> [--name <n>] [--brief <t>] [--full] [--host vercel|pages] [--no-wait] | teardown <org-id> [--keep-repo] | bootstrap <org-id>... --from <org-id> [--scenarios a,b] [--overwrite] | reset <org-id> <scenario> [--clean-slate] [--close-prs since_applied|all_open|none] [--hold <minutes>] [--restore-repo revert|force] [--rebuild] [--reread-company] [--forget-company] [--forget-site] | check <org-id> | ideas <org-id> [--library] [--preview] [--force] | ideas <org-id> --status [--id <run-id>] | sweep [--prepare] [--pull-requests] [--recipes <id>,…] [--variation <n|name>] [--pick <recipe>[:<variation>]]... [--new-org] [--orgs <id>,…] [--exclude <id>,…] [--buy <recipe-id>,…] [--make-orgs] [--no-refit] | sweep --status [--id <sweep-id>] | sweep --reset <recipe-id> [--id <sweep-id>]";
7011
7011
 
7012
7012
  const readinessLines = (r) =>
7013
7013
  r
@@ -7044,12 +7044,32 @@ const SWEEP_MARK = {
7044
7044
  pending: "…",
7045
7045
  };
7046
7046
 
7047
+ /** A variation flag: a number is its place (1 is the recipe itself); anything else its name. */
7048
+ const variationFlag = (v) => (v ? (/^\d+$/.test(v) ? Number(v) : v) : undefined);
7049
+
7050
+ /** `--pick <recipe>[:<variation>]`: a recipe in its variation (the first colon splits; a name may hold more). */
7051
+ const pickFlag = (v) => {
7052
+ const at = v.indexOf(":");
7053
+ return at < 0
7054
+ ? { recipe: v.trim() }
7055
+ : compact({ recipe: v.slice(0, at).trim(), variation: variationFlag(v.slice(at + 1).trim()) });
7056
+ };
7057
+
7058
+ /** A new test org's stages (get_recipe_sweep `new_org`). */
7059
+ const NEW_ORG_STAGE = { site: "site written", connected: "connected", setup: "setup done", fit: "fit decided" };
7060
+
7047
7061
  /** A recipe sweep's report (get_recipe_sweep), one block per recipe. */
7048
7062
  const recipeSweepLines = (sweep, appUrl) =>
7049
7063
  sweep
7050
7064
  ? [
7051
7065
  `${sweep.summary} (${sweep.state}${sweep.note && sweep.state !== "done" ? `: ${sweep.note}` : ""})`,
7052
7066
  `${sweep.start ? "started by the sweep" : "prepared for you to start"}, ${sweep.delivery === "build" ? "previews" : "pull requests"}, by ${sweep.started_by_name ?? "staff"} at ${sweep.created_at} sweep ${sweep.id}`,
7067
+ ...(sweep.new_org
7068
+ ? [
7069
+ `new test org: ${sweep.new_org.name ?? sweep.new_org.domain ?? "being made"} ${sweep.new_org.status === "done" ? "ready" : `${sweep.new_org.status}: ${NEW_ORG_STAGE[sweep.new_org.current] ?? sweep.new_org.current}`}${sweep.new_org.note ? ` (${sweep.new_org.note})` : ""}`,
7070
+ sweep.new_org.ideas ? ` open org: ${appUrl}${sweep.new_org.ideas}` : null,
7071
+ ].filter(Boolean)
7072
+ : []),
7053
7073
  ...sweep.items.map((i) =>
7054
7074
  [
7055
7075
  `\n${(SWEEP_MARK[i.result] ?? i.result).padEnd(10)} ${i.name}${i.variation ? ` (variation ${i.variation}: ${i.variation_name ?? ""})` : ""}`,
@@ -7320,18 +7340,6 @@ async function cmdAdminDemo(ctx) {
7320
7340
  const orgId = requirePositional(positionals, 1, "org-id");
7321
7341
  const path = `${DEMO}/${encodeURIComponent(orgId)}/ideas`;
7322
7342
  const id = flagStr(flags.id);
7323
- // The retest loop: one recipe brought back on its test org (reset_recipe_sweep_item).
7324
- if (flagStr(flags.reset)) {
7325
- const reset = await staffPost(ctx, `${path}/reset`, compact({ id, recipe_id: flagStr(flags.reset) }));
7326
- printResult(io, flags, reset, (r) =>
7327
- [
7328
- ...r.reset.undone.map((u) => `undone: ${u}`),
7329
- ...r.reset.kept.map((k) => `kept: ${k}`),
7330
- recipeSweepLines(r.sweep, ctx.client.url),
7331
- ].join("\n")
7332
- );
7333
- return 0;
7334
- }
7335
7343
  const result =
7336
7344
  flagBool(flags.status) || id
7337
7345
  ? await staffRequest(ctx, "GET", id ? `${path}?id=${encodeURIComponent(id)}` : path)
@@ -7351,6 +7359,18 @@ async function cmdAdminDemo(ctx) {
7351
7359
  // Every live recipe end to end on test orgs (run_recipe_sweep), or the latest sweep's report (get_recipe_sweep).
7352
7360
  const path = `${DEMO}/sweeps`;
7353
7361
  const id = flagStr(flags.id);
7362
+ // The retest loop: one recipe brought back on its test org (reset_recipe_sweep_item).
7363
+ if (flagStr(flags.reset)) {
7364
+ const reset = await staffPost(ctx, `${path}/reset`, compact({ id, recipe_id: flagStr(flags.reset) }));
7365
+ printResult(io, flags, reset, (r) =>
7366
+ [
7367
+ ...r.reset.undone.map((u) => `undone: ${u}`),
7368
+ ...r.reset.kept.map((k) => `kept: ${k}`),
7369
+ recipeSweepLines(r.sweep, ctx.client.url),
7370
+ ].join("\n")
7371
+ );
7372
+ return 0;
7373
+ }
7354
7374
  const result =
7355
7375
  flagBool(flags.status) || id
7356
7376
  ? await staffRequest(ctx, "GET", id ? `${path}?id=${encodeURIComponent(id)}` : path)
@@ -7360,16 +7380,14 @@ async function cmdAdminDemo(ctx) {
7360
7380
  compact({
7361
7381
  start: flagBool(flags.prepare) ? false : undefined,
7362
7382
  delivery: flagBool(flags["pull-requests"]) ? "build_pr" : undefined,
7363
- recipes: flags.recipes ? flagList(flags.recipes) : undefined,
7364
- // A number is the variation's place (1 is the recipe itself); anything else its name.
7365
- variation: flagStr(flags.variation)
7366
- ? /^\d+$/.test(flagStr(flags.variation))
7367
- ? Number(flagStr(flags.variation))
7368
- : flagStr(flags.variation)
7369
- : undefined,
7370
- orgs: flags.orgs ? flagList(flags.orgs) : undefined,
7371
- exclude: flags.exclude ? flagList(flags.exclude) : undefined,
7372
- purchases: flags.buy ? flagList(flags.buy) : undefined,
7383
+ recipes: flags.recipes ? flagCsv(flags.recipes) : undefined,
7384
+ variation: variationFlag(flagStr(flags.variation)),
7385
+ // --pick <recipe>[:<variation>], repeatable: each recipe in its variation.
7386
+ picks: flags.pick ? flagList(flags.pick).map(pickFlag) : undefined,
7387
+ new_org: flagBool(flags["new-org"]) || undefined,
7388
+ orgs: flags.orgs ? flagCsv(flags.orgs) : undefined,
7389
+ exclude: flags.exclude ? flagCsv(flags.exclude) : undefined,
7390
+ purchases: flags.buy ? flagCsv(flags.buy) : undefined,
7373
7391
  make_orgs: flagBool(flags["make-orgs"]) || undefined,
7374
7392
  refit: flagBool(flags["no-refit"]) ? false : undefined,
7375
7393
  })
@@ -58,6 +58,27 @@ export const ARGS = {
58
58
  project_id: { value: (flags) => (flags.project === "none" ? null : flagStr(flags.project)) },
59
59
  },
60
60
  disconnect_linear: {},
61
+ list_mcp_connections: {},
62
+ connect_mcp: {
63
+ name: { positional: 0, label: "name", required: true },
64
+ url: { positional: 1, label: "url", required: true },
65
+ token: { flag: "token" },
66
+ header_name: { flag: "header" },
67
+ allow_tools: { flag: "allow", type: "list" },
68
+ sign_in: { flag: "sign-in", type: "bool" },
69
+ },
70
+ test_mcp_connection: { name: { positional: 0, label: "name", required: true } },
71
+ update_mcp_connection: {
72
+ name: { positional: 0, label: "name", required: true },
73
+ enabled: { value: (flags) => (flags.off ? false : flags.on ? true : undefined) },
74
+ allow_tools: {
75
+ value: (flags) => (flags.allow === "none" ? [] : flags.allow ? flagStr(flags.allow).split(",") : undefined),
76
+ },
77
+ token: { value: (flags) => (flags.clear ? null : flagStr(flags.token)) },
78
+ header_name: { flag: "header" },
79
+ sign_in: { flag: "sign-in", type: "bool" },
80
+ },
81
+ remove_mcp_connection: { name: { positional: 0, label: "name", required: true } },
61
82
  connect_org_config_source: {
62
83
  repo: { positional: 0, label: "owner/name" },
63
84
  branch: { flag: "branch" },
@@ -91,11 +112,13 @@ export async function cmdOrg(ctx) {
91
112
  return orgFields(rest);
92
113
  case "linear":
93
114
  return orgLinear(rest);
115
+ case "mcp":
116
+ return orgMcp(rest);
94
117
  case "billing":
95
118
  return orgBilling(rest);
96
119
  default:
97
120
  throw new CliError(
98
- `Unknown org subcommand: ${sub ?? "(none)"}. Try: export, join, diff, apply, git, integrations, linear, fields, billing`
121
+ `Unknown org subcommand: ${sub ?? "(none)"}. Try: export, join, diff, apply, git, integrations, linear, mcp, fields, billing`
99
122
  );
100
123
  }
101
124
  }
@@ -454,6 +477,105 @@ async function orgLinear(ctx) {
454
477
  }
455
478
  }
456
479
 
480
+ /**
481
+ * `org mcp` — other products' MCP servers Letter's agent can call (docs/architecture/mcp-connections.md):
482
+ * org mcp the connections (list_mcp_connections)
483
+ * org mcp add <name> <url> [--token <value> [--header <name>] | --sign-in] [--allow a,b] (connect_mcp)
484
+ * org mcp signin <name> sign in again with OAuth (update_mcp_connection)
485
+ * org mcp tools <name> read the server's tools again (test_mcp_connection)
486
+ * org mcp allow <name> <a,b|none> the tools that change things the agent may call (update_mcp_connection)
487
+ * org mcp on|off <name> (update_mcp_connection)
488
+ * org mcp token <name> <value>|--clear a new header value (update_mcp_connection)
489
+ * org mcp remove <name> (remove_mcp_connection)
490
+ */
491
+ async function orgMcp(ctx) {
492
+ const { client, flags, io } = ctx;
493
+ const sub = ctx.positionals[0] ?? "list";
494
+ const rest = ctx.positionals.slice(1);
495
+ const call = async (tool, allowed, args) => {
496
+ refuseUnknownFlags(ctx, {}, allowed);
497
+ const result = await client.callTool(tool, args ?? argsFromFlags(ARGS[tool], flags, rest));
498
+ ok(ctx, result.message);
499
+ return result;
500
+ };
501
+ const tools = (r) =>
502
+ r.tools
503
+ .map((t) => `${t.allowed ? "●" : "○"} ${t.name}${t.read_only ? " (reads only)" : ""} ${t.description}`)
504
+ .join("\n");
505
+ switch (sub) {
506
+ case "list": {
507
+ const result = await call("list_mcp_connections", []);
508
+ printResult(
509
+ io,
510
+ flags,
511
+ result,
512
+ (r) =>
513
+ r.connections
514
+ .map(
515
+ (c) =>
516
+ `${c.enabled ? "●" : "○"} ${c.name} ${c.url} ${c.tool_count ?? "?"} tools${c.last_status === "failed" ? ` (failed: ${c.last_error})` : ""}`
517
+ )
518
+ .join("\n") || r.message
519
+ );
520
+ return 0;
521
+ }
522
+ case "add": {
523
+ const result = await call("connect_mcp", ["token", "header", "allow", "sign-in"]);
524
+ printResult(io, flags, result, (r) =>
525
+ r.sign_in_url ? `${r.message}\n${r.sign_in_url}` : `${r.message}\n${tools(r)}`
526
+ );
527
+ return 0;
528
+ }
529
+ case "signin": {
530
+ const result = await call("update_mcp_connection", [], {
531
+ name: requirePositional(rest, 0, "name"),
532
+ sign_in: true,
533
+ });
534
+ printResult(io, flags, result, (r) => `${r.message}\n${r.sign_in_url}`);
535
+ return 0;
536
+ }
537
+ case "tools": {
538
+ const result = await call("test_mcp_connection", []);
539
+ printResult(io, flags, result, tools);
540
+ return 0;
541
+ }
542
+ case "allow": {
543
+ const list = requirePositional(rest, 1, "a,b|none");
544
+ const result = await call("update_mcp_connection", [], {
545
+ name: requirePositional(rest, 0, "name"),
546
+ allow_tools: list === "none" ? [] : list.split(","),
547
+ });
548
+ printResult(io, flags, result, tools);
549
+ return 0;
550
+ }
551
+ case "on":
552
+ case "off": {
553
+ const result = await call("update_mcp_connection", [], {
554
+ name: requirePositional(rest, 0, "name"),
555
+ enabled: sub === "on",
556
+ });
557
+ printResult(io, flags, result, (r) => r.message);
558
+ return 0;
559
+ }
560
+ case "token": {
561
+ const name = requirePositional(rest, 0, "name");
562
+ const value = flags.clear ? null : requirePositional(rest, 1, "value");
563
+ const result = await call("update_mcp_connection", ["clear"], { name, token: value });
564
+ printResult(io, flags, result, (r) => r.message);
565
+ return 0;
566
+ }
567
+ case "remove": {
568
+ const result = await call("remove_mcp_connection", []);
569
+ printResult(io, flags, result, (r) => r.message);
570
+ return 0;
571
+ }
572
+ default:
573
+ throw new CliError(
574
+ `Unknown org mcp subcommand: ${sub}. Try: add, signin, tools, allow, on, off, token, remove`
575
+ );
576
+ }
577
+ }
578
+
457
579
  const ROOT_FILE = "organization.json";
458
580
 
459
581
  function slug(name) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.177.22",
3
+ "version": "0.177.24",
4
4
  "description": "Spin up and manage Letter phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",