@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 +8 -3
- package/lib/commands/admin.mjs +41 -23
- package/lib/commands/org.mjs +123 -1
- package/package.json +1 -1
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.
|
|
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.
|
|
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
|
package/lib/commands/admin.mjs
CHANGED
|
@@ -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 ?
|
|
7364
|
-
|
|
7365
|
-
variation:
|
|
7366
|
-
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
|
|
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
|
})
|
package/lib/commands/org.mjs
CHANGED
|
@@ -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) {
|