@letterstory/cli 0.114.0 → 0.115.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/lib/cli.mjs +8 -3
- package/lib/commands/admin.mjs +52 -12
- package/lib/commands/shared.mjs +13 -0
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -60,7 +60,7 @@ import {
|
|
|
60
60
|
} from "./commands.mjs";
|
|
61
61
|
|
|
62
62
|
// Keep in sync with cli/package.json.
|
|
63
|
-
export const VERSION = "0.
|
|
63
|
+
export const VERSION = "0.115.0";
|
|
64
64
|
|
|
65
65
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
66
66
|
// can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
|
|
@@ -786,7 +786,7 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
|
|
|
786
786
|
| slack-channels [--all] | link-slack <account-id> <channel-id|none>
|
|
787
787
|
| white-glove [--suggested] | set-white-glove <account-id> on|off [--note "…"]
|
|
788
788
|
| churn [--months 12] | record-churn <account-id> <reason> [--note "…"] [--at ISO]
|
|
789
|
-
[--mrr-cents N] | undo-churn <account-id>
|
|
789
|
+
[--mrr-cents N] | undo-churn <account-id> | draft-churn-reason <account-id>
|
|
790
790
|
| onboard <account-id> --domain acme.com [--name "Acme"] [--site "<direction>"]… [--sites N]
|
|
791
791
|
[--slack a@acme.com,b@acme.com] [--invite a@acme.com[:admin]]… [--no-checklist]
|
|
792
792
|
| onboarding <account-id> | retry-onboarding <onboarding-id>
|
|
@@ -802,6 +802,9 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
|
|
|
802
802
|
Churn: 30/90-day logo + revenue rates, who
|
|
803
803
|
churned and why; Stripe cancellations are
|
|
804
804
|
only suggestions until a person records one.
|
|
805
|
+
draft-churn-reason drafts a reason from
|
|
806
|
+
what the CRM kept; a person confirms it in
|
|
807
|
+
the proposals queue.
|
|
805
808
|
Onboard runs org → link → phantom sites →
|
|
806
809
|
Slack channel → checklist → app invites →
|
|
807
810
|
stage in one call (--site "" = infer the
|
|
@@ -1403,7 +1406,9 @@ export async function run(argv, io = defaultIo()) {
|
|
|
1403
1406
|
await resolveDocumentFlags(client, flags);
|
|
1404
1407
|
|
|
1405
1408
|
const ctx = { client, config, positionals: positionals.slice(1), flags, io, bin };
|
|
1406
|
-
return
|
|
1409
|
+
// A handler may return what its last `io.log` returned (`true`): only a number is an exit code.
|
|
1410
|
+
const code = await handler(ctx);
|
|
1411
|
+
return typeof code === "number" ? code : 0;
|
|
1407
1412
|
} catch (err) {
|
|
1408
1413
|
if (err instanceof CliError) {
|
|
1409
1414
|
io.error(`Error: ${err.message}`);
|
package/lib/commands/admin.mjs
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// The old names — evals, eval-add, eval-set, proposals, propose-eval, capture-eval, reject-eval — still work.)
|
|
15
15
|
// admin evals scoreboard|cases|runs|run|triage|guard|retire … (fleet:read / tuning:experiment)
|
|
16
16
|
// admin fleet-experiments list|articles|clients|mark|unmark|new|assign|assign-eligible|assign-orgs|spin-up|apply|start|stop|decide … (fleet:read / fleet:experiment)
|
|
17
|
-
// admin crm import|capture|outreach|signals|record|signal|accounts|account|score|slack-channels|link-slack|white-glove|set-white-glove|churn|revenue|record-churn|undo-churn|onboard|onboarding|retry-onboarding|onboard-plan|payment-link … (crm:read / crm:write)
|
|
17
|
+
// admin crm import|capture|outreach|signals|record|signal|accounts|account|score|slack-channels|link-slack|white-glove|set-white-glove|churn|revenue|record-churn|draft-churn-reason|undo-churn|onboard|onboarding|retry-onboarding|onboard-plan|payment-link … (crm:read / crm:write)
|
|
18
18
|
// admin spend summary|reconcile|pnl|entries|add|import|usage|remove|reference|stripe-sync|services|service … (spend:read / spend:write)
|
|
19
19
|
// admin brand-detection <org-id> (fleet:read)
|
|
20
20
|
// admin brand-detection <org-id> [--term t]... [--exact t]... [--exclude p]... [--note "…"] | --clear [--note "…"] (fleet:instrument)
|
|
@@ -44,7 +44,17 @@
|
|
|
44
44
|
|
|
45
45
|
import { readFileSync } from "node:fs";
|
|
46
46
|
import { CliError, LetterstoryClient } from "../client.mjs";
|
|
47
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
flagStr,
|
|
49
|
+
flagNum,
|
|
50
|
+
flagBool,
|
|
51
|
+
flagList,
|
|
52
|
+
flagCsv,
|
|
53
|
+
printResult,
|
|
54
|
+
compact,
|
|
55
|
+
ok,
|
|
56
|
+
requirePositional,
|
|
57
|
+
} from "./shared.mjs";
|
|
48
58
|
import { watchDomainJob } from "./onboard.mjs";
|
|
49
59
|
|
|
50
60
|
const SEER_MODES = ["suggest", "skip"];
|
|
@@ -682,6 +692,21 @@ async function cmdAdminTuningContext(ctx) {
|
|
|
682
692
|
* admin tuning enhance-inputs [--set v1] | enhance-split [--since-hours 72] [--at <iso>,…] | enhance-standing
|
|
683
693
|
* admin tuning promote-enhancer <experiment-id> --version <enhancer-version-id> promote our enhancer from a "ship" decision (picks when it changes visuals)
|
|
684
694
|
*/
|
|
695
|
+
/** `--flag '<json array>'` → the parsed array of objects, or undefined when the flag is absent. */
|
|
696
|
+
function jsonArrayFlag(raw, name) {
|
|
697
|
+
const text = flagStr(raw);
|
|
698
|
+
if (text === undefined) return undefined;
|
|
699
|
+
let v;
|
|
700
|
+
try {
|
|
701
|
+
v = JSON.parse(text);
|
|
702
|
+
} catch (err) {
|
|
703
|
+
throw new CliError(`${name} is not JSON: ${err.message}`);
|
|
704
|
+
}
|
|
705
|
+
if (!Array.isArray(v) || v.some((x) => !x || typeof x !== "object" || Array.isArray(x)))
|
|
706
|
+
throw new CliError(`${name} is a JSON array of objects: '[{…},{…}]'`);
|
|
707
|
+
return v;
|
|
708
|
+
}
|
|
709
|
+
|
|
685
710
|
/** `--flag '<json>'` → the parsed object, or undefined when the flag is absent. */
|
|
686
711
|
function jsonFlag(raw, name) {
|
|
687
712
|
const text = flagStr(raw);
|
|
@@ -846,7 +871,7 @@ async function cmdAdminTuning(ctx) {
|
|
|
846
871
|
}
|
|
847
872
|
case "compare": {
|
|
848
873
|
const title = flagStr(flags.title);
|
|
849
|
-
const list =
|
|
874
|
+
const list = flagCsv(flags.candidates);
|
|
850
875
|
if (!title || !list?.length)
|
|
851
876
|
throw new CliError(
|
|
852
877
|
'Usage: admin tuning compare --title "…" --candidates Hazel:21,Aspen:20 [--experiment <id>] [--pairs 20] (default 10 pairs)'
|
|
@@ -2178,10 +2203,8 @@ async function cmdAdminTuning(ctx) {
|
|
|
2178
2203
|
const versionId = flagStr(flags.candidate);
|
|
2179
2204
|
const patch = jsonFlag(flags.patch, "--patch");
|
|
2180
2205
|
const nullCheck = flagBool(flags["null-check"]);
|
|
2181
|
-
const versionIds =
|
|
2182
|
-
const patches =
|
|
2183
|
-
if (patches !== undefined && !Array.isArray(patches))
|
|
2184
|
-
throw new CliError("--patches is a JSON array of patches: '[{…},{…}]'");
|
|
2206
|
+
const versionIds = flagCsv(flags.candidates);
|
|
2207
|
+
const patches = jsonArrayFlag(flags.patches, "--patches");
|
|
2185
2208
|
const named = [
|
|
2186
2209
|
versionId !== undefined,
|
|
2187
2210
|
patch !== undefined,
|
|
@@ -2217,7 +2240,7 @@ async function cmdAdminTuning(ctx) {
|
|
|
2217
2240
|
candidates: several,
|
|
2218
2241
|
// The kernel that writes every arm into a finished article (the arena the only difference).
|
|
2219
2242
|
write: flagStr(flags.write) ? { kernel: flagStr(flags.write) } : undefined,
|
|
2220
|
-
keys:
|
|
2243
|
+
keys: flagCsv(flags.keys),
|
|
2221
2244
|
concurrency: flagNum(flags.concurrency),
|
|
2222
2245
|
experiment_id: flagStr(flags.experiment),
|
|
2223
2246
|
})
|
|
@@ -2244,7 +2267,7 @@ async function cmdAdminTuning(ctx) {
|
|
|
2244
2267
|
case "comparison-extend": {
|
|
2245
2268
|
// A Write comparison grows by the next N pairs; one on fixed inputs by named keys or all.
|
|
2246
2269
|
const pairs = flagNum(flags.pairs);
|
|
2247
|
-
const keys = flagBool(flags.all) ? "all" :
|
|
2270
|
+
const keys = flagBool(flags.all) ? "all" : flagCsv(flags.keys);
|
|
2248
2271
|
const usage =
|
|
2249
2272
|
"Usage: admin tuning comparison-extend <comparison-id> --pairs N | --keys k1 --keys k2 | --all";
|
|
2250
2273
|
if (!id || (pairs === undefined && !keys?.length) || (pairs !== undefined && keys?.length))
|
|
@@ -2304,7 +2327,7 @@ async function cmdAdminTuning(ctx) {
|
|
|
2304
2327
|
primitive: "shred",
|
|
2305
2328
|
title,
|
|
2306
2329
|
candidate,
|
|
2307
|
-
keys:
|
|
2330
|
+
keys: flagCsv(flags.keys),
|
|
2308
2331
|
concurrency: flagNum(flags.concurrency),
|
|
2309
2332
|
experiment_id: flagStr(flags.experiment),
|
|
2310
2333
|
})
|
|
@@ -2422,7 +2445,7 @@ async function cmdAdminTuning(ctx) {
|
|
|
2422
2445
|
primitive: "enhance",
|
|
2423
2446
|
title,
|
|
2424
2447
|
candidate,
|
|
2425
|
-
keys:
|
|
2448
|
+
keys: flagCsv(flags.keys),
|
|
2426
2449
|
concurrency: flagNum(flags.concurrency),
|
|
2427
2450
|
experiment_id: flagStr(flags.experiment),
|
|
2428
2451
|
})
|
|
@@ -3366,6 +3389,23 @@ async function cmdAdminCrm(ctx) {
|
|
|
3366
3389
|
`${x.change === "updated" ? "churn updated" : "churn recorded"}: ${accountLine(x.account)} · ${x.account.churnMrrCents == null ? "no MRR on file" : `$${Math.round(x.account.churnMrrCents / 100)}/mo lost`}`
|
|
3367
3390
|
);
|
|
3368
3391
|
}
|
|
3392
|
+
case "draft-churn-reason": {
|
|
3393
|
+
// admin crm draft-churn-reason <account-id> — draft why a churned account left, from
|
|
3394
|
+
// what the CRM kept. The draft waits in the proposals queue for a person to confirm.
|
|
3395
|
+
if (!id) throw new CliError("Usage: admin crm draft-churn-reason <account-id>");
|
|
3396
|
+
const r = await staffPost(ctx, `${CRM}/accounts/${id}/churn/draft-reason`, {});
|
|
3397
|
+
return out(r, (x) =>
|
|
3398
|
+
x.status === "drafted"
|
|
3399
|
+
? [
|
|
3400
|
+
`drafted: ${x.reason}${x.contributing.length ? ` (also ${x.contributing.join(", ")})` : ""} · ${x.cause} · proposal ${x.proposalId}`,
|
|
3401
|
+
` "${x.quote}"`,
|
|
3402
|
+
x.summary ? ` ${x.summary}` : null,
|
|
3403
|
+
]
|
|
3404
|
+
.filter(Boolean)
|
|
3405
|
+
.join("\n")
|
|
3406
|
+
: `${x.status.replaceAll("_", " ")}${x.detail ? ` (${x.detail.replaceAll("_", " ")})` : ""}`
|
|
3407
|
+
);
|
|
3408
|
+
}
|
|
3369
3409
|
case "undo-churn": {
|
|
3370
3410
|
// admin crm undo-churn <account-id> — clear the churn and move back to active.
|
|
3371
3411
|
if (!id) throw new CliError("Usage: admin crm undo-churn <account-id>");
|
|
@@ -3420,7 +3460,7 @@ async function cmdAdminCrm(ctx) {
|
|
|
3420
3460
|
}
|
|
3421
3461
|
default:
|
|
3422
3462
|
throw new CliError(
|
|
3423
|
-
"Usage: admin crm import|capture|outreach|signals|record|signal|accounts|account|score|slack-channels|link-slack|white-glove|set-white-glove|churn|record-churn|undo-churn|onboard|onboarding|retry-onboarding|onboard-plan|payment-link … (see --help)"
|
|
3463
|
+
"Usage: admin crm import|capture|outreach|signals|record|signal|accounts|account|score|slack-channels|link-slack|white-glove|set-white-glove|churn|record-churn|draft-churn-reason|undo-churn|onboard|onboarding|retry-onboarding|onboard-plan|payment-link … (see --help)"
|
|
3424
3464
|
);
|
|
3425
3465
|
}
|
|
3426
3466
|
}
|
package/lib/commands/shared.mjs
CHANGED
|
@@ -32,6 +32,19 @@ export function flagList(v) {
|
|
|
32
32
|
return typeof v === "string" ? [v] : [];
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* A list of ids or keys, repeatable AND comma-separated: `--keys a,b --keys c` → `["a","b","c"]`.
|
|
37
|
+
* `flagList` keeps each occurrence whole (free text such as `--stance` may carry a comma); the id
|
|
38
|
+
* and key flags whose usage says `k1,k2` read through this instead, or the whole string went to the
|
|
39
|
+
* server as one id and failed validation.
|
|
40
|
+
*/
|
|
41
|
+
export function flagCsv(v) {
|
|
42
|
+
return flagList(v)
|
|
43
|
+
.flatMap((x) => x.split(","))
|
|
44
|
+
.map((x) => x.trim())
|
|
45
|
+
.filter(Boolean);
|
|
46
|
+
}
|
|
47
|
+
|
|
35
48
|
/**
|
|
36
49
|
* `--prefer <site>` (repeatable or comma-separated) → preferred_sources `[{ url }]` (a site; a URL is read as its site); `--prefer none`
|
|
37
50
|
* → `[]` (clears a standing list); absent → undefined.
|