promigence 0.1.0-beta.14 → 0.1.0-beta.15

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/dist/account.js CHANGED
@@ -3,6 +3,8 @@ import { queuedLines } from "./capacity.js";
3
3
  import { duration } from "./report.js";
4
4
  import { emit, ms, okEnvelope, overMost, parseCount, parseDuration, parseUsd, usageError } from "./io.js";
5
5
  import { sh } from "./term.js";
6
+ import { autoReloadShort, creditsLine, resumeAllPausedForCredits } from "./billing.js";
7
+ import { dollars } from "./money.js";
6
8
  const usd = (n) => n === undefined ? "—" : `$${n.toFixed(n < 0.01 && n > 0 ? 4 : 2)}`;
7
9
  export const PAYMENT_METHOD_NAMES = {
8
10
  apple_pay: "Apple Pay",
@@ -115,10 +117,10 @@ const me = async (ctx) => {
115
117
  lines.push(` plan: ${plan}${trial?.on_trial ? " (trial)" : ""}`);
116
118
  if (conc)
117
119
  lines.push(` concurrency: ${concurrencyText(conc)}`);
118
- if (trial?.on_trial) {
119
- const left = trial.credits_available ?? trial.credits_remaining;
120
- lines.push(` trial credit: ${usd(left)} left of ${usd(trial.credits_granted)}` +
121
- `${trial.credits_reserved ? ` (${usd(trial.credits_reserved)} held for running sandboxes)` : ""}`);
120
+ if (trial) {
121
+ const available = trial.credits_available ?? trial.credits_remaining;
122
+ const held = trial.credits_reserved ?? 0;
123
+ lines.push(` credits: ${dollars(available)} available${held > 0 ? ` (${dollars(held)} held by running work)` : ""}`);
122
124
  }
123
125
  if (typeof info.spend === "number") {
124
126
  const kinds = Object.entries(info.spend_detail?.by_kind ?? {}).filter(([, v]) => v > 0);
@@ -213,7 +215,7 @@ const billingUsage = async (ctx) => {
213
215
  ? [["promigence me", "your limits, concurrency and trial credit"]]
214
216
  : b.card_on_file
215
217
  ? [["promigence billing portal", "the card on file and past invoices"]]
216
- : [["promigence billing card", "an owner adds a card (nothing is charged now)"]]), ...(moreLedger.length ? moreLedger : [["promigence billing usage --ledger", "this key's project's metered rows"]]));
218
+ : [["promigence billing card", "an owner adds a card"]]), ...(moreLedger.length ? moreLedger : [["promigence billing usage --ledger", "this key's project's metered rows"]]));
217
219
  const plan = planName(me, b);
218
220
  const onTrial = (b.trial?.on_trial ?? me?.trial?.on_trial) === true;
219
221
  const env = okEnvelope(ctx.cmd.name, {
@@ -255,22 +257,26 @@ const billingUsage = async (ctx) => {
255
257
  lines.push(` ${k}: ${detail(row)}`);
256
258
  }
257
259
  if (typeof usage["credits_applied"] === "number" && usage["credits_applied"] > 0) {
258
- lines.push(` paid by trial credits: ${usd(usage["credits_applied"])} · to the card: ${usd(usage["billable"])}`);
260
+ lines.push(` paid from credits: ${usd(usage["credits_applied"])}`);
259
261
  }
260
262
  const trial = b.trial;
261
- if (trial?.credits_remaining !== undefined)
262
- lines.push(` trial credits left: ${usd(trial.credits_remaining)}`);
263
- lines.push(` card: ${b.card_on_file ? paymentMethodText(b.card) : `none on file${billingOff ? " (card payments are not open yet)" : ""}`}`);
263
+ const credits = creditsLine(b.credits) ?? (trial?.credits_remaining === undefined ? undefined : `credits: ${dollars(trial.credits_remaining)} available`);
264
+ if (credits)
265
+ lines.push(` ${credits}`);
266
+ if (b.auto_reload)
267
+ lines.push(` auto-reload: ${autoReloadShort(b.auto_reload)}`);
268
+ lines.push(` card: ${b.card_on_file ? paymentMethodText(b.card) : `none on file${billingOff ? " (payments are not open yet)" : ""}`}`);
264
269
  if (b.card_note)
265
270
  lines.push(` ${b.card_note}`);
266
- if (b.upcoming_invoice?.amount_due !== undefined && b.upcoming_invoice?.amount_due !== null) {
271
+ const invoiceComes = b.plan_status === undefined || !!b.plan_status.renews_at || b.model === "postpaid";
272
+ if (invoiceComes && b.upcoming_invoice?.amount_due !== undefined && b.upcoming_invoice?.amount_due !== null) {
267
273
  lines.push(` next invoice: ${usd(b.upcoming_invoice.amount_due)}`);
268
274
  }
269
- else if (b.upcoming_invoice_error) {
275
+ else if (invoiceComes && b.upcoming_invoice_error) {
270
276
  lines.push(` next invoice: not available (${b.upcoming_invoice_error})`);
271
277
  }
272
278
  if (b.suspended)
273
- lines.push(` SUSPENDED: ${b.suspend_reason ?? "see promigence billing card"}`);
279
+ lines.push(` SUSPENDED: ${b.suspend_reason ?? "see promigence billing"}`);
274
280
  for (const e of entries) {
275
281
  lines.push(` ${e.created_at ?? ""} ${e.kind}${e.tier ? ` ${e.tier}` : ""} ${usd(e.amount)}${e.seconds ? ` · ${e.seconds}s` : ""}${e.run_id ? ` · ${e.run_id}` : ""}`);
276
282
  }
@@ -294,12 +300,19 @@ const CREDIT_WHAT = {
294
300
  from_promigence: "Credit from Promigence",
295
301
  service_restart: "Credit for a service restart",
296
302
  confirmation_credit: "Credit for the $1 confirmation",
303
+ credits_bought: "Credits bought",
304
+ auto_reload: "Auto-reload",
305
+ credit_taken_back: "Credit taken back",
306
+ credit_regranted: "Credit given back",
297
307
  };
298
308
  const CREDIT_ENDED = { used: "used", expired: "expired", taken_back: "taken back" };
299
309
  const PAYMENT_WHAT = {
300
310
  invoice_paid: "Invoice paid",
301
311
  payment_failed: "Payment failed",
302
312
  refund: "Refund",
313
+ credits_bought: "Credits bought",
314
+ auto_reload: "Auto-reload",
315
+ dispute_repaid: "Repayment",
303
316
  };
304
317
  const PAYMENT_STATUS = {
305
318
  paid: "paid",
@@ -323,15 +336,30 @@ export function historyLines(entries) {
323
336
  if (!p)
324
337
  return ` ${day} (a row this version cannot show: see --json)`;
325
338
  const check = p.kind === "method_check";
339
+ const planFee = p.kind === "plan_invoice";
326
340
  const what = check
327
341
  ? `$${Number.isInteger(p.amount_usd) ? String(p.amount_usd) : p.amount_usd.toFixed(2)} to confirm ${p.method || "your payment method"}`
328
- : (wordFor(PAYMENT_WHAT, p.kind) ?? "Payment");
329
- const detail = [p.plan ? `${p.plan} plan` : null, p.invoice_number ? `invoice ${p.invoice_number}` : null]
342
+ : planFee
343
+ ? `Plan: ${p.plan ?? "your plan"}`
344
+ : (wordFor(PAYMENT_WHAT, p.kind) ?? "Payment");
345
+ const detail = [p.plan && !planFee ? `${p.plan} plan` : null, p.invoice_number ? `invoice ${p.invoice_number}` : null]
330
346
  .filter(Boolean)
331
347
  .join(", ");
332
348
  const status = check && p.status === "failed" ? "failed" : (wordFor(PAYMENT_STATUS, p.status) ?? p.status);
333
- const url = typeof p.invoice_url === "string" && p.invoice_url.startsWith("https://") ? p.invoice_url : null;
334
- const link = url && !check ? ` · ${unpaid(p.status) ? "pay at" : "receipt"} ${url}` : "";
349
+ const https = (u) => (typeof u === "string" && u.startsWith("https://") ? u : null);
350
+ const linked = [
351
+ https(p.invoice_pdf) ? `invoice ${p.invoice_pdf}` : null,
352
+ https(p.receipt_url) ? `receipt ${p.receipt_url}` : null,
353
+ https(p.pay_url) ? `pay at ${p.pay_url}` : null,
354
+ ].filter(Boolean);
355
+ const url = https(p.invoice_url);
356
+ const link = check
357
+ ? ""
358
+ : linked.length
359
+ ? ` · ${linked.join(" · ")}`
360
+ : url
361
+ ? ` · ${unpaid(p.status) ? "pay at" : "receipt"} ${url}`
362
+ : "";
335
363
  return ` ${day} ${what}${detail ? ` (${detail})` : ""} ${usd(p.amount_usd)} ${status}${link}`;
336
364
  });
337
365
  }
@@ -378,11 +406,11 @@ const referrals = async (ctx) => {
378
406
  const o = r.offer;
379
407
  lines.push(`promigence referrals: ${o.headline}`);
380
408
  lines.push(` your code: ${r.code} (a friend signs up with: ${sh `promigence login --ref ${r.code}`})`);
381
- lines.push(` they get ${usd(o.give_usd)} of compute credit when they pay their first plan invoice` +
382
- `${o.qualifying_plans.length ? ` on ${o.qualifying_plans.join(" / ")}` : ""}; you get ${usd(o.get_usd)} ` +
383
- `${o.hold_days > 0 ? `${o.hold_days} day(s) later` : "then"}`);
409
+ lines.push(` they get ${usd(o.give_usd)} of compute credit once they first pay at least ${usd(o.give_usd)}, for credits or a plan` +
410
+ `${o.qualifying_plans.length ? ` (plans: ${o.qualifying_plans.join(" / ")})` : ""}; you get ${usd(o.get_usd)} ` +
411
+ `${o.hold_days > 0 ? `${o.hold_days} ${o.hold_days === 1 ? "day" : "days"} later` : "then"}`);
384
412
  if (o.credit_expiry_days !== null)
385
- lines.push(` credit lasts ${o.credit_expiry_days} day(s) from its grant`);
413
+ lines.push(` credit lasts ${o.credit_expiry_days} ${o.credit_expiry_days === 1 ? "day" : "days"} from its grant`);
386
414
  if (o.ends_at)
387
415
  lines.push(` offer ends ${o.ends_at}`);
388
416
  }
@@ -405,8 +433,8 @@ const referralsApply = async (ctx) => {
405
433
  ? [`promigence referrals apply: ${r.code} recorded; it is being checked before any credit is granted`]
406
434
  : [
407
435
  `promigence referrals apply: ${r.code} applied`,
408
- ` you get ${usd(r.reward_usd)} of compute credit when you pay your first plan invoice` +
409
- `${r.qualifying_plans.length ? ` on ${r.qualifying_plans.join(" / ")}` : ""}`,
436
+ ` you get ${usd(r.reward_usd)} of compute credit once you first pay at least ${usd(r.reward_usd)}, for credits or a plan` +
437
+ `${r.qualifying_plans.length ? ` (plans: ${r.qualifying_plans.join(" / ")})` : ""}`,
410
438
  ];
411
439
  return emit(ctx.mode, okEnvelope(ctx.cmd.name, { ...r }, next), lines, 0, ctx.pointer);
412
440
  };
@@ -636,7 +664,7 @@ const sandboxInfo = async (ctx) => {
636
664
  const limitNext = s.pause_reason === "spend_cap"
637
665
  ? [[sh `promigence sandbox resume ${sid} --cap USD`, "raise its run's cap and go on (paused time is not billed)"]]
638
666
  : s.pause_reason === "credits_exhausted"
639
- ? [["promigence billing card", "add a card, then resume it"], [sh `promigence sandbox resume ${sid}`, "go on once credits are added"]]
667
+ ? [["promigence billing buy USD", "add credits, then resume it"], [sh `promigence sandbox resume ${sid}`, "go on once credits are added"]]
640
668
  : [];
641
669
  const next = nexts(...limitNext, ...(execs.length ? [[sh `promigence exec output ${execs[execs.length - 1].exec_id} --sandbox ${sid}`, "what its last command printed"]] : []), ...(ended
642
670
  ? (s.run_id ? [[sh `promigence run report ${s.run_id} --sandbox ${sid}`, "how it ended, in its run's report"]] : [])
@@ -684,6 +712,12 @@ const sandboxPause = async (ctx) => {
684
712
  return emit(ctx.mode, okEnvelope(ctx.cmd.name, { ...res, sandbox_id: sid, state: "paused" }, next), lines, 0, ctx.pointer);
685
713
  };
686
714
  const sandboxResume = async (ctx) => {
715
+ if (bool(ctx, "--all-paused-for-credits")) {
716
+ if (ctx.args[0] !== undefined) {
717
+ throw usageError("--all-paused-for-credits takes no sandbox id", "Pass a sandbox id, or --all-paused-for-credits alone.");
718
+ }
719
+ return resumeAllPausedForCredits(ctx);
720
+ }
687
721
  const sid = positional(ctx, "a sandbox id", "promigence sandbox resume sbx_01");
688
722
  const optimizeFor = optimizeForFlag(ctx);
689
723
  const cap = str(ctx, "--cap");
@@ -0,0 +1,266 @@
1
+ import { bool, client, nexts, str, waitBound } from "./commands.js";
2
+ import { emit, errorEnvelope, okEnvelope, parseUsd, usageError } from "./io.js";
3
+ import { dollars } from "./money.js";
4
+ import { PromigenceError, TimeoutError } from "./sdk/index.js";
5
+ import { sh } from "./term.js";
6
+ export const BILLING_PAGE = "https://console.promigence.ai/settings/org/billing";
7
+ export const MIN_PURCHASE_USD = 5;
8
+ export function cardWords(card) {
9
+ const NAMES = {
10
+ visa: "Visa", mastercard: "Mastercard", amex: "American Express", discover: "Discover", diners: "Diners Club",
11
+ jcb: "JCB", unionpay: "UnionPay", apple_pay: "Apple Pay", google_pay: "Google Pay", link: "Link",
12
+ cashapp: "Cash App Pay", amazon_pay: "Amazon Pay",
13
+ };
14
+ const brand = card?.brand ?? "";
15
+ const named = brand ? (Object.prototype.hasOwnProperty.call(NAMES, brand) ? NAMES[brand] : brand) : "";
16
+ const last4 = card?.last4 ?? "";
17
+ if (named && last4)
18
+ return `your ${named} ending ${last4}`;
19
+ if (named)
20
+ return `your ${named}`;
21
+ if (last4)
22
+ return `your card ending ${last4}`;
23
+ return "your card";
24
+ }
25
+ export function dateWords(at) {
26
+ const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(at ?? "");
27
+ const MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
28
+ if (!m)
29
+ return at ?? "";
30
+ const month = MONTHS[Number(m[2]) - 1];
31
+ return month ? `${Number(m[3])} ${month} ${m[1]}` : (at ?? "");
32
+ }
33
+ const PAUSED_PAYMENT = {
34
+ declined: "the card was declined",
35
+ authentication_required: "your bank asks you to confirm the payment",
36
+ };
37
+ const LOWER_IT = "promigence billing auto-reload --on --below <usd> --add <usd> --accept";
38
+ export function autoReloadLines(a, trust) {
39
+ if (!a)
40
+ return ["Auto-reload is off."];
41
+ if (a.unavailable_reason === "card_country") {
42
+ return ["Auto-reload isn't available yet for cards issued in India. Top up by hand: promigence billing buy <usd>."];
43
+ }
44
+ if (a.paused_reason) {
45
+ switch (a.paused_reason) {
46
+ case "declined":
47
+ case "authentication_required": {
48
+ const pay = a.last?.pay_url ? ` (${a.last.pay_url})` : "";
49
+ return [`Auto-reload is paused: ${PAUSED_PAYMENT[a.paused_reason]}. Finish the payment${pay} or add credits to turn it back on.`];
50
+ }
51
+ case "daily_limit":
52
+ return ["Auto-reload is paused: it starts again tomorrow (UTC)."];
53
+ case "disputed":
54
+ return ["Auto-reload is paused while a payment for this organization is disputed."];
55
+ case "amount_over_limit":
56
+ return [`Auto-reload is paused: its amount is above this organization's limit (${dollars(trust?.reload_max_usd)}). Lower it: ${LOWER_IT}`];
57
+ case "support":
58
+ return ["Auto-reload is paused by Promigence support. Write to support@promigence.ai to turn it back on."];
59
+ default:
60
+ return [`Auto-reload is paused: ${a.paused_reason}.`];
61
+ }
62
+ }
63
+ if (!a.on)
64
+ return ["Auto-reload is off."];
65
+ return [`Auto-reload is on: when available credits fall below ${dollars(a.threshold_usd)}, add ${dollars(a.amount_usd)} from ${cardWords(a.card)}.`];
66
+ }
67
+ export function autoReloadShort(a) {
68
+ if (!a || !a.on)
69
+ return "off";
70
+ const base = `below ${dollars(a.threshold_usd)}, add ${dollars(a.amount_usd)}`;
71
+ return a.paused_reason ? `${base} (paused: ${a.paused_reason.replaceAll("_", " ")})` : base;
72
+ }
73
+ export function creditsLine(c) {
74
+ if (!c)
75
+ return undefined;
76
+ return `credits: ${dollars(c.available_usd)} available${c.held_usd > 0 ? ` (${dollars(c.held_usd)} held by running work)` : ""}`;
77
+ }
78
+ const billing = async (ctx) => {
79
+ const c = client(ctx);
80
+ const [b, me] = await Promise.all([c.billing.usage(), c.me().catch(() => undefined)]);
81
+ const org = me?.org && me.org !== me.org_id ? me.org : (b.org_id ?? me?.org_id ?? "org");
82
+ const lines = [`promigence billing: ${org}`];
83
+ const plan = b.plan_status;
84
+ if (plan?.name)
85
+ lines.push(` plan: ${plan.name}${plan.renews_at ? ` (renews ${dateWords(plan.renews_at)})` : ""}`);
86
+ if (plan?.renewal) {
87
+ const said = plan.renewal.state === "action_required"
88
+ ? `Your bank asks you to confirm the ${plan.name} payment.`
89
+ : `Your ${plan.name} renewal did not go through.${plan.renewal.next_try_at ? ` We try again on ${dateWords(plan.renewal.next_try_at)}.` : ""}`;
90
+ lines.push(` renewal: ${said}${plan.renewal.pay_url ? ` ${plan.renewal.pay_url}` : ""}`);
91
+ }
92
+ if (b.credits)
93
+ lines.push(` credits: ${dollars(b.credits.available_usd)} available`);
94
+ if (b.model === "postpaid" && b.switch) {
95
+ lines.push(` moves to credits on ${dateWords(b.switch.at)} (${dollars(b.switch.credits_waiting_usd)} ready)`);
96
+ }
97
+ lines.push(` auto-reload: ${autoReloadShort(b.auto_reload)}`);
98
+ if (b.hold)
99
+ lines.push(` on hold: ${b.hold.reason}: ${b.hold.url ?? BILLING_PAGE}`);
100
+ lines.push(` change plan: ${BILLING_PAGE}`);
101
+ const next = nexts(...(b.hold?.dispute_id ? [[sh `promigence billing repay ${b.hold.dispute_id}`, "repay the disputed payment, which lifts the hold"]] : []), ["promigence billing buy USD", "add credits on a payment page"], ["promigence billing auto-reload", "add credits from the saved card when available credits run low"], ["promigence billing usage", "this month's spend"]);
102
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { billing: b }, next), lines, 0, ctx.pointer);
103
+ };
104
+ const FINAL = new Set(["paid", "paid_awaiting_switch", "expired", "failed"]);
105
+ const delay = (msMs) => new Promise((r) => setTimeout(r, msMs));
106
+ function settledLine(s) {
107
+ switch (s.state) {
108
+ case "paid":
109
+ return `${dollars(s.amount_usd)} of credits added.${s.available_usd === undefined || s.available_usd === null ? "" : ` Available: ${dollars(s.available_usd)}.`}`;
110
+ case "paid_awaiting_switch":
111
+ return `${dollars(s.amount_usd)} of credits are ready on ${dateWords(s.switch_at)}.`;
112
+ case "expired":
113
+ return "No payment was made.";
114
+ case "failed":
115
+ return "The payment did not go through. No credits were added.";
116
+ default:
117
+ return "Payment received. Your credits will appear in a minute.";
118
+ }
119
+ }
120
+ const billingBuy = async (ctx) => {
121
+ const c = client(ctx);
122
+ const session = str(ctx, "--session");
123
+ const bound = waitBound(ctx);
124
+ let page;
125
+ if (session === undefined) {
126
+ const raw = ctx.args[0];
127
+ if (raw === undefined)
128
+ throw usageError("billing buy needs an amount in dollars", "Example: promigence billing buy 50.");
129
+ const amount = parseUsd(raw.replace(/^\$/, ""), "USD");
130
+ if (amount < MIN_PURCHASE_USD) {
131
+ throw usageError(`promigence billing buy: the smallest purchase is ${dollars(MIN_PURCHASE_USD)}.`, "Choose an amount from $5 to $10,000.");
132
+ }
133
+ page = await c.billing.buyCredits(amount);
134
+ process.stderr.write(`promigence billing buy: open this page to pay ${dollars(page.amount_usd)} for credits:\n ${page.url}\n`);
135
+ if (bool(ctx, "--no-wait")) {
136
+ const next = nexts([sh `promigence billing buy --session ${page.session_id}`, "check the payment and add its credits"]);
137
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { page }, next), [
138
+ `promigence billing buy: open this page to pay ${dollars(page.amount_usd)} for credits:`,
139
+ ` ${page.url}`,
140
+ ], 0, ctx.pointer);
141
+ }
142
+ }
143
+ const id = session ?? page.session_id;
144
+ const settle = () => c.billing.settleCredits(id);
145
+ const until = Date.now() + bound;
146
+ if (page)
147
+ process.stderr.write("Waiting for the payment (Ctrl-C stops waiting; a payment still counts)\n");
148
+ let s = await settle();
149
+ while (!FINAL.has(s.state) && Date.now() < until) {
150
+ await delay(Math.min(2000, Math.max(0, until - Date.now())));
151
+ s = await settle();
152
+ }
153
+ if (!FINAL.has(s.state)) {
154
+ const err = new TimeoutError(`the payment page was not paid within the ${Math.round(bound / 1000)} s wait; nothing was charged by this command`, `A payment on the page still counts. Check it: ${sh `promigence billing buy --session ${id}`}.`);
155
+ const next = nexts([sh `promigence billing buy --session ${id}`, "check the payment and add its credits"]);
156
+ return emit(ctx.mode, { ...errorEnvelope(ctx.cmd.name, err, next), ...(page ? { page } : {}), settle: s }, [`promigence billing buy: ${err.message}`, ` ${err.fix}`], 11, ctx.pointer);
157
+ }
158
+ const ok = s.state === "paid" || s.state === "paid_awaiting_switch";
159
+ const next = ok
160
+ ? nexts(["promigence billing", "the credits, auto-reload and the plan"])
161
+ : nexts(["promigence billing buy USD", "open a new payment page"]);
162
+ const line = settledLine(s);
163
+ if (!ok) {
164
+ const err = new PromigenceError({ code: "usage", message: line, fix: "Open a new payment page: promigence billing buy <usd>.", exitCode: 2 });
165
+ return emit(ctx.mode, { ...errorEnvelope(ctx.cmd.name, err, next), settle: s }, [`promigence billing buy: ${line}`], 2, ctx.pointer);
166
+ }
167
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { ...(page ? { page } : {}), settle: s }, next), [line], 0, ctx.pointer);
168
+ };
169
+ export function authorizationText(b, threshold, amount) {
170
+ const a = b.auto_reload;
171
+ const template = a?.authorization?.template ?? "";
172
+ return template
173
+ .replaceAll("{card}", cardWords(a?.card))
174
+ .replaceAll("{amount}", dollars(amount))
175
+ .replaceAll("{threshold}", dollars(threshold))
176
+ .replaceAll("{n}", String(b.trust?.reloads_per_day ?? ""))
177
+ .replaceAll("{day_max}", dollars(b.trust?.per_day_max_usd));
178
+ }
179
+ const billingAutoReload = async (ctx) => {
180
+ const c = client(ctx);
181
+ const on = bool(ctx, "--on");
182
+ const off = bool(ctx, "--off");
183
+ const below = str(ctx, "--below");
184
+ const add = str(ctx, "--add");
185
+ if (on && off)
186
+ throw usageError("--on and --off together", "Pass one of them.");
187
+ if (!on && (below !== undefined || add !== undefined || bool(ctx, "--accept"))) {
188
+ throw usageError("--below, --add and --accept go with --on", "Example: promigence billing auto-reload --on --below 10 --add 50 --accept.");
189
+ }
190
+ const next = nexts(["promigence billing", "the credits and the plan"]);
191
+ if (!on && !off) {
192
+ const a = await c.billing.autoReload();
193
+ const trust = a.paused_reason === "amount_over_limit" ? (await c.billing.usage()).trust : undefined;
194
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { auto_reload: a }, next), autoReloadLines(a, trust), 0, ctx.pointer);
195
+ }
196
+ if (off) {
197
+ const a = await c.billing.setAutoReload({ on: false });
198
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { auto_reload: a }, next), autoReloadLines(a), 0, ctx.pointer);
199
+ }
200
+ const b = await c.billing.usage();
201
+ if (b.auto_reload?.unavailable_reason === "card_country") {
202
+ throw usageError("Auto-reload isn't available yet for cards issued in India.", "Top up by hand: promigence billing buy <usd>.");
203
+ }
204
+ const threshold = below === undefined ? (b.auto_reload?.threshold_usd ?? 10) : parseUsd(below, "--below");
205
+ const amount = add === undefined ? (b.auto_reload?.amount_usd ?? 50) : parseUsd(add, "--add");
206
+ if (!bool(ctx, "--accept")) {
207
+ throw usageError(authorizationText(b, threshold, amount), "Pass --accept to agree.");
208
+ }
209
+ const a = await c.billing.setAutoReload({
210
+ on: true,
211
+ thresholdUsd: threshold,
212
+ amountUsd: amount,
213
+ ...(b.auto_reload?.authorization?.version === undefined ? {} : { authorizationVersion: b.auto_reload.authorization.version }),
214
+ });
215
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { auto_reload: a }, next), autoReloadLines(a, b.trust), 0, ctx.pointer);
216
+ };
217
+ const billingHolds = async (ctx) => {
218
+ const h = await client(ctx).billing.holds();
219
+ const n = h.sandboxes.length;
220
+ const lines = [`promigence billing holds: ${dollars(h.held_usd)} held by ${n} ${n === 1 ? "sandbox" : "sandboxes"}`];
221
+ for (const s of h.sandboxes) {
222
+ lines.push(` ${s.sandbox_id} ${dollars(s.held_usd)} ${s.state === "paused" || !s.ends_at ? "paused" : `ends ${s.ends_at}`}`);
223
+ }
224
+ for (const b of h.builds)
225
+ lines.push(` ${b.build_id} ${dollars(b.held_usd)} build`);
226
+ if (h.data_out_usd > 0)
227
+ lines.push(` data out not yet charged ${dollars(h.data_out_usd)}`);
228
+ if (h.storage_usd > 0)
229
+ lines.push(` storage, the next 7 days ${dollars(h.storage_usd)}`);
230
+ const first = h.sandboxes[0];
231
+ const next = first
232
+ ? nexts([sh `promigence sandbox kill ${first.sandbox_id}`, "end it, and free what it holds"], [sh `promigence sandbox extend ${first.sandbox_id} --timeout DUR`, "change its lifetime (a shorter one holds less)"])
233
+ : nexts(["promigence billing", "the credits and the plan"]);
234
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { holds: h }, next), lines, 0, ctx.pointer);
235
+ };
236
+ const billingRepay = async (ctx) => {
237
+ const dispute = ctx.args[0];
238
+ if (dispute === undefined) {
239
+ throw usageError("billing repay needs a dispute id", "`promigence billing` names the hold; its dispute id is in `promigence billing --json` (hold.dispute_id).");
240
+ }
241
+ const page = await client(ctx).billing.repayDispute(dispute);
242
+ const next = nexts(["promigence billing", "once it is paid: the hold is lifted"]);
243
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { page }, next), [
244
+ `promigence billing repay: open this page to repay ${dollars(page.amount_usd)}:`,
245
+ ` ${page.url}`,
246
+ ], 0, ctx.pointer);
247
+ };
248
+ export const BILLING_HANDLERS = {
249
+ billing,
250
+ "billing repay": billingRepay,
251
+ "billing buy": billingBuy,
252
+ "billing auto-reload": billingAutoReload,
253
+ "billing holds": billingHolds,
254
+ };
255
+ export async function resumeAllPausedForCredits(ctx) {
256
+ const r = await client(ctx).sandboxes.resumePausedForCredits();
257
+ const n = r.resumed.length;
258
+ const m = r.still_need_credits.length;
259
+ const line = m > 0
260
+ ? `${n} resumed. ${m} still need credits: add credits with promigence billing buy <usd>.`
261
+ : `${n} resumed. ${m} still need credits.`;
262
+ const next = m > 0
263
+ ? nexts(["promigence billing buy USD", "add credits, then resume the rest"])
264
+ : nexts(["promigence sandbox list", "your sandboxes"]);
265
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { ...r }, next), [line], 0, ctx.pointer);
266
+ }
package/dist/bin.js CHANGED
@@ -18,6 +18,7 @@ import { RUNNERS_HANDLERS } from "./runners.js";
18
18
  import { PRICING_HANDLERS } from "./pricing.js";
