@letterstory/cli 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -171,6 +171,22 @@ letterstory collections delete <id> --yes
171
171
  letterstory collections assign <article-id> <collection-id>
172
172
  ```
173
173
 
174
+ ## Landing pages
175
+
176
+ A landing page is a link plus context describing what lives there. Collections with
177
+ auto-mention enabled (toggle in Collection Settings) weave relevant landing pages into
178
+ articles as they're written. Pass the URL as `--link` — `--url` is the global flag that
179
+ overrides the API base URL.
180
+
181
+ ```bash
182
+ letterstory landings list
183
+ letterstory landings get <landing-id>
184
+ letterstory landings create --link https://example.com/dataset --context "Our 2026 benchmark dataset. Cite it when an article discusses response-time or CSAT metrics."
185
+ letterstory landings create --link https://example.com/report --file ./context.md # context from a file (or --file - for stdin)
186
+ letterstory landings update <landing-id> --context "Updated context"
187
+ letterstory landings delete <landing-id> --yes
188
+ ```
189
+
174
190
  ## Flows
175
191
 
176
192
  Run an editorial pass over an article, check on it, and manage the completion webhook:
@@ -213,21 +229,6 @@ letterstory onboarding status
213
229
  letterstory onboarding step --complete connect_domain
214
230
  ```
215
231
 
216
- ### Magical onboarding
217
-
218
- One command from a domain to a filled-out strategy: it infers and saves the manifesto,
219
- stances, and identity guardrail from the client's website (never clobbering anything
220
- hand-written), optionally imports the blog's post history from a sitemap, infers keywords
221
- and builds topic clusters, then detects the content series those posts already imply and
222
- adopts them — coverage reconstructed from existing posts, gaps left for the queue to write.
223
- Idempotent; safe to re-run.
224
-
225
- ```bash
226
- letterstory onboarding magic # use the saved company domain
227
- letterstory onboarding magic --domain acme.com --sitemap https://acme.com/sitemap.xml
228
- letterstory onboarding magic --dry-run # preview series proposals only
229
- ```
230
-
231
232
  ## Content series
232
233
 
233
234
  A series is a coverage obligation, not a keyword bet: "content that looks like this",
@@ -235,7 +236,6 @@ one post per declared item. Series feed the same planner queue as topic clusters
235
236
 
236
237
  ```bash
237
238
  letterstory series list
238
- letterstory series propose # detect series in existing posts
239
239
  letterstory series create --name "Secrets, tool by tool" \
240
240
  --description "One practical secrets guide per IaC tool." \
241
241
  --coverage "Pulumi,Terraform,Ansible" --template <key>
@@ -330,7 +330,7 @@ Content templates (the compose structures seers bind via `config.template_key`):
330
330
  ```bash
331
331
  letterstory templates list # custom + read-only built-ins
332
332
  letterstory templates create --name "Release notes" --angle "What shipped and why" \
333
- --sections-file sections.json
333
+ --blocks-file blocks.json
334
334
  letterstory templates update <template-id> --angle "…" # unset fields keep their values
335
335
  letterstory templates delete <template-id> --yes # bound seers fall back to defaults
