posthorn 0.2.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 +9 -3
  2. package/dist/index.js +60 -15
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -106,7 +106,12 @@ 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 <account-id>`
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`.)
110
115
  - `posthorn domains buy <domain> --cloudflare <id> --contact '{...}'`
111
116
 
112
117
  2. EXISTING domain, recommended → user's own Cloudflare account.
@@ -149,8 +154,9 @@ Anonymous tier can do setup (accounts, domains). To send/warmup, verify:
149
154
 
150
155
  ## Tips for agents
151
156
 
152
- - Use `--json` on read commands (auth status, domains list/get, warmup stats) for
153
- machine-readable output instead of parsing the pretty text.
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`.
154
160
  - Async steps (domain provisioning, nameserver propagation) need polling, not blocking.
155
161
  - Every command stores state locally (~/.config), so the user doesn't re-enter keys.
156
162
  - Translate everything into plain English for the user. Never show them raw JSON
package/dist/index.js CHANGED
@@ -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";
@@ -80,7 +87,7 @@ async function register(options) {
80
87
  }
81
88
  async function status(options = {}) {
82
89
  const config = getConfig();
83
- if (options.json) {
90
+ if (useJson(options)) {
84
91
  console.log(JSON.stringify({
85
92
  loggedIn: !!config.apiKey,
86
93
  apiUrl: config.apiUrl,
@@ -128,7 +135,7 @@ var ApiError = class extends Error {
128
135
  async function api(path, options = {}) {
129
136
  const config = getConfig();
130
137
  if (!config.apiKey) {
131
- throw new Error("Not logged in. Run: posthorn setup");
138
+ throw new Error("Not logged in. Run: posthorn auth register");
132
139
  }
133
140
  const url = `${config.apiUrl}${path}`;
134
141
  const res = await fetch(url, {
@@ -206,7 +213,7 @@ import chalk3 from "chalk";
206
213
  import ora3 from "ora";
207
214
  async function listDomains(options = {}) {
208
215
  const data = await api("/api/domains");
209
- if (options.json) {
216
+ if (useJson(options)) {
210
217
  console.log(JSON.stringify(data.domains, null, 2));
211
218
  return;
212
219
  }
@@ -291,7 +298,7 @@ async function activateDomain(domainId) {
291
298
  async function getDomain(domainId, options = {}) {
292
299
  const data = await api(`/api/domains/${domainId}`);
293
300
  const d = data.domain;
294
- if (options.json) {
301
+ if (useJson(options)) {
295
302
  console.log(JSON.stringify(d, null, 2));
296
303
  return;
297
304
  }
@@ -302,6 +309,18 @@ async function getDomain(domainId, options = {}) {
302
309
  console.log(` DNS: ${d.dns_mode === "managed" ? "managed by Posthorn" : "your Cloudflare"}`);
303
310
  if (d.cloudflare_zone_id) console.log(` Zone: ${chalk3.dim(d.cloudflare_zone_id)}`);
304
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
+ }
305
324
  async function checkDomains(domains2, options) {
306
325
  const spinner = ora3("Checking availability...").start();
307
326
  const data = await api("/api/domains/check", {
@@ -309,11 +328,35 @@ async function checkDomains(domains2, options) {
309
328
  body: { domains: domains2, cloudflareAccountId: options.cloudflare }
310
329
  });
311
330
  spinner.stop();
331
+ if (useJson(options)) {
332
+ console.log(JSON.stringify(data.results, null, 2));
333
+ return;
334
+ }
312
335
  console.log(chalk3.bold("Domain Availability\n"));
313
336
  for (const r of data.results) {
314
337
  const icon = r.available ? chalk3.green(" \u2713") : chalk3.red(" \u2717");
315
338
  const price = r.price ? chalk3.dim(`$${r.price}/yr`) : "";
316
- 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
+ }
317
360
  }
318
361
  }
319
362
 
@@ -377,8 +420,8 @@ async function provisionCredentials(mailboxId, options) {
377
420
  method: "POST",
378
421
  body: { authType: options.type ?? "xoauth2" }
379
422
  });
380
- if (data.connectivity.smtp && data.connectivity.imap) {
381
- 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!`);
382
425
  } else {
383
426
  spinner.warn("Credentials provisioned but connectivity issues:");
384
427
  for (const err of data.connectivity.errors) {
@@ -410,7 +453,7 @@ async function pauseWarmup(campaignId) {
410
453
  }
411
454
  async function warmupStats(campaignId, options = {}) {
412
455
  const data = await api(`/api/warmup/campaigns/${campaignId}`);
413
- if (options.json) {
456
+ if (useJson(options)) {
414
457
  console.log(JSON.stringify(data, null, 2));
415
458
  return;
416
459
  }
@@ -483,21 +526,23 @@ Typical flow:
483
526
  posthorn auth verify unlock sending + warmup
484
527
  posthorn warmup start <mailbox-id>
485
528
 
486
- Most read commands support --json for machine-readable output.
529
+ Output: read commands auto-detect \u2014 agents (non-TTY) get JSON, humans get
530
+ formatted text. Force either with --json or --pretty.
487
531
  `);
488
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);
489
533
  var auth = program.command("auth").description("Account management");
490
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);
491
- auth.command("status").description("Show current account info and setup progress").option("--json", "Output as JSON").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);
492
536
  auth.command("logout").description("Clear stored credentials").action(logout);
493
537
  var accounts = program.command("accounts").description("Connected accounts (Cloudflare, Workspace)");
494
538
  accounts.command("list").description("List connected accounts").action(listAccounts);
495
539
  accounts.command("cloudflare <token>").description("Connect a Cloudflare account").option("--label <label>", "Account label", "main").action(connectCloudflare);
496
540
  accounts.command("workspace <admin-email>").description("Connect Google Workspace via domain-wide delegation").action(connectWorkspace);
497
541
  var domains = program.command("domains").description("Domain management");
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);
500
- 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);
501
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);
502
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);
503
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);
@@ -511,14 +556,14 @@ var warmup = program.command("warmup").description("Email warmup management");
511
556
  warmup.command("list").description("List warmup campaigns").action(listCampaigns);
512
557
  warmup.command("start <mailbox-id>").description("Start warming a mailbox").action(startWarmup);
513
558
  warmup.command("pause <campaign-id>").description("Pause a warmup campaign").action(pauseWarmup);
514
- warmup.command("stats <campaign-id>").description("Show warmup stats and daily breakdown").option("--json", "Output as JSON").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);
515
560
  program.hook("preAction", () => {
516
561
  });
517
562
  program.parseAsync(process.argv).catch((err) => {
518
563
  if (err.statusCode === 401) {
519
564
  console.log(chalk6.red("\n Authentication failed. Run: posthorn auth register\n"));
520
565
  } else if (err.statusCode === 403) {
521
- console.log(chalk6.red("\n Account not verified. Complete onboarding: posthorn setup\n"));
566
+ console.log(chalk6.red("\n Account not verified. Run: posthorn auth verify\n"));
522
567
  } else if (err.statusCode === 429) {
523
568
  console.log(chalk6.yellow("\n Rate limit exceeded. Try again in a minute.\n"));
524
569
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "posthorn",
3
- "version": "0.2.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": {