@letterstory/cli 0.177.12 → 0.177.14

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/lib/cli.mjs CHANGED
@@ -88,7 +88,7 @@ import {
88
88
  import { fetchParkedSections, withoutParked } from "./parked.mjs";
89
89
 
90
90
  // Matches cli/package.json; publish-cli.yml rewrites both to the published version.
91
- export const VERSION = "0.177.12";
91
+ export const VERSION = "0.177.14";
92
92
 
93
93
  /** The exit code for a gate refusal (CARD_REQUIRED, PAYMENT_FAILED, OUT_OF_TOKENS): 1 is a plain error. */
94
94
  export const EXIT_PAYMENT_REQUIRED = 3;
@@ -37,7 +37,7 @@ export const ARGS = {
37
37
  };
38
38
 
39
39
  /** A row's state in words, as its badge says it; an older server's raw status said the same way. */
40
- const STATUS_WORDS = { ready: "ready", blocked: "waiting", running: "running", done: "done" };
40
+ const STATUS_WORDS = { ready: "Ready", blocked: "Waiting", running: "Running", done: "Done" };
41
41
 
42
42
  /** Pure: the lines one item prints as, numbered `n` (1-based), indented under its title. */
43
43
  export function ideaLines(item, n) {
@@ -46,6 +46,17 @@ const ARGS = {
46
46
  ask: "ask",
47
47
  // --confirms <build id>: the person's yes to the price a build stopped on.
48
48
  confirms: "confirms",
49
+ // --purchase '{"tool":"register_domain","args":{"domain":"olapdb.com"}}': priced and held for a yes, never bought.
50
+ purchase: {
51
+ value: (flags) => {
52
+ if (typeof flags.purchase !== "string") return undefined;
53
+ try {
54
+ return JSON.parse(flags.purchase);
55
+ } catch {
56
+ throw new CliError("--purchase takes JSON: {tool, args}.");
57
+ }
58
+ },
59
+ },
49
60
  // --build-it: the words are a request to build, never read as a question.
50
61
  asked_to_build: { flag: "build-it", type: "bool" },
51
62
  domain: "site",
@@ -12,7 +12,7 @@
12
12
  // letterstory deploy page <id>
13
13
 
14
14
  import { CliError } from "../client.mjs";
15
- import { argsFromFlags, flagStr, printResult, refuseUnknownFlags } from "./shared.mjs";
15
+ import { argsFromFlags, domainPrice, flagStr, printResult, refuseUnknownFlags } from "./shared.mjs";
16
16
 
17
17
  const appId = { positional: 0, label: "app-id", required: true };
18
18
 
@@ -46,6 +46,8 @@ export const ARGS = {
46
46
  writing: { flag: "writing", enum: ["on"] },
47
47
  // --no-spend: with --domain, buy nothing; the domain is one you hold, or one registered elsewhere.
48
48
  no_spend: { flag: "no-spend", type: "bool" },
49
+ // A new domain's price: read with register_domain's dry run on this key and echoed with --yes (deploy create's way).
50
+ accepted_quote: { internal: "the new domain's price read by the command (domainPrice), echoed with --yes" },
49
51
  },
50
52
  advance_deployment: { id: appId, retry: { flag: "retry", type: "bool" } },
51
53
  add_deployment_page: { id: appId },
@@ -70,19 +72,49 @@ export async function cmdDeploy(ctx) {
70
72
  case "ls":
71
73
  return deployList(rest);
72
74
  case "create": {
73
- refuseUnknownFlags(rest, ARGS.create_deployment, ["prompt"]);
75
+ refuseUnknownFlags(rest, ARGS.create_deployment, ["prompt", "yes"]);
74
76
  const args = argsFromFlags(ARGS.create_deployment, rest.flags, rest.positionals);
75
77
  if (!args.prompt)
76
78
  throw new CliError(
77
79
  'Say what to build: letterstory deploy create "a website for HVAC contractors to collect leads"'
78
80
  );
81
+ // Its domain is bought only at the price Letter shows this key, with --yes, echoed (domains buy's own way).
82
+ if (args.domain && !args.no_spend && args.domain_quote === undefined) {
83
+ const priced = await domainPrice(rest.client, rest.io, args.domain);
84
+ if (priced?.held) args.no_spend = true;
85
+ else if (priced?.quote) {
86
+ if (rest.flags.yes !== true) {
87
+ rest.io.error(
88
+ "Building it buys this domain (real money). Run again with --yes to buy it at that price."
89
+ );
90
+ return 1;
91
+ }
92
+ args.domain_quote = priced.quote;
93
+ }
94
+ }
79
95
  return deployShow(rest, await rest.client.callTool("create_deployment", args));
80
96
  }
81
97
  case "show":
82
98
  return deployShow(rest, await call(rest, "get_deployment"));
83
- case "update":
84
- refuseUnknownFlags(rest, ARGS.update_deployment, []);
85
- return deployShow(rest, await call(rest, "update_deployment"));
99
+ case "update": {
100
+ refuseUnknownFlags(rest, ARGS.update_deployment, ["yes"]);
101
+ const args = argsFromFlags(ARGS.update_deployment, rest.flags, rest.positionals);
102
+ // A new domain is bought only at the price Letter shows this key, with --yes, echoed (deploy create's way).
103
+ if (args.domain && !args.no_spend) {
104
+ const priced = await domainPrice(rest.client, rest.io, args.domain);
105
+ if (priced?.held) args.no_spend = true;
106
+ else if (priced?.quote) {
107
+ if (rest.flags.yes !== true) {
108
+ rest.io.error(
109
+ "Its new domain is bought (real money). Run again with --yes to buy it at that price."
110
+ );
111
+ return 1;
112
+ }
113
+ args.accepted_quote = priced.quote;
114
+ }
115
+ }
116
+ return deployShow(rest, await rest.client.callTool("update_deployment", args));
117
+ }
86
118
  case "pause":
87
119
  case "resume":
88
120
  case "archive":
@@ -16,7 +16,7 @@
16
16
  // plan and puts it live (spends money; domains:write)
17
17
 
18
18
  import { CliError } from "../client.mjs";
19
- import { flagCsv, requirePositional, printResult, argsFromFlags } from "./shared.mjs";
19
+ import { flagCsv, requirePositional, printResult, argsFromFlags, domainPrice } from "./shared.mjs";
20
20
 
21
21
  export const ARGS = {
22
22
  list_domains: {},
@@ -152,10 +152,16 @@ async function domainsList(ctx) {
152
152
 
153
153
  async function domainsBuy(ctx) {
154
154
  const { client, flags, io } = ctx;
155
- const result = await client.callTool(
156
- "register_domain",
157
- argsFromFlags(ARGS.register_domain, flags, ctx.positionals)
158
- );
155
+ const args = argsFromFlags(ARGS.register_domain, flags, ctx.positionals);
156
+ // A purchase goes only at a price Letter issued to this key (Mahir, 2026-11-11): --yes reads it first and buys at
157
+ // that price, echoing its quote, unless one was given (--accepted-quote).
158
+ if (flags.yes === true && !args.dry_run && args.accepted_quote === undefined && !args.no_spend) {
159
+ const priced = await domainPrice(client, io, args.domain);
160
+ // One the org holds is made its own with nothing bought; anything else at its price.
161
+ if (priced?.held) args.no_spend = true;
162
+ else if (priced?.quote) args.accepted_quote = priced.quote;
163
+ }
164
+ const result = await client.callTool("register_domain", args);
159
165
  printResult(io, flags, result, (r) => {
160
166
  const p = r.purchase;
161
167
  const on = p?.on === "org_vercel_key" ? "on your own Vercel account" : "by Letter, billed in tokens";
@@ -214,10 +220,13 @@ async function domainsPlan(ctx) {
214
220
 
215
221
  async function domainsLaunch(ctx) {
216
222
  const { client, flags, io } = ctx;
217
- const result = await client.callTool(
218
- "launch_domain_site",
219
- argsFromFlags(ARGS.launch_domain_site, flags, ctx.positionals)
220
- );
223
+ const args = argsFromFlags(ARGS.launch_domain_site, flags, ctx.positionals);
224
+ // --yes buys its domain at the price Letter shows this key, echoed (domains buy's own way): never above it.
225
+ if (args.accepted_quote === undefined) {
226
+ const priced = await domainPrice(client, io, args.domain);
227
+ if (priced?.quote) args.accepted_quote = priced.quote;
228
+ }
229
+ const result = await client.callTool("launch_domain_site", args);
221
230
  printResult(io, flags, result, (r) =>
222
231
  [
223
232
  ...(r.plan ? planLines(r.plan) : []),
@@ -61,6 +61,7 @@ import {
61
61
  pollUntil,
62
62
  readTextInput,
63
63
  argsFromFlags,
64
+ domainPrice,
64
65
  } from "./shared.mjs";
65
66
 
66
67
  const COVER_STYLES = ["editorial_image", "full_ai", "line_art"];
@@ -120,6 +121,8 @@ export const ARGS = {
120
121
  is_test: { flag: "test", type: "bool" },
121
122
  readiness_minutes: { flag: "readiness-minutes", type: "num" },
122
123
  client_email: "client-email",
124
+ // A buy_now phantom's domain price: read with register_domain's dry run on this key and echoed with --yes.
125
+ accepted_quote: { internal: "the phantom domain's price read by the command (domainPrice), echoed with --yes" },
123
126
  },
124
127
  get_domain_onboard_status: {
125
128
  job_id: "attach",
@@ -134,6 +137,8 @@ export const ARGS = {
134
137
  // flag mapping straight through: it comes from `--accept-plan <path|->` /
135
138
  // `--accept-plan-json`, or from the dry run this same command just printed.
136
139
  plan: { internal: "the reviewed plan — from --accept-plan, or the dry run just printed" },
140
+ // A buy_now launch's domain price: read with register_domain's dry run on this key and echoed with --yes.
141
+ accepted_quote: { internal: "the domain's price read by the command (domainPrice), echoed with --yes" },
137
142
  overrides: {
138
143
  object: {
139
144
  name: "name",
@@ -654,6 +659,19 @@ async function cmdOnboardDomain(ctx) {
654
659
  }
655
660
 
656
661
  const args = argsFromFlags(ARGS.onboard_domain, flags, ctx.positionals);
662
+ // Its phantom's domain is bought only at the price Letter shows this key, with --yes, echoed (domains buy's way).
663
+ if (args.phantom?.domain_mode === "buy_now" && args.phantom.site_domain) {
664
+ const priced = await domainPrice(client, ctx.io, args.phantom.site_domain);
665
+ if (priced?.quote) {
666
+ if (!flagBool(flags.yes)) {
667
+ ctx.io.error(
668
+ "Onboarding buys the phantom's domain (real money). Run again with --yes to buy it at that price."
669
+ );
670
+ return 1;
671
+ }
672
+ args.accepted_quote = priced.quote;
673
+ }
674
+ }
657
675
 
658
676
  ok(ctx, "Starting domain onboarding…");
659
677
  const started = await client.callTool("onboard_domain", args);
@@ -889,6 +907,19 @@ export async function cmdOnboard(ctx) {
889
907
  reviewedPlan = dryRun.plan;
890
908
  }
891
909
 
910
+ // A launch that buys its domain goes only at the price Letter shows this key, with --yes, echoed (domains buy's way).
911
+ let accepted_quote;
912
+ if (overrides.domain_mode === "buy_now" && overrides.site_domain) {
913
+ const priced = await domainPrice(client, io, overrides.site_domain);
914
+ if (priced?.quote) {
915
+ if (!flagBool(flags.yes)) {
916
+ io.error("Launching it buys this domain (real money). Run again with --yes to buy it at that price.");
917
+ return 1;
918
+ }
919
+ accepted_quote = priced.quote;
920
+ }
921
+ }
922
+
892
923
  // Step 3: apply — start the durable onboarding job.
893
924
  ok(ctx, "Applying — starting the onboarding job…");
894
925
  const started = await client.callTool(
@@ -898,6 +929,7 @@ export async function cmdOnboard(ctx) {
898
929
  accept: true,
899
930
  overrides: Object.keys(overrides).length ? overrides : undefined,
900
931
  plan: reviewedPlan,
932
+ accepted_quote,
901
933
  })
902
934
  );
903
935
  const jobId = started.job_id;
@@ -8,7 +8,16 @@
8
8
 
9
9
  import { readFileSync } from "node:fs";
10
10
  import { CliError } from "../client.mjs";
11
- import { flagStr, requirePositional, printResult, ok, toolField, followJob, argsFromFlags } from "./shared.mjs";
11
+ import {
12
+ flagStr,
13
+ requirePositional,
14
+ printResult,
15
+ ok,
16
+ toolField,
17
+ followJob,
18
+ argsFromFlags,
19
+ domainPrice,
20
+ } from "./shared.mjs";
12
21
 
13
22
  function readJsonFileFlag(flags, flag) {
14
23
  const path = flagStr(flags[flag]);
@@ -130,6 +139,8 @@ export const ARGS = {
130
139
  buy_domain: {
131
140
  phantom_id: { positional: 0, label: "phantom-id", required: true },
132
141
  domain: { value: (flags, positionals) => stripScheme(requirePositional(positionals, 1, "domain")) },
142
+ // Its price: read with register_domain's dry run on this key and echoed with --yes (domains buy's way).
143
+ accepted_quote: { internal: "the domain's price read by the command (domainPrice), echoed with --yes" },
133
144
  },
134
145
  connect_domain: {
135
146
  phantom_id: { positional: 0, label: "phantom-id", required: true },
@@ -265,7 +276,14 @@ async function phantomDomain(ctx) {
265
276
  rest.io.error("Buying a domain spends real money. Re-run with --yes.");
266
277
  return 1;
267
278
  }
268
- return call(rest, "buy_domain");
279
+ {
280
+ const args = argsFromFlags(ARGS.buy_domain, rest.flags, rest.positionals);
281
+ const priced = await domainPrice(rest.client, rest.io, args.domain);
282
+ if (priced?.quote) args.accepted_quote = priced.quote;
283
+ const result = await rest.client.callTool("buy_domain", args);
284
+ printResult(rest.io, rest.flags, result);
285
+ return 0;
286
+ }
269
287
  case "connect":
270
288
  return call(rest, "connect_domain");
271
289
  default:
@@ -385,3 +385,21 @@ export function readTextInput(flags, valueFlag, fileFlag) {
385
385
  export function readBodyInput(flags) {
386
386
  return readTextInput(flags, "body", "file");
387
387
  }
388
+
389
+ /**
390
+ * The price Letter shows for buying `domain`, read with register_domain's dry run on this key, for a command that buys
391
+ * (domains buy, domains launch, deploy create, onboard): a purchase goes only at a price Letter issued to this key,
392
+ * echoed, and never above it (Mahir, 2026-11-11). `held`: the organization has it, or takes it over: nothing is bought.
393
+ * `quote`: what to echo, the price said on stderr. Null: no purchase to price (the dispatcher says why if one is made).
394
+ */
395
+ export async function domainPrice(client, io, domain) {
396
+ if (!domain) return null;
397
+ const priced = await client.callTool("register_domain", { domain, dry_run: true });
398
+ const held = priced?.outcome === "already_owned" || priced?.outcome === "would_adopt";
399
+ // A held domain the registrar would still sell (recorded, never bought) carries a quote too: a purchase of it (a
400
+ // phantom's buy, a launch) moves money and goes only at that price (review of #6952).
401
+ if (!priced?.quote) return held ? { held: true } : null;
402
+ const usd = priced.purchase?.price_usd ?? priced.quote.purchase_usd;
403
+ io.error(`${priced.domain}: $${usd} for the first year, if bought.`);
404
+ return { ...(held ? { held: true } : {}), quote: priced.quote, price_usd: usd ?? null };
405
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.177.12",
3
+ "version": "0.177.14",
4
4
  "description": "Spin up and manage Letter phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",