posthorn 0.1.0 → 0.2.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 +165 -0
- package/dist/index.js +145 -216
- package/package.json +3 -2
package/README.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Posthorn
|
|
2
|
+
|
|
3
|
+
**Posthorn spins up cold-email infrastructure — sending domains, mailboxes,
|
|
4
|
+
SPF/DKIM/DMARC, and inbox warmup — on accounts you own, drivable entirely by an
|
|
5
|
+
AI agent.**
|
|
6
|
+
|
|
7
|
+
It's a pure orchestration layer: you own your Cloudflare account, your Google
|
|
8
|
+
Workspace, your domains, and your mailboxes. Posthorn automates the setup and
|
|
9
|
+
warmup on top of accounts you control — it never owns or bills for any of it.
|
|
10
|
+
|
|
11
|
+
## Quick start with an AI agent (recommended)
|
|
12
|
+
|
|
13
|
+
Posthorn is built to be driven by an agent (Claude Code, Codex, Cursor, etc.).
|
|
14
|
+
Paste this into your agent:
|
|
15
|
+
|
|
16
|
+
> I want to set up cold-email infrastructure — sending domains, mailboxes, and
|
|
17
|
+
> inbox warmup. Use the Posthorn CLI to do it. First run `npm install -g posthorn`
|
|
18
|
+
> then `posthorn guide`, read the guide, and walk me through the whole setup
|
|
19
|
+
> step by step — run the commands for me and tell me whenever I need to do
|
|
20
|
+
> something in my browser.
|
|
21
|
+
|
|
22
|
+
The agent reads `posthorn guide` (which prints this document) and orchestrates
|
|
23
|
+
the rest.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
npm install -g posthorn
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or run any command without installing via `npx posthorn <command>`.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
# Agent playbook
|
|
36
|
+
|
|
37
|
+
If you're an AI agent driving the `posthorn` CLI for a user, this is the
|
|
38
|
+
knowledge you need that `--help` alone doesn't give you. Read it fully before
|
|
39
|
+
starting. Run `posthorn guide` any time to reprint it.
|
|
40
|
+
|
|
41
|
+
The platform is a pure orchestration layer: the USER owns their Cloudflare
|
|
42
|
+
account, their Google Workspace, their domains, and their mailboxes. Posthorn
|
|
43
|
+
never owns or bills for any of it.
|
|
44
|
+
|
|
45
|
+
## The end-to-end flow (in order)
|
|
46
|
+
|
|
47
|
+
1. Account → `posthorn auth register` (anonymous API key, instant)
|
|
48
|
+
2. Cloudflare → user creates account + API token, then `posthorn accounts cloudflare <token>`
|
|
49
|
+
3. Workspace → user sets up domain-wide delegation, then `posthorn accounts workspace <admin-email>`
|
|
50
|
+
4. Domain → buy new OR connect existing OR managed DNS
|
|
51
|
+
5. DKIM → browser step (see below)
|
|
52
|
+
6. Mailboxes → `posthorn mailboxes create ...`
|
|
53
|
+
7. Verify account → `posthorn auth verify` (unlocks sending/warmup — "verified" tier)
|
|
54
|
+
8. Warmup → `posthorn warmup start <mailbox-id>`
|
|
55
|
+
|
|
56
|
+
Dependencies: you CANNOT create a mailbox until the domain status is `ready`.
|
|
57
|
+
You CANNOT send or start warmup until the account is "verified" tier.
|
|
58
|
+
|
|
59
|
+
Run `posthorn auth status --json` any time to see where things stand. State is
|
|
60
|
+
stored locally, so you can stop and continue any time.
|
|
61
|
+
|
|
62
|
+
## The external/manual steps (things that happen OUTSIDE the CLI)
|
|
63
|
+
|
|
64
|
+
These are the steps you must GUIDE THE USER through in their browser. Give exact
|
|
65
|
+
click-by-click instructions.
|
|
66
|
+
|
|
67
|
+
### Cloudflare API token (one-time, for "buy new" or "own Cloudflare" domains)
|
|
68
|
+
- dash.cloudflare.com/profile/api-tokens → Create Token → Custom Token
|
|
69
|
+
- Permissions: Account>Registrar:Domains>Admin, Account>Billing>Edit,
|
|
70
|
+
Zone>Zone>Edit, Zone>DNS>Edit. Resources: Include All.
|
|
71
|
+
- Then: `posthorn accounts cloudflare <token>`
|
|
72
|
+
- For domain PURCHASES, the user also needs a payment method on their Cloudflare
|
|
73
|
+
account (they pay Cloudflare directly — Posthorn never bills for domains).
|
|
74
|
+
|
|
75
|
+
### Google Workspace domain-wide delegation (one-time per Workspace org)
|
|
76
|
+
- admin.google.com → Security → Access and data control → API controls →
|
|
77
|
+
Domain-wide delegation → Add new
|
|
78
|
+
- Client ID: 110137377718772968374
|
|
79
|
+
- OAuth scopes (paste all, comma-separated):
|
|
80
|
+
`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
|
+
- Then ask for their admin email and run: `posthorn accounts workspace <admin-email>`
|
|
82
|
+
- This replaces OAuth entirely — no consent screen, no app verification, no test users.
|
|
83
|
+
|
|
84
|
+
### Nameservers (only for "managed DNS" or moving a domain to Cloudflare)
|
|
85
|
+
- The CLI prints 2 nameservers. The user sets them at their domain REGISTRAR
|
|
86
|
+
(where they bought the domain), replacing the existing nameservers.
|
|
87
|
+
- Propagation takes minutes to 24h. Check with `posthorn domains activate <id>`
|
|
88
|
+
(managed) or by polling `posthorn domains get <id>`.
|
|
89
|
+
|
|
90
|
+
### DKIM (per domain — Google has no DKIM API)
|
|
91
|
+
If you have browser-automation tools, do it for the user:
|
|
92
|
+
- Navigate to https://admin.google.com/ac/apps/gmail/authenticateemail
|
|
93
|
+
(the URL may need /u/N/ for the right account slot — verify the "Selected
|
|
94
|
+
domain" on the page matches).
|
|
95
|
+
- If status is "Authenticating email with DKIM" → already done, skip.
|
|
96
|
+
- Otherwise click GENERATE NEW RECORD → GENERATE (defaults: 2048-bit, "google"
|
|
97
|
+
prefix). Read the TXT record value (starts with v=DKIM1; k=rsa; p=...).
|
|
98
|
+
- Add it to DNS as a TXT record named google._domainkey.<domain>.
|
|
99
|
+
- Click START AUTHENTICATION on the admin page.
|
|
100
|
+
If no browser tools: have the user do the above and paste you the TXT value,
|
|
101
|
+
then add it to their DNS.
|
|
102
|
+
|
|
103
|
+
## Domain options (3 paths)
|
|
104
|
+
|
|
105
|
+
PRINCIPLE: domain purchase ALWAYS happens on the user's own Cloudflare account
|
|
106
|
+
and card. Posthorn never owns or bills for domains.
|
|
107
|
+
|
|
108
|
+
1. NEW domain → requires the user's own Cloudflare account (+ payment method).
|
|
109
|
+
- `posthorn domains check name1.com name2.io --cloudflare <id>` — fast availability + price
|
|
110
|
+
- `posthorn domains reputation <domain>` — VET it before buying (Spamhaus
|
|
111
|
+
blocklist/score + archive.org prior-use). Verdicts: clean / caution (used
|
|
112
|
+
before) / avoid (blocklisted or poor reputation). Run this on the candidate
|
|
113
|
+
before `buy` — you can't tell a tainted domain from its name. (Slower + uses
|
|
114
|
+
a quota, so it's a separate explicit call, not part of `check`.)
|
|
115
|
+
- `posthorn domains buy <domain> --cloudflare <id> --contact '{...}'`
|
|
116
|
+
|
|
117
|
+
2. EXISTING domain, recommended → user's own Cloudflare account.
|
|
118
|
+
- `posthorn domains connect <domain> --cloudflare <account-id>`
|
|
119
|
+
- (If the domain isn't on Cloudflare yet, the user adds it at dash.cloudflare.com
|
|
120
|
+
and points nameservers to their account first.)
|
|
121
|
+
|
|
122
|
+
3. EXISTING domain, optional → Posthorn-managed DNS (no Cloudflare account needed).
|
|
123
|
+
- `posthorn domains managed <domain>` → prints 2 nameservers
|
|
124
|
+
- user sets them at their registrar
|
|
125
|
+
- `posthorn domains activate <domain-id>` → checks propagation, starts DNS setup
|
|
126
|
+
- Best for DEDICATED email-only domains. Don't use on a domain running a live
|
|
127
|
+
website — Posthorn becomes authoritative for ALL its DNS.
|
|
128
|
+
|
|
129
|
+
After any path, poll `posthorn domains get <id>` until status is `ready`. DNS
|
|
130
|
+
records (MX, SPF, DMARC) are configured automatically. Statuses progress:
|
|
131
|
+
pending_* → purchased → dns_configuring → dns_configured → dns_verified →
|
|
132
|
+
workspace_verifying → workspace_verified → ready.
|
|
133
|
+
|
|
134
|
+
## Mailboxes & sending
|
|
135
|
+
|
|
136
|
+
- 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 — not shown again.
|
|
138
|
+
- Credentials for IMAP/SMTP are auto-provisioned via the service account
|
|
139
|
+
(domain-wide delegation). Google mailboxes need no per-mailbox setup.
|
|
140
|
+
- Send: `posthorn send <mailbox-id> --to x@y.com --subject "Hi" --body "..."`
|
|
141
|
+
(requires "verified" tier)
|
|
142
|
+
|
|
143
|
+
## Account verification (unlocks sending/warmup)
|
|
144
|
+
|
|
145
|
+
Anonymous tier can do setup (accounts, domains). To send/warmup, verify:
|
|
146
|
+
`posthorn auth verify` → choose admin email or a custom email → confirm the code.
|
|
147
|
+
|
|
148
|
+
## Warmup
|
|
149
|
+
|
|
150
|
+
- `posthorn warmup start <mailbox-id>` → joins the shared warmup pool, ramps over ~30 days
|
|
151
|
+
- `posthorn warmup stats <campaign-id>` → placement rate, reputation, daily breakdown
|
|
152
|
+
- `posthorn warmup list` → all campaigns
|
|
153
|
+
- Warmup never fully stops on its own — pause with `posthorn warmup pause <id>`.
|
|
154
|
+
|
|
155
|
+
## Tips for agents
|
|
156
|
+
|
|
157
|
+
- Read commands auto-detect output: when stdout isn't a TTY (i.e. you're an agent
|
|
158
|
+
capturing output), they emit JSON automatically — no flag needed. Humans in a
|
|
159
|
+
terminal get formatted text. Force either way with `--json` / `--pretty`.
|
|
160
|
+
- Async steps (domain provisioning, nameserver propagation) need polling, not blocking.
|
|
161
|
+
- Every command stores state locally (~/.config), so the user doesn't re-enter keys.
|
|
162
|
+
- Translate everything into plain English for the user. Never show them raw JSON
|
|
163
|
+
or ask them to run curl — you run the CLI on their behalf.
|
|
164
|
+
- When a step needs the user's browser (Cloudflare token, delegation, nameservers,
|
|
165
|
+
DKIM), give exact click-by-click instructions and wait for confirmation.
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import
|
|
5
|
+
import chalk6 from "chalk";
|
|
6
6
|
|
|
7
7
|
// src/commands/auth.ts
|
|
8
8
|
import chalk from "chalk";
|
|
@@ -46,6 +46,13 @@ function getConfigPath() {
|
|
|
46
46
|
return conf.path;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// src/output.ts
|
|
50
|
+
function useJson(options = {}) {
|
|
51
|
+
if (options.json) return true;
|
|
52
|
+
if (options.pretty) return false;
|
|
53
|
+
return !process.stdout.isTTY;
|
|
54
|
+
}
|
|
55
|
+
|
|
49
56
|
// src/commands/auth.ts
|
|
50
57
|
async function register(options) {
|
|
51
58
|
const apiUrl = options.url ?? "https://api-production-08f2.up.railway.app";
|
|
@@ -78,8 +85,19 @@ async function register(options) {
|
|
|
78
85
|
console.log(chalk.dim(` Is the server running? Try: posthorn auth register --url <your-api-url>`));
|
|
79
86
|
}
|
|
80
87
|
}
|
|
81
|
-
async function status() {
|
|
88
|
+
async function status(options = {}) {
|
|
82
89
|
const config = getConfig();
|
|
90
|
+
if (useJson(options)) {
|
|
91
|
+
console.log(JSON.stringify({
|
|
92
|
+
loggedIn: !!config.apiKey,
|
|
93
|
+
apiUrl: config.apiUrl,
|
|
94
|
+
userId: config.userId,
|
|
95
|
+
cloudflareConnected: config.cloudflareConnected,
|
|
96
|
+
workspaceConnected: config.workspaceConnected,
|
|
97
|
+
setupStep: config.setupStep
|
|
98
|
+
}, null, 2));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
83
101
|
if (!config.apiKey) {
|
|
84
102
|
console.log(chalk.yellow("Not logged in."));
|
|
85
103
|
console.log(` Run: ${chalk.cyan("posthorn auth register")}`);
|
|
@@ -117,7 +135,7 @@ var ApiError = class extends Error {
|
|
|
117
135
|
async function api(path, options = {}) {
|
|
118
136
|
const config = getConfig();
|
|
119
137
|
if (!config.apiKey) {
|
|
120
|
-
throw new Error("Not logged in. Run: posthorn
|
|
138
|
+
throw new Error("Not logged in. Run: posthorn auth register");
|
|
121
139
|
}
|
|
122
140
|
const url = `${config.apiUrl}${path}`;
|
|
123
141
|
const res = await fetch(url, {
|
|
@@ -193,10 +211,12 @@ async function listAccounts() {
|
|
|
193
211
|
// src/commands/domains.ts
|
|
194
212
|
import chalk3 from "chalk";
|
|
195
213
|
import ora3 from "ora";
|
|
196
|
-
async function listDomains() {
|
|
197
|
-
const spinner = ora3("Fetching domains...").start();
|
|
214
|
+
async function listDomains(options = {}) {
|
|
198
215
|
const data = await api("/api/domains");
|
|
199
|
-
|
|
216
|
+
if (useJson(options)) {
|
|
217
|
+
console.log(JSON.stringify(data.domains, null, 2));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
200
220
|
if (data.domains.length === 0) {
|
|
201
221
|
console.log(chalk3.dim(" No domains yet. Run: posthorn domains buy <domain>"));
|
|
202
222
|
return;
|
|
@@ -250,15 +270,57 @@ async function connectDomain(domain, options) {
|
|
|
250
270
|
spinner.succeed(data.message);
|
|
251
271
|
console.log(` Domain ID: ${chalk3.dim(data.domain.id)}`);
|
|
252
272
|
}
|
|
253
|
-
async function
|
|
273
|
+
async function managedDomain(domain, options) {
|
|
274
|
+
const spinner = ora3(`Adding ${domain} to managed DNS...`).start();
|
|
275
|
+
const body = { name: domain };
|
|
276
|
+
if (options.workspace) body.workspaceAccountId = options.workspace;
|
|
277
|
+
const data = await api("/api/domains/managed", { method: "POST", body });
|
|
278
|
+
spinner.succeed("Domain added to managed DNS!");
|
|
279
|
+
console.log();
|
|
280
|
+
console.log(" Set these 2 nameservers at your registrar:");
|
|
281
|
+
for (const ns of data.nameServers) {
|
|
282
|
+
console.log(` ${chalk3.cyan(ns)}`);
|
|
283
|
+
}
|
|
284
|
+
console.log();
|
|
285
|
+
console.log(chalk3.dim(" Once changed (takes minutes to 24h), run:"));
|
|
286
|
+
console.log(chalk3.cyan(` posthorn domains activate ${data.domain.id}`));
|
|
287
|
+
}
|
|
288
|
+
async function activateDomain(domainId) {
|
|
289
|
+
const spinner = ora3("Checking nameserver propagation...").start();
|
|
290
|
+
const data = await api(`/api/domains/${domainId}/activate`, { method: "POST" });
|
|
291
|
+
if (data.active) {
|
|
292
|
+
spinner.succeed(data.message);
|
|
293
|
+
} else {
|
|
294
|
+
spinner.warn(data.message);
|
|
295
|
+
console.log(chalk3.dim(` Cloudflare status: ${data.cloudflareStatus ?? "pending"}`));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
async function getDomain(domainId, options = {}) {
|
|
254
299
|
const data = await api(`/api/domains/${domainId}`);
|
|
255
300
|
const d = data.domain;
|
|
301
|
+
if (useJson(options)) {
|
|
302
|
+
console.log(JSON.stringify(d, null, 2));
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
256
305
|
console.log(chalk3.bold("Domain\n"));
|
|
257
306
|
console.log(` Name: ${chalk3.cyan(d.name)}`);
|
|
258
307
|
console.log(` Status: ${d.status === "ready" ? chalk3.green(d.status) : chalk3.yellow(d.status)}`);
|
|
259
308
|
console.log(` ID: ${chalk3.dim(d.id)}`);
|
|
309
|
+
console.log(` DNS: ${d.dns_mode === "managed" ? "managed by Posthorn" : "your Cloudflare"}`);
|
|
260
310
|
if (d.cloudflare_zone_id) console.log(` Zone: ${chalk3.dim(d.cloudflare_zone_id)}`);
|
|
261
311
|
}
|
|
312
|
+
function verdictText(rep) {
|
|
313
|
+
switch (rep.verdict) {
|
|
314
|
+
case "avoid":
|
|
315
|
+
return chalk3.red(`\u26A0 ${rep.verdictLabel} \u2014 avoid`);
|
|
316
|
+
case "caution":
|
|
317
|
+
return chalk3.yellow(rep.verdictLabel);
|
|
318
|
+
case "clean":
|
|
319
|
+
return chalk3.green(rep.verdictLabel);
|
|
320
|
+
default:
|
|
321
|
+
return chalk3.dim(rep.verdictLabel);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
262
324
|
async function checkDomains(domains2, options) {
|
|
263
325
|
const spinner = ora3("Checking availability...").start();
|
|
264
326
|
const data = await api("/api/domains/check", {
|
|
@@ -266,11 +328,35 @@ async function checkDomains(domains2, options) {
|
|
|
266
328
|
body: { domains: domains2, cloudflareAccountId: options.cloudflare }
|
|
267
329
|
});
|
|
268
330
|
spinner.stop();
|
|
331
|
+
if (useJson(options)) {
|
|
332
|
+
console.log(JSON.stringify(data.results, null, 2));
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
269
335
|
console.log(chalk3.bold("Domain Availability\n"));
|
|
270
336
|
for (const r of data.results) {
|
|
271
337
|
const icon = r.available ? chalk3.green(" \u2713") : chalk3.red(" \u2717");
|
|
272
338
|
const price = r.price ? chalk3.dim(`$${r.price}/yr`) : "";
|
|
273
|
-
console.log(`${icon} ${r.domain}
|
|
339
|
+
console.log(`${icon} ${String(r.domain).padEnd(30)}${price}`);
|
|
340
|
+
}
|
|
341
|
+
console.log(chalk3.dim("\n Vet a domain before buying: posthorn domains reputation <domain>"));
|
|
342
|
+
}
|
|
343
|
+
async function domainReputation(domains2, options = {}) {
|
|
344
|
+
const spinner = ora3("Checking reputation (blocklist + history)...").start();
|
|
345
|
+
const data = await api("/api/domains/reputation", {
|
|
346
|
+
method: "POST",
|
|
347
|
+
body: { domains: domains2 }
|
|
348
|
+
});
|
|
349
|
+
spinner.stop();
|
|
350
|
+
if (useJson(options)) {
|
|
351
|
+
console.log(JSON.stringify(data.results, null, 2));
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
console.log(chalk3.bold("Domain Reputation\n"));
|
|
355
|
+
for (const rep of data.results) {
|
|
356
|
+
console.log(` ${chalk3.cyan(rep.domain.padEnd(28))}${verdictText(rep)}`);
|
|
357
|
+
for (const w of rep.warnings) {
|
|
358
|
+
console.log(chalk3.dim(` \u2022 ${w}`));
|
|
359
|
+
}
|
|
274
360
|
}
|
|
275
361
|
}
|
|
276
362
|
|
|
@@ -334,8 +420,8 @@ async function provisionCredentials(mailboxId, options) {
|
|
|
334
420
|
method: "POST",
|
|
335
421
|
body: { authType: options.type ?? "xoauth2" }
|
|
336
422
|
});
|
|
337
|
-
if (data.connectivity.
|
|
338
|
-
spinner.succeed(
|
|
423
|
+
if (data.connectivity.send && data.connectivity.imap) {
|
|
424
|
+
spinner.succeed(`Credentials provisioned \u2014 sending (${data.connectivity.sendMethod}) and IMAP connected!`);
|
|
339
425
|
} else {
|
|
340
426
|
spinner.warn("Credentials provisioned but connectivity issues:");
|
|
341
427
|
for (const err of data.connectivity.errors) {
|
|
@@ -365,8 +451,12 @@ async function pauseWarmup(campaignId) {
|
|
|
365
451
|
});
|
|
366
452
|
console.log(chalk5.yellow("Warmup paused."));
|
|
367
453
|
}
|
|
368
|
-
async function warmupStats(campaignId) {
|
|
454
|
+
async function warmupStats(campaignId, options = {}) {
|
|
369
455
|
const data = await api(`/api/warmup/campaigns/${campaignId}`);
|
|
456
|
+
if (useJson(options)) {
|
|
457
|
+
console.log(JSON.stringify(data, null, 2));
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
370
460
|
const s = data.stats;
|
|
371
461
|
const c = data.campaign;
|
|
372
462
|
console.log(chalk5.bold("Warmup Stats\n"));
|
|
@@ -403,221 +493,60 @@ async function listCampaigns() {
|
|
|
403
493
|
}
|
|
404
494
|
}
|
|
405
495
|
|
|
406
|
-
// src/commands/
|
|
407
|
-
import
|
|
408
|
-
import
|
|
409
|
-
import
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
function printProgress(currentStep) {
|
|
420
|
-
console.log();
|
|
421
|
-
for (let i = 0; i < STEPS.length; i++) {
|
|
422
|
-
const icon = i < currentStep ? chalk6.green(" \u2713") : i === currentStep ? chalk6.cyan(" \u2192") : chalk6.dim(" \u25CB");
|
|
423
|
-
const label = i === currentStep ? chalk6.bold(STEPS[i]) : i < currentStep ? STEPS[i] : chalk6.dim(STEPS[i]);
|
|
424
|
-
console.log(`${icon} Step ${i + 1}: ${label}`);
|
|
496
|
+
// src/commands/guide.ts
|
|
497
|
+
import { readFileSync } from "fs";
|
|
498
|
+
import { fileURLToPath } from "url";
|
|
499
|
+
import { dirname, join } from "path";
|
|
500
|
+
function guide() {
|
|
501
|
+
try {
|
|
502
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
503
|
+
const readme = readFileSync(join(here, "..", "README.md"), "utf8");
|
|
504
|
+
console.log(readme);
|
|
505
|
+
} catch {
|
|
506
|
+
console.log(
|
|
507
|
+
"Posthorn \u2014 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>."
|
|
508
|
+
);
|
|
425
509
|
}
|
|
426
|
-
console.log();
|
|
427
|
-
}
|
|
428
|
-
async function setup(options) {
|
|
429
|
-
console.log(chalk6.bold("\n Posthorn Setup\n"));
|
|
430
|
-
const config = getConfig();
|
|
431
|
-
let step = config.setupStep;
|
|
432
|
-
if (step < 1) {
|
|
433
|
-
printProgress(0);
|
|
434
|
-
if (!config.apiKey) {
|
|
435
|
-
await register({ url: options.url });
|
|
436
|
-
} else {
|
|
437
|
-
console.log(chalk6.green(" Account already exists."));
|
|
438
|
-
}
|
|
439
|
-
setConfig({ setupStep: 1 });
|
|
440
|
-
step = 1;
|
|
441
|
-
}
|
|
442
|
-
if (step < 2) {
|
|
443
|
-
printProgress(1);
|
|
444
|
-
if (!config.cloudflareConnected) {
|
|
445
|
-
const { hasCf } = await inquirer.prompt([
|
|
446
|
-
{ type: "confirm", name: "hasCf", message: "Do you have a Cloudflare account?", default: true }
|
|
447
|
-
]);
|
|
448
|
-
if (!hasCf) {
|
|
449
|
-
console.log();
|
|
450
|
-
console.log(" 1. Go to " + chalk6.cyan("https://dash.cloudflare.com/sign-up"));
|
|
451
|
-
console.log(" 2. Create a free account");
|
|
452
|
-
console.log(" 3. Add a payment method if you want to purchase domains");
|
|
453
|
-
console.log();
|
|
454
|
-
await inquirer.prompt([{ type: "confirm", name: "ready", message: "Ready to continue?" }]);
|
|
455
|
-
}
|
|
456
|
-
console.log();
|
|
457
|
-
console.log(" Create an API token:");
|
|
458
|
-
console.log(" 1. Go to " + chalk6.cyan("https://dash.cloudflare.com/profile/api-tokens"));
|
|
459
|
-
console.log(' 2. Click "Create Token" \u2192 "Create Custom Token"');
|
|
460
|
-
console.log(" 3. Add these permissions:");
|
|
461
|
-
console.log(" - Account \u2192 Registrar: Domains \u2192 Admin");
|
|
462
|
-
console.log(" - Account \u2192 Billing \u2192 Edit");
|
|
463
|
-
console.log(" - Zone \u2192 Zone \u2192 Edit");
|
|
464
|
-
console.log(" - Zone \u2192 DNS \u2192 Edit");
|
|
465
|
-
console.log(" 4. Account/Zone Resources: Include All");
|
|
466
|
-
console.log(' 5. Click "Continue to summary" \u2192 "Create Token"');
|
|
467
|
-
console.log();
|
|
468
|
-
const { token } = await inquirer.prompt([
|
|
469
|
-
{ type: "password", name: "token", message: "Paste your Cloudflare API token:", mask: "*" }
|
|
470
|
-
]);
|
|
471
|
-
await connectCloudflare(token);
|
|
472
|
-
} else {
|
|
473
|
-
console.log(chalk6.green(" Cloudflare already connected."));
|
|
474
|
-
}
|
|
475
|
-
setConfig({ setupStep: 2 });
|
|
476
|
-
step = 2;
|
|
477
|
-
}
|
|
478
|
-
if (step < 3) {
|
|
479
|
-
printProgress(2);
|
|
480
|
-
if (!config.workspaceConnected) {
|
|
481
|
-
const { hasWs } = await inquirer.prompt([
|
|
482
|
-
{ type: "confirm", name: "hasWs", message: "Do you have a Google Workspace account?", default: true }
|
|
483
|
-
]);
|
|
484
|
-
if (!hasWs) {
|
|
485
|
-
console.log();
|
|
486
|
-
console.log(" Set up a new Google Workspace:");
|
|
487
|
-
console.log(" 1. Go to " + chalk6.cyan("https://workspace.google.com") + " \u2192 Get Started");
|
|
488
|
-
console.log(' 2. Enter your business name, select "Just you"');
|
|
489
|
-
console.log(' 3. Click "Set up using your existing domain" and enter your domain');
|
|
490
|
-
console.log(" 4. Create your first email (this will be your outbound email)");
|
|
491
|
-
console.log(" 5. Complete billing setup (14-day free trial available)");
|
|
492
|
-
console.log();
|
|
493
|
-
await inquirer.prompt([{ type: "confirm", name: "ready", message: "Done setting up Workspace?" }]);
|
|
494
|
-
}
|
|
495
|
-
console.log();
|
|
496
|
-
console.log(" Set up domain-wide delegation:");
|
|
497
|
-
console.log(" 1. Go to " + chalk6.cyan("admin.google.com") + " \u2192 Security \u2192 API controls");
|
|
498
|
-
console.log(" 2. Click Domain-wide delegation \u2192 Add new");
|
|
499
|
-
console.log(" 3. Client ID: " + chalk6.yellow("110137377718772968374"));
|
|
500
|
-
console.log(" 4. Scopes: " + chalk6.dim("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"));
|
|
501
|
-
console.log(" 5. Click Authorize");
|
|
502
|
-
console.log();
|
|
503
|
-
await inquirer.prompt([{ type: "confirm", name: "ready", message: "Done with delegation?" }]);
|
|
504
|
-
const { adminEmail } = await inquirer.prompt([
|
|
505
|
-
{ type: "input", name: "adminEmail", message: "Your Workspace admin email:" }
|
|
506
|
-
]);
|
|
507
|
-
await connectWorkspace(adminEmail);
|
|
508
|
-
} else {
|
|
509
|
-
console.log(chalk6.green(" Google Workspace already connected."));
|
|
510
|
-
}
|
|
511
|
-
setConfig({ setupStep: 3 });
|
|
512
|
-
step = 3;
|
|
513
|
-
}
|
|
514
|
-
if (step < 4) {
|
|
515
|
-
printProgress(3);
|
|
516
|
-
const { domainChoice } = await inquirer.prompt([
|
|
517
|
-
{
|
|
518
|
-
type: "list",
|
|
519
|
-
name: "domainChoice",
|
|
520
|
-
message: "Domain setup:",
|
|
521
|
-
choices: [
|
|
522
|
-
{ name: "Buy a new domain", value: "buy" },
|
|
523
|
-
{ name: "Connect an existing domain (must be on Cloudflare)", value: "connect" },
|
|
524
|
-
{ name: "Skip for now", value: "skip" }
|
|
525
|
-
]
|
|
526
|
-
}
|
|
527
|
-
]);
|
|
528
|
-
if (domainChoice === "connect") {
|
|
529
|
-
const { domain } = await inquirer.prompt([
|
|
530
|
-
{ type: "input", name: "domain", message: "Domain name:" }
|
|
531
|
-
]);
|
|
532
|
-
const accounts2 = await api("/api/accounts");
|
|
533
|
-
const cfAccount = accounts2.accounts.find((a) => a.provider === "cloudflare");
|
|
534
|
-
if (cfAccount) {
|
|
535
|
-
const spinner = ora6(`Connecting ${domain}...`).start();
|
|
536
|
-
try {
|
|
537
|
-
const data = await api("/api/domains/connect", {
|
|
538
|
-
method: "POST",
|
|
539
|
-
body: {
|
|
540
|
-
name: domain,
|
|
541
|
-
cloudflareAccountId: cfAccount.id,
|
|
542
|
-
workspaceAccountId: config.workspaceAccountId
|
|
543
|
-
}
|
|
544
|
-
});
|
|
545
|
-
spinner.succeed(data.message);
|
|
546
|
-
} catch (err) {
|
|
547
|
-
spinner.fail(err.message);
|
|
548
|
-
console.log(chalk6.dim("\n If the domain isn't on Cloudflare yet:"));
|
|
549
|
-
console.log(chalk6.dim(" 1. Go to dash.cloudflare.com \u2192 Add a site \u2192 enter your domain"));
|
|
550
|
-
console.log(chalk6.dim(" 2. Change nameservers at your registrar to Cloudflare's"));
|
|
551
|
-
console.log(chalk6.dim(" 3. Run this setup again\n"));
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
} else if (domainChoice === "buy") {
|
|
555
|
-
console.log(chalk6.dim("\n Use the domains command to search and buy:"));
|
|
556
|
-
console.log(chalk6.cyan(" posthorn domains check example.com example.io --cloudflare <id>"));
|
|
557
|
-
console.log(chalk6.cyan(" posthorn domains buy example.com --cloudflare <id> --contact '{...}'"));
|
|
558
|
-
console.log();
|
|
559
|
-
}
|
|
560
|
-
setConfig({ setupStep: 4 });
|
|
561
|
-
step = 4;
|
|
562
|
-
}
|
|
563
|
-
if (step < 5) {
|
|
564
|
-
printProgress(4);
|
|
565
|
-
console.log(" DKIM needs to be set up per domain.");
|
|
566
|
-
console.log(" If you're using Claude Code, run the setup skill \u2014 it automates DKIM via browser.");
|
|
567
|
-
console.log(" Otherwise, go to admin.google.com \u2192 Apps \u2192 Gmail \u2192 Authenticate email");
|
|
568
|
-
console.log();
|
|
569
|
-
await inquirer.prompt([{ type: "confirm", name: "ready", message: "DKIM set up (or skipping for now)?" }]);
|
|
570
|
-
setConfig({ setupStep: 5 });
|
|
571
|
-
step = 5;
|
|
572
|
-
}
|
|
573
|
-
if (step < 6) {
|
|
574
|
-
printProgress(5);
|
|
575
|
-
console.log(" Create mailboxes with:");
|
|
576
|
-
console.log(chalk6.cyan(" posthorn mailboxes create <domain-id> --email john@yourdomain.com --first John --last Smith"));
|
|
577
|
-
console.log();
|
|
578
|
-
await inquirer.prompt([{ type: "confirm", name: "ready", message: "Mailboxes created (or skipping)?" }]);
|
|
579
|
-
setConfig({ setupStep: 6 });
|
|
580
|
-
step = 6;
|
|
581
|
-
}
|
|
582
|
-
if (step < 7) {
|
|
583
|
-
printProgress(6);
|
|
584
|
-
console.log(" Start warming your mailboxes:");
|
|
585
|
-
console.log(chalk6.cyan(" posthorn warmup start <mailbox-id>"));
|
|
586
|
-
console.log(chalk6.cyan(" posthorn warmup stats <campaign-id>"));
|
|
587
|
-
console.log();
|
|
588
|
-
await inquirer.prompt([{ type: "confirm", name: "ready", message: "Ready to finish setup?" }]);
|
|
589
|
-
setConfig({ setupStep: 7 });
|
|
590
|
-
step = 7;
|
|
591
|
-
}
|
|
592
|
-
printProgress(7);
|
|
593
|
-
console.log(chalk6.bold.green(" Setup complete!\n"));
|
|
594
|
-
console.log(" Useful commands:");
|
|
595
|
-
console.log(chalk6.cyan(" posthorn domains list"));
|
|
596
|
-
console.log(chalk6.cyan(" posthorn mailboxes list <domain-id>"));
|
|
597
|
-
console.log(chalk6.cyan(" posthorn warmup stats <campaign-id>"));
|
|
598
|
-
console.log(chalk6.cyan(" posthorn send <mailbox-id> --to user@example.com --subject 'Hi' --body 'Hello!'"));
|
|
599
|
-
console.log(chalk6.cyan(" posthorn auth status"));
|
|
600
|
-
console.log();
|
|
601
510
|
}
|
|
602
511
|
|
|
603
512
|
// src/index.ts
|
|
604
513
|
var program = new Command();
|
|
605
514
|
program.name("posthorn").description("Posthorn \u2014 domain setup, mailbox creation, and email warmup").version("0.1.0");
|
|
606
|
-
program.
|
|
515
|
+
program.addHelpText("after", `
|
|
516
|
+
Agents: run 'posthorn guide' first for the full workflow playbook.
|
|
517
|
+
|
|
518
|
+
Typical flow:
|
|
519
|
+
posthorn guide learn the workflow
|
|
520
|
+
posthorn auth register create an account
|
|
521
|
+
posthorn accounts cloudflare <token> connect Cloudflare
|
|
522
|
+
posthorn accounts workspace <admin-email> connect Google Workspace
|
|
523
|
+
posthorn domains connect <domain> --cloudflare <id>
|
|
524
|
+
posthorn domains get <id> --json poll until status: ready
|
|
525
|
+
posthorn mailboxes create <domain-id> --email you@dom.com --first A --last B
|
|
526
|
+
posthorn auth verify unlock sending + warmup
|
|
527
|
+
posthorn warmup start <mailbox-id>
|
|
528
|
+
|
|
529
|
+
Output: read commands auto-detect \u2014 agents (non-TTY) get JSON, humans get
|
|
530
|
+
formatted text. Force either with --json or --pretty.
|
|
531
|
+
`);
|
|
532
|
+
program.command("guide").description("Print the full agent playbook \u2014 workflow, external steps, and how to drive the CLI. Run this first.").action(guide);
|
|
607
533
|
var auth = program.command("auth").description("Account management");
|
|
608
534
|
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);
|
|
609
|
-
auth.command("status").description("Show current account info").action(status);
|
|
535
|
+
auth.command("status").description("Show current account info and setup progress").option("--json", "Force JSON output").option("--pretty", "Force human-readable output").action(status);
|
|
610
536
|
auth.command("logout").description("Clear stored credentials").action(logout);
|
|
611
537
|
var accounts = program.command("accounts").description("Connected accounts (Cloudflare, Workspace)");
|
|
612
538
|
accounts.command("list").description("List connected accounts").action(listAccounts);
|
|
613
539
|
accounts.command("cloudflare <token>").description("Connect a Cloudflare account").option("--label <label>", "Account label", "main").action(connectCloudflare);
|
|
614
540
|
accounts.command("workspace <admin-email>").description("Connect Google Workspace via domain-wide delegation").action(connectWorkspace);
|
|
615
541
|
var domains = program.command("domains").description("Domain management");
|
|
616
|
-
domains.command("list").description("List all domains").action(listDomains);
|
|
617
|
-
domains.command("get <domain-id>").description("Get domain details").action(getDomain);
|
|
618
|
-
domains.command("check <domains...>").description("Check domain availability").requiredOption("--cloudflare <account-id>", "Cloudflare account ID").action(checkDomains);
|
|
542
|
+
domains.command("list").description("List all domains").option("--json", "Force JSON output").option("--pretty", "Force human-readable output").action(listDomains);
|
|
543
|
+
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);
|
|
544
|
+
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);
|
|
545
|
+
domains.command("reputation <domains...>").description("Vet a domain before buying \u2014 blocklist + reputation + prior-use history").option("--json", "Force JSON output").option("--pretty", "Force human-readable output").action(domainReputation);
|
|
619
546
|
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);
|
|
620
|
-
domains.command("connect <domain>").description("Connect an existing domain
|
|
547
|
+
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);
|
|
548
|
+
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);
|
|
549
|
+
domains.command("activate <domain-id>").description("Check if managed-DNS nameservers propagated, then start DNS setup").action(activateDomain);
|
|
621
550
|
var mailboxes = program.command("mailboxes").description("Mailbox management");
|
|
622
551
|
mailboxes.command("list <domain-id>").description("List mailboxes for a domain").action(listMailboxes);
|
|
623
552
|
mailboxes.command("create <domain-id>").description("Create a new mailbox").requiredOption("--email <email>", "Email address (e.g. john@example.com)").requiredOption("--first <name>", "First name").requiredOption("--last <name>", "Last name").action(createMailbox);
|
|
@@ -627,18 +556,18 @@ var warmup = program.command("warmup").description("Email warmup management");
|
|
|
627
556
|
warmup.command("list").description("List warmup campaigns").action(listCampaigns);
|
|
628
557
|
warmup.command("start <mailbox-id>").description("Start warming a mailbox").action(startWarmup);
|
|
629
558
|
warmup.command("pause <campaign-id>").description("Pause a warmup campaign").action(pauseWarmup);
|
|
630
|
-
warmup.command("stats <campaign-id>").description("Show warmup stats and daily breakdown").action(warmupStats);
|
|
559
|
+
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);
|
|
631
560
|
program.hook("preAction", () => {
|
|
632
561
|
});
|
|
633
562
|
program.parseAsync(process.argv).catch((err) => {
|
|
634
563
|
if (err.statusCode === 401) {
|
|
635
|
-
console.log(
|
|
564
|
+
console.log(chalk6.red("\n Authentication failed. Run: posthorn auth register\n"));
|
|
636
565
|
} else if (err.statusCode === 403) {
|
|
637
|
-
console.log(
|
|
566
|
+
console.log(chalk6.red("\n Account not verified. Run: posthorn auth verify\n"));
|
|
638
567
|
} else if (err.statusCode === 429) {
|
|
639
|
-
console.log(
|
|
568
|
+
console.log(chalk6.yellow("\n Rate limit exceeded. Try again in a minute.\n"));
|
|
640
569
|
} else {
|
|
641
|
-
console.error(
|
|
570
|
+
console.error(chalk6.red(`
|
|
642
571
|
Error: ${err.message}
|
|
643
572
|
`));
|
|
644
573
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "posthorn",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Posthorn — domain setup, mailbox creation, and email warmup from the command line",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"posthorn": "dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"dist"
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md"
|
|
11
12
|
],
|
|
12
13
|
"keywords": [
|
|
13
14
|
"email",
|