@letterstory/cli 0.135.0 → 0.137.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 +17 -1
- package/lib/commands/admin.mjs +76 -0
- package/lib/commands/campaigns.mjs +159 -0
- package/lib/commands/site.mjs +47 -2
- package/lib/commands/strategy.mjs +30 -1
- package/lib/commands.mjs +1 -0
- 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
|
@@ -57,12 +57,13 @@ import {
|
|
|
57
57
|
cmdEvals,
|
|
58
58
|
cmdOfferings,
|
|
59
59
|
cmdEnclosures,
|
|
60
|
+
cmdCampaigns,
|
|
60
61
|
cmdTargets,
|
|
61
62
|
cmdVisits,
|
|
62
63
|
} from "./commands.mjs";
|
|
63
64
|
|
|
64
65
|
// Keep in sync with cli/package.json.
|
|
65
|
-
export const VERSION = "0.
|
|
66
|
+
export const VERSION = "0.137.0";
|
|
66
67
|
|
|
67
68
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
68
69
|
// can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
|
|
@@ -288,6 +289,8 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
288
289
|
"todo",
|
|
289
290
|
// admin spend margin: list nested work (never billed twice) beside its parent.
|
|
290
291
|
"nested",
|
|
292
|
+
// admin billing lago catalog --apply --include-drafts
|
|
293
|
+
"include-drafts",
|
|
291
294
|
]);
|
|
292
295
|
|
|
293
296
|
// Tiny argv parser: `--flag value`, `--flag=value`, boolean `--flag`, and positionals.
|
|
@@ -566,6 +569,11 @@ Enclosures (pictures, clips and sentences made for each prospect; each output is
|
|
|
566
569
|
enclosures apps connect <sign-in-url> [--no-remember] A sign-in link a person opens (no password passes through)
|
|
567
570
|
enclosures apps disconnect <app-id> --yes Remove it here and at the browser provider
|
|
568
571
|
|
|
572
|
+
Campaigns (one prompt, and Letter's agent builds the whole cold-email campaign; nothing sends until you approve):
|
|
573
|
+
campaigns build "<prompt>" [--limit <n>] [--watch] The list, the picture (tested on 3), the email, the review desk
|
|
574
|
+
campaigns show <build-id> [--watch] Its stage, what it built, and what it is doing
|
|
575
|
+
campaigns list | cancel <build-id> | suggest Its builds; stop one (what it built is kept); three to build
|
|
576
|
+
|
|
569
577
|
Offerings (context layered org → offering → collection; a layer sets only what it overrides):
|
|
570
578
|
offerings list The org's offerings (things you'd name when recommending them)
|
|
571
579
|
offerings show <offering-id> One offering + its competitor overrides
|
|
@@ -908,6 +916,13 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
|
|
|
908
916
|
cover what the app ledger cannot see; a CSV
|
|
909
917
|
import sums Concentrate's log export.
|
|
910
918
|
Needs spend:read (reads) / spend:write (writes).
|
|
919
|
+
admin billing lago catalog [--apply] [--include-drafts]
|
|
920
|
+
| provision <org-id> --plan starter|growth|scale [--ratio 1|2|4|8 (8 needs --why "…")] [--complimentary N] [--stripe-customer cus_…] [--start ISO] [--review D]
|
|
921
|
+
| reconcile [--org <org-id>] | forward
|
|
922
|
+
Usage billing on Lago: the plan catalog, an org's
|
|
923
|
+
plan and gifted ratio, Lago's tokens against ours,
|
|
924
|
+
and forwarding usage now. Needs spend:read (reads)
|
|
925
|
+
/ billing:write (writes).
|
|
911
926
|
admin crm import <file.csv> [--source <name>] [--tag "…"] [--dry-run]
|
|
912
927
|
| capture [shot.png …] [--text "…" | --text-file f] [--context "this is for acme.com"]
|
|
913
928
|
[--account <id>] [--dry-run] [--retry] screenshots / a thread / a note → a lead
|
|
@@ -1485,6 +1500,7 @@ const CLIENT_COMMANDS = {
|
|
|
1485
1500
|
directives: cmdDirectives,
|
|
1486
1501
|
offerings: cmdOfferings,
|
|
1487
1502
|
enclosures: cmdEnclosures,
|
|
1503
|
+
campaigns: cmdCampaigns,
|
|
1488
1504
|
targets: cmdTargets,
|
|
1489
1505
|
probes: cmdProbes,
|
|
1490
1506
|
suggestions: cmdSuggestions,
|
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) });
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// `campaigns` — one prompt, and Letter's agent builds the whole cold-email campaign: the list, the
|
|
2
|
+
// enclosure (tested on 3 and chosen), the sequence email that carries each person's picture, and
|
|
3
|
+
// the launch into review. Thin wrappers over the campaign build tools, which call the same
|
|
4
|
+
// functions the app's /api/campaign-builds routes do.
|
|
5
|
+
|
|
6
|
+
import { CliError } from "../client.mjs";
|
|
7
|
+
import { argsFromFlags, flagNum, pollUntil, printResult, refuseUnknownFlags, ok } from "./shared.mjs";
|
|
8
|
+
|
|
9
|
+
// How every tool this module calls gets its arguments — see `argsFromFlags` in shared.mjs.
|
|
10
|
+
// `scripts/parity-check.ts` compares these keys against each tool's published input schema.
|
|
11
|
+
export const ARGS = {
|
|
12
|
+
build_campaign: {
|
|
13
|
+
prompt: {
|
|
14
|
+
value: (_flags, positionals) => {
|
|
15
|
+
const words = positionals.join(" ").trim();
|
|
16
|
+
if (!words)
|
|
17
|
+
throw new CliError(
|
|
18
|
+
'Say what to build, e.g. campaigns build "Cold-email 10 SaaS founders with an audit of their own site"'
|
|
19
|
+
);
|
|
20
|
+
return words;
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
people_limit: { flag: "limit", type: "num" },
|
|
24
|
+
},
|
|
25
|
+
get_campaign_build: { build_id: { positional: 0, label: "build-id", required: true } },
|
|
26
|
+
list_campaign_builds: { limit: { flag: "limit", type: "num" } },
|
|
27
|
+
cancel_campaign_build: { build_id: { positional: 0, label: "build-id", required: true } },
|
|
28
|
+
suggest_campaigns: {},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const USAGE = `Usage:
|
|
32
|
+
campaigns build "<prompt>" [--limit <n>] [--watch] Build a whole campaign from one prompt (at most 50 people)
|
|
33
|
+
campaigns show <build-id> [--watch] Its stage, what it built, and what it is doing
|
|
34
|
+
campaigns list [--limit <n>] This org's builds, newest first
|
|
35
|
+
campaigns cancel <build-id> Stop it; what it built is kept
|
|
36
|
+
campaigns suggest Three campaigns to build in one step`;
|
|
37
|
+
|
|
38
|
+
const STAGES = { who: "Who", picture: "The picture", test: "Tested on 3", email: "The email", desk: "On the desk" };
|
|
39
|
+
|
|
40
|
+
export async function cmdCampaigns(ctx) {
|
|
41
|
+
const sub = ctx.positionals[0];
|
|
42
|
+
const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
|
|
43
|
+
switch (sub) {
|
|
44
|
+
case "build":
|
|
45
|
+
return campaignsBuild(rest);
|
|
46
|
+
case "show":
|
|
47
|
+
case "get":
|
|
48
|
+
return campaignsShow(rest);
|
|
49
|
+
case "list":
|
|
50
|
+
case "ls":
|
|
51
|
+
return campaignsList(rest);
|
|
52
|
+
case "cancel":
|
|
53
|
+
return campaignsCancel(rest);
|
|
54
|
+
case "suggest":
|
|
55
|
+
return campaignsSuggest(rest);
|
|
56
|
+
default:
|
|
57
|
+
throw new CliError(USAGE);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function formatBuild(b) {
|
|
62
|
+
const a = b.artifacts ?? {};
|
|
63
|
+
const lines = [`${b.id} [${b.status}, ${STAGES[b.stage] ?? b.stage}] ${b.prompt}`];
|
|
64
|
+
if (a.list)
|
|
65
|
+
lines.push(
|
|
66
|
+
` who: ${a.list.people} people on "${a.list.name}"${a.list.pulling ? " (still finding them)" : ""}`
|
|
67
|
+
);
|
|
68
|
+
if (a.enclosure) {
|
|
69
|
+
const p = a.enclosure.prints ?? {};
|
|
70
|
+
lines.push(
|
|
71
|
+
` picture: ${a.enclosure.name}${a.enclosure.current_version ? `, version ${a.enclosure.current_version}` : ", testing"}: ${p.done ?? 0} ready, ${p.making ?? 0} making, ${p.no_fit ?? 0} no fit, ${p.failed ?? 0} failed`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
if (a.sequence) lines.push(` email: ${a.sequence.name} [${a.sequence.status}]`);
|
|
75
|
+
if (a.desk)
|
|
76
|
+
lines.push(
|
|
77
|
+
` desk: ${a.desk.waiting} copies waiting (${a.desk.with_picture} with their picture), ${a.desk.on_the_way} on the way`
|
|
78
|
+
);
|
|
79
|
+
lines.push(` spent: $${Number(b.cost_usd).toFixed(2)} of $${Number(b.budget_usd).toFixed(2)}`);
|
|
80
|
+
if (b.summary) lines.push(` ${b.summary}`);
|
|
81
|
+
if (b.error) lines.push(` ${b.error}`);
|
|
82
|
+
return lines.join("\n");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const formatLine = (l) => `${l.at.slice(11, 19)} ${l.kind === "say" ? "" : `${l.kind}: `}${l.text}`;
|
|
86
|
+
|
|
87
|
+
// Follow a build: print each new activity line as it lands, until it stops.
|
|
88
|
+
async function follow(ctx, buildId) {
|
|
89
|
+
const { client, io } = ctx;
|
|
90
|
+
let shown = 0;
|
|
91
|
+
return pollUntil(
|
|
92
|
+
io,
|
|
93
|
+
async () => {
|
|
94
|
+
const { build } = await client.callTool("get_campaign_build", { build_id: buildId });
|
|
95
|
+
const lines = build.activity ?? [];
|
|
96
|
+
for (const line of lines.slice(shown)) io.log(formatLine(line));
|
|
97
|
+
shown = Math.max(shown, lines.length);
|
|
98
|
+
return { done: build.status !== "running", value: build };
|
|
99
|
+
},
|
|
100
|
+
{ intervalMs: 5000, timeoutMs: 60 * 60_000, label: "the campaign build" }
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function campaignsBuild(ctx) {
|
|
105
|
+
const { client, flags, positionals, io } = ctx;
|
|
106
|
+
refuseUnknownFlags(ctx, ARGS.build_campaign, ["watch"]);
|
|
107
|
+
if (flags.limit !== undefined) flagNum(flags.limit);
|
|
108
|
+
const result = await client.callTool("build_campaign", argsFromFlags(ARGS.build_campaign, flags, positionals));
|
|
109
|
+
ok(ctx, `Building it (${result.build.id}). It takes 10 to 20 minutes; nothing sends until you approve a copy.`);
|
|
110
|
+
const build = flags.watch === true && !flags.json ? await follow(ctx, result.build.id) : result.build;
|
|
111
|
+
printResult(io, flags, flags.watch === true ? { build } : result, (r) => formatBuild(r.build));
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function campaignsShow(ctx) {
|
|
116
|
+
const { client, flags, positionals, io } = ctx;
|
|
117
|
+
refuseUnknownFlags(ctx, ARGS.get_campaign_build, ["watch"]);
|
|
118
|
+
const args = argsFromFlags(ARGS.get_campaign_build, flags, positionals);
|
|
119
|
+
if (flags.watch === true && !flags.json) {
|
|
120
|
+
const build = await follow(ctx, args.build_id);
|
|
121
|
+
io.log(formatBuild(build));
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
const result = await client.callTool("get_campaign_build", args);
|
|
125
|
+
printResult(io, flags, result, (r) =>
|
|
126
|
+
[formatBuild(r.build), ...(r.build.activity ?? []).slice(-15).map(formatLine)].join("\n")
|
|
127
|
+
);
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function campaignsList(ctx) {
|
|
132
|
+
const { client, flags, io } = ctx;
|
|
133
|
+
if (flags.limit !== undefined) flagNum(flags.limit);
|
|
134
|
+
const result = await client.callTool("list_campaign_builds", argsFromFlags(ARGS.list_campaign_builds, flags));
|
|
135
|
+
printResult(io, flags, result, (r) =>
|
|
136
|
+
r.builds?.length ? r.builds.map(formatBuild).join("\n\n") : "No campaign builds yet."
|
|
137
|
+
);
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function campaignsCancel(ctx) {
|
|
142
|
+
const { client, flags, positionals, io } = ctx;
|
|
143
|
+
const result = await client.callTool(
|
|
144
|
+
"cancel_campaign_build",
|
|
145
|
+
argsFromFlags(ARGS.cancel_campaign_build, flags, positionals)
|
|
146
|
+
);
|
|
147
|
+
ok(ctx, "Stopped. What it built is kept.");
|
|
148
|
+
printResult(io, flags, result, (r) => formatBuild(r.build));
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function campaignsSuggest(ctx) {
|
|
153
|
+
const { client, flags, io } = ctx;
|
|
154
|
+
const result = await client.callTool("suggest_campaigns", {});
|
|
155
|
+
printResult(io, flags, result, (r) =>
|
|
156
|
+
(r.suggestions ?? []).map((s) => `${s.title}\n letterstory campaigns build "${s.prompt}"`).join("\n\n")
|
|
157
|
+
);
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
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.");
|
package/lib/commands.mjs
CHANGED
|
@@ -39,6 +39,7 @@ export * from "./commands/landings.mjs";
|
|
|
39
39
|
export * from "./commands/outbound.mjs";
|
|
40
40
|
export * from "./commands/offerings.mjs";
|
|
41
41
|
export * from "./commands/enclosures.mjs";
|
|
42
|
+
export * from "./commands/campaigns.mjs";
|
|
42
43
|
export * from "./commands/targets.mjs";
|
|
43
44
|
export * from "./commands/admin.mjs";
|
|
44
45
|
export * from "./commands/feedback.mjs";
|