336
336
  ```
package/lib/cli.mjs CHANGED
@@ -28,22 +28,28 @@ import {
28
28
  cmdSeries,
29
29
  cmdInsights,
30
30
  cmdResearch,
31
+ cmdAutoPen,
31
32
  cmdCovers,
32
33
  cmdKernel,
33
34
  cmdPhantomJob,
35
+ cmdOnboard,
34
36
  cmdSeers,
35
37
  cmdTemplates,
38
+ cmdShredder,
39
+ cmdLandings,
36
40
  } from "./commands.mjs";
37
41
 
38
42
  // Keep in sync with cli/package.json.
39
- export const VERSION = "0.6.0";
43
+ export const VERSION = "0.8.0";
40
44
 
41
45
  // Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
42
46
  // can't accidentally swallow the id as --json's value.
43
47
  const BOOLEAN_FLAGS = new Set([
44
48
  "json",
45
49
  "yes",
50
+ "confirm-missing-target",
46
51
  "no-wait",
52
+ "accept",
47
53
  "help",
48
54
  "version",
49
55
  "quiet",
@@ -166,6 +172,13 @@ Collections:
166
172
  collections update <id> [--name] [--description] [--cadence-target <n>] [--cadence-period week|month] [--cover-canvas-type-id <uuid>|none]
167
173
  collections delete <id> --yes
168
174
  collections assign <article-id> <collection-id>
175
+ collections autopen <id> [--template <key>|none] [--kernel-family <name>|org]
176
+ [--mention off|subtle|woven] [--outline-depth barebones|fleshed_out]
177
+ [--direction-gate true|false] [--auto-start true|false]
178
+ The Auto-Pen settings the app's panel writes. --template binds a content
179
+ template (from 'templates list') as this collection's default, which is how
180
+ a template you author here actually gets used. A patch: unset flags are
181
+ left alone.
169
182
 
170
183
  Authors (the per-collection author bank — recurring bylines):
171
184
  authors list <collection-id> List the bank + byline distribution
@@ -187,6 +200,8 @@ Flows:
187
200
  Connectors:
188
201
  connectors list List external publish destinations
189
202
  connectors publish <article-id> --to <connector> --target <uuid> [--flow-run <uuid>]
203
+ [--confirm-missing-target] re-publish even though
204
+ the item was deleted/unpublished on the provider
190
205
  connectors status --connector <connector> --publish-id <id>
191
206
 
192
207
  Strategy & onboarding:
@@ -199,12 +214,7 @@ Strategy & onboarding:
199
214
  strategy topics set --collection <uuid> (--topic <topic-id> | --suggestion <suggestion-id>)
200
215
  onboarding status Show the onboarding checklist
201
216
  onboarding step [--current <step>] [--complete <step>] [--skip <step>] [--status <status>]
202
- onboarding magic [--domain <domain>] [--sitemap <url>] [--collection <uuid>] [--dry-run]
203
- Enter a domain -> strategy filled out:
204
- manifesto+stances, optional history import,
205
- clusters, detected series (see below)
206
217
  series list The org's content series + coverage state
207
- series propose [--collection <uuid>] Detect series existing posts already imply
208
218
  series create --name <n> --description <d> [--coverage a,b,c] [--collection <uuid>] [--template <key>]
209
219
  series backfill <series-id> [--collection <uuid>] Reconstruct a series from existing posts
210
220
 
@@ -217,6 +227,12 @@ Research agent (deep research -> outline written into the post):
217
227
  Answer the --gate pause
218
228
  research cancel --article <uuid> Cancel a run in flight
219
229
 
230
+ Auto-Pen (research -> kernel -> Enhance, in one run — and the ONLY path that
231
+ applies a collection's content template; kernel run does not):
232
+ autopen start --article <uuid> Start the run; takes 10-40 min
233
+ autopen status --article <uuid> Step states + the template in use
234
+ autopen cancel --article <uuid> --yes Stop a run in flight
235
+
220
236
  Cover images (which image model draws the post's cover):
221
237
  covers models Image models connected right now
222
238
  covers variants --article <uuid> [--model <id> …] [--ref-url <url> …]
@@ -237,6 +253,14 @@ Phantom orchestrator:
237
253
  Topic -> draft -> publish -> rebuild
238
254
  phantom-job status <job-id> Check a job's stage
239
255
 
256
+ Phantom onboarding recipe (domain -> live phantom blog with first posts):
257
+ ${bin === "phantom" ? "onboard" : "phantom-onboard"} <domain> [--name X] [--posts N] [--cadence N]
258
+ [--cover editorial_image|full_ai|line_art] [--theme T] [--accept] [--dry-run]
259
+ Plan (read-only) then, with --accept
260
+ or an override flag, provision + configure
261
+ + seed. --name is committed at create (it
262
+ seeds the URL); pass it before applying.
263
+
240
264
  Seers (event-driven signals -> drafts):
241
265
  seers list List seers with recent activity
242
266
  seers get <seer-id> One seer's full config + poll state
@@ -263,10 +287,32 @@ Seers (event-driven signals -> drafts):
263
287
  Content templates (compose structures seers bind via config.template_key):
264
288
  templates list Custom + built-in catalog
265
289
  templates show <template-id>
266
- templates create --name <n> --angle <text> (--sections '<json>'|--sections-file <path>)
267
- templates update <template-id> [--name] [--angle] [--sections '<json>'|--sections-file <path>]
290
+ templates create --name <n> (--blocks '<json>'|--blocks-file <path>) [--angle <text>]
291
+ templates update <template-id> [--name] [--blocks '<json>'|--blocks-file <path>] [--angle]
292
+ A block is { id, kind: 'formatted'|'open', text (markdown), optional? }. In a
293
+ 'formatted' block the markdown IS the shape: prose -> paragraphs; "- item" -> a
294
+ bulleted list; "1. item" -> numbered steps; a table skeleton -> a comparison;
295
+ "> quoted" -> reproduced word for word. Empty items ("- ") ask for a list without
296
+ dictating it — how many it has is decided from the material. An 'open' block asks
297
+ for coverage in your own words and lets the engine pick the shape.
268
298
  templates delete <template-id> --yes
269
299
 
300
+ Shredder (diversify content produced outside Letterstory; structure preserved):
301
+ shredder list Your shredder endpoints + run counts
302
+ shredder create --name <n> New endpoint (returns a callable public id)
303
+ shredder delete <endpoint-id> --yes
304
+ shredder run <endpoint-id> (--body <text>|--file <path>|--file -) [--coverage <0..1>] [--attempts <n>]
305
+ Shred prose through an endpoint (logs telemetry)
306
+ shredder runs [--endpoint <id>] [--limit <n>] Recent shred calls (telemetry), newest first
307
+
308
+ Landings (a link + context; auto-mentioned in articles by collections you enable):
309
+ landings list Your landing pages + mention counts
310
+ landings get <landing-id> Show one landing page
311
+ landings create --link <url> [--context <text>|--file <path>|--file -]
312
+ Add a landing page (--link, not --url)
313
+ landings update <landing-id> [--link <url>] [--context <text>|--file <path>]
314
+ landings delete <landing-id> --yes
315
+
270
316
  Insights:
271
317
  insights site [--period 14d|30d|90d] [--collection <uuid>]
272
318
  insights post <article-id> [--period 14d|30d|90d]
@@ -274,7 +320,7 @@ Insights:
274
320
 
275
321
  Anything else:
276
322
  tools list List every tool this server exposes, by name
277
- tools show <name> Show one tool's capability + full argument schema
323
+ tools show <name> Show one tool's capability, argument schema, and return shape
278
324
  call <tool> [--args '<json>'] [--flag value …] Call any tool directly
279
325
  tool <name> [--arg k=v …] [--json-args '<json>'] [--stdin]
280
326
  Call any tool with schema-coerced arguments
@@ -325,11 +371,15 @@ const CLIENT_COMMANDS = {
325
371
  series: cmdSeries,
326
372
  insights: cmdInsights,
327
373
  research: cmdResearch,
374
+ autopen: cmdAutoPen,
328
375
  covers: cmdCovers,
329
376
  kernel: cmdKernel,
330
377
  "phantom-job": cmdPhantomJob,
378
+ "phantom-onboard": cmdOnboard,
331
379
  seers: cmdSeers,
332
380
  templates: cmdTemplates,
381
+ shredder: cmdShredder,
382
+ landings: cmdLandings,
333
383
  };
334
384
 
335
385
  // LETTERSTORY_POLL_INTERVAL_MS / LETTERSTORY_MAX_POLLS let an operator (or an
@@ -372,7 +422,10 @@ export async function run(argv, io = defaultIo()) {
372
422
  return 0;
373
423
  }
374
424
 
375
- const handler = CLIENT_COMMANDS[command];
425
+ // Under the phantom-branded binary, `phantom onboard` is the natural spelling — accept
426
+ // it as an alias for the recipe so users don't type the stuttering `phantom phantom-onboard`.
427
+ // The `letterstory` binary keeps only `phantom-onboard`, so it never shadows `onboarding`.
428
+ const handler = CLIENT_COMMANDS[command] ?? (bin === "phantom" && command === "onboard" ? cmdOnboard : undefined);
376
429
  if (!handler) {
377
430
  io.error(`Unknown command: ${command}`);
378
431
  io.error(`Run \`${bin} help\` for usage.`);
package/lib/client.mjs CHANGED
@@ -87,8 +87,9 @@ export class LetterstoryClient {
87
87
  return `${this.url}/api/mcp`;
88
88
  }
89
89
 
