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.
Files changed (3) hide show
  1. package/README.md +159 -0
  2. package/dist/index.js +95 -211
  3. 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 chalk7 from "chalk";
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
- spinner.stop();
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 getDomain(domainId) {
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/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}`);
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.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);
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 (must be on Cloudflare)").requiredOption("--cloudflare <account-id>", "Cloudflare account ID").option("--workspace <account-id>", "Workspace account ID").action(connectDomain);
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(chalk7.red("\n Authentication failed. Run: posthorn auth register\n"));
519
+ console.log(chalk6.red("\n Authentication failed. Run: posthorn auth register\n"));
636
520
  } else if (err.statusCode === 403) {
637
- console.log(chalk7.red("\n Account not verified. Complete onboarding: posthorn setup\n"));
521
+ console.log(chalk6.red("\n Account not verified. Complete onboarding: posthorn setup\n"));
638
522
  } else if (err.statusCode === 429) {
639
- console.log(chalk7.yellow("\n Rate limit exceeded. Try again in a minute.\n"));
523
+ console.log(chalk6.yellow("\n Rate limit exceeded. Try again in a minute.\n"));
640
524
  } else {
641
- console.error(chalk7.red(`
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.1.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",