19
19
  import { GITHUB_HANDLERS } from "./github.js";
20
20
  import { GITLAB_HANDLERS } from "./gitlab.js";
21
+ import { BILLING_HANDLERS } from "./billing.js";
21
22
  import { emit, errorEnvelope, errorTextLines, parseDuration, setBeforeExit, toPromigenceError, usageError as usageFailure, USAGE_DOCS, write, } from "./io.js";
22
23
  import { setCaller } from "./sdk/caller.js";
23
24
  import { sh } from "./term.js";
@@ -270,7 +271,7 @@ async function main(argv) {
270
271
  await write(process.stdout, renderCommandHelp(cmd));
271
272
  return;
272
273
  }
273
- const handler = HANDLERS[cmd.name] ?? EXTRA_HANDLERS[cmd.name] ?? BUNDLE_HANDLERS[cmd.name] ?? LOGIN_HANDLERS[cmd.name] ?? ORG_HANDLERS[cmd.name] ?? LIVE_HANDLERS[cmd.name] ?? SSH_HANDLERS[cmd.name] ?? ACCOUNT_HANDLERS[cmd.name] ?? MCP_HANDLERS[cmd.name] ?? AGENT_HANDLERS[cmd.name] ?? CREDITS_HANDLERS[cmd.name] ?? RUNNERS_HANDLERS[cmd.name] ?? PRICING_HANDLERS[cmd.name] ?? GITHUB_HANDLERS[cmd.name] ?? GITLAB_HANDLERS[cmd.name] ?? TEMPLATES_HANDLERS[cmd.name];
274
+ const handler = HANDLERS[cmd.name] ?? EXTRA_HANDLERS[cmd.name] ?? BUNDLE_HANDLERS[cmd.name] ?? LOGIN_HANDLERS[cmd.name] ?? ORG_HANDLERS[cmd.name] ?? LIVE_HANDLERS[cmd.name] ?? SSH_HANDLERS[cmd.name] ?? ACCOUNT_HANDLERS[cmd.name] ?? MCP_HANDLERS[cmd.name] ?? AGENT_HANDLERS[cmd.name] ?? CREDITS_HANDLERS[cmd.name] ?? RUNNERS_HANDLERS[cmd.name] ?? PRICING_HANDLERS[cmd.name] ?? GITHUB_HANDLERS[cmd.name] ?? GITLAB_HANDLERS[cmd.name] ?? TEMPLATES_HANDLERS[cmd.name] ?? BILLING_HANDLERS[cmd.name];
274
275
  if (!handler)
