@letterstory/cli 0.3.0 → 0.4.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
@@ -207,6 +207,35 @@ letterstory onboarding status
207
207
  letterstory onboarding step --complete connect_domain
208
208
  ```
209
209
 
210
+ ## Research and writing kernels
211
+
212
+ Two different steps, in this order. The **research agent** reads the live web and writes a
213
+ sourced outline into the post; a **writing kernel** writes the draft from the brief it's
214
+ given and does no retrieval of its own. Both take minutes, so both are start-then-poll:
215
+
216
+ ```bash
217
+ # 1. Research the topic and write an outline into the post
218
+ letterstory research start --article <uuid> --topic "payroll solutions" \
219
+ --url https://example.com/a --must-include "Pricing" --depth fleshed_out
220
+ letterstory research status --article <uuid> # phases, sources, outline
221
+ letterstory research cancel --article <uuid>
222
+
223
+ # …with --gate, the run pauses for you to pick the angle:
224
+ letterstory research start --article <uuid> --gate
225
+ letterstory research status --article <uuid> # status: awaiting_direction + direction_options
226
+ letterstory research direction --article <uuid> --choice 1
227
+ letterstory research direction --article <uuid> --custom "Lead with the hidden costs"
228
+ letterstory research direction --article <uuid> --skip # let the agent decide
229
+
230
+ # 2. Have a kernel write the draft
231
+ letterstory kernel list
232
+ letterstory kernel run --article <uuid> --kernel <uuid> [--brief "…" | --brief-file brief.md]
233
+ letterstory kernel status <kernel-job-id>
234
+ ```
235
+
236
+ `phantom-job create --topic "…" --collection <uuid>` runs topic → draft → publish → rebuild
237
+ end to end, with `phantom-job status <job-id>` to follow it.
238
+
210
239
  ## Insights
211
240
 
212
241
  Search Console performance, network-wide, per post, or top posts:
package/lib/cli.mjs CHANGED
@@ -20,17 +20,19 @@ import {
20
20
  cmdPosts,
21
21
  cmdPublished,
22
22
  cmdCollections,
23
+ cmdAuthors,
23
24
  cmdFlows,
24
25
  cmdConnectors,
25
26
  cmdStrategy,
26
27
  cmdOnboarding,
27
28
  cmdInsights,
29
+ cmdResearch,
28
30
  cmdKernel,
29
31
  cmdPhantomJob,
30
32
  } from "./commands.mjs";
31
33
 
32
34
  // Keep in sync with cli/package.json.
33
- export const VERSION = "0.3.0";
35
+ export const VERSION = "0.4.0";
34
36
 
35
37
  // Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
36
38
  // can't accidentally swallow the id as --json's value.
@@ -150,10 +152,20 @@ Content:
150
152
  Collections:
151
153
  collections list List your collections
152
154
  collections new --name <n> [--description <text>]
153
- collections update <id> [--name] [--description] [--cadence-target <n>] [--cadence-period week|month]
155
+ collections update <id> [--name] [--description] [--cadence-target <n>] [--cadence-period week|month] [--cover-canvas-type-id <uuid>|none]
154
156
  collections delete <id> --yes
155
157
  collections assign <article-id> <collection-id>
156
158
 
159
+ Authors (the per-collection author bank — recurring bylines):
160
+ authors list <collection-id> List the bank + byline distribution
161
+ authors generate <collection-id> [--count <n>] Generate a believable masthead
162
+ authors add <collection-id> --name <n> [--bio <t>] [--role <t>] [--expertise a,b] [--started-at YYYY-MM-DD]
163
+ authors edit <collection-id> <author-id> [--name] [--bio] [--role] [--expertise a,b] [--started-at] [--active true|false]
164
+ authors retire <collection-id> <author-id> Stop assigning new posts to them
165
+ authors delete <collection-id> <author-id> --yes
166
+ authors assign <collection-id> (--all | --articles id,id) [--author <id>] [--no-rebuild]
167
+ Batch-swap bylines (auto-distribute, or --author to pin one)
168
+
157
169
  Flows:
158
170
  flows list List available editorial flows
159
171
  flows run <flow-id> <article-id> Start a flow run
@@ -177,7 +189,16 @@ Strategy & onboarding:
177
189
  onboarding status Show the onboarding checklist
178
190
  onboarding step [--current <step>] [--complete <step>] [--skip <step>] [--status <status>]
179
191
 
180
- Writing kernels:
192
+ Research agent (deep research -> outline written into the post):
193
+ research start --article <uuid> [--topic <text>] [--url <url> …] [--must-include <text> …]
194
+ [--depth barebones|fleshed_out] [--gate]
195
+ Start a run; takes minutes
196
+ research status --article <uuid> Phases, sources, outline
197
+ research direction --article <uuid> (--choice <n> | --custom <text> | --skip)
198
+ Answer the --gate pause
199
+ research cancel --article <uuid> Cancel a run in flight
200
+
201
+ Writing kernels (kernels write the draft; they do not research — run research first):
181
202
  kernel list List available writing kernels
182
203
  kernel run --article <uuid> --kernel <uuid> [--brief <text>|--brief-file <path>]
183
204
  Submit a run; takes 15-20 min
@@ -238,11 +259,13 @@ const CLIENT_COMMANDS = {
238
259
  posts: cmdPosts,
239
260
  published: cmdPublished,
240
261
  collections: cmdCollections,
262
+ authors: cmdAuthors,
241
263
  flows: cmdFlows,
242
264
  connectors: cmdConnectors,
243
265
  strategy: cmdStrategy,
244
266
  onboarding: cmdOnboarding,
245
267
  insights: cmdInsights,
268
+ research: cmdResearch,
246
269
  kernel: cmdKernel,
247
270
  "phantom-job": cmdPhantomJob,
248
271
  };
@@ -0,0 +1,165 @@
1
+ // `authors` — manage a collection's author bank: the recurring bylines its posts
2
+ // publish under, instead of a fresh throwaway name per article. The editable-at-
3
+ // scale escape hatch behind the in-app Authors panel.
4
+
5
+ import { CliError } from "../client.mjs";
6
+ import {
7
+ flagStr,
8
+ flagNum,
9
+ flagBool,
10
+ flagList,
11
+ requireFlag,
12
+ requirePositional,
13
+ printResult,
14
+ compact,
15
+ ok,
16
+ } from "./shared.mjs";
17
+
18
+ export async function cmdAuthors(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 authorsList(rest);
25
+ case "generate":
26
+ return authorsGenerate(rest);
27
+ case "add":
28
+ case "create":
29
+ return authorsAdd(rest);
30
+ case "edit":
31
+ case "update":
32
+ return authorsEdit(rest);
33
+ case "retire":
34
+ return authorsSetActive(rest, false);
35
+ case "activate":
36
+ return authorsSetActive(rest, true);
37
+ case "delete":
38
+ case "rm":
39
+ return authorsDelete(rest);
40
+ case "assign":
41
+ case "swap":
42
+ return authorsAssign(rest);
43
+ default:
44
+ throw new CliError(
45
+ `Unknown authors subcommand: ${sub ?? "(none)"}. Try: list, generate, add, edit, retire, activate, delete, assign`
46
+ );
47
+ }
48
+ }
49
+
50
+ /** Positional 0 is the collection id for every subcommand except delete/edit (which take an author id too). */
51
+ function collectionId(ctx) {
52
+ return requirePositional(ctx.positionals, 0, "collection-id");
53
+ }
54
+
55
+ async function authorsList(ctx) {
56
+ const { client, flags, io } = ctx;
57
+ const result = await client.callTool("list_collection_authors", { collection_id: collectionId(ctx) });
58
+ printResult(io, flags, result);
59
+ return 0;
60
+ }
61
+
62
+ async function authorsGenerate(ctx) {
63
+ const { client, flags, io } = ctx;
64
+ const args = compact({ collection_id: collectionId(ctx), count: flagNum(flags.count) });
65
+ const result = await client.callTool("generate_collection_authors", args);
66
+ ok(ctx, "Generated authors.");
67
+ printResult(io, flags, result);
68
+ return 0;
69
+ }
70
+
71
+ async function authorsAdd(ctx) {
72
+ const { client, flags, io } = ctx;
73
+ const cid = collectionId(ctx);
74
+ const name = requireFlag(flags, "name");
75
+ const args = compact({
76
+ collection_id: cid,
77
+ name,
78
+ bio: flagStr(flags.bio),
79
+ role: flagStr(flags.role),
80
+ expertise: flagList(flags.expertise),
81
+ started_at: flagStr(flags["started-at"]),
82
+ });
83
+ const result = await client.callTool("upsert_collection_author", args);
84
+ ok(ctx, `Added author "${name}".`);
85
+ printResult(io, flags, result);
86
+ return 0;
87
+ }
88
+
89
+ async function authorsEdit(ctx) {
90
+ const { client, positionals, flags, io } = ctx;
91
+ const cid = requirePositional(positionals, 0, "collection-id");
92
+ const authorId = requirePositional(positionals, 1, "author-id");
93
+ const patch = compact({
94
+ collection_id: cid,
95
+ author_id: authorId,
96
+ name: flagStr(flags.name),
97
+ bio: flagStr(flags.bio),
98
+ role: flagStr(flags.role),
99
+ expertise: flagList(flags.expertise),
100
+ started_at: flagStr(flags["started-at"]),
101
+ active: flagBool(flags.active),
102
+ });
103
+ if (Object.keys(patch).length <= 2) {
104
+ throw new CliError(
105
+ "Nothing to edit — pass at least one of --name/--bio/--role/--expertise/--started-at/--active."
106
+ );
107
+ }
108
+ const result = await client.callTool("upsert_collection_author", patch);
109
+ ok(ctx, "Updated author.");
110
+ printResult(io, flags, result);
111
+ return 0;
112
+ }
113
+
114
+ async function authorsSetActive(ctx, active) {
115
+ const { client, positionals, flags, io } = ctx;
116
+ const cid = requirePositional(positionals, 0, "collection-id");
117
+ const authorId = requirePositional(positionals, 1, "author-id");
118
+ const result = await client.callTool("upsert_collection_author", {
119
+ collection_id: cid,
120
+ author_id: authorId,
121
+ active,
122
+ });
123
+ ok(ctx, active ? "Author activated." : "Author retired.");
124
+ printResult(io, flags, result);
125
+ return 0;
126
+ }
127
+
128
+ async function authorsDelete(ctx) {
129
+ const { client, positionals, flags, io } = ctx;
130
+ const cid = requirePositional(positionals, 0, "collection-id");
131
+ const authorId = requirePositional(positionals, 1, "author-id");
132
+ if (!flags.yes) {
133
+ io.error(`Re-run with --yes to confirm: ${ctx.bin} authors delete ${cid} ${authorId} --yes`);
134
+ return 1;
135
+ }
136
+ const result = await client.callTool("delete_collection_author", { collection_id: cid, author_id: authorId });
137
+ ok(ctx, "Author deleted.");
138
+ printResult(io, flags, result);
139
+ return 0;
140
+ }
141
+
142
+ async function authorsAssign(ctx) {
143
+ const { client, flags, io } = ctx;
144
+ const cid = collectionId(ctx);
145
+ // --author <id> pins one author; omit it to auto-distribute by topic.
146
+ // --all reassigns every post; otherwise pass --articles <id,id,...>.
147
+ const authorId = flagStr(flags.author);
148
+ const articleIds = flagList(flags.articles);
149
+ const all = flagBool(flags.all) === true;
150
+ if (!all && (!articleIds || articleIds.length === 0)) {
151
+ throw new CliError("Provide --all to reassign every post, or --articles <id,id,...> for specific posts.");
152
+ }
153
+ const args = compact({
154
+ collection_id: cid,
155
+ mode: authorId ? "set" : "auto",
156
+ author_id: authorId,
157
+ article_ids: all ? undefined : articleIds,
158
+ all: all ? true : undefined,
159
+ rebuild: flags["no-rebuild"] ? false : undefined,
160
+ });
161
+ const result = await client.callTool("assign_collection_authors", args);
162
+ ok(ctx, authorId ? "Reassigned bylines to the chosen author." : "Auto-distributed bylines across active authors.");
163
+ printResult(io, flags, result);
164
+ return 0;
165
+ }
@@ -48,6 +48,7 @@ async function collectionsNew(ctx) {
48
48
  async function collectionsUpdate(ctx) {
49
49
  const { client, positionals, flags, io } = ctx;
50
50
  const id = requirePositional(positionals, 0, "collection-id");
51
+ const coverCanvasTypeId = flagStr(flags["cover-canvas-type-id"]);
51
52
  const patch = compact({
52
53
  collection_id: id,
53
54
  name: flagStr(flags.name),
@@ -55,9 +56,16 @@ async function collectionsUpdate(ctx) {
55
56
  cadence_target: flagNum(flags["cadence-target"]),
56
57
  cadence_period: flagStr(flags["cadence-period"]),
57
58
  });
59
+ if (coverCanvasTypeId !== undefined) {
60
+ // compact() drops undefined, not null — set it after so "none"/"off" can clear
61
+ // the field instead of being dropped. Same convention as `webhook set --url none`.
62
+ patch.cover_canvas_type_id = ["none", "off", ""].includes(coverCanvasTypeId.toLowerCase())
63
+ ? null
64
+ : coverCanvasTypeId;
65
+ }
58
66
  if (Object.keys(patch).length <= 1) {
59
67
  throw new CliError(
60
- "Nothing to update — pass at least one of --name/--description/--cadence-target/--cadence-period."
68
+ "Nothing to update — pass at least one of --name/--description/--cadence-target/--cadence-period/--cover-canvas-type-id."
61
69
  );
62
70
  }
63
71
  const result = await client.callTool("update_collection", patch);
@@ -0,0 +1,89 @@
1
+ // `research` — the deep-research agent: read supplied URLs, search the live web, and
2
+ // write a sourced outline into a post. Same shape as `kernel`: a run takes minutes, so
3
+ // `start` returns as soon as the run is queued and `status` checks on demand. Writing
4
+ // kernels do NOT research — `research start` then `kernel run` is the full pipeline.
5
+
6
+ import { CliError } from "../client.mjs";
7
+ import { flagStr, flagNum, flagBool, flagList, requireFlag, printResult, compact, ok } from "./shared.mjs";
8
+
9
+ export async function cmdResearch(ctx) {
10
+ const sub = ctx.positionals[0];
11
+ const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
12
+ switch (sub) {
13
+ case "start":
14
+ case "run":
15
+ return researchStart(rest);
16
+ case "status":
17
+ return researchStatus(rest);
18
+ case "direction":
19
+ return researchDirection(rest);
20
+ case "cancel":
21
+ return researchCancel(rest);
22
+ default:
23
+ throw new CliError(
24
+ `Unknown research subcommand: ${sub ?? "(none)"}. Try: start, status, direction, cancel`
25
+ );
26
+ }
27
+ }
28
+
29
+ async function researchStart(ctx) {
30
+ const { client, flags, io } = ctx;
31
+ const article = requireFlag(flags, "article");
32
+ const depth = flagStr(flags.depth);
33
+ if (depth !== undefined && depth !== "barebones" && depth !== "fleshed_out") {
34
+ throw new CliError(`--depth must be barebones or fleshed_out, got "${depth}"`);
35
+ }
36
+ const sourceUrls = flagList(flags.url);
37
+ const mustInclude = flagList(flags["must-include"]);
38
+ const args = compact({
39
+ article_id: article,
40
+ topic: flagStr(flags.topic),
41
+ source_urls: sourceUrls.length ? sourceUrls : undefined,
42
+ must_include: mustInclude.length ? mustInclude : undefined,
43
+ depth,
44
+ direction_gate: flagBool(flags.gate) ? true : undefined,
45
+ });
46
+ const result = await client.callTool("start_research", args);
47
+ ok(ctx, `Research started. Check progress with: research status --article ${article}`);
48
+ printResult(io, flags, result);
49
+ return 0;
50
+ }
51
+
52
+ async function researchStatus(ctx) {
53
+ const { client, flags, io } = ctx;
54
+ const article = requireFlag(flags, "article");
55
+ const result = await client.callTool("get_research_status", { article_id: article });
56
+ printResult(io, flags, result);
57
+ return 0;
58
+ }
59
+
60
+ async function researchDirection(ctx) {
61
+ const { client, flags, io } = ctx;
62
+ const article = requireFlag(flags, "article");
63
+ const choice = flagNum(flags.choice);
64
+ const custom = flagStr(flags.custom);
65
+ const skip = flagBool(flags.skip);
66
+ const given = [choice !== undefined, custom !== undefined, skip].filter(Boolean).length;
67
+ if (given !== 1) {
68
+ throw new CliError("Provide exactly one of --choice <n>, --custom <text>, --skip");
69
+ }
70
+ const args = compact({
71
+ article_id: article,
72
+ choice,
73
+ custom,
74
+ skip: skip ? true : undefined,
75
+ });
76
+ const result = await client.callTool("submit_research_direction", args);
77
+ ok(ctx, "Direction recorded; the run resumed into synthesis.");
78
+ printResult(io, flags, result);
79
+ return 0;
80
+ }
81
+
82
+ async function researchCancel(ctx) {
83
+ const { client, flags, io } = ctx;
84
+ const article = requireFlag(flags, "article");
85
+ const result = await client.callTool("cancel_research", { article_id: article });
86
+ ok(ctx, result?.cancelled ? "Research run cancelled." : "No research run was in flight.");
87
+ printResult(io, flags, result);
88
+ return 0;
89
+ }
package/lib/commands.mjs CHANGED
@@ -10,9 +10,11 @@ export * from "./commands/deploy.mjs";
10
10
  export * from "./commands/mcp.mjs";
11
11
  export * from "./commands/posts.mjs";
12
12
  export * from "./commands/collections.mjs";
13
+ export * from "./commands/authors.mjs";
13
14
  export * from "./commands/flows.mjs";
14
15
  export * from "./commands/connectors.mjs";
15
16
  export * from "./commands/strategy.mjs";
16
17
  export * from "./commands/insights.mjs";
18
+ export * from "./commands/research.mjs";
17
19
  export * from "./commands/kernel.mjs";
18
20
  export * from "./commands/phantom-job.mjs";
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
7
7
  "author": "Letterstory",
8
- "homepage": "https://github.com/letterstory/Letterbrace/tree/main/cli#readme",
8
+ "homepage": "https://github.com/letterstory/letterstory/tree/main/cli#readme",
9
9
  "repository": {
10
10
  "type": "git",
11
- "url": "git+https://github.com/letterstory/Letterbrace.git",
11
+ "url": "git+https://github.com/letterstory/letterstory.git",
12
12
  "directory": "cli"
13
13
  },
14
14
  "bugs": {
15
- "url": "https://github.com/letterstory/Letterbrace/issues"
15
+ "url": "https://github.com/letterstory/letterstory/issues"
16
16
  },
17
17
  "keywords": [
18
18
  "letterstory",