posthorn 0.2.3 → 0.2.5
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 +32 -19
- package/dist/index.js +97 -26
- 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
|
|
4
|
-
SPF/DKIM/DMARC, and inbox warmup
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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`.
|
|
@@ -70,7 +70,7 @@ click-by-click instructions.
|
|
|
70
70
|
Zone>Zone>Edit, Zone>DNS>Edit. Resources: Include All.
|
|
71
71
|
- Then: `posthorn accounts cloudflare <token>`
|
|
72
72
|
- For domain PURCHASES, the user also needs a payment method on their Cloudflare
|
|
73
|
-
account (they pay Cloudflare directly
|
|
73
|
+
account (they pay Cloudflare directly; Posthorn never bills for domains).
|
|
74
74
|
|
|
75
75
|
### Google Workspace domain-wide delegation (one-time per Workspace org)
|
|
76
76
|
- admin.google.com → Security → Access and data control → API controls →
|
|
@@ -79,7 +79,7 @@ click-by-click instructions.
|
|
|
79
79
|
- OAuth scopes (paste all, comma-separated):
|
|
80
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
81
|
- Then ask for their admin email and run: `posthorn accounts workspace <admin-email>`
|
|
82
|
-
- This replaces OAuth entirely
|
|
82
|
+
- This replaces OAuth entirely: no consent screen, no app verification, no test users.
|
|
83
83
|
|
|
84
84
|
### Nameservers (only for "managed DNS" or moving a domain to Cloudflare)
|
|
85
85
|
- The CLI prints 2 nameservers. The user sets them at their domain REGISTRAR
|
|
@@ -87,10 +87,10 @@ click-by-click instructions.
|
|
|
87
87
|
- Propagation takes minutes to 24h. Check with `posthorn domains activate <id>`
|
|
88
88
|
(managed) or by polling `posthorn domains get <id>`.
|
|
89
89
|
|
|
90
|
-
### DKIM (per domain
|
|
90
|
+
### DKIM (per domain, Google has no DKIM API)
|
|
91
91
|
If you have browser-automation tools, do it for the user:
|
|
92
92
|
- Navigate to https://admin.google.com/ac/apps/gmail/authenticateemail
|
|
93
|
-
(the URL may need /u/N/ for the right account slot
|
|
93
|
+
(the URL may need /u/N/ for the right account slot, verify the "Selected
|
|
94
94
|
domain" on the page matches).
|
|
95
95
|
- If status is "Authenticating email with DKIM" → already done, skip.
|
|
96
96
|
- Otherwise click GENERATE NEW RECORD → GENERATE (defaults: 2048-bit, "google"
|
|
@@ -106,11 +106,11 @@ PRINCIPLE: domain purchase ALWAYS happens on the user's own Cloudflare account
|
|
|
106
106
|
and card. Posthorn never owns or bills for domains.
|
|
107
107
|
|
|
108
108
|
1. NEW domain → requires the user's own Cloudflare account (+ payment method).
|
|
109
|
-
- `posthorn domains check name1.com name2.io --cloudflare <id
|
|
110
|
-
- `posthorn domains reputation <domain
|
|
109
|
+
- `posthorn domains check name1.com name2.io --cloudflare <id>`, fast availability + price
|
|
110
|
+
- `posthorn domains reputation <domain>`, VET it before buying (Spamhaus
|
|
111
111
|
blocklist/score + archive.org prior-use). Verdicts: clean / caution (used
|
|
112
112
|
before) / avoid (blocklisted or poor reputation). Run this on the candidate
|
|
113
|
-
before `buy
|
|
113
|
+
before `buy`; you can't tell a tainted domain from its name. (Slower + uses
|
|
114
114
|
a quota, so it's a separate explicit call, not part of `check`.)
|
|
115
115
|
- `posthorn domains buy <domain> --cloudflare <id> --contact '{...}'`
|
|
116
116
|
|
|
@@ -124,7 +124,7 @@ and card. Posthorn never owns or bills for domains.
|
|
|
124
124
|
- user sets them at their registrar
|
|
125
125
|
- `posthorn domains activate <domain-id>` → checks propagation, starts DNS setup
|
|
126
126
|
- Best for DEDICATED email-only domains. Don't use on a domain running a live
|
|
127
|
-
website
|
|
127
|
+
website; Posthorn becomes authoritative for ALL its DNS.
|
|
128
128
|
|
|
129
129
|
After any path, poll `posthorn domains get <id>` until status is `ready`. DNS
|
|
130
130
|
records (MX, SPF, DMARC) are configured automatically. Statuses progress:
|
|
@@ -134,7 +134,7 @@ workspace_verifying → workspace_verified → ready.
|
|
|
134
134
|
## Mailboxes & sending
|
|
135
135
|
|
|
136
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
|
|
137
|
+
Returns a one-time password (Google Workspace login). Save it (not shown again).
|
|
138
138
|
- Credentials for IMAP/SMTP are auto-provisioned via the service account
|
|
139
139
|
(domain-wide delegation). Google mailboxes need no per-mailbox setup.
|
|
140
140
|
- Send: `posthorn send <mailbox-id> --to x@y.com --subject "Hi" --body "..."`
|
|
@@ -148,18 +148,31 @@ Anonymous tier can do setup (accounts, domains). To send/warmup, verify:
|
|
|
148
148
|
## Warmup
|
|
149
149
|
|
|
150
150
|
- `posthorn warmup start <mailbox-id>` → joins the shared warmup pool, ramps over ~30 days
|
|
151
|
+
- Volume and reply behavior are platform-managed (ramps from 2/day to 5/day;
|
|
152
|
+
peers reply to ~30% of warmup mail). Not configurable: the pool is shared,
|
|
153
|
+
so settings are tuned for everyone's deliverability.
|
|
151
154
|
- `posthorn warmup stats <campaign-id>` → placement rate, reputation, daily breakdown
|
|
152
155
|
- `posthorn warmup list` → all campaigns
|
|
153
|
-
- Warmup never fully stops on its own
|
|
156
|
+
- Warmup never fully stops on its own, pause with `posthorn warmup pause <id>`.
|
|
157
|
+
|
|
158
|
+
## Feedback (bugs, feature requests)
|
|
159
|
+
|
|
160
|
+
`posthorn feedback --type bug|feature|question|other "<message>"`
|
|
161
|
+
|
|
162
|
+
If you hit a bug, missing capability, or rough edge while driving this CLI,
|
|
163
|
+
report it, it goes straight to the Posthorn team. Agents: you are encouraged
|
|
164
|
+
to file feedback yourself when something doesn't work the way you expected
|
|
165
|
+
(include the command you ran and what you expected). Works on any tier, no
|
|
166
|
+
verification needed.
|
|
154
167
|
|
|
155
168
|
## Tips for agents
|
|
156
169
|
|
|
157
170
|
- Read commands auto-detect output: when stdout isn't a TTY (i.e. you're an agent
|
|
158
|
-
capturing output), they emit JSON automatically
|
|
171
|
+
capturing output), they emit JSON automatically, no flag needed. Humans in a
|
|
159
172
|
terminal get formatted text. Force either way with `--json` / `--pretty`.
|
|
160
173
|
- Async steps (domain provisioning, nameserver propagation) need polling, not blocking.
|
|
161
174
|
- Every command stores state locally (~/.config), so the user doesn't re-enter keys.
|
|
162
175
|
- Translate everything into plain English for the user. Never show them raw JSON
|
|
163
|
-
or ask them to run curl
|
|
176
|
+
or ask them to run curl, you run the CLI on their behalf.
|
|
164
177
|
- When a step needs the user's browser (Cloudflare token, delegation, nameservers,
|
|
165
178
|
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 chalk7 from "chalk";
|
|
6
6
|
|
|
7
7
|
// src/commands/auth.ts
|
|
8
8
|
import chalk from "chalk";
|
|
@@ -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
|
|
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}`);
|
|
@@ -158,6 +158,21 @@ async function api(path, options = {}) {
|
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
// src/commands/accounts.ts
|
|
161
|
+
var SERVICE_ACCOUNT_CLIENT_ID = "110137377718772968374";
|
|
162
|
+
var DELEGATION_SCOPES = "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";
|
|
163
|
+
function printDelegationHelp(adminEmail) {
|
|
164
|
+
const domain = adminEmail.includes("@") ? adminEmail.split("@")[1] : "your-domain.com";
|
|
165
|
+
console.log();
|
|
166
|
+
console.log(chalk2.bold(" Set up domain-wide delegation first (one-time per Workspace org):"));
|
|
167
|
+
console.log(` 1. Go to ${chalk2.cyan("admin.google.com")} signed in as a ${chalk2.bold("super-admin")} of ${chalk2.bold(domain)}`);
|
|
168
|
+
console.log(" 2. Security \u2192 Access and data control \u2192 API controls \u2192 Domain-wide delegation \u2192 Add new");
|
|
169
|
+
console.log(` 3. Client ID: ${chalk2.bold(SERVICE_ACCOUNT_CLIENT_ID)}`);
|
|
170
|
+
console.log(" 4. OAuth scopes (paste all, comma-separated):");
|
|
171
|
+
console.log(` ${chalk2.green(DELEGATION_SCOPES)}`);
|
|
172
|
+
console.log(" 5. Authorize, then re-run this command.");
|
|
173
|
+
console.log(chalk2.dim(` The admin email you pass must be a super-admin of ${domain}'s own org.`));
|
|
174
|
+
console.log();
|
|
175
|
+
}
|
|
161
176
|
async function connectCloudflare(token, options) {
|
|
162
177
|
const spinner = ora2("Connecting Cloudflare...").start();
|
|
163
178
|
const data = await api("/api/accounts", {
|
|
@@ -183,10 +198,21 @@ async function connectCloudflare(token, options) {
|
|
|
183
198
|
}
|
|
184
199
|
async function connectWorkspace(adminEmail) {
|
|
185
200
|
const spinner = ora2("Connecting Google Workspace...").start();
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
201
|
+
let data;
|
|
202
|
+
try {
|
|
203
|
+
data = await api("/api/accounts/workspace", {
|
|
204
|
+
method: "POST",
|
|
205
|
+
body: { adminEmail }
|
|
206
|
+
});
|
|
207
|
+
} catch (err) {
|
|
208
|
+
spinner.fail("Could not connect Google Workspace.");
|
|
209
|
+
if (err instanceof ApiError) {
|
|
210
|
+
console.log(chalk2.red(` ${err.message}`));
|
|
211
|
+
}
|
|
212
|
+
printDelegationHelp(adminEmail);
|
|
213
|
+
process.exitCode = 1;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
190
216
|
setConfig({
|
|
191
217
|
workspaceConnected: true,
|
|
192
218
|
workspaceAccountId: data.account.id
|
|
@@ -312,7 +338,7 @@ async function getDomain(domainId, options = {}) {
|
|
|
312
338
|
function verdictText(rep) {
|
|
313
339
|
switch (rep.verdict) {
|
|
314
340
|
case "avoid":
|
|
315
|
-
return chalk3.red(`\u26A0 ${rep.verdictLabel}
|
|
341
|
+
return chalk3.red(`\u26A0 ${rep.verdictLabel}, avoid`);
|
|
316
342
|
case "caution":
|
|
317
343
|
return chalk3.yellow(rep.verdictLabel);
|
|
318
344
|
case "clean":
|
|
@@ -393,7 +419,7 @@ async function createMailbox(domainId, options) {
|
|
|
393
419
|
console.log(` ${chalk4.bold("Email:")} ${data.credentials.email}`);
|
|
394
420
|
console.log(` ${chalk4.bold("Password:")} ${chalk4.yellow(data.credentials.password)}`);
|
|
395
421
|
console.log();
|
|
396
|
-
console.log(chalk4.red(" Save this password now
|
|
422
|
+
console.log(chalk4.red(" Save this password now, it will not be shown again."));
|
|
397
423
|
console.log(chalk4.dim(" Login at: https://mail.google.com"));
|
|
398
424
|
}
|
|
399
425
|
async function sendEmail(mailboxId, options) {
|
|
@@ -421,7 +447,7 @@ async function provisionCredentials(mailboxId, options) {
|
|
|
421
447
|
body: { authType: options.type ?? "xoauth2" }
|
|
422
448
|
});
|
|
423
449
|
if (data.connectivity.send && data.connectivity.imap) {
|
|
424
|
-
spinner.succeed(`Credentials provisioned
|
|
450
|
+
spinner.succeed(`Credentials provisioned, sending (${data.connectivity.sendMethod}) and IMAP connected!`);
|
|
425
451
|
} else {
|
|
426
452
|
spinner.warn("Credentials provisioned but connectivity issues:");
|
|
427
453
|
for (const err of data.connectivity.errors) {
|
|
@@ -435,14 +461,19 @@ import chalk5 from "chalk";
|
|
|
435
461
|
import ora5 from "ora";
|
|
436
462
|
async function startWarmup(mailboxId) {
|
|
437
463
|
const spinner = ora5("Starting warmup...").start();
|
|
438
|
-
const data = await api(
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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 ?? {};
|
|
442
472
|
spinner.succeed("Warmup started!");
|
|
443
473
|
console.log(` Campaign ID: ${chalk5.dim(data.campaign.id)}`);
|
|
444
474
|
console.log(` Status: ${chalk5.green("active")}`);
|
|
445
|
-
console.log(
|
|
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)}%`);
|
|
446
477
|
}
|
|
447
478
|
async function pauseWarmup(campaignId) {
|
|
448
479
|
await api(`/api/warmup/campaigns/${campaignId}`, {
|
|
@@ -493,25 +524,64 @@ async function listCampaigns() {
|
|
|
493
524
|
}
|
|
494
525
|
}
|
|
495
526
|
|
|
496
|
-
// src/commands/
|
|
527
|
+
// src/commands/feedback.ts
|
|
497
528
|
import { readFileSync } from "fs";
|
|
498
529
|
import { fileURLToPath } from "url";
|
|
499
530
|
import { dirname, join } from "path";
|
|
500
|
-
|
|
531
|
+
import chalk6 from "chalk";
|
|
532
|
+
function cliVersion() {
|
|
501
533
|
try {
|
|
502
534
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
503
|
-
|
|
535
|
+
return JSON.parse(readFileSync(join(here, "..", "package.json"), "utf8")).version;
|
|
536
|
+
} catch {
|
|
537
|
+
return "unknown";
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
async function sendFeedback(messageWords, options) {
|
|
541
|
+
const message = messageWords.join(" ").trim();
|
|
542
|
+
if (!message) {
|
|
543
|
+
console.error(chalk6.red("Feedback message is required."));
|
|
544
|
+
console.error(chalk6.dim(' Example: posthorn feedback --type feature "bulk domain checks would save me round trips"'));
|
|
545
|
+
process.exitCode = 1;
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
const data = await api("/api/feedback", {
|
|
549
|
+
method: "POST",
|
|
550
|
+
body: {
|
|
551
|
+
message,
|
|
552
|
+
type: options.type ?? "other",
|
|
553
|
+
metadata: {
|
|
554
|
+
cliVersion: cliVersion(),
|
|
555
|
+
platform: process.platform,
|
|
556
|
+
agent: !process.stdout.isTTY
|
|
557
|
+
// non-TTY usually means an agent is driving
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
console.log(chalk6.green("Feedback recorded, thank you!"));
|
|
562
|
+
console.log(` id: ${chalk6.dim(data.feedback.id)}`);
|
|
563
|
+
console.log(` type: ${chalk6.dim(data.feedback.type)}`);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// src/commands/guide.ts
|
|
567
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
568
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
569
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
570
|
+
function guide() {
|
|
571
|
+
try {
|
|
572
|
+
const here = dirname2(fileURLToPath2(import.meta.url));
|
|
573
|
+
const readme = readFileSync2(join2(here, "..", "README.md"), "utf8");
|
|
504
574
|
console.log(readme);
|
|
505
575
|
} catch {
|
|
506
576
|
console.log(
|
|
507
|
-
"Posthorn
|
|
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>."
|
|
508
578
|
);
|
|
509
579
|
}
|
|
510
580
|
}
|
|
511
581
|
|
|
512
582
|
// src/index.ts
|
|
513
583
|
var program = new Command();
|
|
514
|
-
program.name("posthorn").description("Posthorn
|
|
584
|
+
program.name("posthorn").description("Posthorn: domain setup, mailbox creation, and email warmup").version("0.2.5");
|
|
515
585
|
program.addHelpText("after", `
|
|
516
586
|
Agents: run 'posthorn guide' first for the full workflow playbook.
|
|
517
587
|
|
|
@@ -526,10 +596,10 @@ Typical flow:
|
|
|
526
596
|
posthorn auth verify unlock sending + warmup
|
|
527
597
|
posthorn warmup start <mailbox-id>
|
|
528
598
|
|
|
529
|
-
Output: read commands auto-detect
|
|
599
|
+
Output: read commands auto-detect. Agents (non-TTY) get JSON, humans get
|
|
530
600
|
formatted text. Force either with --json or --pretty.
|
|
531
601
|
`);
|
|
532
|
-
program.command("guide").description("Print the full agent playbook
|
|
602
|
+
program.command("guide").description("Print the full agent playbook: workflow, external steps, and how to drive the CLI. Run this first.").action(guide);
|
|
533
603
|
var auth = program.command("auth").description("Account management");
|
|
534
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);
|
|
535
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);
|
|
@@ -542,7 +612,7 @@ var domains = program.command("domains").description("Domain management");
|
|
|
542
612
|
domains.command("list").description("List all domains").option("--json", "Force JSON output").option("--pretty", "Force human-readable output").action(listDomains);
|
|
543
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);
|
|
544
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);
|
|
545
|
-
domains.command("reputation <domains...>").description("Vet a domain before buying
|
|
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);
|
|
546
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);
|
|
547
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);
|
|
548
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);
|
|
@@ -557,17 +627,18 @@ warmup.command("list").description("List warmup campaigns").action(listCampaigns
|
|
|
557
627
|
warmup.command("start <mailbox-id>").description("Start warming a mailbox").action(startWarmup);
|
|
558
628
|
warmup.command("pause <campaign-id>").description("Pause a warmup campaign").action(pauseWarmup);
|
|
559
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);
|
|
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);
|
|
560
631
|
program.hook("preAction", () => {
|
|
561
632
|
});
|
|
562
633
|
program.parseAsync(process.argv).catch((err) => {
|
|
563
634
|
if (err.statusCode === 401) {
|
|
564
|
-
console.log(
|
|
635
|
+
console.log(chalk7.red("\n Authentication failed. Run: posthorn auth register\n"));
|
|
565
636
|
} else if (err.statusCode === 403) {
|
|
566
|
-
console.log(
|
|
637
|
+
console.log(chalk7.red("\n Account not verified. Run: posthorn auth verify\n"));
|
|
567
638
|
} else if (err.statusCode === 429) {
|
|
568
|
-
console.log(
|
|
639
|
+
console.log(chalk7.yellow("\n Rate limit exceeded. Try again in a minute.\n"));
|
|
569
640
|
} else {
|
|
570
|
-
console.error(
|
|
641
|
+
console.error(chalk7.red(`
|
|
571
642
|
Error: ${err.message}
|
|
572
643
|
`));
|
|
573
644
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "posthorn",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.5",
|
|
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"
|