275
276
  return void (await notReleased(mode, cmd));
276
277
  const ctx = {
package/dist/commands.js CHANGED
@@ -1864,7 +1864,7 @@ const runComposite = async (ctx) => {
1864
1864
  const next = nexts(...(capPaused
1865
1865
  ? [limitPauseOf(capPaused) === "spend_cap"
1866
1866
  ? [sh `promigence sandbox resume ${capPaused.sandbox_id} --cap USD`, "go on: a cap above the run's (its files and state are kept)"]
1867
- : ["promigence billing card", `add a card, then ${sh `promigence sandbox resume ${capPaused.sandbox_id}`}`]]
1867
+ : ["promigence billing buy USD", `add credits, then ${sh `promigence sandbox resume ${capPaused.sandbox_id}`}`]]
1868
1868
  : []), [sh `promigence run report ${runId} --json`, "the full validity report (per-sandbox evidence)"], ...((report.totals?.env_failed ?? 0) > 0
1869
1869
  ? [[sh `promigence run replay ${runId}`, "re-fork the env failures (a replay is a new sandbox, billed like any other)"]]
1870
1870
  : []), ...(kept.length
@@ -2062,7 +2062,7 @@ export function limitPauseLines(report) {
2062
2062
  }
2063
2063
  else {
2064
2064
  lines.push(` ${s.sandbox_id}: paused: the organization's credits ran out; its files and state are kept`);
2065
- lines.push(` go on: add a card (owners: promigence billing card), then ${sh `promigence sandbox resume ${s.sandbox_id}`}, or end it: ${sh `promigence sandbox kill ${s.sandbox_id}`}`);
2065
+ lines.push(` go on: add credits (owners: promigence billing buy <usd>), then ${sh `promigence sandbox resume ${s.sandbox_id}`}, or end it: ${sh `promigence sandbox kill ${s.sandbox_id}`}`);
2066
2066
  }
2067
2067
  }
2068
2068
  if (paused.length > FAILURES_SHOWN) {
package/dist/errfix.js CHANGED
@@ -103,18 +103,24 @@ function notFoundFor(kind, ctx, apiFix) {
103
103
  };
104
104
  }
105
105
  }
106
- const BILLING_CMD = /`promigence billing (?:card|portal)`/;
106
+ const BILLING_CMD = /`promigence billing (?:card|portal|buy <usd>)`|Change plan in Billing/;
107
107
  export function withoutBilling(text) {
108
+ const more = `Payments are not open yet: for more credits, email ${SUPPORT}.`;
108
109
  return text
109
110
  .replace(/Owners: update the card with `promigence billing portal`; the hold lifts once the invoice is paid\.(?: For a disputed payment, write to support@promigence\.ai\.)?/, `To lift it, email ${SUPPORT}.`)
110
- .replace(/,? or move to a plan that allows it: `promigence billing (?:card|portal)`\.?/, `, or email ${SUPPORT} for a plan that allows it.`)
111
+ .replace(/,? or move to a plan that allows it: (?:`promigence billing (?:card|portal)`|Change plan in Billing \(`promigence billing` prints the link\))\.?/, `, or email ${SUPPORT} for a plan that allows it.`)
111
112
  .replace(/or add a card to lift it: `promigence billing (?:card|portal)`/, `or email ${SUPPORT} to lift it`)
113
+ .replace(/or lift it by adding a card \(`promigence billing card`\) or buying credits \(`promigence billing buy <usd>`\)/, `or email ${SUPPORT} to lift it`)
112
114
  .replace(/adding a card raises it \(owners: `promigence billing (?:card|portal)`\)\.?/, `to raise it, email ${SUPPORT}.`)
113
- .replace(/Add a card to keep going \(owners: `promigence billing (?:card|portal)`[^)]*\)\.?/, `Card payments are not open yet: for more credits, email ${SUPPORT}.`)
114
- .replace(/\(?owners: `promigence billing (?:card|portal)`(?:; members ask an owner)?\)?/g, `email ${SUPPORT}`)
115
- .replace(/`promigence billing (?:card|portal)`/g, SUPPORT);
115
+ .replace(/Add credits(?: to keep going)? \(owners: `promigence billing buy <usd>`, or Add credits in Billing; members ask an owner\)(?:, or turn on auto-reload)?\./, more)
116
+ .replace(/Add credits to go on \(owners: `promigence billing buy <usd>`; members ask an owner\), then resume it:/, `Payments are not open yet: email ${SUPPORT} for more credits, then resume it:`)
117
+ .replace(/Add credits by hand: `promigence billing buy <usd>`\./, `For more credits, email ${SUPPORT}.`)
118
+ .replace(/ Or add credits: `promigence billing buy <usd>`\./, "")
119
+ .replace(/Add a card(?: to keep going)? \(owners: `promigence billing (?:card|portal)`[^)]*\)\.?/, more)
120
+ .replace(/\(?owners: `promigence billing (?:card|portal|buy <usd>)`(?:; members ask an owner)?\)?/g, `email ${SUPPORT}`)
121
+ .replace(/`promigence billing (?:card|portal|buy <usd>)`/g, SUPPORT);
116
122
  }
