posthorn 0.2.4 → 0.2.6
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 +34 -22
- package/dist/index.js +21 -16
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
# Posthorn
|
|
2
2
|
|
|
3
|
-
**Posthorn spins up cold-email infrastructure
|
|
4
|
-
SPF/DKIM/DMARC, and inbox warmup
|
|
5
|
-
AI agent.**
|
|
3
|
+
**Posthorn spins up cold-email infrastructure: sending domains, mailboxes,
|
|
4
|
+
SPF/DKIM/DMARC, and inbox warmup. It runs on accounts you own and is drivable
|
|
5
|
+
entirely by an AI agent.**
|
|
6
6
|
|
|
7
7
|
It's a pure orchestration layer: you own your Cloudflare account, your Google
|
|
8
8
|
Workspace, your domains, and your mailboxes. Posthorn automates the setup and
|
|
9
|
-
warmup on top of accounts you control
|
|
9
|
+
warmup on top of accounts you control. It never owns or bills for any of it.
|
|
10
10
|
|
|
11
11
|
## Quick start with an AI agent (recommended)
|
|
12
12
|
|
|
13
13
|
Posthorn is built to be driven by an agent (Claude Code, Codex, Cursor, etc.).
|
|
14
14
|
Paste this into your agent:
|
|
15
15
|
|
|
16
|
-
> I want to set up cold-email infrastructure
|
|
16
|
+
> I want to set up cold-email infrastructure: sending domains, mailboxes, and
|
|
17
17
|
> inbox warmup. Use the Posthorn CLI to do it. First run `npm install -g posthorn`
|
|
18
18
|
> then `posthorn guide`, read the guide, and walk me through the whole setup
|
|
19
|
-
> step by step
|
|
19
|
+
> step by step. Run the commands for me and tell me whenever I need to do
|
|
20
20
|
> something in my browser.
|
|
21
21
|
|
|
22
22
|
The agent reads `posthorn guide` (which prints this document) and orchestrates
|
|
@@ -50,7 +50,7 @@ never owns or bills for any of it.
|
|
|
50
50
|
4. Domain → buy new OR connect existing OR managed DNS
|
|
51
51
|
5. DKIM → browser step (see below)
|
|
52
52
|
6. Mailboxes → `posthorn mailboxes create ...`
|
|
53
|
-
7. Verify account → `posthorn auth verify` (unlocks sending/warmup
|
|
53
|
+
7. Verify account → `posthorn auth verify` (unlocks sending/warmup, "verified" tier)
|
|
54
54
|
8. Warmup → `posthorn warmup start <mailbox-id>`
|
|
55
55
|
|
|
56
56
|
Dependencies: you CANNOT create a mailbox until the domain status is `ready`.
|
|
@@ -62,15 +62,24 @@ stored locally, so you can stop and continue any time.
|
|
|
62
62
|
## The external/manual steps (things that happen OUTSIDE the CLI)
|
|
63
63
|
|
|
64
64
|
These are the steps you must GUIDE THE USER through in their browser. Give exact
|
|
65
|
-
click-by-click instructions.
|
|
65
|
+
click-by-click instructions. Tell the user up front that these are one-time
|
|
66
|
+
setup steps; once done, they never repeat them. Future domains and mailboxes
|
|
67
|
+
are fully automated (the only exceptions: DKIM is once per domain, delegation
|
|
68
|
+
is once per Workspace org).
|
|
66
69
|
|
|
67
70
|
### Cloudflare API token (one-time, for "buy new" or "own Cloudflare" domains)
|
|
68
71
|
- dash.cloudflare.com/profile/api-tokens → Create Token → Custom Token
|
|
69
|
-
- Permissions: Account>
|
|
72
|
+
- Permissions: Account>Account Settings>Read, Account>Registrar: Domains>Admin,
|
|
70
73
|
Zone>Zone>Edit, Zone>DNS>Edit. Resources: Include All.
|
|
74
|
+
- Do NOT add Billing permissions; they are not needed. Domain purchases charge
|
|
75
|
+
the payment method already on the Cloudflare account, which is a dashboard
|
|
76
|
+
setting, not a token permission.
|
|
71
77
|
- Then: `posthorn accounts cloudflare <token>`
|
|
78
|
+
- This token is only for Cloudflare (domains and DNS). Google Workspace is NOT
|
|
79
|
+
connected with this token; it has its own separate one-time step below
|
|
80
|
+
(domain-wide delegation, no token involved).
|
|
72
81
|
- For domain PURCHASES, the user also needs a payment method on their Cloudflare
|
|
73
|
-
account (they pay Cloudflare directly
|
|
82
|
+
account (they pay Cloudflare directly; Posthorn never bills for domains).
|
|
74
83
|
|
|
75
84
|
### Google Workspace domain-wide delegation (one-time per Workspace org)
|
|
76
85
|
- admin.google.com → Security → Access and data control → API controls →
|
|
@@ -79,7 +88,7 @@ click-by-click instructions.
|
|
|
79
88
|
- OAuth scopes (paste all, comma-separated):
|
|
80
89
|
`https://mail.google.com/,https://www.googleapis.com/auth/admin.directory.user,https://www.googleapis.com/auth/admin.directory.domain,https://www.googleapis.com/auth/siteverification`
|
|
81
90
|
- Then ask for their admin email and run: `posthorn accounts workspace <admin-email>`
|
|
82
|
-
- This replaces OAuth entirely
|
|
91
|
+
- This replaces OAuth entirely: no consent screen, no app verification, no test users.
|
|
83
92
|
|
|
84
93
|
### Nameservers (only for "managed DNS" or moving a domain to Cloudflare)
|
|
85
94
|
- The CLI prints 2 nameservers. The user sets them at their domain REGISTRAR
|
|
@@ -87,10 +96,10 @@ click-by-click instructions.
|
|
|
87
96
|
- Propagation takes minutes to 24h. Check with `posthorn domains activate <id>`
|
|
88
97
|
(managed) or by polling `posthorn domains get <id>`.
|
|
89
98
|
|
|
90
|
-
### DKIM (per domain
|
|
99
|
+
### DKIM (per domain, Google has no DKIM API)
|
|
91
100
|
If you have browser-automation tools, do it for the user:
|
|
92
101
|
- Navigate to https://admin.google.com/ac/apps/gmail/authenticateemail
|
|
93
|
-
(the URL may need /u/N/ for the right account slot
|
|
102
|
+
(the URL may need /u/N/ for the right account slot, verify the "Selected
|
|
94
103
|
domain" on the page matches).
|
|
95
104
|
- If status is "Authenticating email with DKIM" → already done, skip.
|
|
96
105
|
- Otherwise click GENERATE NEW RECORD → GENERATE (defaults: 2048-bit, "google"
|
|
@@ -106,11 +115,11 @@ PRINCIPLE: domain purchase ALWAYS happens on the user's own Cloudflare account
|
|
|
106
115
|
and card. Posthorn never owns or bills for domains.
|
|
107
116
|
|
|
108
117
|
1. NEW domain → requires the user's own Cloudflare account (+ payment method).
|
|
109
|
-
- `posthorn domains check name1.com name2.io --cloudflare <id
|
|
110
|
-
- `posthorn domains reputation <domain
|
|
118
|
+
- `posthorn domains check name1.com name2.io --cloudflare <id>`, fast availability + price
|
|
119
|
+
- `posthorn domains reputation <domain>`, VET it before buying (Spamhaus
|
|
111
120
|
blocklist/score + archive.org prior-use). Verdicts: clean / caution (used
|
|
112
121
|
before) / avoid (blocklisted or poor reputation). Run this on the candidate
|
|
113
|
-
before `buy
|
|
122
|
+
before `buy`; you can't tell a tainted domain from its name. (Slower + uses
|
|
114
123
|
a quota, so it's a separate explicit call, not part of `check`.)
|
|
115
124
|
- `posthorn domains buy <domain> --cloudflare <id> --contact '{...}'`
|
|
116
125
|
|
|
@@ -124,7 +133,7 @@ and card. Posthorn never owns or bills for domains.
|
|
|
124
133
|
- user sets them at their registrar
|
|
125
134
|
- `posthorn domains activate <domain-id>` → checks propagation, starts DNS setup
|
|
126
135
|
- Best for DEDICATED email-only domains. Don't use on a domain running a live
|
|
127
|
-
website
|
|
136
|
+
website; Posthorn becomes authoritative for ALL its DNS.
|
|
128
137
|
|
|
129
138
|
After any path, poll `posthorn domains get <id>` until status is `ready`. DNS
|
|
130
139
|
records (MX, SPF, DMARC) are configured automatically. Statuses progress:
|
|
@@ -134,7 +143,7 @@ workspace_verifying → workspace_verified → ready.
|
|
|
134
143
|
## Mailboxes & sending
|
|
135
144
|
|
|
136
145
|
- Create: `posthorn mailboxes create <domain-id> --email john@dom.com --first John --last Smith`
|
|
137
|
-
Returns a one-time password (Google Workspace login). Save it
|
|
146
|
+
Returns a one-time password (Google Workspace login). Save it (not shown again).
|
|
138
147
|
- Credentials for IMAP/SMTP are auto-provisioned via the service account
|
|
139
148
|
(domain-wide delegation). Google mailboxes need no per-mailbox setup.
|
|
140
149
|
- Send: `posthorn send <mailbox-id> --to x@y.com --subject "Hi" --body "..."`
|
|
@@ -148,16 +157,19 @@ Anonymous tier can do setup (accounts, domains). To send/warmup, verify:
|
|
|
148
157
|
## Warmup
|
|
149
158
|
|
|
150
159
|
- `posthorn warmup start <mailbox-id>` → joins the shared warmup pool, ramps over ~30 days
|
|
160
|
+
- Volume and reply behavior are platform-managed (ramps from 2/day to 5/day;
|
|
161
|
+
peers reply to ~30% of warmup mail). Not configurable: the pool is shared,
|
|
162
|
+
so settings are tuned for everyone's deliverability.
|
|
151
163
|
- `posthorn warmup stats <campaign-id>` → placement rate, reputation, daily breakdown
|
|
152
164
|
- `posthorn warmup list` → all campaigns
|
|
153
|
-
- Warmup never fully stops on its own
|
|
165
|
+
- Warmup never fully stops on its own, pause with `posthorn warmup pause <id>`.
|
|
154
166
|
|
|
155
167
|
## Feedback (bugs, feature requests)
|
|
156
168
|
|
|
157
169
|
`posthorn feedback --type bug|feature|question|other "<message>"`
|
|
158
170
|
|
|
159
171
|
If you hit a bug, missing capability, or rough edge while driving this CLI,
|
|
160
|
-
report it
|
|
172
|
+
report it, it goes straight to the Posthorn team. Agents: you are encouraged
|
|
161
173
|
to file feedback yourself when something doesn't work the way you expected
|
|
162
174
|
(include the command you ran and what you expected). Works on any tier, no
|
|
163
175
|
verification needed.
|
|
@@ -165,11 +177,11 @@ verification needed.
|
|
|
165
177
|
## Tips for agents
|
|
166
178
|
|
|
167
179
|
- Read commands auto-detect output: when stdout isn't a TTY (i.e. you're an agent
|
|
168
|
-
capturing output), they emit JSON automatically
|
|
180
|
+
capturing output), they emit JSON automatically, no flag needed. Humans in a
|
|
169
181
|
terminal get formatted text. Force either way with `--json` / `--pretty`.
|
|
170
182
|
- Async steps (domain provisioning, nameserver propagation) need polling, not blocking.
|
|
171
183
|
- Every command stores state locally (~/.config), so the user doesn't re-enter keys.
|
|
172
184
|
- Translate everything into plain English for the user. Never show them raw JSON
|
|
173
|
-
or ask them to run curl
|
|
185
|
+
or ask them to run curl, you run the CLI on their behalf.
|
|
174
186
|
- When a step needs the user's browser (Cloudflare token, delegation, nameservers,
|
|
175
187
|
DKIM), give exact click-by-click instructions and wait for confirmation.
|
package/dist/index.js
CHANGED
|
@@ -78,7 +78,7 @@ async function register(options) {
|
|
|
78
78
|
console.log(` API Key: ${chalk.cyan(data.api_key)}`);
|
|
79
79
|
console.log(` Tier: ${chalk.yellow(data.user.tier)}`);
|
|
80
80
|
console.log();
|
|
81
|
-
console.log(chalk.dim(" Save this API key
|
|
81
|
+
console.log(chalk.dim(" Save this API key, it won't be shown again."));
|
|
82
82
|
console.log(chalk.dim(` Config stored at: ${getConfigPath()}`));
|
|
83
83
|
} catch (err) {
|
|
84
84
|
spinner.fail(`Could not reach ${apiUrl}`);
|
|
@@ -338,7 +338,7 @@ async function getDomain(domainId, options = {}) {
|
|
|
338
338
|
function verdictText(rep) {
|
|
339
339
|
switch (rep.verdict) {
|
|
340
340
|
case "avoid":
|
|
341
|
-
return chalk3.red(`\u26A0 ${rep.verdictLabel}
|
|
341
|
+
return chalk3.red(`\u26A0 ${rep.verdictLabel}, avoid`);
|
|
342
342
|
case "caution":
|
|
343
343
|
return chalk3.yellow(rep.verdictLabel);
|
|
344
344
|
case "clean":
|
|
@@ -419,7 +419,7 @@ async function createMailbox(domainId, options) {
|
|
|
419
419
|
console.log(` ${chalk4.bold("Email:")} ${data.credentials.email}`);
|
|
420
420
|
console.log(` ${chalk4.bold("Password:")} ${chalk4.yellow(data.credentials.password)}`);
|
|
421
421
|
console.log();
|
|
422
|
-
console.log(chalk4.red(" Save this password now
|
|
422
|
+
console.log(chalk4.red(" Save this password now, it will not be shown again."));
|
|
423
423
|
console.log(chalk4.dim(" Login at: https://mail.google.com"));
|
|
424
424
|
}
|
|
425
425
|
async function sendEmail(mailboxId, options) {
|
|
@@ -447,7 +447,7 @@ async function provisionCredentials(mailboxId, options) {
|
|
|
447
447
|
body: { authType: options.type ?? "xoauth2" }
|
|
448
448
|
});
|
|
449
449
|
if (data.connectivity.send && data.connectivity.imap) {
|
|
450
|
-
spinner.succeed(`Credentials provisioned
|
|
450
|
+
spinner.succeed(`Credentials provisioned, sending (${data.connectivity.sendMethod}) and IMAP connected!`);
|
|
451
451
|
} else {
|
|
452
452
|
spinner.warn("Credentials provisioned but connectivity issues:");
|
|
453
453
|
for (const err of data.connectivity.errors) {
|
|
@@ -461,14 +461,19 @@ import chalk5 from "chalk";
|
|
|
461
461
|
import ora5 from "ora";
|
|
462
462
|
async function startWarmup(mailboxId) {
|
|
463
463
|
const spinner = ora5("Starting warmup...").start();
|
|
464
|
-
const data = await api(
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
464
|
+
const data = await api(
|
|
465
|
+
"/api/warmup/campaigns",
|
|
466
|
+
{
|
|
467
|
+
method: "POST",
|
|
468
|
+
body: { mailboxId, autoStart: true }
|
|
469
|
+
}
|
|
470
|
+
);
|
|
471
|
+
const effective = data.campaign.config ?? {};
|
|
468
472
|
spinner.succeed("Warmup started!");
|
|
469
473
|
console.log(` Campaign ID: ${chalk5.dim(data.campaign.id)}`);
|
|
470
474
|
console.log(` Status: ${chalk5.green("active")}`);
|
|
471
|
-
console.log(
|
|
475
|
+
console.log(` Daily cap: ${effective.daily_limit ?? 5}/day (ramps up from 2)`);
|
|
476
|
+
console.log(` Reply rate: ${Math.round((effective.reply_rate ?? 0.3) * 100)}%`);
|
|
472
477
|
}
|
|
473
478
|
async function pauseWarmup(campaignId) {
|
|
474
479
|
await api(`/api/warmup/campaigns/${campaignId}`, {
|
|
@@ -553,7 +558,7 @@ async function sendFeedback(messageWords, options) {
|
|
|
553
558
|
}
|
|
554
559
|
}
|
|
555
560
|
});
|
|
556
|
-
console.log(chalk6.green("Feedback recorded
|
|
561
|
+
console.log(chalk6.green("Feedback recorded, thank you!"));
|
|
557
562
|
console.log(` id: ${chalk6.dim(data.feedback.id)}`);
|
|
558
563
|
console.log(` type: ${chalk6.dim(data.feedback.type)}`);
|
|
559
564
|
}
|
|
@@ -569,14 +574,14 @@ function guide() {
|
|
|
569
574
|
console.log(readme);
|
|
570
575
|
} catch {
|
|
571
576
|
console.log(
|
|
572
|
-
"Posthorn
|
|
577
|
+
"Posthorn, see the full guide at https://www.npmjs.com/package/posthorn\nQuick start: posthorn auth register, then posthorn accounts cloudflare <token>, posthorn accounts workspace <admin-email>, posthorn domains connect <domain> --cloudflare <id>."
|
|
573
578
|
);
|
|
574
579
|
}
|
|
575
580
|
}
|
|
576
581
|
|
|
577
582
|
// src/index.ts
|
|
578
583
|
var program = new Command();
|
|
579
|
-
program.name("posthorn").description("Posthorn
|
|
584
|
+
program.name("posthorn").description("Posthorn: domain setup, mailbox creation, and email warmup").version("0.2.6");
|
|
580
585
|
program.addHelpText("after", `
|
|
581
586
|
Agents: run 'posthorn guide' first for the full workflow playbook.
|
|
582
587
|
|
|
@@ -591,10 +596,10 @@ Typical flow:
|
|
|
591
596
|
posthorn auth verify unlock sending + warmup
|
|
592
597
|
posthorn warmup start <mailbox-id>
|
|
593
598
|
|
|
594
|
-
Output: read commands auto-detect
|
|
599
|
+
Output: read commands auto-detect. Agents (non-TTY) get JSON, humans get
|
|
595
600
|
formatted text. Force either with --json or --pretty.
|
|
596
601
|
`);
|
|
597
|
-
program.command("guide").description("Print the full agent playbook
|
|
602
|
+
program.command("guide").description("Print the full agent playbook: workflow, external steps, and how to drive the CLI. Run this first.").action(guide);
|
|
598
603
|
var auth = program.command("auth").description("Account management");
|
|
599
604
|
auth.command("register").description("Create a new account and get an API key").option("--url <url>", "API server URL", "https://api-production-08f2.up.railway.app").action(register);
|
|
600
605
|
auth.command("status").description("Show current account info and setup progress").option("--json", "Force JSON output").option("--pretty", "Force human-readable output").action(status);
|
|
@@ -607,7 +612,7 @@ var domains = program.command("domains").description("Domain management");
|
|
|
607
612
|
domains.command("list").description("List all domains").option("--json", "Force JSON output").option("--pretty", "Force human-readable output").action(listDomains);
|
|
608
613
|
domains.command("get <domain-id>").description("Get domain details (poll this until status is 'ready')").option("--json", "Force JSON output").option("--pretty", "Force human-readable output").action(getDomain);
|
|
609
614
|
domains.command("check <domains...>").description("Check domain availability + pricing (fast)").requiredOption("--cloudflare <account-id>", "Cloudflare account ID").option("--json", "Force JSON output").option("--pretty", "Force human-readable output").action(checkDomains);
|
|
610
|
-
domains.command("reputation <domains...>").description("Vet a domain before buying
|
|
615
|
+
domains.command("reputation <domains...>").description("Vet a domain before buying, blocklist + reputation + prior-use history").option("--json", "Force JSON output").option("--pretty", "Force human-readable output").action(domainReputation);
|
|
611
616
|
domains.command("buy <domain>").description("Purchase a domain").requiredOption("--cloudflare <account-id>", "Cloudflare account ID").option("--contact <json>", "Registrant contact info as JSON").action(buyDomain);
|
|
612
617
|
domains.command("connect <domain>").description("Connect an existing domain on your own Cloudflare account").requiredOption("--cloudflare <account-id>", "Cloudflare account ID").option("--workspace <account-id>", "Workspace account ID").action(connectDomain);
|
|
613
618
|
domains.command("managed <domain>").description("Add an existing domain to Posthorn-managed DNS (returns nameservers to set at your registrar)").option("--workspace <account-id>", "Workspace account ID").action(managedDomain);
|
|
@@ -622,7 +627,7 @@ warmup.command("list").description("List warmup campaigns").action(listCampaigns
|
|
|
622
627
|
warmup.command("start <mailbox-id>").description("Start warming a mailbox").action(startWarmup);
|
|
623
628
|
warmup.command("pause <campaign-id>").description("Pause a warmup campaign").action(pauseWarmup);
|
|
624
629
|
warmup.command("stats <campaign-id>").description("Show warmup stats and daily breakdown").option("--json", "Force JSON output").option("--pretty", "Force human-readable output").action(warmupStats);
|
|
625
|
-
program.command("feedback <message...>").description("Send feedback to the Posthorn team
|
|
630
|
+
program.command("feedback <message...>").description("Send feedback to the Posthorn team: bug reports, feature requests, anything").option("-t, --type <type>", "bug | feature | question | other", "other").action(sendFeedback);
|
|
626
631
|
program.hook("preAction", () => {
|
|
627
632
|
});
|
|
628
633
|
program.parseAsync(process.argv).catch((err) => {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "posthorn",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.6",
|
|
4
|
+
"description": "Domain setup, mailbox creation, and email warmup from the command line",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"posthorn": "dist/index.js"
|