promigence 0.1.0-beta.7 → 0.1.0-beta.8

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 CHANGED
@@ -21,15 +21,13 @@ own (`npm config set prefix ~/.npm-global` and add `~/.npm-global/bin` to your `
21
21
 
22
22
  ## Create your account
23
23
 
24
- Promigence is in private beta: you need an invite code.
25
-
26
24
  ```sh
27
25
  promigence signup
28
26
  ```
29
27
 
30
- It asks for your invite code, then opens your browser to create the account. Your API key is saved
31
- in your system keychain, or in `~/.config/promigence/credentials` (mode 0600) where there is none. On
32
- another machine, use `promigence login`: it signs you in with a code we email you, or your password. In CI or on an
28
+ It opens your browser to create the account. Your API key is saved in your system keychain, or in
29
+ `~/.config/promigence/credentials` (mode 0600) where there is none.
30
+ On another machine, use `promigence login`: it signs you in with a code we email you. In CI or on an
33
31
  agent's machine, set `PROMIGENCE_API_KEY` instead. `promigence doctor` checks the setup.
34
32
 
35
33
  ## First commands
@@ -57,7 +55,7 @@ promigence fork --snapshot py --count 5 --cap 2
57
55
 
58
56
  ## For AI agents
59
57
 
60
- Run `promigence agent-guide` (about 1.2k tokens) before driving the CLI: it covers the model, the
58
+ Run `promigence agent-guide` (about 1.4k tokens) before driving the CLI: it covers the model, the
61
59
  JSON output contract, exit codes and errors. `promigence agent-guide --install` writes the same
62
60
  guidance into your repo for coding agents, and `promigence mcp` serves the CLI as an MCP server.
63
61
  Every command takes `--json`.
package/dist/account.js CHANGED
@@ -223,6 +223,7 @@ const billingUsage = async (ctx) => {
223
223
  usage,
224
224
  card_on_file: b.card_on_file ?? false,
225
225
  ...(b.card ? { card: b.card } : {}),
226
+ ...(b.card_note ? { card_note: b.card_note } : {}),
226
227
  ...(b.trial ? { trial: b.trial } : {}),
227
228
  ...(b.suspended ? { suspended: true, suspend_reason: b.suspend_reason ?? null } : {}),
228
229
  upcoming_invoice: b.upcoming_invoice ?? null,
@@ -260,6 +261,8 @@ const billingUsage = async (ctx) => {
260
261
  if (trial?.credits_remaining !== undefined)
261
262
  lines.push(` trial credits left: ${usd(trial.credits_remaining)}`);
262
263
  lines.push(` card: ${b.card_on_file ? paymentMethodText(b.card) : `none on file${billingOff ? " (card payments are not open yet)" : ""}`}`);
264
+ if (b.card_note)
265
+ lines.push(` ${b.card_note}`);
263
266
  if (b.upcoming_invoice?.amount_due !== undefined && b.upcoming_invoice?.amount_due !== null) {
264
267
  lines.push(` next invoice: ${usd(b.upcoming_invoice.amount_due)}`);
265
268
  }
@@ -282,6 +285,86 @@ const billingPortal = async (ctx) => {
282
285
  const next = nexts(["promigence billing usage", "this month's spend without leaving the terminal"]);
283
286
  return emit(ctx.mode, okEnvelope(ctx.cmd.name, { url: res.url }, next), ["promigence billing portal: open this page to manage the card and past invoices:", ` ${res.url}`], 0, ctx.pointer);
284
287
  };
288
+ const HISTORY_KINDS = ["all", "credits", "payments"];
289
+ const CREDIT_WHAT = {
290
+ free_credit: "Free credit",
291
+ card_bonus: "Card bonus",
292
+ referral: "Referral reward",
293
+ plan: "Included with your plan",
294
+ from_promigence: "Credit from Promigence",
295
+ service_restart: "Credit for a service restart",
296
+ confirmation_credit: "Credit for the $1 confirmation",
297
+ };
298
+ const CREDIT_ENDED = { used: "used", expired: "expired", taken_back: "taken back" };
299
+ const PAYMENT_WHAT = {
300
+ invoice_paid: "Invoice paid",
301
+ payment_failed: "Payment failed",
302
+ refund: "Refund",
303
+ };
304
+ const PAYMENT_STATUS = {
305
+ paid: "paid",
306
+ retrying: "will be retried",
307
+ failed: "not paid",
308
+ refunded: "refunded",
309
+ confirmed: "confirmed",
310
+ };
311
+ const wordFor = (m, k) => k && Object.prototype.hasOwnProperty.call(m, k) ? m[k] : undefined;
312
+ const unpaid = (status) => status === "retrying" || status === "failed";
313
+ export function historyLines(entries) {
314
+ return entries.map((e) => {
315
+ const day = (e.at ?? "").slice(0, 10);
316
+ const c = e.credit;
317
+ if (c) {
318
+ const state = wordFor(CREDIT_ENDED, c.status) ??
319
+ `${usd(c.remaining_usd)} left, ${c.expires_at ? `expires ${c.expires_at.slice(0, 10)}` : "no expiry"}`;
320
+ return ` ${day} ${wordFor(CREDIT_WHAT, c.kind) ?? "Credit"} ${usd(c.granted_usd)} ${state}`;
321
+ }
322
+ const p = e.payment;
323
+ if (!p)
324
+ return ` ${day} (a row this version cannot show: see --json)`;
325
+ const check = p.kind === "method_check";
326
+ const what = check
327
+ ? `$${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]
330
+ .filter(Boolean)
331
+ .join(", ");
332
+ 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}` : "";
335
+ return ` ${day} ${what}${detail ? ` (${detail})` : ""} ${usd(p.amount_usd)} ${status}${link}`;
336
+ });
337
+ }
338
+ const billingHistory = async (ctx) => {
339
+ const kind = str(ctx, "--kind") ?? "all";
340
+ if (!HISTORY_KINDS.includes(kind)) {
341
+ throw usageError(`--kind: '${kind}' is not a kind`, `Pass one of ${HISTORY_KINDS.join(" | ")}.`);
342
+ }
343
+ const limit = limitOf(ctx, 50, 100);
344
+ const cursor = str(ctx, "--cursor");
345
+ const page = await client(ctx).billing.historyPage({
346
+ kind: kind,
347
+ limit,
348
+ ...(cursor === undefined ? {} : { cursor }),
349
+ });
350
+ const entries = page.items;
351
+ const more = page.nextCursor;
352
+ const owed = entries.some((e) => e.payment && e.payment.kind !== "method_check" && unpaid(e.payment.status));
353
+ const next = nexts(...(more
354
+ ? [[`${sh `promigence billing history --limit ${String(limit)}`}${kind === "all" ? "" : sh ` --kind ${kind}`}${sh ` --cursor ${more}`}`, "the next page"]]
355
+ : []), ...(owed
356
+ ? [["promigence billing portal", "pay the open invoice, or update the card on file"]]
357
+ : [["promigence billing usage", "this month's spend and credit"]]));
358
+ const empty = { all: "no credits or payments yet", credits: "no credit yet", payments: "no payments yet" };
359
+ const noun = kind === "all" ? "" : ` ${entries.length === 1 ? kind.slice(0, -1) : kind}`;
360
+ const lines = [
361
+ `promigence billing history: ${entries.length}${noun}, newest first`,
362
+ ...historyLines(entries),
363
+ ...(entries.length ? [] : [` (${empty[kind]})`]),
364
+ ...(more ? [` more: --cursor ${more}`] : []),
365
+ ];
366
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { kind, entries, count: entries.length, next_cursor: more }, next), lines, 0, ctx.pointer);
367
+ };
285
368
  const referrals = async (ctx) => {
286
369
  const r = await client(ctx).billing.referrals();
287
370
  const next = r.code
@@ -745,6 +828,7 @@ export const ACCOUNT_HANDLERS = {
745
828
  status,
746
829
  "billing usage": billingUsage,
747
830
  "billing portal": billingPortal,
831
+ "billing history": billingHistory,
748
832
  referrals,
749
833
  "referrals apply": referralsApply,
750
834
  "snapshot delete": snapshotDelete,
package/dist/errfix.js CHANGED
@@ -106,6 +106,7 @@ function notFoundFor(kind, ctx, apiFix) {
106
106
  const BILLING_CMD = /`promigence billing (?:card|portal)`/;
107
107
  export function withoutBilling(text) {
108
108
  return text
109
+ .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}.`)
109
110
  .replace(/,? or move to a plan that allows it: `promigence billing (?:card|portal)`\.?/, `, or email ${SUPPORT} for a plan that allows it.`)
