@letterstory/cli 0.35.0 → 0.36.1
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 +44 -0
- package/lib/cli.mjs +55 -2
- package/lib/commands/compositions.mjs +193 -0
- package/lib/commands/offerings.mjs +423 -0
- package/lib/commands/posts.mjs +8 -1
- package/lib/commands.mjs +2 -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
|
|
@@ -494,6 +522,22 @@ letterstory templates update <template-id> --angle "…" # unset fields keep t
|
|
|
494
522
|
letterstory templates delete <template-id> --yes # bound seers fall back to defaults
|
|
495
523
|
```
|
|
496
524
|
|
|
525
|
+
Primitive compositions (a named bundle of kernel/shredder/template/seer references —
|
|
526
|
+
`docs/formats/compositions.md`; not a primitive, no endpoint or run. Needs
|
|
527
|
+
`composition:read` / `composition:write`):
|
|
528
|
+
|
|
529
|
+
```bash
|
|
530
|
+
letterstory compositions list
|
|
531
|
+
letterstory compositions create --name "Regulation-watch site kit" \
|
|
532
|
+
--content '{"kernel_family":"Cedar","shred":true,"template_key":"tmpl_howto"}' \
|
|
533
|
+
--phantom '{"theme":"gazette"}' \
|
|
534
|
+
--seer '{"name":"AI regulation watch","provider":"regulation_change","mode":"auto","config":{"query":"AI regulation"}}'
|
|
535
|
+
letterstory compositions resolve comp_9f1a2b3c4d5e # preview, read-only — costs nothing
|
|
536
|
+
letterstory compositions update <composition-id> --clear-seer
|
|
537
|
+
letterstory compositions delete <composition-id> --yes # non-destructive to phantoms already built from it
|
|
538
|
+
letterstory phantom create --name "EU AI Act Weekly" --composition '{"key":"comp_9f1a2b3c4d5e"}'
|
|
539
|
+
```
|
|
540
|
+
|
|
497
541
|
## Cover images
|
|
498
542
|
|
|
499
543
|
Which image model draws a post's cover is a CLI decision, not a UI-only one. `variants`
|
package/lib/cli.mjs
CHANGED
|
@@ -37,15 +37,17 @@ import {
|
|
|
37
37
|
cmdOnboard,
|
|
38
38
|
cmdSeers,
|
|
39
39
|
cmdTemplates,
|
|
40
|
+
cmdCompositions,
|
|
40
41
|
cmdShredder,
|
|
41
42
|
cmdLede,
|
|
42
43
|
cmdLandings,
|
|
43
44
|
cmdAdmin,
|
|
44
45
|
cmdFeedback,
|
|
46
|
+
cmdOfferings,
|
|
45
47
|
} from "./commands.mjs";
|
|
46
48
|
|
|
47
49
|
// Keep in sync with cli/package.json.
|
|
48
|
-
export const VERSION = "0.
|
|
50
|
+
export const VERSION = "0.36.1";
|
|
49
51
|
|
|
50
52
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
51
53
|
// can't accidentally swallow the id as --json's value.
|
|
@@ -99,6 +101,8 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
99
101
|
"clear-topics",
|
|
100
102
|
"clear-stances",
|
|
101
103
|
"clear-avoid",
|
|
104
|
+
// `offerings set-collection <id> --none` (the org itself is the subject again).
|
|
105
|
+
"none",
|
|
102
106
|
]);
|
|
103
107
|
|
|
104
108
|
// Tiny argv parser: `--flag value`, `--flag=value`, boolean `--flag`, and positionals.
|
|
@@ -214,7 +218,7 @@ Content:
|
|
|
214
218
|
posts unpublish <article-id> Unpublish an article
|
|
215
219
|
posts delete --id <uuid> [--id <uuid> ...] Permanently delete article(s)
|
|
216
220
|
published list [--limit <n>] [--collection <uuid>] [--format html|md]
|
|
217
|
-
published show (--id <uuid> | --slug <slug>) [--format html|md]
|
|
221
|
+
published show (--id <uuid> | --slug <slug>) [--collection <id>] [--format html|md]
|
|
218
222
|
|
|
219
223
|
Reviewers:
|
|
220
224
|
reviewers standing list List the org's standing reviewers
|
|
@@ -290,6 +294,33 @@ Strategy & onboarding:
|
|
|
290
294
|
directives propose <collection_id> <what you want> Read one instruction back as concrete levers (applies nothing)
|
|
291
295
|
directives apply <directive_id> --effect <id>... Commit the levers you kept (or --reason to decline them all)
|
|
292
296
|
directives off <directive_id> --reason <why> Stop a live instruction; the reason is read back by the next proposal
|
|
297
|
+
|
|
298
|
+
Offerings (context layered org → offering → collection; a layer sets only what it overrides):
|
|
299
|
+
offerings list The org's offerings (things you'd name when recommending them)
|
|
300
|
+
offerings show <offering-id> One offering + its competitor overrides
|
|
301
|
+
offerings create <slug> [--name <n>] [--alias <a>…] [--link <url>] [--manifesto <text>|--manifesto-file <path>]
|
|
302
|
+
[--one-liner <s>] [--is <fact>…] [--is-not <fact>…] [--descriptor <d>…] [--framing <f>]
|
|
303
|
+
[--stance <s>…] [--topic <t>…] [--avoid <t>…] [--block <name>…] [--brand-profile <slug>]
|
|
304
|
+
[--position <n>] [--detach <field>…] Omitted = inherit the org. Stances/avoid/block ADD to the
|
|
305
|
+
org's; --topic (priority topics) REPLACES them. --detach
|
|
306
|
+
stops inheriting a field (e.g. --detach stances = only
|
|
307
|
+
this offering's own; --detach manifesto = none).
|
|
308
|
+
offerings update <offering-id> [same flags] [--slug <s>] [--reset <field>…]
|
|
309
|
+
--reset puts a field back to inheriting. The identity
|
|
310
|
+
flags (--one-liner/--is/--is-not/--descriptor) replace
|
|
311
|
+
the whole identity.
|
|
312
|
+
offerings delete <offering-id> --yes Collections that published as it fall back to the org
|
|
313
|
+
offerings competitor <offering-id> (--competitor <competitor-domain-id> | --add-domain <d> --add-name <n>)
|
|
314
|
+
--stance inherit|mention|avoid This offering's stance on one competitor
|
|
315
|
+
offerings collection <collection-id> Why it writes what it writes: resolved context, the layer
|
|
316
|
+
each field came from, and its mention set
|
|
317
|
+
offerings collection-context <collection-id> [--stance <s>…] [--topic <t>…] [--avoid <t>…] [--block <name>…]
|
|
318
|
+
[--framing <f>] [--detach <field>…] [--reset <field>…]
|
|
319
|
+
Edit the collection's own layer (its stances lead)
|
|
320
|
+
offerings set-collection <collection-id> (--primary <offering-id> [--also <offering-id>[:weight]…] | --none)
|
|
321
|
+
What it may promote; the primary is what it publishes as
|
|
322
|
+
offerings topic <topic-id> --offering <offering-id|none>
|
|
323
|
+
Override which offering one planner topic promotes
|
|
293
324
|
probes list [--collection <uuid>] [--active-only] [--kind mention|citation|page]
|
|
294
325
|
probes keep | pause | resume <probe_id> Keep = protect an AI probe from rotation (no pause)
|
|
295
326
|
probes edit <probe_id> <new question text> Fix/sharpen a generated probe
|
|
@@ -534,6 +565,26 @@ Content templates (compose structures seers bind via config.template_key):
|
|
|
534
565
|
for coverage in your own words and lets the engine pick the shape.
|
|
535
566
|
templates delete <template-id> --yes
|
|
536
567
|
|
|
568
|
+
Primitive compositions (named bundles of kernel/shredder/template/seer; not a primitive —
|
|
569
|
+
no endpoint, no run. Applied on phantom create --composition '{"key":"..."}'):
|
|
570
|
+
compositions list Org's compositions, by key
|
|
571
|
+
compositions show <composition-id>
|
|
572
|
+
compositions create --name <n> [--description <text>]
|
|
573
|
+
[--content '<json>'|--content-file <path>] The collection's own content
|
|
574
|
+
section (kernel_family, shred,
|
|
575
|
+
shredder_endpoint_id, template_key,
|
|
576
|
+
mention_degree, cover, visuals, verify).
|
|
577
|
+
[--phantom '<json>'|--phantom-file <path>] Phantom-hosting defaults ({theme}).
|
|
578
|
+
[--seer '<json>'|--seer-file <path>] Optional — the same shape
|
|
579
|
+
'seers create' takes, minus
|
|
580
|
+
collection/org (filled in once
|
|
581
|
+
the phantom's collection exists).
|
|
582
|
+
compositions update <composition-id> [--name] [--description]
|
|
583
|
+
[--content ...] [--phantom ...] [--seer '<json>'|--seer-file <path>|--clear-seer]
|
|
584
|
+
compositions delete <composition-id> --yes
|
|
585
|
+
compositions resolve <composition-key> Preview what create_phantom would
|
|
586
|
+
merge in — read-only, costs nothing.
|
|
587
|
+
|
|
537
588
|
Shredder (diversify content produced outside Letterstory; structure preserved):
|
|
538
589
|
shredder list Your shredder endpoints + run counts
|
|
539
590
|
shredder create --name <n> New endpoint (returns a callable public id)
|
|
@@ -653,6 +704,7 @@ const CLIENT_COMMANDS = {
|
|
|
653
704
|
connectors: cmdConnectors,
|
|
654
705
|
strategy: cmdStrategy,
|
|
655
706
|
directives: cmdDirectives,
|
|
707
|
+
offerings: cmdOfferings,
|
|
656
708
|
probes: cmdProbes,
|
|
657
709
|
suggestions: cmdSuggestions,
|
|
658
710
|
onboarding: cmdOnboarding,
|
|
@@ -665,6 +717,7 @@ const CLIENT_COMMANDS = {
|
|
|
665
717
|
"phantom-onboard": cmdOnboard,
|
|
666
718
|
seers: cmdSeers,
|
|
667
719
|
templates: cmdTemplates,
|
|
720
|
+
compositions: cmdCompositions,
|
|
668
721
|
shredder: cmdShredder,
|
|
669
722
|
lede: cmdLede,
|
|
670
723
|
ledes: cmdLede,
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// `compositions` — Primitive Compositions, the FORMAT that bundles a kernel, a shredder, a
|
|
2
|
+
// content template and a seer into one named, reusable reference (docs/formats/compositions.md).
|
|
3
|
+
//
|
|
4
|
+
// Not a primitive: no endpoint, no run, no balance. What it has instead of a door is a
|
|
5
|
+
// RESOLUTION — `compositions resolve <key>` — and one consuming door, `phantom create
|
|
6
|
+
// --composition '{"key":"..."}'` (see phantom.mjs), which resolves it exactly once and merges
|
|
7
|
+
// it in. Authoring (list/show/create/update/delete) is CLI/REST parity work only; the mechanism
|
|
8
|
+
// itself shipped with the format.
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { CliError } from "../client.mjs";
|
|
12
|
+
import { flagStr, requirePositional, printResult, ok, argsFromFlags } from "./shared.mjs";
|
|
13
|
+
|
|
14
|
+
function readJsonFileFlag(flags, flag) {
|
|
15
|
+
const path = flagStr(flags[flag]);
|
|
16
|
+
if (path === undefined) return undefined;
|
|
17
|
+
let text;
|
|
18
|
+
try {
|
|
19
|
+
text = readFileSync(path, "utf8");
|
|
20
|
+
} catch (error) {
|
|
21
|
+
throw new CliError(
|
|
22
|
+
`Could not read --${flag} ${path}: ${error instanceof Error ? error.message : String(error)}`
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(text);
|
|
27
|
+
} catch {
|
|
28
|
+
throw new CliError(`--${flag} ${path} is not valid JSON.`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readJsonObjectFlag(flags, name) {
|
|
33
|
+
const inline = flagStr(flags[name]);
|
|
34
|
+
if (inline !== undefined) {
|
|
35
|
+
let parsed;
|
|
36
|
+
try {
|
|
37
|
+
parsed = JSON.parse(inline);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
throw new CliError(`--${name} is not valid JSON: ${err.message}`);
|
|
40
|
+
}
|
|
41
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
42
|
+
throw new CliError(`--${name} must be a JSON object`);
|
|
43
|
+
return parsed;
|
|
44
|
+
}
|
|
45
|
+
return readJsonFileFlag(flags, `${name}-file`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// `--seer '<json>'`/`--seer-file <path>` sets it, `--clear-seer` sends null (the composition
|
|
49
|
+
// stands up no seer), neither leaves it unchanged — same idiom as seers.mjs's clearableValue.
|
|
50
|
+
function seerFromFlags(flags) {
|
|
51
|
+
if (flags["clear-seer"] === true) return null;
|
|
52
|
+
return readJsonObjectFlag(flags, "seer");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* How this command sources its tool inputs — see `argsFromFlags` in shared.mjs and
|
|
57
|
+
* `scripts/parity-check.ts`. `content`/`phantom`/`seer` are each a JSON object (the collection
|
|
58
|
+
* content section, `{theme}`, and a create_seer-shaped spec, respectively) — the same
|
|
59
|
+
* `--flag '<json>'`/`--flag-file <path>` idiom `phantom create --composition` already uses,
|
|
60
|
+
* not decomposed field-by-field: they're bundles authored once and read back, not edited a
|
|
61
|
+
* field at a time from the CLI.
|
|
62
|
+
*/
|
|
63
|
+
export const ARGS = {
|
|
64
|
+
list_compositions: {},
|
|
65
|
+
get_composition: {
|
|
66
|
+
composition_id: { positional: 0, label: "composition-id", required: true },
|
|
67
|
+
},
|
|
68
|
+
resolve_composition: {
|
|
69
|
+
key: { positional: 0, label: "composition-key", required: true },
|
|
70
|
+
},
|
|
71
|
+
create_composition: {
|
|
72
|
+
name: { flag: "name", required: true },
|
|
73
|
+
description: "description",
|
|
74
|
+
content: { value: (flags) => readJsonObjectFlag(flags, "content") },
|
|
75
|
+
phantom: { value: (flags) => readJsonObjectFlag(flags, "phantom") },
|
|
76
|
+
seer: { value: (flags) => readJsonObjectFlag(flags, "seer") },
|
|
77
|
+
},
|
|
78
|
+
update_composition: {
|
|
79
|
+
composition_id: { positional: 0, label: "composition-id", required: true },
|
|
80
|
+
name: "name",
|
|
81
|
+
description: "description",
|
|
82
|
+
content: { value: (flags) => readJsonObjectFlag(flags, "content") },
|
|
83
|
+
phantom: { value: (flags) => readJsonObjectFlag(flags, "phantom") },
|
|
84
|
+
seer: { value: (flags) => seerFromFlags(flags) },
|
|
85
|
+
},
|
|
86
|
+
delete_composition: {
|
|
87
|
+
composition_id: { positional: 0, label: "composition-id", required: true },
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export async function cmdCompositions(ctx) {
|
|
92
|
+
const sub = ctx.positionals[0];
|
|
93
|
+
const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
|
|
94
|
+
switch (sub) {
|
|
95
|
+
case "list":
|
|
96
|
+
case "ls":
|
|
97
|
+
return compositionsList(rest);
|
|
98
|
+
case "show":
|
|
99
|
+
case "get":
|
|
100
|
+
return compositionsShow(rest);
|
|
101
|
+
case "create":
|
|
102
|
+
case "new":
|
|
103
|
+
return compositionsCreate(rest);
|
|
104
|
+
case "update":
|
|
105
|
+
return compositionsUpdate(rest);
|
|
106
|
+
case "delete":
|
|
107
|
+
case "rm":
|
|
108
|
+
return compositionsDelete(rest);
|
|
109
|
+
case "resolve":
|
|
110
|
+
return compositionsResolve(rest);
|
|
111
|
+
default:
|
|
112
|
+
throw new CliError(
|
|
113
|
+
`Unknown compositions subcommand: ${sub ?? "(none)"}. Try: list, show, create, update, delete, resolve`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function compositionsList(ctx) {
|
|
119
|
+
const { client, flags, io } = ctx;
|
|
120
|
+
const result = await client.callTool("list_compositions", {});
|
|
121
|
+
printResult(io, flags, result, (r) =>
|
|
122
|
+
(r.compositions ?? []).length
|
|
123
|
+
? (r.compositions ?? [])
|
|
124
|
+
.map((c) => ` ${c.key} ${c.name}${c.seer ? " (+seer)" : ""} — id ${c.id}`)
|
|
125
|
+
.join("\n")
|
|
126
|
+
: "No compositions yet."
|
|
127
|
+
);
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function compositionsShow(ctx) {
|
|
132
|
+
const { client, positionals, flags, io } = ctx;
|
|
133
|
+
requirePositional(positionals, 0, "composition-id");
|
|
134
|
+
const result = await client.callTool("get_composition", argsFromFlags(ARGS.get_composition, flags, positionals));
|
|
135
|
+
printResult(io, flags, result);
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function compositionsCreate(ctx) {
|
|
140
|
+
const { client, flags, io } = ctx;
|
|
141
|
+
const args = argsFromFlags(ARGS.create_composition, flags, []);
|
|
142
|
+
if (!args.name) throw new CliError("Missing required --name");
|
|
143
|
+
const result = await client.callTool("create_composition", args);
|
|
144
|
+
ok(ctx, `Composition created — key ${result.composition?.key}.`);
|
|
145
|
+
printResult(io, flags, result);
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function compositionsUpdate(ctx) {
|
|
150
|
+
const { client, positionals, flags, io } = ctx;
|
|
151
|
+
requirePositional(positionals, 0, "composition-id");
|
|
152
|
+
const result = await client.callTool(
|
|
153
|
+
"update_composition",
|
|
154
|
+
argsFromFlags(ARGS.update_composition, flags, positionals)
|
|
155
|
+
);
|
|
156
|
+
ok(ctx, "Composition updated.");
|
|
157
|
+
printResult(io, flags, result);
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function compositionsDelete(ctx) {
|
|
162
|
+
const { client, positionals, flags, io } = ctx;
|
|
163
|
+
requirePositional(positionals, 0, "composition-id");
|
|
164
|
+
if (!flags.yes) {
|
|
165
|
+
throw new CliError(
|
|
166
|
+
"Non-destructive to phantoms already built from it (their resolved_from_composition becomes a name pointing at nothing). Re-run with --yes to delete."
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
const result = await client.callTool(
|
|
170
|
+
"delete_composition",
|
|
171
|
+
argsFromFlags(ARGS.delete_composition, flags, positionals)
|
|
172
|
+
);
|
|
173
|
+
ok(ctx, "Composition deleted.");
|
|
174
|
+
printResult(io, flags, result);
|
|
175
|
+
return 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* `compositions resolve <key>` — turn a composition into the sections `phantom create
|
|
180
|
+
* --composition` would merge in. Read-only, costs nothing. Preview a bundle, or build the
|
|
181
|
+
* sections yourself for a door that doesn't take `composition` directly yet (`collection
|
|
182
|
+
* config` / `seers create`).
|
|
183
|
+
*/
|
|
184
|
+
async function compositionsResolve(ctx) {
|
|
185
|
+
const { client, positionals, flags, io } = ctx;
|
|
186
|
+
requirePositional(positionals, 0, "composition-key");
|
|
187
|
+
const result = await client.callTool(
|
|
188
|
+
"resolve_composition",
|
|
189
|
+
argsFromFlags(ARGS.resolve_composition, flags, positionals)
|
|
190
|
+
);
|
|
191
|
+
printResult(io, flags, result);
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
@@ -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
|
@@ -23,6 +23,7 @@ export * from "./commands/insights.mjs";
|
|
|
23
23
|
export * from "./commands/covers.mjs";
|
|
24
24
|
export * from "./commands/arena.mjs";
|
|
25
25
|
export * from "./commands/templates.mjs";
|
|
26
|
+
export * from "./commands/compositions.mjs";
|
|
26
27
|
export * from "./commands/autopen.mjs";
|
|
27
28
|
export * from "./commands/kernel.mjs";
|
|
28
29
|
export * from "./commands/lede.mjs";
|
|
@@ -31,5 +32,6 @@ export * from "./commands/onboard.mjs";
|
|
|
31
32
|
export * from "./commands/seers.mjs";
|
|
32
33
|
export * from "./commands/shredder.mjs";
|
|
33
34
|
export * from "./commands/landings.mjs";
|
|
35
|
+
export * from "./commands/offerings.mjs";
|
|
34
36
|
export * from "./commands/admin.mjs";
|
|
35
37
|
export * from "./commands/feedback.mjs";
|
package/lib/documents.mjs
CHANGED