117
- export const CREDIT_UNCLAIMED_FIX = "Claim your free credit first: `promigence credits claim` (owners; members ask an owner). Or add a card: `promigence billing card`.";
123
+ export const CREDIT_UNCLAIMED_FIX = "Claim your free credit first: `promigence credits claim` (owners; members ask an owner). Or add credits: `promigence billing buy <usd>`.";
118
124
  const ANY_HELP = /See `promigence [a-z][a-z -]* --help`\.?$/;
119
125
  const API_WORDS = [
120
126
  [/`timeout_ms`\s+(\d+)/g, "--timeout", (_m, ms) => `\`--timeout ${duration(Number(ms))}\``],
package/dist/generated.js CHANGED
@@ -79,7 +79,7 @@ export const ERRORS = [
79
79
  fix: "Its files and state are kept, and paused time is not billed. Raise the run's cap to go on: `promigence sandbox resume {sandbox_id} --cap <usd>` (`spend_cap` on POST /v1/sandboxes/{sandbox_id}/resume). Or end it: `promigence sandbox kill {sandbox_id}`." },
80
80
  { code: "credits_paused", category: "cap", http: 402, exit: 5, billed: "no",
81
81
  message: "sandbox {sandbox_id} is paused: the organization's credits ran out",
82
- fix: "Its files and state are kept, and paused time is not billed. Add a card to go on (owners: `promigence billing card`; members ask an owner), then resume it: `promigence sandbox resume {sandbox_id}`. Or end it: `promigence sandbox kill {sandbox_id}`." },
82
+ fix: "Its files and state are kept, and paused time is not billed. Add credits to go on (owners: `promigence billing buy <usd>`; members ask an owner), then resume it: `promigence sandbox resume {sandbox_id}`. Or end it: `promigence sandbox kill {sandbox_id}`." },
83
83
  { code: "aborted_by_client", category: "other", http: 200, exit: 130, billed: "yes",
84
84
  message: "sandbox killed by the client",
85
85
  fix: "Nothing to fix; billed for the seconds used." },
