@letterstory/cli 0.19.0 → 0.23.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 +40 -0
- package/lib/cli.mjs +56 -13
- package/lib/commands/collections.mjs +115 -53
- package/lib/commands/connectors.mjs +69 -51
- package/lib/commands/kernel.mjs +101 -30
- package/lib/commands/onboard.mjs +329 -103
- package/lib/commands/shared.mjs +184 -0
- package/lib/commands/shredder.mjs +31 -29
- package/lib/commands/strategy.mjs +79 -54
- package/package.json +1 -1
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));
|
|
@@ -89,6 +221,58 @@ export async function pollUntil(io, poll, opts = {}) {
|
|
|
89
221
|
}
|
|
90
222
|
}
|
|
91
223
|
|
|
224
|
+
// Pull one field out of a tool result.
|
|
225
|
+
//
|
|
226
|
+
// `client.callTool` ALREADY unwraps the MCP envelope and parses the JSON, so a
|
|
227
|
+
// tool result here is the payload itself. Two commands used to reach for
|
|
228
|
+
// `result.content[0].text` — the envelope shape — which is always undefined
|
|
229
|
+
// after unwrapping, so the job id was never found, the follow loop never ran,
|
|
230
|
+
// and `--no-wait` never printed its hint. The envelope fallback below is kept
|
|
231
|
+
// only so a raw result passed in from somewhere else still reads correctly.
|
|
232
|
+
export function toolField(result, field) {
|
|
233
|
+
if (result && typeof result === "object" && field in result) return result[field];
|
|
234
|
+
const raw = result?.content?.[0]?.text;
|
|
235
|
+
if (typeof raw === "string") {
|
|
236
|
+
try {
|
|
237
|
+
return JSON.parse(raw)?.[field];
|
|
238
|
+
} catch {
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Follow a queued job to a terminal state, SLEEPING FIRST.
|
|
246
|
+
//
|
|
247
|
+
// Unlike `pollUntil`, which checks immediately: a shred or a kernel run takes
|
|
248
|
+
// minutes and starts on the next tick, so an instant first poll only ever says
|
|
249
|
+
// "queued". Sleeping first is also what makes the interval the operator's —
|
|
250
|
+
// both loops used a file-local `setTimeout`, so `io.sleep` (and with it
|
|
251
|
+
// LETTERSTORY_POLL_INTERVAL_MS) was ignored and nobody could pace them.
|
|
252
|
+
//
|
|
253
|
+
// Returns `{ settled, value }`. `settled:false` means the ceiling passed — the
|
|
254
|
+
// caller decides what to say, because the job is still running on the server
|
|
255
|
+
// and that is not an error.
|
|
256
|
+
export async function followJob(io, poll, opts = {}) {
|
|
257
|
+
// The default is the command's own (a shred is slow; polling fast only burns
|
|
258
|
+
// requests). An operator who EXPLICITLY sets LETTERSTORY_POLL_INTERVAL_MS
|
|
259
|
+
// means it, and that is what makes the interval theirs — a CI run or a test
|
|
260
|
+
// harness sets it to 0 and the same loop runs without the wait.
|
|
261
|
+
const explicit = process.env.LETTERSTORY_POLL_INTERVAL_MS;
|
|
262
|
+
const intervalMs =
|
|
263
|
+
explicit !== undefined && explicit !== "" && Number.isFinite(Number(explicit))
|
|
264
|
+
? Number(explicit)
|
|
265
|
+
: (opts.intervalMs ?? 15000);
|
|
266
|
+
const ceilingMs = opts.ceilingMs ?? 30 * 60 * 1000;
|
|
267
|
+
const deadline = Date.now() + ceilingMs;
|
|
268
|
+
for (;;) {
|
|
269
|
+
await io.sleep(intervalMs);
|
|
270
|
+
const { done, value } = await poll();
|
|
271
|
+
if (done) return { settled: true, value };
|
|
272
|
+
if (Date.now() > deadline) return { settled: false, value };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
92
276
|
// Read a text value from an inline flag, or from a file flag (a path, or "-" for
|
|
93
277
|
// stdin). Returns undefined if neither flag was given, so callers can decide whether
|
|
94
278
|
// that's an error. `readBodyInput` is the --body/--file pairing; the propose lane reads
|
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
printResult,
|
|
14
14
|
compact,
|
|
15
15
|
ok,
|
|
16
|
+
toolField,
|
|
17
|
+
followJob,
|
|
16
18
|
} from "./shared.mjs";
|
|
17
19
|
|
|
18
20
|
export async function cmdShredder(ctx) {
|
|
@@ -83,8 +85,6 @@ async function shredderDelete(ctx) {
|
|
|
83
85
|
const POLL_INTERVAL_MS = 15_000;
|
|
84
86
|
const POLL_CEILING_MS = 30 * 60 * 1000;
|
|
85
87
|
|
|
86
|
-
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
87
|
-
|
|
88
88
|
async function shredderRun(ctx) {
|
|
89
89
|
const { client, positionals, flags, io } = ctx;
|
|
90
90
|
const id = requirePositional(positionals, 0, "endpoint-id");
|
|
@@ -105,6 +105,9 @@ async function shredderRun(ctx) {
|
|
|
105
105
|
text,
|
|
106
106
|
coverage: flagNum(flags.coverage),
|
|
107
107
|
attempts_per_unit: flagNum(flags.attempts),
|
|
108
|
+
// What the text IS. Without this an HTML file could only go through the
|
|
109
|
+
// `auto` heuristic, which misreads a markdown post carrying an embed.
|
|
110
|
+
...(flags.format ? { format: String(flags.format) } : {}),
|
|
108
111
|
// How hard to rewrite. Validated service-side; sent only when the caller asked.
|
|
109
112
|
...(flags.aggression ? { aggression: String(flags.aggression) } : {}),
|
|
110
113
|
// Hold every rewrite to the live writing rules. A trade, not a free win: refused rewrites
|
|
@@ -132,36 +135,35 @@ async function shredderRun(ctx) {
|
|
|
132
135
|
}
|
|
133
136
|
|
|
134
137
|
ok(ctx, `Shred queued as ${jobId}. Waiting — this keeps running on the server if you stop.`);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
ok(ctx, `Still ${status ?? "running"} after 30 minutes. Poll it with: letterstory shredder job ${jobId}`);
|
|
147
|
-
printResult(io, flags, polled);
|
|
148
|
-
return 0;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
138
|
+
let lastStatus;
|
|
139
|
+
const { settled, value: polled } = await followJob(
|
|
140
|
+
io,
|
|
141
|
+
async () => {
|
|
142
|
+
const result = await client.callTool("get_shred_job", { job_id: jobId });
|
|
143
|
+
lastStatus = readStatus(result);
|
|
144
|
+
const terminal = !!lastStatus && lastStatus !== "queued" && lastStatus !== "running";
|
|
145
|
+
return { done: terminal, value: result };
|
|
146
|
+
},
|
|
147
|
+
{ intervalMs: POLL_INTERVAL_MS, ceilingMs: POLL_CEILING_MS }
|
|
148
|
+
);
|
|
152
149
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
try {
|
|
158
|
-
return JSON.parse(raw)?.[field];
|
|
159
|
-
} catch {
|
|
160
|
-
return undefined;
|
|
150
|
+
if (!settled) {
|
|
151
|
+
ok(ctx, `Still ${lastStatus ?? "running"} after 30 minutes. Poll it with: letterstory shredder job ${jobId}`);
|
|
152
|
+
printResult(io, flags, polled);
|
|
153
|
+
return 0;
|
|
161
154
|
}
|
|
155
|
+
ok(ctx, `Shred ${lastStatus}.`);
|
|
156
|
+
// The operator followed this job to get their DOCUMENT, so print the document
|
|
157
|
+
// and not the envelope around it. `--json` still gets the whole payload, which
|
|
158
|
+
// is what a script piping this wants.
|
|
159
|
+
printResult(io, flags, polled, (v) => (typeof v?.text === "string" ? v.text : JSON.stringify(v, null, 2)));
|
|
160
|
+
// A job that landed as error or degraded is not a success, and a script that
|
|
161
|
+
// pipes this needs to know without parsing the payload.
|
|
162
|
+
return lastStatus === "done" ? 0 : 1;
|
|
162
163
|
}
|
|
163
|
-
|
|
164
|
-
const
|
|
164
|
+
|
|
165
|
+
const readJobId = (result) => toolField(result, "job_id");
|
|
166
|
+
const readStatus = (result) => toolField(result, "status");
|
|
165
167
|
|
|
166
168
|
async function shredderJob(ctx) {
|
|
167
169
|
const { client, positionals, flags, io } = ctx;
|
|
@@ -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
|
}
|