@letterstory/cli 0.18.0 → 0.20.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/lib/cli.mjs +25 -6
- package/lib/commands/collections.mjs +78 -52
- package/lib/commands/connectors.mjs +69 -51
- package/lib/commands/onboard.mjs +141 -78
- package/lib/commands/shared.mjs +132 -0
- package/lib/commands/shredder.mjs +3 -0
- package/lib/commands/strategy.mjs +79 -54
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -44,7 +44,7 @@ import {
|
|
|
44
44
|
} from "./commands.mjs";
|
|
45
45
|
|
|
46
46
|
// Keep in sync with cli/package.json.
|
|
47
|
-
export const VERSION = "0.
|
|
47
|
+
export const VERSION = "0.20.0";
|
|
48
48
|
|
|
49
49
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
50
50
|
// can't accidentally swallow the id as --json's value.
|
|
@@ -85,6 +85,13 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
85
85
|
"onboard",
|
|
86
86
|
"test",
|
|
87
87
|
"real",
|
|
88
|
+
// `phantom-onboard collection --enable-planner`, `strategy sitemap --posts-only`, and the
|
|
89
|
+
// `strategy positioning set --clear-*` switches: value-less, and each sits next to a value.
|
|
90
|
+
"enable-planner",
|
|
91
|
+
"posts-only",
|
|
92
|
+
"clear-topics",
|
|
93
|
+
"clear-stances",
|
|
94
|
+
"clear-avoid",
|
|
88
95
|
]);
|
|
89
96
|
|
|
90
97
|
// Tiny argv parser: `--flag value`, `--flag=value`, boolean `--flag`, and positionals.
|
|
@@ -194,7 +201,8 @@ Reviewers:
|
|
|
194
201
|
Collections:
|
|
195
202
|
collections list List your collections
|
|
196
203
|
collections new --name <n> [--description <text>]
|
|
197
|
-
collections update <id> [--name] [--description] [--cadence-target <n>] [--cadence-period week|month]
|
|
204
|
+
collections update <id> [--name] [--description] [--cadence-target <n>] [--cadence-period week|month]
|
|
205
|
+
[--cover-canvas-type-id <uuid>|none] [--seo-market <cc>]
|
|
198
206
|
collections delete <id> --yes
|
|
199
207
|
collections assign <article-id> <collection-id>
|
|
200
208
|
collections autopen <id> [--template <key>|none] [--kernel-family <name>|org]
|
|
@@ -245,11 +253,11 @@ Connectors:
|
|
|
245
253
|
Wire a collection to a CMS + field mapping
|
|
246
254
|
|
|
247
255
|
Strategy & onboarding:
|
|
248
|
-
strategy company get | set [--name] [--domain] [--manifesto <text>|--manifesto-file <path>]
|
|
256
|
+
strategy company get | set [--name] [--domain] [--market <cc>] [--manifesto <text>|--manifesto-file <path>]
|
|
249
257
|
strategy positioning get | set [--topic <t> …] [--stance <s> …] [--avoid <t> …]
|
|
250
258
|
[--clear-topics] [--clear-stances] [--clear-avoid]
|
|
251
259
|
strategy competitors list | add <name> <domain>
|
|
252
|
-
strategy sitemap --collection <uuid> --url <sitemap_url> [--sub <url> …] [--pattern <glob>]
|
|
260
|
+
strategy sitemap --collection <uuid> --url <sitemap_url> [--sub <url> …] [--pattern <glob>] [--posts-only]
|
|
253
261
|
strategy topics list --collection <uuid> List the topic map's clusters/spokes
|
|
254
262
|
strategy topics set --collection <uuid> (--topic <topic-id> | --suggestion <suggestion-id>)
|
|
255
263
|
suggestions list <collection_id> List pending strategy suggestions + their auto-apply countdown
|
|
@@ -355,7 +363,8 @@ Propose lane (the Fleet wizard's Plan and Name screens; persists nothing, spends
|
|
|
355
363
|
|
|
356
364
|
Collection onboarding (your OWN blog, on your org's domain — no provisioning):
|
|
357
365
|
${bin === "phantom" ? "onboard" : "phantom-onboard"} collection [--collection <uuid>] [--name "<collection name>"]
|
|
358
|
-
[--about "the direction"] [--sitemap <url>] [--cms <provider>]
|
|
366
|
+
[--about "the direction"] [--sitemap <url>] [--cms <provider>]
|
|
367
|
+
[--enable-planner [--cadence <n/week>] [--approval manual|review_window|autopilot]] [--watch]
|
|
359
368
|
Import the existing blog from its sitemap
|
|
360
369
|
(auto-discovered from the org's domain unless
|
|
361
370
|
--sitemap pins one) OR from a connected CMS with
|
|
@@ -363,7 +372,12 @@ Collection onboarding (your OWN blog, on your org's domain — no provisioning):
|
|
|
363
372
|
derive content areas + series, build the topic
|
|
364
373
|
map, bind SEO demand, and stock Headlines & Queue.
|
|
365
374
|
Omit --collection to create a fresh one (--name,
|
|
366
|
-
default Blog). The Planner stays OFF
|
|
375
|
+
default Blog). The Planner stays OFF unless you
|
|
376
|
+
pass --enable-planner, which turns it on at the
|
|
377
|
+
chosen cadence and approval and fills the queue
|
|
378
|
+
(same activation as the app's Guided review step;
|
|
379
|
+
needs a user-scoped key). --approval defaults to
|
|
380
|
+
manual: nothing publishes unreviewed.
|
|
367
381
|
${bin === "phantom" ? "onboard" : "phantom-onboard"} sync-sitemap --collection <uuid>
|
|
368
382
|
Re-check an already-onboarded blog's sitemap
|
|
369
383
|
right now instead of waiting for its cadence
|
|
@@ -453,6 +467,11 @@ Shredder (diversify content produced outside Letterstory; structure preserved):
|
|
|
453
467
|
standard (default) keeps the piece's own terminology;
|
|
454
468
|
high rewrites more and may reword recurring terms;
|
|
455
469
|
max rewrites hardest — expect vocabulary drift
|
|
470
|
+
[--apply-rules]
|
|
471
|
+
hold every rewrite to the publication's writing rules —
|
|
472
|
+
the shred runs last, so it is the one stage that can put
|
|
473
|
+
back what the rules remove; costs attempts, and a sentence
|
|
474
|
+
with no clean rewrite keeps its original
|
|
456
475
|
[--no-wait] queue it and print the job id, don't follow it
|
|
457
476
|
[--sync] wait inside one request (only for a few
|
|
458
477
|
sentences — anything longer is cut off)
|
|
@@ -2,7 +2,71 @@
|
|
|
2
2
|
// Ported from phantomstory-cli's collections.ts.
|
|
3
3
|
|
|
4
4
|
import { CliError } from "../client.mjs";
|
|
5
|
-
import { flagStr,
|
|
5
|
+
import { flagStr, printResult, ok, argsFromFlags } from "./shared.mjs";
|
|
6
|
+
|
|
7
|
+
// `--flag none|off|""` clears a nullable field; the server takes null for that.
|
|
8
|
+
const clearable = (flag) => ({
|
|
9
|
+
value: (flags) => {
|
|
10
|
+
const v = flagStr(flags[flag]);
|
|
11
|
+
if (v === undefined) return undefined;
|
|
12
|
+
return ["none", "off", ""].includes(v.toLowerCase()) ? null : v;
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// A `--flag false` boolean (the value is the string "false"; anything else is true).
|
|
17
|
+
const stringBool = (flag) => ({
|
|
18
|
+
value: (flags) => (flags[flag] === undefined ? undefined : flags[flag] !== "false"),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// How every tool this module calls gets its arguments — see `argsFromFlags` in shared.mjs.
|
|
22
|
+
// `scripts/parity-check.ts` compares these keys against each tool's published input schema.
|
|
23
|
+
export const ARGS = {
|
|
24
|
+
list_collections: {},
|
|
25
|
+
create_collection: {
|
|
26
|
+
name: { flag: "name", required: true },
|
|
27
|
+
description: "description",
|
|
28
|
+
},
|
|
29
|
+
update_collection: {
|
|
30
|
+
collection_id: { positional: 0, label: "collection-id", required: true },
|
|
31
|
+
name: "name",
|
|
32
|
+
description: "description",
|
|
33
|
+
cadence_target: { flag: "cadence-target", type: "num" },
|
|
34
|
+
cadence_period: { flag: "cadence-period", enum: ["week", "month"] },
|
|
35
|
+
cover_canvas_type_id: clearable("cover-canvas-type-id"),
|
|
36
|
+
seo_market: "seo-market",
|
|
37
|
+
},
|
|
38
|
+
delete_collection: {
|
|
39
|
+
collection_id: { positional: 0, label: "collection-id", required: true },
|
|
40
|
+
},
|
|
41
|
+
assign_article_to_collection: {
|
|
42
|
+
article_id: { positional: 0, label: "article-id", required: true },
|
|
43
|
+
collection_id: { positional: 1, label: "collection-id", required: true },
|
|
44
|
+
},
|
|
45
|
+
// `collections autopen <id>` — a PATCH: only the flags given are sent.
|
|
46
|
+
update_auto_pen_config: {
|
|
47
|
+
collection_id: { positional: 0, label: "collection-id", required: true },
|
|
48
|
+
// `--template none` clears the binding; the server takes null for that.
|
|
49
|
+
template_key: clearable("template"),
|
|
50
|
+
outline: {
|
|
51
|
+
object: {
|
|
52
|
+
depth: "outline-depth",
|
|
53
|
+
direction_gate: stringBool("direction-gate"),
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
kernel: {
|
|
57
|
+
object: {
|
|
58
|
+
family: {
|
|
59
|
+
value: (flags) => {
|
|
60
|
+
const v = flagStr(flags["kernel-family"]);
|
|
61
|
+
return v === undefined ? undefined : v === "org" ? null : v;
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
mention: { object: { degree: "mention" } },
|
|
67
|
+
auto_start_on_curate: stringBool("auto-start"),
|
|
68
|
+
},
|
|
69
|
+
};
|
|
6
70
|
|
|
7
71
|
export async function cmdCollections(ctx) {
|
|
8
72
|
const sub = ctx.positionals[0];
|
|
@@ -39,35 +103,19 @@ async function collectionsList(ctx) {
|
|
|
39
103
|
|
|
40
104
|
async function collectionsNew(ctx) {
|
|
41
105
|
const { client, flags, io } = ctx;
|
|
42
|
-
const
|
|
43
|
-
const args = compact({ name, description: flagStr(flags.description) });
|
|
106
|
+
const args = argsFromFlags(ARGS.create_collection, flags);
|
|
44
107
|
const result = await client.callTool("create_collection", args);
|
|
45
|
-
ok(ctx, `Created collection "${name}".`);
|
|
108
|
+
ok(ctx, `Created collection "${args.name}".`);
|
|
46
109
|
printResult(io, flags, result);
|
|
47
110
|
return 0;
|
|
48
111
|
}
|
|
49
112
|
|
|
50
113
|
async function collectionsUpdate(ctx) {
|
|
51
114
|
const { client, positionals, flags, io } = ctx;
|
|
52
|
-
const
|
|
53
|
-
const coverCanvasTypeId = flagStr(flags["cover-canvas-type-id"]);
|
|
54
|
-
const patch = compact({
|
|
55
|
-
collection_id: id,
|
|
56
|
-
name: flagStr(flags.name),
|
|
57
|
-
description: flagStr(flags.description),
|
|
58
|
-
cadence_target: flagNum(flags["cadence-target"]),
|
|
59
|
-
cadence_period: flagStr(flags["cadence-period"]),
|
|
60
|
-
});
|
|
61
|
-
if (coverCanvasTypeId !== undefined) {
|
|
62
|
-
// compact() drops undefined, not null — set it after so "none"/"off" can clear
|
|
63
|
-
// the field instead of being dropped. Same convention as `webhook set --url none`.
|
|
64
|
-
patch.cover_canvas_type_id = ["none", "off", ""].includes(coverCanvasTypeId.toLowerCase())
|
|
65
|
-
? null
|
|
66
|
-
: coverCanvasTypeId;
|
|
67
|
-
}
|
|
115
|
+
const patch = argsFromFlags(ARGS.update_collection, flags, positionals);
|
|
68
116
|
if (Object.keys(patch).length <= 1) {
|
|
69
117
|
throw new CliError(
|
|
70
|
-
"Nothing to update — pass at least one of --name/--description/--cadence-target/--cadence-period/--cover-canvas-type-id."
|
|
118
|
+
"Nothing to update — pass at least one of --name/--description/--cadence-target/--cadence-period/--cover-canvas-type-id/--seo-market."
|
|
71
119
|
);
|
|
72
120
|
}
|
|
73
121
|
const result = await client.callTool("update_collection", patch);
|
|
@@ -78,12 +126,12 @@ async function collectionsUpdate(ctx) {
|
|
|
78
126
|
|
|
79
127
|
async function collectionsDelete(ctx) {
|
|
80
128
|
const { client, positionals, flags, io } = ctx;
|
|
81
|
-
const
|
|
129
|
+
const args = argsFromFlags(ARGS.delete_collection, flags, positionals);
|
|
82
130
|
if (!flags.yes) {
|
|
83
|
-
io.error(`Re-run with --yes to confirm: ${ctx.bin} collections delete ${
|
|
131
|
+
io.error(`Re-run with --yes to confirm: ${ctx.bin} collections delete ${args.collection_id} --yes`);
|
|
84
132
|
return 1;
|
|
85
133
|
}
|
|
86
|
-
const result = await client.callTool("delete_collection",
|
|
134
|
+
const result = await client.callTool("delete_collection", args);
|
|
87
135
|
ok(ctx, "Collection deleted.");
|
|
88
136
|
printResult(io, flags, result);
|
|
89
137
|
return 0;
|
|
@@ -91,12 +139,10 @@ async function collectionsDelete(ctx) {
|
|
|
91
139
|
|
|
92
140
|
async function collectionsAssign(ctx) {
|
|
93
141
|
const { client, positionals, flags, io } = ctx;
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
collection_id: collectionId,
|
|
99
|
-
});
|
|
142
|
+
const result = await client.callTool(
|
|
143
|
+
"assign_article_to_collection",
|
|
144
|
+
argsFromFlags(ARGS.assign_article_to_collection, flags, positionals)
|
|
145
|
+
);
|
|
100
146
|
ok(ctx, "Article assigned.");
|
|
101
147
|
printResult(io, flags, result);
|
|
102
148
|
return 0;
|
|
@@ -111,27 +157,7 @@ async function collectionsAssign(ctx) {
|
|
|
111
157
|
*/
|
|
112
158
|
async function collectionsAutoPen(ctx) {
|
|
113
159
|
const { client, positionals, flags, io } = ctx;
|
|
114
|
-
const
|
|
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
|
-
|
|
160
|
+
const patch = argsFromFlags(ARGS.update_auto_pen_config, flags, positionals);
|
|
135
161
|
if (Object.keys(patch).length === 1) {
|
|
136
162
|
throw new CliError(
|
|
137
163
|
"Nothing to change. Pass at least one of --template, --kernel-family, --mention, --outline-depth, --direction-gate, --auto-start."
|
|
@@ -139,7 +165,7 @@ async function collectionsAutoPen(ctx) {
|
|
|
139
165
|
}
|
|
140
166
|
|
|
141
167
|
const result = await client.callTool("update_auto_pen_config", patch);
|
|
142
|
-
ok(ctx, `Auto-Pen settings updated for ${
|
|
168
|
+
ok(ctx, `Auto-Pen settings updated for ${patch.collection_id}.`);
|
|
143
169
|
printResult(io, flags, result);
|
|
144
170
|
return 0;
|
|
145
171
|
}
|
|
@@ -3,7 +3,62 @@
|
|
|
3
3
|
// phantomstory-cli's connectors.ts.
|
|
4
4
|
|
|
5
5
|
import { CliError } from "../client.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import { printResult, ok, argsFromFlags } from "./shared.mjs";
|
|
7
|
+
|
|
8
|
+
const CMS_PROVIDERS = ["webflow", "framer", "sanity", "contentful"];
|
|
9
|
+
const PUBLISH_CONNECTORS = ["webflow", "framer", "google_docs", "contentful"];
|
|
10
|
+
|
|
11
|
+
// How every tool this module calls gets its arguments — see `argsFromFlags` in shared.mjs.
|
|
12
|
+
// `scripts/parity-check.ts` compares these keys against each tool's published input schema.
|
|
13
|
+
export const ARGS = {
|
|
14
|
+
list_publish_targets: {},
|
|
15
|
+
publish_to_connector: {
|
|
16
|
+
article_id: { positional: 0, label: "article-id", required: true },
|
|
17
|
+
connector: { flag: "to", required: true, enum: PUBLISH_CONNECTORS },
|
|
18
|
+
target_id: { flag: "target", required: true },
|
|
19
|
+
flow_run_id: "flow-run",
|
|
20
|
+
// A re-publish whose external item was deleted or unpublished in the client's CMS
|
|
21
|
+
// is refused, and the refusal explains itself. --confirm-missing-target is the
|
|
22
|
+
// answer to that specific question: it re-creates / re-publishes the item there.
|
|
23
|
+
// Deliberately not bundled into a general --yes, and never set by default.
|
|
24
|
+
confirm_missing_target: { flag: "confirm-missing-target", type: "bool" },
|
|
25
|
+
},
|
|
26
|
+
get_publish_status: {
|
|
27
|
+
connector: { flag: "connector", required: true, enum: PUBLISH_CONNECTORS },
|
|
28
|
+
publish_id: { flag: "publish-id", required: true },
|
|
29
|
+
},
|
|
30
|
+
import_from_cms: {
|
|
31
|
+
collection_id: "collection",
|
|
32
|
+
provider: { flag: "provider", enum: CMS_PROVIDERS },
|
|
33
|
+
},
|
|
34
|
+
connect_cms: {
|
|
35
|
+
provider: { flag: "provider", required: true, enum: CMS_PROVIDERS },
|
|
36
|
+
// api_token is required for every provider; the rest are provider-specific.
|
|
37
|
+
api_token: { flag: "token", required: true },
|
|
38
|
+
project_url: "project-url",
|
|
39
|
+
auto_deploy: { flag: "auto-deploy", type: "bool", negate: "no-auto-deploy" },
|
|
40
|
+
project_id: "project-id",
|
|
41
|
+
dataset: "dataset",
|
|
42
|
+
project_name: "project-name",
|
|
43
|
+
workspace_name: "workspace",
|
|
44
|
+
space_id: "space-id",
|
|
45
|
+
environment_id: "environment",
|
|
46
|
+
space_name: "space-name",
|
|
47
|
+
default_locale: "locale",
|
|
48
|
+
},
|
|
49
|
+
disconnect_cms: {
|
|
50
|
+
provider: { flag: "provider", required: true, enum: CMS_PROVIDERS },
|
|
51
|
+
},
|
|
52
|
+
list_cms_connections: {},
|
|
53
|
+
list_cms_mappings: {
|
|
54
|
+
provider: { flag: "provider", required: true, enum: CMS_PROVIDERS },
|
|
55
|
+
},
|
|
56
|
+
bind_collection_cms: {
|
|
57
|
+
collection_id: { flag: "collection", required: true },
|
|
58
|
+
provider: { flag: "provider", required: true, enum: CMS_PROVIDERS },
|
|
59
|
+
mapping_id: "mapping",
|
|
60
|
+
},
|
|
61
|
+
};
|
|
7
62
|
|
|
8
63
|
export async function cmdConnectors(ctx) {
|
|
9
64
|
const sub = ctx.positionals[0];
|
|
@@ -46,31 +101,16 @@ async function connectorsList(ctx) {
|
|
|
46
101
|
|
|
47
102
|
async function connectorsPublish(ctx) {
|
|
48
103
|
const { client, positionals, flags, io } = ctx;
|
|
49
|
-
const
|
|
50
|
-
const to = requireFlag(flags, "to");
|
|
51
|
-
const target = requireFlag(flags, "target");
|
|
52
|
-
// A re-publish whose external item was deleted or unpublished in the client's CMS
|
|
53
|
-
// is refused, and the refusal explains itself. --confirm-missing-target is the
|
|
54
|
-
// answer to that specific question: it re-creates / re-publishes the item there.
|
|
55
|
-
// Deliberately not bundled into a general --yes, and never set by default.
|
|
56
|
-
const args = compact({
|
|
57
|
-
article_id: articleId,
|
|
58
|
-
connector: to,
|
|
59
|
-
target_id: target,
|
|
60
|
-
flow_run_id: flagStr(flags["flow-run"]),
|
|
61
|
-
confirm_missing_target: flagBool(flags["confirm-missing-target"]) || undefined,
|
|
62
|
-
});
|
|
104
|
+
const args = argsFromFlags(ARGS.publish_to_connector, flags, positionals);
|
|
63
105
|
const result = await client.callTool("publish_to_connector", args);
|
|
64
|
-
ok(ctx, `Publishing to ${
|
|
106
|
+
ok(ctx, `Publishing to ${args.connector}.`);
|
|
65
107
|
printResult(io, flags, result);
|
|
66
108
|
return 0;
|
|
67
109
|
}
|
|
68
110
|
|
|
69
111
|
async function connectorsStatus(ctx) {
|
|
70
112
|
const { client, flags, io } = ctx;
|
|
71
|
-
const
|
|
72
|
-
const publishId = requireFlag(flags, "publish-id");
|
|
73
|
-
const result = await client.callTool("get_publish_status", { connector, publish_id: publishId });
|
|
113
|
+
const result = await client.callTool("get_publish_status", argsFromFlags(ARGS.get_publish_status, flags));
|
|
74
114
|
printResult(io, flags, result);
|
|
75
115
|
return 0;
|
|
76
116
|
}
|
|
@@ -79,16 +119,14 @@ async function connectorsPull(ctx) {
|
|
|
79
119
|
const { client, flags, io } = ctx;
|
|
80
120
|
// Dual key: --collection (a CMS-bound collection), or --provider (resolve/wire),
|
|
81
121
|
// or both (--provider wires an unbound --collection first). At least one required.
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
if (!collectionId && !provider) {
|
|
122
|
+
const args = argsFromFlags(ARGS.import_from_cms, flags);
|
|
123
|
+
if (!args.collection_id && !args.provider) {
|
|
85
124
|
throw new CliError("Pass --collection <id> and/or --provider <webflow|framer|sanity|contentful>");
|
|
86
125
|
}
|
|
87
|
-
const args = compact({ collection_id: collectionId, provider });
|
|
88
126
|
const result = await client.callTool("import_from_cms", args);
|
|
89
127
|
ok(
|
|
90
128
|
ctx,
|
|
91
|
-
`Pulled from ${result?.provider ?? provider ?? "CMS"} into collection ${result?.collection_id ??
|
|
129
|
+
`Pulled from ${result?.provider ?? args.provider ?? "CMS"} into collection ${result?.collection_id ?? args.collection_id ?? ""}.`
|
|
92
130
|
);
|
|
93
131
|
printResult(io, flags, result);
|
|
94
132
|
return 0;
|
|
@@ -96,33 +134,18 @@ async function connectorsPull(ctx) {
|
|
|
96
134
|
|
|
97
135
|
async function connectorsConnect(ctx) {
|
|
98
136
|
const { client, flags, io } = ctx;
|
|
99
|
-
const
|
|
100
|
-
// api_token is required for every provider; the rest are provider-specific.
|
|
101
|
-
const args = compact({
|
|
102
|
-
provider,
|
|
103
|
-
api_token: requireFlag(flags, "token"),
|
|
104
|
-
project_url: flagStr(flags["project-url"]),
|
|
105
|
-
auto_deploy: flags["no-auto-deploy"] ? false : flagBool(flags["auto-deploy"]) || undefined,
|
|
106
|
-
project_id: flagStr(flags["project-id"]),
|
|
107
|
-
dataset: flagStr(flags.dataset),
|
|
108
|
-
project_name: flagStr(flags["project-name"]),
|
|
109
|
-
workspace_name: flagStr(flags.workspace),
|
|
110
|
-
space_id: flagStr(flags["space-id"]),
|
|
111
|
-
environment_id: flagStr(flags.environment),
|
|
112
|
-
space_name: flagStr(flags["space-name"]),
|
|
113
|
-
default_locale: flagStr(flags.locale),
|
|
114
|
-
});
|
|
137
|
+
const args = argsFromFlags(ARGS.connect_cms, flags);
|
|
115
138
|
const result = await client.callTool("connect_cms", args);
|
|
116
|
-
ok(ctx, `Connected ${provider}${result?.label ? ` (${result.label})` : ""}.`);
|
|
139
|
+
ok(ctx, `Connected ${args.provider}${result?.label ? ` (${result.label})` : ""}.`);
|
|
117
140
|
printResult(io, flags, result);
|
|
118
141
|
return 0;
|
|
119
142
|
}
|
|
120
143
|
|
|
121
144
|
async function connectorsDisconnect(ctx) {
|
|
122
145
|
const { client, flags, io } = ctx;
|
|
123
|
-
const
|
|
124
|
-
const result = await client.callTool("disconnect_cms",
|
|
125
|
-
ok(ctx, `Disconnected ${provider}.`);
|
|
146
|
+
const args = argsFromFlags(ARGS.disconnect_cms, flags);
|
|
147
|
+
const result = await client.callTool("disconnect_cms", args);
|
|
148
|
+
ok(ctx, `Disconnected ${args.provider}.`);
|
|
126
149
|
printResult(io, flags, result);
|
|
127
150
|
return 0;
|
|
128
151
|
}
|
|
@@ -136,19 +159,14 @@ async function connectorsConnections(ctx) {
|
|
|
136
159
|
|
|
137
160
|
async function connectorsMappings(ctx) {
|
|
138
161
|
const { client, flags, io } = ctx;
|
|
139
|
-
const
|
|
140
|
-
const result = await client.callTool("list_cms_mappings", { provider });
|
|
162
|
+
const result = await client.callTool("list_cms_mappings", argsFromFlags(ARGS.list_cms_mappings, flags));
|
|
141
163
|
printResult(io, flags, result);
|
|
142
164
|
return 0;
|
|
143
165
|
}
|
|
144
166
|
|
|
145
167
|
async function connectorsBind(ctx) {
|
|
146
168
|
const { client, flags, io } = ctx;
|
|
147
|
-
const args =
|
|
148
|
-
collection_id: requireFlag(flags, "collection"),
|
|
149
|
-
provider: requireFlag(flags, "provider"),
|
|
150
|
-
mapping_id: flagStr(flags.mapping),
|
|
151
|
-
});
|
|
169
|
+
const args = argsFromFlags(ARGS.bind_collection_cms, flags);
|
|
152
170
|
const result = await client.callTool("bind_collection_cms", args);
|
|
153
171
|
ok(
|
|
154
172
|
ctx,
|
package/lib/commands/onboard.mjs
CHANGED
|
@@ -46,18 +46,109 @@
|
|
|
46
46
|
import { CliError } from "../client.mjs";
|
|
47
47
|
import {
|
|
48
48
|
flagStr,
|
|
49
|
-
flagNum,
|
|
50
49
|
flagBool,
|
|
51
50
|
requirePositional,
|
|
52
51
|
printResult,
|
|
52
|
+
refuseUnknownFlags,
|
|
53
53
|
compact,
|
|
54
54
|
ok,
|
|
55
55
|
pollUntil,
|
|
56
56
|
readTextInput,
|
|
57
|
+
argsFromFlags,
|
|
57
58
|
} from "./shared.mjs";
|
|
58
59
|
|
|
59
60
|
const COVER_STYLES = ["editorial_image", "full_ai", "line_art"];
|
|
60
61
|
const DOMAIN_MODES = ["subdomain", "buy_now", "connect_later"];
|
|
62
|
+
const SEER_MODES = ["suggest", "skip"];
|
|
63
|
+
const CMS_PROVIDERS = ["webflow", "framer", "sanity", "contentful"];
|
|
64
|
+
const APPROVAL_MODES = ["autopilot", "review_window", "manual"];
|
|
65
|
+
|
|
66
|
+
// How every tool this module calls gets its arguments — see `argsFromFlags` in shared.mjs.
|
|
67
|
+
// `scripts/parity-check.ts` compares these keys against each tool's published input schema.
|
|
68
|
+
export const ARGS = {
|
|
69
|
+
onboard_collection: {
|
|
70
|
+
collection_id: "collection",
|
|
71
|
+
collection_name: "name",
|
|
72
|
+
direction: ["about", "direction"],
|
|
73
|
+
sitemap_url: "sitemap",
|
|
74
|
+
// Source the existing blog from a connected CMS instead of a sitemap.
|
|
75
|
+
cms_provider: { flag: "cms", enum: CMS_PROVIDERS },
|
|
76
|
+
// Planner activation (#1731): the same one-call activation the app's Guided review
|
|
77
|
+
// step performs. Off unless asked for; needs a user-scoped key (an acting user).
|
|
78
|
+
enable_planner: { flag: "enable-planner", type: "bool" },
|
|
79
|
+
cadence_per_week: { flag: "cadence", type: "num" },
|
|
80
|
+
approval_mode: { flag: "approval", enum: APPROVAL_MODES },
|
|
81
|
+
},
|
|
82
|
+
get_collection_onboard_status: {
|
|
83
|
+
job_id: { internal: "the job id onboard_collection just returned" },
|
|
84
|
+
},
|
|
85
|
+
sync_collection_sitemap: {
|
|
86
|
+
collection_id: "collection",
|
|
87
|
+
},
|
|
88
|
+
onboard_domain: {
|
|
89
|
+
direction: ["about", "direction"],
|
|
90
|
+
sitemap_url: "sitemap",
|
|
91
|
+
cms_provider: { flag: "cms", enum: CMS_PROVIDERS },
|
|
92
|
+
collection_name: "name",
|
|
93
|
+
phantom: {
|
|
94
|
+
object: {
|
|
95
|
+
enabled: { flag: "no-phantom", type: "bool", invert: true },
|
|
96
|
+
posts_to_seed: { flag: "posts", type: "num" },
|
|
97
|
+
name: "phantom-name",
|
|
98
|
+
site_domain: "site-domain",
|
|
99
|
+
domain_mode: { flag: "domain-mode", enum: DOMAIN_MODES },
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
seers: { flag: "seers", enum: SEER_MODES },
|
|
103
|
+
visuals: { flag: "no-visuals", type: "bool", invert: true },
|
|
104
|
+
enhance: { flag: "no-enhance", type: "bool", invert: true },
|
|
105
|
+
is_test: { flag: "test", type: "bool" },
|
|
106
|
+
readiness_minutes: { flag: "readiness-minutes", type: "num" },
|
|
107
|
+
client_email: "client-email",
|
|
108
|
+
},
|
|
109
|
+
get_domain_onboard_status: {
|
|
110
|
+
job_id: "attach",
|
|
111
|
+
},
|
|
112
|
+
onboard_phantom: {
|
|
113
|
+
domain: { positional: 0, label: "domain", required: true },
|
|
114
|
+
// --accept gates the two-call flow below (dry run, then apply) rather than being
|
|
115
|
+
// forwarded: the first call never sends it, the second always sends true.
|
|
116
|
+
accept: { internal: "false on the dry run, true on apply — decided by the command" },
|
|
117
|
+
overrides: {
|
|
118
|
+
object: {
|
|
119
|
+
name: "name",
|
|
120
|
+
cadence_per_week: { flag: "cadence", type: "num" },
|
|
121
|
+
cover_canvas: { flag: "cover", enum: COVER_STYLES },
|
|
122
|
+
theme: "theme",
|
|
123
|
+
posts_to_seed: { flag: "posts", type: "num" },
|
|
124
|
+
// The highest-leverage override: it becomes the planner's steering prompt, so it
|
|
125
|
+
// shapes the topic map and every post, not just the masthead. A chosen topic
|
|
126
|
+
// plan (--plan-file) supplies it when no --about is given.
|
|
127
|
+
direction: {
|
|
128
|
+
value: (flags, _p, extra) =>
|
|
129
|
+
flagStr(flags.about) ?? flagStr(flags.direction) ?? extra.plan?.direction,
|
|
130
|
+
},
|
|
131
|
+
// The approved plan's content areas become the topic map's pillars VERBATIM.
|
|
132
|
+
pillars: { value: (_f, _p, extra) => extra.plan?.pillars },
|
|
133
|
+
site_domain: "domain",
|
|
134
|
+
domain_mode: { flag: "domain-mode", enum: DOMAIN_MODES },
|
|
135
|
+
is_test: { flag: "real", type: "bool", invert: true },
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
get_phantom_onboard_status: {
|
|
140
|
+
job_id: { internal: "the job id onboard_phantom just returned" },
|
|
141
|
+
},
|
|
142
|
+
propose_phantom_plan: {
|
|
143
|
+
client_domain: { positional: 0, label: "client-domain" },
|
|
144
|
+
direction: ["about", "direction"],
|
|
145
|
+
name: "name",
|
|
146
|
+
},
|
|
147
|
+
propose_phantom_mastheads: {
|
|
148
|
+
client_domain: { positional: 0, label: "client-domain", required: true },
|
|
149
|
+
plan: { value: (_f, _p, extra) => extra.plan },
|
|
150
|
+
},
|
|
151
|
+
};
|
|
61
152
|
|
|
62
153
|
function formatPlan(plan) {
|
|
63
154
|
const lines = [
|
|
@@ -191,11 +282,7 @@ function formatMastheads(r, plan) {
|
|
|
191
282
|
// because the tool falls back to the org's own recorded domain (the key's org IS the client).
|
|
192
283
|
async function cmdOnboardPropose(ctx) {
|
|
193
284
|
const { client, flags, io } = ctx;
|
|
194
|
-
const args =
|
|
195
|
-
client_domain: ctx.positionals[0],
|
|
196
|
-
direction: flagStr(flags.about) ?? flagStr(flags.direction),
|
|
197
|
-
name: flagStr(flags.name),
|
|
198
|
-
});
|
|
285
|
+
const args = argsFromFlags(ARGS.propose_phantom_plan, flags, ctx.positionals);
|
|
199
286
|
ok(ctx, "Proposing…");
|
|
200
287
|
const proposal = await client.callTool("propose_phantom_plan", args);
|
|
201
288
|
printResult(io, flags, proposal, formatProposal);
|
|
@@ -219,7 +306,10 @@ async function cmdOnboardMastheads(ctx) {
|
|
|
219
306
|
);
|
|
220
307
|
}
|
|
221
308
|
ok(ctx, `Proposing mastheads for "${plan.title}"…`);
|
|
222
|
-
const result = await client.callTool(
|
|
309
|
+
const result = await client.callTool(
|
|
310
|
+
"propose_phantom_mastheads",
|
|
311
|
+
argsFromFlags(ARGS.propose_phantom_mastheads, flags, ctx.positionals, { plan })
|
|
312
|
+
);
|
|
223
313
|
printResult(io, flags, result, (r) => formatMastheads(r, plan));
|
|
224
314
|
if (result.options?.length) {
|
|
225
315
|
ok(
|
|
@@ -234,35 +324,13 @@ async function cmdOnboardMastheads(ctx) {
|
|
|
234
324
|
function overridesFromFlags(flags) {
|
|
235
325
|
// A chosen topic plan pins direction + pillars; an explicit --about still wins on direction.
|
|
236
326
|
const plan = readTopicPlan(flags);
|
|
237
|
-
const
|
|
238
|
-
if (cover !== undefined && !COVER_STYLES.includes(cover)) {
|
|
239
|
-
throw new CliError(`--cover must be one of: ${COVER_STYLES.join(", ")}`);
|
|
240
|
-
}
|
|
241
|
-
const domainMode = flagStr(flags["domain-mode"]);
|
|
242
|
-
if (domainMode !== undefined && !DOMAIN_MODES.includes(domainMode)) {
|
|
243
|
-
throw new CliError(`--domain-mode must be one of: ${DOMAIN_MODES.join(", ")}`);
|
|
244
|
-
}
|
|
245
|
-
const siteDomain = flagStr(flags.domain);
|
|
327
|
+
const overrides = argsFromFlags(ARGS.onboard_phantom.overrides.object, flags, [], { plan });
|
|
246
328
|
// buy_now spends real money against a 2-per-org cap; refusing the mismatch here beats
|
|
247
329
|
// silently downgrading it to a subdomain launch the caller didn't ask for.
|
|
248
|
-
if (
|
|
249
|
-
throw new CliError(`--domain-mode ${
|
|
250
|
-
}
|
|
251
|
-
return
|
|
252
|
-
name: flagStr(flags.name),
|
|
253
|
-
cadence_per_week: flagNum(flags.cadence),
|
|
254
|
-
cover_canvas: cover,
|
|
255
|
-
theme: flagStr(flags.theme),
|
|
256
|
-
posts_to_seed: flagNum(flags.posts),
|
|
257
|
-
// The highest-leverage override: it becomes the planner's steering prompt, so it
|
|
258
|
-
// shapes the topic map and every post, not just the masthead.
|
|
259
|
-
direction: flagStr(flags.about) ?? flagStr(flags.direction) ?? plan?.direction,
|
|
260
|
-
// The approved plan's content areas become the topic map's pillars VERBATIM.
|
|
261
|
-
pillars: plan?.pillars,
|
|
262
|
-
site_domain: siteDomain,
|
|
263
|
-
domain_mode: domainMode,
|
|
264
|
-
is_test: flagBool(flags["real"]) ? false : undefined,
|
|
265
|
-
});
|
|
330
|
+
if (overrides.domain_mode && overrides.domain_mode !== "subdomain" && !overrides.site_domain) {
|
|
331
|
+
throw new CliError(`--domain-mode ${overrides.domain_mode} needs --domain <bare-domain>`);
|
|
332
|
+
}
|
|
333
|
+
return overrides;
|
|
266
334
|
}
|
|
267
335
|
|
|
268
336
|
// Final readback of a collection-onboarding job — the counts the engine PERSISTED.
|
|
@@ -282,7 +350,9 @@ function formatCollectionStatus(s) {
|
|
|
282
350
|
` series: ${r.series ?? 0}`,
|
|
283
351
|
` open topics: ${r.spokes_open ?? 0} (${r.spokes_seo_bound ?? 0} SEO-bound)`,
|
|
284
352
|
` headlines suggested: ${r.headlines_suggested ?? 0}`,
|
|
285
|
-
|
|
353
|
+
s.enable_planner
|
|
354
|
+
? ` Planner: ON — ${s.cadence_per_week ?? "?"}/week, ${s.approval_mode ?? "manual"} approval, queue filled.`
|
|
355
|
+
: ` Review them in Headlines & Queue — the Planner is configured but OFF (pass --enable-planner to turn it on).`
|
|
286
356
|
);
|
|
287
357
|
}
|
|
288
358
|
return lines.join("\n");
|
|
@@ -302,14 +372,38 @@ async function cmdOnboardCollection(ctx) {
|
|
|
302
372
|
);
|
|
303
373
|
}
|
|
304
374
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
375
|
+
// READ PATHS FIRST, before anything that writes. `--status` and `--attach` mean here
|
|
376
|
+
// exactly what they mean on `onboard domain`: show me the job, do not start one.
|
|
377
|
+
// They had no branch at all, so they fell through to onboard_collection and STARTED A
|
|
378
|
+
// SECOND ONBOARDING — a read verb that spent money and left a duplicate collection.
|
|
379
|
+
const attach = flagStr(flags.attach);
|
|
380
|
+
if (attach) {
|
|
381
|
+
const s = await client.callTool("get_collection_onboard_status", { job_id: attach });
|
|
382
|
+
printResult(ctx.io, flags, s, formatCollectionStatus);
|
|
383
|
+
return 0;
|
|
384
|
+
}
|
|
385
|
+
if (flagBool(flags.status)) {
|
|
386
|
+
const collectionId = flagStr(flags.collection);
|
|
387
|
+
const s = await client.callTool(
|
|
388
|
+
"get_collection_onboard_status",
|
|
389
|
+
collectionId ? { collection_id: collectionId } : {}
|
|
390
|
+
);
|
|
391
|
+
printResult(ctx.io, flags, s, formatCollectionStatus);
|
|
392
|
+
return 0;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Everything below WRITES. A flag we do not understand is refused rather than ignored:
|
|
396
|
+
// the ARGS map that builds the call is the list of what may be passed to it.
|
|
397
|
+
refuseUnknownFlags(ctx, ARGS.onboard_collection, ["status", "attach", "watch", "no-wait", "domain"]);
|
|
398
|
+
|
|
399
|
+
const args = argsFromFlags(ARGS.onboard_collection, flags, ctx.positionals);
|
|
400
|
+
// --cadence / --approval only mean something once the Planner is being turned on; a
|
|
401
|
+
// silently ignored flag is exactly the kind of invisible opinion this lane exists to remove.
|
|
402
|
+
if ((args.cadence_per_week !== undefined || args.approval_mode !== undefined) && !args.enable_planner) {
|
|
403
|
+
throw new CliError(
|
|
404
|
+
"--cadence and --approval only apply with --enable-planner (the Planner stays OFF otherwise)."
|
|
405
|
+
);
|
|
406
|
+
}
|
|
313
407
|
|
|
314
408
|
ok(ctx, "Starting collection onboarding…");
|
|
315
409
|
const started = await client.callTool("onboard_collection", args);
|
|
@@ -357,13 +451,13 @@ async function cmdOnboardCollection(ctx) {
|
|
|
357
451
|
// bounded per run, so it returns synchronously.
|
|
358
452
|
async function cmdOnboardSyncSitemap(ctx) {
|
|
359
453
|
const { client, flags } = ctx;
|
|
360
|
-
const
|
|
361
|
-
if (!
|
|
454
|
+
const args = argsFromFlags(ARGS.sync_collection_sitemap, flags, ctx.positionals);
|
|
455
|
+
if (!args.collection_id) {
|
|
362
456
|
throw new CliError("onboard sync-sitemap requires --collection <uuid>.");
|
|
363
457
|
}
|
|
364
458
|
|
|
365
459
|
ok(ctx, "Re-checking the sitemap…");
|
|
366
|
-
const result = await client.callTool("sync_collection_sitemap",
|
|
460
|
+
const result = await client.callTool("sync_collection_sitemap", args);
|
|
367
461
|
|
|
368
462
|
printResult(ctx.io, flags, result, (r) =>
|
|
369
463
|
[
|
|
@@ -383,9 +477,6 @@ async function cmdOnboardSyncSitemap(ctx) {
|
|
|
383
477
|
return 0;
|
|
384
478
|
}
|
|
385
479
|
|
|
386
|
-
const SEER_MODES = ["suggest", "skip"];
|
|
387
|
-
const CMS_PROVIDERS = ["webflow", "framer", "sanity", "contentful"];
|
|
388
|
-
|
|
389
480
|
// Final readback of a domain-onboarding job — the counts the engine PERSISTED.
|
|
390
481
|
export function formatDomainStatus(s) {
|
|
391
482
|
const r = s.result ?? {};
|
|
@@ -474,35 +565,7 @@ async function cmdOnboardDomain(ctx) {
|
|
|
474
565
|
return 0;
|
|
475
566
|
}
|
|
476
567
|
|
|
477
|
-
const
|
|
478
|
-
if (seers !== undefined && !SEER_MODES.includes(seers)) {
|
|
479
|
-
throw new CliError(`--seers must be one of: ${SEER_MODES.join(", ")}`);
|
|
480
|
-
}
|
|
481
|
-
const cms = flagStr(flags.cms);
|
|
482
|
-
if (cms !== undefined && !CMS_PROVIDERS.includes(cms)) {
|
|
483
|
-
throw new CliError(`--cms must be one of: ${CMS_PROVIDERS.join(", ")}`);
|
|
484
|
-
}
|
|
485
|
-
const posts = flagNum(flags.posts);
|
|
486
|
-
const phantom = compact({
|
|
487
|
-
enabled: flagBool(flags["no-phantom"]) ? false : undefined,
|
|
488
|
-
posts_to_seed: posts,
|
|
489
|
-
name: flagStr(flags["phantom-name"]),
|
|
490
|
-
site_domain: flagStr(flags["site-domain"]),
|
|
491
|
-
domain_mode: flagStr(flags["domain-mode"]),
|
|
492
|
-
});
|
|
493
|
-
const args = compact({
|
|
494
|
-
direction: flagStr(flags.about) ?? flagStr(flags.direction),
|
|
495
|
-
sitemap_url: flagStr(flags.sitemap),
|
|
496
|
-
cms_provider: cms,
|
|
497
|
-
collection_name: flagStr(flags.name),
|
|
498
|
-
phantom: Object.keys(phantom).length ? phantom : undefined,
|
|
499
|
-
seers,
|
|
500
|
-
visuals: flagBool(flags["no-visuals"]) ? false : undefined,
|
|
501
|
-
enhance: flagBool(flags["no-enhance"]) ? false : undefined,
|
|
502
|
-
is_test: flagBool(flags.test) ? true : undefined,
|
|
503
|
-
readiness_minutes: flagNum(flags["readiness-minutes"]),
|
|
504
|
-
client_email: flagStr(flags["client-email"]),
|
|
505
|
-
});
|
|
568
|
+
const args = argsFromFlags(ARGS.onboard_domain, flags, ctx.positionals);
|
|
506
569
|
|
|
507
570
|
ok(ctx, "Starting domain onboarding…");
|
|
508
571
|
const started = await client.callTool("onboard_domain", args);
|
package/lib/commands/shared.mjs
CHANGED
|
@@ -53,6 +53,138 @@ export function compact(obj) {
|
|
|
53
53
|
return out;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// Flags every command accepts: transport, output shape, and the ones the runner
|
|
57
|
+
// consumes before a command ever sees them.
|
|
58
|
+
export const GLOBAL_FLAGS = new Set([
|
|
59
|
+
"url",
|
|
60
|
+
"key",
|
|
61
|
+
"staff-key",
|
|
62
|
+
"json",
|
|
63
|
+
"quiet",
|
|
64
|
+
"verbose",
|
|
65
|
+
"help",
|
|
66
|
+
"version",
|
|
67
|
+
"yes",
|
|
68
|
+
"no-color",
|
|
69
|
+
"profile",
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Refuse a flag this command does not understand.
|
|
74
|
+
*
|
|
75
|
+
* On a WRITE command an unrecognised flag is not cosmetic. `phantom-onboard
|
|
76
|
+
* collection --status` used to fall straight through to `onboard_collection`
|
|
77
|
+
* and START A SECOND ONBOARDING — a read verb that spent money and left a
|
|
78
|
+
* duplicate collection behind — because nothing checked that `--status` meant
|
|
79
|
+
* anything here. Its sibling `phantom-onboard domain --status` reads, so the
|
|
80
|
+
* same flag meant "read" on one subcommand and "spend" on the other.
|
|
81
|
+
*
|
|
82
|
+
* `own` is the flags the command handles itself; the ARGS map supplies the
|
|
83
|
+
* rest, so the map that BUILDS the call is also the list of what may be passed
|
|
84
|
+
* to it — one source, the same principle the maps exist for.
|
|
85
|
+
*/
|
|
86
|
+
export function refuseUnknownFlags(ctx, argsMap, own = []) {
|
|
87
|
+
const allowed = new Set([...GLOBAL_FLAGS, ...own]);
|
|
88
|
+
for (const source of Object.values(argsMap ?? {})) {
|
|
89
|
+
if (typeof source === "string") allowed.add(source);
|
|
90
|
+
else if (Array.isArray(source)) source.forEach((f) => allowed.add(f));
|
|
91
|
+
else if (source && typeof source === "object" && source.flag) allowed.add(source.flag);
|
|
92
|
+
}
|
|
93
|
+
const unknown = Object.keys(ctx.flags).filter((f) => !allowed.has(f));
|
|
94
|
+
if (unknown.length) {
|
|
95
|
+
throw new CliError(
|
|
96
|
+
`Unknown flag${unknown.length > 1 ? "s" : ""} for this command: ${unknown.map((f) => `--${f}`).join(", ")}. ` +
|
|
97
|
+
"This command writes, so it refuses rather than ignoring a flag it does not understand."
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------------
|
|
103
|
+
// ARGS maps: the one place a curated command says how each tool input key is sourced.
|
|
104
|
+
//
|
|
105
|
+
// A command module exports `ARGS = { <tool name>: { <schema key>: <source> } }` and builds
|
|
106
|
+
// its `callTool` payload with `argsFromFlags(ARGS[tool], flags, positionals)`. The map IS
|
|
107
|
+
// the implementation — not a second copy of it — so `scripts/parity-check.ts` can import it
|
|
108
|
+
// and compare its keys against the tool's published input schema. A tool that gains a
|
|
109
|
+
// parameter the command never learned then FAILS parity instead of drifting silently
|
|
110
|
+
// (#1731 shipped enable_planner to MCP and REST and the CLI never got the flags; nothing
|
|
111
|
+
// noticed, because the old check compared tool NAMES only).
|
|
112
|
+
//
|
|
113
|
+
// Source forms, one per schema key:
|
|
114
|
+
// "flag" a string flag, --flag <value>
|
|
115
|
+
// ["about", "direction"] the first of these string flags that is set
|
|
116
|
+
// { flag, type?, required?, enum?, invert?, negate?, positional?, label? }
|
|
117
|
+
// type: "str" (default) | "num" | "bool" | "list" (a repeatable flag)
|
|
118
|
+
// required: throw "Missing required --flag" when absent
|
|
119
|
+
// enum: throw "--flag must be one of: …" on any other value
|
|
120
|
+
// invert: (bool) a set --flag sends FALSE (e.g. --no-phantom → enabled:false)
|
|
121
|
+
// negate: (bool) a second flag that sends FALSE (e.g. --no-auto-deploy)
|
|
122
|
+
// positional: an index into positionals instead of a flag (label names it in errors)
|
|
123
|
+
// { object: { <sub key>: <source> } } a nested object, omitted when every sub key is absent
|
|
124
|
+
// { value: (flags, positionals, extra) => v }
|
|
125
|
+
// computed from several flags or an earlier result;
|
|
126
|
+
// still DECLARED, so parity counts the key as exposed
|
|
127
|
+
// { internal: "why" } supplied by the command from a prior tool result
|
|
128
|
+
// (a job id it just received), never from the user
|
|
129
|
+
export function argsFromFlags(spec, flags, positionals = [], extra = {}) {
|
|
130
|
+
const out = {};
|
|
131
|
+
for (const [key, source] of Object.entries(spec)) {
|
|
132
|
+
const v = readArgSource(key, source, flags, positionals, extra);
|
|
133
|
+
if (v !== undefined) out[key] = v;
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function readArgSource(key, source, flags, positionals, extra) {
|
|
139
|
+
if (typeof source === "string") return flagStr(flags[source]);
|
|
140
|
+
if (Array.isArray(source)) {
|
|
141
|
+
for (const name of source) {
|
|
142
|
+
const v = flagStr(flags[name]);
|
|
143
|
+
if (v !== undefined) return v;
|
|
144
|
+
}
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
if (!source || typeof source !== "object") {
|
|
148
|
+
throw new CliError(`ARGS.${key}: unsupported source ${JSON.stringify(source)}`);
|
|
149
|
+
}
|
|
150
|
+
if (source.internal !== undefined) return undefined;
|
|
151
|
+
if (typeof source.value === "function") return source.value(flags, positionals, extra);
|
|
152
|
+
if (source.object) {
|
|
153
|
+
const nested = argsFromFlags(source.object, flags, positionals, extra);
|
|
154
|
+
return Object.keys(nested).length ? nested : undefined;
|
|
155
|
+
}
|
|
156
|
+
if (source.positional !== undefined) {
|
|
157
|
+
const v = positionals[source.positional];
|
|
158
|
+
if (v === undefined && source.required) {
|
|
159
|
+
throw new CliError(`Missing required <${source.label ?? key}>`);
|
|
160
|
+
}
|
|
161
|
+
return v;
|
|
162
|
+
}
|
|
163
|
+
const raw = flags[source.flag];
|
|
164
|
+
let v;
|
|
165
|
+
switch (source.type ?? "str") {
|
|
166
|
+
case "num":
|
|
167
|
+
v = flagNum(raw);
|
|
168
|
+
break;
|
|
169
|
+
case "bool":
|
|
170
|
+
if (source.negate && flags[source.negate] === true) return false;
|
|
171
|
+
v = raw === true ? (source.invert ? false : true) : undefined;
|
|
172
|
+
break;
|
|
173
|
+
case "list": {
|
|
174
|
+
const list = flagList(raw);
|
|
175
|
+
v = list.length ? list : undefined;
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
default:
|
|
179
|
+
v = flagStr(raw);
|
|
180
|
+
}
|
|
181
|
+
if (v === undefined && source.required) throw new CliError(`Missing required --${source.flag}`);
|
|
182
|
+
if (v !== undefined && source.enum && !source.enum.includes(v)) {
|
|
183
|
+
throw new CliError(`--${source.flag} must be one of: ${source.enum.join(", ")}`);
|
|
184
|
+
}
|
|
185
|
+
return v;
|
|
186
|
+
}
|
|
187
|
+
|
|
56
188
|
export function printResult(io, flags, value, formatter) {
|
|
57
189
|
if (flags.json || !formatter) {
|
|
58
190
|
io.log(JSON.stringify(value, null, 2));
|
|
@@ -107,6 +107,9 @@ async function shredderRun(ctx) {
|
|
|
107
107
|
attempts_per_unit: flagNum(flags.attempts),
|
|
108
108
|
// How hard to rewrite. Validated service-side; sent only when the caller asked.
|
|
109
109
|
...(flags.aggression ? { aggression: String(flags.aggression) } : {}),
|
|
110
|
+
// Hold every rewrite to the live writing rules. A trade, not a free win: refused rewrites
|
|
111
|
+
// cost provider attempts and a sentence with no clean rewrite keeps its original.
|
|
112
|
+
...(flags["apply-rules"] === true ? { apply_rules: true } : {}),
|
|
110
113
|
...(sync ? {} : { async: true }),
|
|
111
114
|
});
|
|
112
115
|
const result = await client.callTool("run_shred", args);
|
|
@@ -4,16 +4,72 @@
|
|
|
4
4
|
// mirrored here as two exported handlers for the same reason.
|
|
5
5
|
|
|
6
6
|
import { CliError } from "../client.mjs";
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
7
|
+
import { flagStr, printResult, flagList, ok, readBodyInput, argsFromFlags } from "./shared.mjs";
|
|
8
|
+
|
|
9
|
+
// A list flag the API clears with an explicit empty array (which compact() keeps):
|
|
10
|
+
// `--clear-topics` sends [], a repeated --topic sends the list, neither sends nothing.
|
|
11
|
+
const clearableList = (flag, clearFlag) => ({
|
|
12
|
+
value: (flags) => {
|
|
13
|
+
if (flags[clearFlag]) return [];
|
|
14
|
+
const list = flagList(flags[flag]);
|
|
15
|
+
return list.length ? list : undefined;
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// How every tool this module calls gets its arguments — see `argsFromFlags` in shared.mjs.
|
|
20
|
+
// `scripts/parity-check.ts` compares these keys against each tool's published input schema.
|
|
21
|
+
export const ARGS = {
|
|
22
|
+
get_company_info: {},
|
|
23
|
+
set_company_info: {
|
|
24
|
+
name: "name",
|
|
25
|
+
domain: "domain",
|
|
26
|
+
// The API clears the manifesto with an empty string, which a plain flag read would
|
|
27
|
+
// drop — so it is set explicitly whenever the user supplied a source (text or file).
|
|
28
|
+
manifesto: {
|
|
29
|
+
value: (flags) => {
|
|
30
|
+
const inline = flagStr(flags.manifesto);
|
|
31
|
+
const file = flagStr(flags["manifesto-file"]);
|
|
32
|
+
if (inline === undefined && file === undefined) return undefined;
|
|
33
|
+
return inline ?? readBodyInput({ file }) ?? "";
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
market: "market",
|
|
37
|
+
},
|
|
38
|
+
get_positioning: {},
|
|
39
|
+
set_positioning: {
|
|
40
|
+
priority_topics: clearableList("topic", "clear-topics"),
|
|
41
|
+
stances: clearableList("stance", "clear-stances"),
|
|
42
|
+
avoid_topics: clearableList("avoid", "clear-avoid"),
|
|
43
|
+
},
|
|
44
|
+
list_competitors: {},
|
|
45
|
+
add_competitor: {
|
|
46
|
+
name: { positional: 0, label: "name", required: true },
|
|
47
|
+
domain: { positional: 1, label: "domain", required: true },
|
|
48
|
+
},
|
|
49
|
+
import_sitemap: {
|
|
50
|
+
collection_id: { flag: "collection", required: true },
|
|
51
|
+
sitemap_url: { flag: "url", required: true },
|
|
52
|
+
sitemap_urls: { flag: "sub", type: "list" },
|
|
53
|
+
url_pattern: "pattern",
|
|
54
|
+
posts_only: { flag: "posts-only", type: "bool" },
|
|
55
|
+
},
|
|
56
|
+
list_topics: {
|
|
57
|
+
collection_id: { flag: "collection", required: true },
|
|
58
|
+
},
|
|
59
|
+
set_topic: {
|
|
60
|
+
collection_id: { flag: "collection", required: true },
|
|
61
|
+
topic_id: "topic",
|
|
62
|
+
suggestion_id: "suggestion",
|
|
63
|
+
},
|
|
64
|
+
get_onboarding_status: {},
|
|
65
|
+
// Step ids are the server's to define (the registry is the authority); forwarded as given.
|
|
66
|
+
set_onboarding_step: {
|
|
67
|
+
current_step: "current",
|
|
68
|
+
complete_step: "complete",
|
|
69
|
+
skip_step: "skip",
|
|
70
|
+
status: "status",
|
|
71
|
+
},
|
|
72
|
+
};
|
|
17
73
|
|
|
18
74
|
export async function cmdStrategy(ctx) {
|
|
19
75
|
const sub = ctx.positionals[0];
|
|
@@ -60,16 +116,11 @@ async function companyGet(ctx) {
|
|
|
60
116
|
|
|
61
117
|
async function companySet(ctx) {
|
|
62
118
|
const { client, flags, io } = ctx;
|
|
63
|
-
const args =
|
|
64
|
-
// The API clears the manifesto with an empty string, which compact() would drop —
|
|
65
|
-
// so set it explicitly whenever the user supplied a source (text or file).
|
|
66
|
-
const manifesto = flagStr(flags.manifesto);
|
|
67
|
-
const manifestoFile = flagStr(flags["manifesto-file"]);
|
|
68
|
-
if (manifesto !== undefined || manifestoFile !== undefined) {
|
|
69
|
-
args.manifesto = manifesto ?? readBodyInput({ file: manifestoFile }) ?? "";
|
|
70
|
-
}
|
|
119
|
+
const args = argsFromFlags(ARGS.set_company_info, flags);
|
|
71
120
|
if (Object.keys(args).length === 0) {
|
|
72
|
-
throw new CliError(
|
|
121
|
+
throw new CliError(
|
|
122
|
+
'Nothing to set — pass --name/--domain/--manifesto/--market (empty --manifesto "" clears it).'
|
|
123
|
+
);
|
|
73
124
|
}
|
|
74
125
|
const result = await client.callTool("set_company_info", args);
|
|
75
126
|
ok(ctx, "Company profile updated.");
|
|
@@ -101,15 +152,7 @@ async function positioningGet(ctx) {
|
|
|
101
152
|
|
|
102
153
|
async function positioningSet(ctx) {
|
|
103
154
|
const { client, flags, io } = ctx;
|
|
104
|
-
const
|
|
105
|
-
const stances = flagList(flags.stance);
|
|
106
|
-
const avoid = flagList(flags.avoid);
|
|
107
|
-
// The API clears a list with an explicit empty array (which compact() keeps).
|
|
108
|
-
const args = compact({
|
|
109
|
-
priority_topics: flags["clear-topics"] ? [] : topics.length ? topics : undefined,
|
|
110
|
-
stances: flags["clear-stances"] ? [] : stances.length ? stances : undefined,
|
|
111
|
-
avoid_topics: flags["clear-avoid"] ? [] : avoid.length ? avoid : undefined,
|
|
112
|
-
});
|
|
155
|
+
const args = argsFromFlags(ARGS.set_positioning, flags);
|
|
113
156
|
if (Object.keys(args).length === 0) {
|
|
114
157
|
throw new CliError("Pass at least one --topic/--stance/--avoid (or a --clear-* flag).");
|
|
115
158
|
}
|
|
@@ -144,10 +187,9 @@ async function competitorsList(ctx) {
|
|
|
144
187
|
|
|
145
188
|
async function competitorsAdd(ctx) {
|
|
146
189
|
const { client, positionals, flags, io } = ctx;
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
ok(ctx, `Added competitor ${name}.`);
|
|
190
|
+
const args = argsFromFlags(ARGS.add_competitor, flags, positionals);
|
|
191
|
+
const result = await client.callTool("add_competitor", args);
|
|
192
|
+
ok(ctx, `Added competitor ${args.name}.`);
|
|
151
193
|
printResult(io, flags, result);
|
|
152
194
|
return 0;
|
|
153
195
|
}
|
|
@@ -156,15 +198,7 @@ async function competitorsAdd(ctx) {
|
|
|
156
198
|
|
|
157
199
|
async function strategySitemap(ctx) {
|
|
158
200
|
const { client, flags, io } = ctx;
|
|
159
|
-
const
|
|
160
|
-
const url = requireFlag(flags, "url");
|
|
161
|
-
const sub = flagList(flags.sub);
|
|
162
|
-
const args = compact({
|
|
163
|
-
collection_id: collection,
|
|
164
|
-
sitemap_url: url,
|
|
165
|
-
sitemap_urls: sub.length ? sub : undefined,
|
|
166
|
-
url_pattern: flagStr(flags.pattern),
|
|
167
|
-
});
|
|
201
|
+
const args = argsFromFlags(ARGS.import_sitemap, flags);
|
|
168
202
|
const result = await client.callTool("import_sitemap", args);
|
|
169
203
|
ok(ctx, "Sitemap import started.");
|
|
170
204
|
printResult(io, flags, result);
|
|
@@ -189,21 +223,17 @@ async function strategyTopics(ctx) {
|
|
|
189
223
|
|
|
190
224
|
async function topicsList(ctx) {
|
|
191
225
|
const { client, flags, io } = ctx;
|
|
192
|
-
const
|
|
193
|
-
const result = await client.callTool("list_topics", { collection_id: collection });
|
|
226
|
+
const result = await client.callTool("list_topics", argsFromFlags(ARGS.list_topics, flags));
|
|
194
227
|
printResult(io, flags, result);
|
|
195
228
|
return 0;
|
|
196
229
|
}
|
|
197
230
|
|
|
198
231
|
async function topicsSet(ctx) {
|
|
199
232
|
const { client, flags, io } = ctx;
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
const suggestionId = flagStr(flags.suggestion);
|
|
203
|
-
if ((topicId === undefined) === (suggestionId === undefined)) {
|
|
233
|
+
const args = argsFromFlags(ARGS.set_topic, flags);
|
|
234
|
+
if ((args.topic_id === undefined) === (args.suggestion_id === undefined)) {
|
|
204
235
|
throw new CliError("Pass exactly one of --topic <topic-id> or --suggestion <suggestion-id>.");
|
|
205
236
|
}
|
|
206
|
-
const args = compact({ collection_id: collection, topic_id: topicId, suggestion_id: suggestionId });
|
|
207
237
|
const result = await client.callTool("set_topic", args);
|
|
208
238
|
ok(ctx, "Topic queued.");
|
|
209
239
|
printResult(io, flags, result);
|
|
@@ -234,12 +264,7 @@ async function onboardingStatus(ctx) {
|
|
|
234
264
|
|
|
235
265
|
async function onboardingStep(ctx) {
|
|
236
266
|
const { client, flags, io } = ctx;
|
|
237
|
-
const args =
|
|
238
|
-
current_step: flagStr(flags.current),
|
|
239
|
-
complete_step: flagStr(flags.complete),
|
|
240
|
-
skip_step: flagStr(flags.skip),
|
|
241
|
-
status: flagStr(flags.status),
|
|
242
|
-
});
|
|
267
|
+
const args = argsFromFlags(ARGS.set_onboarding_step, flags);
|
|
243
268
|
if (Object.keys(args).length === 0) {
|
|
244
269
|
throw new CliError("Pass --current/--complete/--skip/--status.");
|
|
245
270
|
}
|