@letterstory/cli 0.134.1 → 0.136.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 +1 -0
- package/lib/cli.mjs +13 -2
- package/lib/commands/admin.mjs +76 -0
- package/lib/commands/enclosures.mjs +19 -0
- package/lib/commands/repo.mjs +4 -0
- package/lib/commands/site.mjs +47 -2
- package/lib/commands/strategy.mjs +30 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -254,6 +254,7 @@ letterstory strategy company set --name "Acme" --domain acme.com --manifesto "
|
|
|
254
254
|
|
|
255
255
|
letterstory strategy positioning get
|
|
256
256
|
letterstory strategy positioning set --topic seo --topic ai --stance "…" --clear-avoid
|
|
257
|
+
letterstory strategy positioning set --company-kind devtools --stage seed --motion product-led --category "AI visibility" # the company model; `none` clears a field
|
|
257
258
|
|
|
258
259
|
letterstory strategy competitors list
|
|
259
260
|
letterstory strategy competitors add "Rival Co" rival.com
|
package/lib/cli.mjs
CHANGED
|
@@ -62,7 +62,7 @@ import {
|
|
|
62
62
|
} from "./commands.mjs";
|
|
63
63
|
|
|
64
64
|
// Keep in sync with cli/package.json.
|
|
65
|
-
export const VERSION = "0.
|
|
65
|
+
export const VERSION = "0.136.0";
|
|
66
66
|
|
|
67
67
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
68
68
|
// can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
|
|
@@ -288,6 +288,8 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
288
288
|
"todo",
|
|
289
289
|
// admin spend margin: list nested work (never billed twice) beside its parent.
|
|
290
290
|
"nested",
|
|
291
|
+
// admin billing lago catalog --apply --include-drafts
|
|
292
|
+
"include-drafts",
|
|
291
293
|
]);
|
|
292
294
|
|
|
293
295
|
// Tiny argv parser: `--flag value`, `--flag=value`, boolean `--flag`, and positionals.
|
|
@@ -908,6 +910,13 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
|
|
|
908
910
|
cover what the app ledger cannot see; a CSV
|
|
909
911
|
import sums Concentrate's log export.
|
|
910
912
|
Needs spend:read (reads) / spend:write (writes).
|
|
913
|
+
admin billing lago catalog [--apply] [--include-drafts]
|
|
914
|
+
| provision <org-id> --plan starter|growth|scale [--ratio 1|2|4|8 (8 needs --why "…")] [--complimentary N] [--stripe-customer cus_…] [--start ISO] [--review D]
|
|
915
|
+
| reconcile [--org <org-id>] | forward
|
|
916
|
+
Usage billing on Lago: the plan catalog, an org's
|
|
917
|
+
plan and gifted ratio, Lago's tokens against ours,
|
|
918
|
+
and forwarding usage now. Needs spend:read (reads)
|
|
919
|
+
/ billing:write (writes).
|
|
911
920
|
admin crm import <file.csv> [--source <name>] [--tag "…"] [--dry-run]
|
|
912
921
|
| capture [shot.png …] [--text "…" | --text-file f] [--context "this is for acme.com"]
|
|
913
922
|
[--account <id>] [--dry-run] [--retry] screenshots / a thread / a note → a lead
|
|
@@ -1372,11 +1381,13 @@ Repo changes (Claude Code edits your repo in a sandbox; the Letterstory GitHub A
|
|
|
1372
1381
|
[--branch <base>] [--attach <path>]... [--attach-url <name>=<https url>]...
|
|
1373
1382
|
[--attach-ref <path>]... [--attach-url-ref <name>=<https url>]...
|
|
1374
1383
|
[--action build|build_pr|build_merge|slack|linear] [--with-preview]
|
|
1375
|
-
[--harness claude-code|letterpress-1] [--wait]
|
|
1384
|
+
[--harness claude-code|letterpress-1] [--verifier|--no-verifier] [--wait]
|
|
1376
1385
|
Start a change (attached files go to the sandbox;
|
|
1377
1386
|
-ref ones are context, never put in the repo);
|
|
1378
1387
|
--with-preview keeps a preview up next to the PR;
|
|
1379
1388
|
--harness picks the coding agent (default: the deployment's);
|
|
1389
|
+
--verifier reviews the change and has the agent fix
|
|
1390
|
+
what's blocking before it goes out;
|
|
1380
1391
|
--wait follows it to the PR
|
|
1381
1392
|
repo fix-audit --issue <issue-type>... [--audit <id>] [--branch <base>] [--action build|build_pr|build_merge|slack|linear] [--wait]
|
|
1382
1393
|
One PR fixing site-audit issues (repeat --issue to bundle) on every affected page,
|
package/lib/commands/admin.mjs
CHANGED
|
@@ -4996,12 +4996,88 @@ async function cmdAdminRecipes(ctx) {
|
|
|
4996
4996
|
}
|
|
4997
4997
|
}
|
|
4998
4998
|
|
|
4999
|
+
const LAGO = "/api/superadmin/billing/lago";
|
|
5000
|
+
const LAGO_USAGE = `Usage: admin billing lago <command>
|
|
5001
|
+
catalog [--apply] [--include-drafts] what the plan catalog would change in Lago; --apply makes it so
|
|
5002
|
+
provision <org-id> --plan starter [--ratio 4] [--why "…" above 4x] [--complimentary 20000] [--stripe-customer cus_…] [--start 2026-11-02T00:00:00Z] [--review 2027-05-01]
|
|
5003
|
+
reconcile [--org <org-id>] Lago's tokens this period against what we rated and forwarded
|
|
5004
|
+
forward forward billable usage to Lago now (the cron's pass)`;
|
|
5005
|
+
|
|
5006
|
+
/** admin billing lago … — staff operations on Lago (docs/billing/lago-integration.md). */
|
|
5007
|
+
async function cmdAdminBilling(ctx) {
|
|
5008
|
+
const { flags, positionals, io } = ctx;
|
|
5009
|
+
if (positionals[0] !== "lago") throw new CliError(LAGO_USAGE);
|
|
5010
|
+
const sub = positionals[1];
|
|
5011
|
+
switch (sub) {
|
|
5012
|
+
case "catalog": {
|
|
5013
|
+
const result = flagBool(flags.apply)
|
|
5014
|
+
? await staffPost(
|
|
5015
|
+
ctx,
|
|
5016
|
+
`${LAGO}/catalog`,
|
|
5017
|
+
compact({ includeDrafts: flagBool(flags["include-drafts"]) || undefined })
|
|
5018
|
+
)
|
|
5019
|
+
: await staffRequest(ctx, "GET", `${LAGO}/catalog`);
|
|
5020
|
+
const changed = (result.changes ?? []).filter((c) => c.action === "create" || c.action === "update");
|
|
5021
|
+
ok(
|
|
5022
|
+
ctx,
|
|
5023
|
+
`${result.dryRun ? "Would change" : "Changed"} ${changed.length} of ${(result.changes ?? []).length}.`
|
|
5024
|
+
);
|
|
5025
|
+
printResult(io, flags, result);
|
|
5026
|
+
return 0;
|
|
5027
|
+
}
|
|
5028
|
+
case "provision": {
|
|
5029
|
+
const orgId = requirePositional(positionals, 2, "org-id");
|
|
5030
|
+
const plan = flagStr(flags.plan);
|
|
5031
|
+
if (!plan) throw new CliError(LAGO_USAGE);
|
|
5032
|
+
const result = await staffPost(
|
|
5033
|
+
ctx,
|
|
5034
|
+
`${LAGO}/provision`,
|
|
5035
|
+
compact({
|
|
5036
|
+
orgId,
|
|
5037
|
+
planCode: plan,
|
|
5038
|
+
ratio: flagNum(flags.ratio),
|
|
5039
|
+
exceptionReason: flagStr(flags.why),
|
|
5040
|
+
complimentaryTokens: flagNum(flags.complimentary),
|
|
5041
|
+
stripeCustomerId: flagStr(flags["stripe-customer"]),
|
|
5042
|
+
startAt: flagStr(flags.start),
|
|
5043
|
+
reviewAt: flagStr(flags.review),
|
|
5044
|
+
})
|
|
5045
|
+
);
|
|
5046
|
+
ok(
|
|
5047
|
+
ctx,
|
|
5048
|
+
`${result.action === "created" ? "On Lago" : "Already on Lago"}: ${result.subscriptionId}, ${result.includedTokens} tokens included, overage $${result.overageUsdPerToken} a token, from ${result.startsAt}.`
|
|
5049
|
+
);
|
|
5050
|
+
printResult(io, flags, result);
|
|
5051
|
+
return 0;
|
|
5052
|
+
}
|
|
5053
|
+
case "reconcile": {
|
|
5054
|
+
const params = new URLSearchParams(compact({ orgId: flagStr(flags.org) }));
|
|
5055
|
+
const result = await staffRequest(ctx, "GET", `${LAGO}/reconcile${params.size ? `?${params}` : ""}`);
|
|
5056
|
+
ok(ctx, result.ok ? "Lago agrees with us." : "Lago and we disagree: see the orgs below.");
|
|
5057
|
+
printResult(io, flags, result);
|
|
5058
|
+
return result.ok ? 0 : 1;
|
|
5059
|
+
}
|
|
5060
|
+
case "forward": {
|
|
5061
|
+
const result = await staffPost(ctx, `${LAGO}/forward`, {});
|
|
5062
|
+
ok(
|
|
5063
|
+
ctx,
|
|
5064
|
+
`Sent ${result.sent}, already there ${result.duplicates}, held ${result.held}, refused ${result.refused}.`
|
|
5065
|
+
);
|
|
5066
|
+
printResult(io, flags, result);
|
|
5067
|
+
return 0;
|
|
5068
|
+
}
|
|
5069
|
+
default:
|
|
5070
|
+
throw new CliError(LAGO_USAGE);
|
|
5071
|
+
}
|
|
5072
|
+
}
|
|
5073
|
+
|
|
4999
5074
|
export async function cmdAdmin(ctx) {
|
|
5000
5075
|
const sub = ctx.positionals[0];
|
|
5001
5076
|
if (sub === "replays" || sub === "replay")
|
|
5002
5077
|
return cmdAdminReplays({ ...ctx, positionals: ctx.positionals.slice(1) }, sub);
|
|
5003
5078
|
if (sub === "crm") return cmdAdminCrm({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
5004
5079
|
if (sub === "spend") return cmdAdminSpend({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
5080
|
+
if (sub === "billing") return cmdAdminBilling({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
5005
5081
|
if (sub === "models") return cmdAdminModels({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
5006
5082
|
if (sub === "create-org") return cmdAdminCreateOrg({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
5007
5083
|
if (sub === "ask") return cmdAdminAsk({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
@@ -110,6 +110,10 @@ export const ARGS = {
|
|
|
110
110
|
...fields,
|
|
111
111
|
},
|
|
112
112
|
list_generator_starters: {},
|
|
113
|
+
write_generator_steps: {
|
|
114
|
+
generator_id: { positional: 0, label: "enclosure-id", required: true },
|
|
115
|
+
prospect_id: "prospect",
|
|
116
|
+
},
|
|
113
117
|
get_generator_version: {
|
|
114
118
|
generator_id: { positional: 0, label: "enclosure-id", required: true },
|
|
115
119
|
version: { value: (_flags, positionals) => Number(requirePositional(positionals, 1, "version")) },
|
|
@@ -177,6 +181,7 @@ const USAGE = `Usage: enclosures <list|show|create|update|prints>
|
|
|
177
181
|
enclosures prints <enclosure-id> [--status done|failed|no_fit|…] [--limit <n>]
|
|
178
182
|
enclosures starters
|
|
179
183
|
enclosures create --starter site_audit --login <app-id> Begin from a starter (its version 1 included)
|
|
184
|
+
enclosures write-steps <enclosure-id> [--prospect <id>] Claude Code writes its steps from its description
|
|
180
185
|
enclosures version <enclosure-id> <n> One version in full: its steps and the script prints replay
|
|
181
186
|
enclosures add-version <enclosure-id> (--script <code>|--script-file <path>) --step <text>… [--host <h>…] [--summary <s>]
|
|
182
187
|
enclosures approve <enclosure-id> --version <n>
|
|
@@ -208,6 +213,8 @@ export async function cmdEnclosures(ctx) {
|
|
|
208
213
|
return enclosuresStarters(rest);
|
|
209
214
|
case "version":
|
|
210
215
|
return enclosuresVersion(rest);
|
|
216
|
+
case "write-steps":
|
|
217
|
+
return enclosuresWriteSteps(rest);
|
|
211
218
|
case "add-version":
|
|
212
219
|
return enclosuresAddVersion(rest);
|
|
213
220
|
case "approve":
|
|
@@ -353,6 +360,18 @@ async function enclosuresStarters(ctx) {
|
|
|
353
360
|
return 0;
|
|
354
361
|
}
|
|
355
362
|
|
|
363
|
+
async function enclosuresWriteSteps(ctx) {
|
|
364
|
+
const { client, flags, positionals, io } = ctx;
|
|
365
|
+
refuseUnknownFlags(ctx, ARGS.write_generator_steps, []);
|
|
366
|
+
const result = await client.callTool(
|
|
367
|
+
"write_generator_steps",
|
|
368
|
+
argsFromFlags(ARGS.write_generator_steps, flags, positionals)
|
|
369
|
+
);
|
|
370
|
+
ok(ctx, "Claude Code is writing the steps. It takes a few minutes: watch it with `enclosures show`.");
|
|
371
|
+
printResult(io, flags, result, (r) => `run ${r.run.id} (${r.run.phase}), budget $${r.run.budget_usd}`);
|
|
372
|
+
return 0;
|
|
373
|
+
}
|
|
374
|
+
|
|
356
375
|
async function enclosuresVersion(ctx) {
|
|
357
376
|
const { client, flags, positionals, io } = ctx;
|
|
358
377
|
refuseUnknownFlags(ctx, ARGS.get_generator_version, []);
|
package/lib/commands/repo.mjs
CHANGED
|
@@ -68,6 +68,8 @@ const ARGS = {
|
|
|
68
68
|
with_preview: { flag: "with-preview", type: "bool" },
|
|
69
69
|
// --harness claude-code|letterpress-1: the coding agent; omitted, the deployment's default.
|
|
70
70
|
harness: { flag: "harness", enum: ["claude-code", "letterpress-1"] },
|
|
71
|
+
// --verifier / --no-verifier: review the change before it goes out; omitted, the deployment's choice.
|
|
72
|
+
verifier: { value: (flags) => (flags["no-verifier"] ? false : flags.verifier ? true : undefined) },
|
|
71
73
|
},
|
|
72
74
|
create_repo_change_upload: { file_names: { internal: "the basenames of the --attach files" } },
|
|
73
75
|
list_repo_sites: {},
|
|
@@ -341,6 +343,8 @@ async function repoStart(ctx) {
|
|
|
341
343
|
"preview",
|
|
342
344
|
"with-preview",
|
|
343
345
|
"harness",
|
|
346
|
+
"verifier",
|
|
347
|
+
"no-verifier",
|
|
344
348
|
"wait",
|
|
345
349
|
]);
|
|
346
350
|
// One upload for every file (names must be unique across both), then the references marked.
|
package/lib/commands/site.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// (src/lib/site). One subcommand per tool:
|
|
3
3
|
// site list → list_sites site map <host> → map_site
|
|
4
4
|
// site tree <host> [path] → get_site_tree site page <host> <path> → get_site_page
|
|
5
|
+
// site builds <host> → list_build_suggestions (--limit n)
|
|
5
6
|
// site metrics <host> [path] → get_site_metrics (--window 7d|28d|90d, --page <path>…, --no-compare)
|
|
6
7
|
// site observations <host> → list_site_observations site observe <host> → observe_site
|
|
7
8
|
// site dismiss <host> <id> → dismiss_site_observation
|
|
@@ -78,6 +79,10 @@ const ARGS = {
|
|
|
78
79
|
},
|
|
79
80
|
},
|
|
80
81
|
},
|
|
82
|
+
list_build_suggestions: {
|
|
83
|
+
host: { positional: 0, required: true, label: "host" },
|
|
84
|
+
limit: { flag: "limit", type: "num" },
|
|
85
|
+
},
|
|
81
86
|
get_site_metrics: {
|
|
82
87
|
host: { positional: 0, required: true, label: "host" },
|
|
83
88
|
path: { positional: 1, label: "path" },
|
|
@@ -199,6 +204,8 @@ export async function cmdSite(ctx) {
|
|
|
199
204
|
return siteUnmap(rest);
|
|
200
205
|
case "tree":
|
|
201
206
|
return siteTree(rest);
|
|
207
|
+
case "builds":
|
|
208
|
+
return siteBuilds(rest);
|
|
202
209
|
case "metrics":
|
|
203
210
|
return siteMetrics(rest);
|
|
204
211
|
case "page":
|
|
@@ -229,7 +236,7 @@ export async function cmdSite(ctx) {
|
|
|
229
236
|
return siteHighlight(rest);
|
|
230
237
|
default:
|
|
231
238
|
throw new CliError(
|
|
232
|
-
`Unknown site subcommand: ${sub ?? "(none)"}. Try: list, map, refresh, unmap, tree, metrics, page, observations, observe, dismiss, families, decide-family, proposals, propose, decide, fix, competitors, watch-competitors, highlight`
|
|
239
|
+
`Unknown site subcommand: ${sub ?? "(none)"}. Try: list, map, refresh, unmap, tree, builds, metrics, page, observations, observe, dismiss, families, decide-family, proposals, propose, decide, fix, competitors, watch-competitors, highlight`
|
|
233
240
|
);
|
|
234
241
|
}
|
|
235
242
|
}
|
|
@@ -243,7 +250,26 @@ function describeNode(node, indent = "") {
|
|
|
243
250
|
: "";
|
|
244
251
|
const changed = `${node.rollup.added ? ` +${node.rollup.added}` : ""}${node.rollup.removed ? ` -${node.rollup.removed}` : ""}`;
|
|
245
252
|
const counts = node.hasChildren ? ` (${node.rollup.pages} pages${changed})` : node.page?.added ? " (new)" : "";
|
|
246
|
-
|
|
253
|
+
// A folder's measured pages, only where the source is measured (null draws nothing: never zero).
|
|
254
|
+
const m = node.rollup.measured ?? {};
|
|
255
|
+
const measured = [
|
|
256
|
+
m.indexed != null ? `${m.indexed} indexed` : null,
|
|
257
|
+
m.cited != null ? `${m.cited} cited` : null,
|
|
258
|
+
m.read != null ? `${m.read} read` : null,
|
|
259
|
+
].filter(Boolean);
|
|
260
|
+
const coverage = node.rollup.coverage
|
|
261
|
+
? `${node.rollup.coverage.covered} of ${node.rollup.coverage.total} covered`
|
|
262
|
+
: null;
|
|
263
|
+
const rollup =
|
|
264
|
+
node.hasChildren && (measured.length || coverage)
|
|
265
|
+
? ` [${[...measured, coverage].filter(Boolean).join(" · ")}]`
|
|
266
|
+
: "";
|
|
267
|
+
const out = [`${indent}${glyph.padEnd(2)} ${node.name}${counts}${rollup}${line ? ` ${line}` : ""}`];
|
|
268
|
+
// What is next in the pattern the folder keeps: violet on the Site, its state the letterbase's.
|
|
269
|
+
if (node.next) {
|
|
270
|
+
const owner = node.next.owner ? `${node.next.owner.name} · ${node.next.state}` : "next in the pattern";
|
|
271
|
+
out.push(`${indent} ◇ ${node.next.path ?? node.next.title} ${node.next.why} (${owner})`);
|
|
272
|
+
}
|
|
247
273
|
for (const child of node.children ?? []) out.push(describeNode(child, `${indent} `));
|
|
248
274
|
return out.join("\n");
|
|
249
275
|
}
|
|
@@ -366,6 +392,25 @@ async function siteTree(ctx) {
|
|
|
366
392
|
return 0;
|
|
367
393
|
}
|
|
368
394
|
|
|
395
|
+
async function siteBuilds(ctx) {
|
|
396
|
+
const { client, flags, io } = ctx;
|
|
397
|
+
refuseUnknownFlags(ctx, ARGS.list_build_suggestions);
|
|
398
|
+
const result = await client.callTool(
|
|
399
|
+
"list_build_suggestions",
|
|
400
|
+
argsFromFlags(ARGS.list_build_suggestions, flags, ctx.positionals)
|
|
401
|
+
);
|
|
402
|
+
printResult(io, flags, result, (r) => {
|
|
403
|
+
const lines = r.suggestions.map(
|
|
404
|
+
(s) =>
|
|
405
|
+
`${s.summary} [${s.sources.join(", ")}]${s.action === "article" ? " (an article: the letterbase's)" : ""}\n ${s.relevance}\n ${s.id}`
|
|
406
|
+
);
|
|
407
|
+
if (!lines.length) lines.push("Nothing to build: no source makes a surface this site's right now.");
|
|
408
|
+
if (r.skipped.length) lines.push(`Not read: ${r.skipped.join(", ")}`);
|
|
409
|
+
return lines.join("\n");
|
|
410
|
+
});
|
|
411
|
+
return 0;
|
|
412
|
+
}
|
|
413
|
+
|
|
369
414
|
async function siteMetrics(ctx) {
|
|
370
415
|
const { client, flags, io } = ctx;
|
|
371
416
|
refuseUnknownFlags(ctx, ARGS.get_site_metrics);
|
|
@@ -16,6 +16,18 @@ const clearableList = (flag, clearFlag) => ({
|
|
|
16
16
|
},
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
+
// A scalar the API clears with an explicit null: `--stage none` sends null, a value sends it
|
|
20
|
+
// (one of `values` when given), nothing sends nothing.
|
|
21
|
+
const clearable = (flag, values) => ({
|
|
22
|
+
value: (flags) => {
|
|
23
|
+
const v = flagStr(flags[flag]);
|
|
24
|
+
if (v === undefined || v === null || v === "") return undefined;
|
|
25
|
+
if (v === "none") return null;
|
|
26
|
+
if (values && !values.includes(v)) throw new CliError(`--${flag} must be one of: ${values.join(", ")}, none`);
|
|
27
|
+
return v;
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
19
31
|
// How every tool this module calls gets its arguments — see `argsFromFlags` in shared.mjs.
|
|
20
32
|
// `scripts/parity-check.ts` compares these keys against each tool's published input schema.
|
|
21
33
|
export const ARGS = {
|
|
@@ -40,6 +52,21 @@ export const ARGS = {
|
|
|
40
52
|
priority_topics: clearableList("topic", "clear-topics"),
|
|
41
53
|
stances: clearableList("stance", "clear-stances"),
|
|
42
54
|
avoid_topics: clearableList("avoid", "clear-avoid"),
|
|
55
|
+
// The company model (lib/positioning/company-model.ts): a vocabulary word, or "none" to clear.
|
|
56
|
+
company_kind: clearable("company-kind", [
|
|
57
|
+
"b2b_saas",
|
|
58
|
+
"devtools",
|
|
59
|
+
"ai_product",
|
|
60
|
+
"ecommerce",
|
|
61
|
+
"marketplace",
|
|
62
|
+
"agency",
|
|
63
|
+
"consumer",
|
|
64
|
+
"media",
|
|
65
|
+
"local_business",
|
|
66
|
+
]),
|
|
67
|
+
stage: clearable("stage", ["pre-seed", "seed", "series-a-plus", "established"]),
|
|
68
|
+
motion: clearable("motion", ["self-serve", "sales-led", "product-led", "open-source"]),
|
|
69
|
+
category: clearable("category"),
|
|
43
70
|
},
|
|
44
71
|
list_competitors: {},
|
|
45
72
|
add_competitor: {
|
|
@@ -155,7 +182,9 @@ async function positioningSet(ctx) {
|
|
|
155
182
|
const { client, flags, io } = ctx;
|
|
156
183
|
const args = argsFromFlags(ARGS.set_positioning, flags);
|
|
157
184
|
if (Object.keys(args).length === 0) {
|
|
158
|
-
throw new CliError(
|
|
185
|
+
throw new CliError(
|
|
186
|
+
"Pass at least one --topic/--stance/--avoid (or a --clear-* flag), or --company-kind/--stage/--motion/--category (none clears)."
|
|
187
|
+
);
|
|
159
188
|
}
|
|
160
189
|
const result = await client.callTool("set_positioning", args);
|
|
161
190
|
ok(ctx, "Positioning updated.");
|