@letterstory/cli 0.7.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 +17 -17
- package/lib/cli.mjs +53 -10
- package/lib/client.mjs +3 -2
- package/lib/commands/autopen.mjs +59 -0
- package/lib/commands/collections.mjs +45 -1
- package/lib/commands/connectors.mjs +6 -1
- package/lib/commands/discovery.mjs +40 -2
- package/lib/commands/landings.mjs +86 -0
- package/lib/commands/onboard.mjs +126 -0
- package/lib/commands/seers.mjs +16 -13
- package/lib/commands/series.mjs +1 -25
- package/lib/commands/shared.mjs +19 -0
- package/lib/commands/strategy.mjs +1 -110
- package/lib/commands.mjs +3 -0
- package/package.json +1 -1
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
|
-
--
|
|
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,23 +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,
|
|
36
38
|
cmdShredder,
|
|
39
|
+
cmdLandings,
|
|
37
40
|
} from "./commands.mjs";
|
|
38
41
|
|
|
39
42
|
// Keep in sync with cli/package.json.
|
|
40
|
-
export const VERSION = "0.
|
|
43
|
+
export const VERSION = "0.8.0";
|
|
41
44
|
|
|
42
45
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
43
46
|
// can't accidentally swallow the id as --json's value.
|
|
44
47
|
const BOOLEAN_FLAGS = new Set([
|
|
45
48
|
"json",
|
|
46
49
|
"yes",
|
|
50
|
+
"confirm-missing-target",
|
|
47
51
|
"no-wait",
|
|
52
|
+
"accept",
|
|
48
53
|
"help",
|
|
49
54
|
"version",
|
|
50
55
|
"quiet",
|
|
@@ -167,6 +172,13 @@ Collections:
|
|
|
167
172
|
collections update <id> [--name] [--description] [--cadence-target <n>] [--cadence-period week|month] [--cover-canvas-type-id <uuid>|none]
|
|
168
173
|
collections delete <id> --yes
|
|
169
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.
|
|
170
182
|
|
|
171
183
|
Authors (the per-collection author bank — recurring bylines):
|
|
172
184
|
authors list <collection-id> List the bank + byline distribution
|
|
@@ -188,6 +200,8 @@ Flows:
|
|
|
188
200
|
Connectors:
|
|
189
201
|
connectors list List external publish destinations
|
|
190
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
|
|
191
205
|
connectors status --connector <connector> --publish-id <id>
|
|
192
206
|
|
|
193
207
|
Strategy & onboarding:
|
|
@@ -200,12 +214,7 @@ Strategy & onboarding:
|
|
|
200
214
|
strategy topics set --collection <uuid> (--topic <topic-id> | --suggestion <suggestion-id>)
|
|
201
215
|
onboarding status Show the onboarding checklist
|
|
202
216
|
onboarding step [--current <step>] [--complete <step>] [--skip <step>] [--status <status>]
|
|
203
|
-
onboarding magic [--domain <domain>] [--sitemap <url>] [--collection <uuid>] [--dry-run]
|
|
204
|
-
Enter a domain -> strategy filled out:
|
|
205
|
-
manifesto+stances, optional history import,
|
|
206
|
-
clusters, detected series (see below)
|
|
207
217
|
series list The org's content series + coverage state
|
|
208
|
-
series propose [--collection <uuid>] Detect series existing posts already imply
|
|
209
218
|
series create --name <n> --description <d> [--coverage a,b,c] [--collection <uuid>] [--template <key>]
|
|
210
219
|
series backfill <series-id> [--collection <uuid>] Reconstruct a series from existing posts
|
|
211
220
|
|
|
@@ -218,6 +227,12 @@ Research agent (deep research -> outline written into the post):
|
|
|
218
227
|
Answer the --gate pause
|
|
219
228
|
research cancel --article <uuid> Cancel a run in flight
|
|
220
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
|
+
|
|
221
236
|
Cover images (which image model draws the post's cover):
|
|
222
237
|
covers models Image models connected right now
|
|
223
238
|
covers variants --article <uuid> [--model <id> …] [--ref-url <url> …]
|
|
@@ -238,6 +253,14 @@ Phantom orchestrator:
|
|
|
238
253
|
Topic -> draft -> publish -> rebuild
|
|
239
254
|
phantom-job status <job-id> Check a job's stage
|
|
240
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
|
+
|
|
241
264
|
Seers (event-driven signals -> drafts):
|
|
242
265
|
seers list List seers with recent activity
|
|
243
266
|
seers get <seer-id> One seer's full config + poll state
|
|
@@ -264,8 +287,14 @@ Seers (event-driven signals -> drafts):
|
|
|
264
287
|
Content templates (compose structures seers bind via config.template_key):
|
|
265
288
|
templates list Custom + built-in catalog
|
|
266
289
|
templates show <template-id>
|
|
267
|
-
templates create --name <n>
|
|
268
|
-
templates update <template-id> [--name] [--
|
|
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.
|
|
269
298
|
templates delete <template-id> --yes
|
|
270
299
|
|
|
271
300
|
Shredder (diversify content produced outside Letterstory; structure preserved):
|
|
@@ -276,6 +305,14 @@ Shredder (diversify content produced outside Letterstory; structure preserved):
|
|
|
276
305
|
Shred prose through an endpoint (logs telemetry)
|
|
277
306
|
shredder runs [--endpoint <id>] [--limit <n>] Recent shred calls (telemetry), newest first
|
|
278
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
|
+
|
|
279
316
|
Insights:
|
|
280
317
|
insights site [--period 14d|30d|90d] [--collection <uuid>]
|
|
281
318
|
insights post <article-id> [--period 14d|30d|90d]
|
|
@@ -283,7 +320,7 @@ Insights:
|
|
|
283
320
|
|
|
284
321
|
Anything else:
|
|
285
322
|
tools list List every tool this server exposes, by name
|
|
286
|
-
tools show <name> Show one tool's capability
|
|
323
|
+
tools show <name> Show one tool's capability, argument schema, and return shape
|
|
287
324
|
call <tool> [--args '<json>'] [--flag value …] Call any tool directly
|
|
288
325
|
tool <name> [--arg k=v …] [--json-args '<json>'] [--stdin]
|
|
289
326
|
Call any tool with schema-coerced arguments
|
|
@@ -334,12 +371,15 @@ const CLIENT_COMMANDS = {
|
|
|
334
371
|
series: cmdSeries,
|
|
335
372
|
insights: cmdInsights,
|
|
336
373
|
research: cmdResearch,
|
|
374
|
+
autopen: cmdAutoPen,
|
|
337
375
|
covers: cmdCovers,
|
|
338
376
|
kernel: cmdKernel,
|
|
339
377
|
"phantom-job": cmdPhantomJob,
|
|
378
|
+
"phantom-onboard": cmdOnboard,
|
|
340
379
|
seers: cmdSeers,
|
|
341
380
|
templates: cmdTemplates,
|
|
342
381
|
shredder: cmdShredder,
|
|
382
|
+
landings: cmdLandings,
|
|
343
383
|
};
|
|
344
384
|
|
|
345
385
|
// LETTERSTORY_POLL_INTERVAL_MS / LETTERSTORY_MAX_POLLS let an operator (or an
|
|
@@ -382,7 +422,10 @@ export async function run(argv, io = defaultIo()) {
|
|
|
382
422
|
return 0;
|
|
383
423
|
}
|
|
384
424
|
|
|
385
|
-
|
|
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);
|
|
386
429
|
if (!handler) {
|
|
387
430
|
io.error(`Unknown command: ${command}`);
|
|
388
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
|
|
91
|
-
// Lets `letterstory tools` work before
|
|
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
|
|
85
|
-
|
|
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
|
+
}
|
package/lib/commands/seers.mjs
CHANGED
|
@@ -625,18 +625,18 @@ async function templatesShow(ctx) {
|
|
|
625
625
|
return 0;
|
|
626
626
|
}
|
|
627
627
|
|
|
628
|
-
function
|
|
629
|
-
const inline = flagStr(flags.
|
|
630
|
-
const file = flagStr(flags["
|
|
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, "--
|
|
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(`--
|
|
637
|
+
throw new CliError(`--blocks is not valid JSON: ${err.message}`);
|
|
638
638
|
}
|
|
639
|
-
if (!Array.isArray(parsed)) throw new CliError("--
|
|
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
|
|
647
|
+
const blocks = templateBlocksFromFlags(flags);
|
|
648
648
|
if (!name) throw new CliError("Missing required --name");
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
|
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 || !
|
|
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
|
-
|
|
677
|
+
blocks: blocks ?? current.blocks,
|
|
675
678
|
});
|
|
676
679
|
ok(ctx, "Template updated.");
|
|
677
680
|
printResult(io, flags, result);
|
package/lib/commands/series.mjs
CHANGED
|
@@ -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
|
|
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
|
-
}
|
package/lib/commands/shared.mjs
CHANGED
|
@@ -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) {
|
|
@@ -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
|
|
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,7 +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";
|
|
24
26
|
export * from "./commands/shredder.mjs";
|
|
27
|
+
export * from "./commands/landings.mjs";
|