@@ -175,19 +175,34 @@ export const ERRORS = [
175
175
  fix: "Check the command name; install the program in the image or in a setup step; for a shell builtin or a pipeline, run it through a shell: `-- sh -c '...'`." },
176
176
  { code: "trial_credits_exhausted", category: "cap", http: 402, exit: 5, billed: "no",
177
177
  message: "trial credits left ({remaining}) do not cover this ({needed})",
178
- fix: "Add a card to keep going (owners: `promigence billing card`, or POST /v1/billing/checkout; members ask an owner). Remaining credits are used first; running sandboxes finish." },
178
+ fix: "Add credits to keep going (owners: `promigence billing buy <usd>`, or Add credits in Billing; members ask an owner). Running sandboxes finish the time they reserved." },
179
+ { code: "credits_exhausted", category: "cap", http: 402, exit: 5, billed: "no",
180
+ message: "available credits ({remaining}) do not cover this ({needed})",
181
+ fix: "Add credits (owners: `promigence billing buy <usd>`, or Add credits in Billing; members ask an owner), or turn on auto-reload. Running sandboxes finish the time they reserved." },
179
182
  { code: "credit_unclaimed", category: "other", http: 402, exit: 5, billed: "no",
180
183
  message: "this account has free credit waiting to be claimed",
181
- fix: "Claim it in your browser: `promigence credits claim` (owners; members ask an owner). Or add a card: `promigence billing card`. Nothing was started or charged." },
184
+ fix: "Claim it in your browser: `promigence credits claim` (owners; members ask an owner). Or add credits: `promigence billing buy <usd>`. Nothing was started or charged." },
182
185
  { code: "setup_required", category: "other", http: 403, exit: 7, billed: "no",
183
186
  message: "finish setting up your account before starting new work",
184
187
  fix: "Open {setup_url} and answer the setup questions (under a minute), then run this again. Nothing was started or charged." },
