@letterstory/cli 0.36.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 CHANGED
@@ -522,6 +522,22 @@ letterstory templates update <template-id> --angle "…" # unset fields keep t
522
522
  letterstory templates delete <template-id> --yes # bound seers fall back to defaults
523
523
  ```
524
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
+
525
541
  ## Cover images
526
542
 
527
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,6 +37,7 @@ import {
37
37
  cmdOnboard,
38
38
  cmdSeers,
39
39
  cmdTemplates,
40
+ cmdCompositions,
40
41
  cmdShredder,
41
42
  cmdLede,
42
43
  cmdLandings,
@@ -46,7 +47,7 @@ import {
46
47
  } from "./commands.mjs";
47
48
 
48
49
  // Keep in sync with cli/package.json.
49
- export const VERSION = "0.36.0";
50
+ export const VERSION = "0.36.1";
50
51
 
51
52
  // Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
52
53
  // can't accidentally swallow the id as --json's value.
@@ -564,6 +565,26 @@ Content templates (compose structures seers bind via config.template_key):
564
565
  for coverage in your own words and lets the engine pick the shape.
565
566
  templates delete <template-id> --yes
566
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
+
567
588
  Shredder (diversify content produced outside Letterstory; structure preserved):
568
589
  shredder list Your shredder endpoints + run counts
569
590
  shredder create --name <n> New endpoint (returns a callable public id)
@@ -696,6 +717,7 @@ const CLIENT_COMMANDS = {
696
717
  "phantom-onboard": cmdOnboard,
697
718
  seers: cmdSeers,
698
719
  templates: cmdTemplates,
720
+ compositions: cmdCompositions,
699
721
  shredder: cmdShredder,
700
722
  lede: cmdLede,
701
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
+ }
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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.36.0",
3
+ "version": "0.36.1",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",