@letterstory/cli 0.82.0 → 0.82.3
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 +3 -1
- package/lib/cli.mjs +14 -4
- package/lib/commands/admin.mjs +24 -4
- package/lib/commands/arena.mjs +3 -0
- package/lib/commands/authors.mjs +5 -1
- package/lib/commands/collections.mjs +32 -3
- package/lib/commands/onboard.mjs +17 -0
- package/lib/commands/shared.mjs +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -437,7 +437,9 @@ letterstory admin crm payment-link <account-id> # Stripe Payment Link at
|
|
|
437
437
|
|
|
438
438
|
`onboard` runs org → link → phantom sites → Slack channel → checklist → app invites →
|
|
439
439
|
stage in one call. Each site is a real (not test) fleet build; `--site ""` lets the engine
|
|
440
|
-
infer its beat from the client's homepage. `
|
|
440
|
+
infer its beat from the client's homepage. `--disclose 2` (or `--disclose all`) turns on sponsor
|
|
441
|
+
disclosure ("safe mode") for that site, and `--disclose-note "…"` records the client's words
|
|
442
|
+
as the reason; `onboard-plan` flags the sites whose calls asked for it. `onboard-plan` reads the account's recorded calls
|
|
441
443
|
(newest first, cached per set of calls; `--refresh` re-reads) and says how many sites, what each
|
|
442
444
|
is about, who to invite, and whether they've paid. `payment-link` stamps the account's org on
|
|
443
445
|
the Stripe subscription, so paying through it binds and marks the account paid by itself.
|
package/lib/cli.mjs
CHANGED
|
@@ -57,7 +57,7 @@ import {
|
|
|
57
57
|
} from "./commands.mjs";
|
|
58
58
|
|
|
59
59
|
// Keep in sync with cli/package.json.
|
|
60
|
-
export const VERSION = "0.82.
|
|
60
|
+
export const VERSION = "0.82.3";
|
|
61
61
|
|
|
62
62
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
63
63
|
// can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
|
|
@@ -81,6 +81,8 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
81
81
|
"rebuild",
|
|
82
82
|
"print-key",
|
|
83
83
|
"stdin",
|
|
84
|
+
// `authors generate <id> --with-bios`.
|
|
85
|
+
"with-bios",
|
|
84
86
|
// `covers generate <id> --regenerate` and `arena run --gate --article <id>`
|
|
85
87
|
// both put a value-less flag next to a value: without these entries the parser
|
|
86
88
|
// hands the following token to the flag and the real argument disappears.
|
|
@@ -331,14 +333,21 @@ Collections:
|
|
|
331
333
|
collections autopen <id> [--template <key>|none] [--kernel-family <name>|org]
|
|
332
334
|
[--mention off|subtle|woven] [--outline-depth barebones|fleshed_out]
|
|
333
335
|
[--direction-gate true|false] [--auto-start true|false]
|
|
336
|
+
[--prefer <site> …|none]
|
|
334
337
|
The Auto-Pen settings the app's panel writes. --template binds a content
|
|
335
338
|
template (from 'templates list') as this collection's default, which is how
|
|
336
339
|
a template you author here actually gets used. A patch: unset flags are
|
|
337
|
-
left alone.
|
|
340
|
+
left alone. --prefer REPLACES the preferred sources: SITES research
|
|
341
|
+
searches for every post's topic and reaches for first when a figure fits
|
|
342
|
+
(a URL is saved as its site) — your own site included; 'none' clears them.
|
|
343
|
+
collections cite-preferred <id> [--limit <n>] [--dry-run]
|
|
344
|
+
Bring published posts up to the preferred sources: each eligible post gets
|
|
345
|
+
at most one preferred figure, linked on the figure, then republishes.
|
|
338
346
|
|
|
339
347
|
Authors (the per-collection author bank — recurring bylines):
|
|
340
348
|
authors list <collection-id> List the bank + byline distribution
|
|
341
|
-
authors generate <collection-id> [--count <n>]
|
|
349
|
+
authors generate <collection-id> [--count <n>] [--with-bios]
|
|
350
|
+
Generate a masthead (names only; --with-bios adds a one-line bio + role)
|
|
342
351
|
authors add <collection-id> --name <n> [--bio <t>] [--role <t>] [--expertise a,b] [--started-at YYYY-MM-DD]
|
|
343
352
|
authors edit <collection-id> <author-id> [--name] [--bio] [--role] [--expertise a,b] [--started-at] [--active true|false]
|
|
344
353
|
authors retire <collection-id> <author-id> Stop assigning new posts to them
|
|
@@ -449,7 +458,7 @@ Ranking targets (what the org wants to be THE answer for — company-wide or on
|
|
|
449
458
|
Arenas (adversarial clusters of research agents -> a sourced, fact-checked outline):
|
|
450
459
|
arena run --topic <text> [--source <url> …] [--must-include <text> …] [--depth barebones|fleshed_out]
|
|
451
460
|
[--gate] [--no-verify] [--provider native|exa|youcom|context-dev] [--mention off|subtle|woven]
|
|
452
|
-
[--context-file ctx.json] [--block <name> …] [--block-domain <domain> …]
|
|
461
|
+
[--context-file ctx.json] [--block <name> …] [--block-domain <domain> …] [--prefer <site> …]
|
|
453
462
|
[--arena <arena-id>] [--article <uuid>] [--no-wait]
|
|
454
463
|
Start a run and follow it. --article makes
|
|
455
464
|
it HOSTED (outline written into that post,
|
|
@@ -521,6 +530,7 @@ Phantom launch recipe (a CLIENT domain -> a live phantom with its first posts; d
|
|
|
521
530
|
${bin === "phantom" ? "onboard" : "phantom-onboard"} <domain> [--about "the beat"] [--name X]
|
|
522
531
|
[--posts N] [--cadence N] [--cover editorial_image|full_ai|line_art] [--theme T]
|
|
523
532
|
[--real] [--domain host] [--domain-mode subdomain|buy_now|connect_later]
|
|
533
|
+
[--disclose [surfaces]] [--sponsor-label X] [--disclose-note "why"]
|
|
524
534
|
[--accept] [--dry-run]
|
|
525
535
|
Plan (read-only) then, with --accept
|
|
526
536
|
or an override flag, provision + configure
|
package/lib/commands/admin.mjs
CHANGED
|
@@ -2405,7 +2405,11 @@ function planLines(p) {
|
|
|
2405
2405
|
`domain: ${p.domain ?? "(unknown)"}`,
|
|
2406
2406
|
`sites: ${p.sites.length}`,
|
|
2407
2407
|
...p.sites.map(
|
|
2408
|
-
(s, i) =>
|
|
2408
|
+
(s, i) =>
|
|
2409
|
+
` ${i + 1}. ${s.direction || "(infer from their homepage)"}${s.why ? ` — ${s.why}` : ""}` +
|
|
2410
|
+
(s.disclose_sponsor?.recommended
|
|
2411
|
+
? `\n disclose the sponsor (--disclose ${i + 1})${s.disclose_sponsor.quote ? `: “${s.disclose_sponsor.quote}”` : ""}`
|
|
2412
|
+
: "")
|
|
2409
2413
|
),
|
|
2410
2414
|
`slack: ${p.slackEmails.join(", ") || "(nobody found)"}`,
|
|
2411
2415
|
`invites: ${p.invites.map((i) => `${i.email}:${i.role}`).join(", ") || "(none)"}`,
|
|
@@ -2418,19 +2422,35 @@ function planLines(p) {
|
|
|
2418
2422
|
/**
|
|
2419
2423
|
* `admin crm onboard` body from flags — the same CrmOnboardBodySchema the desk sends.
|
|
2420
2424
|
* --site "<direction>" per site (repeatable; "" = infer from the homepage) and/or
|
|
2421
|
-
* --sites N (pads with inferred sites); --
|
|
2422
|
-
*
|
|
2425
|
+
* --sites N (pads with inferred sites); --disclose N (repeatable, 1-based; "all") turns on
|
|
2426
|
+
* sponsor disclosure ("safe mode": the line after each article + schema.org sponsor) for
|
|
2427
|
+
* that site, with --disclose-note "<the client's words>" as the reason; --slack a@x,b@x
|
|
2428
|
+
* (bare --slack = the channel, no invites yet); --invite email[:admin|editor] (repeatable);
|
|
2429
|
+
* --no-checklist.
|
|
2423
2430
|
*/
|
|
2424
2431
|
function onboardBody(flags) {
|
|
2425
2432
|
const domain = flagStr(flags.domain);
|
|
2426
2433
|
if (!domain) {
|
|
2427
2434
|
throw new CliError(
|
|
2428
|
-
'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]'
|
|
2435
|
+
'Usage: admin crm onboard <account-id> --domain acme.com [--name "Acme"] [--site "<direction>"]… [--sites N] [--disclose N|all]… [--disclose-note "…"] [--slack a@x,b@x] [--invite a@x[:admin]]… [--no-checklist]'
|
|
2429
2436
|
);
|
|
2430
2437
|
}
|
|
2431
2438
|
const sites = flagList(flags.site).map((direction) => ({ direction: direction.trim() || null }));
|
|
2432
2439
|
const total = flagNum(flags.sites);
|
|
2433
2440
|
while (total !== undefined && sites.length < total) sites.push({ direction: null });
|
|
2441
|
+
const disclose = flagList(flags.disclose)
|
|
2442
|
+
.flatMap((v) => String(v).split(","))
|
|
2443
|
+
.map((v) => v.trim())
|
|
2444
|
+
.filter(Boolean);
|
|
2445
|
+
const note = flagStr(flags["disclose-note"]);
|
|
2446
|
+
for (const which of disclose) {
|
|
2447
|
+
const targets =
|
|
2448
|
+
which === "all"
|
|
2449
|
+
? sites.map((_, i) => i)
|
|
2450
|
+
: [Number(which) - 1].filter((i) => Number.isInteger(i) && i >= 0 && i < sites.length);
|
|
2451
|
+
if (targets.length === 0) throw new CliError(`--disclose ${which}: no such site (1–${sites.length}, or "all")`);
|
|
2452
|
+
for (const i of targets) sites[i] = { ...sites[i], disclosure: compact({ note }) };
|
|
2453
|
+
}
|
|
2434
2454
|
const slackEmails = flagList(flags.slack)
|
|
2435
2455
|
.flatMap((v) => v.split(","))
|
|
2436
2456
|
.map((e) => e.trim())
|
package/lib/commands/arena.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
toolField,
|
|
23
23
|
followJob,
|
|
24
24
|
argsFromFlags,
|
|
25
|
+
preferredSourcesFlag,
|
|
25
26
|
} from "./shared.mjs";
|
|
26
27
|
|
|
27
28
|
/** `--<flag> <path>` → the file's parsed JSON, or undefined when the flag is not set. */
|
|
@@ -75,6 +76,8 @@ const RUN_OPTIONS = {
|
|
|
75
76
|
context: { value: (flags) => readJsonObjectFlag(flags, "context") },
|
|
76
77
|
blocked_mentions: { flag: "block", type: "list" },
|
|
77
78
|
blocked_domains: { flag: "block-domain", type: "list" },
|
|
79
|
+
// Pages (read every run) and sites research reaches for first; `--prefer none` declares none.
|
|
80
|
+
preferred_sources: { value: (flags) => preferredSourcesFlag(flags.prefer) },
|
|
78
81
|
};
|
|
79
82
|
|
|
80
83
|
/**
|
package/lib/commands/authors.mjs
CHANGED
|
@@ -61,7 +61,11 @@ async function authorsList(ctx) {
|
|
|
61
61
|
|
|
62
62
|
async function authorsGenerate(ctx) {
|
|
63
63
|
const { client, flags, io } = ctx;
|
|
64
|
-
const args = compact({
|
|
64
|
+
const args = compact({
|
|
65
|
+
collection_id: collectionId(ctx),
|
|
66
|
+
count: flagNum(flags.count),
|
|
67
|
+
with_bios: flagBool(flags["with-bios"]) || undefined,
|
|
68
|
+
});
|
|
65
69
|
const result = await client.callTool("generate_collection_authors", args);
|
|
66
70
|
ok(ctx, "Generated authors.");
|
|
67
71
|
printResult(io, flags, result);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Ported from phantomstory-cli's collections.ts.
|
|
3
3
|
|
|
4
4
|
import { CliError } from "../client.mjs";
|
|
5
|
-
import { flagStr, printResult, ok, argsFromFlags } from "./shared.mjs";
|
|
5
|
+
import { flagStr, printResult, ok, argsFromFlags, preferredSourcesFlag } from "./shared.mjs";
|
|
6
6
|
import { cmdCollection } from "./collection.mjs";
|
|
7
7
|
|
|
8
8
|
// `--flag none|off|""` clears a nullable field; the server takes null for that.
|
|
@@ -47,6 +47,11 @@ export const ARGS = {
|
|
|
47
47
|
regenerate_topic_map: { value: (flags) => (flags["no-topic-map"] === undefined ? undefined : false) },
|
|
48
48
|
stock_headlines: { value: (flags) => (flags["no-headlines"] === undefined ? undefined : false) },
|
|
49
49
|
},
|
|
50
|
+
cite_preferred_sources: {
|
|
51
|
+
collection_id: { positional: 0, label: "collection-id", required: true },
|
|
52
|
+
limit: { flag: "limit", type: "num" },
|
|
53
|
+
dry_run: { value: (flags) => (flags["dry-run"] === true ? true : undefined) },
|
|
54
|
+
},
|
|
50
55
|
update_auto_pen_config: {
|
|
51
56
|
collection_id: { positional: 0, label: "collection-id", required: true },
|
|
52
57
|
// `--template none` clears the binding; the server takes null for that.
|
|
@@ -69,6 +74,8 @@ export const ARGS = {
|
|
|
69
74
|
},
|
|
70
75
|
mention: { object: { degree: "mention" } },
|
|
71
76
|
auto_start_on_curate: stringBool("auto-start"),
|
|
77
|
+
// Replaces the collection's preferred sources; `--prefer none` clears them.
|
|
78
|
+
preferred_sources: { value: (flags) => preferredSourcesFlag(flags.prefer) },
|
|
72
79
|
},
|
|
73
80
|
};
|
|
74
81
|
|
|
@@ -99,9 +106,11 @@ export async function cmdCollections(ctx) {
|
|
|
99
106
|
return collectionsAutoPen(rest);
|
|
100
107
|
case "activate":
|
|
101
108
|
return collectionsActivate(rest);
|
|
109
|
+
case "cite-preferred":
|
|
110
|
+
return collectionsCitePreferred(rest);
|
|
102
111
|
default:
|
|
103
112
|
throw new CliError(
|
|
104
|
-
`Unknown collections subcommand: ${sub ?? "(none)"}. Try: list, new, update, delete, assign, autopen, activate`
|
|
113
|
+
`Unknown collections subcommand: ${sub ?? "(none)"}. Try: list, new, update, delete, assign, autopen, activate, cite-preferred`
|
|
105
114
|
);
|
|
106
115
|
}
|
|
107
116
|
}
|
|
@@ -129,6 +138,26 @@ async function collectionsActivate(ctx) {
|
|
|
129
138
|
return 0;
|
|
130
139
|
}
|
|
131
140
|
|
|
141
|
+
/**
|
|
142
|
+
* `collections cite-preferred <id> [--limit <n>] [--dry-run]` — bring the collection's published
|
|
143
|
+
* posts up to its preferred sources: each eligible post places at most one preferred figure and
|
|
144
|
+
* republishes. The same function as the app's "Apply to published posts".
|
|
145
|
+
*/
|
|
146
|
+
async function collectionsCitePreferred(ctx) {
|
|
147
|
+
const { client, positionals, flags, io } = ctx;
|
|
148
|
+
const args = argsFromFlags(ARGS.cite_preferred_sources, flags, positionals);
|
|
149
|
+
const result = await client.callTool("cite_preferred_sources", args);
|
|
150
|
+
const n = result?.started?.length ?? 0;
|
|
151
|
+
ok(
|
|
152
|
+
ctx,
|
|
153
|
+
result?.dry_run
|
|
154
|
+
? `Would run ${n} post${n === 1 ? "" : "s"}; ${result?.skipped?.length ?? 0} skipped.`
|
|
155
|
+
: `Started ${n} post${n === 1 ? "" : "s"}${result?.republishes === false ? " (changes wait on the draft for approval)" : ""}; ${result?.skipped?.length ?? 0} skipped.`
|
|
156
|
+
);
|
|
157
|
+
printResult(io, flags, result);
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
132
161
|
async function collectionsUpdate(ctx) {
|
|
133
162
|
const { client, positionals, flags, io } = ctx;
|
|
134
163
|
const patch = argsFromFlags(ARGS.update_collection, flags, positionals);
|
|
@@ -166,7 +195,7 @@ async function collectionsAutoPen(ctx) {
|
|
|
166
195
|
const patch = argsFromFlags(ARGS.update_auto_pen_config, flags, positionals);
|
|
167
196
|
if (Object.keys(patch).length === 1) {
|
|
168
197
|
throw new CliError(
|
|
169
|
-
"Nothing to change. Pass at least one of --template, --kernel-family, --mention, --outline-depth, --direction-gate, --auto-start."
|
|
198
|
+
"Nothing to change. Pass at least one of --template, --kernel-family, --mention, --outline-depth, --direction-gate, --auto-start, --prefer."
|
|
170
199
|
);
|
|
171
200
|
}
|
|
172
201
|
|
package/lib/commands/onboard.mjs
CHANGED
|
@@ -151,6 +151,23 @@ export const ARGS = {
|
|
|
151
151
|
is_test: { flag: "real", type: "bool", invert: true },
|
|
152
152
|
// A self-serve integrator's customers launch their own sites: no staff pings.
|
|
153
153
|
notify_staff: { flag: "no-notify-staff", type: "bool", invert: true },
|
|
154
|
+
// Sponsor disclosure ("safe mode"): bare --disclose = the safe-mode surfaces (the
|
|
155
|
+
// server's default); --disclose article_footer,cover_badge picks them. Off unless asked.
|
|
156
|
+
disclosure: {
|
|
157
|
+
value: (flags) => {
|
|
158
|
+
if (flags.disclose === undefined || flags.disclose === false) return undefined;
|
|
159
|
+
const listed = typeof flags.disclose === "string" ? flags.disclose : "";
|
|
160
|
+
const surfaces = listed
|
|
161
|
+
.split(",")
|
|
162
|
+
.map((s) => s.trim())
|
|
163
|
+
.filter(Boolean);
|
|
164
|
+
return compact({
|
|
165
|
+
surfaces: surfaces.length ? surfaces : undefined,
|
|
166
|
+
sponsor_label: flagStr(flags["sponsor-label"]),
|
|
167
|
+
note: flagStr(flags["disclose-note"]),
|
|
168
|
+
});
|
|
169
|
+
},
|
|
170
|
+
},
|
|
154
171
|
},
|
|
155
172
|
},
|
|
156
173
|
},
|
package/lib/commands/shared.mjs
CHANGED
|
@@ -32,6 +32,20 @@ export function flagList(v) {
|
|
|
32
32
|
return typeof v === "string" ? [v] : [];
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* `--prefer <site>` (repeatable or comma-separated) → preferred_sources `[{ url }]` (a site; a URL is read as its site); `--prefer none`
|
|
37
|
+
* → `[]` (clears a standing list); absent → undefined.
|
|
38
|
+
*/
|
|
39
|
+
export function preferredSourcesFlag(v) {
|
|
40
|
+
if (v === undefined) return undefined;
|
|
41
|
+
const list = flagList(v)
|
|
42
|
+
.flatMap((x) => x.split(","))
|
|
43
|
+
.map((x) => x.trim())
|
|
44
|
+
.filter(Boolean);
|
|
45
|
+
if (list.length === 1 && list[0] === "none") return [];
|
|
46
|
+
return list.map((url) => ({ url }));
|
|
47
|
+
}
|
|
48
|
+
|
|
35
49
|
export function requireFlag(flags, name) {
|
|
36
50
|
const v = flagStr(flags[name]);
|
|
37
51
|
if (v === undefined) throw new CliError(`Missing required --${name}`);
|