90
- // Unauthenticated discovery (GET /api/mcp): tool names + descriptions, no schema.
91
- // Lets `letterstory tools` work before you've logged in.
90
+ // Unauthenticated discovery (GET /api/mcp): tool names, descriptions, capability, and
91
+ // the derived input + output schemas. Lets `letterstory tools`/`tools show` work before
92
+ // you've logged in.
92
93
  async discover() {
93
94
  if (this.verbose) this.verboseLog(`» GET ${this.mcpEndpoint}`);
94
95
  let res;
@@ -0,0 +1,59 @@
1
+ // `autopen` — run a collection's full Auto-Pen sequence over an existing draft:
2
+ // research, write with the writing kernel, then the collection's Enhance flow.
3
+ //
4
+ // This is the command that makes a TEMPLATE take effect. A template bound with
5
+ // `collections autopen <id> --template-key <key>` is read inside the Auto-Pen run and
6
+ // nowhere else, so `kernel run` (which hands the draft's current text straight to a
7
+ // kernel) will not produce a templated post no matter how the collection is configured.
8
+ //
9
+ // A run takes 10-40 minutes, so `start` returns as soon as it is queued. `status` both
10
+ // reports and nudges the run along; a sweep also advances it every 5 minutes, so a run
11
+ // finishes whether or not anyone polls.
12
+
13
+ import { CliError } from "../client.mjs";
14
+ import { flagBool, requireFlag, printResult, ok } from "./shared.mjs";
15
+
16
+ export async function cmdAutoPen(ctx) {
17
+ const sub = ctx.positionals[0];
18
+ const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
19
+ switch (sub) {
20
+ case "start":
21
+ case "run":
22
+ return autoPenStart(rest);
23
+ case "status":
24
+ return autoPenStatus(rest);
25
+ case "cancel":
26
+ return autoPenCancel(rest);
27
+ default:
28
+ throw new CliError(`Unknown autopen subcommand: ${sub ?? "(none)"}. Try: start, status, cancel`);
29
+ }
30
+ }
31
+
32
+ async function autoPenStart(ctx) {
33
+ const { client, flags, io } = ctx;
34
+ const article = requireFlag(flags, "article");
35
+ const result = await client.callTool("run_auto_pen", { article_id: article });
36
+ ok(ctx, `Auto-Pen started. Check progress with: autopen status --article ${article}`);
37
+ printResult(io, flags, result);
38
+ return 0;
39
+ }
40
+
41
+ async function autoPenStatus(ctx) {
42
+ const { client, flags, io } = ctx;
43
+ const article = requireFlag(flags, "article");
44
+ const result = await client.callTool("get_auto_pen_status", { article_id: article });
45
+ printResult(io, flags, result);
46
+ return 0;
47
+ }
48
+
49
+ async function autoPenCancel(ctx) {
50
+ const { client, flags, io } = ctx;
51
+ const article = requireFlag(flags, "article");
52
+ if (!flagBool(flags.yes)) {
53
+ throw new CliError("Cancelling stops a run in progress. Re-run with --yes to confirm.");
54
+ }
55
+ const result = await client.callTool("cancel_auto_pen", { article_id: article });
56
+ ok(ctx, "Auto-Pen cancelled.");
57
+ printResult(io, flags, result);
58
+ return 0;
59
+ }
@@ -21,9 +21,11 @@ export async function cmdCollections(ctx) {
21
21
  return collectionsDelete(rest);
22
22
  case "assign":
23
23
  return collectionsAssign(rest);
24
+ case "autopen":
25
+ return collectionsAutoPen(rest);
24
26
  default:
25
27
  throw new CliError(
26
- `Unknown collections subcommand: ${sub ?? "(none)"}. Try: list, new, update, delete, assign`
28
+ `Unknown collections subcommand: ${sub ?? "(none)"}. Try: list, new, update, delete, assign, autopen`
27
29
  );
28
30
  }
29
31
  }
@@ -99,3 +101,45 @@ async function collectionsAssign(ctx) {
99
101
  printResult(io, flags, result);
100
102
  return 0;
101
103
  }
104
+
105
+ /**
106
+ * `collections autopen <id>` — the Auto-Pen settings the app's panel writes, including which
107
+ * template the collection's posts compose with. Without it, a template authored through this
108
+ * CLI could be created but never actually used by anything.
109
+ *
110
+ * A patch: only the flags given are sent, and the server leaves the rest alone.
111
+ */
112
+ async function collectionsAutoPen(ctx) {
113
+ const { client, positionals, flags, io } = ctx;
114
+ const collectionId = requirePositional(positionals, 0, "collection-id");
115
+ const patch = { collection_id: collectionId };
116
+
117
+ // `--template none` clears the binding; the server takes null for that.
118
+ const template = flagStr(flags.template);
119
+ if (template !== undefined) patch.template_key = template === "none" ? null : template;
120
+
121
+ const kernelFamily = flagStr(flags["kernel-family"]);
122
+ if (kernelFamily !== undefined) patch.kernel = { family: kernelFamily === "org" ? null : kernelFamily };
123
+
124
+ const mention = flagStr(flags.mention);
125
+ if (mention !== undefined) patch.mention = { degree: mention };
126
+
127
+ const depth = flagStr(flags["outline-depth"]);
128
+ if (depth !== undefined) patch.outline = { ...(patch.outline ?? {}), depth };
129
+
130
+ if (flags["direction-gate"] !== undefined) {
131
+ patch.outline = { ...(patch.outline ?? {}), direction_gate: flags["direction-gate"] !== "false" };
132
+ }
133
+ if (flags["auto-start"] !== undefined) patch.auto_start_on_curate = flags["auto-start"] !== "false";
134
+
135
+ if (Object.keys(patch).length === 1) {
136
+ throw new CliError(
137
+ "Nothing to change. Pass at least one of --template, --kernel-family, --mention, --outline-depth, --direction-gate, --auto-start."
138
+ );
139
+ }
140
+
141
+ const result = await client.callTool("update_auto_pen_config", patch);
142
+ ok(ctx, `Auto-Pen settings updated for ${collectionId}.`);
143
+ printResult(io, flags, result);
144
+ return 0;
145
+ }
@@ -3,7 +3,7 @@
3
3
  // phantomstory-cli's connectors.ts.
4
4
 
5
5
  import { CliError } from "../client.mjs";
