@letterstory/cli 0.117.0 → 0.120.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 +13 -5
- package/lib/commands/admin.mjs +75 -2
- package/lib/commands/org.mjs +57 -1
- package/lib/commands/repo.mjs +6 -4
- package/lib/commands/site.mjs +1 -1
- 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.120.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
|
|
@@ -795,6 +795,8 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
|
|
|
795
795
|
[--at ISO] [--mrr-before N] [--mrr-after N] [--note "…"] [--save-offer …]
|
|
796
796
|
[--save-result …] [--draft]
|
|
797
797
|
| renewals [--days 90]
|
|
798
|
+
| track-record [--since YYYY-MM-DD] | at-risk | expansion-signals
|
|
799
|
+
| case <account-id> [--outcome <outcome-id>]
|
|
798
800
|
| onboard <account-id> --domain acme.com [--name "Acme"] [--site "<direction>"]… [--sites N]
|
|
799
801
|
[--slack a@acme.com,b@acme.com] [--invite a@acme.com[:admin]]… [--no-checklist]
|
|
800
802
|
| onboarding <account-id> | retry-onboarding <onboarding-id>
|
|
@@ -820,6 +822,10 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
|
|
|
820
822
|
reason; a key records drafts a person
|
|
821
823
|
confirms. Renewals: contracts renewing
|
|
822
824
|
within N days, and Stripe cancels scheduled.
|
|
825
|
+
Track record: how often each churn-risk and
|
|
826
|
+
next-product rule was right (rates show once
|
|
827
|
+
15 outcomes and 90 days exist). At-risk and
|
|
828
|
+
expansion signals show the closest past case.
|
|
823
829
|
Onboard runs org → link → phantom sites →
|
|
824
830
|
Slack channel → checklist → app invites →
|
|
825
831
|
stage in one call (--site "" = infer the
|
|
@@ -914,6 +920,8 @@ Seers (event-driven signals -> drafts):
|
|
|
914
920
|
org export --split <dir> …as one file per record (for keeping config in git)
|
|
915
921
|
org join <dir> [--file <path>] Rebuild the bundle from a --split directory
|
|
916
922
|
org integrations Every integration and whether it's connected
|
|
923
|
+
org linear [teams [--team <id|key>] | set --team <id|key> [--project <id>|none] | disconnect]
|
|
924
|
+
Where Letter files Linear issues (connect in Settings)
|
|
917
925
|
org diff <file|dir> What applying a bundle would change (reads only)
|
|
918
926
|
org apply <file|dir> [--dry-run] [--prune --yes] Make the org match a bundle (adds/updates; --prune deletes)
|
|
919
927
|
[--allow spends,sends,destroys,access --yes] Also apply gated changes (admins; without --yes it only previews)
|
|
@@ -1203,7 +1211,7 @@ Repo changes (Claude Code edits your repo in a sandbox; the Letterstory GitHub A
|
|
|
1203
1211
|
repo branches --repo owner/name Branches a change can start from (default first)
|
|
1204
1212
|
repo prs [--repo owner/name] Every open PR (whoever opened it), longest-idle first
|
|
1205
1213
|
repo suggestions Fixes found and not acted on yet, and how to start each
|
|
1206
|
-
repo settings [--action build|build_pr|build_merge|slack] [--branch <name>|none] [--slack-agent <name|U…>|none]
|
|
1214
|
+
repo settings [--action build|build_pr|build_merge|slack|linear] [--branch <name>|none] [--slack-agent <name|U…>|none]
|
|
1207
1215
|
[--slack-channel <C…>|none] [--slack-reports off|as_found|daily] [--reports-channel <C…>|none] [--autopilot-channel <C…>|none] [--listen <C…,C…>|none] [--autopilot on|off] [--autopilot-weekly <1-20>]
|
|
1208
1216
|
[--autopilot-severity critical|warning]
|
|
1209
1217
|
The org's defaults for every fix: build (preview first),
|
|
@@ -1213,13 +1221,13 @@ Repo changes (Claude Code edits your repo in a sandbox; the Letterstory GitHub A
|
|
|
1213
1221
|
repo slack-test Post a test into the Send to Slack channel
|
|
1214
1222
|
repo start --repo <owner/name> --instructions <text>|--instructions-file <path|->
|
|
1215
1223
|
[--branch <base>] [--attach <path>]... [--attach-url <name>=<https url>]...
|
|
1216
|
-
[--action build|build_pr|build_merge|slack] [--wait]
|
|
1224
|
+
[--action build|build_pr|build_merge|slack|linear] [--wait]
|
|
1217
1225
|
Start a change (attached files go to the sandbox);
|
|
1218
1226
|
--wait follows it to the PR
|
|
1219
|
-
repo fix-audit --issue <issue-type>... [--audit <id>] [--branch <base>] [--action build|build_pr|build_merge|slack] [--wait]
|
|
1227
|
+
repo fix-audit --issue <issue-type>... [--audit <id>] [--branch <base>] [--action build|build_pr|build_merge|slack|linear] [--wait]
|
|
1220
1228
|
One PR fixing site-audit issues (repeat --issue to bundle) on every affected page,
|
|
1221
1229
|
in the repo verified to build the site
|
|
1222
|
-
repo install-letterprove --repo <owner/name> [--branch <base>] [--action build|build_pr|build_merge|slack] [--wait]
|
|
1230
|
+
repo install-letterprove --repo <owner/name> [--branch <base>] [--action build|build_pr|build_merge|slack|linear] [--wait]
|
|
1223
1231
|
One PR adding Letterprove's script and identify calls
|
|
1224
1232
|
(pick the repo where users sign in)
|
|
1225
1233
|
repo sites [--detect [--force]] Which website each repo builds, and the evidence
|
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|draft-churn-reason|undo-churn|outcomes|record-outcome|renewals|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|outcomes|record-outcome|renewals|track-record|at-risk|expansion-signals|case|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)
|
|
@@ -3502,6 +3502,79 @@ async function cmdAdminCrm(ctx) {
|
|
|
3502
3502
|
: `(nothing in the next ${r.days} days)`
|
|
3503
3503
|
);
|
|
3504
3504
|
}
|
|
3505
|
+
case "track-record": {
|
|
3506
|
+
// admin crm track-record [--since YYYY-MM-DD] — how often each risk and next-product rule was right.
|
|
3507
|
+
const pct = (x) => (x == null ? "—" : `${Math.round(x * 100)}%`);
|
|
3508
|
+
const params = new URLSearchParams(compact({ since: flagStr(flags.since) }));
|
|
3509
|
+
return out(await staffRequest(ctx, "GET", `${CRM}/track-record?${params}`), (r) =>
|
|
3510
|
+
[
|
|
3511
|
+
`Since ${r.since ?? "—"} · confirmed: ${r.outcomes.churn} churn, ${r.outcomes.contraction} contraction, ${r.outcomes.expansion} expansion · rates show at ${r.minOutcomes} outcomes and ${r.windowDays} days`,
|
|
3512
|
+
`Base rate: loss ${pct(r.base.loss.rate)} · expansion ${pct(r.base.expansion.rate)}`,
|
|
3513
|
+
"",
|
|
3514
|
+
...r.predictors.map(
|
|
3515
|
+
(p) =>
|
|
3516
|
+
`${p.key.padEnd(40)} flagged ${String(p.flagged).padStart(3)} matured ${String(p.matured).padStart(3)} hits ${String(p.hits).padStart(3)} missed ${String(p.missed).padStart(3)} rate ${pct(p.maturedRate.rate)} acted ${pct(p.acted.rate)} / not ${pct(p.notActed.rate)}${p.mature ? "" : ` (not mature: ${p.notMatureBecause})`}`
|
|
3517
|
+
),
|
|
3518
|
+
].join("\n")
|
|
3519
|
+
);
|
|
3520
|
+
}
|
|
3521
|
+
case "at-risk": {
|
|
3522
|
+
// admin crm at-risk — customers at watch or high, with the closest past churn.
|
|
3523
|
+
const caseLine = (c) =>
|
|
3524
|
+
c
|
|
3525
|
+
? `\n like ${c.name} (${c.kind} ${c.on}${c.primaryReason ? `, ${c.primaryReason}` : ""}): ${c.matched}/5 matched — ${c.features
|
|
3526
|
+
.filter((f) => f.matched)
|
|
3527
|
+
.map((f) => `${f.feature} ${f.now}`)
|
|
3528
|
+
.join(", ")}`
|
|
3529
|
+
: "\n no past case matches 3 of 5";
|
|
3530
|
+
return out(await staffRequest(ctx, "GET", `${CRM}/at-risk`), (r) =>
|
|
3531
|
+
r.accounts.length
|
|
3532
|
+
? r.accounts
|
|
3533
|
+
.map(
|
|
3534
|
+
(a) =>
|
|
3535
|
+
`${a.level.padEnd(5)} ${a.score.toFixed(2)} ${a.name} ${a.reasons.join("; ")} ${a.accountId}${caseLine(a.closestCase)}`
|
|
3536
|
+
)
|
|
3537
|
+
.join("\n")
|
|
3538
|
+
: "(nobody at watch or high)"
|
|
3539
|
+
);
|
|
3540
|
+
}
|
|
3541
|
+
case "expansion-signals": {
|
|
3542
|
+
// admin crm expansion-signals — next-product verdicts on the latest snapshot.
|
|
3543
|
+
return out(await staffRequest(ctx, "GET", `${CRM}/expansion-signals`), (r) =>
|
|
3544
|
+
r.signals.length
|
|
3545
|
+
? r.signals
|
|
3546
|
+
.map(
|
|
3547
|
+
(x) =>
|
|
3548
|
+
`${x.score.toFixed(2)} ${x.name} → ${x.product} (${x.rule}) ${x.reasons.join("; ")} ${x.accountId}${x.beforePastExpansions.length ? `\n before past expansions: ${x.beforePastExpansions.map((p) => `${p.name} ${p.on}`).join(", ")}` : ""}`
|
|
3549
|
+
)
|
|
3550
|
+
.join("\n")
|
|
3551
|
+
: "(no next-product verdicts)"
|
|
3552
|
+
);
|
|
3553
|
+
}
|
|
3554
|
+
case "case": {
|
|
3555
|
+
// admin crm case <account-id> [--outcome <id>] — the 90 days before an outcome, or before today.
|
|
3556
|
+
if (!id) throw new CliError("Usage: admin crm case <account-id> [--outcome <outcome-id>]");
|
|
3557
|
+
const params = new URLSearchParams(compact({ outcomeId: flagStr(flags.outcome) }));
|
|
3558
|
+
const usd = (c) => `$${Math.round(c / 100).toLocaleString("en-US")}`;
|
|
3559
|
+
return out(await staffRequest(ctx, "GET", `${CRM}/accounts/${id}/case?${params}`), (r) =>
|
|
3560
|
+
[
|
|
3561
|
+
`${r.name} ${r.from} → ${r.until}${r.outcome ? ` ${outcomeLine(r.outcome)}` : ""}`,
|
|
3562
|
+
r.signalsBefore.length
|
|
3563
|
+
? `Signals before: ${r.signalsBefore.map((x) => `${x.rule} from ${x.firstFiredOn} (${x.days}d)`).join(", ")}`
|
|
3564
|
+
: "Signals before: none",
|
|
3565
|
+
"",
|
|
3566
|
+
...r.series.map(
|
|
3567
|
+
(p) =>
|
|
3568
|
+
`${p.on} ${usd(p.revenueCents)} posts ${p.phantomPosts30d} clicks ${p.clicks28d ?? "—"} cites ${p.aiCitations28d ?? "—"} waiting ${p.slackWaiting} quiet ${p.quietDays ?? "—"} ${p.riskLevel}`
|
|
3569
|
+
),
|
|
3570
|
+
"",
|
|
3571
|
+
...r.events.map(
|
|
3572
|
+
(e) =>
|
|
3573
|
+
`${e.at.slice(0, 10)} ${e.kind} ${e.title}${e.detail ? `\n ${e.detail}` : ""}${e.quotes.map((q) => `\n "${q}"`).join("")}`
|
|
3574
|
+
),
|
|
3575
|
+
].join("\n")
|
|
3576
|
+
);
|
|
3577
|
+
}
|
|
3505
3578
|
case "undo-churn": {
|
|
3506
3579
|
// admin crm undo-churn <account-id> — clear the churn and move back to active.
|
|
3507
3580
|
if (!id) throw new CliError("Usage: admin crm undo-churn <account-id>");
|
|
@@ -3556,7 +3629,7 @@ async function cmdAdminCrm(ctx) {
|
|
|
3556
3629
|
}
|
|
3557
3630
|
default:
|
|
3558
3631
|
throw new CliError(
|
|
3559
|
-
"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|outcomes|record-outcome|renewals|onboard|onboarding|retry-onboarding|onboard-plan|payment-link … (see --help)"
|
|
3632
|
+
"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|outcomes|record-outcome|renewals|track-record|at-risk|expansion-signals|case|onboard|onboarding|retry-onboarding|onboard-plan|payment-link … (see --help)"
|
|
3560
3633
|
);
|
|
3561
3634
|
}
|
|
3562
3635
|
}
|
package/lib/commands/org.mjs
CHANGED
|
@@ -27,6 +27,13 @@ export const ARGS = {
|
|
|
27
27
|
},
|
|
28
28
|
get_org_config_source: {},
|
|
29
29
|
list_integrations: {},
|
|
30
|
+
get_linear_connection: {},
|
|
31
|
+
list_linear_targets: { team_id: { flag: "team" } },
|
|
32
|
+
set_linear_target: {
|
|
33
|
+
team_id: { flag: "team", required: true, label: "team" },
|
|
34
|
+
project_id: { value: (flags) => (flags.project === "none" ? null : flagStr(flags.project)) },
|
|
35
|
+
},
|
|
36
|
+
disconnect_linear: {},
|
|
30
37
|
connect_org_config_source: {
|
|
31
38
|
repo: { positional: 0, label: "owner/name" },
|
|
32
39
|
branch: { flag: "branch" },
|
|
@@ -56,9 +63,11 @@ export async function cmdOrg(ctx) {
|
|
|
56
63
|
return orgGit(rest);
|
|
57
64
|
case "integrations":
|
|
58
65
|
return orgIntegrations(rest);
|
|
66
|
+
case "linear":
|
|
67
|
+
return orgLinear(rest);
|
|
59
68
|
default:
|
|
60
69
|
throw new CliError(
|
|
61
|
-
`Unknown org subcommand: ${sub ?? "(none)"}. Try: export, join, diff, apply, git, integrations`
|
|
70
|
+
`Unknown org subcommand: ${sub ?? "(none)"}. Try: export, join, diff, apply, git, integrations, linear`
|
|
62
71
|
);
|
|
63
72
|
}
|
|
64
73
|
}
|
|
@@ -75,6 +84,53 @@ async function orgIntegrations(ctx) {
|
|
|
75
84
|
return 0;
|
|
76
85
|
}
|
|
77
86
|
|
|
87
|
+
/**
|
|
88
|
+
* `org linear` — where Letter files Linear issues (connecting is OAuth, in Settings):
|
|
89
|
+
* org linear the connection (get_linear_connection)
|
|
90
|
+
* org linear teams [--team <id|key>] teams, and a team's projects (list_linear_targets)
|
|
91
|
+
* org linear set --team <id|key> [--project <id>|none] (set_linear_target)
|
|
92
|
+
* org linear disconnect (disconnect_linear)
|
|
93
|
+
*/
|
|
94
|
+
async function orgLinear(ctx) {
|
|
95
|
+
const { client, flags, io } = ctx;
|
|
96
|
+
const sub = ctx.positionals[0] ?? "show";
|
|
97
|
+
const call = async (tool, allowed) => {
|
|
98
|
+
refuseUnknownFlags(ctx, {}, allowed);
|
|
99
|
+
const result = await client.callTool(tool, argsFromFlags(ARGS[tool], flags, ctx.positionals.slice(1)));
|
|
100
|
+
ok(ctx, result.message);
|
|
101
|
+
return result;
|
|
102
|
+
};
|
|
103
|
+
switch (sub) {
|
|
104
|
+
case "show": {
|
|
105
|
+
const result = await call("get_linear_connection", []);
|
|
106
|
+
printResult(io, flags, result, (r) => r.message);
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
case "teams": {
|
|
110
|
+
const result = await call("list_linear_targets", ["team"]);
|
|
111
|
+
printResult(io, flags, result, (r) =>
|
|
112
|
+
[
|
|
113
|
+
...r.teams.map((t) => `${t.id === r.team_id ? "●" : "○"} ${t.key} ${t.name} (${t.id})`),
|
|
114
|
+
...(r.projects.length ? ["", "Projects:", ...r.projects.map((p) => ` ${p.name} (${p.id})`)] : []),
|
|
115
|
+
].join("\n")
|
|
116
|
+
);
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
case "set": {
|
|
120
|
+
const result = await call("set_linear_target", ["team", "project"]);
|
|
121
|
+
printResult(io, flags, result, (r) => r.message);
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
case "disconnect": {
|
|
125
|
+
const result = await call("disconnect_linear", []);
|
|
126
|
+
printResult(io, flags, result, (r) => r.message);
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
default:
|
|
130
|
+
throw new CliError(`Unknown org linear subcommand: ${sub}. Try: teams, set, disconnect`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
78
134
|
const ROOT_FILE = "organization.json";
|
|
79
135
|
|
|
80
136
|
function slug(name) {
|
package/lib/commands/repo.mjs
CHANGED
|
@@ -49,7 +49,7 @@ const ARGS = {
|
|
|
49
49
|
instructions: { value: (flags) => readTextInput(flags, "instructions", "instructions-file") },
|
|
50
50
|
// --attach <path> (uploaded first) and --attach-url <name>=<https url>, both repeatable.
|
|
51
51
|
files: { value: (_flags, _positionals, extra) => (extra.files?.length ? extra.files : undefined) },
|
|
52
|
-
action: { flag: "action", enum: ["build", "build_pr", "build_merge", "slack"] },
|
|
52
|
+
action: { flag: "action", enum: ["build", "build_pr", "build_merge", "slack", "linear"] },
|
|
53
53
|
preview: { flag: "preview", type: "bool" },
|
|
54
54
|
},
|
|
55
55
|
create_repo_change_upload: { file_names: { internal: "the basenames of the --attach files" } },
|
|
@@ -74,21 +74,21 @@ const ARGS = {
|
|
|
74
74
|
issue_types: { value: (flags) => (issueList(flags).length > 1 ? issueList(flags) : undefined) },
|
|
75
75
|
audit_id: "audit",
|
|
76
76
|
base_branch: "branch",
|
|
77
|
-
action: { flag: "action", enum: ["build", "build_pr", "build_merge", "slack"] },
|
|
77
|
+
action: { flag: "action", enum: ["build", "build_pr", "build_merge", "slack", "linear"] },
|
|
78
78
|
preview: { flag: "preview", type: "bool" },
|
|
79
79
|
},
|
|
80
80
|
letterprove_open_install_pr: {
|
|
81
81
|
repo_owner: { value: (flags) => splitRepo(flags).owner },
|
|
82
82
|
repo_name: { value: (flags) => splitRepo(flags).name },
|
|
83
83
|
base_branch: "branch",
|
|
84
|
-
action: { flag: "action", enum: ["build", "build_pr", "build_merge", "slack"] },
|
|
84
|
+
action: { flag: "action", enum: ["build", "build_pr", "build_merge", "slack", "linear"] },
|
|
85
85
|
},
|
|
86
86
|
get_repo_change_settings: {},
|
|
87
87
|
list_suggested_fixes: {},
|
|
88
88
|
list_slack_fix_targets: {},
|
|
89
89
|
send_slack_test_message: {},
|
|
90
90
|
set_repo_change_settings: {
|
|
91
|
-
default_action: { flag: "action", enum: ["build", "build_pr", "build_merge", "slack"] },
|
|
91
|
+
default_action: { flag: "action", enum: ["build", "build_pr", "build_merge", "slack", "linear"] },
|
|
92
92
|
// --branch <name>, or --branch none for each repo's own default.
|
|
93
93
|
default_branch: { value: (flags) => (flags.branch === "none" ? null : flagStr(flags.branch)) },
|
|
94
94
|
// --slack-agent Devin (or a U… member id), or --slack-agent none to tag nobody.
|
|
@@ -115,6 +115,8 @@ const ARGS = {
|
|
|
115
115
|
.map((c) => c.trim())
|
|
116
116
|
.filter(Boolean),
|
|
117
117
|
},
|
|
118
|
+
// --linear-reports off|as_found: file each new finding as a Linear issue
|
|
119
|
+
linear_reports: { flag: "linear-reports", enum: ["off", "as_found"] },
|
|
118
120
|
// --listen-all on|off: answer messages meant for Letter in every channel it's in
|
|
119
121
|
slack_listen_all: {
|
|
120
122
|
value: (flags) =>
|
package/lib/commands/site.mjs
CHANGED
|
@@ -177,7 +177,7 @@ const ARGS = {
|
|
|
177
177
|
},
|
|
178
178
|
base_branch: { flag: "base-branch" },
|
|
179
179
|
preview: { flag: "preview", type: "bool" },
|
|
180
|
-
action: { flag: "action", enum: ["build", "build_pr", "build_merge", "slack"] },
|
|
180
|
+
action: { flag: "action", enum: ["build", "build_pr", "build_merge", "slack", "linear"] },
|
|
181
181
|
},
|
|
182
182
|
};
|
|
183
183
|
|