posthorn 0.1.0 → 0.2.0
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 +159 -0
- package/dist/index.js +95 -211
- package/package.json +3 -2
package/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
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 <account-id>`
|
|
110
|
+
- `posthorn domains buy <domain> --cloudflare <id> --contact '{...}'`
|
|
111
|
+
|
|
112
|
+
2. EXISTING domain, recommended → user's own Cloudflare account.
|
|
113
|
+
- `posthorn domains connect <domain> --cloudflare <account-id>`
|
|
114
|
+
- (If the domain isn't on Cloudflare yet, the user adds it at dash.cloudflare.com
|
|
115
|
+
and points nameservers to their account first.)
|
|
116
|
+
|
|
117
|
+
3. EXISTING domain, optional → Posthorn-managed DNS (no Cloudflare account needed).
|
|
118
|
+
- `posthorn domains managed <domain>` → prints 2 nameservers
|
|
119
|
+
- user sets them at their registrar
|
|
120
|
+
- `posthorn domains activate <domain-id>` → checks propagation, starts DNS setup
|
|
121
|
+
- Best for DEDICATED email-only domains. Don't use on a domain running a live
|
|
122
|
+
website — Posthorn becomes authoritative for ALL its DNS.
|
|
123
|
+
|
|
124
|
+
After any path, poll `posthorn domains get <id>` until status is `ready`. DNS
|
|
125
|
+
records (MX, SPF, DMARC) are configured automatically. Statuses progress:
|
|
126
|
+
pending_* → purchased → dns_configuring → dns_configured → dns_verified →
|
|
127
|
+
workspace_verifying → workspace_verified → ready.
|
|
128
|
+
|
|
129
|
+
## Mailboxes & sending
|
|
130
|
+
|
|
131
|
+
- Create: `posthorn mailboxes create <domain-id> --email john@dom.com --first John --last Smith`
|
|
132
|
+
Returns a one-time password (Google Workspace login). Save it — not shown again.
|
|
133
|
+
- Credentials for IMAP/SMTP are auto-provisioned via the service account
|
|
134
|
+
(domain-wide delegation). Google mailboxes need no per-mailbox setup.
|
|
135
|
+
- Send: `posthorn send <mailbox-id> --to x@y.com --subject "Hi" --body "..."`
|
|
136
|
+
(requires "verified" tier)
|
|
137
|
+
|
|
138
|
+
## Account verification (unlocks sending/warmup)
|
|
139
|
+
|
|
140
|
+
Anonymous tier can do setup (accounts, domains). To send/warmup, verify:
|
|
141
|
+
`posthorn auth verify` → choose admin email or a custom email → confirm the code.
|
|
142
|
+
|
|
143
|
+
## Warmup
|
|
144
|
+
|
|
145
|
+
- `posthorn warmup start <mailbox-id>` → joins the shared warmup pool, ramps over ~30 days
|
|
146
|
+
- `posthorn warmup stats <campaign-id>` → placement rate, reputation, daily breakdown
|
|
147
|
+
- `posthorn warmup list` → all campaigns
|
|
148
|
+
- Warmup never fully stops on its own — pause with `posthorn warmup pause <id>`.
|
|
149
|
+
|
|
150
|
+
## Tips for agents
|
|
151
|
+
|
|
152
|
+
- Use `--json` on read commands (auth status, domains list/get, warmup stats) for
|
|
153
|
+
machine-readable output instead of parsing the pretty text.
|
|
154
|
+
- Async steps (domain provisioning, nameserver propagation) need polling, not blocking.
|
|
155
|
+
- Every command stores state locally (~/.config), so the user doesn't re-enter keys.
|
|
156
|
+
- Translate everything into plain English for the user. Never show them raw JSON
|
|
157
|
+
or ask them to run curl — you run the CLI on their behalf.
|
|
158
|
+
- When a step needs the user's browser (Cloudflare token, delegation, nameservers,
|
|
159
|
+
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";
|
|
@@ -78,8 +78,19 @@ async function register(options) {
|
|
|
78
78
|
console.log(chalk.dim(` Is the server running? Try: posthorn auth register --url <your-api-url>`));
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
|
-
async function status() {
|
|
81
|
+
async function status(options = {}) {
|
|
82
82
|
const config = getConfig();
|
|
83
|
+
if (options.json) {
|
|
84
|
+
console.log(JSON.stringify({
|
|
85
|
+
loggedIn: !!config.apiKey,
|
|
86
|
+
apiUrl: config.apiUrl,
|
|
87
|
+
userId: config.userId,
|
|
88
|
+
cloudflareConnected: config.cloudflareConnected,
|
|
89
|
+
workspaceConnected: config.workspaceConnected,
|
|
90
|
+
setupStep: config.setupStep
|
|
91
|
+
}, null, 2));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
83
94
|
if (!config.apiKey) {
|
|
84
95
|
console.log(chalk.yellow("Not logged in."));
|
|
85
96
|
console.log(` Run: ${chalk.cyan("posthorn auth register")}`);
|
|
@@ -193,10 +204,12 @@ async function listAccounts() {
|
|
|
193
204
|
// src/commands/domains.ts
|
|
194
205
|
import chalk3 from "chalk";
|
|
195
206
|
import ora3 from "ora";
|
|
196
|
-
async function listDomains() {
|
|
197
|
-
const spinner = ora3("Fetching domains...").start();
|
|
207
|
+
async function listDomains(options = {}) {
|
|
198
208
|
const data = await api("/api/domains");
|
|
199
|
-
|
|
209
|
+
if (options.json) {
|
|
210
|
+
console.log(JSON.stringify(data.domains, null, 2));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
200
213
|
if (data.domains.length === 0) {
|
|
201
214
|
console.log(chalk3.dim(" No domains yet. Run: posthorn domains buy <domain>"));
|
|
202
215
|
return;
|
|
@@ -250,13 +263,43 @@ async function connectDomain(domain, options) {
|
|
|
250
263
|
spinner.succeed(data.message);
|
|
251
264
|
console.log(` Domain ID: ${chalk3.dim(data.domain.id)}`);
|
|
252
265
|
}
|
|
253
|
-
async function
|
|
266
|
+
async function managedDomain(domain, options) {
|
|
267
|
+
const spinner = ora3(`Adding ${domain} to managed DNS...`).start();
|
|
268
|
+
const body = { name: domain };
|
|
269
|
+
if (options.workspace) body.workspaceAccountId = options.workspace;
|
|
270
|
+
const data = await api("/api/domains/managed", { method: "POST", body });
|
|
271
|
+
spinner.succeed("Domain added to managed DNS!");
|
|
272
|
+
console.log();
|
|
273
|
+
console.log(" Set these 2 nameservers at your registrar:");
|
|
274
|
+
for (const ns of data.nameServers) {
|
|
275
|
+
console.log(` ${chalk3.cyan(ns)}`);
|
|
276
|
+
}
|
|
277
|
+
console.log();
|
|
278
|
+
console.log(chalk3.dim(" Once changed (takes minutes to 24h), run:"));
|
|
279
|
+
console.log(chalk3.cyan(` posthorn domains activate ${data.domain.id}`));
|
|
280
|
+
}
|
|
281
|
+
async function activateDomain(domainId) {
|
|
282
|
+
const spinner = ora3("Checking nameserver propagation...").start();
|
|
283
|
+
const data = await api(`/api/domains/${domainId}/activate`, { method: "POST" });
|
|
284
|
+
if (data.active) {
|
|
285
|
+
spinner.succeed(data.message);
|
|
286
|
+
} else {
|
|
287
|
+
spinner.warn(data.message);
|
|
288
|
+
console.log(chalk3.dim(` Cloudflare status: ${data.cloudflareStatus ?? "pending"}`));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
async function getDomain(domainId, options = {}) {
|
|
254
292
|
const data = await api(`/api/domains/${domainId}`);
|
|
255
293
|
const d = data.domain;
|
|
294
|
+
if (options.json) {
|
|
295
|
+
console.log(JSON.stringify(d, null, 2));
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
256
298
|
console.log(chalk3.bold("Domain\n"));
|
|
257
299
|
console.log(` Name: ${chalk3.cyan(d.name)}`);
|
|
258
300
|
console.log(` Status: ${d.status === "ready" ? chalk3.green(d.status) : chalk3.yellow(d.status)}`);
|
|
259
301
|
console.log(` ID: ${chalk3.dim(d.id)}`);
|
|
302
|
+
console.log(` DNS: ${d.dns_mode === "managed" ? "managed by Posthorn" : "your Cloudflare"}`);
|
|
260
303
|
if (d.cloudflare_zone_id) console.log(` Zone: ${chalk3.dim(d.cloudflare_zone_id)}`);
|
|
261
304
|
}
|
|
262
305
|
async function checkDomains(domains2, options) {
|
|
@@ -365,8 +408,12 @@ async function pauseWarmup(campaignId) {
|
|
|
365
408
|
});
|
|
366
409
|
console.log(chalk5.yellow("Warmup paused."));
|
|
367
410
|
}
|
|
368
|
-
async function warmupStats(campaignId) {
|
|
411
|
+
async function warmupStats(campaignId, options = {}) {
|
|
369
412
|
const data = await api(`/api/warmup/campaigns/${campaignId}`);
|
|
413
|
+
if (options.json) {
|
|
414
|
+
console.log(JSON.stringify(data, null, 2));
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
370
417
|
const s = data.stats;
|
|
371
418
|
const c = data.campaign;
|
|
372
419
|
console.log(chalk5.bold("Warmup Stats\n"));
|
|
@@ -403,221 +450,58 @@ async function listCampaigns() {
|
|
|
403
450
|
}
|
|
404
451
|
}
|
|
405
452
|
|
|
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}`);
|
|
425
|
-
}
|
|
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;
|
|
453
|
+
// src/commands/guide.ts
|
|
454
|
+
import { readFileSync } from "fs";
|
|
455
|
+
import { fileURLToPath } from "url";
|
|
456
|
+
import { dirname, join } from "path";
|
|
457
|
+
function guide() {
|
|
458
|
+
try {
|
|
459
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
460
|
+
const readme = readFileSync(join(here, "..", "README.md"), "utf8");
|
|
461
|
+
console.log(readme);
|
|
462
|
+
} catch {
|
|
463
|
+
console.log(
|
|
464
|
+
"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>."
|
|
465
|
+
);
|
|
591
466
|
}
|
|
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
467
|
}
|
|
602
468
|
|
|
603
469
|
// src/index.ts
|
|
604
470
|
var program = new Command();
|
|
605
471
|
program.name("posthorn").description("Posthorn \u2014 domain setup, mailbox creation, and email warmup").version("0.1.0");
|
|
606
|
-
program.
|
|
472
|
+
program.addHelpText("after", `
|
|
473
|
+
Agents: run 'posthorn guide' first for the full workflow playbook.
|
|
474
|
+
|
|
475
|
+
Typical flow:
|
|
476
|
+
posthorn guide learn the workflow
|
|
477
|
+
posthorn auth register create an account
|
|
478
|
+
posthorn accounts cloudflare <token> connect Cloudflare
|
|
479
|
+
posthorn accounts workspace <admin-email> connect Google Workspace
|
|
480
|
+
posthorn domains connect <domain> --cloudflare <id>
|
|
481
|
+
posthorn domains get <id> --json poll until status: ready
|
|
482
|
+
posthorn mailboxes create <domain-id> --email you@dom.com --first A --last B
|
|
483
|
+
posthorn auth verify unlock sending + warmup
|
|
484
|
+
posthorn warmup start <mailbox-id>
|
|
485
|
+
|
|
486
|
+
Most read commands support --json for machine-readable output.
|
|
487
|
+
`);
|
|
488
|
+
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
489
|
var auth = program.command("auth").description("Account management");
|
|
608
490
|
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);
|
|
491
|
+
auth.command("status").description("Show current account info and setup progress").option("--json", "Output as JSON").action(status);
|
|
610
492
|
auth.command("logout").description("Clear stored credentials").action(logout);
|
|
611
493
|
var accounts = program.command("accounts").description("Connected accounts (Cloudflare, Workspace)");
|
|
612
494
|
accounts.command("list").description("List connected accounts").action(listAccounts);
|
|
613
495
|
accounts.command("cloudflare <token>").description("Connect a Cloudflare account").option("--label <label>", "Account label", "main").action(connectCloudflare);
|
|
614
496
|
accounts.command("workspace <admin-email>").description("Connect Google Workspace via domain-wide delegation").action(connectWorkspace);
|
|
615
497
|
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);
|
|
498
|
+
domains.command("list").description("List all domains").option("--json", "Output as JSON").action(listDomains);
|
|
499
|
+
domains.command("get <domain-id>").description("Get domain details (poll this until status is 'ready')").option("--json", "Output as JSON").action(getDomain);
|
|
618
500
|
domains.command("check <domains...>").description("Check domain availability").requiredOption("--cloudflare <account-id>", "Cloudflare account ID").action(checkDomains);
|
|
619
501
|
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
|
|
502
|
+
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);
|
|
503
|
+
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);
|
|
504
|
+
domains.command("activate <domain-id>").description("Check if managed-DNS nameservers propagated, then start DNS setup").action(activateDomain);
|
|
621
505
|
var mailboxes = program.command("mailboxes").description("Mailbox management");
|
|
622
506
|
mailboxes.command("list <domain-id>").description("List mailboxes for a domain").action(listMailboxes);
|
|
623
507
|
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 +511,18 @@ var warmup = program.command("warmup").description("Email warmup management");
|
|
|
627
511
|
warmup.command("list").description("List warmup campaigns").action(listCampaigns);
|
|
628
512
|
warmup.command("start <mailbox-id>").description("Start warming a mailbox").action(startWarmup);
|
|
629
513
|
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);
|
|
514
|
+
warmup.command("stats <campaign-id>").description("Show warmup stats and daily breakdown").option("--json", "Output as JSON").action(warmupStats);
|
|
631
515
|
program.hook("preAction", () => {
|
|
632
516
|
});
|
|
633
517
|
program.parseAsync(process.argv).catch((err) => {
|
|
634
518
|
if (err.statusCode === 401) {
|
|
635
|
-
console.log(
|
|
519
|
+
console.log(chalk6.red("\n Authentication failed. Run: posthorn auth register\n"));
|
|
636
520
|
} else if (err.statusCode === 403) {
|
|
637
|
-
console.log(
|
|
521
|
+
console.log(chalk6.red("\n Account not verified. Complete onboarding: posthorn setup\n"));
|
|
638
522
|
} else if (err.statusCode === 429) {
|
|
639
|
-
console.log(
|
|
523
|
+
console.log(chalk6.yellow("\n Rate limit exceeded. Try again in a minute.\n"));
|
|
640
524
|
} else {
|
|
641
|
-
console.error(
|
|
525
|
+
console.error(chalk6.red(`
|
|
642
526
|
Error: ${err.message}
|
|
643
527
|
`));
|
|
644
528
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "posthorn",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|