185
188
  { code: "card_required", category: "other", http: 402, exit: 5, billed: "no",
186
189
  message: "a card is required for new work since {effective_at}",
187
- fix: "Add a card (owners: `promigence billing card`, or POST /v1/billing/checkout; members ask an owner). Your trial credits are kept and used first; reads and results stay available." },
190
+ fix: "Add a card (owners: `promigence billing card`, or POST /v1/billing/checkout; members ask an owner). Your credits are kept, and reads and results stay available." },
188
191
  { code: "billing_unavailable", category: "other", http: 503, exit: 5, billed: "no",
189
- message: "card payments are not open yet",
190
- fix: "Nothing was charged. Your trial credits are kept and used first; for more credits, email support@promigence.ai." },
192
+ message: "payments are not available right now",
193
+ fix: "Nothing was charged. Your credits and running work are unaffected. Try again later, or write to support@promigence.ai." },
194
+ { code: "purchase_limit", category: "other", http: 402, exit: 5, billed: "no",
195
+ message: "{needed} is more than this organization can buy at once right now (up to {limit})",
196
+ fix: "Buy {limit} or less, or choose a smaller plan. The limit rises as the organization's payment history grows; for more now, write to support@promigence.ai." },
197
+ { code: "card_needed", category: "other", http: 409, exit: 5, billed: "no",
198
+ message: "auto-reload needs a saved card",
199
+ fix: "Add credits once, or add a card in Billing, then turn auto-reload on." },
200
+ { code: "auto_reload_unavailable", category: "other", http: 409, exit: 5, billed: "no",
201
+ message: "auto-reload isn't available yet for cards issued in India",
202
+ fix: "Add credits by hand: `promigence billing buy <usd>`." },
203
+ { code: "purchases_blocked", category: "other", http: 409, exit: 5, billed: "no",
204
+ message: "this organization cannot buy credits right now: a payment for this organization is disputed",
205
+ fix: "Write to support@promigence.ai." },
191
206
  { code: "concurrency_limit", category: "other", http: 409, exit: 5, billed: "no",
192
207
  message: "{live} live + {requested} requested sandboxes is over your limit of {limit}",
193
208
  fix: "Wait for sandboxes to end or lower `--count` (`--allow-partial` covers the spend cap only); a retry of the same request fails the same way. Trial orgs get {trial_limit}; to raise it, email support@promigence.ai." },
