@letterstory/cli 0.35.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 +28 -0
- package/lib/cli.mjs +33 -2
- package/lib/commands/offerings.mjs +423 -0
- package/lib/commands/posts.mjs +8 -1
- package/lib/commands.mjs +1 -0
- package/lib/documents.mjs +2 -0
- package/package.json +1 -1
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.
|
|
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.
|
|
@@ -99,6 +100,8 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
99
100
|
"clear-topics",
|
|
100
101
|
"clear-stances",
|
|
101
102
|
"clear-avoid",
|
|
103
|
+
// `offerings set-collection <id> --none` (the org itself is the subject again).
|
|
104
|
+
"none",
|
|
102
105
|
]);
|
|
103
106
|
|
|
104
107
|
// Tiny argv parser: `--flag value`, `--flag=value`, boolean `--flag`, and positionals.
|
|
@@ -214,7 +217,7 @@ Content:
|
|
|
214
217
|
posts unpublish <article-id> Unpublish an article
|
|
215
218
|
posts delete --id <uuid> [--id <uuid> ...] Permanently delete article(s)
|
|
216
219
|
published list [--limit <n>] [--collection <uuid>] [--format html|md]
|
|
217
|
-
published show (--id <uuid> | --slug <slug>) [--format html|md]
|
|
220
|
+
published show (--id <uuid> | --slug <slug>) [--collection <id>] [--format html|md]
|
|
218
221
|
|
|
219
222
|
Reviewers:
|
|
220
223
|
reviewers standing list List the org's standing reviewers
|
|
@@ -290,6 +293,33 @@ Strategy & onboarding:
|
|
|
290
293
|
directives propose <collection_id> <what you want> Read one instruction back as concrete levers (applies nothing)
|
|
291
294
|
directives apply <directive_id> --effect <id>... Commit the levers you kept (or --reason to decline them all)
|
|
292
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
|
|
293
323
|
probes list [--collection <uuid>] [--active-only] [--kind mention|citation|page]
|
|
294
324
|
probes keep | pause | resume <probe_id> Keep = protect an AI probe from rotation (no pause)
|
|
295
325
|
probes edit <probe_id> <new question text> Fix/sharpen a generated probe
|
|
@@ -653,6 +683,7 @@ const CLIENT_COMMANDS = {
|
|
|
653
683
|
connectors: cmdConnectors,
|
|
654
684
|
strategy: cmdStrategy,
|
|
655
685
|
directives: cmdDirectives,
|
|
686
|
+
offerings: cmdOfferings,
|
|
656
687
|
probes: cmdProbes,
|
|
657
688
|
suggestions: cmdSuggestions,
|
|
658
689
|
onboarding: cmdOnboarding,
|
|
@@ -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
|
+
}
|
package/lib/commands/posts.mjs
CHANGED
|
@@ -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
|
-
|
|
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