@letterstory/cli 0.34.0 → 0.36.0

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 CHANGED
@@ -245,6 +245,34 @@ letterstory onboarding status
245
245
  letterstory onboarding step --complete connect_domain
246
246
  ```
247
247
 
248
+ ### Offerings — one org, many products
249
+
250
+ Context is layered **org → offering → collection**. An offering is anything the org would
251
+ name when recommending it ("use Lettershred"). Each layer stores only what it overrides: an
252
+ omitted field inherits, and `--reset <field>` puts one back to inheriting. Stances, avoid
253
+ topics and blocked mentions **add** across layers (a collection's stances lead in prompts);
254
+ priority topics (`--topic`) **replace** the inherited list, so a product never picks up its
255
+ siblings' focus.
256
+
257
+ ```bash
258
+ letterstory offerings list
259
+ letterstory offerings create lettershred --name Lettershred --link https://lettershred.com \
260
+ --one-liner "Removes AI watermarks from text" --stance "Detection is an arms race" --topic "AI watermarks"
261
+ letterstory offerings update <offering-id> --reset priority_topics # inherit the org's focus again
262
+ letterstory offerings competitor <offering-id> --competitor <competitor-domain-id> --stance avoid
263
+
264
+ # A collection publishes as its primary offering and may mention the others it lists.
265
+ letterstory offerings set-collection <collection-id> --primary <offering-id> --also <other-id>:0.5
266
+ letterstory offerings set-collection <collection-id> --none # the org itself again
267
+
268
+ # Why is this collection writing what it's writing? (every field + the layer that set it)
269
+ letterstory offerings collection <collection-id>
270
+ letterstory offerings collection-context <collection-id> --stance "…" --avoid "pricing" --framing "…"
271
+
272
+ # The planner picks an offering per topic; override one pick (or --offering none to clear).
273
+ letterstory offerings topic <topic-id> --offering <offering-id>
274
+ ```
275
+
248
276
  ### One-shot domain onboarding
249
277
 
250
278
  A bare company domain in, a fully-configured organization out — as ONE durable job that
package/lib/cli.mjs CHANGED
@@ -42,10 +42,11 @@ import {
42
42
  cmdLandings,
43
43
  cmdAdmin,
44
44
  cmdFeedback,
45
+ cmdOfferings,
45
46
  } from "./commands.mjs";
46
47
 
47
48
  // Keep in sync with cli/package.json.
48
- export const VERSION = "0.34.0";
49
+ export const VERSION = "0.36.0";
49
50
 
50
51
  // Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
51
52
  // can't accidentally swallow the id as --json's value.
@@ -84,6 +85,8 @@ const BOOLEAN_FLAGS = new Set([
84
85
  "no-visuals",
85
86
  "no-enhance",
86
87
  "no-key",
88
+ // `autopen start --article <id> --from-scratch`
89
+ "from-scratch",
87
90
  // `collections activate <id> --no-topic-map` / `--no-headlines`
88
91
  "no-topic-map",
89
92
  "no-headlines",
@@ -97,6 +100,8 @@ const BOOLEAN_FLAGS = new Set([
97
100
  "clear-topics",
98
101
  "clear-stances",
99
102
  "clear-avoid",
103
+ // `offerings set-collection <id> --none` (the org itself is the subject again).
104
+ "none",
100
105
  ]);
101
106
 
102
107
  // Tiny argv parser: `--flag value`, `--flag=value`, boolean `--flag`, and positionals.
@@ -212,7 +217,7 @@ Content:
212
217
  posts unpublish <article-id> Unpublish an article
213
218
  posts delete --id <uuid> [--id <uuid> ...] Permanently delete article(s)
214
219
  published list [--limit <n>] [--collection <uuid>] [--format html|md]
215
- published show (--id <uuid> | --slug <slug>) [--format html|md]
220
+ published show (--id <uuid> | --slug <slug>) [--collection <id>] [--format html|md]
216
221
 
217
222
  Reviewers:
218
223
  reviewers standing list List the org's standing reviewers
@@ -288,6 +293,33 @@ Strategy & onboarding:
288
293
  directives propose <collection_id> <what you want> Read one instruction back as concrete levers (applies nothing)
289
294
  directives apply <directive_id> --effect <id>... Commit the levers you kept (or --reason to decline them all)
290
295
  directives off <directive_id> --reason <why> Stop a live instruction; the reason is read back by the next proposal
296
+
297
+ Offerings (context layered org → offering → collection; a layer sets only what it overrides):
298
+ offerings list The org's offerings (things you'd name when recommending them)
299
+ offerings show <offering-id> One offering + its competitor overrides
300
+ offerings create <slug> [--name <n>] [--alias <a>…] [--link <url>] [--manifesto <text>|--manifesto-file <path>]
301
+ [--one-liner <s>] [--is <fact>…] [--is-not <fact>…] [--descriptor <d>…] [--framing <f>]
302
+ [--stance <s>…] [--topic <t>…] [--avoid <t>…] [--block <name>…] [--brand-profile <slug>]
303
+ [--position <n>] [--detach <field>…] Omitted = inherit the org. Stances/avoid/block ADD to the
304
+ org's; --topic (priority topics) REPLACES them. --detach
305
+ stops inheriting a field (e.g. --detach stances = only
306
+ this offering's own; --detach manifesto = none).
307
+ offerings update <offering-id> [same flags] [--slug <s>] [--reset <field>…]
308
+ --reset puts a field back to inheriting. The identity
309
+ flags (--one-liner/--is/--is-not/--descriptor) replace
310
+ the whole identity.
311
+ offerings delete <offering-id> --yes Collections that published as it fall back to the org
312
+ offerings competitor <offering-id> (--competitor <competitor-domain-id> | --add-domain <d> --add-name <n>)
313
+ --stance inherit|mention|avoid This offering's stance on one competitor
314
+ offerings collection <collection-id> Why it writes what it writes: resolved context, the layer
315
+ each field came from, and its mention set
316
+ offerings collection-context <collection-id> [--stance <s>…] [--topic <t>…] [--avoid <t>…] [--block <name>…]
317
+ [--framing <f>] [--detach <field>…] [--reset <field>…]
318
+ Edit the collection's own layer (its stances lead)
319
+ offerings set-collection <collection-id> (--primary <offering-id> [--also <offering-id>[:weight]…] | --none)
320
+ What it may promote; the primary is what it publishes as
321
+ offerings topic <topic-id> --offering <offering-id|none>
322
+ Override which offering one planner topic promotes
291
323
  probes list [--collection <uuid>] [--active-only] [--kind mention|citation|page]
292
324
  probes keep | pause | resume <probe_id> Keep = protect an AI probe from rotation (no pause)
293
325
  probes edit <probe_id> <new question text> Fix/sharpen a generated probe
@@ -321,6 +353,7 @@ Arenas (adversarial clusters of research agents -> a sourced, fact-checked outli
321
353
  Auto-Pen (research -> kernel -> Enhance, in one run — and the ONLY path that
322
354
  applies a collection's content template; kernel write --article does not):
323
355
  autopen start --article <uuid> Start the run; takes 10-40 min
356
+ autopen start --article <uuid> --from-scratch Clear the draft first, then run
324
357
  autopen status --article <uuid> Step states + the template in use
325
358
  autopen cancel --article <uuid> --yes Stop a run in flight
326
359
 
@@ -650,6 +683,7 @@ const CLIENT_COMMANDS = {
650
683
  connectors: cmdConnectors,
651
684
  strategy: cmdStrategy,
652
685
  directives: cmdDirectives,
686
+ offerings: cmdOfferings,
653
687
  probes: cmdProbes,
654
688
  suggestions: cmdSuggestions,
655
689
  onboarding: cmdOnboarding,
@@ -17,6 +17,7 @@ import { flagBool, requireFlag, printResult, ok, argsFromFlags } from "./shared.
17
17
  export const ARGS = {
18
18
  run_auto_pen: {
19
19
  article_id: { flag: "article", required: true },
20
+ from_scratch: { flag: "from-scratch", type: "bool" },
20
21
  },
21
22
  };
22
23
 
@@ -0,0 +1,423 @@
1
+ // `offerings` — the org context hierarchy: org → offering → collection.
2
+ //
3
+ // An offering is anything the org would NAME when recommending it ("use Lettershred").
4
+ // Each layer stores only what it overrides: an omitted field inherits, and
5
+ // `--reset <field>` sends null (or an empty list), which puts the field back to
6
+ // inheriting. Stances, avoid topics and blocked mentions ADD across layers; priority
7
+ // topics REPLACE. Thin wrappers over the MCP tools, which call the same store functions
8
+ // the app's /api/offerings routes do.
9
+
10
+ import { CliError } from "../client.mjs";
11
+ import {
12
+ flagStr,
13
+ flagNum,
14
+ flagList,
15
+ requirePositional,
16
+ printResult,
17
+ argsFromFlags,
18
+ refuseUnknownFlags,
19
+ readTextInput,
20
+ ok,
21
+ } from "./shared.mjs";
22
+
23
+ // What `--reset <field>` sends for each field: null where the schema reads null as
24
+ // "inherit", an empty list where the field is additive (no additions = inherit).
25
+ const OFFERING_RESETS = {
26
+ name: null,
27
+ aliases: null,
28
+ url: null,
29
+ manifesto: null,
30
+ identity: null,
31
+ default_framing: null,
32
+ priority_topics: null,
33
+ brand_profile_slug: null,
34
+ stances: [],
35
+ avoid_topics: [],
36
+ blocked_mentions: [],
37
+ detached_fields: [],
38
+ };
39
+ const COLLECTION_RESETS = {
40
+ priority_topics: null,
41
+ default_framing: null,
42
+ stances: [],
43
+ avoid_topics: [],
44
+ blocked_mentions: [],
45
+ detached_fields: [],
46
+ };
47
+
48
+ function resets(flags, allowed) {
49
+ const fields = flagList(flags.reset);
50
+ const unknown = fields.filter((f) => !(f in allowed));
51
+ if (unknown.length) {
52
+ throw new CliError(`--reset takes one of: ${Object.keys(allowed).join(", ")} (got ${unknown.join(", ")})`);
53
+ }
54
+ return new Set(fields);
55
+ }
56
+
57
+ const list = (flag) => (flags) => {
58
+ const v = flagList(flags[flag]);
59
+ return v.length ? v : undefined;
60
+ };
61
+ const str = (flag) => (flags) => flagStr(flags[flag]);
62
+
63
+ // A field that `--reset <key>` can put back to inheriting. `read` is how the value is
64
+ // taken from flags otherwise.
65
+ function resettable(key, read, table) {
66
+ return {
67
+ value: (flags, positionals) => (flagList(flags.reset).includes(key) ? table[key] : read(flags, positionals)),
68
+ };
69
+ }
70
+
71
+ // identity is one field (nearest layer wins as a whole), so its four flags build one
72
+ // object — and sending any of them replaces the whole identity.
73
+ function readIdentity(flags) {
74
+ const out = {};
75
+ const oneLiner = flagStr(flags["one-liner"]);
76
+ if (oneLiner !== undefined) out.one_liner = oneLiner;
77
+ for (const [key, flag] of [
78
+ ["is", "is"],
79
+ ["is_not", "is-not"],
80
+ ["descriptors", "descriptor"],
81
+ ]) {
82
+ const v = flagList(flags[flag]);
83
+ if (v.length) out[key] = v;
84
+ }
85
+ return Object.keys(out).length ? out : undefined;
86
+ }
87
+
88
+ const offeringFields = (table) => ({
89
+ name: resettable("name", str("name"), table),
90
+ aliases: resettable("aliases", list("alias"), table),
91
+ url: resettable("url", str("link"), table),
92
+ manifesto: resettable("manifesto", (flags) => readTextInput(flags, "manifesto", "manifesto-file"), table),
93
+ identity: resettable("identity", readIdentity, table),
94
+ default_framing: resettable("default_framing", str("framing"), table),
95
+ stances: resettable("stances", list("stance"), table),
96
+ priority_topics: resettable("priority_topics", list("topic"), table),
97
+ avoid_topics: resettable("avoid_topics", list("avoid"), table),
98
+ blocked_mentions: resettable("blocked_mentions", list("block"), table),
99
+ brand_profile_slug: resettable("brand_profile_slug", str("brand-profile"), table),
100
+ // `--detach stances` = stop inheriting that field from the org (only this offering's
101
+ // own value, or none). `--reset detached_fields` = inherit everything again.
102
+ detached_fields: resettable("detached_fields", list("detach"), table),
103
+ position: { flag: "position", type: "num" },
104
+ });
105
+
106
+ // Flags the `value` sources above read (argsFromFlags can't list them for
107
+ // refuseUnknownFlags, since a computed source names no single flag).
108
+ const OFFERING_FIELD_FLAGS = [
109
+ "name",
110
+ "alias",
111
+ "link",
112
+ "manifesto",
113
+ "manifesto-file",
114
+ "one-liner",
115
+ "is",
116
+ "is-not",
117
+ "descriptor",
118
+ "framing",
119
+ "stance",
120
+ "topic",
121
+ "avoid",
122
+ "block",
123
+ "brand-profile",
124
+ "detach",
125
+ "reset",
126
+ ];
127
+
128
+ // `--primary <id>` plus any `--also <id>[:weight]`; `--none` = the org itself.
129
+ function readMentionSet(flags) {
130
+ if (flags.none === true) return [];
131
+ const primary = flagStr(flags.primary);
132
+ if (!primary) return undefined;
133
+ const also = flagList(flags.also).map((entry) => {
134
+ const [id, weight] = entry.split(":");
135
+ const out = { offering_id: id, is_primary: false };
136
+ if (weight !== undefined) out.weight = flagNum(weight);
137
+ return out;
138
+ });
139
+ return [{ offering_id: primary, is_primary: true }, ...also];
140
+ }
141
+
142
+ // How every tool this module calls gets its arguments — see `argsFromFlags` in shared.mjs.
143
+ // `scripts/parity-check.ts` compares these keys against each tool's published input schema.
144
+ export const ARGS = {
145
+ list_offerings: {},
146
+ get_offering: {
147
+ offering_id: { positional: 0, label: "offering-id", required: true },
148
+ },
149
+ create_offering: {
150
+ slug: { positional: 0, label: "slug", required: true },
151
+ // Nothing to reset on create: an omitted field already inherits.
152
+ ...offeringFields({}),
153
+ },
154
+ update_offering: {
155
+ offering_id: { positional: 0, label: "offering-id", required: true },
156
+ slug: "slug",
157
+ ...offeringFields(OFFERING_RESETS),
158
+ },
159
+ delete_offering: {
160
+ offering_id: { positional: 0, label: "offering-id", required: true },
161
+ },
162
+ set_offering_competitor: {
163
+ offering_id: { positional: 0, label: "offering-id", required: true },
164
+ competitor_domain_id: "competitor",
165
+ add: { object: { domain: "add-domain", name: "add-name" } },
166
+ mention_stance: { flag: "stance", required: true, enum: ["inherit", "mention", "avoid"] },
167
+ },
168
+ get_collection_context: {
169
+ collection_id: { positional: 0, label: "collection-id", required: true },
170
+ },
171
+ set_collection_context: {
172
+ collection_id: { positional: 0, label: "collection-id", required: true },
173
+ stances: resettable("stances", list("stance"), COLLECTION_RESETS),
174
+ priority_topics: resettable("priority_topics", list("topic"), COLLECTION_RESETS),
175
+ avoid_topics: resettable("avoid_topics", list("avoid"), COLLECTION_RESETS),
176
+ blocked_mentions: resettable("blocked_mentions", list("block"), COLLECTION_RESETS),
177
+ default_framing: resettable("default_framing", str("framing"), COLLECTION_RESETS),
178
+ detached_fields: resettable("detached_fields", list("detach"), COLLECTION_RESETS),
179
+ },
180
+ set_collection_offerings: {
181
+ collection_id: { positional: 0, label: "collection-id", required: true },
182
+ offerings: { value: readMentionSet },
183
+ },
184
+ set_topic_offering: {
185
+ topic_id: { positional: 0, label: "topic-id", required: true },
186
+ offering_id: {
187
+ value: (flags) => {
188
+ const v = flagStr(flags.offering);
189
+ if (v === undefined) throw new CliError("Missing required --offering <offering-id|none>");
190
+ return v === "none" ? null : v;
191
+ },
192
+ },
193
+ },
194
+ };
195
+
196
+ const USAGE = `Usage: offerings <list|show|create|update|delete|competitor|collection|collection-context|set-collection|topic>
197
+ offerings list
198
+ offerings show <offering-id>
199
+ offerings create <slug> [--name <n>] [--alias <a>…] [--link <url>] [--manifesto <text>|--manifesto-file <path>]
200
+ [--one-liner <s>] [--is <fact>…] [--is-not <fact>…] [--descriptor <d>…] [--framing <f>]
201
+ [--stance <s>…] [--topic <t>…] [--avoid <t>…] [--block <name>…] [--brand-profile <slug>] [--position <n>]
202
+ offerings update <offering-id> [same flags] [--slug <s>] [--reset <field>…]
203
+ offerings delete <offering-id> --yes
204
+ offerings competitor <offering-id> (--competitor <competitor-domain-id> | --add-domain <d> --add-name <n>)
205
+ --stance inherit|mention|avoid
206
+ offerings collection <collection-id>
207
+ offerings collection-context <collection-id> [--stance <s>…] [--topic <t>…] [--avoid <t>…] [--block <name>…]
208
+ [--framing <f>] [--reset <field>…]
209
+ offerings set-collection <collection-id> (--primary <offering-id> [--also <offering-id>[:weight]…] | --none)
210
+ offerings topic <topic-id> --offering <offering-id|none>`;
211
+
212
+ export async function cmdOfferings(ctx) {
213
+ const sub = ctx.positionals[0];
214
+ const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
215
+ switch (sub) {
216
+ case "list":
217
+ case "ls":
218
+ return offeringsList(rest);
219
+ case "show":
220
+ case "get":
221
+ return offeringsShow(rest);
222
+ case "create":
223
+ return offeringsCreate(rest);
224
+ case "update":
225
+ return offeringsUpdate(rest);
226
+ case "delete":
227
+ return offeringsDelete(rest);
228
+ case "competitor":
229
+ return offeringsCompetitor(rest);
230
+ case "collection":
231
+ return offeringsCollection(rest);
232
+ case "collection-context":
233
+ return offeringsCollectionContext(rest);
234
+ case "set-collection":
235
+ return offeringsSetCollection(rest);
236
+ case "topic":
237
+ return offeringsTopic(rest);
238
+ default:
239
+ throw new CliError(USAGE);
240
+ }
241
+ }
242
+
243
+ const label = (o) => o.name ?? `${o.slug} (org name)`;
244
+
245
+ function formatOffering(o) {
246
+ const lines = [`${o.id} ${o.slug} ${label(o)}`];
247
+ const set = (k, v) =>
248
+ v !== null && v !== undefined && (!Array.isArray(v) || v.length) && lines.push(` ${k}: ${v}`);
249
+ set("url", o.url);
250
+ set("framing", o.default_framing);
251
+ set("one-liner", o.identity?.one_liner);
252
+ set("stances (+org)", o.stances?.join("; "));
253
+ set("priority topics (replaces org)", o.priority_topics?.join("; "));
254
+ set("avoid (+org)", o.avoid_topics?.join("; "));
255
+ set("never mention (+org)", o.blocked_mentions?.join("; "));
256
+ return lines.join("\n");
257
+ }
258
+
259
+ async function offeringsList(ctx) {
260
+ const { client, flags, io } = ctx;
261
+ const result = await client.callTool("list_offerings", argsFromFlags(ARGS.list_offerings, flags));
262
+ printResult(io, flags, result, (r) => {
263
+ const all = r.offerings ?? [];
264
+ if (!all.length) return "No offerings — the org itself is the subject everywhere.";
265
+ return all.map(formatOffering).join("\n\n");
266
+ });
267
+ return 0;
268
+ }
269
+
270
+ async function offeringsShow(ctx) {
271
+ const { client, flags, positionals, io } = ctx;
272
+ const result = await client.callTool("get_offering", argsFromFlags(ARGS.get_offering, flags, positionals));
273
+ printResult(io, flags, result, (r) => {
274
+ const lines = [formatOffering(r.offering)];
275
+ for (const c of r.competitors ?? []) {
276
+ lines.push(
277
+ ` competitor ${c.name ?? c.competitor_domain_id}: ${c.mention_stance}${c.added_here ? " (only here)" : ""}`
278
+ );
279
+ }
280
+ return lines.join("\n");
281
+ });
282
+ return 0;
283
+ }
284
+
285
+ async function offeringsCreate(ctx) {
286
+ const { client, flags, positionals, io } = ctx;
287
+ refuseUnknownFlags(
288
+ ctx,
289
+ ARGS.create_offering,
290
+ OFFERING_FIELD_FLAGS.filter((f) => f !== "reset")
291
+ );
292
+ const result = await client.callTool("create_offering", argsFromFlags(ARGS.create_offering, flags, positionals));
293
+ ok(ctx, `Created ${result.offering?.slug} (${result.offering?.id}).`);
294
+ printResult(io, flags, result, (r) => formatOffering(r.offering));
295
+ return 0;
296
+ }
297
+
298
+ async function offeringsUpdate(ctx) {
299
+ const { client, flags, positionals, io } = ctx;
300
+ refuseUnknownFlags(ctx, ARGS.update_offering, OFFERING_FIELD_FLAGS);
301
+ resets(flags, OFFERING_RESETS);
302
+ const args = argsFromFlags(ARGS.update_offering, flags, positionals);
303
+ if (Object.keys(args).length === 1) throw new CliError("Nothing to update — pass a field flag or --reset <field>.");
304
+ const result = await client.callTool("update_offering", args);
305
+ ok(ctx, `Updated ${result.offering?.slug}.`);
306
+ printResult(io, flags, result, (r) => formatOffering(r.offering));
307
+ return 0;
308
+ }
309
+
310
+ async function offeringsDelete(ctx) {
311
+ const { client, flags, positionals, io } = ctx;
312
+ const id = requirePositional(positionals, 0, "offering-id");
313
+ if (!flags.yes) {
314
+ io.error(`Re-run with --yes to confirm: ${ctx.bin} offerings delete ${id} --yes`);
315
+ return 1;
316
+ }
317
+ await client.callTool("delete_offering", argsFromFlags(ARGS.delete_offering, flags, positionals));
318
+ ok(ctx, "Offering deleted. Collections that published as it now fall back to the org.");
319
+ return 0;
320
+ }
321
+
322
+ async function offeringsCompetitor(ctx) {
323
+ const { client, flags, positionals, io } = ctx;
324
+ // `add`'s nested flags aren't visible to refuseUnknownFlags (it reads top-level flags only).
325
+ refuseUnknownFlags(ctx, ARGS.set_offering_competitor, ["add-domain", "add-name"]);
326
+ const result = await client.callTool(
327
+ "set_offering_competitor",
328
+ argsFromFlags(ARGS.set_offering_competitor, flags, positionals)
329
+ );
330
+ ok(ctx, `Competitor ${result.competitor_domain_id}: ${result.mention_stance} for this offering.`);
331
+ if (flags.json) printResult(io, flags, result);
332
+ return 0;
333
+ }
334
+
335
+ function formatContext(c) {
336
+ const r = c.resolved;
337
+ const from = (field) => ` [${r.origin?.[field] ?? "org"}]`;
338
+ const lines = [
339
+ r.offering
340
+ ? `Subject: ${r.offering.name ?? r.offering.slug} (offering ${r.offering.id})`
341
+ : "Subject: the org itself",
342
+ `name: ${r.name ?? "—"}${from("name")}`,
343
+ ];
344
+ if (r.default_framing) lines.push(`framing: ${r.default_framing}${from("default_framing")}`);
345
+ if (r.stances.length) {
346
+ lines.push("stances (most targeted first):");
347
+ for (const s of r.stances) lines.push(` - ${s.text} [${s.origin}]`);
348
+ }
349
+ if (r.priority_topics.length)
350
+ lines.push(`priority topics${from("priority_topics")}: ${r.priority_topics.join("; ")}`);
351
+ if (r.avoid_topics.length) lines.push(`avoid: ${r.avoid_topics.join("; ")}`);
352
+ if (r.blocked_mentions.length) lines.push(`never mention: ${r.blocked_mentions.join("; ")}`);
353
+ lines.push("", "Mention set:");
354
+ if (!c.offerings.length) lines.push(" (none — the org is the subject)");
355
+ for (const m of c.offerings) {
356
+ lines.push(
357
+ ` ${m.is_primary ? "*" : " "} ${m.offering_id} ${m.name ?? m.slug}${m.is_primary ? "" : ` weight ${m.weight}`}`
358
+ );
359
+ }
360
+ return lines.join("\n");
361
+ }
362
+
363
+ async function offeringsCollection(ctx) {
364
+ const { client, flags, positionals, io } = ctx;
365
+ const result = await client.callTool(
366
+ "get_collection_context",
367
+ argsFromFlags(ARGS.get_collection_context, flags, positionals)
368
+ );
369
+ printResult(io, flags, result, (r) => formatContext(r.context));
370
+ return 0;
371
+ }
372
+
373
+ async function offeringsCollectionContext(ctx) {
374
+ const { client, flags, positionals, io } = ctx;
375
+ refuseUnknownFlags(ctx, ARGS.set_collection_context, [
376
+ "stance",
377
+ "topic",
378
+ "avoid",
379
+ "block",
380
+ "framing",
381
+ "detach",
382
+ "reset",
383
+ ]);
384
+ resets(flags, COLLECTION_RESETS);
385
+ const args = argsFromFlags(ARGS.set_collection_context, flags, positionals);
386
+ if (Object.keys(args).length === 1) throw new CliError("Nothing to update — pass a field flag or --reset <field>.");
387
+ const result = await client.callTool("set_collection_context", args);
388
+ ok(ctx, "Collection context saved.");
389
+ printResult(io, flags, result, (r) => formatContext(r.context));
390
+ return 0;
391
+ }
392
+
393
+ async function offeringsSetCollection(ctx) {
394
+ const { client, flags, positionals, io } = ctx;
395
+ refuseUnknownFlags(ctx, ARGS.set_collection_offerings, ["primary", "also", "none"]);
396
+ const args = argsFromFlags(ARGS.set_collection_offerings, flags, positionals);
397
+ if (!args.offerings) {
398
+ throw new CliError(
399
+ "Pass --primary <offering-id> (plus any --also <offering-id>), or --none for the org itself."
400
+ );
401
+ }
402
+ const result = await client.callTool("set_collection_offerings", args);
403
+ ok(ctx, args.offerings.length ? "Mention set saved." : "Cleared — the org is this collection's subject again.");
404
+ if (flags.json) printResult(io, flags, result);
405
+ return 0;
406
+ }
407
+
408
+ async function offeringsTopic(ctx) {
409
+ const { client, flags, positionals } = ctx;
410
+ refuseUnknownFlags(ctx, ARGS.set_topic_offering, ["offering"]);
411
+ const result = await client.callTool(
412
+ "set_topic_offering",
413
+ argsFromFlags(ARGS.set_topic_offering, flags, positionals)
414
+ );
415
+ ok(
416
+ ctx,
417
+ result.mention_offering_id
418
+ ? `Topic now promotes ${result.mention_offering_id}.`
419
+ : "Override cleared — the topic falls back to the collection's primary offering."
420
+ );
421
+ if (flags.json) printResult(ctx.io, flags, result);
422
+ return 0;
423
+ }
@@ -141,7 +141,14 @@ async function publishedShow(ctx) {
141
141
  const id = flagStr(flags.id);
142
142
  const slug = flagStr(flags.slug);
143
143
  if (id === undefined && slug === undefined) throw new CliError("Pass --id or --slug.");
144
- const args = compact({ article_id: id, slug, format: flagStr(flags.format) });
144
+ // --collection scopes a slug lookup to one collection (slugs are only unique per
145
+ // collection once an org runs several blogs).
146
+ const args = compact({
147
+ article_id: id,
148
+ slug,
149
+ collection_id: flagStr(flags.collection),
150
+ format: flagStr(flags.format),
151
+ });
145
152
  const result = await client.callTool("get_published", args);
146
153
  printResult(io, flags, result);
147
154
  return 0;
package/lib/commands.mjs CHANGED
@@ -31,5 +31,6 @@ export * from "./commands/onboard.mjs";
31
31
  export * from "./commands/seers.mjs";
32
32
  export * from "./commands/shredder.mjs";
33
33
  export * from "./commands/landings.mjs";
34
+ export * from "./commands/offerings.mjs";
34
35
  export * from "./commands/admin.mjs";
35
36
  export * from "./commands/feedback.mjs";
package/lib/documents.mjs CHANGED
@@ -59,6 +59,8 @@ export const DOCUMENT_FLAG_PAIRS = [
59
59
  ["body", "file"],
60
60
  ["brief", "brief-file"],
61
61
  ["text", "text-file"],
62
+ // An offering's (or the company's) manifesto is prose — a PDF or a .docx is fair game.
63
+ ["manifesto", "manifesto-file"],
62
64
  ];
63
65
 
64
66
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.34.0",
3
+ "version": "0.36.0",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",