@letterstory/cli 0.2.2 → 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 +29 -0
- package/lib/cli.mjs +42 -2
- package/lib/commands/authors.mjs +165 -0
- package/lib/commands/collections.mjs +9 -1
- package/lib/commands/kernel.mjs +50 -0
- package/lib/commands/phantom-job.mjs +41 -0
- package/lib/commands/research.mjs +89 -0
- package/lib/commands/strategy.mjs +42 -1
- package/lib/commands.mjs +4 -0
- package/package.json +4 -4
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,15 +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,
|
|
30
|
+
cmdKernel,
|
|
31
|
+
cmdPhantomJob,
|
|
28
32
|
} from "./commands.mjs";
|
|
29
33
|
|
|
30
34
|
// Keep in sync with cli/package.json.
|
|
31
|
-
export const VERSION = "0.
|
|
35
|
+
export const VERSION = "0.4.0";
|
|
32
36
|
|
|
33
37
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
34
38
|
// can't accidentally swallow the id as --json's value.
|
|
@@ -148,10 +152,20 @@ Content:
|
|
|
148
152
|
Collections:
|
|
149
153
|
collections list List your collections
|
|
150
154
|
collections new --name <n> [--description <text>]
|
|
151
|
-
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]
|
|
152
156
|
collections delete <id> --yes
|
|
153
157
|
collections assign <article-id> <collection-id>
|
|
154
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
|
+
|
|
155
169
|
Flows:
|
|
156
170
|
flows list List available editorial flows
|
|
157
171
|
flows run <flow-id> <article-id> Start a flow run
|
|
@@ -170,9 +184,31 @@ Strategy & onboarding:
|
|
|
170
184
|
[--clear-topics] [--clear-stances] [--clear-avoid]
|
|
171
185
|
strategy competitors list | add <name> <domain>
|
|
172
186
|
strategy sitemap --collection <uuid> --url <sitemap_url> [--sub <url> …] [--pattern <glob>]
|
|
187
|
+
strategy topics list --collection <uuid> List the topic map's clusters/spokes
|
|
188
|
+
strategy topics set --collection <uuid> (--topic <topic-id> | --suggestion <suggestion-id>)
|
|
173
189
|
onboarding status Show the onboarding checklist
|
|
174
190
|
onboarding step [--current <step>] [--complete <step>] [--skip <step>] [--status <status>]
|
|
175
191
|
|
|
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):
|
|
202
|
+
kernel list List available writing kernels
|
|
203
|
+
kernel run --article <uuid> --kernel <uuid> [--brief <text>|--brief-file <path>]
|
|
204
|
+
Submit a run; takes 15-20 min
|
|
205
|
+
kernel status <kernel-job-id> Check a run's status
|
|
206
|
+
|
|
207
|
+
Phantom orchestrator:
|
|
208
|
+
phantom-job create --topic <text> --collection <uuid> [--kernel <uuid>]
|
|
209
|
+
Topic -> draft -> publish -> rebuild
|
|
210
|
+
phantom-job status <job-id> Check a job's stage
|
|
211
|
+
|
|
176
212
|
Insights:
|
|
177
213
|
insights site [--period 14d|30d|90d] [--collection <uuid>]
|
|
178
214
|
insights post <article-id> [--period 14d|30d|90d]
|
|
@@ -223,11 +259,15 @@ const CLIENT_COMMANDS = {
|
|
|
223
259
|
posts: cmdPosts,
|
|
224
260
|
published: cmdPublished,
|
|
225
261
|
collections: cmdCollections,
|
|
262
|
+
authors: cmdAuthors,
|
|
226
263
|
flows: cmdFlows,
|
|
227
264
|
connectors: cmdConnectors,
|
|
228
265
|
strategy: cmdStrategy,
|
|
229
266
|
onboarding: cmdOnboarding,
|
|
230
267
|
insights: cmdInsights,
|
|
268
|
+
research: cmdResearch,
|
|
269
|
+
kernel: cmdKernel,
|
|
270
|
+
"phantom-job": cmdPhantomJob,
|
|
231
271
|
};
|
|
232
272
|
|
|
233
273
|
// LETTERSTORY_POLL_INTERVAL_MS / LETTERSTORY_MAX_POLLS let an operator (or an
|
|
@@ -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,50 @@
|
|
|
1
|
+
// `kernel` — list writing kernels, submit a run against an article, and check a
|
|
2
|
+
// run's status. A run can take 15-20 minutes, so unlike `deploy create` this does
|
|
3
|
+
// not block waiting for it: `run` returns as soon as the job is queued and `status`
|
|
4
|
+
// checks on demand, same shape as `flows run` / `flows status`.
|
|
5
|
+
|
|
6
|
+
import { CliError } from "../client.mjs";
|
|
7
|
+
import { flagStr, requireFlag, requirePositional, printResult, compact, ok, readBodyInput } from "./shared.mjs";
|
|
8
|
+
|
|
9
|
+
export async function cmdKernel(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 kernelList(rest);
|
|
16
|
+
case "run":
|
|
17
|
+
return kernelRun(rest);
|
|
18
|
+
case "status":
|
|
19
|
+
return kernelStatus(rest);
|
|
20
|
+
default:
|
|
21
|
+
throw new CliError(`Unknown kernel subcommand: ${sub ?? "(none)"}. Try: list, run, status`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function kernelList(ctx) {
|
|
26
|
+
const { client, flags, io } = ctx;
|
|
27
|
+
const result = await client.callTool("list_kernels", {});
|
|
28
|
+
printResult(io, flags, result);
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function kernelRun(ctx) {
|
|
33
|
+
const { client, flags, io } = ctx;
|
|
34
|
+
const article = requireFlag(flags, "article");
|
|
35
|
+
const kernelId = requireFlag(flags, "kernel");
|
|
36
|
+
const brief = flagStr(flags.brief) ?? readBodyInput({ file: flagStr(flags["brief-file"]) });
|
|
37
|
+
const args = compact({ article_id: article, kernel_id: kernelId, brief });
|
|
38
|
+
const result = await client.callTool("submit_kernel_run", args);
|
|
39
|
+
ok(ctx, `Kernel run started. Check progress with: kernel status ${result?.kernel_job_id ?? "<kernel-job-id>"}`);
|
|
40
|
+
printResult(io, flags, result);
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function kernelStatus(ctx) {
|
|
45
|
+
const { client, positionals, flags, io } = ctx;
|
|
46
|
+
const jobId = requirePositional(positionals, 0, "kernel-job-id");
|
|
47
|
+
const result = await client.callTool("get_kernel_run_status", { kernel_job_id: jobId });
|
|
48
|
+
printResult(io, flags, result);
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// `phantom-job` — the topic-to-published-article orchestrator: kick off a job with
|
|
2
|
+
// a topic + collection, then check its stage (topic_set -> kernel_running ->
|
|
3
|
+
// publishing -> rebuilding -> done, or failed). The underlying kernel run alone can
|
|
4
|
+
// take 15-20 minutes, so `create` returns as soon as the job is queued rather than
|
|
5
|
+
// blocking — same shape as `flows run` / `flows status`, not `deploy create`'s
|
|
6
|
+
// wait-until-live loop.
|
|
7
|
+
|
|
8
|
+
import { CliError } from "../client.mjs";
|
|
9
|
+
import { flagStr, requireFlag, requirePositional, printResult, compact, ok } from "./shared.mjs";
|
|
10
|
+
|
|
11
|
+
export async function cmdPhantomJob(ctx) {
|
|
12
|
+
const sub = ctx.positionals[0];
|
|
13
|
+
const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
|
|
14
|
+
switch (sub) {
|
|
15
|
+
case "create":
|
|
16
|
+
return phantomJobCreate(rest);
|
|
17
|
+
case "status":
|
|
18
|
+
return phantomJobStatus(rest);
|
|
19
|
+
default:
|
|
20
|
+
throw new CliError(`Unknown phantom-job subcommand: ${sub ?? "(none)"}. Try: create, status`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function phantomJobCreate(ctx) {
|
|
25
|
+
const { client, flags, io } = ctx;
|
|
26
|
+
const topic = requireFlag(flags, "topic");
|
|
27
|
+
const collection = requireFlag(flags, "collection");
|
|
28
|
+
const args = compact({ topic, collection_id: collection, kernel_id: flagStr(flags.kernel) });
|
|
29
|
+
const result = await client.callTool("create_phantom_from_topic", args);
|
|
30
|
+
ok(ctx, `Phantom job started. Check progress with: phantom-job status ${result?.job_id ?? "<job-id>"}`);
|
|
31
|
+
printResult(io, flags, result);
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function phantomJobStatus(ctx) {
|
|
36
|
+
const { client, positionals, flags, io } = ctx;
|
|
37
|
+
const jobId = requirePositional(positionals, 0, "job-id");
|
|
38
|
+
const result = await client.callTool("get_phantom_job_status", { job_id: jobId });
|
|
39
|
+
printResult(io, flags, result);
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -27,9 +27,11 @@ export async function cmdStrategy(ctx) {
|
|
|
27
27
|
return strategyCompetitors(rest);
|
|
28
28
|
case "sitemap":
|
|
29
29
|
return strategySitemap(rest);
|
|
30
|
+
case "topics":
|
|
31
|
+
return strategyTopics(rest);
|
|
30
32
|
default:
|
|
31
33
|
throw new CliError(
|
|
32
|
-
`Unknown strategy subcommand: ${sub ?? "(none)"}. Try: company, positioning, competitors, sitemap`
|
|
34
|
+
`Unknown strategy subcommand: ${sub ?? "(none)"}. Try: company, positioning, competitors, sitemap, topics`
|
|
33
35
|
);
|
|
34
36
|
}
|
|
35
37
|
}
|
|
@@ -169,6 +171,45 @@ async function strategySitemap(ctx) {
|
|
|
169
171
|
return 0;
|
|
170
172
|
}
|
|
171
173
|
|
|
174
|
+
// -- topics ---------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
async function strategyTopics(ctx) {
|
|
177
|
+
const sub = ctx.positionals[0];
|
|
178
|
+
const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
|
|
179
|
+
switch (sub) {
|
|
180
|
+
case "list":
|
|
181
|
+
case "ls":
|
|
182
|
+
return topicsList(rest);
|
|
183
|
+
case "set":
|
|
184
|
+
return topicsSet(rest);
|
|
185
|
+
default:
|
|
186
|
+
throw new CliError(`Unknown strategy topics subcommand: ${sub ?? "(none)"}. Try: list, set`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function topicsList(ctx) {
|
|
191
|
+
const { client, flags, io } = ctx;
|
|
192
|
+
const collection = requireFlag(flags, "collection");
|
|
193
|
+
const result = await client.callTool("list_topics", { collection_id: collection });
|
|
194
|
+
printResult(io, flags, result);
|
|
195
|
+
return 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function topicsSet(ctx) {
|
|
199
|
+
const { client, flags, io } = ctx;
|
|
200
|
+
const collection = requireFlag(flags, "collection");
|
|
201
|
+
const topicId = flagStr(flags.topic);
|
|
202
|
+
const suggestionId = flagStr(flags.suggestion);
|
|
203
|
+
if ((topicId === undefined) === (suggestionId === undefined)) {
|
|
204
|
+
throw new CliError("Pass exactly one of --topic <topic-id> or --suggestion <suggestion-id>.");
|
|
205
|
+
}
|
|
206
|
+
const args = compact({ collection_id: collection, topic_id: topicId, suggestion_id: suggestionId });
|
|
207
|
+
const result = await client.callTool("set_topic", args);
|
|
208
|
+
ok(ctx, "Topic queued.");
|
|
209
|
+
printResult(io, flags, result);
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
172
213
|
// --- onboarding ---------------------------------------------------------
|
|
173
214
|
|
|
174
215
|
export async function cmdOnboarding(ctx) {
|
package/lib/commands.mjs
CHANGED
|
@@ -10,7 +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";
|
|
19
|
+
export * from "./commands/kernel.mjs";
|
|
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
|
+
"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/
|
|
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/
|
|
11
|
+
"url": "git+https://github.com/letterstory/letterstory.git",
|
|
12
12
|
"directory": "cli"
|
|
13
13
|
},
|
|
14
14
|
"bugs": {
|
|
15
|
-
"url": "https://github.com/letterstory/
|
|
15
|
+
"url": "https://github.com/letterstory/letterstory/issues"
|
|
16
16
|
},
|
|
17
17
|
"keywords": [
|
|
18
18
|
"letterstory",
|