@promigence/sdk 0.1.0-beta.6 → 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/dist/client.d.ts CHANGED
@@ -253,6 +253,11 @@ export interface Totals {
253
253
  grader_failed?: number;
254
254
  snapshot_failed: number;
255
255
  aborted_by_client: number;
256
+ /**
257
+ * Sandboxes Promigence support stopped (outcome `stopped_by_support`), each with its reason in its
258
+ * failure's message. Never counted as your own kill (`aborted_by_client`). Absent from an older API.
259
+ */
260
+ stopped_by_support?: number;
256
261
  expired: number;
257
262
  cap_killed: number;
258
263
  not_started: number;
@@ -349,7 +354,9 @@ export interface SecretRecord {
349
354
  name: string;
350
355
  /** Length of the stored value, so a caller can sanity-check without reading it. */
351
356
  bytes: number;
357
+ /** On `secrets.set` only: `false` when an existing secret of that name was overwritten. */
352
358
  created?: boolean;
359
+ /** On `secrets.list` only. */
353
360
  created_at?: string;
354
361
  updated_at?: string;
355
362
  /**
@@ -370,7 +377,7 @@ export interface SecretRecord {
370
377
  * `api.anthropic.com`. Absent when there is none. A variable the caller sets itself wins.
371
378
  */
372
379
  env?: Record<string, string>;
373
- /** The request headers the secret authenticates (present when `hosts` is). */
380
+ /** The request headers the secret authenticates: on `secrets.list`, when `hosts` is present. */
374
381
  headers?: string[];
375
382
  /** What the variable in a sandbox that names it holds instead of the value (on `set` of a bound secret). */
376
383
  placeholder?: string;
@@ -467,13 +474,17 @@ export interface SnapshotRecord {
467
474
  source?: SnapshotSource;
468
475
  /** `false` for an image snapshot built without `verify` — stored, but nothing was verified. */
469
476
  verified?: boolean;
470
- start?: string | string[];
471
477
  created_at?: string;
472
478
  /**
473
479
  * Set when the snapshot can no longer be started, saying why (it was lost before it could be
474
480
  * kept); a fork of it is refused `snapshot_unavailable`. Absent while it can be started.
475
481
  */
476
482
  unavailable?: string;
483
+ /**
484
+ * The user commands, terminals and SSH sessions in its sandboxes run as when they name none:
485
+ * its image's own `USER`, or the `user` it was created with. Absent: root.
486
+ */
487
+ user?: string | null;
477
488
  [extra: string]: unknown;
478
489
  }
479
490
  /** `GET /snapshots/:id/manifest`: the snapshot's manifest, sealed (`buildBundle` turns it into a `.promigence` bundle). */
@@ -510,8 +521,6 @@ export interface ExecResponse {
510
521
  stdout: string;
511
522
  stderr: string;
512
523
  truncated?: boolean;
513
- stdout_ref?: string;
514
- stderr_ref?: string;
515
524
  /** True when the command was killed at its own timeout. */
516
525
  timed_out?: boolean;
517
526
  /** True when the command was killed for running out of memory. */
@@ -539,7 +548,13 @@ export interface SandboxRecord extends QueuedInfo {
539
548
  */
540
549
  pause_reason?: "spend_cap" | "credits_exhausted";
541
550
  run_id?: string;
551
+ /**
552
+ * The snapshot it was started from, as its run names it (`runs.get(runId).snapshot_id`): the
553
+ * snapshot's id, or `promigence/base` for the built-in base.
554
+ */
542
555
  snapshot_id?: string;
556
+ /** When the sandbox was created, RFC 3339. */
557
+ created_at?: string;
543
558
  outcome?: Outcome;
544
559
  /**
545
560
  * The labels this sandbox inherited from its run — the same map `runs.start({ labels })` took.
@@ -627,7 +642,7 @@ export interface Page<T> {
627
642
  /** What the server actually used, which is not always what was asked for (it caps at 500). */
628
643
  limit: number;
629
644
  }
630
- /** `GET /health` — unauthenticated: `ok`, `version`, `build_sha` and `serving`. */
645
+ /** `GET /health` — unauthenticated: `ok`, `version`, `serving` and `location`; `build_sha` to a caller with a key. */
631
646
  export interface HealthResponse {
632
647
  ok?: boolean;
633
648
  version?: string;
@@ -733,6 +748,8 @@ export interface CreditOffer {
733
748
  state: string;
734
749
  credit_usd: number;
735
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;
736
753
  expires_at?: string | null;
737
754
  campaign?: string | null;
738
755
  message: string;
@@ -891,6 +908,16 @@ export interface BillingUsage {
891
908
  brand?: string;
892
909
  last4?: string;
893
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;
894
921
  suspended?: boolean;
895
922
  suspend_reason?: string | null;
896
923
  period?: {
@@ -914,6 +941,10 @@ export interface BillingUsage {
914
941
  upcoming_invoice_error?: string | null;
915
942
  [extra: string]: unknown;
916
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";
917
948
  /** One metered row of `GET /me/ledger`. */
918
949
  export interface LedgerEntry {
919
950
  entry_id: string;
@@ -1184,9 +1215,15 @@ export interface SearchOpts {
1184
1215
  * `POST /sandboxes/:id/files/archive` — where the tar landed **inside the sandbox** and how big
1185
1216
  * it is. The bytes stay in the sandbox on purpose; see `files.downloadDir`.
1186
1217
  */
1187
- export interface ArchiveResult extends FileInfo {
1218
+ export interface ArchiveResult {
1219
+ sandbox_id: string;
1220
+ /** Where the tar landed inside the sandbox. */
1188
1221
  archive_path: string;
1222
+ /** Its size in bytes. */
1223
+ size: number;
1224
+ /** The directory that was packed. */
1189
1225
  source: string;
1226
+ served_by?: ServedBy;
1190
1227
  }
1191
1228
  /** `POST /sandboxes/:id/files/extract`. `entries` is `null` on the fallback path, which cannot
1192
1229
  * count them without a second pass — a number invented there would be worse than none. */
@@ -1312,8 +1349,6 @@ export interface MeResponse {
1312
1349
  email?: string;
1313
1350
  provider?: string;
1314
1351
  } | null;
1315
- /** @deprecated Never sent: the key is `key.key_id`. */
1316
- key_id?: string;
1317
1352
  /**
1318
1353
  * The plan the organisation's limits and prices come from: `hobby` for a trial organisation
1319
1354
  * with no plan assigned. The name `tier_over_plan` and the other refusals use.
@@ -1349,7 +1384,6 @@ export interface MeResponse {
1349
1384
  /** The cap a run gets when it names none, in USD; `0` names none (the run is capped at its quote). */
1350
1385
  default_spend_cap?: number;
1351
1386
  card_on_file?: boolean;
1352
- default_class?: string;
1353
1387
  /** The free trial: credits, data out and the limits that apply until a card is on file. */
1354
1388
  trial?: TrialStatus;
1355
1389
  /** Something about the account to act on (the card-required switch), or null. */
@@ -1661,6 +1695,18 @@ export interface SnapshotCreateOpts {
1661
1695
  tier?: Tier;
1662
1696
  /** Image forms: the name of a stored secret holding the registry credentials. */
1663
1697
  registrySecret?: string;
1698
+ /**
1699
+ * Image and Dockerfile forms: the user commands, terminals and SSH sessions in its sandboxes
1700
+ * run as when they name none. A user name or uid, with an optional `:group` or `:gid`, as an
1701
+ * image's `USER` takes it; `root` for root. Default: the image's own `USER`, as `docker run`
1702
+ * does (root when it sets none).
1703
+ */
1704
+ user?: string;
1705
+ /**
1706
+ * Image and Dockerfile forms: the user its `start`, `setup` and `verify` run as (for example
1707
+ * the snapshot's own `user`). Default: `root`.
1708
+ */
1709
+ setupUser?: string;
1664
1710
  alias?: string;
1665
1711
  /**
1666
1712
  * A DEFAULT grader for this snapshot: a run on it with no `grade` of its own uses this one.
@@ -1951,12 +1997,15 @@ export declare class PromigenceClient {
1951
1997
  timeoutMs?: number;
1952
1998
  }): Promise<StatusResponse>;
1953
1999
  /**
1954
- * `GET /pricing` — the price list, live: the per-second rates, every sandbox shape on sale and
1955
- * its price, the build price, the limits, the free credit and the plan they come from, read on
1956
- * every call from what billing charges. With a key: what this key's organization pays. Without
1957
- * one, or with `newAccount: true` (no key is sent): what a new account gets.
2000
+ * `GET /pricing` — your organization's price list, live: the per-second rates, every sandbox
2001
+ * shape on sale and its price, the build price, the limits, the free credit and the plan they
2002
+ * come from, read on every call from what billing charges. Needs a key: without one this throws
2003
+ * `AuthError` and sends nothing. The published list prices are on the pricing page.
1958
2004
  *
1959
2005
  * Prices change: read them when you need them rather than keeping a copy.
2006
+ *
2007
+ * @param opts.newAccount No longer supported: there is no key-less price list. Passing `true`
2008
+ * throws `InvalidArgumentError` and sends nothing.
1960
2009
  */
1961
2010
  pricing(opts?: {
1962
2011
  newAccount?: boolean;
@@ -2000,6 +2049,16 @@ export declare class PromigenceClient {
2000
2049
  limit?: number;
2001
2050
  cursor?: string;
2002
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>>;
2003
2062
  /** `POST /billing/checkout` — the hosted page that puts a card on file. */
2004
2063
  checkout: () => Promise<{
2005
2064
  url: string;
@@ -2400,6 +2459,18 @@ export declare class PromigenceClient {
2400
2459
  onData?: (data: Uint8Array) => void;
2401
2460
  onExit?: (exit: PtyExit) => void;
2402
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;
2403
2474
  }) => Promise<PtySession>;
2404
2475
  /** `GET /sandboxes/:id`. */
2405
2476
  get: (sandboxId: string) => Promise<SandboxRecord>;
@@ -2475,6 +2546,12 @@ export declare class PromigenceClient {
2475
2546
  /**
2476
2547
  * `POST /sandboxes/:id/probe` — is this environment still the one that was verified?
2477
2548
  *
2549
+ * Compares the sandbox's files under the paths its snapshot's verification recorded with what
2550
+ * was recorded. It is the same check a failed run gets, which decides whether the failure was
2551
+ * the environment's (`integrity_mismatch`, not billed) or a path the snapshot's own `verify`
2552
+ * never covered (`verify_did_not_cover`, billed). Asking for it up front is cheaper than
2553
+ * spending a run to find out.
2554
+ *
2478
2555
  * `first_bad_path` names the **first differing path**, which is the only part of a failed
2479
2556
  * probe anyone can act on.
2480
2557
  */
@@ -2485,8 +2562,7 @@ export declare class PromigenceClient {
2485
2562
  ok: boolean;
2486
2563
  ms: number;
2487
2564
  checked: string | string[];
2488
- first_bad_path: string | null;
2489
- probe: Record<string, unknown>;
2565
+ first_bad_path?: string | null;
2490
2566
  }>;
2491
2567
  /** `DELETE /sandboxes/:id`. */
2492
2568
  kill: (sandboxId: string) => Promise<unknown>;
@@ -2623,9 +2699,11 @@ export declare class PromigenceClient {
2623
2699
  /**
2624
2700
  * Write a file of **any size**, in [`FILE_PUT_CHUNK_BYTES`] requests.
2625
2701
  *
2626
- * A failure mid-loop leaves a **prefix** in the sandbox, not a corrupt file, and the
2627
- * thrown error carries how far it got (`written` in the error's evidence).
2628
- * Re-running the same `put` is safe: chunk 0 truncates.
2702
+ * Chunk 0 goes without `?offset=`, which means "this is the file" (create, truncate);
2703
+ * every later chunk carries one, which appends, so a file has no size limit here.
2704
+ *
2705
+ * A failure mid-loop leaves a **prefix** in the sandbox, not a corrupt file. Re-running the
2706
+ * same `put` is safe: chunk 0 truncates.
2629
2707
  *
2630
2708
  * `mode` (`0o755`) sets the file's permission bits. Without it a file that already exists
2631
2709
  * keeps its mode, as a shell `>` does, and a new one is `0o644`.
@@ -2735,8 +2813,7 @@ export declare class PromigenceClient {
2735
2813
  ok: boolean;
2736
2814
  ms: number;
2737
2815
  checked: string | string[];
2738
- first_bad_path: string | null;
2739
- probe: Record<string, unknown>;
2816
+ first_bad_path?: string | null;
2740
2817
  }>;
2741
2818
  };
2742
2819
  /**
@@ -2883,15 +2960,14 @@ export declare class PromigenceClient {
2883
2960
  manifest: (idOrAlias: string) => Promise<SnapshotManifestResponse>;
2884
2961
  /**
2885
2962
  * The `.promigence` bundle for a snapshot: the sealed form, which names exactly one
2886
- * environment and verifies offline (`verifyBundle`). `served_by` says whether the API wrote it
2887
- * (`control`) or this client composed it from the snapshot's manifest (`client`); both are the
2888
- * same document.
2963
+ * environment and verifies offline (`verifyBundle`). This client composes it from the
2964
+ * snapshot's sealed manifest (`served_by: "client"`); the API serves no bundle of its own.
2889
2965
  */
2890
2966
  bundle: (idOrAlias: string, opts?: {
2891
2967
  alias?: string;
2892
2968
  }) => Promise<{
2893
2969
  bundle: Bundle;
2894
- served_by: "control" | "client";
2970
+ served_by: "client";
2895
2971
  record?: SnapshotRecord;
2896
2972
  }>;
2897
2973
  };
package/dist/client.js CHANGED
@@ -2,8 +2,8 @@ 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";
6
- import { BUNDLE_SCHEMA, buildBundle } from "./bundle.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
+ import { buildBundle } from "./bundle.js";
7
7
  import { keychainReadCached, keychainStore, storeLabel } from "./credstore.js";
8
8
  import { CALLER_HEADER, resolveCaller, userAgentWithCaller } from "./caller.js";
9
9
  import { connectPty } from "./pty.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 };
@@ -507,6 +507,12 @@ export function snapshotCreateBody(opts) {
507
507
  if (o.env !== undefined && Object.keys(o.env).length && forms[0] === "repo") {
508
508
  throw new InvalidArgumentError("snapshots.create env is for an image or a Dockerfile", "A repository snapshot sets its environment in its setup steps, or pass env on each command.");
509
509
  }
510
+ if ((o.user !== undefined || o.setupUser !== undefined) && forms[0] === "repo") {
511
+ throw new InvalidArgumentError("snapshots.create user and setupUser are for an image or a Dockerfile", "A repository snapshot runs as root; name a user on each command instead ({ user: 'app' }).");
512
+ }
513
+ if (o.setupUser !== undefined && o.user === undefined) {
514
+ throw new InvalidArgumentError("snapshots.create setupUser needs user", "Pass { user: 'app', setupUser: 'app' }: start, setup and verify as app too (without setupUser they run as root), every later command as app.");
515
+ }
510
516
  if (!github && (o.ref !== undefined || o.from !== undefined || o.track)) {
511
517
  throw new InvalidArgumentError("snapshots.create ref, from and track are for a connected repository (repo: 'github:owner/name' or 'gitlab:group/project')", "Pass { repo: 'github:your-org/app', ref: 'main' }, or drop them for this form.");
512
518
  }
@@ -516,7 +522,7 @@ export function snapshotCreateBody(opts) {
516
522
  setup: o.setup, verify, tier: o.tier, alias: o.alias,
517
523
  others: [
518
524
  ["sources", Boolean(o.sources?.length)], ["start", o.start !== undefined],
519
- ["registrySecret", Boolean(o.registrySecret)], ["cpuClass", Boolean(o.cpuClass)],
525
+ ["registrySecret", Boolean(o.registrySecret)],
520
526
  ],
521
527
  });
522
528
  }
@@ -564,11 +570,13 @@ export function snapshotCreateBody(opts) {
564
570
  body["registry_secret"] = o.registrySecret;
565
571
  if (o.env !== undefined && Object.keys(o.env).length)
566
572
  body["env"] = { ...o.env };
573
+ if (o.user !== undefined)
574
+ body["user"] = o.user;
575
+ if (o.setupUser !== undefined)
576
+ body["setup_user"] = o.setupUser;
567
577
  }
568
578
  if (o.alias)
569
579
  body["alias"] = o.alias;
570
- if (o.cpuClass)
571
- body["cpu_class"] = o.cpuClass;
572
580
  if (o.grade) {
573
581
  if (!o.grade.cmd)
574
582
  throw new InvalidArgumentError("snapshots.create grade needs a cmd", "Pass { grade: { cmd: 'pytest -q tests/' } }.");
@@ -644,6 +652,7 @@ export function errorFor(code, message, fix, extra = {}) {
644
652
  case "invalid_argument":
645
653
  return attach(new InvalidArgumentError(message, extra.cli ? fix : withoutCliHelp(fix), code));
646
654
  case "not_released": return attach(new NotReleasedError(message));
655
+ case "service_restart": return attach(new ServiceRestartError(message, fix));
647
656
  default:
648
657
  if (ENV_CODES.has(code) || extra.category === "env")
649
658
  return attach(new EnvFailedError(code, message, fix));
@@ -1207,12 +1216,14 @@ export class PromigenceClient {
1207
1216
  ...(opts.timeoutMs === undefined ? {} : { timeoutMs: opts.timeoutMs }),
1208
1217
  });
1209
1218
  }
1210
- pricing(opts = {}) {
1211
- const anonymous = opts.newAccount === true || !this.authenticated;
1219
+ async pricing(opts = {}) {
1220
+ if (opts.newAccount === true) {
1221
+ throw new InvalidArgumentError("pricing({ newAccount: true }) is no longer supported: the price list needs a key", "Call pricing() with your API key for your organization's prices. The published list prices are on the pricing page, and `promigence pricing` prints them without a key.");
1222
+ }
1223
+ this.requireKey();
1212
1224
  return this.request({
1213
1225
  method: "GET",
1214
1226
  path: "/pricing",
1215
- ...(anonymous ? { anonymous: true } : {}),
1216
1227
  ...(opts.signal ? { signal: opts.signal } : {}),
1217
1228
  ...(opts.timeoutMs === undefined ? {} : { timeoutMs: opts.timeoutMs }),
1218
1229
  });
@@ -1248,6 +1259,14 @@ export class PromigenceClient {
1248
1259
  const r = await this.request({ method: "GET", path: `/me/ledger?${pageQuery(limit, 1000, opts.cursor)}` });
1249
1260
  return pageOf(r, "entries", limit);
1250
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
+ },
1251
1270
  checkout: () => this.request({ method: "POST", path: "/billing/checkout", body: {} }),
1252
1271
  portal: () => this.request({ method: "POST", path: "/billing/portal", body: {} }),
1253
1272
  referrals: () => this.request({ method: "GET", path: "/referrals" }),
@@ -1742,6 +1761,10 @@ export class PromigenceClient {
1742
1761
  on.heartbeat?.();
1743
1762
  continue;
1744
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
+ }
1745
1768
  let parsed = {};
1746
1769
  try {
1747
1770
  parsed = JSON.parse(data);
@@ -1977,6 +2000,9 @@ export class PromigenceClient {
1977
2000
  ...(opts.onData ? { onData: opts.onData } : {}),
1978
2001
  ...(opts.onExit ? { onExit: opts.onExit } : {}),
1979
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 } : {}),
1980
2006
  refuse: (status, text, retryAfter) => {
1981
2007
  let payload = {};
1982
2008
  try {
@@ -2414,19 +2440,6 @@ export class PromigenceClient {
2414
2440
  },
2415
2441
  manifest: (idOrAlias) => this.request({ method: "GET", path: `/snapshots/${seg(idOrAlias)}/manifest` }),
2416
2442
  bundle: async (idOrAlias, opts = {}) => {
2417
- try {
2418
- const r = await this.request({
2419
- method: "GET", path: `/snapshots/${seg(idOrAlias)}/bundle`,
2420
- });
2421
- if (r?.bundle?.schema === BUNDLE_SCHEMA) {
2422
- return { bundle: r.bundle, served_by: "control" };
2423
- }
2424
- }
2425
- catch (e) {
2426
- const code = e?.code;
2427
- if (code !== "not_found" && code !== "enoent" && e?.status !== 405)
2428
- throw e;
2429
- }
2430
2443
  const [record, m] = await Promise.all([
2431
2444
  this.snapshots.get(idOrAlias).catch(() => undefined),
2432
2445
  this.snapshots.manifest(idOrAlias),
package/dist/core.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Promigence SDK — constants, error hierarchy and shared types. */
2
- export declare const VERSION = "0.1.0-beta.6";
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/core.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { hintArg } from "./hint.js";
2
- export const VERSION = "0.1.0-beta.6";
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,
@@ -226,7 +232,7 @@ export const EXIT_BY_CODE = {
226
232
  nonzero_exit: 4, grade_not_passed: 4, task_timeout: 4, oom_over_tier: 4, disk_over_tier: 4,
227
233
  shutdown_by_task: 4, modified_by_task: 4,
228
234
  cap_exceeded: 5, cap_killed: 12, cap_paused: 5, credits_paused: 5,
229
- aborted_by_client: 130, expired: 12, not_started: 12, superseded: 0,
235
+ aborted_by_client: 130, stopped_by_support: 12, expired: 12, not_started: 12, superseded: 0,
230
236
  usage: 2, internal: 1, auth: 7, not_found: 8, unreachable: 9, rate_limited: 10,
231
237
  timeout: 11, not_verified: 6, enoent: 8,
232
238
  snapshot_incompatible: 6, storage_limit_reached: 5,
@@ -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,
@@ -251,6 +258,8 @@ export const EXIT_BY_CODE = {
251
258
  invite_invalid: 7,
252
259
  invite_required: 7,
253
260
  key_expired: 7,
261
+ account_blocked: 7,
262
+ setup_required: 7,
254
263
  };
255
264
  export function exitForCode(code) {
256
265
  return Object.prototype.hasOwnProperty.call(EXIT_BY_CODE, code) ? EXIT_BY_CODE[code] : 1;
@@ -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
package/dist/credstore.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { accessSync, constants, existsSync } from "node:fs";
3
+ import { homedir } from "node:os";
3
4
  import { delimiter, join } from "node:path";
4
5
  export const KEYCHAIN_SERVICE = "promigence-cli";
5
6
  const KEYCHAIN_LABEL = "Promigence CLI credentials";
@@ -30,8 +31,19 @@ function sessionBus(env) {
30
31
  const run = env["XDG_RUNTIME_DIR"];
31
32
  return Boolean(run) && existsSync(join(run, "bus"));
32
33
  }
34
+ function userHome(env, home = homedir) {
35
+ const h = env["HOME"];
36
+ if (h)
37
+ return h;
38
+ try {
39
+ return home() || undefined;
40
+ }
41
+ catch {
42
+ return undefined;
43
+ }
44
+ }
33
45
  const warnedSetting = new Set();
34
- export function keychainStore(env = process.env, platform = process.platform) {
46
+ export function keychainStore(env = process.env, platform = process.platform, home = homedir) {
35
47
  const raw = (env["PROMIGENCE_CREDENTIAL_STORE"] ?? "").trim().toLowerCase();
36
48
  switch (raw) {
37
49
  case "file": return undefined;
@@ -46,8 +58,8 @@ export function keychainStore(env = process.env, platform = process.platform) {
46
58
  }
47
59
  }
48
60
  if (platform === "darwin") {
49
- const home = env["HOME"];
50
- return home && existsSync(join(home, "Library", "Keychains")) && onPath("security", env) ? "macos-keychain" : undefined;
61
+ const h = userHome(env, home);
62
+ return h && existsSync(join(h, "Library", "Keychains")) && onPath("security", env) ? "macos-keychain" : undefined;
51
63
  }
52
64
  if (platform === "win32")
53
65
  return undefined;
@@ -94,8 +106,9 @@ function runTool(store, args, env, input, timeout) {
94
106
  const bin = onPath(toolName(store), env);
95
107
  if (bin === undefined)
96
108
  throw Object.assign(new Error(`${toolName(store)} not found`), { code: "ENOENT" });
109
+ const h = store === "macos-keychain" && !env["HOME"] ? userHome(env) : undefined;
97
110
  return execFileSync(bin, args, {
98
- encoding: "utf8", timeout, env,
111
+ encoding: "utf8", timeout, env: h ? { ...env, HOME: h } : env,
99
112
  ...(input === undefined ? { stdio: ["ignore", "pipe", "pipe"] } : { input, stdio: ["pipe", "pipe", "pipe"] }),
100
113
  });
101
114
  }