@letterstory/cli 0.145.3 → 0.146.1
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 +12 -0
- package/lib/cli.mjs +16 -1
- package/lib/commands/admin.mjs +149 -2
- package/lib/commands/org.mjs +56 -3
- package/lib/commands/site.mjs +28 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -442,6 +442,18 @@ letterstory admin recipes save --name "Get cited" --prompt "…" --goals geo --c
|
|
|
442
442
|
letterstory admin recipes save <id> --disable # retire one (delete <id> --yes removes it)
|
|
443
443
|
```
|
|
444
444
|
|
|
445
|
+
### Staff: Build's suggestion recipes
|
|
446
|
+
|
|
447
|
+
```bash
|
|
448
|
+
# The same staff key (recipes:read / recipes:write). Build's suggestion recipes are plain-language
|
|
449
|
+
# ways Letterpress finds what a site should build: what to look at, what an obvious yes is, what is
|
|
450
|
+
# not one, an example. Guidance, never rules: each proposal still cites evidence and passes Jev.
|
|
451
|
+
letterstory admin suggestion-recipes export --out suggestion-recipes.json
|
|
452
|
+
letterstory admin suggestion-recipes import suggestion-recipes.json --dry-run
|
|
453
|
+
letterstory admin suggestion-recipes save --name "Docs it already writes" --look-at "…" --obvious-yes "…"
|
|
454
|
+
letterstory admin suggestion-recipes stats # per recipe: shown, dismissed, and why
|
|
455
|
+
```
|
|
456
|
+
|
|
445
457
|
### Staff: the CRM pipeline
|
|
446
458
|
|
|
447
459
|
```bash
|
package/lib/cli.mjs
CHANGED
|
@@ -64,7 +64,8 @@ import {
|
|
|
64
64
|
} from "./commands.mjs";
|
|
65
65
|
|
|
66
66
|
// Matches cli/package.json; publish-cli.yml rewrites both to the published version.
|
|
67
|
-
|
|
67
|
+
// Matches cli/package.json; publish-cli.yml rewrites both to the published version.
|
|
68
|
+
export const VERSION = "0.146.1";
|
|
68
69
|
|
|
69
70
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
70
71
|
// can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
|
|
@@ -843,6 +844,15 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
|
|
|
843
844
|
[--primitives a,b|none] [--goals a,b|none] [--company-types a,b|none] [--enable|--disable] [--position n]
|
|
844
845
|
Create one (no id) or change one. Needs recipes:write.
|
|
845
846
|
admin recipes delete <id> --yes Delete for good (save --disable retires and keeps it).
|
|
847
|
+
admin suggestion-recipes list|export [--out f.json]|import <f.json> [--dry-run] [--retire-missing]|stats
|
|
848
|
+
Build's suggestion recipes: plain-language ways
|
|
849
|
+
Letterpress finds what a site should build (look at,
|
|
850
|
+
obvious yes, not a yes, example). stats: per recipe,
|
|
851
|
+
suggestions shown and dismissed, with the reasons.
|
|
852
|
+
admin suggestion-recipes save [<id>] [--file r.json] [--name …] [--look-at …] [--obvious-yes …] [--not-a-yes …]
|
|
853
|
+
[--example …] [--company-types a,b|none] [--enable|--disable] [--position n]
|
|
854
|
+
Create one (no id) or change one. Needs recipes:write.
|
|
855
|
+
admin suggestion-recipes delete <id> --yes Delete for good (save --disable retires and keeps it).
|
|
846
856
|
admin brand-detection <org-id> [--term <t>]... [--exact <t>]... [--exclude <phrase>]... [--note "…"] | --clear [--note "…"]
|
|
847
857
|
How a client's brand is detected in AI answers, with
|
|
848
858
|
every earlier setting; with flags, set the exact set
|
|
@@ -935,6 +945,7 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
|
|
|
935
945
|
| reconcile [--org <org-id>] | forward
|
|
936
946
|
| payments [--org <org-id>] [--open] | payments retry <lago-invoice-id> | payments run [--dry]
|
|
937
947
|
| signups [--status open|card_saved|provisioned|paid|abandoned]
|
|
948
|
+
| cap <org-id> <dollars|off> [--why "…"]
|
|
938
949
|
Usage billing on Lago: the plan catalog, an org's
|
|
939
950
|
plan and gifted ratio, Lago's tokens against ours,
|
|
940
951
|
forwarding usage now, failed payments and the
|
|
@@ -1092,6 +1103,10 @@ Seers (event-driven signals -> drafts):
|
|
|
1092
1103
|
org export --split <dir> …as one file per record (for keeping config in git)
|
|
1093
1104
|
org join <dir> [--file <path>] Rebuild the bundle from a --split directory
|
|
1094
1105
|
org integrations Every integration and whether it's connected
|
|
1106
|
+
org billing [cap <dollars|off> [--why "…"] | request [--note "…"]]
|
|
1107
|
+
Usage billing: plan, this period against the
|
|
1108
|
+
allowance and spend cap, failed payments; set
|
|
1109
|
+
the cap (admins) or ask the admins for more
|
|
1095
1110
|
org linear [teams [--team <id|key>] | set --team <id|key> [--project <id>|none] | disconnect]
|
|
1096
1111
|
Where Letter files Linear issues (connect in Settings)
|
|
1097
1112
|
org diff <file|dir> What applying a bundle would change (reads only)
|
package/lib/commands/admin.mjs
CHANGED
|
@@ -4885,6 +4885,129 @@ async function cmdAdminRepublish(ctx) {
|
|
|
4885
4885
|
return failed.length ? 1 : 0;
|
|
4886
4886
|
}
|
|
4887
4887
|
|
|
4888
|
+
const SUGGESTION_RECIPES = "/api/superadmin/suggestion-recipes";
|
|
4889
|
+
const SUGGESTION_RECIPE_FIELDS = [
|
|
4890
|
+
"id",
|
|
4891
|
+
"name",
|
|
4892
|
+
"look_at",
|
|
4893
|
+
"obvious_yes",
|
|
4894
|
+
"not_a_yes",
|
|
4895
|
+
"example",
|
|
4896
|
+
"company_types",
|
|
4897
|
+
"enabled",
|
|
4898
|
+
"position",
|
|
4899
|
+
];
|
|
4900
|
+
const SUGGESTION_RECIPES_USAGE =
|
|
4901
|
+
"Usage: admin suggestion-recipes list | export [--out <file>] | import <file> [--dry-run] [--retire-missing] | save [<id>] [--name …] [--look-at …] [--obvious-yes …] [--not-a-yes …] [--example …] [--company-types a,b|none] [--enable|--disable] [--position n] | delete <id> --yes | stats";
|
|
4902
|
+
|
|
4903
|
+
const suggestionRecipeForFile = (r) => Object.fromEntries(SUGGESTION_RECIPE_FIELDS.map((k) => [k, r[k]]));
|
|
4904
|
+
|
|
4905
|
+
/**
|
|
4906
|
+
* `admin suggestion-recipes …` — the ways Build's suggestion pass looks for what a site should
|
|
4907
|
+
* build (src/lib/suggestion-recipes): guidance Letterpress reads, never rules. The loop is
|
|
4908
|
+
* `export --out recipes.json` → edit → `import recipes.json --dry-run` → `import recipes.json`,
|
|
4909
|
+
* and `stats` says how each recipe's suggestions fare (shown, dismissed, why). Reads need
|
|
4910
|
+
* recipes:read on the staff key, writes recipes:write.
|
|
4911
|
+
*/
|
|
4912
|
+
async function cmdAdminSuggestionRecipes(ctx) {
|
|
4913
|
+
const { flags, positionals, io } = ctx;
|
|
4914
|
+
const all = async () => (await staffRequest(ctx, "GET", SUGGESTION_RECIPES)).recipes ?? [];
|
|
4915
|
+
switch (positionals[0]) {
|
|
4916
|
+
case "list": {
|
|
4917
|
+
const recipes = await all();
|
|
4918
|
+
printResult(io, flags, { recipes }, (r) =>
|
|
4919
|
+
r.recipes.length
|
|
4920
|
+
? r.recipes
|
|
4921
|
+
.map(
|
|
4922
|
+
(x) =>
|
|
4923
|
+
`${String(x.position).padStart(4)} ${x.enabled ? "on " : "off"} ${x.name} [${x.company_types?.length ? x.company_types.join(",") : "every kind"}] ${x.id}`
|
|
4924
|
+
)
|
|
4925
|
+
.join("\n")
|
|
4926
|
+
: "No recipes yet."
|
|
4927
|
+
);
|
|
4928
|
+
return 0;
|
|
4929
|
+
}
|
|
4930
|
+
case "export": {
|
|
4931
|
+
const file = { recipes: (await all()).map(suggestionRecipeForFile) };
|
|
4932
|
+
const out = flagStr(flags.out);
|
|
4933
|
+
if (out) {
|
|
4934
|
+
writeFileSync(out, `${JSON.stringify(file, null, 2)}\n`);
|
|
4935
|
+
ok(ctx, `Wrote ${file.recipes.length} recipe(s) to ${out}.`);
|
|
4936
|
+
} else io.log(JSON.stringify(file, null, 2));
|
|
4937
|
+
return 0;
|
|
4938
|
+
}
|
|
4939
|
+
case "import": {
|
|
4940
|
+
const path = requirePositional(positionals, 1, "file");
|
|
4941
|
+
const parsed = readJson(path);
|
|
4942
|
+
const recipes = Array.isArray(parsed) ? parsed : parsed?.recipes;
|
|
4943
|
+
if (!Array.isArray(recipes)) throw new CliError(`${path}: expected { "recipes": [...] } or an array`);
|
|
4944
|
+
const result = await staffPost(ctx, `${SUGGESTION_RECIPES}/import`, {
|
|
4945
|
+
recipes: recipes.map((r) => compact(suggestionRecipeForFile(r))),
|
|
4946
|
+
dry_run: flagBool(flags["dry-run"]),
|
|
4947
|
+
retire_missing: flagBool(flags["retire-missing"]),
|
|
4948
|
+
});
|
|
4949
|
+
printResult(io, flags, result, (r) =>
|
|
4950
|
+
[
|
|
4951
|
+
...r.results.map(
|
|
4952
|
+
(row) =>
|
|
4953
|
+
`${row.action.padEnd(9)} ${row.name}${row.changed.length && row.action !== "create" ? ` (${row.changed.join(", ")})` : ""}${row.error ? ` → ${row.error}` : ""}`
|
|
4954
|
+
),
|
|
4955
|
+
r.dry_run ? "Nothing was written (--dry-run)." : "",
|
|
4956
|
+
]
|
|
4957
|
+
.filter(Boolean)
|
|
4958
|
+
.join("\n")
|
|
4959
|
+
);
|
|
4960
|
+
return result.results?.some((r) => r.action === "failed") ? 1 : 0;
|
|
4961
|
+
}
|
|
4962
|
+
case "save": {
|
|
4963
|
+
const fields = compact({
|
|
4964
|
+
name: flagStr(flags.name),
|
|
4965
|
+
look_at: flagStr(flags["look-at"]),
|
|
4966
|
+
obvious_yes: flagStr(flags["obvious-yes"]),
|
|
4967
|
+
not_a_yes: flagStr(flags["not-a-yes"]),
|
|
4968
|
+
example: flagStr(flags.example),
|
|
4969
|
+
company_types: recipeListFlag(flags, "company-types"),
|
|
4970
|
+
enabled: flagBool(flags.disable) ? false : flagBool(flags.enable) ? true : undefined,
|
|
4971
|
+
position: flagNum(flags.position),
|
|
4972
|
+
});
|
|
4973
|
+
const id = positionals[1];
|
|
4974
|
+
const result = id
|
|
4975
|
+
? await staffRequest(ctx, "PATCH", `${SUGGESTION_RECIPES}/${encodeURIComponent(id)}`, fields)
|
|
4976
|
+
: await staffPost(ctx, SUGGESTION_RECIPES, fields);
|
|
4977
|
+
ok(ctx, `${result.created ? "Created" : "Saved"} "${result.recipe.name}" (${result.recipe.id}).`);
|
|
4978
|
+
printResult(io, flags, result);
|
|
4979
|
+
return 0;
|
|
4980
|
+
}
|
|
4981
|
+
case "delete": {
|
|
4982
|
+
const id = requirePositional(positionals, 1, "id");
|
|
4983
|
+
if (!flagBool(flags.yes)) {
|
|
4984
|
+
io.error(`Re-run with --yes to delete for good (save ${id} --disable retires one and keeps it).`);
|
|
4985
|
+
return 1;
|
|
4986
|
+
}
|
|
4987
|
+
const result = await staffRequest(ctx, "DELETE", `${SUGGESTION_RECIPES}/${encodeURIComponent(id)}`);
|
|
4988
|
+
ok(ctx, `Deleted ${id}.`);
|
|
4989
|
+
printResult(io, flags, result);
|
|
4990
|
+
return 0;
|
|
4991
|
+
}
|
|
4992
|
+
case "stats": {
|
|
4993
|
+
const result = await staffRequest(ctx, "GET", `${SUGGESTION_RECIPES}/stats`);
|
|
4994
|
+
printResult(io, flags, result, (r) =>
|
|
4995
|
+
r.stats.length
|
|
4996
|
+
? r.stats
|
|
4997
|
+
.map(
|
|
4998
|
+
(s) =>
|
|
4999
|
+
`${String(s.shown).padStart(5)} shown ${String(s.dismissed).padStart(4)} dismissed ${s.recipe}${s.reasons.length ? `\n why: ${s.reasons.slice(0, 3).join(" · ")}` : ""}`
|
|
5000
|
+
)
|
|
5001
|
+
.join("\n")
|
|
5002
|
+
: "No suggestions from the pass yet."
|
|
5003
|
+
);
|
|
5004
|
+
return 0;
|
|
5005
|
+
}
|
|
5006
|
+
default:
|
|
5007
|
+
throw new CliError(SUGGESTION_RECIPES_USAGE);
|
|
5008
|
+
}
|
|
5009
|
+
}
|
|
5010
|
+
|
|
4888
5011
|
const RECIPES = "/api/superadmin/plan-recipes";
|
|
4889
5012
|
/** The fields a recipe file carries: what import_plan_recipes reads back. */
|
|
4890
5013
|
const RECIPE_FILE_FIELDS = [
|
|
@@ -5089,7 +5212,8 @@ const LAGO_USAGE = `Usage: admin billing lago <command>
|
|
|
5089
5212
|
payments [--org <org-id>] [--open] failed payments and what the retry loop did about each
|
|
5090
5213
|
payments retry <lago-invoice-id> charge one failed invoice again now
|
|
5091
5214
|
payments run [--dry] one pass of the failed-payment loop now (--dry: report only)
|
|
5092
|
-
signups [--status open|card_saved|provisioned|paid|abandoned] new paying orgs on their way onto Lago
|
|
5215
|
+
signups [--status open|card_saved|provisioned|paid|abandoned] new paying orgs on their way onto Lago
|
|
5216
|
+
cap <org-id> <dollars|off> [--why "…"] set an account's monthly spend cap on overage (off: no cap)`;
|
|
5093
5217
|
|
|
5094
5218
|
/** admin billing lago … — staff operations on Lago (docs/billing/lago-integration.md). */
|
|
5095
5219
|
async function cmdAdminBilling(ctx) {
|
|
@@ -5212,6 +5336,27 @@ async function cmdAdminBilling(ctx) {
|
|
|
5212
5336
|
);
|
|
5213
5337
|
return 0;
|
|
5214
5338
|
}
|
|
5339
|
+
case "cap": {
|
|
5340
|
+
const orgId = positionals[2];
|
|
5341
|
+
const value = positionals[3];
|
|
5342
|
+
if (!orgId || value === undefined) throw new CliError(LAGO_USAGE);
|
|
5343
|
+
const off = ["off", "none", "null"].includes(value.toLowerCase());
|
|
5344
|
+
const capUsd = off ? null : Number(value.replace(/^\$/, ""));
|
|
5345
|
+
if (capUsd !== null && (!Number.isFinite(capUsd) || capUsd < 0))
|
|
5346
|
+
throw new CliError(`Not a cap: ${value}. Give dollars (0 stops at the allowance) or off.`);
|
|
5347
|
+
const result = await staffRequest(
|
|
5348
|
+
ctx,
|
|
5349
|
+
"POST",
|
|
5350
|
+
`${LAGO}/cap`,
|
|
5351
|
+
compact({ orgId, cap_usd: capUsd, reason: flagStr(flags.why) })
|
|
5352
|
+
);
|
|
5353
|
+
printResult(io, flags, result, (r) =>
|
|
5354
|
+
r.spendCap
|
|
5355
|
+
? `Spend cap: $${r.spendCap.usd} of overage a month (alerts: ${r.alert}).`
|
|
5356
|
+
: "No spend cap: overage is on."
|
|
5357
|
+
);
|
|
5358
|
+
return 0;
|
|
5359
|
+
}
|
|
5215
5360
|
default:
|
|
5216
5361
|
throw new CliError(LAGO_USAGE);
|
|
5217
5362
|
}
|
|
@@ -5237,8 +5382,10 @@ export async function cmdAdmin(ctx) {
|
|
|
5237
5382
|
if (sub === "brand-detection") return cmdAdminBrandDetection({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
5238
5383
|
if (sub === "republish") return cmdAdminRepublish({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
5239
5384
|
if (sub === "recipes") return cmdAdminRecipes({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
5385
|
+
if (sub === "suggestion-recipes")
|
|
5386
|
+
return cmdAdminSuggestionRecipes({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
5240
5387
|
throw new CliError(
|
|
5241
|
-
'Usage: admin recipes … | admin republish … | 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)'
|
|
5388
|
+
'Usage: admin recipes … | admin suggestion-recipes … | admin republish … | 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)'
|
|
5242
5389
|
);
|
|
5243
5390
|
}
|
|
5244
5391
|
|
package/lib/commands/org.mjs
CHANGED
|
@@ -28,6 +28,11 @@ export const ARGS = {
|
|
|
28
28
|
describe_org_config_fields: {},
|
|
29
29
|
get_org_config_source: {},
|
|
30
30
|
get_billing_payments: {},
|
|
31
|
+
set_billing_spend_cap: {
|
|
32
|
+
cap_usd: { value: (_flags, positionals) => capFrom(requirePositional(positionals, 0, "dollars|off")) },
|
|
33
|
+
reason: { flag: "why" },
|
|
34
|
+
},
|
|
35
|
+
request_billing_tokens: { note: { flag: "note" } },
|
|
31
36
|
list_integrations: {},
|
|
32
37
|
get_linear_connection: {},
|
|
33
38
|
list_linear_targets: { team_id: { flag: "team" } },
|
|
@@ -90,9 +95,45 @@ async function orgIntegrations(ctx) {
|
|
|
90
95
|
return 0;
|
|
91
96
|
}
|
|
92
97
|
|
|
93
|
-
/**
|
|
98
|
+
/** A cap from the command line: dollars, or `off` (no cap). */
|
|
99
|
+
function capFrom(value) {
|
|
100
|
+
if (["off", "none", "null"].includes(String(value).toLowerCase())) return null;
|
|
101
|
+
const usd = Number(String(value).replace(/^\$/, ""));
|
|
102
|
+
if (!Number.isFinite(usd) || usd < 0)
|
|
103
|
+
throw new CliError(`Not a cap: ${value}. Give dollars (0 stops at the allowance) or off.`);
|
|
104
|
+
return usd;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const usd = (n) => `$${Number(n).toFixed(2)}`;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* `org billing`: the org's plan on Lago, this period against its allowance and spend cap, whether
|
|
111
|
+
* it is past due, and its failed payments (get_billing_payments). `org billing cap <dollars|off>`
|
|
112
|
+
* sets the cap (set_billing_spend_cap, admins); `org billing request` asks the admins for more
|
|
113
|
+
* (request_billing_tokens).
|
|
114
|
+
*/
|
|
94
115
|
async function orgBilling(ctx) {
|
|
95
|
-
const { client, flags, io } = ctx;
|
|
116
|
+
const { client, flags, io, positionals } = ctx;
|
|
117
|
+
if (positionals[0] === "cap") {
|
|
118
|
+
refuseUnknownFlags(ctx, ARGS.set_billing_spend_cap, []);
|
|
119
|
+
const args = argsFromFlags(ARGS.set_billing_spend_cap, flags, positionals.slice(1));
|
|
120
|
+
const result = await client.callTool("set_billing_spend_cap", args);
|
|
121
|
+
printResult(io, flags, result, (r) =>
|
|
122
|
+
r.spendCap
|
|
123
|
+
? `Spend cap: ${usd(r.spendCap.usd)} of overage a month${r.spendCap.usd === 0 ? " (work stops at the allowance)" : ""}.${r.alert === "failed" ? " The alerts couldn't be moved; the cap holds." : ""}`
|
|
124
|
+
: "No spend cap: overage is on."
|
|
125
|
+
);
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
if (positionals[0] === "request") {
|
|
129
|
+
refuseUnknownFlags(ctx, ARGS.request_billing_tokens, []);
|
|
130
|
+
const result = await client.callTool(
|
|
131
|
+
"request_billing_tokens",
|
|
132
|
+
argsFromFlags(ARGS.request_billing_tokens, flags, [])
|
|
133
|
+
);
|
|
134
|
+
printResult(io, flags, result, (r) => `Asked ${r.emailedTo} admin${r.emailedTo === 1 ? "" : "s"} for more.`);
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
96
137
|
refuseUnknownFlags(ctx, {}, []);
|
|
97
138
|
const result = await client.callTool("get_billing_payments", {});
|
|
98
139
|
printResult(io, flags, result, (r) =>
|
|
@@ -100,6 +141,16 @@ async function orgBilling(ctx) {
|
|
|
100
141
|
r.plan
|
|
101
142
|
? `Plan: ${r.plan.name}${r.plan.ratio !== 1 ? ` (${r.plan.ratio}x)` : ""}`
|
|
102
143
|
: "Not billed by usage yet.",
|
|
144
|
+
...(r.usage
|
|
145
|
+
? [
|
|
146
|
+
`This period (${r.usage.periodFrom.slice(0, 10)} to ${r.usage.periodTo.slice(0, 10)}): ${r.usage.usedTokens} of ${r.usage.allowanceTokens} tokens included${r.usage.overageUsd > 0 ? `, ${usd(r.usage.overageUsd)} of overage` : ""}.`,
|
|
147
|
+
]
|
|
148
|
+
: []),
|
|
149
|
+
r.spendCap
|
|
150
|
+
? `Spend cap: ${usd(r.spendCap.usd)} of overage a month${r.usage?.availableTokens != null ? `, ${Math.max(0, r.usage.availableTokens)} tokens left under it` : ""}.`
|
|
151
|
+
: r.plan
|
|
152
|
+
? "No spend cap: overage is on."
|
|
153
|
+
: null,
|
|
103
154
|
...(r.pastDueAt ? [`Past due since ${r.pastDueAt}.`] : []),
|
|
104
155
|
...(r.payments.length
|
|
105
156
|
? r.payments.map(
|
|
@@ -107,7 +158,9 @@ async function orgBilling(ctx) {
|
|
|
107
158
|
` ${p.invoiceNumber ?? p.invoiceId} $${(p.amountCents / 100).toFixed(2)} ${p.status}${p.nextAttemptAt ? `, next try ${p.nextAttemptAt}` : ""}${p.payUrl ? ` pay: ${p.payUrl}` : ""}`
|
|
108
159
|
)
|
|
109
160
|
: ["No failed payments."]),
|
|
110
|
-
]
|
|
161
|
+
]
|
|
162
|
+
.filter((line) => line !== null)
|
|
163
|
+
.join("\n")
|
|
111
164
|
);
|
|
112
165
|
return 0;
|
|
113
166
|
}
|
package/lib/commands/site.mjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
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
5
|
// site builds <host> → list_build_suggestions (--limit n)
|
|
6
|
+
// site research-builds <host> → research_builds (Build's suggestion pass: propose, check, Jev, store)
|
|
6
7
|
// site metrics <host> [path] → get_site_metrics (--window 7d|28d|90d, --page <path>…, --no-compare)
|
|
7
8
|
// site observations <host> → list_site_observations site observe <host> → observe_site
|
|
8
9
|
// site dismiss <host> <id> → dismiss_site_observation
|
|
@@ -83,6 +84,9 @@ const ARGS = {
|
|
|
83
84
|
host: { positional: 0, required: true, label: "host" },
|
|
84
85
|
limit: { flag: "limit", type: "num" },
|
|
85
86
|
},
|
|
87
|
+
research_builds: {
|
|
88
|
+
host: { positional: 0, required: true, label: "host" },
|
|
89
|
+
},
|
|
86
90
|
get_site_metrics: {
|
|
87
91
|
host: { positional: 0, required: true, label: "host" },
|
|
88
92
|
path: { positional: 1, label: "path" },
|
|
@@ -204,6 +208,8 @@ export async function cmdSite(ctx) {
|
|
|
204
208
|
return siteUnmap(rest);
|
|
205
209
|
case "tree":
|
|
206
210
|
return siteTree(rest);
|
|
211
|
+
case "research-builds":
|
|
212
|
+
return siteResearchBuilds(rest);
|
|
207
213
|
case "builds":
|
|
208
214
|
return siteBuilds(rest);
|
|
209
215
|
case "metrics":
|
|
@@ -406,6 +412,28 @@ async function siteBuilds(ctx) {
|
|
|
406
412
|
);
|
|
407
413
|
if (!lines.length) lines.push("Nothing to build: no source makes a surface this site's right now.");
|
|
408
414
|
if (r.skipped.length) lines.push(`Not read: ${r.skipped.join(", ")}`);
|
|
415
|
+
lines.push(
|
|
416
|
+
r.pass
|
|
417
|
+
? `Suggestion pass ${r.pass.ranAt}: kept ${r.pass.kept}, dropped ${r.pass.rejected}.`
|
|
418
|
+
: "Suggestion pass: never run (site research-builds <host>)."
|
|
419
|
+
);
|
|
420
|
+
return lines.join("\n");
|
|
421
|
+
});
|
|
422
|
+
return 0;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function siteResearchBuilds(ctx) {
|
|
426
|
+
const { client, flags, io } = ctx;
|
|
427
|
+
refuseUnknownFlags(ctx, ARGS.research_builds);
|
|
428
|
+
const result = await client.callTool(
|
|
429
|
+
"research_builds",
|
|
430
|
+
argsFromFlags(ARGS.research_builds, flags, ctx.positionals)
|
|
431
|
+
);
|
|
432
|
+
printResult(io, flags, result, (r) => {
|
|
433
|
+
if (!r.ran) return `Not run (${r.reason ?? "fresh"}); list_build_suggestions shows the stored pass.`;
|
|
434
|
+
const lines = r.suggestions.map((s) => `KEPT ${s.summary}\n ${s.relevance}\n ${s.id}`);
|
|
435
|
+
for (const x of r.rejected) lines.push(`DROPPED ${x.summary || x.surface}: ${x.reason}`);
|
|
436
|
+
if (!r.suggestions.length) lines.push("Nothing passed: zero is a valid result.");
|
|
409
437
|
return lines.join("\n");
|
|
410
438
|
});
|
|
411
439
|
return 0;
|