@@ -202,10 +217,10 @@ export const ERRORS = [
202
217
  fix: "Running sandboxes finish. The limit resets at 00:00 UTC and grows with each paid invoice; ask for less, or email support@promigence.ai." },
203
218
  { code: "billing_hold", category: "other", http: 402, exit: 5, billed: "no",
204
219
  message: "new work is on hold: {reason}",
205
- fix: "Running sandboxes finish. Owners: update the card with `promigence billing portal`; the hold lifts once the invoice is paid. For a disputed payment, write to support@promigence.ai." },
220
+ fix: "Running sandboxes finish, and files and results stay available. An owner sees why, and what lifts it, in Billing (`promigence billing` prints the link), or write to support@promigence.ai." },
206
221
  { code: "tier_over_plan", category: "other", http: 403, exit: 2, billed: "no",
207
222
  message: "the {tier} tier ({vcpus} vCPU) is over the {limit} vCPU per-sandbox limit of your {plan} plan",
208
- fix: "Use a tier of at most {limit} vCPU (`promigence snapshot create … --tier {max_tier}` is the largest; a fork runs on its snapshot's), or move to a plan that allows it: `promigence billing portal`." },
223
+ fix: "Use a tier of at most {limit} vCPU (`promigence snapshot create … --tier {max_tier}` is the largest; a fork runs on its snapshot's), or move to a plan that allows it: Change plan in Billing (`promigence billing` prints the link)." },
209
224
  { code: "network_host_refused", category: "other", http: 403, exit: 9, billed: "no",
210
225
  message: "{host} is not on this sandbox's allowed list, so the connection was refused",
211
226
  fix: "Add it when you start the sandbox — `--allow {host}` — or drop `--allow` to let the sandbox reach anything. `promigence sandbox network <id>` lists everything that has been refused." },
