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.
Files changed (3) hide show
  1. package/README.md +165 -0
  2. package/dist/index.js +145 -216
  3. 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 chalk7 from "chalk";
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 setup");
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
- spinner.stop();
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 getDomain(domainId) {
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} ${price}`);
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.smtp && data.connectivity.imap) {
338
- spinner.succeed("Credentials provisioned \u2014 SMTP and IMAP connected!");
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/setup.ts
407
- import chalk6 from "chalk";
408
- import ora6 from "ora";
409
- import inquirer from "inquirer";
410
- var STEPS = [
411
- "Account",
412
- "Cloudflare",
413
- "Google Workspace",
414
- "Domain",
415
- "DKIM",
416
- "Mailboxes",
417
- "Warmup"
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.command("setup").description("Guided setup \u2014 walk through the full onboarding flow").option("--url <url>", "API server URL", "https://api-production-08f2.up.railway.app").action(setup);
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 (must be on Cloudflare)").requiredOption("--cloudflare <account-id>", "Cloudflare account ID").option("--workspace <account-id>", "Workspace account ID").action(connectDomain);
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(chalk7.red("\n Authentication failed. Run: posthorn auth register\n"));
564
+ console.log(chalk6.red("\n Authentication failed. Run: posthorn auth register\n"));
636
565
  } else if (err.statusCode === 403) {
637
- console.log(chalk7.red("\n Account not verified. Complete onboarding: posthorn setup\n"));
566
+ console.log(chalk6.red("\n Account not verified. Run: posthorn auth verify\n"));
638
567
  } else if (err.statusCode === 429) {
639
- console.log(chalk7.yellow("\n Rate limit exceeded. Try again in a minute.\n"));
568
+ console.log(chalk6.yellow("\n Rate limit exceeded. Try again in a minute.\n"));
640
569
  } else {
641
- console.error(chalk7.red(`
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.1.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",