@letterstory/cli 0.126.0 → 0.128.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/lib/cli.mjs +46 -4
- package/lib/commands/admin.mjs +108 -4
- package/lib/commands/automations.mjs +41 -3
- package/lib/commands/collection.mjs +3 -1
- package/lib/commands/enclosures.mjs +389 -0
- package/lib/commands/site.mjs +20 -5
- package/lib/commands/visits.mjs +34 -1
- package/lib/commands.mjs +1 -0
- package/lib/documents.mjs +2 -0
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -56,22 +56,36 @@ import {
|
|
|
56
56
|
cmdSite,
|
|
57
57
|
cmdEvals,
|
|
58
58
|
cmdOfferings,
|
|
59
|
+
cmdEnclosures,
|
|
59
60
|
cmdTargets,
|
|
60
61
|
cmdVisits,
|
|
61
62
|
} from "./commands.mjs";
|
|
62
63
|
|
|
63
64
|
// Keep in sync with cli/package.json.
|
|
64
|
-
export const VERSION = "0.
|
|
65
|
+
export const VERSION = "0.128.1";
|
|
65
66
|
|
|
66
67
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
67
68
|
// can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
|
|
68
69
|
// `negate` spelling) in a command's ARGS map must be listed; boolean-flags.test.ts enforces it.
|
|
69
70
|
export const BOOLEAN_FLAGS = new Set([
|
|
71
|
+
"remap",
|
|
70
72
|
"json",
|
|
71
73
|
// admin models runs: only the starred ones.
|
|
72
74
|
"starred",
|
|
73
75
|
// shredder file / arena file: save the version as a candidate beside live.
|
|
74
76
|
"candidate",
|
|
77
|
+
// enclosures create/update: what it needs, and the AI-edited label.
|
|
78
|
+
"browser",
|
|
79
|
+
"image-model",
|
|
80
|
+
"record",
|
|
81
|
+
"stealth",
|
|
82
|
+
"disclosure",
|
|
83
|
+
"no-disclosure",
|
|
84
|
+
"no-cta",
|
|
85
|
+
"remember",
|
|
86
|
+
"no-remember",
|
|
87
|
+
// enclosures print: make a new one even for people with a recent print.
|
|
88
|
+
"redo",
|
|
75
89
|
// site tree: include children a fold-by-default family would hide; items over the whole subtree.
|
|
76
90
|
"unfold",
|
|
77
91
|
"subtree",
|
|
@@ -272,6 +286,8 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
272
286
|
"not-reached",
|
|
273
287
|
"reached",
|
|
274
288
|
"todo",
|
|
289
|
+
// admin spend margin: list nested work (never billed twice) beside its parent.
|
|
290
|
+
"nested",
|
|
275
291
|
]);
|
|
276
292
|
|
|
277
293
|
// Tiny argv parser: `--flag value`, `--flag=value`, boolean `--flag`, and positionals.
|
|
@@ -350,7 +366,7 @@ Collections (the content engine as a primitive — one door, 'collection run'):
|
|
|
350
366
|
Print the ONE FILE (identity, content, cadence,
|
|
351
367
|
publishing, targets, measurement) — or PATCH it
|
|
352
368
|
collection run --collection <id> [--topic <t> | --spoke <topic-id> | --headline <suggestion-id>]
|
|
353
|
-
[--source <url> …] [--must-include <t> …] [--depth barebones|fleshed_out]
|
|
369
|
+
[--source <url> …] [--notes <text>] [--must-include <t> …] [--depth barebones|fleshed_out]
|
|
354
370
|
[--kernel <id>] [--no-verify] [--shred] [--mention off|subtle|woven]
|
|
355
371
|
[--approval autopilot|review_window|manual] [--no-publish] [--no-rebuild]
|
|
356
372
|
[--at <iso-datetime>] [--no-wait]
|
|
@@ -361,7 +377,7 @@ Collections (the content engine as a primitive — one door, 'collection run'):
|
|
|
361
377
|
collection queue <suggestion-id> Queue a headline, as the app's queue button does: the
|
|
362
378
|
next slot of the schedule when the Planner is on,
|
|
363
379
|
else a draft to write by hand
|
|
364
|
-
collection add "<headline>" --collection <id> [--queue]
|
|
380
|
+
collection add "<headline>" --collection <id> [--notes <text>] [--queue]
|
|
365
381
|
A headline you wrote: joins Up next, or with --queue
|
|
366
382
|
is queued at once (the next slot, or a draft)
|
|
367
383
|
collection headline <suggestion-id> [--dismiss | --later | --restore] [--headline "…"]
|
|
@@ -431,9 +447,16 @@ Reviewers:
|
|
|
431
447
|
reviewers assigned Posts waiting on your review, across letterbases
|
|
432
448
|
automations list --collection <id> What a letterbase writes, how often, and why
|
|
433
449
|
automations show <automation-id> --collection <id> What it covers, its queued posts, its next ideas
|
|
450
|
+
automations suggest --collection <id> What it could add: topics, series formats, signals
|
|
451
|
+
automations preview topic|series --collection <id> [--name <name>|--topic <id>] [--brief <text>]
|
|
452
|
+
What it would write, before adding it
|
|
453
|
+
automations more --collection <id> New topics it doesn't cover yet
|
|
454
|
+
automations dismiss <topic-id> --collection <id> Set one subject aside
|
|
434
455
|
automations add topic|series|signal|best --collection <id> [--topic <id>] [--goal <target-id>]
|
|
435
|
-
[--seer <id>] [--pace <n>]
|
|
456
|
+
[--seer <id>] [--pace <n>] [--brief <text>] [--new-topic <name>] [--new-series <name>]
|
|
457
|
+
Add one (refused if it says the same as another)
|
|
436
458
|
automations set <automation-id> --collection <id> [--pace <n>] [--pause | --resume] [--goal <id>]
|
|
459
|
+
[--brief <text>] [--remap]
|
|
437
460
|
automations remove <automation-id> --collection <id>
|
|
438
461
|
reviewers invite <article-id> --email <e> [--name <n>]
|
|
439
462
|
Invite a guest to review one post
|
|
@@ -524,6 +547,20 @@ Strategy & onboarding:
|
|
|
524
547
|
directives apply <directive_id> --effect <id>... Commit the levers you kept (or --reason to decline them all)
|
|
525
548
|
directives off <directive_id> --reason <why> Stop a live instruction; the reason is read back by the next proposal
|
|
526
549
|
|
|
550
|
+
Enclosures (pictures, clips and sentences made for each prospect; each output is a Prospects column):
|
|
551
|
+
enclosures list The org's enclosures with print counts
|
|
552
|
+
enclosures show <enclosure-id> One enclosure + its versions
|
|
553
|
+
enclosures create --name <n> --output key:shape:Label… [--instructions <text>] [--input website…] [--browser]
|
|
554
|
+
[--login <app-connection-id>] [--tool run_site_audit…] [--skip-when <sentence>]
|
|
555
|
+
enclosures update <enclosure-id> [same flags] Outputs are fixed once a version is in use
|
|
556
|
+
enclosures prints <enclosure-id> [--status <s>] Its current prints, newest first
|
|
557
|
+
enclosures starters What an enclosure can begin from (each with a version 1)
|
|
558
|
+
enclosures approve <enclosure-id> --version <n> Choose the version prints run
|
|
559
|
+
enclosures print <enclosure-id> (--prospect <id>…|--list <id>) [--dry-run] Test (3 people) or print
|
|
560
|
+
enclosures apps list|show <app-id> Your apps: sign-ins enclosures use, with their state
|
|
561
|
+
enclosures apps connect <sign-in-url> [--no-remember] A sign-in link a person opens (no password passes through)
|
|
562
|
+
enclosures apps disconnect <app-id> --yes Remove it here and at the browser provider
|
|
563
|
+
|
|
527
564
|
Offerings (context layered org → offering → collection; a layer sets only what it overrides):
|
|
528
565
|
offerings list The org's offerings (things you'd name when recommending them)
|
|
529
566
|
offerings show <offering-id> One offering + its competitor overrides
|
|
@@ -886,6 +923,8 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
|
|
|
886
923
|
| renewals [--days 90]
|
|
887
924
|
| reach-outs [--day YYYY-MM-DD] [--fresh] the day's reach-outs, N of 5 (#crm-reminders' count)
|
|
888
925
|
| track-record [--since YYYY-MM-DD] | at-risk | expansion-signals
|
|
926
|
+
| collections [--org-ids a,b | --account-stages won,active]
|
|
927
|
+
| delivery-flags [--org-ids a,b | --account-stages won,active] [--include collections]
|
|
889
928
|
| case <account-id> [--outcome <outcome-id>]
|
|
890
929
|
| onboard <account-id> --domain acme.com [--name "Acme"] [--site "<direction>"]… [--sites N]
|
|
891
930
|
[--slack a@acme.com,b@acme.com] [--invite a@acme.com[:admin]]… [--no-checklist]
|
|
@@ -1355,6 +1394,8 @@ Visits (Visitors: AI agents reading your site, and AI answers sending people to
|
|
|
1355
1394
|
Pages one agent read that the other didn't
|
|
1356
1395
|
visits export [--since 30d] [--format json|csv] [--table agent_visits|citations]
|
|
1357
1396
|
visits sites Your Visitors sites and their install checks
|
|
1397
|
+
visits pages [--since 28d] [--path '/docs/*'] [--agent <id>] [--limit <n>] [--site <id>]
|
|
1398
|
+
People and AI agents per page
|
|
1358
1399
|
|
|
1359
1400
|
Evals (how the pipeline is doing on your posts, and the regression cases captured from them):
|
|
1360
1401
|
evals scoreboard [--collection <id>] [--days N] Every measure by stage: posts, failing, rate
|
|
@@ -1419,6 +1460,7 @@ const CLIENT_COMMANDS = {
|
|
|
1419
1460
|
strategy: cmdStrategy,
|
|
1420
1461
|
directives: cmdDirectives,
|
|
1421
1462
|
offerings: cmdOfferings,
|
|
1463
|
+
enclosures: cmdEnclosures,
|
|
1422
1464
|
targets: cmdTargets,
|
|
1423
1465
|
probes: cmdProbes,
|
|
1424
1466
|
suggestions: cmdSuggestions,
|
package/lib/commands/admin.mjs
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
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|reach-outs|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
|
-
// admin spend summary|reconcile|pnl|entries|add|import|usage|remove|reference|stripe-sync|services|service … (spend:read / spend:write)
|
|
17
|
+
// admin crm import|capture|outreach|signals|record|signal|accounts|account|score|slack-channels|link-slack|white-glove|set-white-glove|churn|revenue|reach-outs|record-churn|draft-churn-reason|undo-churn|outcomes|record-outcome|renewals|track-record|at-risk|expansion-signals|collections|delivery-flags|case|onboard|onboarding|retry-onboarding|onboard-plan|payment-link … (crm:read / crm:write)
|
|
18
|
+
// admin spend summary|reconcile|pnl|margin|pricing|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)
|
|
21
21
|
// admin recipes list|show|vocab|export|import|save|delete … (recipes:read / recipes:write)
|
|
@@ -3572,6 +3572,46 @@ async function cmdAdminCrm(ctx) {
|
|
|
3572
3572
|
: "(no next-product verdicts)"
|
|
3573
3573
|
);
|
|
3574
3574
|
}
|
|
3575
|
+
case "collections": {
|
|
3576
|
+
// admin crm collections [--org-ids a,b | --account-stages won,active] — what we publish for each client.
|
|
3577
|
+
const params = new URLSearchParams(
|
|
3578
|
+
compact({ org_ids: flagStr(flags["org-ids"]), account_stages: flagStr(flags["account-stages"]) })
|
|
3579
|
+
);
|
|
3580
|
+
return out(await staffRequest(ctx, "GET", `${CRM}/collections?${params}`), (r) =>
|
|
3581
|
+
r.orgs.length
|
|
3582
|
+
? r.orgs
|
|
3583
|
+
.map(
|
|
3584
|
+
(o) =>
|
|
3585
|
+
`${o.orgName} ${o.phantomCount} phantom · ${o.mainBlogCount} blog · ${o.postsLast30d} posts in 30d${o.internal.value ? ` (${o.internal.label ?? o.internal.source})` : ""}${o.issues.length ? ` ${o.issues.map((i) => i.message).join("; ")}` : ""} ${o.orgId}`
|
|
3586
|
+
)
|
|
3587
|
+
.join("\n")
|
|
3588
|
+
: "(no org with a collection in that scope)"
|
|
3589
|
+
);
|
|
3590
|
+
}
|
|
3591
|
+
case "delivery-flags": {
|
|
3592
|
+
// admin crm delivery-flags [--org-ids a,b | --account-stages won,active] [--include collections]
|
|
3593
|
+
// — does what we spend on a client match what they pay? Findings per org, worst first.
|
|
3594
|
+
const params = new URLSearchParams(
|
|
3595
|
+
compact({
|
|
3596
|
+
org_ids: flagStr(flags["org-ids"]),
|
|
3597
|
+
account_stages: flagStr(flags["account-stages"]),
|
|
3598
|
+
include: flagStr(flags.include),
|
|
3599
|
+
})
|
|
3600
|
+
);
|
|
3601
|
+
return out(await staffRequest(ctx, "GET", `${CRM}/delivery-flags?${params}`), (r) => {
|
|
3602
|
+
if (!r.accounts)
|
|
3603
|
+
return "Billing cannot say who pays yet, so there are no flags (collections are in --json).";
|
|
3604
|
+
const flagged = r.accounts.filter((a) => a.findings.length);
|
|
3605
|
+
return flagged.length
|
|
3606
|
+
? flagged
|
|
3607
|
+
.map(
|
|
3608
|
+
(a) =>
|
|
3609
|
+
`${a.owedDelivery ? "owed " : a.notPaying ? "unpaid" : " "} ${a.orgName} ${a.findings.map((f) => f.message).join("; ")} ${a.orgId}`
|
|
3610
|
+
)
|
|
3611
|
+
.join("\n")
|
|
3612
|
+
: `(${r.accounts.length} org${r.accounts.length === 1 ? "" : "s"}, nothing flagged)`;
|
|
3613
|
+
});
|
|
3614
|
+
}
|
|
3575
3615
|
case "case": {
|
|
3576
3616
|
// admin crm case <account-id> [--outcome <id>] — the 90 days before an outcome, or before today.
|
|
3577
3617
|
if (!id) throw new CliError("Usage: admin crm case <account-id> [--outcome <outcome-id>]");
|
|
@@ -3650,7 +3690,7 @@ async function cmdAdminCrm(ctx) {
|
|
|
3650
3690
|
}
|
|
3651
3691
|
default:
|
|
3652
3692
|
throw new CliError(
|
|
3653
|
-
"Usage: admin crm import|capture|outreach|signals|record|signal|accounts|account|score|slack-channels|link-slack|white-glove|set-white-glove|churn|revenue|reach-outs|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)"
|
|
3693
|
+
"Usage: admin crm import|capture|outreach|signals|record|signal|accounts|account|score|slack-channels|link-slack|white-glove|set-white-glove|churn|revenue|reach-outs|record-churn|draft-churn-reason|undo-churn|outcomes|record-outcome|renewals|track-record|at-risk|expansion-signals|collections|delivery-flags|case|onboard|onboarding|retry-onboarding|onboard-plan|payment-link … (see --help)"
|
|
3654
3694
|
);
|
|
3655
3695
|
}
|
|
3656
3696
|
}
|
|
@@ -4224,6 +4264,70 @@ async function cmdAdminSpend(ctx) {
|
|
|
4224
4264
|
);
|
|
4225
4265
|
return 0;
|
|
4226
4266
|
}
|
|
4267
|
+
case "margin": {
|
|
4268
|
+
// Shadow usage metering: usage per SKU, our cost, draft tokens and margin
|
|
4269
|
+
// (GET /api/superadmin/spend/usage-margin → summarizeUsage).
|
|
4270
|
+
const params = new URLSearchParams(
|
|
4271
|
+
compact({
|
|
4272
|
+
from: flagStr(flags.from),
|
|
4273
|
+
to: flagStr(flags.to),
|
|
4274
|
+
orgId: flagStr(flags.org),
|
|
4275
|
+
groupBy: flagStr(flags["group-by"]),
|
|
4276
|
+
includeNested: flagBool(flags.nested) ? "true" : undefined,
|
|
4277
|
+
rateCard: flagStr(flags["rate-card"]),
|
|
4278
|
+
})
|
|
4279
|
+
);
|
|
4280
|
+
const result = await staffRequest(ctx, "GET", `${SPEND}/usage-margin${params.size ? `?${params}` : ""}`);
|
|
4281
|
+
printResult(io, flags, result, (m) =>
|
|
4282
|
+
[
|
|
4283
|
+
`${m.range.from} → ${m.range.to}: ${m.totals.billableEvents} billable of ${m.totals.events} events · ${m.totals.tokens ?? "?"} tokens (${m.rateCard?.version ?? "no card"}) · cost ${usd(m.totals.cost?.totalUsd)} · margin ${m.totals.marginUsd === null ? "?" : usd(m.totals.marginUsd)}`,
|
|
4284
|
+
"",
|
|
4285
|
+
...m.rows.map(
|
|
4286
|
+
(r) =>
|
|
4287
|
+
` ${[r.day, r.orgId, r.collectionId, r.customerRef, r.sku].filter(Boolean).join(" ").slice(0, 60).padEnd(60)} ${String(r.billableQuantity).padStart(9)} ${r.unit.padEnd(10)} ${String(r.tokens ?? "?").padStart(9)} tok cost ${usd(r.cost?.totalUsd).padStart(9)}${r.cost?.eventsWithoutCost ? ` (${r.cost.eventsWithoutCost} unpriced)` : ""}`
|
|
4288
|
+
),
|
|
4289
|
+
].join("\n")
|
|
4290
|
+
);
|
|
4291
|
+
return 0;
|
|
4292
|
+
}
|
|
4293
|
+
case "pricing": {
|
|
4294
|
+
// What shadow metering says prices should be (GET /api/superadmin/spend/pricing-read
|
|
4295
|
+
// → readPricing). Reads only; nothing changes a price.
|
|
4296
|
+
const params = new URLSearchParams(
|
|
4297
|
+
compact({
|
|
4298
|
+
from: flagStr(flags.from),
|
|
4299
|
+
to: flagStr(flags.to),
|
|
4300
|
+
multiple: flagStr(flags.multiple),
|
|
4301
|
+
tiers: flagStr(flags.tiers),
|
|
4302
|
+
rateCard: flagStr(flags["rate-card"]),
|
|
4303
|
+
})
|
|
4304
|
+
);
|
|
4305
|
+
const result = await staffRequest(ctx, "GET", `${SPEND}/pricing-read${params.size ? `?${params}` : ""}`);
|
|
4306
|
+
const cents = (c) => usd((c ?? 0) / 100);
|
|
4307
|
+
const unit = (n) => (n === null || n === undefined ? "?" : `$${Number(n).toFixed(n < 0.1 ? 4 : 2)}`);
|
|
4308
|
+
printResult(io, flags, result, (p) =>
|
|
4309
|
+
[
|
|
4310
|
+
`${p.range.from} → ${p.range.to} (${p.range.days}d, ${p.range.buckets} month buckets) · card ${p.rateCard.version}${p.rateCard.published ? "" : " (draft)"} · proposal at ${p.multiple}× p75 cost · revenue as of ${p.revenueCapturedOn ?? "no snapshot"}`,
|
|
4311
|
+
"",
|
|
4312
|
+
"Per SKU: unit cost p50 / p75 / p90 · priced of billable events · tokens a unit now → proposed",
|
|
4313
|
+
...p.skus
|
|
4314
|
+
.filter((s) => s.events > 0)
|
|
4315
|
+
.map(
|
|
4316
|
+
(s) =>
|
|
4317
|
+
` ${s.sku.padEnd(30)} ${unit(s.unitCostUsd.p50).padStart(8)} ${unit(s.unitCostUsd.p75).padStart(8)} ${unit(s.unitCostUsd.p90).padStart(8)} ${String(s.pricedEvents).padStart(6)}/${String(s.events).padEnd(6)} ${s.pricing === "per_unit" ? `${s.current ?? "?"} → ${s.proposedTokensPerUnit ?? "?"}` : `cost × ${s.current ?? "?"}`}${s.confidence === "measured" ? "" : ` (${s.confidence})`}${s.gaps.unattributed || s.gaps.unpricedCalls || s.gaps.partialOwnCost || s.gaps.withoutCost ? ` [left out: ${s.gaps.unattributed} unattributed, ${s.gaps.unpricedCalls} unpriced calls, ${s.gaps.partialOwnCost} partial own cost, ${s.gaps.withoutCost} no cost]` : ""}`
|
|
4318
|
+
),
|
|
4319
|
+
"",
|
|
4320
|
+
`Paying clients: ${p.fleet.clients} · monthly tokens p50 ${p.fleet.monthlyTokens.p50 ?? "?"} · p75 ${p.fleet.monthlyTokens.p75 ?? "?"} · p90 ${p.fleet.monthlyTokens.p90 ?? "?"}`,
|
|
4321
|
+
"Per client (child accounts rolled up): revenue/mo · tokens/mo mean, peak · cost/mo · tier · complimentary tokens",
|
|
4322
|
+
...p.clients.map(
|
|
4323
|
+
(o) =>
|
|
4324
|
+
` ${`${o.name ?? o.clientId}${o.orgIds.length > 1 ? ` (${o.orgIds.length} orgs)` : ""}`.slice(0, 28).padEnd(28)} ${cents(o.revenueMonthlyCents).padStart(9)} ${String(o.monthlyTokens.mean).padStart(9)} ${String(o.monthlyTokens.peak).padStart(9)} ${usd(o.monthlyCostUsd.mean).padStart(9)} ${o.ratioTier ? `${o.ratioTier}×` : "—"} ${o.complimentaryTokens ?? "—"}${o.flags.length ? ` [${o.flags.join(", ")}]` : ""}`
|
|
4325
|
+
),
|
|
4326
|
+
...p.notes.map((n) => `· ${n}`),
|
|
4327
|
+
].join("\n")
|
|
4328
|
+
);
|
|
4329
|
+
return 0;
|
|
4330
|
+
}
|
|
4227
4331
|
case "reference": {
|
|
4228
4332
|
const vendor = flagStr(flags.vendor);
|
|
4229
4333
|
const metric = flagStr(flags.metric);
|
|
@@ -4298,7 +4402,7 @@ async function cmdAdminSpend(ctx) {
|
|
|
4298
4402
|
}
|
|
4299
4403
|
default:
|
|
4300
4404
|
throw new CliError(
|
|
4301
|
-
"Usage: admin spend summary [--range day|week|month|year | --from D --to D] [--grain day|week|month] | reconcile [--from D --to D] | pnl [--range …] | reference add … | stripe-sync --from D --to D | entries [--from D] [--to D] [--vendor V] [--area A] [--source S] | add --vendor V --amount N --from D … | import <file.csv> … | usage <events.json> | usage list --source S --from D --to D | remove <entry-id> | services | service <slug> --owner … --monthly …"
|
|
4405
|
+
"Usage: admin spend summary [--range day|week|month|year | --from D --to D] [--grain day|week|month] | reconcile [--from D --to D] | pnl [--range …] | margin [--from D --to D] [--org <uuid>] [--group-by sku,org,collection,customer_ref,day] [--nested] [--rate-card V] | pricing [--from D --to D] [--multiple 3] [--tiers 1,2,4,8] [--rate-card V] | reference add … | stripe-sync --from D --to D | entries [--from D] [--to D] [--vendor V] [--area A] [--source S] | add --vendor V --amount N --from D … | import <file.csv> … | usage <events.json> | usage list --source S --from D --to D | remove <entry-id> | services | service <slug> --owner … --monthly …"
|
|
4302
4406
|
);
|
|
4303
4407
|
}
|
|
4304
4408
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// `automations` — a letterbase's automations (docs/architecture/automations.md): what it writes,
|
|
2
|
-
// how often, and why. list / show / add / set / remove, through the same tools the app uses.
|
|
2
|
+
// how often, and why. list / show / suggest / preview / more / add / set / dismiss / remove, through the same tools the app uses.
|
|
3
3
|
// Same flag/positional/io conventions as posts.mjs — no commander, no color.
|
|
4
4
|
|
|
5
5
|
import { CliError } from "../client.mjs";
|
|
@@ -10,6 +10,9 @@ export const ARGS = {
|
|
|
10
10
|
list_automations: {
|
|
11
11
|
collection_id: { flag: "collection", required: true },
|
|
12
12
|
},
|
|
13
|
+
suggest_automations: {
|
|
14
|
+
collection_id: { flag: "collection", required: true },
|
|
15
|
+
},
|
|
13
16
|
get_automation: {
|
|
14
17
|
collection_id: { flag: "collection", required: true },
|
|
15
18
|
automation_id: { positional: 0, label: "automation-id", required: true },
|
|
@@ -21,6 +24,13 @@ export const ARGS = {
|
|
|
21
24
|
target_id: { flag: "goal" },
|
|
22
25
|
seer_id: { flag: "seer" },
|
|
23
26
|
pace_per_week: { flag: "pace", type: "num" },
|
|
27
|
+
brief: { flag: "brief" },
|
|
28
|
+
new_topic: {
|
|
29
|
+
value: (flags) => (typeof flags["new-topic"] === "string" ? { name: flags["new-topic"] } : undefined),
|
|
30
|
+
},
|
|
31
|
+
new_series: {
|
|
32
|
+
value: (flags) => (typeof flags["new-series"] === "string" ? { name: flags["new-series"] } : undefined),
|
|
33
|
+
},
|
|
24
34
|
},
|
|
25
35
|
update_automation: {
|
|
26
36
|
collection_id: { flag: "collection", required: true },
|
|
@@ -30,6 +40,24 @@ export const ARGS = {
|
|
|
30
40
|
value: (flags) => (flags.pause === true ? true : flags.resume === true ? false : undefined),
|
|
31
41
|
},
|
|
32
42
|
target_id: { flag: "goal" },
|
|
43
|
+
brief: { flag: "brief" },
|
|
44
|
+
remap: { value: (flags) => (flags.remap === true ? true : undefined) },
|
|
45
|
+
subjects: { value: () => undefined },
|
|
46
|
+
},
|
|
47
|
+
preview_automation: {
|
|
48
|
+
collection_id: { flag: "collection", required: true },
|
|
49
|
+
kind: { positional: 0, label: "kind", required: true, enum: ["topic", "series"] },
|
|
50
|
+
cluster_id: { flag: "topic" },
|
|
51
|
+
name: { flag: "name" },
|
|
52
|
+
brief: { flag: "brief" },
|
|
53
|
+
items: { flag: "items", type: "list" },
|
|
54
|
+
},
|
|
55
|
+
suggest_more_topics: {
|
|
56
|
+
collection_id: { flag: "collection", required: true },
|
|
57
|
+
},
|
|
58
|
+
dismiss_subject: {
|
|
59
|
+
collection_id: { flag: "collection", required: true },
|
|
60
|
+
topic_id: { positional: 0, label: "topic-id", required: true },
|
|
33
61
|
},
|
|
34
62
|
delete_automation: {
|
|
35
63
|
collection_id: { flag: "collection", required: true },
|
|
@@ -46,16 +74,26 @@ export async function cmdAutomations(ctx) {
|
|
|
46
74
|
return run(rest, "list_automations");
|
|
47
75
|
case "show":
|
|
48
76
|
return run(rest, "get_automation");
|
|
77
|
+
case "suggest":
|
|
78
|
+
return run(rest, "suggest_automations");
|
|
79
|
+
case "preview":
|
|
80
|
+
return run(rest, "preview_automation");
|
|
81
|
+
case "more":
|
|
82
|
+
return run(rest, "suggest_more_topics");
|
|
83
|
+
case "dismiss":
|
|
84
|
+
return run(rest, "dismiss_subject", "Set aside.");
|
|
49
85
|
case "add":
|
|
50
86
|
return run(rest, "create_automation", "Added.");
|
|
51
87
|
case "set":
|
|
52
|
-
refuseUnknownFlags(rest, ARGS.update_automation, ["pause", "resume"]);
|
|
88
|
+
refuseUnknownFlags(rest, ARGS.update_automation, ["pause", "resume", "remap"]);
|
|
53
89
|
return run(rest, "update_automation", "Changed.");
|
|
54
90
|
case "remove":
|
|
55
91
|
case "rm":
|
|
56
92
|
return run(rest, "delete_automation", "Removed.");
|
|
57
93
|
default:
|
|
58
|
-
throw new CliError(
|
|
94
|
+
throw new CliError(
|
|
95
|
+
`Unknown automations subcommand: ${sub ?? "(none)"}. Try: list, show, suggest, preview, more, add, set, dismiss, remove`
|
|
96
|
+
);
|
|
59
97
|
}
|
|
60
98
|
}
|
|
61
99
|
|
|
@@ -76,6 +76,7 @@ export const ARGS = {
|
|
|
76
76
|
topic_id: { flag: "spoke" },
|
|
77
77
|
suggestion_id: { flag: "headline" },
|
|
78
78
|
source_urls: { flag: "source", type: "list" },
|
|
79
|
+
context: { flag: "notes" },
|
|
79
80
|
must_include: { flag: "must-include", type: "list" },
|
|
80
81
|
research_depth: { flag: "depth", enum: ["barebones", "fleshed_out"] },
|
|
81
82
|
kernel_id: { flag: "kernel" },
|
|
@@ -93,6 +94,7 @@ export const ARGS = {
|
|
|
93
94
|
add_headline: {
|
|
94
95
|
collection_id: { flag: "collection", required: true },
|
|
95
96
|
headline: { positional: 0, label: "headline", required: true },
|
|
97
|
+
context: { flag: "notes" },
|
|
96
98
|
queue: { flag: "queue", type: "bool" },
|
|
97
99
|
},
|
|
98
100
|
update_suggestion: {
|
|
@@ -302,7 +304,7 @@ async function collectionHeadline(ctx) {
|
|
|
302
304
|
return 0;
|
|
303
305
|
}
|
|
304
306
|
|
|
305
|
-
// `collection add "<headline>" --collection <id> [--queue]` — the letterbase page's New post: a
|
|
307
|
+
// `collection add "<headline>" --collection <id> [--notes <text>] [--queue]` — the letterbase page's New post: a
|
|
306
308
|
// headline you wrote joins Up next, or with --queue takes the next slot (a draft, planner off).
|
|
307
309
|
async function collectionAdd(ctx) {
|
|
308
310
|
const { client, flags, io } = ctx;
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
// `enclosures` — pictures, clips and sentences made for each prospect (generated
|
|
2
|
+
// prospect columns). Each output IS a Prospects column that sequence emails read as
|
|
3
|
+
// {{custom:<key>}}. Thin wrappers over the MCP tools, which call the same store functions
|
|
4
|
+
// the app's /api/generators routes do.
|
|
5
|
+
|
|
6
|
+
import { CliError } from "../client.mjs";
|
|
7
|
+
import {
|
|
8
|
+
flagStr,
|
|
9
|
+
flagNum,
|
|
10
|
+
flagList,
|
|
11
|
+
requirePositional,
|
|
12
|
+
printResult,
|
|
13
|
+
argsFromFlags,
|
|
14
|
+
refuseUnknownFlags,
|
|
15
|
+
readTextInput,
|
|
16
|
+
ok,
|
|
17
|
+
} from "./shared.mjs";
|
|
18
|
+
|
|
19
|
+
const SHAPES = ["text", "image", "animation", "video"];
|
|
20
|
+
|
|
21
|
+
// `--output key:shape:Label` (repeatable), e.g. --output audit_shot:image:"Site audit".
|
|
22
|
+
function readOutputs(flags) {
|
|
23
|
+
const raw = flagList(flags.output);
|
|
24
|
+
if (!raw.length) return undefined;
|
|
25
|
+
return raw.map((entry) => {
|
|
26
|
+
const [key, shape, ...label] = entry.split(":");
|
|
27
|
+
if (!key || !SHAPES.includes(shape) || !label.length) {
|
|
28
|
+
throw new CliError(
|
|
29
|
+
`--output takes key:shape:Label with shape one of ${SHAPES.join(", ")} (got "${entry}")`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return { key, shape, label: label.join(":") };
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// --browser, --login <app-connection-id>, --stealth, --record, --region, --image-model, --tool <op>…
|
|
37
|
+
function readNeeds(flags) {
|
|
38
|
+
const browser = {};
|
|
39
|
+
if (flagStr(flags.login)) browser.login = flagStr(flags.login);
|
|
40
|
+
if (flags.stealth === true) browser.stealth = true;
|
|
41
|
+
if (flags.record === true) browser.record = true;
|
|
42
|
+
if (flagStr(flags.region)) browser.region = flagStr(flags.region);
|
|
43
|
+
const needs = {};
|
|
44
|
+
if (flags.browser === true || Object.keys(browser).length) needs.browser = browser;
|
|
45
|
+
if (flags["image-model"] === true) needs.image_model = true;
|
|
46
|
+
const tools = flagList(flags.tool);
|
|
47
|
+
if (tools.length) needs.tools = tools;
|
|
48
|
+
return Object.keys(needs).length ? needs : undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const inputs = (flags) => {
|
|
52
|
+
const v = flagList(flags.input);
|
|
53
|
+
return v.length ? v : undefined;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const fields = {
|
|
57
|
+
name: "name",
|
|
58
|
+
instructions: { value: (flags) => readTextInput(flags, "instructions", "instructions-file") },
|
|
59
|
+
inputs: { value: inputs },
|
|
60
|
+
outputs: { value: readOutputs },
|
|
61
|
+
needs: { value: readNeeds },
|
|
62
|
+
cache_scope: { flag: "share", enum: ["prospect", "company_domain"] },
|
|
63
|
+
freshness_days: { flag: "fresh-days", type: "num" },
|
|
64
|
+
budget_per_person_usd: { flag: "budget", type: "num" },
|
|
65
|
+
no_fit_rule: "skip-when",
|
|
66
|
+
disclosure: { flag: "disclosure", type: "bool", negate: "no-disclosure" },
|
|
67
|
+
cta: {
|
|
68
|
+
value: (flags) => {
|
|
69
|
+
if (flags["no-cta"] === true) return null;
|
|
70
|
+
const label = flagStr(flags["cta-label"]);
|
|
71
|
+
const url = flagStr(flags["cta-url"]);
|
|
72
|
+
return label || url ? { label, url } : undefined;
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Flags the computed sources above read (refuseUnknownFlags can't see inside `value`).
|
|
78
|
+
const FIELD_FLAGS = [
|
|
79
|
+
"starter",
|
|
80
|
+
"instructions",
|
|
81
|
+
"instructions-file",
|
|
82
|
+
"input",
|
|
83
|
+
"output",
|
|
84
|
+
"browser",
|
|
85
|
+
"login",
|
|
86
|
+
"stealth",
|
|
87
|
+
"record",
|
|
88
|
+
"region",
|
|
89
|
+
"image-model",
|
|
90
|
+
"tool",
|
|
91
|
+
"cta-label",
|
|
92
|
+
"cta-url",
|
|
93
|
+
"no-cta",
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
// How every tool this module calls gets its arguments — see `argsFromFlags` in shared.mjs.
|
|
97
|
+
// `scripts/parity-check.ts` compares these keys against each tool's published input schema.
|
|
98
|
+
export const ARGS = {
|
|
99
|
+
list_generators: {},
|
|
100
|
+
get_generator: {
|
|
101
|
+
generator_id: { positional: 0, label: "enclosure-id", required: true },
|
|
102
|
+
},
|
|
103
|
+
create_generator: {
|
|
104
|
+
key: "key",
|
|
105
|
+
starter: "starter",
|
|
106
|
+
...fields,
|
|
107
|
+
},
|
|
108
|
+
update_generator: {
|
|
109
|
+
generator_id: { positional: 0, label: "enclosure-id", required: true },
|
|
110
|
+
...fields,
|
|
111
|
+
},
|
|
112
|
+
list_generator_starters: {},
|
|
113
|
+
add_generator_version: {
|
|
114
|
+
generator_id: { positional: 0, label: "enclosure-id", required: true },
|
|
115
|
+
script: { value: (flags) => readTextInput(flags, "script", "script-file") },
|
|
116
|
+
steps: {
|
|
117
|
+
value: (flags) => {
|
|
118
|
+
const v = flagList(flags.step);
|
|
119
|
+
return v.length ? v.map((text, i) => ({ n: i + 1, text })) : undefined;
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
allowed_hosts: { flag: "host", type: "list" },
|
|
123
|
+
region: {
|
|
124
|
+
value: (flags) => {
|
|
125
|
+
const v = flagList(flags["region-selector"]);
|
|
126
|
+
return v.length ? { selectors: v } : undefined;
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
params_schema: { internal: "set by the app when a person edits steps; not a flag" },
|
|
130
|
+
record_span: { internal: "set by the app when a recording is marked; not a flag" },
|
|
131
|
+
change_summary: "summary",
|
|
132
|
+
},
|
|
133
|
+
approve_generator_version: {
|
|
134
|
+
generator_id: { positional: 0, label: "enclosure-id", required: true },
|
|
135
|
+
version: { flag: "version", type: "num", required: true },
|
|
136
|
+
},
|
|
137
|
+
print_generator: {
|
|
138
|
+
generator_id: { positional: 0, label: "enclosure-id", required: true },
|
|
139
|
+
prospect_ids: { flag: "prospect", type: "list" },
|
|
140
|
+
list_id: "list",
|
|
141
|
+
version: { flag: "version", type: "num" },
|
|
142
|
+
redo: { flag: "redo", type: "bool" },
|
|
143
|
+
dry_run: { flag: "dry-run", type: "bool" },
|
|
144
|
+
},
|
|
145
|
+
list_app_connections: {},
|
|
146
|
+
get_app_connection: {
|
|
147
|
+
connection_id: { positional: 0, label: "app-id", required: true },
|
|
148
|
+
},
|
|
149
|
+
connect_app: {
|
|
150
|
+
login_url: { positional: 0, label: "sign-in-url", required: true },
|
|
151
|
+
name: "name",
|
|
152
|
+
remember_sign_in: { flag: "remember", type: "bool", negate: "no-remember" },
|
|
153
|
+
},
|
|
154
|
+
disconnect_app: {
|
|
155
|
+
connection_id: { positional: 0, label: "app-id", required: true },
|
|
156
|
+
},
|
|
157
|
+
list_generations: {
|
|
158
|
+
generator_id: { positional: 0, label: "enclosure-id", required: true },
|
|
159
|
+
status: { flag: "status", enum: ["queued", "running", "done", "no_fit", "failed", "skipped"] },
|
|
160
|
+
limit: { flag: "limit", type: "num" },
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const USAGE = `Usage: enclosures <list|show|create|update|prints>
|
|
165
|
+
enclosures list
|
|
166
|
+
enclosures show <enclosure-id>
|
|
167
|
+
enclosures create --name <n> --output key:shape:Label… [--key <k>] [--instructions <text>|--instructions-file <path>]
|
|
168
|
+
[--input website…] [--browser] [--login <app-connection-id>] [--stealth] [--record] [--region us-east|eu-west]
|
|
169
|
+
[--image-model] [--tool run_site_audit…] [--share prospect|company_domain] [--fresh-days <n>]
|
|
170
|
+
[--budget <usd>] [--skip-when <sentence>] [--no-disclosure]
|
|
171
|
+
[--cta-label "Book a time" --cta-url <https link>] [--no-cta]
|
|
172
|
+
enclosures update <enclosure-id> [same flags]
|
|
173
|
+
enclosures prints <enclosure-id> [--status done|failed|no_fit|…] [--limit <n>]
|
|
174
|
+
enclosures starters
|
|
175
|
+
enclosures create --starter site_audit --login <app-id> Begin from a starter (its version 1 included)
|
|
176
|
+
enclosures add-version <enclosure-id> (--script <code>|--script-file <path>) --step <text>… [--host <h>…] [--summary <s>]
|
|
177
|
+
enclosures approve <enclosure-id> --version <n>
|
|
178
|
+
enclosures print <enclosure-id> (--prospect <id>…|--list <list-id>) [--version <n>] [--redo] [--dry-run]
|
|
179
|
+
enclosures apps list
|
|
180
|
+
enclosures apps show <app-id>
|
|
181
|
+
enclosures apps connect <sign-in-url> [--name <n>] [--no-remember] Prints a sign-in link a person opens
|
|
182
|
+
enclosures apps disconnect <app-id> --yes`;
|
|
183
|
+
|
|
184
|
+
export async function cmdEnclosures(ctx) {
|
|
185
|
+
const sub = ctx.positionals[0];
|
|
186
|
+
const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
|
|
187
|
+
switch (sub) {
|
|
188
|
+
case "list":
|
|
189
|
+
case "ls":
|
|
190
|
+
return enclosuresList(rest);
|
|
191
|
+
case "show":
|
|
192
|
+
case "get":
|
|
193
|
+
return enclosuresShow(rest);
|
|
194
|
+
case "create":
|
|
195
|
+
return enclosuresCreate(rest);
|
|
196
|
+
case "update":
|
|
197
|
+
return enclosuresUpdate(rest);
|
|
198
|
+
case "prints":
|
|
199
|
+
return enclosuresPrints(rest);
|
|
200
|
+
case "apps":
|
|
201
|
+
return enclosuresApps(rest);
|
|
202
|
+
case "starters":
|
|
203
|
+
return enclosuresStarters(rest);
|
|
204
|
+
case "add-version":
|
|
205
|
+
return enclosuresAddVersion(rest);
|
|
206
|
+
case "approve":
|
|
207
|
+
return enclosuresApprove(rest);
|
|
208
|
+
case "print":
|
|
209
|
+
return enclosuresPrint(rest);
|
|
210
|
+
default:
|
|
211
|
+
throw new CliError(USAGE);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function formatEnclosure(g) {
|
|
216
|
+
const c = g.counts ?? {};
|
|
217
|
+
const lines = [
|
|
218
|
+
`${g.id} ${g.key} ${g.name} [${g.status}${g.current_version ? `, version ${g.current_version}` : ""}]`,
|
|
219
|
+
];
|
|
220
|
+
lines.push(` columns: ${(g.outputs ?? []).map((o) => `${o.label} {{custom:${o.key}}} (${o.shape})`).join(", ")}`);
|
|
221
|
+
if (g.inputs?.length) lines.push(` reads: ${g.inputs.join(", ")}`);
|
|
222
|
+
lines.push(
|
|
223
|
+
` prints: ${c.done ?? 0} ready, ${(c.queued ?? 0) + (c.running ?? 0)} making, ${c.no_fit ?? 0} no fit, ${c.failed ?? 0} failed`
|
|
224
|
+
);
|
|
225
|
+
return lines.join("\n");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function enclosuresList(ctx) {
|
|
229
|
+
const { client, flags, io } = ctx;
|
|
230
|
+
const result = await client.callTool("list_generators", argsFromFlags(ARGS.list_generators, flags));
|
|
231
|
+
printResult(io, flags, result, (r) => {
|
|
232
|
+
const all = r.generators ?? [];
|
|
233
|
+
if (!all.length) return "No enclosures yet.";
|
|
234
|
+
return all.map(formatEnclosure).join("\n\n");
|
|
235
|
+
});
|
|
236
|
+
return 0;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function enclosuresShow(ctx) {
|
|
240
|
+
const { client, flags, positionals, io } = ctx;
|
|
241
|
+
const result = await client.callTool("get_generator", argsFromFlags(ARGS.get_generator, flags, positionals));
|
|
242
|
+
printResult(io, flags, result, (r) => {
|
|
243
|
+
const lines = [formatEnclosure(r.generator)];
|
|
244
|
+
for (const v of r.generator.versions ?? []) {
|
|
245
|
+
lines.push(
|
|
246
|
+
` version ${v.version} (${v.author}): ${v.steps} steps${v.change_summary ? `, ${v.change_summary}` : ""}`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
return lines.join("\n");
|
|
250
|
+
});
|
|
251
|
+
return 0;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function enclosuresCreate(ctx) {
|
|
255
|
+
const { client, flags, positionals, io } = ctx;
|
|
256
|
+
refuseUnknownFlags(ctx, ARGS.create_generator, FIELD_FLAGS);
|
|
257
|
+
const result = await client.callTool("create_generator", argsFromFlags(ARGS.create_generator, flags, positionals));
|
|
258
|
+
ok(ctx, `Created ${result.generator?.name} (${result.generator?.id}).`);
|
|
259
|
+
printResult(io, flags, result, (r) => formatEnclosure(r.generator));
|
|
260
|
+
return 0;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function enclosuresUpdate(ctx) {
|
|
264
|
+
const { client, flags, positionals, io } = ctx;
|
|
265
|
+
refuseUnknownFlags(ctx, ARGS.update_generator, FIELD_FLAGS);
|
|
266
|
+
const args = argsFromFlags(ARGS.update_generator, flags, positionals);
|
|
267
|
+
if (Object.keys(args).length === 1) throw new CliError("Nothing to update. Pass a field flag.");
|
|
268
|
+
const result = await client.callTool("update_generator", args);
|
|
269
|
+
ok(ctx, `Updated ${result.generator?.name}.`);
|
|
270
|
+
printResult(io, flags, result, (r) => formatEnclosure(r.generator));
|
|
271
|
+
return 0;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function enclosuresPrints(ctx) {
|
|
275
|
+
const { client, flags, positionals, io } = ctx;
|
|
276
|
+
requirePositional(positionals, 0, "enclosure-id");
|
|
277
|
+
if (flags.limit !== undefined) flagNum(flags.limit);
|
|
278
|
+
const result = await client.callTool("list_generations", argsFromFlags(ARGS.list_generations, flags, positionals));
|
|
279
|
+
printResult(io, flags, result, (r) => {
|
|
280
|
+
const all = r.generations ?? [];
|
|
281
|
+
if (!all.length) return "No prints yet.";
|
|
282
|
+
return all
|
|
283
|
+
.map((g) => `${g.status.padEnd(8)} ${g.prospect?.email ?? g.prospect_id}${g.error ? ` ${g.error}` : ""}`)
|
|
284
|
+
.join("\n");
|
|
285
|
+
});
|
|
286
|
+
return 0;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function formatApp(a) {
|
|
290
|
+
const state = { connected: "signed in", connecting: "signing in", needs_login: "signed out", error: "error" }[
|
|
291
|
+
a.status
|
|
292
|
+
];
|
|
293
|
+
const lines = [`${a.id} ${a.name} [${state ?? a.status}] ${a.login_url}`];
|
|
294
|
+
if (a.last_error) lines.push(` ${a.last_error}`);
|
|
295
|
+
if (a.static_ip) lines.push(` requests come from ${a.static_ip}`);
|
|
296
|
+
return lines.join("\n");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function enclosuresApps(ctx) {
|
|
300
|
+
const { client, flags, io } = ctx;
|
|
301
|
+
const sub = ctx.positionals[0];
|
|
302
|
+
const positionals = ctx.positionals.slice(1);
|
|
303
|
+
if (sub === "list" || sub === "ls") {
|
|
304
|
+
const result = await client.callTool("list_app_connections", argsFromFlags(ARGS.list_app_connections, flags));
|
|
305
|
+
printResult(io, flags, result, (r) =>
|
|
306
|
+
r.connections?.length ? r.connections.map(formatApp).join("\n\n") : "No apps yet."
|
|
307
|
+
);
|
|
308
|
+
return 0;
|
|
309
|
+
}
|
|
310
|
+
if (sub === "show" || sub === "get") {
|
|
311
|
+
const result = await client.callTool(
|
|
312
|
+
"get_app_connection",
|
|
313
|
+
argsFromFlags(ARGS.get_app_connection, flags, positionals)
|
|
314
|
+
);
|
|
315
|
+
printResult(io, flags, result, (r) => formatApp(r.connection));
|
|
316
|
+
return 0;
|
|
317
|
+
}
|
|
318
|
+
if (sub === "connect") {
|
|
319
|
+
refuseUnknownFlags({ ...ctx, positionals }, ARGS.connect_app, []);
|
|
320
|
+
const result = await client.callTool("connect_app", argsFromFlags(ARGS.connect_app, flags, positionals));
|
|
321
|
+
ok(ctx, `Open this page and sign in (it expires about 10 minutes after it opens):\n${result.hosted_url}`);
|
|
322
|
+
printResult(io, flags, result, (r) => formatApp(r.connection));
|
|
323
|
+
return 0;
|
|
324
|
+
}
|
|
325
|
+
if (sub === "disconnect") {
|
|
326
|
+
const id = requirePositional(positionals, 0, "app-id");
|
|
327
|
+
if (!flags.yes) {
|
|
328
|
+
io.error(`Re-run with --yes to confirm: ${ctx.bin} enclosures apps disconnect ${id} --yes`);
|
|
329
|
+
return 1;
|
|
330
|
+
}
|
|
331
|
+
await client.callTool("disconnect_app", argsFromFlags(ARGS.disconnect_app, flags, positionals));
|
|
332
|
+
ok(ctx, "App removed. Enclosures that use it stop printing; prints already made stay.");
|
|
333
|
+
return 0;
|
|
334
|
+
}
|
|
335
|
+
throw new CliError(USAGE);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function enclosuresStarters(ctx) {
|
|
339
|
+
const { client, flags, io } = ctx;
|
|
340
|
+
const result = await client.callTool("list_generator_starters", argsFromFlags(ARGS.list_generator_starters, flags));
|
|
341
|
+
printResult(io, flags, result, (r) =>
|
|
342
|
+
(r.starters ?? [])
|
|
343
|
+
.map((s) => `${s.key} ${s.name}: ${s.blurb}\n${s.steps.map((st) => ` ${st.n}. ${st.text}`).join("\n")}`)
|
|
344
|
+
.join("\n\n")
|
|
345
|
+
);
|
|
346
|
+
return 0;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function enclosuresAddVersion(ctx) {
|
|
350
|
+
const { client, flags, positionals, io } = ctx;
|
|
351
|
+
refuseUnknownFlags(ctx, ARGS.add_generator_version, ["script", "script-file", "step", "region-selector"]);
|
|
352
|
+
const result = await client.callTool(
|
|
353
|
+
"add_generator_version",
|
|
354
|
+
argsFromFlags(ARGS.add_generator_version, flags, positionals)
|
|
355
|
+
);
|
|
356
|
+
ok(ctx, `Added version ${result.version}. Prints keep running the chosen version until you approve this one.`);
|
|
357
|
+
printResult(io, flags, result, (r) => `version ${r.version}`);
|
|
358
|
+
return 0;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function enclosuresApprove(ctx) {
|
|
362
|
+
const { client, flags, positionals, io } = ctx;
|
|
363
|
+
refuseUnknownFlags(ctx, ARGS.approve_generator_version, []);
|
|
364
|
+
const result = await client.callTool(
|
|
365
|
+
"approve_generator_version",
|
|
366
|
+
argsFromFlags(ARGS.approve_generator_version, flags, positionals)
|
|
367
|
+
);
|
|
368
|
+
ok(ctx, `${result.generator?.name} now prints version ${result.generator?.current_version}.`);
|
|
369
|
+
printResult(io, flags, result, (r) => formatEnclosure(r.generator));
|
|
370
|
+
return 0;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function enclosuresPrint(ctx) {
|
|
374
|
+
const { client, flags, positionals, io } = ctx;
|
|
375
|
+
refuseUnknownFlags(ctx, ARGS.print_generator, []);
|
|
376
|
+
const result = await client.callTool("print_generator", argsFromFlags(ARGS.print_generator, flags, positionals));
|
|
377
|
+
printResult(io, flags, result, (r) => {
|
|
378
|
+
const skipped = [
|
|
379
|
+
r.recent && `${r.recent} already have a recent one`,
|
|
380
|
+
r.no_website && `${r.no_website} have no website`,
|
|
381
|
+
r.never_emailed && `${r.never_emailed} unsubscribed`,
|
|
382
|
+
].filter(Boolean);
|
|
383
|
+
const head = r.dry_run
|
|
384
|
+
? `Would print ${r.will_print} (version ${r.version}): about $${r.estimate.usd}, about ${r.estimate.minutes} minutes.`
|
|
385
|
+
: `Printing ${r.queued} (version ${r.version}). Watch: enclosures prints ${positionals[0]}`;
|
|
386
|
+
return skipped.length ? `${head}\n${skipped.join(". ")}. They're skipped.` : head;
|
|
387
|
+
});
|
|
388
|
+
return 0;
|
|
389
|
+
}
|
package/lib/commands/site.mjs
CHANGED
|
@@ -373,13 +373,24 @@ async function siteMetrics(ctx) {
|
|
|
373
373
|
);
|
|
374
374
|
printResult(io, flags, result, (r) => {
|
|
375
375
|
const lines = [`${r.host} ${r.window} (${r.range.start} to ${r.range.end}) source: ${r.source}`];
|
|
376
|
+
const counted = r.unit === "accesses";
|
|
376
377
|
if (r.source !== "lettervisit") lines.push(` ${r.message}`);
|
|
377
378
|
for (const p of r.pages) {
|
|
378
|
-
const value =
|
|
379
|
-
const delta =
|
|
380
|
-
const
|
|
381
|
-
? `
|
|
379
|
+
const value = counted ? p.accesses?.total : p.clicks;
|
|
380
|
+
const delta = counted ? p.accesses_delta_pct : p.clicks_delta_pct;
|
|
381
|
+
const agents = p.accesses?.by_agent?.length
|
|
382
|
+
? ` [${p.accesses.by_agent
|
|
383
|
+
.slice(0, 3)
|
|
384
|
+
.map((a) => `${a.agent} ${a.requests}`)
|
|
385
|
+
.join(", ")}]`
|
|
382
386
|
: "";
|
|
387
|
+
const identified =
|
|
388
|
+
p.accesses?.identified_humans != null ? `, ${p.accesses.identified_humans} identified` : "";
|
|
389
|
+
const split = !p.accesses
|
|
390
|
+
? ""
|
|
391
|
+
: r.source === "visitors"
|
|
392
|
+
? ` (AI ${p.accesses.ai_agents}${agents}, people ${p.accesses.humans}${identified})`
|
|
393
|
+
: ` (AI ${p.accesses.ai_agents}, crawlers ${p.accesses.search_crawlers}, people ${p.accesses.humans})`;
|
|
383
394
|
const marks = p.indicators.map((i) => i.label).join(", ");
|
|
384
395
|
lines.push(
|
|
385
396
|
` ${p.path} ${value ?? "-"} ${r.unit ?? ""}${delta !== null ? ` ${delta > 0 ? "+" : ""}${delta}%` : ""}${split}${marks ? ` [${marks}]` : ""}`
|
|
@@ -464,7 +475,11 @@ async function siteFamilies(ctx) {
|
|
|
464
475
|
r.families
|
|
465
476
|
.map((f) => {
|
|
466
477
|
const members = f.members.filter((m) => m.included);
|
|
467
|
-
|
|
478
|
+
const line = `${f.id} ${f.name} [${f.status}] ${members.length} members (${f.source})`;
|
|
479
|
+
if (!f.coverage) return line;
|
|
480
|
+
const total = f.coverage.covered.length + f.coverage.missing.length;
|
|
481
|
+
const missing = f.coverage.missing.length ? ` missing: ${f.coverage.missing.join(", ")}` : "";
|
|
482
|
+
return `${line} ${f.coverage.covered.length} of ${total} ${f.universe?.list ?? ""}${missing}`;
|
|
468
483
|
})
|
|
469
484
|
.join("\n") || "No families yet."
|
|
470
485
|
);
|
package/lib/commands/visits.mjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// letterstory visits citations --since 30d
|
|
10
10
|
// letterstory visits export --since 30d --format csv
|
|
11
11
|
// letterstory visits sites
|
|
12
|
+
// letterstory visits pages --since 28d [--path '/docs/*'] [--agent openai-crawler]
|
|
12
13
|
|
|
13
14
|
import { CliError } from "../client.mjs";
|
|
14
15
|
import { flagStr, printResult, argsFromFlags } from "./shared.mjs";
|
|
@@ -27,6 +28,13 @@ export const ARGS = {
|
|
|
27
28
|
purpose: { flag: "purpose", enum: PURPOSES },
|
|
28
29
|
},
|
|
29
30
|
visitors_get_citations: { site_id: "site", days },
|
|
31
|
+
visitors_get_page_traffic: {
|
|
32
|
+
site_id: "site",
|
|
33
|
+
days,
|
|
34
|
+
path: "path",
|
|
35
|
+
agent: "agent",
|
|
36
|
+
limit: { flag: "limit", type: "num" },
|
|
37
|
+
},
|
|
30
38
|
visitors_compare_agents: {
|
|
31
39
|
site_id: "site",
|
|
32
40
|
a: { positional: 0, label: "agent-a (e.g. chatgpt)", required: true },
|
|
@@ -70,8 +78,10 @@ export async function cmdVisits(ctx) {
|
|
|
70
78
|
return visitsExport(rest);
|
|
71
79
|
case "sites":
|
|
72
80
|
return visitsSites(rest);
|
|
81
|
+
case "pages":
|
|
82
|
+
return visitsPages(rest);
|
|
73
83
|
default:
|
|
74
|
-
throw new CliError("Usage: visits [list|citations|compare|export|sites]");
|
|
84
|
+
throw new CliError("Usage: visits [list|citations|compare|export|sites|pages]");
|
|
75
85
|
}
|
|
76
86
|
}
|
|
77
87
|
|
|
@@ -156,6 +166,29 @@ async function visitsExport(ctx) {
|
|
|
156
166
|
return 0;
|
|
157
167
|
}
|
|
158
168
|
|
|
169
|
+
async function visitsPages(ctx) {
|
|
170
|
+
const { flags, io } = ctx;
|
|
171
|
+
const result = await call(ctx, "visitors_get_page_traffic");
|
|
172
|
+
printResult(io, flags, result, (r) => {
|
|
173
|
+
if (r.unavailable) return r.message ?? `No page traffic (${r.unavailable}).`;
|
|
174
|
+
const t = r.totals;
|
|
175
|
+
const identified = t.identified_humans != null ? `, ${t.identified_humans} identified` : "";
|
|
176
|
+
const lines = [
|
|
177
|
+
`${r.host} ${r.since} to ${r.until}: ${t.humans} people page views (${t.unique_visitors} visitors${identified}), ${t.ai_requests} AI agent requests`,
|
|
178
|
+
];
|
|
179
|
+
if (r.agents?.length) lines.push(`Agents: ${r.agents.map((a) => `${a.agent} ${a.requests}`).join(", ")}`);
|
|
180
|
+
if (!r.server_collected)
|
|
181
|
+
lines.push("No server snippet requests yet: AI agents need the server snippet (visits sites).");
|
|
182
|
+
lines.push("", " people AI page");
|
|
183
|
+
for (const p of r.pages.slice(0, 50)) {
|
|
184
|
+
const who = p.people?.length ? ` (${p.people.map((x) => x.name ?? x.id).join(", ")})` : "";
|
|
185
|
+
lines.push(` ${String(p.humans).padStart(6)} ${String(p.ai_requests).padStart(6)} ${p.path}${who}`);
|
|
186
|
+
}
|
|
187
|
+
return lines.join("\n");
|
|
188
|
+
});
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
159
192
|
async function visitsSites(ctx) {
|
|
160
193
|
const { flags, io } = ctx;
|
|
161
194
|
const result = await call(ctx, "visitors_list_sites");
|
package/lib/commands.mjs
CHANGED
|
@@ -38,6 +38,7 @@ export * from "./commands/enhance.mjs";
|
|
|
38
38
|
export * from "./commands/landings.mjs";
|
|
39
39
|
export * from "./commands/outbound.mjs";
|
|
40
40
|
export * from "./commands/offerings.mjs";
|
|
41
|
+
export * from "./commands/enclosures.mjs";
|
|
41
42
|
export * from "./commands/targets.mjs";
|
|
42
43
|
export * from "./commands/admin.mjs";
|
|
43
44
|
export * from "./commands/feedback.mjs";
|
package/lib/documents.mjs
CHANGED
|
@@ -87,6 +87,8 @@ export const JSON_ARG_FLAG_PAIRS = [
|
|
|
87
87
|
["value", "value-file"],
|
|
88
88
|
// A Vercel API token read verbatim, so it stays out of shell history (outbound.mjs mail vercel-key set).
|
|
89
89
|
["token", "token-file"],
|
|
90
|
+
// An enclosure version's script (enclosures.mjs add-version): code, read verbatim.
|
|
91
|
+
["script", "script-file"],
|
|
90
92
|
];
|
|
91
93
|
|
|
92
94
|
/** Lower-case extension without the dot. */
|