@letterstory/cli 0.102.1 → 0.102.3
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 +6 -3
- package/lib/cli.mjs +29 -5
- package/lib/commands/admin.mjs +47 -6
- package/lib/commands/outbound.mjs +96 -2
- package/lib/commands/shared.mjs +15 -9
- package/lib/commands/site.mjs +36 -1
- package/lib/documents.mjs +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -815,6 +815,12 @@ here can SEND email or spend money, so they refuse unknown flags instead of igno
|
|
|
815
815
|
|
|
816
816
|
```bash
|
|
817
817
|
# Mailers
|
|
818
|
+
letterstory mail vercel-key set --token-file - --team team_AbC123 # optional: buy domains on YOUR Vercel team (token on stdin)
|
|
819
|
+
letterstory mail registrant --first-name Dana --last-name Lee --email d@acme.com --phone +1.4155550100 \
|
|
820
|
+
--address "1 Main St" --city Oakland --state CA --zip 94607 --country US # WHOIS owner for purchases on that key
|
|
821
|
+
letterstory mailers quote acme-mail.com # price it first: nothing is bought
|
|
822
|
+
letterstory mailers launch acme-mail.com --mailer "hello:Hello Team" --forward-to me@acme.com --yes
|
|
823
|
+
letterstory mailers domains # every domain: setup job, mailers, warmup
|
|
818
824
|
letterstory mailers list [--domain acme.com] # status + daily send limit
|
|
819
825
|
letterstory mailers domain <owned-domain-id> # provisioning stage, DNS, mailers on it
|
|
820
826
|
letterstory mailers messages <mailer-id> [--folder inbox|sent|all] [--limit 25] # envelopes
|
|
@@ -828,9 +834,6 @@ letterstory mailers suppressions [list] [--reason bounce] [--limit 100]
|
|
|
828
834
|
letterstory mailers suppressions add person@x.com --reason manual --note "asked to stop"
|
|
829
835
|
letterstory mailers suppressions remove person@x.com # only 'manual' entries; opt-outs stay
|
|
830
836
|
|
|
831
|
-
# Launching buys a domain (up to $30) and the mailboxes, so it is a tool call, not a command:
|
|
832
|
-
letterstory call launch_mailers --args '{"domain":"acme-mail.com","mailers":[{"local_part":"hello"}],"consent_attested":true,"mail_from_postal_address":"…"}'
|
|
833
|
-
|
|
834
837
|
# Org-level mail settings
|
|
835
838
|
letterstory mail connections # connected mailboxes, key + postal address status
|
|
836
839
|
letterstory mail postal-address "123 Main St, Portland, OR 97201" # CAN-SPAM footer
|
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.102.
|
|
63
|
+
export const VERSION = "0.102.3";
|
|
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
|
|
@@ -71,6 +71,11 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
71
71
|
"unfold",
|
|
72
72
|
// site map: do not start an audit for a host that has none.
|
|
73
73
|
"no-audit",
|
|
74
|
+
// site refresh / observe: the loop's switches.
|
|
75
|
+
"force-map",
|
|
76
|
+
"force-audit",
|
|
77
|
+
"skip-observe",
|
|
78
|
+
"ignore-budget",
|
|
74
79
|
// lede library update / lede proposals accept: clear or drop the line's condition.
|
|
75
80
|
"no-condition",
|
|
76
81
|
// prospects update: clear the HeyReach LinkedIn-reply marker.
|
|
@@ -178,6 +183,10 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
178
183
|
"no-notify-staff",
|
|
179
184
|
"no-opt-out-footer",
|
|
180
185
|
"no-postal-address-footer",
|
|
186
|
+
// `mailers launch … --no-warmup` / `--warmup` / `--consent`: value-less switches.
|
|
187
|
+
"no-warmup",
|
|
188
|
+
"warmup",
|
|
189
|
+
"consent",
|
|
181
190
|
"no-redesign",
|
|
182
191
|
"no-test-mode",
|
|
183
192
|
"no-unsubscribe-links",
|
|
@@ -1091,7 +1100,13 @@ Prospects (who you write to):
|
|
|
1091
1100
|
[--first-name] [--last-name] [--title] [--company] [--website] [--linkedin] [--school]
|
|
1092
1101
|
[--city] [--country] [--years] [--notes] [--custom key=value]... [--clear-linkedin-reply]
|
|
1093
1102
|
|
|
1094
|
-
Mailers (the sending mailboxes
|
|
1103
|
+
Mailers (the sending mailboxes):
|
|
1104
|
+
mailers quote <domain> Price a launch domain (read-only, nothing bought)
|
|
1105
|
+
mailers launch <domain> --mailer <name>[:<Display Name>]... --yes
|
|
1106
|
+
[--forward-to <email>] [--no-warmup] [--postal-address "<addr>"] [--consent] [--customer-ref <r>]
|
|
1107
|
+
Buy or reuse the domain, create mailers, warm them up
|
|
1108
|
+
(a net-new domain is a real, auto-renewing purchase)
|
|
1109
|
+
mailers domains Every domain you own: mail status, setup job, mailers
|
|
1095
1110
|
mailers list [--domain <apex>] Your mailers + status + daily send limit
|
|
1096
1111
|
mailers domain <owned-domain-id> Provisioning stage, DNS state, mailers on a domain
|
|
1097
1112
|
mailers messages <mailer-id> [--folder all|inbox|sent] [--limit <n>]
|
|
@@ -1110,6 +1125,12 @@ Mailers (the sending mailboxes; launching one buys a domain, so use: call launch
|
|
|
1110
1125
|
Mail:
|
|
1111
1126
|
mail connections Connected mailboxes, Google connect, Vercel key + mailing address status
|
|
1112
1127
|
mail postal-address "<address>" Set the org's CAN-SPAM mailing address (email footers)
|
|
1128
|
+
mail vercel-key set (--token <t> | --token-file <path|->) [--team <team_id>]
|
|
1129
|
+
Buy and own mail domains on your Vercel account / team
|
|
1130
|
+
mail vercel-key remove --yes Future purchases go back to Letterstory's account
|
|
1131
|
+
mail registrant --first-name <f> --last-name <l> --email <e> --phone +1.4155551234
|
|
1132
|
+
--address <a> [--address2 <a>] --city <c> --state <s> --zip <z> --country US [--company <c>]
|
|
1133
|
+
WHOIS registrant for purchases on your Vercel key
|
|
1113
1134
|
|
|
1114
1135
|
Insights:
|
|
1115
1136
|
insights summary [--period …] [--part <block>]... Measure Overview: AI named/cited rates, share of voice, search, targets
|
|
@@ -1127,13 +1148,16 @@ Insights:
|
|
|
1127
1148
|
|
|
1128
1149
|
The Site (one model of your whole marketing site, keyed by host and path):
|
|
1129
1150
|
site list Mapped hosts, and hosts you already have elsewhere
|
|
1130
|
-
site map <host> [--sitemap <url>] [--max-pages <n>] [--no-audit]
|
|
1151
|
+
site map <host> [--sitemap <url>] [--max-pages <n>] [--no-audit] [--relation owned|watched]
|
|
1131
1152
|
Map a host from its sitemap; starts an audit if it has none
|
|
1153
|
+
site refresh <host> [--force-map] [--force-audit] [--skip-observe]
|
|
1154
|
+
Run the daily loop now: re-map, re-audit, signals, Jev
|
|
1155
|
+
site unmap <host> Remove a host and everything hanging off it
|
|
1132
1156
|
site tree <host> [path] [--depth <n>] [--unfold] The tree one folder at a time, one item per node
|
|
1133
1157
|
site page <host> <path> One page: facts, bindings, raw URLs, open items
|
|
1134
|
-
site observations <host> [path] [--state open|dismissed|resolved] [--tone risk|warn|info]
|
|
1158
|
+
site observations <host> [path] [--state open|dismissed|resolved] [--tone risk|warn|info] [--key <k>] [--since <iso>] [--min-reach <0..1>]
|
|
1135
1159
|
site observe <host> [path] [--max-pages <n>] Jev pass: meta accuracy, title, outdated
|
|
1136
|
-
site dismiss <host> <observation-id> --reason <why> [--decision dismiss|reopen]
|
|
1160
|
+
site dismiss <host> <observation-id> --reason <why> [--kind not_relevant|already_handled|wrong|later] [--decision dismiss|reopen]
|
|
1137
1161
|
site families <host> [path] [--state suggested|tracked|dismissed]
|
|
1138
1162
|
site decide-family <family-id> [--decision tracked|dismissed] [--reason <why>] [--name <n>]
|
|
1139
1163
|
[--include <path>]... [--exclude <path>]...
|
package/lib/commands/admin.mjs
CHANGED
|
@@ -2810,6 +2810,43 @@ async function cmdAdminCrmCapture(ctx, paths) {
|
|
|
2810
2810
|
})
|
|
2811
2811
|
);
|
|
2812
2812
|
if (flagBool(flags.json)) return printResult(io, flags, r);
|
|
2813
|
+
for (const line of captureSummary(r)) io.log(line);
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
/** What a batch entry's status says, in the CLI's words. */
|
|
2817
|
+
const BATCH_STATUS = {
|
|
2818
|
+
would_match: "would add to",
|
|
2819
|
+
would_create: "would create",
|
|
2820
|
+
matched: "added to",
|
|
2821
|
+
created: "created",
|
|
2822
|
+
duplicate: "already in",
|
|
2823
|
+
unidentified: "not found",
|
|
2824
|
+
not_found: "no account",
|
|
2825
|
+
failed: "didn't save",
|
|
2826
|
+
};
|
|
2827
|
+
|
|
2828
|
+
/**
|
|
2829
|
+
* A capture result as lines (pure). A single capture is one line and what it
|
|
2830
|
+
* read; one that became several entries — people on a list, or notes on
|
|
2831
|
+
* several accounts from one roundup — is a line per entry, so the person sees
|
|
2832
|
+
* where each went (the CRM's paste box and the Slack reply list the same).
|
|
2833
|
+
*/
|
|
2834
|
+
export function captureSummary(r) {
|
|
2835
|
+
if (r.batch) {
|
|
2836
|
+
const width = Math.max(...r.batch.map((b) => (BATCH_STATUS[b.status] ?? b.status).length), 0);
|
|
2837
|
+
return [
|
|
2838
|
+
`${r.dryRun ? "would save" : "saved"} ${r.accountName ?? `${r.batch.length} entries`}`,
|
|
2839
|
+
...r.batch.map((b) => {
|
|
2840
|
+
const status = (BATCH_STATUS[b.status] ?? b.status).padEnd(width);
|
|
2841
|
+
const where = b.accountName ?? b.companyName ?? "?";
|
|
2842
|
+
const extra =
|
|
2843
|
+
b.status === "failed" || b.status === "not_found" || b.status === "unidentified"
|
|
2844
|
+
? b.why
|
|
2845
|
+
: b.headline;
|
|
2846
|
+
return ` ${status} ${b.name} → ${where}${b.accountUrl ? ` ${b.accountUrl}` : ""}${extra ? ` — ${extra}` : ""}`;
|
|
2847
|
+
}),
|
|
2848
|
+
];
|
|
2849
|
+
}
|
|
2813
2850
|
const verb = r.dryRun
|
|
2814
2851
|
? r.accountId
|
|
2815
2852
|
? "would add to"
|
|
@@ -2819,15 +2856,16 @@ async function cmdAdminCrmCapture(ctx, paths) {
|
|
|
2819
2856
|
: r.accountCreated
|
|
2820
2857
|
? "created"
|
|
2821
2858
|
: "added to";
|
|
2822
|
-
|
|
2823
|
-
`${verb} ${r.accountName ?? "a new account"}${r.accountUrl ? ` ${r.accountUrl}` : ""}${r.matchedVia ? ` (via ${r.matchedVia})` : ""}
|
|
2824
|
-
|
|
2859
|
+
const lines = [
|
|
2860
|
+
`${verb} ${r.accountName ?? "a new account"}${r.accountUrl ? ` ${r.accountUrl}` : ""}${r.matchedVia ? ` (via ${r.matchedVia})` : ""}`,
|
|
2861
|
+
];
|
|
2825
2862
|
if (r.read) {
|
|
2826
|
-
|
|
2863
|
+
lines.push(` ${r.read.title}`);
|
|
2827
2864
|
if (r.contacts.length)
|
|
2828
|
-
|
|
2829
|
-
if (r.read.notes)
|
|
2865
|
+
lines.push(` contacts: ${r.contacts.map((p) => p.name + (p.email ? ` <${p.email}>` : "")).join(", ")}`);
|
|
2866
|
+
if (r.read.notes) lines.push(` notes: ${r.read.notes}`);
|
|
2830
2867
|
}
|
|
2868
|
+
return lines;
|
|
2831
2869
|
}
|
|
2832
2870
|
|
|
2833
2871
|
/**
|
|
@@ -2845,6 +2883,7 @@ async function cmdAdminCrmCapture(ctx, paths) {
|
|
|
2845
2883
|
* outreach catch-up [--dry-run] handle #launch-outreach posts the bot missed
|
|
2846
2884
|
* outreach set <id> [--reached|--not-reached] [--response "…" --tone positive]
|
|
2847
2885
|
* [--owner-id <uuid>] [--channel …] [--ask …] [--draft "…"] [--archive|--restore]
|
|
2886
|
+
* [--timezone Europe/London|PT|""] "" goes back to the guess from their location
|
|
2848
2887
|
*/
|
|
2849
2888
|
async function cmdAdminCrmOutreach(ctx, args) {
|
|
2850
2889
|
const { flags, io } = ctx;
|
|
@@ -2952,6 +2991,8 @@ async function cmdAdminCrmOutreach(ctx, args) {
|
|
|
2952
2991
|
relationship: flagStr(flags.relationship),
|
|
2953
2992
|
draftMessage: flagStr(flags.draft),
|
|
2954
2993
|
notes: flagStr(flags.notes),
|
|
2994
|
+
// "Europe/London", or "PT" / "eastern"; "" clears it back to the guess from their location.
|
|
2995
|
+
timezone: flagStr(flags.timezone),
|
|
2955
2996
|
archived: flagBool(flags.archive) ? true : flagBool(flags.restore) ? false : undefined,
|
|
2956
2997
|
});
|
|
2957
2998
|
const r = await staffRequest(ctx, "PATCH", `${CRM}/outreach/${id}`, body);
|
|
@@ -393,8 +393,62 @@ export const ARGS = {
|
|
|
393
393
|
set_mail_postal_address: {
|
|
394
394
|
mail_postal_address: { positional: 0, label: "address", required: true },
|
|
395
395
|
},
|
|
396
|
+
list_mail_domains: {},
|
|
397
|
+
quote_mail_domain: {
|
|
398
|
+
domain: { positional: 0, label: "domain", required: true },
|
|
399
|
+
},
|
|
400
|
+
launch_mailers: {
|
|
401
|
+
domain: { positional: 0, label: "domain", required: true },
|
|
402
|
+
mailers: { value: launchMailerRows },
|
|
403
|
+
customer_ref: "customer-ref",
|
|
404
|
+
forward_to: "forward-to",
|
|
405
|
+
warmup: { flag: "warmup", type: "bool", negate: "no-warmup" },
|
|
406
|
+
consent_attested: { flag: "consent", type: "bool" },
|
|
407
|
+
mail_from_postal_address: "postal-address",
|
|
408
|
+
},
|
|
409
|
+
set_vercel_key: {
|
|
410
|
+
// --token-file <path|-> keeps the token out of shell history.
|
|
411
|
+
token: {
|
|
412
|
+
value: (flags) => {
|
|
413
|
+
const token = readTextInput(flags, "token", "token-file")?.trim();
|
|
414
|
+
if (!token) throw new CliError("Pass --token <vercel-token> (or --token-file <path|->)");
|
|
415
|
+
return token;
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
team_id: "team",
|
|
419
|
+
},
|
|
420
|
+
remove_vercel_key: {},
|
|
421
|
+
set_registrant_contact: {
|
|
422
|
+
registrant_contact: {
|
|
423
|
+
object: {
|
|
424
|
+
firstName: "first-name",
|
|
425
|
+
lastName: "last-name",
|
|
426
|
+
email: "email",
|
|
427
|
+
phone: "phone",
|
|
428
|
+
address1: "address",
|
|
429
|
+
address2: "address2",
|
|
430
|
+
city: "city",
|
|
431
|
+
state: "state",
|
|
432
|
+
zip: "zip",
|
|
433
|
+
country: "country",
|
|
434
|
+
companyName: "company",
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
},
|
|
396
438
|
};
|
|
397
439
|
|
|
440
|
+
// `--mailer <name>[:<Display Name>]`, repeatable: the addresses a launch creates.
|
|
441
|
+
function launchMailerRows(flags) {
|
|
442
|
+
const rows = flagList(flags.mailer).map((raw) => {
|
|
443
|
+
const at = raw.indexOf(":");
|
|
444
|
+
const localPart = (at < 0 ? raw : raw.slice(0, at)).trim();
|
|
445
|
+
const displayName = at < 0 ? "" : raw.slice(at + 1).trim();
|
|
446
|
+
return displayName ? { local_part: localPart, display_name: displayName } : { local_part: localPart };
|
|
447
|
+
});
|
|
448
|
+
if (rows.length === 0) throw new CliError("Pass at least one --mailer <name>[:<Display Name>]");
|
|
449
|
+
return rows;
|
|
450
|
+
}
|
|
451
|
+
|
|
398
452
|
// `message` may be a function of the tool's result, for a line that reports what actually happened.
|
|
399
453
|
async function run(ctx, tool, message) {
|
|
400
454
|
refuseUnknownFlags(ctx, ARGS[tool]);
|
|
@@ -639,7 +693,7 @@ async function prospectsColumns(ctx, sub) {
|
|
|
639
693
|
}
|
|
640
694
|
}
|
|
641
695
|
|
|
642
|
-
const MAILERS_USAGE = `Usage: mailers <list|domain|messages|message|forward|warmup|limit|retire|suppressions>`;
|
|
696
|
+
const MAILERS_USAGE = `Usage: mailers <list|domains|domain|quote|launch|messages|message|forward|warmup|limit|retire|suppressions>`;
|
|
643
697
|
|
|
644
698
|
export async function cmdMailers(ctx) {
|
|
645
699
|
const sub = ctx.positionals[0];
|
|
@@ -648,8 +702,23 @@ export async function cmdMailers(ctx) {
|
|
|
648
702
|
case "list":
|
|
649
703
|
case "ls":
|
|
650
704
|
return run(rest, "list_mailers");
|
|
705
|
+
case "domains":
|
|
706
|
+
return run(rest, "list_mail_domains");
|
|
651
707
|
case "domain":
|
|
652
708
|
return run(rest, "get_mailer_domain_status");
|
|
709
|
+
case "quote":
|
|
710
|
+
return run(rest, "quote_mail_domain");
|
|
711
|
+
case "launch": {
|
|
712
|
+
// A net-new domain is a real purchase, so a launch needs --yes (quote it first).
|
|
713
|
+
if (!rest.flags.yes) {
|
|
714
|
+
const domain = flagStr(rest.positionals[0]) ?? "<domain>";
|
|
715
|
+
ctx.io.error(
|
|
716
|
+
`Launching may BUY ${domain} (auto-renewing). Check the price with: ${ctx.bin} mailers quote ${domain}\nThen re-run with --yes to confirm.`
|
|
717
|
+
);
|
|
718
|
+
return 1;
|
|
719
|
+
}
|
|
720
|
+
return run(rest, "launch_mailers", (r) => `Mail setup started on ${r?.domain ?? "the domain"}.`);
|
|
721
|
+
}
|
|
653
722
|
case "messages":
|
|
654
723
|
return run(rest, "list_mailer_messages");
|
|
655
724
|
case "message":
|
|
@@ -708,7 +777,32 @@ export async function cmdMail(ctx) {
|
|
|
708
777
|
return run(rest, "get_mail_connections");
|
|
709
778
|
case "postal-address":
|
|
710
779
|
return run(rest, "set_mail_postal_address", "Mailing address saved.");
|
|
780
|
+
case "vercel-key": {
|
|
781
|
+
const action = rest.positionals[0];
|
|
782
|
+
const inner = { ...rest, positionals: rest.positionals.slice(1) };
|
|
783
|
+
if (action === "set") {
|
|
784
|
+
return run(inner, "set_vercel_key", (r) =>
|
|
785
|
+
r?.team_id
|
|
786
|
+
? `Vercel key saved: new domains are bought in team ${r.team_id}.`
|
|
787
|
+
: "Vercel key saved: new domains are bought in the token's personal account."
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
if (action === "remove") {
|
|
791
|
+
if (!inner.flags.yes) {
|
|
792
|
+
ctx.io.error(
|
|
793
|
+
`Removing the key sends future purchases back to Letterstory's account and drops the registrant contact. Re-run with --yes to confirm: ${ctx.bin} mail vercel-key remove --yes`
|
|
794
|
+
);
|
|
795
|
+
return 1;
|
|
796
|
+
}
|
|
797
|
+
return run(inner, "remove_vercel_key", "Vercel key removed.");
|
|
798
|
+
}
|
|
799
|
+
throw new CliError(`Unknown vercel-key action: ${action ?? "(none)"}. Try: set, remove`);
|
|
800
|
+
}
|
|
801
|
+
case "registrant":
|
|
802
|
+
return run(rest, "set_registrant_contact", "Registrant contact saved.");
|
|
711
803
|
default:
|
|
712
|
-
throw new CliError(
|
|
804
|
+
throw new CliError(
|
|
805
|
+
`Unknown mail subcommand: ${sub ?? "(none)"}. Try: connections, postal-address, vercel-key, registrant`
|
|
806
|
+
);
|
|
713
807
|
}
|
|
714
808
|
}
|
package/lib/commands/shared.mjs
CHANGED
|
@@ -128,16 +128,22 @@ export function valueSourceFlags(value) {
|
|
|
128
128
|
*/
|
|
129
129
|
export function refuseUnknownFlags(ctx, argsMap, own = []) {
|
|
130
130
|
const allowed = new Set([...GLOBAL_FLAGS, ...own]);
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
131
|
+
const collect = (map) => {
|
|
132
|
+
for (const source of Object.values(map ?? {})) {
|
|
133
|
+
if (typeof source === "string") allowed.add(source);
|
|
134
|
+
else if (Array.isArray(source)) source.forEach((f) => allowed.add(f));
|
|
135
|
+
else if (source && typeof source === "object" && source.flag) {
|
|
136
|
+
allowed.add(source.flag);
|
|
137
|
+
if (source.negate) allowed.add(source.negate);
|
|
138
|
+
} else if (source && typeof source === "object" && typeof source.value === "function") {
|
|
139
|
+
valueSourceFlags(source.value).forEach((f) => allowed.add(f));
|
|
140
|
+
} else if (source && typeof source === "object" && source.object) {
|
|
141
|
+
// A nested `{ object }` source reads its own flags (e.g. a contact's fields).
|
|
142
|
+
collect(source.object);
|
|
143
|
+
}
|
|
139
144
|
}
|
|
140
|
-
}
|
|
145
|
+
};
|
|
146
|
+
collect(argsMap);
|
|
141
147
|
const unknown = Object.keys(ctx.flags).filter((f) => !allowed.has(f));
|
|
142
148
|
if (unknown.length) {
|
|
143
149
|
throw new CliError(
|
package/lib/commands/site.mjs
CHANGED
|
@@ -41,7 +41,15 @@ const ARGS = {
|
|
|
41
41
|
sitemap_url: "sitemap",
|
|
42
42
|
max_pages: { flag: "max-pages", type: "num" },
|
|
43
43
|
audit: { flag: "no-audit", type: "bool", invert: true },
|
|
44
|
+
relation: { flag: "relation", enum: ["owned", "watched"] },
|
|
44
45
|
},
|
|
46
|
+
refresh_site: {
|
|
47
|
+
host: { positional: 0, required: true, label: "host" },
|
|
48
|
+
force_map: { flag: "force-map", type: "bool" },
|
|
49
|
+
force_audit: { flag: "force-audit", type: "bool" },
|
|
50
|
+
skip_observe: { flag: "skip-observe", type: "bool" },
|
|
51
|
+
},
|
|
52
|
+
unmap_site: { host: { positional: 0, required: true, label: "host" } },
|
|
45
53
|
get_site_tree: {
|
|
46
54
|
host: { positional: 0, required: true, label: "host" },
|
|
47
55
|
path: { positional: 1, label: "path" },
|
|
@@ -58,18 +66,23 @@ const ARGS = {
|
|
|
58
66
|
status: { flag: "state", enum: ["open", "dismissed", "resolved"] },
|
|
59
67
|
tone: { flag: "tone", enum: ["ok", "warn", "risk", "info"] },
|
|
60
68
|
source: "source",
|
|
69
|
+
key: "key",
|
|
70
|
+
since: "since",
|
|
71
|
+
min_reach: { flag: "min-reach", type: "num" },
|
|
61
72
|
limit: { flag: "limit", type: "num" },
|
|
62
73
|
},
|
|
63
74
|
observe_site: {
|
|
64
75
|
host: { positional: 0, required: true, label: "host" },
|
|
65
76
|
path: { positional: 1, label: "path" },
|
|
66
77
|
max_pages: { flag: "max-pages", type: "num" },
|
|
78
|
+
ignore_budget: { flag: "ignore-budget", type: "bool" },
|
|
67
79
|
},
|
|
68
80
|
dismiss_site_observation: {
|
|
69
81
|
host: { positional: 0, required: true, label: "host" },
|
|
70
82
|
observation_id: { positional: 1, required: true, label: "observation-id" },
|
|
71
83
|
decision: { flag: "decision", enum: ["dismiss", "reopen"] },
|
|
72
84
|
reason: "reason",
|
|
85
|
+
reason_kind: { flag: "kind", enum: ["not_relevant", "already_handled", "wrong", "later"] },
|
|
73
86
|
},
|
|
74
87
|
list_site_families: {
|
|
75
88
|
host: { positional: 0, required: true, label: "host" },
|
|
@@ -113,6 +126,10 @@ export async function cmdSite(ctx) {
|
|
|
113
126
|
return siteList(rest);
|
|
114
127
|
case "map":
|
|
115
128
|
return siteMap(rest);
|
|
129
|
+
case "refresh":
|
|
130
|
+
return siteRefresh(rest);
|
|
131
|
+
case "unmap":
|
|
132
|
+
return siteUnmap(rest);
|
|
116
133
|
case "tree":
|
|
117
134
|
return siteTree(rest);
|
|
118
135
|
case "page":
|
|
@@ -135,7 +152,7 @@ export async function cmdSite(ctx) {
|
|
|
135
152
|
return siteDecide(rest);
|
|
136
153
|
default:
|
|
137
154
|
throw new CliError(
|
|
138
|
-
`Unknown site subcommand: ${sub ?? "(none)"}. Try: list, map, tree, page, observations, observe, dismiss, families, decide-family, proposals, propose, decide`
|
|
155
|
+
`Unknown site subcommand: ${sub ?? "(none)"}. Try: list, map, refresh, unmap, tree, page, observations, observe, dismiss, families, decide-family, proposals, propose, decide`
|
|
139
156
|
);
|
|
140
157
|
}
|
|
141
158
|
}
|
|
@@ -187,6 +204,24 @@ async function siteMap(ctx) {
|
|
|
187
204
|
return 0;
|
|
188
205
|
}
|
|
189
206
|
|
|
207
|
+
async function siteRefresh(ctx) {
|
|
208
|
+
const { client, flags, io } = ctx;
|
|
209
|
+
refuseUnknownFlags(ctx, ARGS.refresh_site);
|
|
210
|
+
const result = await client.callTool("refresh_site", argsFromFlags(ARGS.refresh_site, flags, ctx.positionals));
|
|
211
|
+
ok(ctx, result.message);
|
|
212
|
+
printResult(io, flags, result.report, (r) => r.skipped.map((s) => ` skipped: ${s}`).join("\n"));
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function siteUnmap(ctx) {
|
|
217
|
+
const { client, flags, io } = ctx;
|
|
218
|
+
refuseUnknownFlags(ctx, ARGS.unmap_site);
|
|
219
|
+
const result = await client.callTool("unmap_site", argsFromFlags(ARGS.unmap_site, flags, ctx.positionals));
|
|
220
|
+
ok(ctx, result.message);
|
|
221
|
+
printResult(io, flags, result, () => "");
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
190
225
|
async function siteTree(ctx) {
|
|
191
226
|
const { client, flags, io } = ctx;
|
|
192
227
|
refuseUnknownFlags(ctx, ARGS.get_site_tree);
|
package/lib/documents.mjs
CHANGED
|
@@ -83,6 +83,8 @@ export const JSON_ARG_FLAG_PAIRS = [
|
|
|
83
83
|
["csv", "csv-file"],
|
|
84
84
|
// A site proposal's tool input, parsed as JSON (site.mjs proposalInput).
|
|
85
85
|
["input", "input-file"],
|
|
86
|
+
// A Vercel API token read verbatim, so it stays out of shell history (outbound.mjs mail vercel-key set).
|
|
87
|
+
["token", "token-file"],
|
|
86
88
|
];
|
|
87
89
|
|
|
88
90
|
/** Lower-case extension without the dot. */
|