6
- import { flagStr, requireFlag, requirePositional, printResult, compact, ok } from "./shared.mjs";
6
+ import { flagStr, flagBool, requireFlag, requirePositional, printResult, compact, ok } from "./shared.mjs";
7
7
 
8
8
  export async function cmdConnectors(ctx) {
9
9
  const sub = ctx.positionals[0];
@@ -34,11 +34,16 @@ async function connectorsPublish(ctx) {
34
34
  const articleId = requirePositional(positionals, 0, "article-id");
35
35
  const to = requireFlag(flags, "to");
36
36
  const target = requireFlag(flags, "target");
37
+ // A re-publish whose external item was deleted or unpublished in the client's CMS
38
+ // is refused, and the refusal explains itself. --confirm-missing-target is the
39
+ // answer to that specific question: it re-creates / re-publishes the item there.
40
+ // Deliberately not bundled into a general --yes, and never set by default.
37
41
  const args = compact({
38
42
  article_id: articleId,
39
43
  connector: to,
40
44
  target_id: target,
41
45
  flow_run_id: flagStr(flags["flow-run"]),
46
+ confirm_missing_target: flagBool(flags["confirm-missing-target"]) || undefined,
42
47
  });
43
48
  const result = await client.callTool("publish_to_connector", args);
44
49
  ok(ctx, `Publishing to ${to}.`);
@@ -73,17 +73,55 @@ function renderTool(io, tool) {
73
73
  const detail = detailOf(props[name]);
74
74
  io.log(` ${name} (${typeLabel(props[name])}, ${req})${detail ? ` — ${detail}` : ""}`);
75
75
  }
76
+ renderReturns(io, tool.outputSchema);
77
+ }
78
+
79
+ // The object node to descend into for a field: the node itself if it has
80
+ // properties, or an array's element when that element is an object. Mirrors the
81
+ // `/docs` "Returns" tree so the CLI and the web console describe one shape.
82
+ function returnObject(node) {
83
+ if (!node || typeof node !== "object") return null;
84
+ if (node.properties) return node;
85
+ if (node.type === "array" && node.items?.properties) return node.items;
86
+ if (Array.isArray(node.anyOf)) return node.anyOf.find((n) => n?.properties) ?? null;
87
+ return null;
88
+ }
89
+
90
+ // Print the tool's success-response shape as an indented `name (type)` tree, so a
91
+ // caller sees what comes back without running the call. Fed by the `outputSchema`
92
+ // that `tools/list` now advertises; silent for a tool whose return isn't modelled.
93
+ function renderReturns(io, schema) {
94
+ const root = returnObject(schema);
95
+ if (!root?.properties) return;
96
+ io.log(``);
97
+ io.log(` returns:`);
98
+ const walk = (node, indent) => {
99
+ const obj = returnObject(node);
100
+ if (!obj?.properties) return;
101
+ const req = new Set(obj.required ?? []);
102
+ for (const [name, child] of Object.entries(obj.properties)) {
103
+ const opt = req.has(name) ? "" : ", optional";
104
+ const desc = child.description ? ` — ${child.description}` : "";
105
+ io.log(`${" ".repeat(indent)}${name} (${typeLabel(child)}${opt})${desc}`);
106
+ walk(child, indent + 2);
107
+ }
108
+ };
109
+ walk(root, 4);
76
110
  }
77
111
 
78
112
  function typeLabel(prop) {
79
113
  if (!prop) return "any";
80
114
  if (prop.enum) return "enum";
115
+ if (prop.type === "array") return `${typeLabel(prop.items)}[]`;
81
116
  if (typeof prop.type === "string") return prop.type;
82
117
  if (Array.isArray(prop.type)) return prop.type.filter((t) => t !== "null").join("|") || "any";
83
118
  if (prop.anyOf) {
84
- const types = prop.anyOf.map((a) => (typeof a.type === "string" ? a.type : "any")).filter((t) => t !== "null");
85
- return [...new Set(types)].join("|") || "any";
119
+ const nullable = prop.anyOf.some((a) => a?.type === "null");
120
+ const types = prop.anyOf.filter((a) => a?.type !== "null").map(typeLabel);
121
+ const base = [...new Set(types)].join("|") || "any";
122
+ return nullable ? `${base}|null` : base;
86
123
  }
124
+ if (prop.properties) return "object";
87
125
  return "any";
88
126
  }
89
127
 
@@ -0,0 +1,86 @@
1
+ // `landings` — manage landing pages: a link plus context describing what lives there.
2
+ // Collections with auto-mention enabled weave relevant landing pages into articles as
3
+ // they're written. NOTE: the landing's URL is passed as --link, not --url, because
4
+ // --url is the global flag that overrides the API base URL.
5
+
6
+ import { CliError } from "../client.mjs";
7
+ import { flagStr, requireFlag, requirePositional, readBodyInput, printResult, compact, ok } from "./shared.mjs";
8
+
9
+ export async function cmdLandings(ctx) {
10
+ const sub = ctx.positionals[0];
11
+ const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
12
+ switch (sub) {
13
+ case "list":
14
+ case "ls":
15
+ return landingsList(rest);
16
+ case "get":
17
+ return landingsGet(rest);
18
+ case "new":
19
+ case "create":
20
+ return landingsCreate(rest);
21
+ case "update":
22
+ return landingsUpdate(rest);
23
+ case "delete":
24
+ case "rm":
25
+ return landingsDelete(rest);
26
+ default:
27
+ throw new CliError(
28
+ `Unknown landings subcommand: ${sub ?? "(none)"}. Try: list, get, create, update, delete`
29
+ );
30
+ }
31
+ }
32
+
33
+ async function landingsList(ctx) {
34
+ const { client, flags, io } = ctx;
35
+ const result = await client.callTool("list_landings", {});
36
+ printResult(io, flags, result);
37
+ return 0;
38
+ }
39
+
40
+ async function landingsGet(ctx) {
41
+ const { client, positionals, flags, io } = ctx;
42
+ const id = requirePositional(positionals, 0, "landing-id");
43
+ const result = await client.callTool("get_landing", { landing_id: id });
44
+ printResult(io, flags, result);
45
+ return 0;
46
+ }
47
+
48
+ async function landingsCreate(ctx) {
49
+ const { client, flags, io } = ctx;
50
+ // --link (not --url: --url is the global API-base override) is the landing's URL.
51
+ const url = requireFlag(flags, "link");
52
+ // Context comes from --context, or --body/--file/stdin for a larger blob.
53
+ const context = flagStr(flags.context) ?? readBodyInput(flags);
54
+ const result = await client.callTool("create_landing", compact({ url, context }));
55
+ ok(ctx, `Created landing page ${url}.`);
56
+ printResult(io, flags, result);
57
+ return 0;
58
+ }
59
+
60
+ async function landingsUpdate(ctx) {
61
+ const { client, positionals, flags, io } = ctx;
62
+ const id = requirePositional(positionals, 0, "landing-id");
63
+ const url = flagStr(flags.link);
64
+ const context = flagStr(flags.context) ?? readBodyInput(flags);
65
+ const patch = compact({ landing_id: id, url, context });
66
+ if (Object.keys(patch).length <= 1) {
67
+ throw new CliError("Nothing to update — pass --link <url> and/or --context <text> (or --file <path>).");
68
+ }
69
+ const result = await client.callTool("update_landing", patch);
70
+ ok(ctx, "Updated landing page.");
71
+ printResult(io, flags, result);
72
+ return 0;
73
+ }
74
+
75
+ async function landingsDelete(ctx) {
76
+ const { client, positionals, flags, io } = ctx;
77
+ const id = requirePositional(positionals, 0, "landing-id");
78
+ if (!flags.yes) {
79
+ io.error(`Re-run with --yes to confirm: ${ctx.bin} landings delete ${id} --yes`);
80
+ return 1;
81
+ }
82
+ const result = await client.callTool("delete_landing", { landing_id: id });
83
+ ok(ctx, "Landing page deleted.");
84
+ printResult(io, flags, result);
85
+ return 0;
86
+ }
@@ -0,0 +1,126 @@
1
+ // `onboard` — the one-shot phantom onboarding recipe: a client domain in, a live phantom
2
+ // blog with its first post(s) out. It drives ONE server tool, `onboard_phantom`:
3
+ // - a bare `onboard <domain>` is a safe DRY RUN (accept:false) — it prints the proposed
4
+ // plan (masthead name, description, strategy, cadence/cover/seed defaults) and stops.
5
+ // - `--accept` (or any override flag) APPLIES: `onboard_phantom{accept:true}` starts a
6
+ // durable onboarding job server-side and returns a job_id; this command then polls
7
+ // `get_phantom_onboard_status` until the job is done (provision → configure → seed).
8
+ //
9
+ // IMPORTANT: the masthead name is committed when the site is created (it seeds the URL),
10
+ // so to use a name other than the inferred one, pass --name BEFORE applying. The first
11
+ // posts are written by background jobs (20-40min each); they're reported as job ids and
12
+ // this command does not block on them — poll with `phantom-job status <job-id>`.
13
+
14
+ import { CliError } from "../client.mjs";
15
+ import { flagStr, flagNum, flagBool, requirePositional, printResult, compact, ok, pollUntil } from "./shared.mjs";
16
+
17
+ const COVER_STYLES = ["editorial_image", "full_ai", "line_art"];
18
+
19
+ function formatPlan(plan) {
20
+ const lines = [
21
+ `Plan for ${plan.domain} (client: ${plan.client_label})`,
22
+ ` proposed name: ${plan.proposed_name ?? "(none — pass --name)"}`,
23
+ ` description: ${plan.description}`,
24
+ ` defaults: ${plan.defaults.cadence_per_week}/week · ${plan.defaults.cover_canvas} · seed ${plan.defaults.posts_to_seed}`,
25
+ ];
26
+ if (plan.strategy) {
27
+ lines.push(` topics: ${(plan.strategy.priority_topics ?? []).slice(0, 5).join(", ") || "(none)"}`);
28
+ } else {
29
+ lines.push(` strategy: (none inferred)`);
30
+ }
31
+ lines.push(` note: ${plan.note}`);
32
+ return lines.join("\n");
33
+ }
34
+
35
+ function formatStatus(s) {
36
+ const lines = [
37
+ `Onboarding ${s.status} at stage "${s.stage}"`,
38
+ ` domain: ${s.domain}`,
39
+ ` deployment: ${s.deployment_id ?? "(pending)"}`,
40
+ ` collection: ${s.collection_id ?? "(pending)"}`,
41
+ ];
42
+ if (s.error) lines.push(` error: ${s.error}`);
43
+ if (s.seed_job_ids?.length) {
44
+ lines.push(` seeded ${s.seed_job_ids.length} post job(s):`);
45
+ for (const id of s.seed_job_ids) lines.push(` - ${id}`);
46
+ lines.push(` poll them with: phantom-job status <job-id>`);
47
+ }
48
+ return lines.join("\n");
49
+ }
50
+
51
+ // Build the overrides object the tool accepts from the CLI flags (snake_case keys).
52
+ function overridesFromFlags(flags) {
53
+ const cover = flagStr(flags.cover);
54
+ if (cover !== undefined && !COVER_STYLES.includes(cover)) {
55
+ throw new CliError(`--cover must be one of: ${COVER_STYLES.join(", ")}`);
56
+ }
57
+ return compact({
58
+ name: flagStr(flags.name),
59
+ cadence_per_week: flagNum(flags.cadence),
60
+ cover_canvas: cover,
61
+ theme: flagStr(flags.theme),
62
+ posts_to_seed: flagNum(flags.posts),
63
+ });
64
+ }
65
+
66
+ export async function cmdOnboard(ctx) {
67
+ const { client, flags, io } = ctx;
68
+ const domain = requirePositional(ctx.positionals, 0, "domain");
69
+ const overrides = overridesFromFlags(flags);
70
+
71
+ // Step 1: dry-run plan (accept defaults to false server-side). Persists nothing.
72
+ const plan = await client.callTool(
73
+ "onboard_phantom",
74
+ compact({ domain, overrides: Object.keys(overrides).length ? overrides : undefined })
75
+ );
76
+ ok(ctx, "Onboarding plan:");
77
+ printResult(io, flags, plan, formatPlan);
78
+
79
+ if (flagBool(flags["dry-run"])) return 0;
80
+
81
+ // Step 2: the review gate. A bare, unaccepted run stops here so nothing is provisioned
82
+ // by accident — re-run with --accept (or an override flag) once happy.
83
+ const overrideGiven = Object.keys(overrides).length > 0;
84
+ if (!flagBool(flags.accept) && !overrideGiven) {
85
+ ok(
86
+ ctx,
87
+ "Re-run with --accept to apply, or pass --name/--cadence/--cover/--posts/--theme to adjust. " +
88
+ "Note: --name is committed at create (it seeds the URL)."
89
+ );
90
+ return 0;
91
+ }
92
+ if (plan.proposed_name === null && flagStr(flags.name) === undefined) {
93
+ throw new CliError(
94
+ "No blog name available: the plan proposed none (strategy soft-failed). Pass --name <MastheadName> to apply."
95
+ );
96
+ }
97
+
98
+ // Step 3: apply — start the durable onboarding job.
99
+ ok(ctx, "Applying — starting the onboarding job…");
100
+ const started = await client.callTool(
101
+ "onboard_phantom",
102
+ compact({ domain, accept: true, overrides: Object.keys(overrides).length ? overrides : undefined })
103
+ );
104
+ const jobId = started.job_id;
105
+ if (!jobId) throw new CliError("onboard_phantom did not return a job id.");
106
+
107
+ // Step 4: poll the job to a terminal state, printing stage transitions.
108
+ let lastStage = null;
109
+ const final = await pollUntil(
110
+ io,
111
+ async () => {
112
+ const s = await client.callTool("get_phantom_onboard_status", { job_id: jobId });
113
+ if (s.status === "failed")
114
+ throw new CliError(`Onboarding failed at stage "${s.stage}": ${s.error ?? "unknown error"}`);
115
+ if (s.stage !== lastStage) {
116
+ ok(ctx, ` … ${s.stage}`);
117
+ lastStage = s.stage;
118
+ }
119
+ return { done: s.status === "done", value: s };
120
+ },
121
+ { label: "onboarding to finish", timeoutMs: 20 * 60 * 1000, intervalMs: 4000 }
122
+ );
123
+
124
+ printResult(io, flags, final, formatStatus);
125
+ return 0;
126
+ }
@@ -625,18 +625,18 @@ async function templatesShow(ctx) {
625
625
  return 0;
626
626
  }
627
627
 
628
- function templateSectionsFromFlags(flags) {
629
- const inline = flagStr(flags.sections);
630
- const file = flagStr(flags["sections-file"]);
628
+ function templateBlocksFromFlags(flags) {
629
+ const inline = flagStr(flags.blocks);
630
+ const file = flagStr(flags["blocks-file"]);
631
631
  if (inline === undefined && file === undefined) return undefined;
632
- const raw = inline !== undefined ? inline : readFileOrThrow(file, "--sections-file");
632
+ const raw = inline !== undefined ? inline : readFileOrThrow(file, "--blocks-file");
633
633
  let parsed;
634
634
  try {
635
635
  parsed = JSON.parse(raw);
636
636
  } catch (err) {
637
- throw new CliError(`--sections is not valid JSON: ${err.message}`);
637
+ throw new CliError(`--blocks is not valid JSON: ${err.message}`);
638
638
  }
639
- if (!Array.isArray(parsed)) throw new CliError("--sections must be a JSON array of section objects");
639
+ if (!Array.isArray(parsed)) throw new CliError("--blocks must be a JSON array of block objects");
640
640
  return parsed;
641
641
  }
642
642
 
@@ -644,11 +644,14 @@ async function templatesCreate(ctx) {
644
644
  const { client, flags, io } = ctx;
645
645
  const name = flagStr(flags.name);
646
646
  const angle = flagStr(flags.angle);
647
- const sections = templateSectionsFromFlags(flags);
647
+ const blocks = templateBlocksFromFlags(flags);
648
648
  if (!name) throw new CliError("Missing required --name");
649
- if (!angle) throw new CliError("Missing required --angle");
650
- if (!sections) throw new CliError("Missing required --sections '<json>' or --sections-file <path>");
651
- const result = await client.callTool("create_template", { name, angle, sections });
649
+ // `--angle` is OPTIONAL, and omitting it is the better answer. It was required here while
650
+ // the server schema had already made it optional and the app had dropped the field: a
651
+ // template constrains a post's SHAPE and never says what it is about, so an angle belongs
652
+ // to the post — which already has one, from its research direction.
653
+ if (!blocks) throw new CliError("Missing required --blocks '<json>' or --blocks-file <path>");
654
+ const result = await client.callTool("create_template", { name, blocks, ...(angle ? { angle } : {}) });
652
655
  ok(ctx, `Template created — key ${result.template?.key}.`);
653
656
  printResult(io, flags, result);
654
657
  return 0;
@@ -659,11 +662,11 @@ async function templatesUpdate(ctx) {
659
662
  const templateId = requirePositional(positionals, 0, "template-id");
660
663
  const name = flagStr(flags.name);
661
664
  const angle = flagStr(flags.angle);
662
- const sections = templateSectionsFromFlags(flags);
665
+ const blocks = templateBlocksFromFlags(flags);
663
666
  // update_template replaces the whole template; fetch current values for any
664
667
  // field the caller didn't pass so a partial edit doesn't blank the rest.
665
668
  let current;
666
- if (!name || !angle || !sections) {
669
+ if (!name || !angle || !blocks) {
667
670
  const existing = await client.callTool("get_template", { template_id: templateId });
668
671
  current = existing.template;
669
672
  }
@@ -671,7 +674,7 @@ async function templatesUpdate(ctx) {
671
674
  template_id: templateId,
672
675
  name: name ?? current.name,
673
676
  angle: angle ?? current.angle,
674
- sections: sections ?? current.sections,
677
+ blocks: blocks ?? current.blocks,
675
678
  });
676
679
  ok(ctx, "Template updated.");
677
680
  printResult(io, flags, result);
@@ -17,10 +17,8 @@ export async function cmdSeries(ctx) {
17
17
  return seriesCreate(rest);
18
18
  case "backfill":
19
19
  return seriesBackfill(rest);
20
- case "propose":
21
- return seriesPropose(rest);
22
20
  default:
23
- throw new CliError(`Unknown series subcommand: ${sub ?? "(none)"}. Try: list, create, backfill, propose`);
21
+ throw new CliError(`Unknown series subcommand: ${sub ?? "(none)"}. Try: list, create, backfill`);
24
22
  }
25
23
  }
26
24
 
@@ -77,25 +75,3 @@ async function seriesBackfill(ctx) {
77
75
  printResult(io, flags, result);
78
76
  return 0;
79
77
  }
80
-
81
- function formatProposals(result) {
82
- const proposals = result?.proposals ?? [];
83
- if (proposals.length === 0) return "No series detected — this blog has no clearly repeating shapes.";
84
- return proposals
85
- .map((p) => {
86
- const matched = p.matchedTitles?.length ?? 0;
87
- return [
88
- `${p.name} — ${p.description}`,
89
- ` covers ${matched} existing post(s); items: ${p.coverage.join(", ")}`,
90
- ].join("\n");
91
- })
92
- .join("\n");
93
- }
94
-
95
- async function seriesPropose(ctx) {
96
- const { client, flags, io } = ctx;
97
- const args = compact({ collection_id: flagStr(flags.collection) });
98
- const result = await client.callTool("propose_series", args);
99
- printResult(io, flags, result, formatProposals);
100
- return 0;
101
- }
@@ -70,6 +70,25 @@ export function ok(ctx, message) {
70
70
  ctx.io.log(message);
71
71
  }
72
72
 
73
+ // Poll an async step to a terminal state. `poll` runs one check and returns
74
+ // `{ done, value }`: `done:true` ends the loop and `pollUntil` resolves to `value`.
75
+ // Progress logging is the caller's job (it happens inside `poll`, which has ctx),
76
+ // so this stays --json/--quiet-clean. Throws a CliError once the deadline passes.
77
+ export async function pollUntil(io, poll, opts = {}) {
78
+ const intervalMs = opts.intervalMs ?? 3000;
79
+ const timeoutMs = opts.timeoutMs ?? 300000;
80
+ const label = opts.label ?? "operation";
81
+ const deadline = Date.now() + timeoutMs;
82
+ for (;;) {
83
+ const { done, value } = await poll();
84
+ if (done) return value;
85
+ if (Date.now() >= deadline) {
86
+ throw new CliError(`Timed out waiting for ${label} after ${Math.round(timeoutMs / 1000)}s.`);
87
+ }
88
+ await io.sleep(intervalMs);
89
+ }
90
+ }
91
+
73
92
  // Read a text body from --body, or --file (a path, or "-" for stdin). Returns
74
93
  // undefined if neither flag was given, so callers can decide whether that's an error.
75
94
  export function readBodyInput(flags) {
@@ -0,0 +1,99 @@
1
+ // `shredder` — create shredder endpoints and run content produced OUTSIDE Letterstory
2
+ // through the cross-provider shredder. A shred rewrites each sentence across multiple
3
+ // LLM providers to diversify, leaving structure (headings, lists, tables) untouched, and
4
+ // logs every call to telemetry.
5
+
6
+ import { CliError } from "../client.mjs";
7
+ import {
8
+ flagStr,
9
+ flagNum,
10
+ requireFlag,
11
+ requirePositional,
12
+ readBodyInput,
13
+ printResult,
14
+ compact,
15
+ ok,
16
+ } from "./shared.mjs";
17
+
18
+ export async function cmdShredder(ctx) {
19
+ const sub = ctx.positionals[0];
20
+ const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
21
+ switch (sub) {
22
+ case "list":
23
+ case "ls":
24
+ return shredderList(rest);
25
+ case "new":
26
+ case "create":
27
+ return shredderCreate(rest);
28
+ case "delete":
29
+ case "rm":
30
+ return shredderDelete(rest);
31
+ case "run":
32
+ case "shred":
33
+ return shredderRun(rest);
34
+ case "runs":
35
+ case "telemetry":
36
+ return shredderRuns(rest);
37
+ default:
38
+ throw new CliError(`Unknown shredder subcommand: ${sub ?? "(none)"}. Try: list, create, delete, run, runs`);
39
+ }
40
+ }
41
+
42
+ async function shredderList(ctx) {
43
+ const { client, flags, io } = ctx;
44
+ const result = await client.callTool("list_shredder_endpoints", {});
45
+ printResult(io, flags, result);
46
+ return 0;
47
+ }
48
+
49
+ async function shredderCreate(ctx) {
50
+ const { client, flags, io } = ctx;
51
+ const name = requireFlag(flags, "name");
52
+ const result = await client.callTool("create_shredder_endpoint", { name });
53
+ ok(ctx, `Created shredder endpoint "${name}".`);
54
+ printResult(io, flags, result);
55
+ return 0;
56
+ }
57
+
58
+ async function shredderDelete(ctx) {
59
+ const { client, positionals, flags, io } = ctx;
60
+ const id = requirePositional(positionals, 0, "endpoint-id");
61
+ if (!flags.yes) {
62
+ io.error(`Re-run with --yes to confirm: ${ctx.bin} shredder delete ${id} --yes`);
63
+ return 1;
64
+ }
65
+ const result = await client.callTool("delete_shredder_endpoint", { endpoint_id: id });
66
+ ok(ctx, "Shredder endpoint deleted.");
67
+ printResult(io, flags, result);
68
+ return 0;
69
+ }
70
+
71
+ async function shredderRun(ctx) {
72
+ const { client, positionals, flags, io } = ctx;
73
+ const id = requirePositional(positionals, 0, "endpoint-id");
74
+ const text = readBodyInput(flags);
75
+ if (text === undefined) {
76
+ throw new CliError("Provide the prose to shred with --body <text>, --file <path>, or --file - (stdin).");
77
+ }
78
+ const args = compact({
79
+ endpoint_id: id,
80
+ text,
81
+ coverage: flagNum(flags.coverage),
82
+ attempts_per_unit: flagNum(flags.attempts),
83
+ });
84
+ const result = await client.callTool("run_shred", args);
85
+ ok(ctx, "Shred complete.");
86
+ printResult(io, flags, result);
87
+ return 0;
88
+ }
89
+
90
+ async function shredderRuns(ctx) {
91
+ const { client, flags, io } = ctx;
92
+ const args = compact({
93
+ endpoint_id: flagStr(flags.endpoint),
94
+ limit: flagNum(flags.limit),
95
+ });
96
+ const result = await client.callTool("list_shred_runs", args);
97
+ printResult(io, flags, result);
98
+ return 0;
99
+ }
@@ -220,120 +220,11 @@ export async function cmdOnboarding(ctx) {
220
220
  return onboardingStatus(rest);
221
221
  case "step":
222
222
  return onboardingStep(rest);
223
- case "magic":
224
- return onboardingMagic(rest);
225
223
  default:
226
- throw new CliError(`Unknown onboarding subcommand: ${sub ?? "(none)"}. Try: status, step, magic`);
224
+ throw new CliError(`Unknown onboarding subcommand: ${sub ?? "(none)"}. Try: status, step`);
227
225
  }
228
226
  }
229
227
 
230
- // --- magic: enter a domain → strategy filled out -------------------------
231
- //
232
- // The G1 "magical onboarding": one command that composes the platform's
233
- // existing inference into a filled-out strategy — manifesto + stances +
234
- // identity (infer_org_strategy), optional post-history import from the
235
- // sitemap, keywords + topic clusters (infer_client_keywords), and detected
236
- // content series (propose_series → create_series → backfill_series). Every
237
- // step is idempotent and never clobbers hand-authored data, so re-running is
238
- // always safe. --dry-run previews series proposals instead of adopting them.
239
-
240
- async function onboardingMagic(ctx) {
241
- const { client, flags, io } = ctx;
242
- const dryRun = flags["dry-run"] === true;
243
- const summary = {};
244
-
245
- // 1. Company profile → the domain everything reads from.
246
- ok(ctx, "Reading company profile…");
247
- const company = await client.callTool("get_company_info", {});
248
- const domain = flagStr(flags.domain) ?? company.domain ?? undefined;
249
- summary.domain = domain ?? null;
250
-
251
- // 2. Strategy: manifesto, stances, identity — saved with never-clobber guards.
252
- ok(ctx, domain ? `Inferring strategy from ${domain}…` : "Inferring strategy…");
253
- const strategy = await client.callTool("infer_org_strategy", compact({ domain }));
254
- summary.strategy = strategy;
255
- if (strategy.status === "skipped") {
256
- ok(ctx, `Strategy skipped (${strategy.reason}): ${strategy.detail}`);
257
- } else {
258
- ok(ctx, strategy.status === "reused" ? "Strategy already in place — kept as-is." : "Strategy saved.");
259
- }
260
-
261
- // 3. Post history: optionally import the blog's existing posts from a sitemap
262
- // so clustering/series have an inventory to read.
263
- let inventory = await client.callTool("get_content_inventory", {});
264
- if (flagStr(flags.sitemap)) {
265
- const collectionId = flagStr(flags.collection) ?? (await resolveMainCollection(client));
266
- ok(ctx, "Importing existing posts from the sitemap (this can take a while)…");
267
- const imported = await client.callTool("import_sitemap", {
268
- collection_id: collectionId,
269
- sitemap_url: flagStr(flags.sitemap),
270
- });
271
- summary.sitemap_import = imported;
272
- inventory = await client.callTool("get_content_inventory", {});
273
- } else if ((inventory.total ?? 0) === 0) {
274
- ok(ctx, "No posts in the inventory yet — pass --sitemap <url> to import the blog's history.");
275
- }
276
- summary.inventory = inventory;
277
-
278
- // 4. Keywords + topic clusters over whatever inventory exists.
279
- if ((inventory.total ?? 0) > 0) {
280
- ok(ctx, "Inferring keywords and building topic clusters…");
281
- summary.keywords = await client.callTool("infer_client_keywords", {});
282
- }
283
-
284
- // 5. Series detection — adopt each grounded proposal (create + backfill),
285
- // or just show them under --dry-run.
286
- ok(ctx, "Detecting content series…");
287
- const proposed = await client.callTool("propose_series", compact({ collection_id: flagStr(flags.collection) }));
288
- const proposals = proposed.proposals ?? [];
289
- summary.series = [];
290
- for (const p of proposals) {
291
- if (dryRun) {
292
- summary.series.push({ name: p.name, coverage: p.coverage, adopted: false });
293
- continue;
294
- }
295
- const created = await client.callTool("create_series", {
296
- name: p.name,
297
- description: p.description,
298
- coverage: p.coverage,
299
- });
300
- const backfilled = await client.callTool("backfill_series", {
301
- series_id: created.id,
302
- collection_id: proposed.collection_id,
303
- });
304
- summary.series.push({ name: p.name, coverage: p.coverage, adopted: true, backfilled: backfilled.matched });
305
- ok(ctx, `Series "${p.name}" created — ${backfilled.matched} existing post(s) reconstructed into it.`);
306
- }
307
- if (proposals.length === 0) ok(ctx, "No series detected — this blog has no clearly repeating shapes.");
308
-
309
- printResult(io, flags, summary, (s) => {
310
- const lines = [];
311
- lines.push(`Strategy: ${s.strategy.status}${s.strategy.reason ? ` (${s.strategy.reason})` : ""}`);
312
- lines.push(`Inventory: ${s.inventory.total ?? 0} post(s)${s.inventory.lowInventory ? " (low)" : ""}`);
313
- if (s.keywords) {
314
- lines.push(
315
- `Clusters: ${s.keywords.assigned ?? 0} post(s) assigned, ${s.keywords.newClusters ?? 0} new cluster(s)`
316
- );
317
- }
318
- lines.push(
319
- s.series.length === 0
320
- ? "Series: none detected"
321
- : `Series: ${s.series.map((x) => `${x.name}${x.adopted ? "" : " (proposed)"}`).join("; ")}`
322
- );
323
- return lines.join("\n");
324
- });
325
- return 0;
326
- }
327
-
328
- // The org's main (non-phantom) collection — where history imports and
329
- // org-level series live. list_collections includes `kind` for exactly this.
330
- async function resolveMainCollection(client) {
331
- const result = await client.callTool("list_collections", {});
332
- const main = (result.items ?? []).find((c) => c.kind !== "phantom");
333
- if (!main) throw new CliError("No main collection found — create one first, or pass --collection <id>.");
334
- return main.collection_id;
335
- }
336
-
337
228
  async function onboardingStatus(ctx) {
338
229
  const { client, flags, io } = ctx;
339
230
  const result = await client.callTool("get_onboarding_status", {});
package/lib/commands.mjs CHANGED
@@ -18,6 +18,10 @@ export * from "./commands/series.mjs";
18
18
  export * from "./commands/insights.mjs";
19
19
  export * from "./commands/covers.mjs";
20
20
  export * from "./commands/research.mjs";
21
+ export * from "./commands/autopen.mjs";
21
22
  export * from "./commands/kernel.mjs";
22
23
  export * from "./commands/phantom-job.mjs";
24
+ export * from "./commands/onboard.mjs";
23
25
  export * from "./commands/seers.mjs";
26
+ export * from "./commands/shredder.mjs";
27
+ export * from "./commands/landings.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",