110
111
  .replace(/or add a card to lift it: `promigence billing (?:card|portal)`/, `or email ${SUPPORT} to lift it`)
111
112
  .replace(/adding a card raises it \(owners: `promigence billing (?:card|portal)`\)\.?/, `to raise it, email ${SUPPORT}.`)
package/dist/generated.js CHANGED
@@ -106,7 +106,7 @@ export const ERRORS = [
106
106
  fix: "Set PROMIGENCE_API_KEY or run `promigence auth login --from-env VAR`. No key yet: `promigence login` (sign in) or https://promigence.ai." },
107
107
  { code: "forbidden", category: "other", http: 403, exit: 7, billed: "no",
108
108
  message: "{detail}",
109
- fix: "Do what the message names: sign in (`promigence login`), use a key from that org, or ask an owner." },
109
+ fix: "Do what the message names: sign in (`promigence login`), use a full-access key (`promigence keys create`) or one from that org, or ask an owner." },
110
110
  { code: "not_found", category: "other", http: 404, exit: 8, billed: "no",
111
111
  message: "{kind} `{id}` not found",
112
112
  fix: "Check the id; aliases resolve in your org first, then public snapshots (`promigence examples`)." },
@@ -191,7 +191,7 @@ export const ERRORS = [
191
191
  { code: "concurrency_limit", category: "other", http: 409, exit: 5, billed: "no",
192
192
  message: "{live} live + {requested} requested sandboxes is over your limit of {limit}",
193
193
  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." },
194
- { code: "egress_cap_reached", category: "other", http: 403, exit: 10, billed: "no",
194
+ { code: "egress_cap_reached", category: "other", http: 403, exit: 5, billed: "no",
195
195
  message: "monthly data-out cap reached: {used_gb} of {cap_gb} GB",
196
196
  fix: "Use `--network none` (still allowed) or wait for the 1st (UTC). For more data out a month, email support@promigence.ai." },
197
197
  { code: "project_cap_reached", category: "cap", http: 403, exit: 5, billed: "no",
@@ -199,7 +199,10 @@ export const ERRORS = [
199
199
  fix: "Ask for less (`--count`, `--cap`, a shorter extension), wait for the 1st (UTC), or have an owner raise it: `promigence project cap {project} <usd>`." },
200
200
  { code: "spend_limit_reached", category: "cap", http: 402, exit: 5, billed: "no",
201
201
  message: "today's spend limit {limit} has {remaining} left; this needs {needed}",
202
- 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. After a failed payment, new work waits until it is paid (owners: `promigence billing portal`)." },
202
+ 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
+ { code: "billing_hold", category: "other", http: 402, exit: 5, billed: "no",
204
+ 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." },
203
206
  { code: "tier_over_plan", category: "other", http: 403, exit: 2, billed: "no",
204
207
  message: "the {tier} tier ({vcpus} vCPU) is over the {limit} vCPU per-sandbox limit of your {plan} plan",
205
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`." },
@@ -266,6 +269,9 @@ export const CLIENT_ERRORS = [
266
269
  { code: "build_failed", category: "env", http: 0, exit: 3, billed: "no",
267
270
  message: "the build could not finish; nothing was stored",
268
271
  fix: "Not charged. Build again; if it repeats, email support@promigence.ai with the build id." },
272
+ { code: "service_restart", category: "env", http: 0, exit: 3, billed: "no",
273
+ message: "a service restart stopped the command while its output was streaming",
274
+ fix: "Not billed: the time the command ran is credited back. The sandbox kept running; the command may have done part of its work before it stopped: run it again if that is safe." },
269
275
  { code: "not_connected", category: "other", http: 0, exit: 7, billed: "no",
270
276
  message: "the connection request ended without connecting anything",
271
277
  fix: "Nothing was connected. Do what the message names, then run the connect command again; `promigence github status` (or `gitlab status`) lists what is connected." },
package/dist/guide.js CHANGED
@@ -5,11 +5,11 @@ export const BILLED_TEXT = {
5
5
  credited: "no: ours, credited back",
6
6
  unless_ours: "the time used is billed, unless the cause is shown to be ours: then it is credited back",
7
7
  };
8
- import { DOCS_URL, EXIT_CODES, EXIT_PRECEDENCE_TEXT, GOLDEN_PATH, GROUPS, GUIDE_COMMON_ERRORS, LISTED_COMMANDS, LIVE_SUMMARY, NOT_LIVE_SUMMARY, SCHEMA, VERSION, WAIT_TIMEOUT_DEFAULT, } from "./surface.js";
8
+ import { DOCS_URL, EXIT_CODES, EXIT_PRECEDENCE_TEXT, GOLDEN_PATH, GROUPS, GUIDE_COMMON_ERRORS, LISTED_COMMANDS, NOT_LIVE_SUMMARY, SCHEMA, VERSION, WAIT_TIMEOUT_DEFAULT, compactNames, } from "./surface.js";
9
9
  export const SECTIONS = ["core", "commands", "errors", "cost"];
10
10
  export const BUDGETS = {
11
- core: 1200,
12
- commands: 4475,
11
+ core: 1500,
12
+ commands: 4500,
13
13
  errors: 2325,
14
14
  cost: 600,
15
15
  };
@@ -20,7 +20,25 @@ export function lookupError(code) {
20
20
  export const ALL_ERROR_CODES = [...ERRORS, ...CLIENT_ERRORS].map((e) => e.code);
21
21
  export const estimateTokens = (s) => Math.ceil(s.length / 4);
22
22
  const exitLine = EXIT_CODES.filter((e) => e.code !== 125 && e.code !== 13).map((e) => `${e.code} ${e.name}`).join(" · ");
23
+ const codesOn = (n) => ERRORS.filter((e) => e.exit === n).map((e) => e.code).join(", ");
24
+ const EXIT_NOTE = {
25
+ 7: codesOn(7),
26
+ 10: `${codesOn(10)} only: retry after Retry-After`,
27
+ 12: `some sandboxes ${codesOn(12)}`,
28
+ 13: "agent-guide --check: files would change",
29
+ 125: "exec --sandbox: ours; else the command's code",
30
+ };
31
+ const exitLegend = EXIT_CODES.map((e) => `${e.code} ${e.name}${EXIT_NOTE[e.code] ? ` (${EXIT_NOTE[e.code]})` : ""}`).join(" · ");
23
32
  const liveNames = new Set(LISTED_COMMANDS.map((c) => c.name));
33
+ const HOSTED_LIVE = compactNames(LISTED_COMMANDS.filter((c) => !c.notOnHosted).map((c) => c.name));
34
+ const NOT_ON_HOSTED = LISTED_COMMANDS.filter((c) => c.notOnHosted);
35
+ const COUNT_WORDS = ["no", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"];
36
+ const exitOf = (code) => {
37
+ const e = ERROR_BY_CODE[code];
38
+ if (!e)
39
+ throw new Error(`the guide names ${code}, which is not a known error code`);
40
+ return e.exit;
41
+ };
24
42
  function renderCore() {
25
43
  const common = GUIDE_COMMON_ERRORS.map((code) => {
26
44
  const e = ERROR_BY_CODE[code];
@@ -63,15 +81,22 @@ function renderCore() {
63
81
  "Progress, prices and diagnostics go to **stderr** — never parse them. `next[].cmd` is",
64
82
  "id-based, runnable verbatim (fill any UPPERCASE word); use it, never invent one.",
65
83
  "",
66
- `Exit codes: ${exitLine}.`,
84
+ `Exit codes: ${exitLegend}.`,
67
85
  `Highest-precedence code wins: ${EXIT_PRECEDENCE_TEXT}.`,
68
86
  "",
69
- "## The six you will actually hit",
87
+ `## The ${COUNT_WORDS[common.length] ?? common.length} you will actually hit`,
70
88
  "",
71
89
  "(`{name}` in a fix is filled from the failure's evidence.)",
72
90
  "",
73
91
  ...common,
74
92
  "",
93
+ "## Limits and stops",
94
+ "",
95
+ `- \`setup_required\` (exit ${exitOf("setup_required")}): answer the account's setup questions in the console first.`,
96
+ `- \`egress_cap_reached\` (exit ${exitOf("egress_cap_reached")}): the month's data out is spent; \`--network none\` still starts; it resets on the 1st (UTC).`,
97
+ "- A paused sandbox is kept 30 days (`pause_expires_at`); resuming keeps it, else it is deleted (`expired`).",
98
+ `- \`stopped_by_support\` (exit ${exitOf("stopped_by_support")}): support stopped it; the message says why.`,
99
+ "",
75
100
  "## Do",
76
101
  "",
77
102
  "- Pass `--cap USD` on every `fork`/`run`: a run over it is refused (exit 5) before anything starts.",
@@ -99,7 +124,10 @@ function renderCore() {
99
124
  `· \`promigence <command> --help\` · ${DOCS_URL}`,
100
125
  "Same surface as MCP tools: `promigence mcp` (stdio; `claude mcp add promigence -- promigence mcp`).",
101
126
  "",
102
- `Live in v${VERSION}: ${LIVE_SUMMARY}`,
127
+ `Live in v${VERSION}: ${HOSTED_LIVE}`,
128
+ ...(NOT_ON_HOSTED.length
129
+ ? [`Not on the hosted service yet: ${compactNames(NOT_ON_HOSTED.map((c) => c.name))} (${[...new Set(NOT_ON_HOSTED.map((c) => c.notOnHosted))].join(", ")}).`]
130
+ : []),
103
131
  ...(NOT_LIVE_SUMMARY ? ["Every other command exits 1 `not_released` in the same envelope."] : []),
104
132
  "",
105
133
  ].join("\n");
@@ -320,7 +348,7 @@ export function renderLlmsTxt() {
320
348
  "",
321
349
  "## Index",
322
350
  "",
323
- ...LISTED_COMMANDS.map((c) => `- \`promigence ${c.name}\` — ${c.summary}`),
351
+ ...LISTED_COMMANDS.map((c) => `- \`promigence ${c.name}\` — ${c.summary}${c.notOnHosted ? ` (not on the hosted service yet)` : ""}`),
324
352
  "",
325
353
  "- `promigence agent-guide` — this document's first section",
326
354
  "- `promigence agent-guide --section commands` — every command with its flags",
package/dist/handlers.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { existsSync, statSync, writeFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
- import { BUNDLE_EXT, DEFAULT_SNAPSHOT, PromigenceError, VERSION as SDK_VERSION, configDir, credentialsPath, compatApiKey, platformOf, readCredentialsWithStore, renderBundle, verifyBundle, } from "./sdk/index.js";
3
+ import { BUNDLE_EXT, DEFAULT_SNAPSHOT, PromigenceError, VERSION as SDK_VERSION, configDir, credentialsPath, platformOf, readCredentialsWithStore, renderBundle, verifyBundle, } from "./sdk/index.js";
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";
@@ -30,7 +30,7 @@ const agentGuide = async (ctx) => {
30
30
  const skipped = res.artifacts.find((a) => a.action === "skipped");
31
31
  const next = nexts(...(skipped
32
32
  ? [["promigence agent-guide --install --create", "create AGENTS.md and write the fence into it"]]
33
- : []), ["promigence agent-guide", "the guide itself (~1.2k tokens)"], ["promigence examples", "what you can fork with no build (the built-in base)"]);
33
+ : []), ["promigence agent-guide", "the guide itself (~1.4k tokens)"], ["promigence examples", "what you can fork with no build (the built-in base)"]);
34
34
  const env = okEnvelope(ctx.cmd.name, {
35
35
  mode: checking ? "check" : "install",
36
36
  dir: res.dir,
@@ -259,9 +259,6 @@ function keySource(ctx) {
259
259
  if (creds.api_key) {
260
260
  return { source: store === "file" || store === undefined ? credentialsPath() : `${storeLabel(store)} (item ${credentialsPath()})`, present: true };
261
261
  }
262
- if (compatApiKey(process.env["E2B_API_KEY"])) {
263
- return { source: "E2B_API_KEY (compatibility)", present: true };
264
- }
265
262
  return { source: "none", present: false };
266
263
  }
267
264
  async function clientChecks(ctx) {
package/dist/live.js CHANGED
@@ -8,6 +8,7 @@ const FOLLOW_WAIT_MS = 1_000;
8
8
  const FOLLOW_POLL_MS = 250;
9
9
  const pause = (ms) => new Promise((r) => { setTimeout(r, ms); });
10
10
  const sandboxFlag = (ctx) => required(ctx, "--sandbox", `Pass --sandbox sbx_<id> (from \`promigence run report <run_id>\`), e.g. promigence ${ctx.cmd.name} … --sandbox sbx_01.`);
11
+ export const RECONNECTED_LINE = "the service restarted; reconnected to a new shell";
11
12
  const sandboxShell = async (ctx) => {
12
13
  const sid = positional(ctx, "a sandbox id", "promigence sandbox shell sbx_01");
13
14
  const tail = ctx.cmdTail.length ? ctx.cmdTail.map(shellQuote).join(" ") : undefined;
@@ -45,6 +46,13 @@ const sandboxShell = async (ctx) => {
45
46
  ...(cmd === undefined ? {} : { cmd }),
46
47
  ...(secrets.length ? { secrets } : {}),
47
48
  onData: (d) => { stdout.write(d); },
49
+ reconnect: inner === undefined,
50
+ onReconnected: ({ resumed }) => {
51
+ if (resumed)
52
+ return;
53
+ const line = RECONNECTED_LINE;
54
+ void write(process.stderr, process.stderr.isTTY ? `\r\n\x1b[2m${line}\x1b[0m\r\n` : `promigence sandbox shell: ${line}\n`);
55
+ },
48
56
  });
49
57
  const restore = () => {
50
58
  if (tty && stdin.isRaw)
package/dist/login.js CHANGED
@@ -121,7 +121,7 @@ function apiUrlSource() {
121
121
  return "PROMIGENCE_API_URL";
122
122
  if (readCredentials().api_url)
123
123
  return `the api_url in ${credentialsPath()}`;
124
- return "E2B_API_URL / E2B_DOMAIN";
124
+ return "the stored sign-in";
125
125
  }
126
126
  async function ask(prompt) {
127
127
  const rl = createInterface({ input: process.stdin, output: process.stderr });
package/dist/mcp.js CHANGED
@@ -688,8 +688,11 @@ const mcp = async (ctx) => {
688
688
  });
689
689
  let cached;
690
690
  let cachedFrom = "";
691
+ const keyGiven = ctx.apiKey !== undefined || Boolean(process.env["PROMIGENCE_API_KEY"]?.trim());
691
692
  const session = {
692
693
  client: () => {
694
+ if (keyGiven)
695
+ return (cached ??= freshClient());
693
696
  forgetKeychainReads();
694
697
  const from = JSON.stringify(readCredentials());
695
698
  if (cached && from === cachedFrom)
package/dist/routes.js CHANGED
@@ -42,6 +42,7 @@ export const ROUTE_COMMANDS = {
42
42
  "GET /orgs/:id/audit": "org audit",
43
43
  "GET /pricing": "pricing",
44
44
  "GET /billing": "billing usage",
45
+ "GET /billing/history": "billing history",
45
46
  "GET /me/ledger": "billing usage",
46
47
  "GET /me/outcomes": "me",
47
48
  "POST /billing/checkout": "billing card",
@@ -748,6 +748,8 @@ export interface CreditOffer {
748
748
  state: string;
749
749
  credit_usd: number;
750
750
  card_bonus_usd: number;
751
+ /** USD charged to confirm Apple Pay, Google Pay or Amazon Pay when one is added (added to the credit). */
752
+ method_check_usd?: number;
751
753
  expires_at?: string | null;
752
754
  campaign?: string | null;
753
755
  message: string;
@@ -906,6 +908,16 @@ export interface BillingUsage {
906
908
  brand?: string;
907
909
  last4?: string;
908
910
  } | null;
911
+ /** Set when the card on file does not unlock paid limits for this organization (it already pays for another one,
912
+ * it is being confirmed with a $1 charge, or that charge did not go through). */
913
+ card_note?: string | null;
914
+ /** The $1 that confirms Apple Pay, Google Pay or Amazon Pay on file: `confirming`, `paid` or `failed`. */
915
+ method_check?: {
916
+ method: string;
917
+ status: string;
918
+ amount_usd: number;
919
+ bonus_usd?: number | null;
920
+ } | null;
909
921
  suspended?: boolean;
910
922
  suspend_reason?: string | null;
911
923
  period?: {
@@ -929,6 +941,10 @@ export interface BillingUsage {
929
941
  upcoming_invoice_error?: string | null;
930
942
  [extra: string]: unknown;
931
943
  }
944
+ /** One row of `GET /billing/history`: a credit the organization received, or a payment. */
945
+ export type BillingHistoryEntry = M.BillingHistoryEntry;
946
+ /** What `billing.historyPage()` lists: everything, only credits, or only payments. */
947
+ export type BillingHistoryKind = "all" | "credits" | "payments";
932
948
  /** One metered row of `GET /me/ledger`. */
933
949
  export interface LedgerEntry {
934
950
  entry_id: string;
@@ -2033,6 +2049,16 @@ export declare class PromigenceClient {
2033
2049
  limit?: number;
2034
2050
  cursor?: string;
2035
2051
  }) => Promise<Page<LedgerEntry>>;
2052
+ /**
2053
+ * `GET /billing/history` — the credits the organization received and its payments, newest
2054
+ * first (one page). `kind` narrows it to `credits` or `payments`; feed `nextCursor` back as
2055
+ * `cursor` until it is `null`. An owner's key.
2056
+ */
2057
+ historyPage: (opts?: {
2058
+ kind?: BillingHistoryKind;
2059
+ limit?: number;
2060
+ cursor?: string;
2061
+ }) => Promise<Page<BillingHistoryEntry>>;
2036
2062
  /** `POST /billing/checkout` — the hosted page that puts a card on file. */
2037
2063
  checkout: () => Promise<{
2038
2064
  url: string;
@@ -2433,6 +2459,18 @@ export declare class PromigenceClient {
2433
2459
  onData?: (data: Uint8Array) => void;
2434
2460
  onExit?: (exit: PtyExit) => void;
2435
2461
  timeoutMs?: number;
2462
+ /** Pick the same terminal up again when its connection drops or the service is updated;
2463
+ * see `PtyConnectOpts.resume`. Default: on. */
2464
+ resume?: boolean;
2465
+ /** Reconnect in a new shell when the service is updated (a WebSocket `Close` 1012) and the
2466
+ * terminal cannot be picked up again; see `PtyConnectOpts.reconnect`. Default: on without
2467
+ * `cmd`, off with one. */
2468
+ reconnect?: boolean;
2469
+ /** Called after each reconnect: `resumed` says whether it is the same terminal or a new
2470
+ * shell. */
2471
+ onReconnected?: (how: {
2472
+ resumed: boolean;
2473
+ }) => void;
2436
2474
  }) => Promise<PtySession>;
2437
2475
  /** `GET /sandboxes/:id`. */
2438
2476
  get: (sandboxId: string) => Promise<SandboxRecord>;
@@ -2,7 +2,7 @@ import { readFileSync, statSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { inspect } from "node:util";
4
4
  import { dirname, join } from "node:path";
5
- import { ApiError, AuthError, EnvFailedError, PromigenceError, InvalidArgumentError, NotFoundError, NotReleasedError, NotVerifiedError, RateLimitError, SpendCapError, TimeoutError, UnreachableError, DEFAULT_API_URL, ERRORS_DOCS_URL, FILE_PUT_CHUNK_BYTES, MAX_OUTPUT_BYTES_DEFAULT, VERSION, WAIT_TIMEOUT_MS, PreviewsUnavailableError, exitForCode, previewsOff, withPreviewsFix, } from "./core.js";
5
+ import { ApiError, AuthError, EnvFailedError, PromigenceError, InvalidArgumentError, NotFoundError, NotReleasedError, NotVerifiedError, RateLimitError, ServiceRestartError, SpendCapError, TimeoutError, UnreachableError, DEFAULT_API_URL, ERRORS_DOCS_URL, FILE_PUT_CHUNK_BYTES, MAX_OUTPUT_BYTES_DEFAULT, VERSION, WAIT_TIMEOUT_MS, PreviewsUnavailableError, exitForCode, previewsOff, withPreviewsFix, } from "./core.js";
6
6
  import { buildBundle } from "./bundle.js";
7
7
  import { keychainReadCached, keychainStore, storeLabel } from "./credstore.js";
8
8
  import { CALLER_HEADER, resolveCaller, userAgentWithCaller } from "./caller.js";
@@ -135,7 +135,7 @@ function decodeCredentials(buf) {
135
135
  return buf.subarray(3).toString("utf8");
136
136
  return buf.toString("utf8");
137
137
  }
138
- export function storedCredentials(env = process.env) {
138
+ export function storedCredentials(env = process.env, platform = process.platform) {
139
139
  const path = credentialsPath(env);
140
140
  let present = true;
141
141
  try {
@@ -145,7 +145,7 @@ export function storedCredentials(env = process.env) {
145
145
  present = e.code !== "ENOENT";
146
146
  }
147
147
  if (!present) {
148
- const store = keychainStore(env);
148
+ const store = keychainStore(env, platform);
149
149
  if (store === undefined)
150
150
  return undefined;
151
151
  const r = keychainReadCached(store, path, env);
@@ -259,8 +259,6 @@ export function keyOrigin(env = process.env) {
259
259
  const stored = readCredentialsWithStore(env);
260
260
  if (stored.creds.api_key)
261
261
  return stored.store === "macos-keychain" ? "macOS Keychain" : stored.store === "secret-service" ? "Secret Service keyring" : "credentials file";
262
- if (compatApiKey(env["E2B_API_KEY"]))
263
- return "E2B_API_KEY (compat)";
264
262
  return undefined;
265
263
  }
266
264
  function resolveCredentials(opts, env = process.env) {
@@ -269,17 +267,19 @@ function resolveCredentials(opts, env = process.env) {
269
267
  const t = u.replace(/\/+$/, "");
270
268
  return /^https?:\/\/[^/?#]+$/i.test(t) ? `${t}/v1` : t;
271
269
  };
272
- const { creds, store } = readCredentialsWithStore(env);
273
270
  const envKey = set(env["PROMIGENCE_API_KEY"]?.trim());
274
271
  const envUrl = set(env["PROMIGENCE_API_URL"]);
275
272
  const optKey = opts.apiKey?.trim();
273
+ const given = optKey !== undefined || envKey !== undefined;
274
+ const { creds, store } = given
275
+ ? { creds: {}, store: undefined }
276
+ : readCredentialsWithStore(env);
276
277
  const source = optKey !== undefined ? "opts" : envKey !== undefined ? "env" : creds.api_key !== undefined ? "file" : undefined;
277
278
  const ours = optKey ?? envKey ?? creds.api_key;
278
- const compat = ours === undefined ? compatApiKey(env["E2B_API_KEY"]) : undefined;
279
- const compatUrl = compat === undefined
280
- ? undefined
281
- : apiUrlFromCompatUrl(env["E2B_API_URL"]) ?? apiUrlFromCompatDomain(env["E2B_DOMAIN"]);
282
- const fromFile = trim(creds.api_url ?? compatUrl ?? DEFAULT_API_URL);
279
+ const fallback = ours === undefined ? opts.fallbackKey : undefined;
280
+ const fallbackKey = set(fallback?.apiKey.trim());
281
+ const fallbackUrl = fallbackKey === undefined ? undefined : set(fallback?.apiUrl);
282
+ const fromFile = trim(creds.api_url ?? fallbackUrl ?? DEFAULT_API_URL);
283
283
  const fromEnv = envUrl !== undefined ? trim(envUrl) : fromFile;
284
284
  const apiUrl = opts.apiUrl !== undefined ? trim(opts.apiUrl) : fromEnv;
285
285
  const paired = source === "file" ? fromFile : source === "env" ? fromEnv : apiUrl;
@@ -291,10 +291,10 @@ function resolveCredentials(opts, env = process.env) {
291
291
  : "Pass apiKey together with apiUrl, or set PROMIGENCE_API_URL to that URL instead of passing apiUrl.");
292
292
  return { apiKey: undefined, apiUrl, refusal };
293
293
  }
294
- const key = ours ?? compat;
294
+ const key = ours ?? fallbackKey;
295
295
  if (key) {
296
296
  const from = source === "opts" ? "the apiKey option" : source === "env" ? "PROMIGENCE_API_KEY"
297
- : source === "file" ? storedName(store, env) : "E2B_API_KEY";
297
+ : source === "file" ? storedName(store, env) : fallback.from;
298
298
  const refusal = keyRefusal(key, from);
299
299
  if (refusal)
300
300
  return { apiKey: undefined, apiUrl, refusal };
@@ -652,6 +652,7 @@ export function errorFor(code, message, fix, extra = {}) {
652
652
  case "invalid_argument":
653
653
  return attach(new InvalidArgumentError(message, extra.cli ? fix : withoutCliHelp(fix), code));
654
654
  case "not_released": return attach(new NotReleasedError(message));
655
+ case "service_restart": return attach(new ServiceRestartError(message, fix));
655
656
  default:
656
657
  if (ENV_CODES.has(code) || extra.category === "env")
657
658
  return attach(new EnvFailedError(code, message, fix));
@@ -1258,6 +1259,14 @@ export class PromigenceClient {
1258
1259
  const r = await this.request({ method: "GET", path: `/me/ledger?${pageQuery(limit, 1000, opts.cursor)}` });
1259
1260
  return pageOf(r, "entries", limit);
1260
1261
  },
1262
+ historyPage: async (opts = {}) => {
1263
+ const limit = opts.limit ?? 50;
1264
+ const q = new URLSearchParams(pageQuery(limit, 100, opts.cursor));
1265
+ if (opts.kind !== undefined && opts.kind !== "all")
1266
+ q.set("kind", opts.kind);
1267
+ const r = await this.request({ method: "GET", path: `/billing/history?${q.toString()}` });
1268
+ return pageOf(r, "entries", limit);
1269
+ },
1261
1270
  checkout: () => this.request({ method: "POST", path: "/billing/checkout", body: {} }),
1262
1271
  portal: () => this.request({ method: "POST", path: "/billing/portal", body: {} }),
1263
1272
  referrals: () => this.request({ method: "GET", path: "/referrals" }),
@@ -1752,6 +1761,10 @@ export class PromigenceClient {
1752
1761
  on.heartbeat?.();
1753
1762
  continue;
1754
1763
  }
1764
+ if (event === "reconnect") {
1765
+ reader.cancel().catch(() => { });
1766
+ throw new ServiceRestartError(`a service restart stopped the command on sandbox ${sandboxId} while its output was streaming`, "Not billed: the time the command ran is credited back. The sandbox kept running; the command may have done part of its work before it stopped: run it again if that is safe.");
1767
+ }
1755
1768
  let parsed = {};
1756
1769
  try {
1757
1770
  parsed = JSON.parse(data);
@@ -1987,6 +2000,9 @@ export class PromigenceClient {
1987
2000
  ...(opts.onData ? { onData: opts.onData } : {}),
1988
2001
  ...(opts.onExit ? { onExit: opts.onExit } : {}),
1989
2002
  ...(opts.timeoutMs === undefined ? {} : { timeoutMs: opts.timeoutMs }),
2003
+ ...(opts.resume === undefined ? {} : { resume: opts.resume }),
2004
+ ...(opts.reconnect === undefined ? {} : { reconnect: opts.reconnect }),
2005
+ ...(opts.onReconnected ? { onReconnected: opts.onReconnected } : {}),
1990
2006
  refuse: (status, text, retryAfter) => {
1991
2007
  let payload = {};
1992
2008
  try {
@@ -1,5 +1,5 @@
1
1
  /** Promigence SDK — constants, error hierarchy and shared types. */
2
- export declare const VERSION = "0.1.0-beta.7";
2
+ export declare const VERSION = "0.1.0-beta.8";
3
3
  export declare const DOCS_URL = "https://promigence.ai";
4
4
  /** The error reference; each code is an anchor on it (the API's `docs` link has the same form). */
5
5
  export declare const ERRORS_DOCS_URL = "https://www.promigence.ai/docs/errors";
@@ -190,6 +190,15 @@ export declare class TimeoutError extends PromigenceError {
190
190
  runId?: string;
191
191
  });
192
192
  }
193
+ /**
194
+ * A service update may stop a command that was running while its output was streaming to you
195
+ * (`execStream`, `commands.run` with output callbacks); the time it ran is credited back, and the
196
+ * sandbox keeps running. `retryable` is `false`: the command may have done part of its work before
197
+ * it stopped, so run it again only if that is safe. exit 3
198
+ */
199
+ export declare class ServiceRestartError extends PromigenceError {
200
+ constructor(message: string, fix: string);
201
+ }
193
202
  /** An API error with no more specific class (5xx, unknown code). exit from the table. */
194
203
  export declare class ApiError extends PromigenceError {
195
204
  }
package/dist/sdk/core.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { hintArg } from "./hint.js";
2
- export const VERSION = "0.1.0-beta.7";
2
+ export const VERSION = "0.1.0-beta.8";
3
3
  export const DOCS_URL = "https://promigence.ai";
4
4
  export const ERRORS_DOCS_URL = "https://www.promigence.ai/docs/errors";
5
5
  export const DEFAULT_API_URL = "https://api.promigence.ai/v1";
@@ -194,6 +194,11 @@ export class TimeoutError extends PromigenceError {
194
194
  this.runId = ids.runId;
195
195
  }
196
196
  }
197
+ export class ServiceRestartError extends PromigenceError {
198
+ constructor(message, fix) {
199
+ super({ code: "service_restart", message, fix, exitCode: 3, category: "env", retryable: false });
200
+ }
201
+ }
197
202
  export class ApiError extends PromigenceError {
198
203
  }
199
204
  export class NotReleasedError extends PromigenceError {
@@ -210,6 +215,7 @@ export const EXIT_BY_CODE = {
210
215
  invalid_argument: 2, unsupported: 2, not_released: 1, task_failed: 4,
211
216
  env_failed: 3, snapshot_rejected: 6, build_failed: 3, snapshot_failed: 4,
212
217
  not_connected: 7,
218
+ service_restart: 3,
213
219
  fork_failed: 3, start_timeout: 3, readiness_failed: 3, verify_too_slow: 3,
214
220
  verify_each_failed: 3, toolchain_mismatch: 3, sandbox_unresponsive: 3, oom_within_tier: 3,
215
221
  disk_full_within_tier: 3, sandbox_interrupted: 3, integrity_mismatch: 3, egress_unavailable: 3,
@@ -236,10 +242,11 @@ export const EXIT_BY_CODE = {
236
242
  build_timeout: 4,
237
243
  image_unresolvable: 2,
238
244
  registry_unavailable: 3,
239
- trial_credits_exhausted: 5, card_required: 5, credit_unclaimed: 5, concurrency_limit: 5, egress_cap_reached: 10,
245
+ trial_credits_exhausted: 5, card_required: 5, credit_unclaimed: 5, concurrency_limit: 5, egress_cap_reached: 5,
240
246
  billing_unavailable: 5,
241
247
  project_cap_reached: 5,
242
248
  spend_limit_reached: 5,
249
+ billing_hold: 5,
243
250
  tier_over_plan: 2,
244
251
  network_host_refused: 9, network_policy_unsupported: 3,
245
252
  secrets_in_snapshot: 2, secrets_unavailable: 3,
@@ -3,7 +3,8 @@
3
3
  * the credentials file (`~/.config/promigence/credentials`, 0600). The CLI writes it
4
4
  * (`promigence login`, `promigence auth login`, `promigence logout`); the CLI and both SDKs read it.
5
5
  *
6
- * macOS the login Keychain, through `security` (when HOME holds one)
6
+ * macOS the login Keychain, through `security` (when the user's home holds one: HOME,
7
+ * or the account's own home when the environment has no HOME)
7
8
  * Linux and BSD the Secret Service (GNOME Keyring, KWallet), through `secret-tool`, when a
8
9
  * desktop session bus is there (a CI job or an SSH shell has none: the file)
9
10
  * Windows the file: no tool that ships with Windows can read a stored secret back
@@ -34,7 +35,7 @@ export declare function storeLabel(store: CredentialStore, path?: string): strin
34
35
  * The keychain this process stores a sign-in in, or undefined for the file. `auto` is decided by
35
36
  * the platform and by whether its tool (and, for the Secret Service, a session bus) is there.
36
37
  */
37
- export declare function keychainStore(env?: NodeJS.ProcessEnv, platform?: string): KeychainStore | undefined;
38
+ export declare function keychainStore(env?: NodeJS.ProcessEnv, platform?: string, home?: () => string): KeychainStore | undefined;
38
39
  /**
39
40
  * The keychains a sign-out (or a sign-in that lands in the file) must clear for this path: the one
40
41
  * in use, and the platform's own even when `PROMIGENCE_CREDENTIAL_STORE=file` (an item a previous