@letterstory/cli 0.39.0 → 0.40.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 +20 -0
- package/lib/cli.mjs +27 -2
- package/lib/commands/collection.mjs +84 -2
- package/lib/commands/outbound.mjs +6 -1
- package/lib/commands/revisions.mjs +81 -0
- package/lib/commands/targets.mjs +223 -0
- package/lib/commands.mjs +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -172,6 +172,9 @@ letterstory collection run-status <run-id> # status, steps,
|
|
|
172
172
|
letterstory collection runs --collection <id> --status ready
|
|
173
173
|
letterstory collection approve <run-id> --now
|
|
174
174
|
letterstory collection cancel <run-id>
|
|
175
|
+
letterstory collection inventory <id> # every piece; each finished draft's publish-on-its-own time
|
|
176
|
+
letterstory collection links <id> # internal-link refresh: on/off, posts due
|
|
177
|
+
letterstory collection links <id> --refresh # run the next batch now
|
|
175
178
|
letterstory collection delete <id> --yes
|
|
176
179
|
|
|
177
180
|
# the legacy verbs still work under `collections`
|
|
@@ -245,6 +248,23 @@ letterstory onboarding status
|
|
|
245
248
|
letterstory onboarding step --complete connect_domain
|
|
246
249
|
```
|
|
247
250
|
|
|
251
|
+
### What needs you — the collection's "needs you" strip, from the terminal
|
|
252
|
+
|
|
253
|
+
Everything in the strip either applies itself after its countdown or waits for your
|
|
254
|
+
decision; the same reads and decisions are here.
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
letterstory suggestions list <collection-id> # strategy cards + when each applies itself
|
|
258
|
+
letterstory suggestions dismiss proposal <id> # reject one inside its window
|
|
259
|
+
letterstory collection inventory <collection-id> # finished drafts + when each publishes on its own
|
|
260
|
+
letterstory collection links <collection-id> # internal links due for a refresh
|
|
261
|
+
|
|
262
|
+
# proposed edits to live posts: a mention an AI citation earned, or fresher internal links
|
|
263
|
+
letterstory revisions list --collection <collection-id>
|
|
264
|
+
letterstory revisions accept <revision-id> # every change; live where we publish
|
|
265
|
+
letterstory revisions reject <revision-id> --change <change-id> --note "not this one"
|
|
266
|
+
```
|
|
267
|
+
|
|
248
268
|
### Offerings — one org, many products
|
|
249
269
|
|
|
250
270
|
Context is layered **org → offering → collection**. An offering is anything the org would
|
package/lib/cli.mjs
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
cmdDirectives,
|
|
28
28
|
cmdProbes,
|
|
29
29
|
cmdSuggestions,
|
|
30
|
+
cmdRevisions,
|
|
30
31
|
cmdOnboarding,
|
|
31
32
|
cmdSeries,
|
|
32
33
|
cmdInsights,
|
|
@@ -47,10 +48,11 @@ import {
|
|
|
47
48
|
cmdAdmin,
|
|
48
49
|
cmdFeedback,
|
|
49
50
|
cmdOfferings,
|
|
51
|
+
cmdTargets,
|
|
50
52
|
} from "./commands.mjs";
|
|
51
53
|
|
|
52
54
|
// Keep in sync with cli/package.json.
|
|
53
|
-
export const VERSION = "0.
|
|
55
|
+
export const VERSION = "0.40.0";
|
|
54
56
|
|
|
55
57
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
56
58
|
// can't accidentally swallow the id as --json's value.
|
|
@@ -116,6 +118,8 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
116
118
|
"none",
|
|
117
119
|
// `prospects import|source --one-per-company`.
|
|
118
120
|
"one-per-company",
|
|
121
|
+
// `targets update <id> --clear-framing`.
|
|
122
|
+
"clear-framing",
|
|
119
123
|
]);
|
|
120
124
|
|
|
121
125
|
// Tiny argv parser: `--flag value`, `--flag=value`, boolean `--flag`, and positionals.
|
|
@@ -205,6 +209,8 @@ Collections (the content engine as a primitive — one door, 'collection run'):
|
|
|
205
209
|
collection runs [--collection <id>] [--status <s>] [--limit <n>]
|
|
206
210
|
collection approve <run-id> [--now] Answer the gate (--now publishes immediately)
|
|
207
211
|
collection cancel <run-id>
|
|
212
|
+
collection inventory <id> Where every piece is; each finished draft's publish-on-its-own time
|
|
213
|
+
collection links <id> [--refresh] Internal-link refresh: on/off, posts due (--refresh runs the next batch now)
|
|
208
214
|
collection delete <id> --yes
|
|
209
215
|
|
|
210
216
|
Phantomstory (a collection Letterstory hosts as an independent publication):
|
|
@@ -303,6 +309,9 @@ Strategy & onboarding:
|
|
|
303
309
|
strategy topics set --collection <uuid> (--topic <topic-id> | --suggestion <suggestion-id>)
|
|
304
310
|
suggestions list <collection_id> List pending strategy suggestions + their auto-apply countdown
|
|
305
311
|
suggestions approve | dismiss | snooze <verdict|proposal> <id> [--days <n>]
|
|
312
|
+
revisions list [--collection <uuid>] Proposed edits to live posts waiting for a decision
|
|
313
|
+
revisions accept | reject <revision-id> [--change <change-id>] [--note <text>]
|
|
314
|
+
Decide one change or all; accepting goes live where we publish
|
|
306
315
|
directives list <collection_id> What is steering this collection right now
|
|
307
316
|
directives propose <collection_id> <what you want> Read one instruction back as concrete levers (applies nothing)
|
|
308
317
|
directives apply <directive_id> --effect <id>... Commit the levers you kept (or --reason to decline them all)
|
|
@@ -344,6 +353,19 @@ Offerings (context layered org → offering → collection; a layer sets only wh
|
|
|
344
353
|
series create --name <n> --description <d> [--coverage a,b,c] [--collection <uuid>] [--template <key>]
|
|
345
354
|
series backfill <series-id> [--collection <uuid>] Reconstruct a series from existing posts
|
|
346
355
|
|
|
356
|
+
Ranking targets (what the org wants to be THE answer for — company-wide or on one offering):
|
|
357
|
+
targets list [--offering <offering-id|company>] Targets grouped by scope, each with its active cap and usage
|
|
358
|
+
targets add <phrase> [--framing <f>] [--offering <offering-id|company>]
|
|
359
|
+
Lands active while its scope has room, inactive after —
|
|
360
|
+
adding never fails on the cap
|
|
361
|
+
targets update <target-id> [--phrase <p>] [--framing <f> | --clear-framing] [--offering <offering-id|company>]
|
|
362
|
+
[--position <n>] [--state active|inactive|achieved|retired]
|
|
363
|
+
Moving an ACTIVE target needs room in the new scope
|
|
364
|
+
targets activate | deactivate <target-id> Switch a target on/off; activating is refused at the cap
|
|
365
|
+
targets cap <offering-id|company> <n|default> Active targets allowed in that scope (preset 5; default =
|
|
366
|
+
inherit the company's cap / back to the preset)
|
|
367
|
+
targets suggest [--offering <offering-id|company>] Evidence-informed suggestions (nothing saved)
|
|
368
|
+
|
|
347
369
|
Arenas (adversarial clusters of research agents -> a sourced, fact-checked outline):
|
|
348
370
|
arena run --topic <text> [--source <url> …] [--must-include <text> …] [--depth barebones|fleshed_out]
|
|
349
371
|
[--gate] [--no-verify] [--provider native|exa|youcom|firecrawl] [--mention off|subtle|woven]
|
|
@@ -740,7 +762,8 @@ Prospects (who you write to):
|
|
|
740
762
|
[--city] [--country] [--years] [--notes] [--custom key=value]...
|
|
741
763
|
|
|
742
764
|
Mail:
|
|
743
|
-
mail connections Connected mailboxes, Google connect
|
|
765
|
+
mail connections Connected mailboxes, Google connect, Vercel key + mailing address status
|
|
766
|
+
mail postal-address "<address>" Set the org's CAN-SPAM mailing address (email footers)
|
|
744
767
|
|
|
745
768
|
Insights:
|
|
746
769
|
insights site [--period 14d|30d|90d] [--collection <uuid>]
|
|
@@ -806,8 +829,10 @@ const CLIENT_COMMANDS = {
|
|
|
806
829
|
strategy: cmdStrategy,
|
|
807
830
|
directives: cmdDirectives,
|
|
808
831
|
offerings: cmdOfferings,
|
|
832
|
+
targets: cmdTargets,
|
|
809
833
|
probes: cmdProbes,
|
|
810
834
|
suggestions: cmdSuggestions,
|
|
835
|
+
revisions: cmdRevisions,
|
|
811
836
|
onboarding: cmdOnboarding,
|
|
812
837
|
series: cmdSeries,
|
|
813
838
|
insights: cmdInsights,
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { readFileSync } from "node:fs";
|
|
12
12
|
import { CliError } from "../client.mjs";
|
|
13
|
-
import { flagStr, printResult, ok, toolField, followJob, argsFromFlags } from "./shared.mjs";
|
|
13
|
+
import { flagStr, printResult, ok, toolField, followJob, argsFromFlags, refuseUnknownFlags } from "./shared.mjs";
|
|
14
14
|
|
|
15
15
|
function readJsonFileFlag(flags, flag) {
|
|
16
16
|
const path = flagStr(flags[flag]);
|
|
@@ -114,6 +114,15 @@ export const ARGS = {
|
|
|
114
114
|
cancel_collection_run: {
|
|
115
115
|
run_id: { positional: 0, label: "run-id", required: true },
|
|
116
116
|
},
|
|
117
|
+
get_collection_inventory: {
|
|
118
|
+
collection_id: { positional: 0, label: "collection-id", required: true },
|
|
119
|
+
},
|
|
120
|
+
get_internal_link_status: {
|
|
121
|
+
collection_id: { positional: 0, label: "collection-id", required: true },
|
|
122
|
+
},
|
|
123
|
+
refresh_internal_links: {
|
|
124
|
+
collection_id: { positional: 0, label: "collection-id", required: true },
|
|
125
|
+
},
|
|
117
126
|
};
|
|
118
127
|
|
|
119
128
|
export async function cmdCollection(ctx) {
|
|
@@ -142,9 +151,13 @@ export async function cmdCollection(ctx) {
|
|
|
142
151
|
return collectionApprove(rest);
|
|
143
152
|
case "cancel":
|
|
144
153
|
return collectionCancel(rest);
|
|
154
|
+
case "inventory":
|
|
155
|
+
return collectionInventory(rest);
|
|
156
|
+
case "links":
|
|
157
|
+
return collectionLinks(rest);
|
|
145
158
|
default:
|
|
146
159
|
throw new CliError(
|
|
147
|
-
`Unknown collection subcommand: ${sub ?? "(none)"}. Try: list, new, config, delete, run, run-status, runs, approve, cancel`
|
|
160
|
+
`Unknown collection subcommand: ${sub ?? "(none)"}. Try: list, new, config, delete, run, run-status, runs, approve, cancel, inventory, links`
|
|
148
161
|
);
|
|
149
162
|
}
|
|
150
163
|
}
|
|
@@ -277,3 +290,72 @@ async function collectionCancel(ctx) {
|
|
|
277
290
|
printResult(ctx.io, ctx.flags, result);
|
|
278
291
|
return 0;
|
|
279
292
|
}
|
|
293
|
+
|
|
294
|
+
// `collection inventory <id>` — where every piece is, and each finished draft's
|
|
295
|
+
// publish-on-its-own time (the collection room's Work tab + "needs you" strip).
|
|
296
|
+
function formatInventory(r) {
|
|
297
|
+
const lines = [
|
|
298
|
+
`Planner: ${r.planner?.enabled ? "on" : "off"} (${r.planner?.approvalMode ?? "?"})`,
|
|
299
|
+
`Live ${r.live?.count ?? 0} · written, not live ${r.readyNotLive?.count ?? 0} · being written ${r.beingWritten?.count ?? 0} · imported ${r.imported?.count ?? 0}`,
|
|
300
|
+
];
|
|
301
|
+
const pending = r.pendingReview?.items ?? [];
|
|
302
|
+
if (pending.length) {
|
|
303
|
+
lines.push(`Finished drafts (${r.pendingReview.count}):`);
|
|
304
|
+
for (const d of pending) {
|
|
305
|
+
const when = d.autoPublishAt ? `publishes on its own ${d.autoPublishAt}` : "waits for a person";
|
|
306
|
+
lines.push(` ${d.articleId} ${d.title} [${when}]`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return lines.join("\n");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function collectionInventory(ctx) {
|
|
313
|
+
refuseUnknownFlags(ctx, {});
|
|
314
|
+
const result = await ctx.client.callTool(
|
|
315
|
+
"get_collection_inventory",
|
|
316
|
+
argsFromFlags(ARGS.get_collection_inventory, ctx.flags, ctx.positionals)
|
|
317
|
+
);
|
|
318
|
+
printResult(ctx.io, ctx.flags, result, formatInventory);
|
|
319
|
+
return 0;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// `collection links <id>` reads the internal-link refresh; `--refresh` runs the next
|
|
323
|
+
// batch now (republished, or proposed for approval on a manual-approval collection).
|
|
324
|
+
async function collectionLinks(ctx) {
|
|
325
|
+
const { client, flags, positionals, io } = ctx;
|
|
326
|
+
refuseUnknownFlags(ctx, {}, ["refresh"]);
|
|
327
|
+
if (flags.refresh === true) {
|
|
328
|
+
const r = await client.callTool(
|
|
329
|
+
"refresh_internal_links",
|
|
330
|
+
argsFromFlags(ARGS.refresh_internal_links, flags, positionals)
|
|
331
|
+
);
|
|
332
|
+
const parts = [];
|
|
333
|
+
if (r.posts_started) parts.push(`${r.posts_started} refreshing (each republishes when it finishes)`);
|
|
334
|
+
if (r.posts_proposed) parts.push(`${r.posts_proposed} proposed for approval (see: revisions list)`);
|
|
335
|
+
if (r.posts_skipped) parts.push(`${r.posts_skipped} busy with another run`);
|
|
336
|
+
if (!parts.length) {
|
|
337
|
+
parts.push(
|
|
338
|
+
r.skipped?.no_sitemap
|
|
339
|
+
? "no sitemap to link from"
|
|
340
|
+
: r.next_due_in_days === null
|
|
341
|
+
? "nothing due: every post already links to everything published since"
|
|
342
|
+
: `nothing due yet (next in ${r.next_due_in_days ?? "?"} days)`
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
ok(ctx, `Internal links: ${parts.join("; ")}.`);
|
|
346
|
+
if (flags.json) printResult(io, flags, r);
|
|
347
|
+
return 0;
|
|
348
|
+
}
|
|
349
|
+
const s = await client.callTool(
|
|
350
|
+
"get_internal_link_status",
|
|
351
|
+
argsFromFlags(ARGS.get_internal_link_status, flags, positionals)
|
|
352
|
+
);
|
|
353
|
+
printResult(io, flags, s, (r) =>
|
|
354
|
+
[
|
|
355
|
+
`Internal links: ${r.enabled ? "on" : "off"}`,
|
|
356
|
+
`Due now: ${r.due}`,
|
|
357
|
+
`Next due: ${r.next_due_in_days === null ? "only when something new publishes" : `in ${r.next_due_in_days} days`}`,
|
|
358
|
+
].join("\n")
|
|
359
|
+
);
|
|
360
|
+
return 0;
|
|
361
|
+
}
|
|
@@ -167,6 +167,9 @@ export const ARGS = {
|
|
|
167
167
|
custom: { value: readCustom },
|
|
168
168
|
},
|
|
169
169
|
get_mail_connections: {},
|
|
170
|
+
set_mail_postal_address: {
|
|
171
|
+
mail_postal_address: { positional: 0, label: "address", required: true },
|
|
172
|
+
},
|
|
170
173
|
};
|
|
171
174
|
|
|
172
175
|
// Flags the computed `value` sources read (refuseUnknownFlags can't see them).
|
|
@@ -318,7 +321,9 @@ export async function cmdMail(ctx) {
|
|
|
318
321
|
case "connections":
|
|
319
322
|
case "status":
|
|
320
323
|
return run(rest, "get_mail_connections");
|
|
324
|
+
case "postal-address":
|
|
325
|
+
return run(rest, "set_mail_postal_address", "Mailing address saved.");
|
|
321
326
|
default:
|
|
322
|
-
throw new CliError(`Unknown mail subcommand: ${sub ?? "(none)"}. Try: connections`);
|
|
327
|
+
throw new CliError(`Unknown mail subcommand: ${sub ?? "(none)"}. Try: connections, postal-address`);
|
|
323
328
|
}
|
|
324
329
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// `revisions` — proposed edits to already-live posts, waiting for a decision: the
|
|
2
|
+
// Approvals queue in a collection's "needs you" strip. A mention an AI citation
|
|
3
|
+
// earned (review-posture collection), or internal links the nightly refresh would
|
|
4
|
+
// add (manual-approval or client-CMS collection). Thin wrappers over the MCP tools
|
|
5
|
+
// list_revisions / decide_revision, which call the same lib/revisions functions the
|
|
6
|
+
// web queue does — accepting on a page we publish puts it live.
|
|
7
|
+
|
|
8
|
+
import { CliError } from "../client.mjs";
|
|
9
|
+
import { printResult, argsFromFlags, refuseUnknownFlags, ok } from "./shared.mjs";
|
|
10
|
+
|
|
11
|
+
const DECISIONS = ["accepted", "rejected"];
|
|
12
|
+
|
|
13
|
+
// How every tool this module calls gets its arguments — see `argsFromFlags` in shared.mjs.
|
|
14
|
+
// `scripts/parity-check.ts` compares these keys against each tool's published input schema.
|
|
15
|
+
export const ARGS = {
|
|
16
|
+
list_revisions: { collection_id: "collection" },
|
|
17
|
+
decide_revision: {
|
|
18
|
+
revision_id: { positional: 0, label: "revision-id", required: true },
|
|
19
|
+
// `accept` / `reject` set this for you.
|
|
20
|
+
decision: { flag: "decision", enum: DECISIONS },
|
|
21
|
+
change_id: "change",
|
|
22
|
+
note: "note",
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const USAGE = `Usage: revisions <list|accept|reject>
|
|
27
|
+
revisions list [--collection <uuid>]
|
|
28
|
+
revisions accept <revision-id> [--change <change-id>]
|
|
29
|
+
revisions reject <revision-id> [--change <change-id>] [--note <text>]`;
|
|
30
|
+
|
|
31
|
+
export async function cmdRevisions(ctx) {
|
|
32
|
+
const sub = ctx.positionals[0];
|
|
33
|
+
const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
|
|
34
|
+
switch (sub) {
|
|
35
|
+
case "list":
|
|
36
|
+
case "ls":
|
|
37
|
+
return revisionsList(rest);
|
|
38
|
+
case "accept":
|
|
39
|
+
return revisionsDecide(rest, "accepted");
|
|
40
|
+
case "reject":
|
|
41
|
+
return revisionsDecide(rest, "rejected");
|
|
42
|
+
default:
|
|
43
|
+
throw new CliError(USAGE);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function formatRevision(r) {
|
|
48
|
+
const where = r.publish_control === "platform" ? "goes live on accept" : "apply it in your CMS after accepting";
|
|
49
|
+
const lines = [
|
|
50
|
+
`${r.id} ${r.article_title ?? "Untitled"} (${r.source}; ${where})${r.is_stale ? " [STALE — needs recomputing]" : ""}`,
|
|
51
|
+
];
|
|
52
|
+
if (r.reason) lines.push(` ${r.reason}`);
|
|
53
|
+
for (const c of r.changes ?? []) {
|
|
54
|
+
const state = c.decision ?? "undecided";
|
|
55
|
+
lines.push(` - ${c.id} [${state}] ${c.before ? `${c.before} → ` : ""}${c.after}`);
|
|
56
|
+
}
|
|
57
|
+
return lines.join("\n");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function revisionsList(ctx) {
|
|
61
|
+
const { client, flags, io } = ctx;
|
|
62
|
+
refuseUnknownFlags(ctx, ARGS.list_revisions);
|
|
63
|
+
const result = await client.callTool("list_revisions", argsFromFlags(ARGS.list_revisions, flags));
|
|
64
|
+
printResult(io, flags, result, (r) => {
|
|
65
|
+
const items = r.revisions ?? [];
|
|
66
|
+
return items.length ? items.map(formatRevision).join("\n\n") : "No proposed edits waiting.";
|
|
67
|
+
});
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function revisionsDecide(ctx, decision) {
|
|
72
|
+
const { client, flags, positionals, io } = ctx;
|
|
73
|
+
refuseUnknownFlags(ctx, ARGS.decide_revision);
|
|
74
|
+
const result = await client.callTool(
|
|
75
|
+
"decide_revision",
|
|
76
|
+
argsFromFlags(ARGS.decide_revision, { ...flags, decision }, positionals)
|
|
77
|
+
);
|
|
78
|
+
ok(ctx, result.applied ?? `Revision is ${result.status}.`);
|
|
79
|
+
if (flags.json) printResult(io, flags, result);
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// `targets` — ranking targets: what the org wants to be THE answer for.
|
|
2
|
+
//
|
|
3
|
+
// A target belongs to the company (company-wide) or to one offering. Each scope — the
|
|
4
|
+
// company layer and every offering — holds its own ACTIVE targets up to its cap (a preset
|
|
5
|
+
// of 5, adjustable per scope with `targets cap`); inactive targets are never capped, so
|
|
6
|
+
// `add` never fails on the cap: it lands inactive when the scope is full. Thin wrappers
|
|
7
|
+
// over the MCP tools, which call the same store functions the app's /api/ranking-targets
|
|
8
|
+
// routes and staff Fleet do.
|
|
9
|
+
|
|
10
|
+
import { CliError } from "../client.mjs";
|
|
11
|
+
import { flagStr, flagNum, printResult, argsFromFlags, refuseUnknownFlags, ok } from "./shared.mjs";
|
|
12
|
+
|
|
13
|
+
const STATUSES = ["active", "inactive", "achieved", "retired"];
|
|
14
|
+
const COMPANY = "company";
|
|
15
|
+
|
|
16
|
+
// `--offering <offering-id|company>`: company = the company layer (null). Absent = not sent.
|
|
17
|
+
function readOffering(flags) {
|
|
18
|
+
const v = flagStr(flags.offering);
|
|
19
|
+
if (v === undefined) {
|
|
20
|
+
if (flags.offering === true) throw new CliError("--offering takes an offering id, or `company`");
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
return v === COMPANY ? null : v;
|
|
24
|
+
}
|
|
25
|
+
const offering = { value: readOffering };
|
|
26
|
+
|
|
27
|
+
// How every tool this module calls gets its arguments — see `argsFromFlags` in shared.mjs.
|
|
28
|
+
// `scripts/parity-check.ts` compares these keys against each tool's published input schema.
|
|
29
|
+
export const ARGS = {
|
|
30
|
+
list_ranking_targets: { offering_id: offering },
|
|
31
|
+
add_ranking_target: {
|
|
32
|
+
// The phrase is every positional, so `targets add best payroll software` needs no quotes.
|
|
33
|
+
phrase: {
|
|
34
|
+
value: (_flags, positionals) => {
|
|
35
|
+
const phrase = positionals.join(" ").trim();
|
|
36
|
+
if (!phrase) throw new CliError("Missing required <phrase>");
|
|
37
|
+
return phrase;
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
mention_framing: "framing",
|
|
41
|
+
offering_id: offering,
|
|
42
|
+
},
|
|
43
|
+
update_ranking_target: {
|
|
44
|
+
target_id: { positional: 0, label: "target-id", required: true },
|
|
45
|
+
phrase: "phrase",
|
|
46
|
+
mention_framing: {
|
|
47
|
+
value: (flags) => (flags["clear-framing"] === true ? null : flagStr(flags.framing)),
|
|
48
|
+
},
|
|
49
|
+
// `activate` / `deactivate` set this for you.
|
|
50
|
+
status: { flag: "state", enum: STATUSES },
|
|
51
|
+
offering_id: offering,
|
|
52
|
+
position: { flag: "position", type: "num" },
|
|
53
|
+
},
|
|
54
|
+
set_ranking_target_cap: {
|
|
55
|
+
offering_id: {
|
|
56
|
+
value: (_flags, positionals) => {
|
|
57
|
+
const scope = positionals[0];
|
|
58
|
+
if (scope === undefined) throw new CliError("Missing required <offering-id|company>");
|
|
59
|
+
return scope === COMPANY ? null : scope;
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
// `default` = back to inheriting (an offering → the company's cap; the company → the preset).
|
|
63
|
+
cap: {
|
|
64
|
+
value: (_flags, positionals) => {
|
|
65
|
+
const raw = positionals[1];
|
|
66
|
+
if (raw === undefined) throw new CliError("Missing required <n|default>");
|
|
67
|
+
return raw === "default" ? null : flagNum(raw);
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
suggest_ranking_targets: { offering_id: offering },
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const USAGE = `Usage: targets <list|add|update|activate|deactivate|cap|suggest>
|
|
75
|
+
targets list [--offering <offering-id|company>]
|
|
76
|
+
targets add <phrase> [--framing <f>] [--offering <offering-id|company>]
|
|
77
|
+
targets update <target-id> [--phrase <p>] [--framing <f> | --clear-framing] [--offering <offering-id|company>]
|
|
78
|
+
[--position <n>] [--state active|inactive|achieved|retired]
|
|
79
|
+
targets activate <target-id>
|
|
80
|
+
targets deactivate <target-id>
|
|
81
|
+
targets cap <offering-id|company> <n|default>
|
|
82
|
+
targets suggest [--offering <offering-id|company>]`;
|
|
83
|
+
|
|
84
|
+
export async function cmdTargets(ctx) {
|
|
85
|
+
const sub = ctx.positionals[0];
|
|
86
|
+
const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
|
|
87
|
+
switch (sub) {
|
|
88
|
+
case "list":
|
|
89
|
+
case "ls":
|
|
90
|
+
return targetsList(rest);
|
|
91
|
+
case "add":
|
|
92
|
+
return targetsAdd(rest);
|
|
93
|
+
case "update":
|
|
94
|
+
return targetsUpdate(rest);
|
|
95
|
+
case "activate":
|
|
96
|
+
return targetsSetStatus(rest, "active");
|
|
97
|
+
case "deactivate":
|
|
98
|
+
return targetsSetStatus(rest, "inactive");
|
|
99
|
+
case "cap":
|
|
100
|
+
return targetsCap(rest);
|
|
101
|
+
case "suggest":
|
|
102
|
+
return targetsSuggest(rest);
|
|
103
|
+
default:
|
|
104
|
+
throw new CliError(USAGE);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const scopeName = (s) => (s.offeringId === null ? "Company-wide" : s.label);
|
|
109
|
+
|
|
110
|
+
function formatScope(s) {
|
|
111
|
+
const where = s.capSetHere ? "set here" : s.offeringId === null ? "preset" : "company's cap";
|
|
112
|
+
return `${scopeName(s)} — ${s.active}/${s.cap} active (${where})`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function formatTarget(t) {
|
|
116
|
+
const framing = t.mention_framing ? ` — as ${t.mention_framing}` : "";
|
|
117
|
+
return ` ${t.id} [${t.status}] ${t.phrase}${framing}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Targets grouped under their scope, each headed by its cap and usage. A target on an
|
|
121
|
+
// offering the scopes list doesn't carry (archived) still shows, under its id.
|
|
122
|
+
export function formatTargets(r) {
|
|
123
|
+
const scopes = r.scopes ?? [];
|
|
124
|
+
const items = r.items ?? [];
|
|
125
|
+
if (!items.length && !scopes.length) return "No ranking targets yet.";
|
|
126
|
+
const lines = [];
|
|
127
|
+
const shown = new Set();
|
|
128
|
+
for (const s of scopes) {
|
|
129
|
+
const mine = items.filter((t) => (t.offering_id ?? null) === s.offeringId);
|
|
130
|
+
lines.push(formatScope(s));
|
|
131
|
+
for (const t of mine) {
|
|
132
|
+
lines.push(formatTarget(t));
|
|
133
|
+
shown.add(t.id);
|
|
134
|
+
}
|
|
135
|
+
if (!mine.length) lines.push(" (none)");
|
|
136
|
+
}
|
|
137
|
+
const orphans = items.filter((t) => !shown.has(t.id));
|
|
138
|
+
for (const id of [...new Set(orphans.map((t) => t.offering_id))]) {
|
|
139
|
+
lines.push(`Offering ${id} (not active)`);
|
|
140
|
+
for (const t of orphans.filter((o) => o.offering_id === id)) lines.push(formatTarget(t));
|
|
141
|
+
}
|
|
142
|
+
return lines.join("\n");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function targetsList(ctx) {
|
|
146
|
+
const { client, flags, io } = ctx;
|
|
147
|
+
const result = await client.callTool("list_ranking_targets", argsFromFlags(ARGS.list_ranking_targets, flags));
|
|
148
|
+
printResult(io, flags, result, formatTargets);
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function targetsAdd(ctx) {
|
|
153
|
+
const { client, flags, positionals, io } = ctx;
|
|
154
|
+
refuseUnknownFlags(ctx, ARGS.add_ranking_target, ["offering"]);
|
|
155
|
+
const result = await client.callTool(
|
|
156
|
+
"add_ranking_target",
|
|
157
|
+
argsFromFlags(ARGS.add_ranking_target, flags, positionals)
|
|
158
|
+
);
|
|
159
|
+
const t = result.target;
|
|
160
|
+
ok(
|
|
161
|
+
ctx,
|
|
162
|
+
t?.status === "active"
|
|
163
|
+
? `Added "${t.phrase}" — active.`
|
|
164
|
+
: `Added "${t?.phrase}" as inactive: its scope is at its active cap. Deactivate one there, or raise the cap (targets cap), then: ${ctx.bin} targets activate ${t?.id}`
|
|
165
|
+
);
|
|
166
|
+
printResult(io, flags, result, (r) => formatTarget(r.target));
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function update(ctx, flags) {
|
|
171
|
+
const { client, positionals, io } = ctx;
|
|
172
|
+
const args = argsFromFlags(ARGS.update_ranking_target, flags, positionals);
|
|
173
|
+
if (Object.keys(args).length === 1) {
|
|
174
|
+
throw new CliError(
|
|
175
|
+
"Nothing to update — pass --phrase, --framing, --clear-framing, --offering, --position or --state."
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
const result = await client.callTool("update_ranking_target", args);
|
|
179
|
+
ok(ctx, `Updated "${result.target?.phrase}" — ${result.target?.status}.`);
|
|
180
|
+
printResult(io, ctx.flags, result, (r) => formatTarget(r.target));
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function targetsUpdate(ctx) {
|
|
185
|
+
refuseUnknownFlags(ctx, ARGS.update_ranking_target, ["framing", "clear-framing", "offering"]);
|
|
186
|
+
return update(ctx, ctx.flags);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Activate / deactivate are the first-class switch. Activating is refused when the
|
|
190
|
+
// target's scope is at its cap — the server's message names the scope.
|
|
191
|
+
async function targetsSetStatus(ctx, status) {
|
|
192
|
+
refuseUnknownFlags(ctx, {});
|
|
193
|
+
return update(ctx, { ...ctx.flags, state: status });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function targetsCap(ctx) {
|
|
197
|
+
const { client, flags, positionals, io } = ctx;
|
|
198
|
+
refuseUnknownFlags(ctx, {});
|
|
199
|
+
const result = await client.callTool(
|
|
200
|
+
"set_ranking_target_cap",
|
|
201
|
+
argsFromFlags(ARGS.set_ranking_target_cap, flags, positionals)
|
|
202
|
+
);
|
|
203
|
+
ok(ctx, "Cap saved.");
|
|
204
|
+
printResult(io, flags, result, (r) => (r.scopes ?? []).map(formatScope).join("\n"));
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function targetsSuggest(ctx) {
|
|
209
|
+
const { client, flags, io } = ctx;
|
|
210
|
+
const result = await client.callTool("suggest_ranking_targets", argsFromFlags(ARGS.suggest_ranking_targets, flags));
|
|
211
|
+
printResult(io, flags, result, (r) => {
|
|
212
|
+
const all = r.suggestions ?? [];
|
|
213
|
+
if (!all.length) return "No suggestions yet — add positioning first.";
|
|
214
|
+
const scope = r.offering_id ? ` --offering ${r.offering_id}` : "";
|
|
215
|
+
const lines = all.map(
|
|
216
|
+
(s) =>
|
|
217
|
+
` ${s.phrase} [${s.basis}]${s.suggestedFraming ? ` — as ${s.suggestedFraming}` : ""}\n ${s.rationale}`
|
|
218
|
+
);
|
|
219
|
+
lines.push(`Nothing saved. Keep one with: ${ctx.bin} targets add <phrase>${scope}`);
|
|
220
|
+
return lines.join("\n");
|
|
221
|
+
});
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
package/lib/commands.mjs
CHANGED
|
@@ -18,6 +18,7 @@ export * from "./commands/connectors.mjs";
|
|
|
18
18
|
export * from "./commands/strategy.mjs";
|
|
19
19
|
export * from "./commands/directives.mjs";
|
|
20
20
|
export * from "./commands/probes.mjs";
|
|
21
|
+
export * from "./commands/revisions.mjs";
|
|
21
22
|
export * from "./commands/series.mjs";
|
|
22
23
|
export * from "./commands/insights.mjs";
|
|
23
24
|
export * from "./commands/covers.mjs";
|
|
@@ -34,5 +35,6 @@ export * from "./commands/shredder.mjs";
|
|
|
34
35
|
export * from "./commands/landings.mjs";
|
|
35
36
|
export * from "./commands/outbound.mjs";
|
|
36
37
|
export * from "./commands/offerings.mjs";
|
|
38
|
+
export * from "./commands/targets.mjs";
|
|
37
39
|
export * from "./commands/admin.mjs";
|
|
38
40
|
export * from "./commands/feedback.mjs";
|