package/dist/guide.js CHANGED
@@ -9,8 +9,8 @@ import { DOCS_URL, EXIT_CODES, EXIT_PRECEDENCE_TEXT, GOLDEN_PATH, GROUPS, GUIDE_
9
9
  export const SECTIONS = ["core", "commands", "errors", "cost"];
10
10
  export const BUDGETS = {
11
11
  core: 1500,
12
- commands: 4500,
13
- errors: 2325,
12
+ commands: 4825,
13
+ errors: 2475,
14
14
  cost: 600,
15
15
  };
16
16
  export function lookupError(code) {
@@ -253,7 +253,9 @@ function renderCost(prices) {
253
253
  : prices.source === "api"
254
254
  ? `Live from the API (${view?.as_of ?? "now"}): your organization's prices. \`promigence pricing\` shows the rest.`
255
255
  : prices.source === "published"
256
- ? "Published list prices. Your organization's own: `promigence login`, then `promigence pricing`."
256
+ ? prices.reason !== undefined
257
+ ? "Published list prices: the API refused this key. Your organization's own: `promigence login`, then `promigence pricing`."
258
+ : "Published list prices. Your organization's own: `promigence login`, then `promigence pricing`."
257
259
  : `${prices.note ?? "list prices at build time; could not reach the API"} (\`promigence pricing\` asks again).`;
258
260
  const limit = view?.limits.max_sandbox_vcpus;
259
261
  const out = [
package/dist/handlers.js CHANGED
@@ -4,7 +4,7 @@ import { BUNDLE_EXT, DEFAULT_SNAPSHOT, PromigenceError, VERSION as SDK_VERSION,
4
4
  import { storeLabel } from "./sdk/credstore.js";
5
5
  import { bool, client, list, nexts, positional, required, str, } from "./commands.js";
6
6
  import { emit, exitWith, forkSummary, markGuideSeen, ms, okEnvelope, parseCount, parseDuration, parseUsd, pickExit, usageError, write, } from "./io.js";
7
- import { ALL_ERROR_CODES, BUDGETS, estimateTokens, guideJson, humanHeader, lookupError, renderGuide } from "./guide.js";
7
+ import { ALL_ERROR_CODES, BUDGETS, estimateTokens, guideJson, humanHeader, lookupError, renderGuide, renderLlmsTxt } from "./guide.js";
8
8
  import { livePricing, quickClient } from "./pricing.js";
9
9
  import { install, installLines } from "./install.js";
10
10
  import { backgroundOutput } from "./live.js";
@@ -20,6 +20,15 @@ const agentGuide = async (ctx) => {
20
20
  throw usageError(`--format: '${format}' is not a format`, "Pass --format md (default) or --format json.");
21
21
  }
22
22
  const json = format === "json";
23
+ if (bool(ctx, "--llms")) {
24
+ const other = ["--section", "--code", "--install", "--check", "--create"].find((f) => ctx.flags.has(f));
25
+ if (other !== undefined || str(ctx, "--format") !== undefined) {
26
+ throw usageError(`--llms takes no other flag (got ${other ?? "--format"})`, "Run promigence agent-guide --llms on its own.");
27
+ }
28
+ markGuideSeen();
29
+ await write(process.stdout, renderLlmsTxt());
30
+ return exitWith(0);
31
+ }
23
32
  if (bool(ctx, "--create") && !bool(ctx, "--install") && !bool(ctx, "--check")) {
24
33
  throw usageError("--create needs --install", "Pass --install --create to write AGENTS.md (and the skill and rule files) here.");
25
34
  }