@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 +16 -0
- package/lib/cli.mjs +23 -1
- package/lib/commands/compositions.mjs +193 -0
- package/lib/commands.mjs +1 -0
- package/package.json +1 -1
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.
|
|
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";
|