@letterstory/cli 0.89.0 → 0.91.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 +15 -0
- package/lib/cli.mjs +28 -4
- package/lib/commands/admin.mjs +77 -1
- package/lib/commands/lede.mjs +82 -1
- package/lib/commands/outbound.mjs +29 -13
- package/lib/commands/shared.mjs +31 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -346,6 +346,21 @@ letterstory admin ask "which clients published nothing in the last 30 days?" --s
|
|
|
346
346
|
This is the headless half of `/superadmin/ask` — same function, same schema — so a
|
|
347
347
|
long-running agent can watch the fleet on a schedule without a browser.
|
|
348
348
|
|
|
349
|
+
### Staff: client patterns and Fable
|
|
350
|
+
|
|
351
|
+
```bash
|
|
352
|
+
# What separates clients whose AI visibility moves (docs/architecture/client-patterns.md):
|
|
353
|
+
# every client against itself on share of voice, mention rate and page access, with what we
|
|
354
|
+
# did for it. Needs fleet:read; `run` and `ask` spend Fable tokens and need fleet:query.
|
|
355
|
+
letterstory admin patterns --period 30d
|
|
356
|
+
letterstory admin patterns reports # Fable's weekly reports and runs, newest first
|
|
357
|
+
letterstory admin patterns report <id> # one report's markdown
|
|
358
|
+
letterstory admin patterns run # a report now (takes minutes)
|
|
359
|
+
letterstory admin patterns ask "do answer-gap posts help paying clients too?"
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
The headless half of Admin → Client performance → Patterns: same functions, same schemas.
|
|
363
|
+
|
|
349
364
|
### Staff: kernel experiments
|
|
350
365
|
|
|
351
366
|
```bash
|
package/lib/cli.mjs
CHANGED
|
@@ -57,13 +57,15 @@ import {
|
|
|
57
57
|
} from "./commands.mjs";
|
|
58
58
|
|
|
59
59
|
// Keep in sync with cli/package.json.
|
|
60
|
-
export const VERSION = "0.
|
|
60
|
+
export const VERSION = "0.91.0";
|
|
61
61
|
|
|
62
62
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
63
63
|
// can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
|
|
64
64
|
// `negate` spelling) in a command's ARGS map must be listed; boolean-flags.test.ts enforces it.
|
|
65
65
|
export const BOOLEAN_FLAGS = new Set([
|
|
66
66
|
"json",
|
|
67
|
+
// lede library update / lede proposals accept: clear or drop the line's condition.
|
|
68
|
+
"no-condition",
|
|
67
69
|
"yes",
|
|
68
70
|
"confirm-missing-target",
|
|
69
71
|
"no-wait",
|
|
@@ -133,9 +135,10 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
133
135
|
"no-enrich",
|
|
134
136
|
// `offerings set-collection <id> --none` (the org itself is the subject again).
|
|
135
137
|
"none",
|
|
136
|
-
// `prospects import|source --one-per-company`, `prospects source --enrich`.
|
|
138
|
+
// `prospects import|source --one-per-company`, `prospects source --enrich`, `prospects import --find-emails`.
|
|
137
139
|
"one-per-company",
|
|
138
140
|
"enrich",
|
|
141
|
+
"find-emails",
|
|
139
142
|
// `targets update <id> --clear-framing`; `targets subqueries --retired`,
|
|
140
143
|
// `targets subquery update <id> --retire` / `--restore`.
|
|
141
144
|
"clear-framing",
|
|
@@ -645,6 +648,14 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
|
|
|
645
648
|
(fleet:instrument) — e.g. --term Context.dev for a name
|
|
646
649
|
that is also a word. --clear goes back to inferred,
|
|
647
650
|
recorded as your decision.
|
|
651
|
+
admin patterns [--period 14d|30d] [--segment "<segment>"] [--json]
|
|
652
|
+
Client patterns: every client's share of voice,
|
|
653
|
+
mention rate and page access against itself, with
|
|
654
|
+
what we did for it. Needs fleet:read.
|
|
655
|
+
admin patterns reports [--kind reports|weekly|run|ask] | report <id>
|
|
656
|
+
Fable's weekly reports, runs and Ask Fable answers.
|
|
657
|
+
admin patterns run | ask "<question>" Run a Fable report now, or ask Fable a question
|
|
658
|
+
about client patterns (minutes). Needs fleet:query.
|
|
648
659
|
admin replays [--org <uuid>] [--target <uuid>] [--status captured|not_reproduced|queued|running|unsupported|failed] [--limit <n>]
|
|
649
660
|
Live answer replays: breakthroughs re-asked in ChatGPT /
|
|
650
661
|
Gemini / Google / Perplexity / Claude, with screenshots.
|
|
@@ -932,7 +943,7 @@ Ledes (opening lines for cold email, chosen per prospect from a hand-approved li
|
|
|
932
943
|
lede balance Ledes left on this org
|
|
933
944
|
lede library list The org's opener library: pre-built + custom lines
|
|
934
945
|
lede library create --text <line> [--alts <v>]... [--peer-alts <v>]... [--requires <trigger>]...
|
|
935
|
-
[--threshold <0..100>] [--tone warm|direct|playful|formal] [--tag <t>]...
|
|
946
|
+
[--threshold <0..100>] [--tone warm|direct|playful|formal] [--tag <t>]... [--condition <use when...>]
|
|
936
947
|
Add a custom line. A {{placeholder}} must appear in
|
|
937
948
|
--requires; proprietary-engine triggers stay
|
|
938
949
|
reserved for the pre-built lines. --threshold
|
|
@@ -941,6 +952,7 @@ Ledes (opening lines for cold email, chosen per prospect from a hand-approved li
|
|
|
941
952
|
lower = looser.
|
|
942
953
|
lede library update <lede-id> [--text <line>] [--alts <v>]... [--peer-alts <v>]...
|
|
943
954
|
[--requires <trigger>]... [--threshold <0..100>] [--tone <t>] [--tag <t>]...
|
|
955
|
+
[--condition <use when...> | --no-condition]
|
|
944
956
|
Edit a line; only the flags you pass change
|
|
945
957
|
lede library enable <lede-id> Make a line a run candidate again
|
|
946
958
|
lede library disable <lede-id> Keep a line out of runs (reversible; the way to
|
|
@@ -951,6 +963,13 @@ Ledes (opening lines for cold email, chosen per prospect from a hand-approved li
|
|
|
951
963
|
Propose brand-voice rewrites of the wording only.
|
|
952
964
|
Applies nothing: accept a proposal with
|
|
953
965
|
lede library update --text/--alts
|
|
966
|
+
lede proposals [list] [--status proposed|captured|active|rejected] [--limit <n>]
|
|
967
|
+
Lines reviewers wrote while personalizing in
|
|
968
|
+
Dispatch, templated and judged, each with a
|
|
969
|
+
drafted "Use when ..." condition
|
|
970
|
+
lede proposals accept <proposal-id> [--text <line>] [--condition <c> | --no-condition] [--tone <t>]
|
|
971
|
+
Add it to the library (live immediately)
|
|
972
|
+
lede proposals reject <proposal-id> --reason <why> Keep it out; the reason stops it recurring
|
|
954
973
|
|
|
955
974
|
Landings (a link + context; auto-mentioned in articles by collections you enable):
|
|
956
975
|
landings list Your landing pages + mention counts
|
|
@@ -974,14 +993,16 @@ Sequences (multi-step cold email sent from your mailers; approving or enrolling
|
|
|
974
993
|
[--[no-]opt-out-footer] [--[no-]postal-address-footer] [--[no-]unsubscribe-links] [--[no-]detect-reply-opt-outs]
|
|
975
994
|
[--[no-]test-mode] [--default-lede <fallback opener line>]
|
|
976
995
|
[--send-window-start <HH:MM>] [--send-window-end <HH:MM>] [--send-window-timezone <IANA tz>]
|
|
977
|
-
[--enroll-on-cake paid|scheduled|out_for_delivery|delivered|none]
|
|
996
|
+
[--enroll-on-cake paid|scheduled|out_for_delivery|delivered|none] [--enroll-on-cake-list <list-id>|none]
|
|
978
997
|
--test-mode (internal accounts only): walks as live, sends nothing
|
|
979
998
|
--enroll-on-cake: start it when a recipient's Daymaker cake reaches that stage
|
|
999
|
+
--enroll-on-cake-list: only for prospects in that list (none = any)
|
|
980
1000
|
sequences pause|resume <sequence-id> Stop / restart sending
|
|
981
1001
|
sequences enroll <sequence-id> (--list <list-id> | --email <addr>... | --recipients '<json>'|--recipients-file <path>)
|
|
982
1002
|
sequences stop <sequence-id> <enrollment-id> Stop one recipient
|
|
983
1003
|
sequences review list Emails waiting for a decision
|
|
984
1004
|
sequences review edit <item-id> [--subject <s>] [--body <text>|--file <path>]
|
|
1005
|
+
[--personalize <line> [--personalize-mode replace|insert]] Mark the edit a personalization (proposed to the opener library)
|
|
985
1006
|
sequences review approve|skip|delete <item-id> Decide one (approve sends now)
|
|
986
1007
|
sequences links list Live Pocket Dispatch links (no tokens)
|
|
987
1008
|
sequences links create --days <1..3> Mint a no-login review link (shown once)
|
|
@@ -1004,6 +1025,9 @@ Prospects (who you write to):
|
|
|
1004
1025
|
prospects get <prospect-id>
|
|
1005
1026
|
prospects create-list --name <n>
|
|
1006
1027
|
prospects import --csv-file <path|-> [--list <list-id>|--list-name <n>] [--one-per-company]
|
|
1028
|
+
[--find-emails [--max-email-lookups <1..100>]]
|
|
1029
|
+
--find-emails: look up the business email of rows with a
|
|
1030
|
+
LinkedIn URL and no email (paid; default 25 lookups)
|
|
1007
1031
|
prospects source --description <who you want> [--list <list-id>|--list-name <n>] [--limit <1..100>]
|
|
1008
1032
|
[--one-per-company] [--enrich] Fill a list from paid data providers (background);
|
|
1009
1033
|
--enrich starts enrichment as soon as the pull lands
|
package/lib/commands/admin.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// routes, because they exist precisely to act BEFORE an org exists.
|
|
5
5
|
//
|
|
6
6
|
// admin ask "which kernels are hooked up to the most clients?" [--sql] [--json]
|
|
7
|
+
// admin patterns [--period 14d|30d] [--segment "…"] | reports [--kind reports|weekly|run|ask] | report <id> | run | ask "<question>" (fleet:read / fleet:query)
|
|
7
8
|
// admin experiments list|show|new|update|note|started|spend|conclude|abandon … (tuning:experiment)
|
|
8
9
|
// admin tuning-context [--q "<question>"] (tuning:experiment)
|
|
9
10
|
// admin tuning-import <brief-results.json> [--experiment <id>] (tuning:experiment)
|
|
@@ -241,6 +242,80 @@ async function cmdAdminAsk(ctx) {
|
|
|
241
242
|
}
|
|
242
243
|
}
|
|
243
244
|
|
|
245
|
+
/* ------------------------------------------------------------ client patterns */
|
|
246
|
+
|
|
247
|
+
const PATTERNS = "/api/superadmin/client-patterns";
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* `admin patterns …` — the Patterns tab without a browser (docs/architecture/client-patterns.md):
|
|
251
|
+
* the data, Fable's report history, a report on demand, and Ask Fable. `run` and `ask` wait for
|
|
252
|
+
* the finished answer (minutes, not seconds) and print its markdown.
|
|
253
|
+
*/
|
|
254
|
+
async function cmdAdminPatterns(ctx) {
|
|
255
|
+
const [sub = "show", ...rest] = ctx.positionals;
|
|
256
|
+
const json = flagBool(ctx.flags.json);
|
|
257
|
+
switch (sub) {
|
|
258
|
+
case "show": {
|
|
259
|
+
const { payload } = await staffRequest(ctx, "GET", PATTERNS);
|
|
260
|
+
if (!payload) throw new CliError("The patterns snapshot has not been computed yet.");
|
|
261
|
+
const period = flagStr(ctx.flags.period) ?? "30d";
|
|
262
|
+
const segment = flagStr(ctx.flags.segment);
|
|
263
|
+
const clients = payload.clients.filter((c) => !segment || c.segment === segment);
|
|
264
|
+
if (json) return printResult(ctx.io, ctx.flags, { ...payload, clients });
|
|
265
|
+
const pts = (v) => (v == null ? "—" : `${v >= 0 ? "+" : "−"}${Math.abs(v * 100).toFixed(1)} pts`);
|
|
266
|
+
ctx.io.log(`${clients.length} clients, ${payload.postCount} posts · computed ${payload.generated}`);
|
|
267
|
+
for (const c of clients) {
|
|
268
|
+
const s = c.sov?.[period];
|
|
269
|
+
const n = c.named?.[period];
|
|
270
|
+
ctx.io.log(
|
|
271
|
+
`${c.name.padEnd(28)} ${c.segment.padEnd(30)} sov ${pts(s?.change)} mention ${pts(n?.change)} posts30 ${c.posts30}`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
case "reports": {
|
|
277
|
+
const kind = flagStr(ctx.flags.kind) ?? "reports";
|
|
278
|
+
const limit = flagStr(ctx.flags.limit) ?? "12";
|
|
279
|
+
const { reports } = await staffRequest(
|
|
280
|
+
ctx,
|
|
281
|
+
"GET",
|
|
282
|
+
`${PATTERNS}/reports?kind=${encodeURIComponent(kind)}&limit=${encodeURIComponent(limit)}`
|
|
283
|
+
);
|
|
284
|
+
if (json) return printResult(ctx.io, ctx.flags, { reports });
|
|
285
|
+
for (const r of reports)
|
|
286
|
+
ctx.io.log(
|
|
287
|
+
`${r.id} ${r.created_at} ${r.kind.padEnd(6)} ${r.status.padEnd(7)} ${(r.question ?? (r.summary ?? "").split("\n")[0] ?? "").slice(0, 80)}`
|
|
288
|
+
);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
case "report": {
|
|
292
|
+
const id = rest[0];
|
|
293
|
+
if (!id) throw new CliError("Usage: admin patterns report <id>");
|
|
294
|
+
const { report } = await staffRequest(ctx, "GET", `${PATTERNS}/reports/${encodeURIComponent(id)}`);
|
|
295
|
+
if (json) return printResult(ctx.io, ctx.flags, { report });
|
|
296
|
+
ctx.io.log(report.report ?? report.error ?? "(no report)");
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
case "run": {
|
|
300
|
+
const { report } = await staffPost(ctx, `${PATTERNS}/reports`, {});
|
|
301
|
+
if (json) return printResult(ctx.io, ctx.flags, { report });
|
|
302
|
+
ctx.io.log(report.report ?? report.error ?? "(no report)");
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
case "ask": {
|
|
306
|
+
const question = rest.join(" ").trim();
|
|
307
|
+
if (question.length < 3)
|
|
308
|
+
throw new CliError('Usage: admin patterns ask "do answer-gap posts help paying clients too?"');
|
|
309
|
+
const { answer } = await staffPost(ctx, `${PATTERNS}/ask`, { question });
|
|
310
|
+
if (json) return printResult(ctx.io, ctx.flags, { answer });
|
|
311
|
+
ctx.io.log(answer.report ?? answer.error ?? "(no answer)");
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
default:
|
|
315
|
+
throw new CliError('Usage: admin patterns [show] | reports | report <id> | run | ask "<question>"');
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
244
319
|
/* ------------------------------------------------------------ kernel experiments */
|
|
245
320
|
|
|
246
321
|
const EXPERIMENTS = "/api/superadmin/tuning/experiments";
|
|
@@ -3576,6 +3651,7 @@ export async function cmdAdmin(ctx) {
|
|
|
3576
3651
|
if (sub === "spend") return cmdAdminSpend({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3577
3652
|
if (sub === "create-org") return cmdAdminCreateOrg({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3578
3653
|
if (sub === "ask") return cmdAdminAsk({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3654
|
+
if (sub === "patterns") return cmdAdminPatterns({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3579
3655
|
if (sub === "experiments") return cmdAdminExperiments({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3580
3656
|
if (sub === "fleet-experiments") return cmdAdminFleetExperiments({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3581
3657
|
if (sub === "tuning-context") return cmdAdminTuningContext({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
@@ -3584,7 +3660,7 @@ export async function cmdAdmin(ctx) {
|
|
|
3584
3660
|
if (sub === "evals") return cmdAdminEvals({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3585
3661
|
if (sub === "brand-detection") return cmdAdminBrandDetection({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3586
3662
|
throw new CliError(
|
|
3587
|
-
'Usage: admin create-org --domain <bare-domain> [--onboard] … | admin ask "<question>" | admin experiments … | admin fleet-experiments … | admin crm … | admin spend … | admin evals … | admin brand-detection … | admin tuning … | admin tuning-context | admin replays … (see --help)'
|
|
3663
|
+
'Usage: admin create-org --domain <bare-domain> [--onboard] … | admin ask "<question>" | admin patterns … | admin experiments … | admin fleet-experiments … | admin crm … | admin spend … | admin evals … | admin brand-detection … | admin tuning … | admin tuning-context | admin replays … (see --help)'
|
|
3588
3664
|
);
|
|
3589
3665
|
}
|
|
3590
3666
|
|
package/lib/commands/lede.mjs
CHANGED
|
@@ -130,6 +130,8 @@ export const ARGS = {
|
|
|
130
130
|
threshold: { flag: "threshold", type: "num" },
|
|
131
131
|
tone: { flag: "tone", enum: ["warm", "direct", "playful", "formal"] },
|
|
132
132
|
tags: { flag: "tag", type: "list" },
|
|
133
|
+
// "Use when ..." clause, checked per prospect before the line is used.
|
|
134
|
+
condition: "condition",
|
|
133
135
|
},
|
|
134
136
|
update_lede: {
|
|
135
137
|
id: { positional: 0, label: "lede-id", required: true },
|
|
@@ -140,6 +142,8 @@ export const ARGS = {
|
|
|
140
142
|
threshold: { flag: "threshold", type: "num" },
|
|
141
143
|
tone: { flag: "tone", enum: ["warm", "direct", "playful", "formal"] },
|
|
142
144
|
tags: { flag: "tag", type: "list" },
|
|
145
|
+
// --condition "<clause>" sets it; --no-condition clears it.
|
|
146
|
+
condition: { value: (flags) => (flags["no-condition"] ? null : flagStr(flags.condition)) },
|
|
143
147
|
},
|
|
144
148
|
set_lede_enabled: {
|
|
145
149
|
id: { positional: 0, label: "lede-id", required: true },
|
|
@@ -153,6 +157,22 @@ export const ARGS = {
|
|
|
153
157
|
voice: { flag: "voice", required: true },
|
|
154
158
|
ids: { flag: "ids", type: "list" },
|
|
155
159
|
},
|
|
160
|
+
// Opener proposals (`lede proposals ...`): lines captured from Dispatch personalizations,
|
|
161
|
+
// evaluated, waiting for an admin to accept into the library or reject with a reason.
|
|
162
|
+
list_lede_proposals: {
|
|
163
|
+
status: { flag: "status", enum: ["captured", "proposed", "active", "rejected"] },
|
|
164
|
+
limit: { flag: "limit", type: "num" },
|
|
165
|
+
},
|
|
166
|
+
decide_lede_proposal: {
|
|
167
|
+
proposal_id: { positional: 0, label: "proposal-id", required: true },
|
|
168
|
+
// `lede proposals accept|reject <id>` decides this; the subcommand passes it in.
|
|
169
|
+
action: { value: (_flags, _positionals, extra) => extra.action },
|
|
170
|
+
text: "text",
|
|
171
|
+
// --condition "<clause>" edits it on accept; --no-condition accepts with none.
|
|
172
|
+
condition: { value: (flags) => (flags["no-condition"] ? null : flagStr(flags.condition)) },
|
|
173
|
+
tone: { flag: "tone", enum: ["warm", "direct", "playful", "formal"] },
|
|
174
|
+
reason: "reason",
|
|
175
|
+
},
|
|
156
176
|
};
|
|
157
177
|
|
|
158
178
|
export async function cmdLede(ctx) {
|
|
@@ -173,6 +193,9 @@ export async function cmdLede(ctx) {
|
|
|
173
193
|
case "library":
|
|
174
194
|
case "lib":
|
|
175
195
|
return ledeLibrary(rest);
|
|
196
|
+
case "proposals":
|
|
197
|
+
case "proposal":
|
|
198
|
+
return ledeProposals(rest);
|
|
176
199
|
case "run":
|
|
177
200
|
return ledeRun(rest);
|
|
178
201
|
case "runs":
|
|
@@ -193,7 +216,7 @@ export async function cmdLede(ctx) {
|
|
|
193
216
|
return ledeFunnel(rest);
|
|
194
217
|
default:
|
|
195
218
|
throw new CliError(
|
|
196
|
-
`Unknown lede subcommand: ${sub ?? "(none)"}. Try: list, create, delete, endpoint, library, run, runs, history, evals, balance, outcome, funnel`
|
|
219
|
+
`Unknown lede subcommand: ${sub ?? "(none)"}. Try: list, create, delete, endpoint, library, proposals, run, runs, history, evals, balance, outcome, funnel`
|
|
197
220
|
);
|
|
198
221
|
}
|
|
199
222
|
}
|
|
@@ -445,3 +468,61 @@ async function ledeBalance(ctx) {
|
|
|
445
468
|
printResult(io, flags, result);
|
|
446
469
|
return 0;
|
|
447
470
|
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* `lede proposals [list] [--status proposed|captured|active|rejected] [--limit n]`,
|
|
474
|
+
* `lede proposals accept <id> [--text ...] [--condition ...|--no-condition] [--tone ...]`,
|
|
475
|
+
* `lede proposals reject <id> --reason ...` — lines reviewers wrote while personalizing on
|
|
476
|
+
* the Dispatch desk, templatized and judged; accept adds one to the library (live now).
|
|
477
|
+
*/
|
|
478
|
+
async function ledeProposals(ctx) {
|
|
479
|
+
const { client, positionals, flags, io } = ctx;
|
|
480
|
+
const action = positionals[0];
|
|
481
|
+
const rest = { ...ctx, positionals: positionals.slice(1) };
|
|
482
|
+
switch (action) {
|
|
483
|
+
case undefined:
|
|
484
|
+
case "list":
|
|
485
|
+
case "ls": {
|
|
486
|
+
const result = await client.callTool("list_lede_proposals", argsFromFlags(ARGS.list_lede_proposals, flags));
|
|
487
|
+
// One block per proposal: id, how often captured, the template, then its condition
|
|
488
|
+
// (or why it was rejected). `--json` for the judge's scores and the raw line.
|
|
489
|
+
printResult(
|
|
490
|
+
io,
|
|
491
|
+
flags,
|
|
492
|
+
result,
|
|
493
|
+
(r) =>
|
|
494
|
+
(r?.proposals ?? [])
|
|
495
|
+
.map((p) =>
|
|
496
|
+
[
|
|
497
|
+
`${p.id} x${p.capture_count} [${p.status}] ${p.templated_text ?? p.raw_line}`,
|
|
498
|
+
p.status_reason
|
|
499
|
+
? ` why: ${p.status_reason}`
|
|
500
|
+
: p.suggested_condition
|
|
501
|
+
? ` ${p.suggested_condition}`
|
|
502
|
+
: null,
|
|
503
|
+
]
|
|
504
|
+
.filter(Boolean)
|
|
505
|
+
.join("\n")
|
|
506
|
+
)
|
|
507
|
+
.join("\n") || "No proposals."
|
|
508
|
+
);
|
|
509
|
+
return 0;
|
|
510
|
+
}
|
|
511
|
+
case "accept":
|
|
512
|
+
case "reject": {
|
|
513
|
+
const args = argsFromFlags(ARGS.decide_lede_proposal, flags, rest.positionals, { action });
|
|
514
|
+
if (action === "reject" && !args.reason) throw new CliError("A rejection needs --reason <why>.");
|
|
515
|
+
const result = await client.callTool("decide_lede_proposal", args);
|
|
516
|
+
ok(
|
|
517
|
+
ctx,
|
|
518
|
+
action === "accept"
|
|
519
|
+
? `Accepted ${args.proposal_id}: the line is in the library now.`
|
|
520
|
+
: `Rejected ${args.proposal_id}.`
|
|
521
|
+
);
|
|
522
|
+
printResult(io, flags, result);
|
|
523
|
+
return 0;
|
|
524
|
+
}
|
|
525
|
+
default:
|
|
526
|
+
throw new CliError(`Unknown lede proposals action: ${action}. Try: list, accept, reject`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
@@ -93,6 +93,13 @@ const SEQUENCE_WINDOW_ARGS = {
|
|
|
93
93
|
// reaches it; `none` clears it.
|
|
94
94
|
const CAKE_TRIGGER_STAGES = ["paid", "scheduled", "out_for_delivery", "delivered"];
|
|
95
95
|
const CAKE_STAGES = ["draft", ...CAKE_TRIGGER_STAGES, "delivery_failed", "cancelled"];
|
|
96
|
+
const enrollOnCakeList = {
|
|
97
|
+
value: (flags) => {
|
|
98
|
+
const raw = flagStr(flags["enroll-on-cake-list"]);
|
|
99
|
+
if (raw === undefined) return undefined;
|
|
100
|
+
return raw === "none" ? null : raw;
|
|
101
|
+
},
|
|
102
|
+
};
|
|
96
103
|
const enrollOnCake = {
|
|
97
104
|
value: (flags) => {
|
|
98
105
|
const raw = flagStr(flags["enroll-on-cake"]);
|
|
@@ -163,6 +170,7 @@ export const ARGS = {
|
|
|
163
170
|
...SEQUENCE_RAIL_ARGS,
|
|
164
171
|
...SEQUENCE_WINDOW_ARGS,
|
|
165
172
|
enroll_on_cake: enrollOnCake,
|
|
173
|
+
enroll_on_cake_list_id: enrollOnCakeList,
|
|
166
174
|
},
|
|
167
175
|
update_sequence: {
|
|
168
176
|
sequence_id: { positional: 0, label: "sequence-id", required: true },
|
|
@@ -173,6 +181,7 @@ export const ARGS = {
|
|
|
173
181
|
...SEQUENCE_RAIL_ARGS,
|
|
174
182
|
...SEQUENCE_WINDOW_ARGS,
|
|
175
183
|
enroll_on_cake: enrollOnCake,
|
|
184
|
+
enroll_on_cake_list_id: enrollOnCakeList,
|
|
176
185
|
},
|
|
177
186
|
list_cakes: {
|
|
178
187
|
stage: { flag: "stage", enum: CAKE_STAGES },
|
|
@@ -197,6 +206,23 @@ export const ARGS = {
|
|
|
197
206
|
item_id: { positional: 0, label: "item-id", required: true },
|
|
198
207
|
subject: "subject",
|
|
199
208
|
body: { value: (flags) => readTextInput(flags, "body", "file") },
|
|
209
|
+
// --personalize "<line>" marks the edit as a personalization (captured as an opener
|
|
210
|
+
// proposal for the org's admins); --personalize-mode says whether the line replaced
|
|
211
|
+
// the opener (default) or was inserted after the greeting.
|
|
212
|
+
personalization: {
|
|
213
|
+
value: (flags) => {
|
|
214
|
+
const line = flagStr(flags.personalize);
|
|
215
|
+
const mode = flagStr(flags["personalize-mode"]);
|
|
216
|
+
if (line === undefined) {
|
|
217
|
+
if (mode !== undefined) throw new CliError("--personalize-mode needs --personalize <line>");
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
if (mode !== undefined && mode !== "replace" && mode !== "insert") {
|
|
221
|
+
throw new CliError(`--personalize-mode must be replace or insert (got ${mode})`);
|
|
222
|
+
}
|
|
223
|
+
return { line, mode: mode ?? "replace" };
|
|
224
|
+
},
|
|
225
|
+
},
|
|
200
226
|
},
|
|
201
227
|
decide_review_item: {
|
|
202
228
|
item_id: { positional: 0, label: "item-id", required: true },
|
|
@@ -248,6 +274,8 @@ export const ARGS = {
|
|
|
248
274
|
return csv;
|
|
249
275
|
},
|
|
250
276
|
},
|
|
277
|
+
find_emails: { flag: "find-emails", type: "bool" },
|
|
278
|
+
max_email_lookups: { flag: "max-email-lookups", type: "num" },
|
|
251
279
|
one_per_company: { flag: "one-per-company", type: "bool" },
|
|
252
280
|
list_id: "list",
|
|
253
281
|
list_name: "list-name",
|
|
@@ -361,20 +389,8 @@ export const ARGS = {
|
|
|
361
389
|
},
|
|
362
390
|
};
|
|
363
391
|
|
|
364
|
-
// Flags the computed `value` sources read (refuseUnknownFlags can't see them).
|
|
365
|
-
const VALUE_FLAGS = {
|
|
366
|
-
create_sequence: ["flow", "flow-file", "mailer", "send-window-start", "send-window-end"],
|
|
367
|
-
update_sequence: ["flow", "flow-file", "mailer", "send-window-start", "send-window-end"],
|
|
368
|
-
enroll_in_sequence: ["recipients", "recipients-file", "email"],
|
|
369
|
-
update_review_item: ["body", "file"],
|
|
370
|
-
delete_prospects: ["id"],
|
|
371
|
-
import_prospects_csv: ["csv", "csv-file"],
|
|
372
|
-
update_prospect: ["custom"],
|
|
373
|
-
set_mailer_forwarding: ["to", "clear"],
|
|
374
|
-
};
|
|
375
|
-
|
|
376
392
|
async function run(ctx, tool, message) {
|
|
377
|
-
refuseUnknownFlags(ctx, ARGS[tool]
|
|
393
|
+
refuseUnknownFlags(ctx, ARGS[tool]);
|
|
378
394
|
const result = await ctx.client.callTool(tool, argsFromFlags(ARGS[tool], ctx.flags, ctx.positionals));
|
|
379
395
|
if (message) ok(ctx, message);
|
|
380
396
|
printResult(ctx.io, ctx.flags, result);
|
package/lib/commands/shared.mjs
CHANGED
|
@@ -83,6 +83,35 @@ export const GLOBAL_FLAGS = new Set([
|
|
|
83
83
|
"profile",
|
|
84
84
|
]);
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* The flags a computed `{ value }` source reads, found by running it against
|
|
88
|
+
* no flags and recording each key it looks up. A value source names its flags
|
|
89
|
+
* inside code the map can't see, so `sequences update --enroll-on-cake` was
|
|
90
|
+
* refused by a command whose help and parser both took it.
|
|
91
|
+
*/
|
|
92
|
+
export function valueSourceFlags(value) {
|
|
93
|
+
const read = new Set();
|
|
94
|
+
const flags = new Proxy(
|
|
95
|
+
{},
|
|
96
|
+
{
|
|
97
|
+
get: (_target, key) => {
|
|
98
|
+
if (typeof key === "string") read.add(key);
|
|
99
|
+
return undefined;
|
|
100
|
+
},
|
|
101
|
+
has: (_target, key) => {
|
|
102
|
+
if (typeof key === "string") read.add(key);
|
|
103
|
+
return false;
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
try {
|
|
108
|
+
value(flags, []);
|
|
109
|
+
} catch {
|
|
110
|
+
// A required source throws when its flag is absent; the read was recorded first.
|
|
111
|
+
}
|
|
112
|
+
return read;
|
|
113
|
+
}
|
|
114
|
+
|
|
86
115
|
/**
|
|
87
116
|
* Refuse a flag this command does not understand.
|
|
88
117
|
*
|
|
@@ -105,6 +134,8 @@ export function refuseUnknownFlags(ctx, argsMap, own = []) {
|
|
|
105
134
|
else if (source && typeof source === "object" && source.flag) {
|
|
106
135
|
allowed.add(source.flag);
|
|
107
136
|
if (source.negate) allowed.add(source.negate);
|
|
137
|
+
} else if (source && typeof source === "object" && typeof source.value === "function") {
|
|
138
|
+
valueSourceFlags(source.value).forEach((f) => allowed.add(f));
|
|
108
139
|
}
|
|
109
140
|
}
|
|
110
141
|
const unknown = Object.keys(ctx.flags).filter((f) => !allowed.has(f));
|