posthorn 0.2.4 → 0.2.6

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