promigence 0.1.0-beta.2 → 0.1.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/account.js CHANGED
@@ -87,7 +87,7 @@ const status = async (ctx) => {
87
87
  };
88
88
  const me = async (ctx) => {
89
89
  const c = client(ctx);
90
- const info = await c.me();
90
+ const [info, outcomes] = await Promise.all([c.me(), c.outcomes().catch(() => undefined)]);
91
91
  const conc = await concurrencyOf(c, info);
92
92
  const plan = planName(info);
93
93
  const trial = info.trial;
@@ -124,12 +124,42 @@ const me = async (ctx) => {
124
124
  ].filter(Boolean);
125
125
  if (shape.length)
126
126
  lines.push(` limits: ${shape.join(" · ")}`);
127
+ if (outcomes && outcomes.sandboxes > 0) {
128
+ const [head, ...rest] = outcomeLines(outcomes);
129
+ lines.push(` ${head}:`, ...rest.map((l) => ` ${l}`));
130
+ }
127
131
  return emit(ctx.mode, okEnvelope(ctx.cmd.name, {
128
132
  me: info,
133
+ ...(outcomes ? { outcomes } : {}),
129
134
  ...(plan ? { plan } : {}),
130
135
  ...(conc ? { concurrency: { in_use: conc.in_use, limit: conc.limit, source: conc.source, ...(conc.at_least ? { at_least: true } : {}) } } : {}),
131
136
  }, next), lines, 0, ctx.pointer);
132
137
  };
138
+ export function outcomeLines(o) {
139
+ const usd = (n) => `$${n.toFixed(2)}`;
140
+ const lines = [`this month (since ${o.since.slice(0, 10)})`];
141
+ if (o.sandboxes <= 0) {
142
+ lines.push(" nothing ran yet this month");
143
+ return lines;
144
+ }
145
+ lines.push(` runs ${o.runs.toLocaleString("en-US")}`);
146
+ const ready = o.ready_ms_p50 ?? null;
147
+ lines.push(` sandboxes ${o.sandboxes.toLocaleString("en-US")}${ready === null ? "" : `, ready in ${ms(ready)} (median)`}`);
148
+ if (o.failures_ours > 0)
149
+ lines.push(` failures of ours ${o.failures_ours}, not billed${o.failures_ours_usd > 0 ? ` (${usd(o.failures_ours_usd)} given back)` : ""}`);
150
+ const speedup = o.batch_speedup ?? null;
151
+ const saved = o.batch_saved_s ?? null;
152
+ if (speedup !== null && saved !== null) {
153
+ lines.push(` many at once ${o.batch_runs} runs, ${speedup.toFixed(1)}x faster than one after another, ${duration(saved * 1000)} not spent waiting`);
154
+ }
155
+ if (o.kept_failures > 0)
156
+ lines.push(` kept failures ${o.kept_failures} you can still reopen`);
157
+ const hours = o.paused_hours ?? null;
158
+ const paidNot = o.paused_usd ?? null;
159
+ if (hours !== null)
160
+ lines.push(` paused, not billed ${hours.toFixed(1)} h${paidNot === null ? "" : ` (${usd(paidNot)} you did not pay)`}`);
161
+ return lines;
162
+ }
133
163
  const billingUsage = async (ctx) => {
134
164
  const limit = limitOf(ctx, 20, 1000);
135
165
  const wantLedger = bool(ctx, "--ledger");
package/dist/bin.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { GLOBAL_FLAGS, LISTED_COMMANDS, REQUEST_TIMEOUT_DEFAULT, SCHEMA, VERSION, findCommand, nearestFlag, notReleasedEnvelope, notReleasedLines, renderCommandHelp, renderHelp, unknownFlags, } from "./surface.js";
3
- import { HANDLERS, client } from "./commands.js";
3
+ import { HANDLERS, client, serverLocation } from "./commands.js";
4
+ import { maybeFarNotice } from "./timing.js";
4
5
  import { refineError } from "./errfix.js";
5
6
  import { EXTRA_HANDLERS } from "./handlers.js";
6
7
  import { BUNDLE_HANDLERS } from "./bundle.js";
@@ -17,7 +18,7 @@ import { RUNNERS_HANDLERS } from "./runners.js";
17
18
  import { PRICING_HANDLERS } from "./pricing.js";
18
19
  import { GITHUB_HANDLERS } from "./github.js";
19
20
  import { GITLAB_HANDLERS } from "./gitlab.js";
20
- import { emit, errorEnvelope, errorTextLines, parseDuration, toPromigenceError, usageError as usageFailure, USAGE_DOCS, write, } from "./io.js";
21
+ import { emit, errorEnvelope, errorTextLines, parseDuration, setBeforeExit, toPromigenceError, usageError as usageFailure, USAGE_DOCS, write, } from "./io.js";
21
22
  import { setCaller } from "./sdk/caller.js";
22
23
  import { sh } from "./term.js";
23
24
  import { parse, positionalsBeforeTail, takesTail } from "./argv.js";
@@ -286,6 +287,7 @@ async function main(argv) {
286
287
  if (ctx.apiKey !== undefined && cmd.name !== "auth login" && cmd.name !== "doctor") {
287
288
  process.stderr.write("promigence: warning: --api-key puts the key on the command line, where `ps` and shell history can read it; prefer PROMIGENCE_API_KEY or `promigence auth login --from-env VAR`.\n");
288
289
  }
290
+ setBeforeExit(() => maybeFarNotice({ json: ctx.mode === "json", location: () => serverLocation(ctx) }));
289
291
  try {
290
292
  checkFlags(cmd, p, argv);
291
293
  ctx.requestTimeoutMs = requestTimeoutMs(p);
@@ -24,6 +24,7 @@ export declare function clientOpts(ctx: Ctx): {
24
24
  cliErrors: true;
25
25
  };
26
26
  export declare function client(ctx: Ctx): PromigenceClient;
27
+ export declare function serverLocation(ctx: Ctx): Promise<string | undefined>;
27
28
  export declare const nexts: (...items: Array<[string, string]>) => NextHint[];
28
29
  export declare const KEPT_BY_HOLDERS = "sandboxes that were already given its value keep it until they end; it stays masked in their output";
29
30
  export declare function waitBound(ctx: Ctx): number;
@@ -52,6 +53,7 @@ export declare function snapshotSource(s: {
52
53
  commit?: string;
53
54
  verification?: Record<string, unknown>;
54
55
  }): string;
56
+ export declare function readyLine(ourMs: number | undefined, totalMs: number): string;
55
57
  export declare function sandboxIdLines(sandboxes: ReadonlyArray<Pick<SandboxRef, "sandbox_id" | "state">>, cap?: number): string[];
56
58
  type EnvError = {
57
59
  code: string;
@@ -67,6 +69,7 @@ export interface ExecOutcome {
67
69
  error?: EnvError;
68
70
  }
69
71
  export declare function execOutcome(res: ExecResponse, sandboxId: string, timeoutMs?: number, words?: "cli" | "mcp"): ExecOutcome;
72
+ export declare function execTimingLine(exitCode: number | null | undefined, wallMs: unknown, totalMs: number, isTTY?: boolean, env?: NodeJS.ProcessEnv): string;
70
73
  export declare function endNotice(res: ExecResponse, timeoutMs?: number): string;
71
74
  export declare const MAX_OUTPUT_BYTES_PIPED: number;
72
75
  export declare function truncationNotice(kept: "first" | "last", maxOutputBytes: number | undefined): string;
package/dist/commands.js CHANGED
@@ -4,6 +4,7 @@ import { closeSync, constants as fsConstants, existsSync, fstatSync, lstatSync,
4
4
  import { tmpdir } from "node:os";
5
5
  import { basename, dirname, join, resolve } from "node:path";
6
6
  import { DEFAULT_API_URL, DEFAULT_SNAPSHOT, ERRORS_DOCS_URL, MAX_OUTPUT_BYTES_DEFAULT, CONTEXT_GIT_RE, PromigenceClient, checkRepoUrl, withoutPin, PromigenceError, SpendCapError, TimeoutError, checkImageRef, contextRelative, credentialsPath, FILE_PUT_CHUNK_BYTES, imageSnapshot, isWaiting, execOutputBytes, packContext, readCredentials, } from "./sdk/index.js";
7
+ import { fmtMs, recordTiming } from "./timing.js";
7
8
  import { capacityLine, queuedClause, queuedLines, statusLine } from "./capacity.js";
8
9
  import { DevcontainerError, detectOne, load, planJson, planLines, planSetup, resolveGiven, translate, workspaceRootFor, wrapCommand, } from "./devcontainer.js";
9
10
  import { detect, ecosystemOfCommands, imageCreateCommand, readConfig } from "./init.js";
@@ -52,7 +53,11 @@ export function clientOpts(ctx) {
52
53
  };
53
54
  }
54
55
  export function client(ctx) {
55
- return new PromigenceClient({ ...clientOpts(ctx), onNotice: recordNotice });
56
+ return new PromigenceClient({ ...clientOpts(ctx), onNotice: recordNotice, onTiming: recordTiming });
57
+ }
58
+ export async function serverLocation(ctx) {
59
+ const h = await client(ctx).health({ timeoutMs: 3000 }).catch(() => undefined);
60
+ return typeof h?.location === "string" && h.location !== "" ? h.location : undefined;
56
61
  }
57
62
  export const nexts = (...items) => items.map(([cmd, why]) => ({ cmd, why }));
58
63
  export const KEPT_BY_HOLDERS = "sandboxes that were already given its value keep it until they end; it stays masked in their output";
@@ -1048,13 +1053,16 @@ const fork = async (ctx) => {
1048
1053
  env.error = failure;
1049
1054
  return emit(ctx.mode, env, [
1050
1055
  ...(replayed ? [replayed] : []),
1051
- `${up} of ${opts.count} sandbox${opts.count === 1 ? "" : "es"} ready in ${waited.waitedMs} ms (run ${runId})`,
1056
+ `${up} of ${opts.count} sandbox${opts.count === 1 ? "" : "es"} ready in ${readyLine(report?.fork?.ready_ms?.p50, waited.waitedMs)} (run ${runId})`,
1052
1057
  ...sandboxIdLines(waited.sandboxes),
1053
1058
  ...(report && bool(ctx, "--verbose") ? [` ${forkLine(report.fork)}`, ` ${totalsLine(report.totals)}`] : []),
1054
1059
  ...(report && exit !== 0 ? failureLines(report, new Map(), false) : []),
1055
1060
  ` next: ${execNext[0].cmd}`,
1056
1061
  ], exit, ctx.pointer);
1057
1062
  };
1063
+ export function readyLine(ourMs, totalMs) {
1064
+ return ourMs === undefined ? `${totalMs} ms` : `${fmtMs(ourMs)} on our side · ${fmtMs(totalMs)} total`;
1065
+ }
1058
1066
  export function sandboxIdLines(sandboxes, cap = 10) {
1059
1067
  const lines = sandboxes.slice(0, cap).map((s) => ` ${s.sandbox_id} ${s.state}`);
1060
1068
  if (sandboxes.length > cap)
@@ -1135,6 +1143,91 @@ function wantsStream(ctx) {
1135
1143
  return true;
1136
1144
  return Boolean(process.stdout.isTTY);
1137
1145
  }
1146
+ let followKill;
1147
+ const FOLLOW_MAX_OUTPUT = 8 * 1024 * 1024;
1148
+ const FOLLOW_RECONNECT_MS = 10 * 60_000;
1149
+ async function followExec(c, sandboxId, execOpts) {
1150
+ let rec;
1151
+ try {
1152
+ rec = await c.sandboxes.execBackground(sandboxId, {
1153
+ argv: execOpts.argv,
1154
+ ...(execOpts.secrets ? { secrets: execOpts.secrets } : {}),
1155
+ ...(execOpts.timeoutMs === undefined ? {} : { timeoutMs: execOpts.timeoutMs }),
1156
+ maxOutputBytes: execOpts.maxOutputBytes ?? FOLLOW_MAX_OUTPUT,
1157
+ follow: true,
1158
+ });
1159
+ }
1160
+ catch (err) {
1161
+ const e = toPromigenceError(err);
1162
+ if (e.code === "usage" && /follow/.test(e.message))
1163
+ return undefined;
1164
+ throw err;
1165
+ }
1166
+ const eid = rec.exec_id;
1167
+ followKill = () => Promise.race([
1168
+ c.sandboxes.execs.forget(sandboxId, eid).then(() => undefined, () => undefined),
1169
+ new Promise((r) => setTimeout(r, 2000)),
1170
+ ]);
1171
+ const offset = { stdout: 0, stderr: 0 };
1172
+ let last = rec;
1173
+ let lostAt;
1174
+ const say = (s) => write(process.stderr, s);
1175
+ const pump = async (fd) => {
1176
+ for (let backoff = 500;;) {
1177
+ try {
1178
+ const r = await c.sandboxes.execs.read(sandboxId, eid, fd, { offset: offset[fd], waitMs: 25_000, untilOutput: true });
1179
+ if (lostAt !== undefined) {
1180
+ lostAt = undefined;
1181
+ await say("promigence: reconnected; the command's output continues from where it was\n");
1182
+ }
1183
+ backoff = 500;
1184
+ if (r.skipped > 0) {
1185
+ await say(`\npromigence: ${r.skipped} bytes of the command's ${fd} were not kept while this connection was down (the newest 8 MiB are)\n`);
1186
+ }
1187
+ if (r.data.length)
1188
+ await writeBytes(fd === "stdout" ? process.stdout : process.stderr, r.data);
1189
+ offset[fd] = r.offset + r.skipped + r.data.length;
1190
+ last = r.record;
1191
+ if (r.eof || (r.record.state !== "running" && r.data.length === 0))
1192
+ return;
1193
+ }
1194
+ catch (err) {
1195
+ const e = toPromigenceError(err);
1196
+ if (e.code !== "unreachable" && e.code !== "timeout")
1197
+ throw err;
1198
+ if (lostAt === undefined) {
1199
+ lostAt = Date.now();
1200
+ await say(`\npromigence: connection lost; the command keeps running in the sandbox. Reconnecting… ` +
1201
+ `(or later: ${sh `promigence exec output ${eid} --sandbox ${sandboxId} --follow`})\n`);
1202
+ }
1203
+ else if (Date.now() - lostAt > FOLLOW_RECONNECT_MS) {
1204
+ throw err;
1205
+ }
1206
+ await new Promise((r) => setTimeout(r, backoff));
1207
+ backoff = Math.min(backoff * 2, 5_000);
1208
+ }
1209
+ }
1210
+ };
1211
+ try {
1212
+ await Promise.all([pump("stdout"), pump("stderr")]);
1213
+ }
1214
+ finally {
1215
+ followKill = undefined;
1216
+ }
1217
+ const fin = last.state === "running" || !last.exit ? await c.sandboxes.execs.get(sandboxId, eid, { waitMs: 5_000 }) : last;
1218
+ await c.sandboxes.execs.forget(sandboxId, eid).catch(() => undefined);
1219
+ const x = fin.exit;
1220
+ const signal = typeof x?.signal === "number" ? x.signal : undefined;
1221
+ return {
1222
+ exec_id: eid,
1223
+ exit_code: x?.exit_code ?? null,
1224
+ ...(signal === undefined ? {} : { signal }),
1225
+ ...(x?.oom_killed ? { reason: "out_of_memory" } : signal !== undefined ? { reason: "signal" } : {}),
1226
+ ...(typeof x?.wall_ms === "number" ? { wall_ms: x.wall_ms } : {}),
1227
+ ...(typeof x?.cpu_ms === "number" ? { cpu_ms: x.cpu_ms } : {}),
1228
+ truncated: false,
1229
+ };
1230
+ }
1138
1231
  async function streamExec(ctx, c, sandboxId, execOpts) {
1139
1232
  const ndjson = ctx.mode === "json";
1140
1233
  const dec = { stdout: new TextDecoder(), stderr: new TextDecoder() };
@@ -1195,6 +1288,11 @@ async function emitChild(res, exit, notice) {
1195
1288
  await write(process.stderr, notice);
1196
1289
  return exitWith(exit);
1197
1290
  }
1291
+ export function execTimingLine(exitCode, wallMs, totalMs, isTTY = Boolean(process.stderr.isTTY), env = process.env) {
1292
+ if (!isTTY || env["PROMIGENCE_QUIET"] === "1" || typeof wallMs !== "number" || !Number.isFinite(wallMs))
1293
+ return "";
1294
+ return `promigence: exit ${exitCode ?? "none"} · ran ${fmtMs(wallMs)} on our side · ${fmtMs(totalMs)} round trip\n`;
1295
+ }
1198
1296
  export function endNotice(res, timeoutMs) {
1199
1297
  const sig = typeof res["signal"] === "number" ? ` (signal ${res["signal"]})` : "";
1200
1298
  switch (res.reason) {
@@ -1284,6 +1382,10 @@ const exec = async (ctx) => {
1284
1382
  execOpts.maxOutputBytes = MAX_OUTPUT_BYTES_PIPED;
1285
1383
  }
1286
1384
  const interrupted = (sig) => {
1385
+ const ending = followKill?.() ?? Promise.resolve();
1386
+ void ending.finally(() => interruptedEnvelope(sig));
1387
+ };
1388
+ const interruptedEnvelope = (sig) => {
1287
1389
  const err = new PromigenceError({
1288
1390
  code: "aborted_by_client",
1289
1391
  message: `interrupted by ${sig}; the command in the sandbox ends with this connection`,
@@ -1297,9 +1399,14 @@ const exec = async (ctx) => {
1297
1399
  };
1298
1400
  for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"])
1299
1401
  process.once(sig, () => interrupted(sig));
1300
- const res = streaming
1402
+ const execStarted = performance.now();
1403
+ const followed = streaming && ctx.mode === "text" && process.env["PROMIGENCE_EXEC_FOLLOW"] !== "0"
1404
+ ? await followExec(c, sandboxId, execOpts)
1405
+ : undefined;
1406
+ const res = followed ?? (streaming
1301
1407
  ? await streamExec(ctx, c, sandboxId, execOpts)
1302
- : asText(await c.sandboxes.exec(sandboxId, { ...execOpts, encoding: "base64" }));
1408
+ : asText(await c.sandboxes.exec(sandboxId, { ...execOpts, encoding: "base64" })));
1409
+ const execTotalMs = performance.now() - execStarted;
1303
1410
  const outcome = execOutcome(res, sandboxId, execOpts.timeoutMs);
1304
1411
  const passthrough = !bool(ctx, "--no-passthrough");
1305
1412
  let exit = passthrough ? (outcome.exitCode ?? 4) : (outcome.exitCode === 0 ? 0 : 4);
@@ -1309,13 +1416,15 @@ const exec = async (ctx) => {
1309
1416
  const notice = (res.truncated ? truncationNotice(streaming ? "first" : "last", execOpts.maxOutputBytes) : "")
1310
1417
  + endNotice(res, execOpts.timeoutMs);
1311
1418
  const raw = streaming || typeof res.stdout_b64 === "string";
1312
- const gap = notice && raw && process.stdout.isTTY && !endsWithNewline(res) ? "\n" : "";
1419
+ const timed = execTimingLine(outcome.exitCode, res.wall_ms, execTotalMs);
1420
+ const all = notice + timed;
1421
+ const gap = all && raw && process.stdout.isTTY && !endsWithNewline(res) ? "\n" : "";
1313
1422
  if (streaming) {
1314
- if (notice)
1315
- await write(process.stderr, gap + notice);
1423
+ if (all)
1424
+ await write(process.stderr, gap + all);
1316
1425
  return exitWith(exit);
1317
1426
  }
1318
- return emitChild(res, exit, notice ? gap + notice : undefined);
1427
+ return emitChild(res, exit, all ? gap + all : undefined);
1319
1428
  }
1320
1429
  const next = nexts(...(res.truncated
1321
1430
  ? [[`${sh `promigence exec --sandbox ${sandboxId} --max-output 32m --`} ${argv.map(shellArg).join(" ")}`, "re-run with a larger --max-output"]]
package/dist/handlers.js CHANGED
@@ -12,6 +12,7 @@ import { detect, gitFacts, imageCreateCommand, renderConfig, stripRemoteCredenti
12
12
  import { TIERS } from "./generated.js";
13
13
  import { GOLDEN_PATH, SCHEMA, VERSION } from "./surface.js";
14
14
  import { jsonLine, sh } from "./term.js";
15
+ import { fmtMs, median, where } from "./timing.js";
15
16
  const SECTION_NAMES = new Set(["core", "commands", "errors", "cost"]);
16
17
  const agentGuide = async (ctx) => {
17
18
  const format = str(ctx, "--format") ?? (ctx.explicitMode === "json" ? "json" : "md");
@@ -323,6 +324,46 @@ async function apiChecks(ctx, checks) {
323
324
  checks.push(fail("auth_accepted", `${e.code}: ${e.message}`, e.fix, e.cliExit ?? 7));
324
325
  }
325
326
  }
327
+ const ping = async (ctx) => {
328
+ const n = Number(process.env["PROMIGENCE_PING_COUNT"] ?? "5");
329
+ if (!Number.isInteger(n) || n < 1 || n > 50)
330
+ throw usageError("PROMIGENCE_PING_COUNT takes 1 to 50", "Unset it for the default of five.");
331
+ const c = client(ctx);
332
+ const rounds = [];
333
+ let location;
334
+ for (let i = 0; i < n; i++) {
335
+ const h = await c.health({ timeoutMs: 10_000 });
336
+ if (typeof h.location === "string" && h.location)
337
+ location = h.location;
338
+ const tm = c.lastTiming;
339
+ if (tm)
340
+ rounds.push({ totalMs: tm.totalMs, ourMs: tm.ourMs });
341
+ }
342
+ const totals = rounds.map((r) => r.totalMs);
343
+ const ours = rounds.map((r) => r.ourMs).filter((x) => x !== null);
344
+ const nets = rounds.filter((r) => r.ourMs !== null).map((r) => Math.max(0, r.totalMs - r.ourMs));
345
+ const rt = median(totals);
346
+ const our = median(ours);
347
+ const net = median(nets);
348
+ const r1 = (x) => (x === undefined ? null : Math.round(x * 10) / 10);
349
+ const fields = {
350
+ location: location ?? null,
351
+ round_trips: n,
352
+ round_trip_ms: { median: r1(rt), min: r1(Math.min(...totals)), max: r1(Math.max(...totals)) },
353
+ our_side_ms: r1(our),
354
+ network_ms: r1(net),
355
+ };
356
+ const lines = [
357
+ `promigence ping: ${where(location)}`,
358
+ ` round trip ${rt === undefined ? "—" : fmtMs(rt)} (median of ${n}; ${fmtMs(Math.min(...totals))} to ${fmtMs(Math.max(...totals))})`,
359
+ ` our side ${our === undefined ? "— (not said by this service)" : fmtMs(our)}`,
360
+ ` your network ${net === undefined ? "—" : fmtMs(net)}`,
361
+ ...(net !== undefined && net >= 150
362
+ ? [" Times marked 'on our side' are the platform's own; the rest is the distance to our servers."]
363
+ : []),
364
+ ];
365
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, fields, nexts(["promigence doctor", "check the client, your key and the API"])), lines, 0, ctx.pointer);
366
+ };
326
367
  const doctor = async (ctx) => {
327
368
  const checks = await clientChecks(ctx);
328
369
  await apiChecks(ctx, checks);
@@ -466,5 +507,6 @@ export const EXTRA_HANDLERS = {
466
507
  init,
467
508
  examples,
468
509
  doctor,
510
+ ping,
469
511
  "exec output": execOutput,
470
512
  };
package/dist/io.d.ts CHANGED
@@ -6,6 +6,7 @@ export declare function markGuideSeen(): void;
6
6
  export declare function isGoneReader(code: string | undefined): boolean;
7
7
  export declare function writeBytes(stream: NodeJS.WriteStream, data: Uint8Array): Promise<void>;
8
8
  export declare function write(stream: NodeJS.WriteStream, text: string): Promise<void>;
9
+ export declare function setBeforeExit(fn: (() => Promise<unknown>) | undefined): void;
9
10
  export declare function exitWith(code: number): Promise<never>;
10
11
  export declare function recordNotice(n: AccountNotice): void;
11
12
  export declare function noticeLine(n: AccountNotice): string;
package/dist/io.js CHANGED
@@ -53,7 +53,15 @@ function flush(stream) {
53
53
  stream.write("", () => resolve());
54
54
  });
55
55
  }
56
+ let beforeExit;
57
+ export function setBeforeExit(fn) {
58
+ beforeExit = fn;
59
+ }
56
60
  export async function exitWith(code) {
61
+ const hook = beforeExit;
62
+ beforeExit = undefined;
63
+ if (hook)
64
+ await hook().catch(() => undefined);
57
65
  await flush(process.stdout);
58
66
  await flush(process.stderr);
59
67
  process.exit(code);
@@ -238,7 +246,7 @@ export function forkLine(fork) {
238
246
  return "fork: (no timings yet)";
239
247
  const p = (name, v) => v === undefined ? `${name} —` : `${name} p50 ${ms(v.p50)} p95 ${ms(v.p95)}${v.p99 === undefined ? "" : ` p99 ${ms(v.p99)}`}`;
240
248
  return [
241
- `fork n=${fork.n}`,
249
+ `fork n=${fork.n} (on our side)`,
242
250
  p("queued", fork.queued_ms),
243
251
  p("ready", fork.ready_ms),
244
252
  p("e2e", fork.e2e_ms),
package/dist/login.js CHANGED
@@ -809,6 +809,7 @@ async function signInAndStore(ctx, isSignup) {
809
809
  }
810
810
  const consoleHome = viaConsole ? `${new URL(viaConsole).origin}/home` : undefined;
811
811
  const credit = await offerAfterLogin(ctx, anon.apiUrl, session.api_key, consoleHome);
812
+ const location = await anon.health({ timeoutMs: 3000 }).then((h) => (typeof h.location === "string" && h.location ? h.location : undefined)).catch(() => undefined);
812
813
  const next = nexts(["promigence me", "your org, limits and spend"], ["promigence examples", "what you can fork with no build (the built-in base)"]);
813
814
  const fields = {
814
815
  credentials: path, store: stored.store, mode: stored.store === "file" ? "0600" : null, created: session.created,
@@ -820,6 +821,8 @@ async function signInAndStore(ctx, isSignup) {
820
821
  if (session.referral)
821
822
  fields["referral"] = session.referral;
822
823
  fields["credit_offer"] = credit.field;
824
+ if (location)
825
+ fields["location"] = location;
823
826
  if (warnings.length)
824
827
  fields["warnings"] = warnings;
825
828
  const rf = session.referral;
@@ -831,6 +834,7 @@ async function signInAndStore(ctx, isSignup) {
831
834
  return emit(ctx.mode, okEnvelope(ctx.cmd.name, fields, next), [
832
835
  `promigence login: ${session.created ? "account created" : "signed in"} as ${session.user.email} (${session.user.provider})`,
833
836
  ` org: ${session.org.name} (${session.org.org_id})`,
837
+ ...(location ? [` Your sandboxes run on our servers on the ${location}.`] : []),
834
838
  ` key: ${session.key?.prefix ?? ""}… "${session.key?.name ?? keyName}" stored in ${storedWhere(stored)}`,
835
839
  ...(session.key?.expires_at ? [` key expires: ${session.key.expires_at.slice(0, 10)} (sign in again before then; \`promigence keys list\` shows it)`] : []),
836
840
  ...(stored.note ? [` note: ${stored.note}`] : []),
package/dist/routes.js CHANGED
@@ -43,6 +43,7 @@ export const ROUTE_COMMANDS = {
43
43
  "GET /pricing": "pricing",
44
44
  "GET /billing": "billing usage",
45
45
  "GET /me/ledger": "billing usage",
46
+ "GET /me/outcomes": "me",
46
47
  "POST /billing/checkout": "billing card",
47
48
  "POST /billing/portal": "billing portal",
48
49
  "GET /referrals": "referrals",
@@ -83,6 +83,8 @@ export interface Capacity {
83
83
  queued_message?: string;
84
84
  }
85
85
  export type Outcome = M.Outcome;
86
+ /** `GET /me/outcomes`: what the organization got this month, measured. */
87
+ export type Outcomes = M.Outcomes;
86
88
  export interface Percentiles {
87
89
  p50: number;
88
90
  p95: number;
@@ -631,6 +633,8 @@ export interface HealthResponse {
631
633
  build_sha?: string;
632
634
  /** Whether the service can start new sandboxes right now. */
633
635
  serving?: boolean;
636
+ /** Where sandboxes run, in plain words (for example "US East Coast"); absent when not said. */
637
+ location?: string;
634
638
  [extra: string]: unknown;
635
639
  }
636
640
  /** One service's row in `GET /status` (`api`, `sandboxes`): `up`, `degraded` or `down`. */
@@ -1213,6 +1217,10 @@ export interface BackgroundExecRecord {
1213
1217
  stdout_total?: number;
1214
1218
  stderr_total?: number;
1215
1219
  truncated?: boolean;
1220
+ /** Started with `follow`: the newest output is kept, and the handle goes 10 minutes after it ends. */
1221
+ follow?: boolean;
1222
+ /** A range read of a `follow` command: bytes dropped between the asked offset and the first held. */
1223
+ skipped?: number;
1216
1224
  exit?: {
1217
1225
  exit_code: number | null;
1218
1226
  signal: number | null;
@@ -1421,6 +1429,11 @@ export interface PromigenceClientOpts {
1421
1429
  * on `client.notice`.
1422
1430
  */
1423
1431
  onNotice?: (notice: AccountNotice) => void;
1432
+ /**
1433
+ * Called with every answer's timing ([`Timing`]): the round trip this process saw and how long
1434
+ * the answer took on Promigence's side. A CLI uses it to say how far away the caller is.
1435
+ */
1436
+ onTiming?: (timing: Timing) => void;
1424
1437
  }
1425
1438
  /** `GET /sandboxes/:id/network`. */
1426
1439
  export interface SandboxNetwork {
@@ -1857,6 +1870,17 @@ interface RequestOpts {
1857
1870
  /** Extra request headers (e.g. `promigence-replaced-by`). Never the authorization. */
1858
1871
  headers?: Record<string, string>;
1859
1872
  }
1873
+ /** One answer's timing ([`PromigenceClient.lastTiming`]). */
1874
+ export interface Timing {
1875
+ method: string;
1876
+ path: string;
1877
+ /** The round trip as the client saw it, in ms, until the answer's headers arrived. */
1878
+ totalMs: number;
1879
+ /** How long the answer took on Promigence's side, in ms; `null` when the service did not say. */
1880
+ ourMs: number | null;
1881
+ }
1882
+ /** The `app` span of a `Server-Timing` header (`app;dur=12.35`), in ms; `null` without one. */
1883
+ export declare function serverAppMs(header: string | null | undefined): number | null;
1860
1884
  export declare class PromigenceClient {
1861
1885
  #private;
1862
1886
  readonly apiUrl: string;
@@ -1879,9 +1903,16 @@ export declare class PromigenceClient {
1879
1903
  requests: number;
1880
1904
  retries: number;
1881
1905
  };
1906
+ /**
1907
+ * The most recent answer's timing: `totalMs` is the whole round trip as this process saw it,
1908
+ * until the answer's headers arrived; `ourMs` is how long the answer took on Promigence's side
1909
+ * (`null` when the service did not say). `totalMs − ourMs` is the network between you and us.
1910
+ */
1911
+ lastTiming: Timing | null;
1882
1912
  /** The latest account notice any response carried (`Promigence-Notice`), if one did. */
1883
1913
  notice: AccountNotice | undefined;
1884
1914
  private readonly onNotice;
1915
+ private readonly onTiming;
1885
1916
  private readonly noticesSeen;
1886
1917
  constructor(opts?: PromigenceClientOpts);
1887
1918
  /** Read `Promigence-Notice` off a response (its raw value); tell the caller once per distinct notice. */
@@ -1975,6 +2006,13 @@ export declare class PromigenceClient {
1975
2006
  me(opts?: {
1976
2007
  signal?: AbortSignal;
1977
2008
  }): Promise<MeResponse>;
2009
+ /**
2010
+ * `GET /me/outcomes?period=month` — what this organization (with a project's key: that project) got
2011
+ * this month, measured from its own runs and sandboxes. A figure with nothing behind it is `null`.
2012
+ */
2013
+ outcomes(opts?: {
2014
+ signal?: AbortSignal;
2015
+ }): Promise<Outcomes>;
1978
2016
  readonly billing: {
1979
2017
  /** `GET /billing` — plan, card, period, usage, trial, upcoming invoice. */
1980
2018
  usage: () => Promise<BillingUsage>;
@@ -2245,6 +2283,7 @@ export declare class PromigenceClient {
2245
2283
  */
2246
2284
  execBackground: (sandboxId: string, opts: Omit<ExecOpts, "terminal" | "killOnExit" | "signal"> & {
2247
2285
  stdin?: boolean;
2286
+ follow?: boolean;
2248
2287
  }) => Promise<BackgroundExecRecord>;
2249
2288
  /** Background execs of one sandbox (`GET|POST|DELETE /sandboxes/:id/execs/:exec_id…`). */
2250
2289
  execs: {
@@ -2262,10 +2301,14 @@ export declare class PromigenceClient {
2262
2301
  read: (sandboxId: string, execId: string, stream: "stdout" | "stderr", opts?: {
2263
2302
  offset?: number;
2264
2303
  len?: number;
2304
+ waitMs?: number;
2305
+ untilOutput?: boolean;
2306
+ signal?: AbortSignal;
2265
2307
  }) => Promise<{
2266
2308
  data: Uint8Array;
2267
2309
  offset: number;
2268
2310
  eof: boolean;
2311
+ skipped: number;
2269
2312
  record: BackgroundExecRecord;
2270
2313
  }>;
2271
2314
  /**
@@ -904,6 +904,16 @@ function waitQuery(waitMs) {
904
904
  : "";
905
905
  }
906
906
  const notReleased = (what) => Promise.reject(new NotReleasedError(what));
907
+ export function serverAppMs(header) {
908
+ if (!header)
909
+ return null;
910
+ for (const part of header.split(",")) {
911
+ const m = /^\s*app\s*;(?:.*;)?\s*dur=([0-9]+(?:\.[0-9]+)?)/.exec(part);
912
+ if (m)
913
+ return Number(m[1]);
914
+ }
915
+ return null;
916
+ }
907
917
  function headerOf(res, name) {
908
918
  return res instanceof H2Response ? res.header(name) : res.headers.get(name);
909
919
  }
@@ -920,8 +930,10 @@ export class PromigenceClient {
920
930
  cliErrors;
921
931
  caller;
922
932
  stats = { requests: 0, retries: 0 };
933
+ lastTiming = null;
923
934
  notice;
924
935
  onNotice;
936
+ onTiming;
925
937
  noticesSeen = new Set();
926
938
  constructor(opts = {}) {
927
939
  traceEvent("client:ctor");
@@ -940,6 +952,7 @@ export class PromigenceClient {
940
952
  this.caller = resolveCaller(opts.caller);
941
953
  this.userAgent = userAgentWithCaller(opts.userAgent ?? `promigence/${VERSION} node/${process.versions.node}`, this.caller);
942
954
  this.onNotice = opts.onNotice;
955
+ this.onTiming = opts.onTiming;
943
956
  this.cliErrors = opts.cliErrors === true;
944
957
  traceEvent("client:ready");
945
958
  }
@@ -1045,6 +1058,7 @@ export class PromigenceClient {
1045
1058
  for (let attempt = 0;; attempt++) {
1046
1059
  let res;
1047
1060
  this.stats.requests++;
1061
+ const sentAt = performance.now();
1048
1062
  try {
1049
1063
  if (this.h2) {
1050
1064
  const init = {
@@ -1074,6 +1088,13 @@ export class PromigenceClient {
1074
1088
  res = await this.fetchImpl(url, init);
1075
1089
  }
1076
1090
  mark(tr, "res");
1091
+ this.lastTiming = {
1092
+ method: opts.method,
1093
+ path: opts.path,
1094
+ totalMs: performance.now() - sentAt,
1095
+ ourMs: serverAppMs(headerOf(res, "server-timing")),
1096
+ };
1097
+ this.onTiming?.(this.lastTiming);
1077
1098
  }
1078
1099
  catch (err) {
1079
1100
  const e = err;
@@ -1391,6 +1412,9 @@ export class PromigenceClient {
1391
1412
  me(opts = {}) {
1392
1413
  return this.request({ method: "GET", path: "/me", ...(opts.signal ? { signal: opts.signal } : {}) });
1393
1414
  }
1415
+ outcomes(opts = {}) {
1416
+ return this.request({ method: "GET", path: "/me/outcomes?period=month", ...(opts.signal ? { signal: opts.signal } : {}) });
1417
+ }
1394
1418
  billing = {
1395
1419
  usage: () => this.request({ method: "GET", path: "/billing" }),
1396
1420
  ledger: async (limit = 100) => (await this.billing.ledgerPage({ limit })).items,
@@ -1966,6 +1990,8 @@ export class PromigenceClient {
1966
1990
  body["secrets"] = opts.secrets;
1967
1991
  if (opts.stdin)
1968
1992
  body["stdin"] = true;
1993
+ if (opts.follow)
1994
+ body["follow"] = true;
1969
1995
  const timeoutMs = commandTimeoutMs(opts.timeoutMs);
1970
1996
  if (timeoutMs !== undefined)
1971
1997
  body["timeout_ms"] = timeoutMs;
@@ -1998,11 +2024,18 @@ export class PromigenceClient {
1998
2024
  const q = [`stream=${stream}`, `offset=${Math.max(0, Math.floor(opts.offset ?? 0))}`];
1999
2025
  if (opts.len !== undefined)
2000
2026
  q.push(`len=${Math.max(0, Math.floor(opts.len))}`);
2027
+ const wait = opts.waitMs !== undefined && opts.waitMs > 0 ? Math.min(Math.floor(opts.waitMs), WAIT_MS_CAP) : 0;
2028
+ if (wait)
2029
+ q.push(`wait_ms=${wait}`);
2030
+ if (wait && opts.untilOutput)
2031
+ q.push("until=output");
2001
2032
  const r = await this.request({
2002
2033
  method: "GET",
2003
2034
  path: `/sandboxes/${encodeURIComponent(sandboxId)}/execs/${encodeURIComponent(execId)}?${q.join("&")}`,
2035
+ ...(wait ? { timeoutMs: wait + this.requestTimeoutMs } : {}),
2036
+ ...(opts.signal ? { signal: opts.signal } : {}),
2004
2037
  });
2005
- return { data: b64ToBytes(r.data ?? ""), offset: r.offset ?? opts.offset ?? 0, eof: Boolean(r.eof), record: r };
2038
+ return { data: b64ToBytes(r.data ?? ""), offset: r.offset ?? opts.offset ?? 0, eof: Boolean(r.eof), skipped: r.skipped ?? 0, record: r };
2006
2039
  },
2007
2040
  stdin: (sandboxId, execId, opts = {}) => {
2008
2041
  if (opts.data === undefined && !opts.eof) {
@@ -1,5 +1,5 @@
1
1
  /** Promigence SDK — constants, error hierarchy and shared types. */
2
- export declare const VERSION = "0.1.0-beta.2";
2
+ export declare const VERSION = "0.1.0-beta.3";
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";
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.2";
2
+ export const VERSION = "0.1.0-beta.3";
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";
@@ -146,6 +146,11 @@ export interface BackgroundExec {
146
146
  * How the command ended; only once `state` is `exited`.
147
147
  */
148
148
  exit?: BackgroundExecExit | null;
149
+ /**
150
+ * Started with `follow`: the newest output is kept, and the handle is freed 10 minutes
151
+ * after the command ends.
152
+ */
153
+ follow?: boolean | null;
149
154
  /**
150
155
  * Present when the sandbox pauses itself when idle: after this command ends, the idle rule
151
156
  * waits up to this many seconds for its result to be read before it pauses the sandbox (a
@@ -166,6 +171,11 @@ export interface BackgroundExec {
166
171
  len?: number | null;
167
172
  offset?: number | null;
168
173
  sandbox_id: string;
174
+ /**
175
+ * A `follow` command only: bytes between the `offset` asked for and the first byte still
176
+ * held, dropped to keep the newest output; `data` then starts at `offset + skipped`.
177
+ */
178
+ skipped?: number | null;
169
179
  /**
170
180
  * Unix nanoseconds.
171
181
  */
@@ -1018,6 +1028,14 @@ export interface ExecCreate {
1018
1028
  * Extra environment variables.
1019
1029
  */
1020
1030
  env?: Record<string, string>;
1031
+ /**
1032
+ * With `background`: the command is read as it runs, by a caller that may lose its
1033
+ * connection and come back. The newest `max_output_bytes` of each stream are kept (8 MiB by
1034
+ * default; older bytes are dropped, and a read that asks for them says how many in
1035
+ * `skipped`), and the handle is freed 10 minutes after the command ends, read or not. The
1036
+ * command never stops because a caller went away.
1037
+ */
1038
+ follow?: boolean;
1021
1039
  /**
1022
1040
  * End the sandbox when the command exits (default: the value of `terminal`).
1023
1041
  */
@@ -1904,6 +1922,11 @@ export interface Health {
1904
1922
  * The commit this build came from; empty for a local build.
1905
1923
  */
1906
1924
  build_sha: string;
1925
+ /**
1926
+ * Where sandboxes run, in plain words (for example "US East Coast"). Absent when the service
1927
+ * does not say.
1928
+ */
1929
+ location?: string | null;
1907
1930
  ok: boolean;
1908
1931
  /**
1909
1932
  * Whether new sandboxes can start right now. Answered to a request made with a key; absent
@@ -2325,6 +2348,16 @@ export interface Me {
2325
2348
  */
2326
2349
  key?: KeyRef | null;
2327
2350
  limits: MeLimits;
2351
+ /**
2352
+ * Where sandboxes run, in plain words (for example "US East Coast"), as `GET /v1/health` says
2353
+ * it. Absent when the service does not say.
2354
+ */
2355
+ location?: string | null;
2356
+ /**
2357
+ * The same place as a short code for an icon: `us-east`, `us-west`, `us-midwest`, `eu-west` or
2358
+ * `india`. Only beside `location`; a code you do not know is fine to ignore.
2359
+ */
2360
+ location_code?: string | null;
2328
2361
  /**
2329
2362
  * A notice about a coming change to the trial rules; `null` when there is none.
2330
2363
  */
@@ -2627,6 +2660,67 @@ export interface OtherUsage {
2627
2660
  entries: number;
2628
2661
  }
2629
2662
  export type Outcome = "completed" | "task_failed" | "env_failed" | "snapshot_failed" | "aborted_by_client" | "expired" | "cap_killed" | "not_started" | "superseded" | "grader_failed" | "preempted";
2663
+ /**
2664
+ * `GET /v1/me/outcomes?period=month` — what the organization (with a project's key: that project)
2665
+ * got this period, measured from its own runs and sandboxes. A figure with nothing behind it is
2666
+ * `null`; counts are `0`.
2667
+ */
2668
+ export interface Outcomes {
2669
+ /**
2670
+ * Finished runs of two or more sandboxes this period.
2671
+ */
2672
+ batch_runs: number;
2673
+ /**
2674
+ * For those runs: seconds not spent waiting, the sum of their sandboxes' own times minus
2675
+ * the runs' wall time; `null` without such a run.
2676
+ */
2677
+ batch_saved_s?: number | null;
2678
+ /**
2679
+ * For those runs: the sum of their sandboxes' own times divided by the runs' wall time
2680
+ * (how many times faster than one after another); `null` without such a run.
2681
+ */
2682
+ batch_speedup?: number | null;
2683
+ /**
2684
+ * Sandboxes that ended on a failure on our side and were not billed for it.
2685
+ */
2686
+ failures_ours: number;
2687
+ /**
2688
+ * What was given back for those failures, in USD (`0` when they ended before any charge).
2689
+ */
2690
+ failures_ours_usd: number;
2691
+ /**
2692
+ * Failed sandboxes kept to reopen that can still be reopened.
2693
+ */
2694
+ kept_failures: number;
2695
+ /**
2696
+ * Hours this period's sandboxes spent paused, which is not billed; `null` if none paused.
2697
+ */
2698
+ paused_hours?: number | null;
2699
+ /**
2700
+ * What those paused hours would have cost running, in USD; `null` if none paused.
2701
+ */
2702
+ paused_usd?: number | null;
2703
+ /**
2704
+ * `month`: the calendar month so far, UTC.
2705
+ */
2706
+ period: string;
2707
+ /**
2708
+ * The median milliseconds until a sandbox was ready to take work; `null` with no sandbox.
2709
+ */
2710
+ ready_ms_p50?: number | null;
2711
+ /**
2712
+ * Runs started this period.
2713
+ */
2714
+ runs: number;
2715
+ /**
2716
+ * Sandboxes started this period.
2717
+ */
2718
+ sandboxes: number;
2719
+ /**
2720
+ * When the period began (RFC 3339, UTC).
2721
+ */
2722
+ since: string;
2723
+ }
2630
2724
  /**
2631
2725
  * What a parked sandbox occupies on disk.
2632
2726
  */
@@ -4256,8 +4350,9 @@ export interface SandboxView {
4256
4350
  */
4257
4351
  billable_seconds?: number | null;
4258
4352
  /**
4259
- * Once it has ended, what it cost in USD at its run's rates (the run report's `cost`).
4260
- * Present on `GET /v1/sandboxes/{id}` for an ended sandbox; absent from list rows.
4353
+ * What it has cost in USD at its run's rates: once it has ended, the run report's `cost`; while
4354
+ * it is live, the seconds billed so far (running time only). Present on `GET /v1/sandboxes/{id}`
4355
+ * once it has started; absent from list rows.
4261
4356
  */
4262
4357
  cost_usd?: number | null;
4263
4358
  /**
package/dist/surface.d.ts CHANGED
@@ -6,7 +6,7 @@ export declare const DESCRIPTION = "Promigence \u2014 verified, reproducible, fo
6
6
  export declare const GOLDEN_PATH = "promigence run --snapshot promigence/base --count 2 --cap 1 --json -- python3 -c 'print(6*7)'";
7
7
  export declare const WAIT_TIMEOUT_DEFAULT = "100s";
8
8
  export declare const REQUEST_TIMEOUT_DEFAULT = "30s";
9
- export declare const LIVE_COMMANDS: readonly ["login", "signup", "logout", "auth login", "keys list", "keys create", "keys revoke", "org list", "org create", "org switch", "org members", "org invite", "org invites", "org uninvite", "org resend", "org role", "org remove", "org leave", "org join", "org audit", "org rename", "org delete", "project list", "project create", "project rename", "project delete", "project cap", "project move", "me", "billing card", "doctor", "init", "examples", "templates", "templates show", "agent-guide", "mcp", "snapshot create", "snapshot status", "snapshot list", "snapshot inspect", "snapshot verify", "snapshot export", "fork", "exec", "exec output", "exec ps", "exec kill", "files put", "files get", "files ls", "run", "run quote", "run report", "run status", "run cancel", "run kill", "run replay", "run open", "run captures", "sandbox kill", "sandbox shell", "sandbox ssh", "sandbox network", "port expose", "port list", "port revoke", "secrets set", "bench burst", "status", "billing usage", "billing portal", "snapshot delete", "snapshot builds", "snapshot alias", "run list", "sandbox list", "sandbox info", "sandbox pause", "sandbox resume", "sandbox extend", "sandbox probe", "files rm", "secrets list", "secrets delete", "image resolve", "sandbox snapshot", "files search", "files mkdir", "files mv", "files cp", "files chmod", "exec stdin", "sandbox autopause", "agent start", "referrals", "referrals apply", "credits", "credits claim", "runners github", "runners gitlab", "pricing", "github connect", "github status", "github disconnect", "github untrack", "repos", "gitlab connect", "gitlab status", "gitlab disconnect", "gitlab untrack"];
9
+ export declare const LIVE_COMMANDS: readonly ["login", "signup", "logout", "auth login", "keys list", "keys create", "keys revoke", "org list", "org create", "org switch", "org members", "org invite", "org invites", "org uninvite", "org resend", "org role", "org remove", "org leave", "org join", "org audit", "org rename", "org delete", "project list", "project create", "project rename", "project delete", "project cap", "project move", "me", "billing card", "doctor", "ping", "init", "examples", "templates", "templates show", "agent-guide", "mcp", "snapshot create", "snapshot status", "snapshot list", "snapshot inspect", "snapshot verify", "snapshot export", "fork", "exec", "exec output", "exec ps", "exec kill", "files put", "files get", "files ls", "run", "run quote", "run report", "run status", "run cancel", "run kill", "run replay", "run open", "run captures", "sandbox kill", "sandbox shell", "sandbox ssh", "sandbox network", "port expose", "port list", "port revoke", "secrets set", "bench burst", "status", "billing usage", "billing portal", "snapshot delete", "snapshot builds", "snapshot alias", "run list", "sandbox list", "sandbox info", "sandbox pause", "sandbox resume", "sandbox extend", "sandbox probe", "files rm", "secrets list", "secrets delete", "image resolve", "sandbox snapshot", "files search", "files mkdir", "files mv", "files cp", "files chmod", "exec stdin", "sandbox autopause", "agent start", "referrals", "referrals apply", "credits", "credits claim", "runners github", "runners gitlab", "pricing", "github connect", "github status", "github disconnect", "github untrack", "repos", "gitlab connect", "gitlab status", "gitlab disconnect", "gitlab untrack"];
10
10
  export declare const GUIDE_COMMON_ERRORS: readonly ["auth", "not_found", "not_verified", "cap_exceeded", "timeout", "nonzero_exit"];
11
11
  export type Group = "Setup" | "Snapshots" | "Runs" | "Sandboxes" | "Ops";
12
12
  export type CommandStatus = "planned" | "released";
package/dist/surface.js CHANGED
@@ -10,7 +10,7 @@ export const LIVE_COMMANDS = [
10
10
  "login", "signup", "logout", "auth login", "keys list", "keys create", "keys revoke",
11
11
  "org list", "org create", "org switch", "org members", "org invite", "org invites", "org uninvite", "org resend", "org role", "org remove", "org leave", "org join", "org audit", "org rename", "org delete",
12
12
  "project list", "project create", "project rename", "project delete", "project cap", "project move",
13
- "me", "billing card", "doctor", "init", "examples", "templates", "templates show", "agent-guide", "mcp",
13
+ "me", "billing card", "doctor", "ping", "init", "examples", "templates", "templates show", "agent-guide", "mcp",
14
14
  "snapshot create", "snapshot status", "snapshot list", "snapshot inspect", "snapshot verify",
15
15
  "snapshot export",
16
16
  "fork", "exec", "exec output", "exec ps", "exec kill", "files put", "files get", "files ls",
@@ -322,6 +322,13 @@ export const COMMANDS = [
322
322
  flags: [json],
323
323
  next: ["promigence examples"],
324
324
  },
325
+ {
326
+ name: "ping", group: "Setup", status: "released",
327
+ helpRow: { key: "doctor", summary: "" },
328
+ summary: "your distance to our servers",
329
+ flags: [json],
330
+ next: ["promigence doctor"],
331
+ },
325
332
  {
326
333
  name: "init", group: "Setup", status: "released",
327
334
  summary: "write promigence.toml",
package/dist/timing.js ADDED
@@ -0,0 +1,71 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ const seen = [];
5
+ export function recordTiming(t) {
6
+ seen.push(t);
7
+ }
8
+ export function timings() {
9
+ return seen.filter((t) => t.ourMs !== null);
10
+ }
11
+ export function networkMs(list = timings()) {
12
+ let best;
13
+ for (const t of list) {
14
+ if (t.ourMs === null)
15
+ continue;
16
+ const n = Math.max(0, t.totalMs - t.ourMs);
17
+ if (best === undefined || n < best)
18
+ best = n;
19
+ }
20
+ return best;
21
+ }
22
+ export function ourSideLine(ourMs, totalMs) {
23
+ const ours = `${fmtMs(ourMs)} on our side`;
24
+ return totalMs === undefined ? ours : `${ours} · ${fmtMs(totalMs)} round trip`;
25
+ }
26
+ export function fmtMs(ms) {
27
+ return ms >= 10 ? `${Math.round(ms)} ms` : `${Math.round(ms * 10) / 10} ms`;
28
+ }
29
+ export function where(location) {
30
+ return location ? `our servers on the ${location}` : "our servers";
31
+ }
32
+ export const FAR_MS = 150;
33
+ export function farMarker(env = process.env) {
34
+ const base = env["XDG_CONFIG_HOME"] ? join(env["XDG_CONFIG_HOME"], "promigence") : join(homedir(), ".config", "promigence");
35
+ return join(base, "far-notice-shown");
36
+ }
37
+ export function farNoticeText(ms, location) {
38
+ return `promigence: You're about ${Math.round(ms)} ms from ${where(location)}. ` +
39
+ "Times marked 'on our side' are the platform's own; the rest is your network. " +
40
+ "(Shown once. `promigence ping` measures it again.)\n";
41
+ }
42
+ export async function maybeFarNotice(opts) {
43
+ const env = opts.env ?? process.env;
44
+ if (opts.json || env["PROMIGENCE_QUIET"] === "1")
45
+ return undefined;
46
+ if (!(opts.isTTY ?? process.stderr.isTTY))
47
+ return undefined;
48
+ const ms = networkMs(opts.list ?? timings());
49
+ if (ms === undefined || ms < FAR_MS)
50
+ return undefined;
51
+ const marker = farMarker(env);
52
+ if (existsSync(marker))
53
+ return undefined;
54
+ const loc = await opts.location().catch(() => undefined);
55
+ const text = farNoticeText(ms, loc);
56
+ (opts.write ?? ((s) => process.stderr.write(s)))(text);
57
+ try {
58
+ mkdirSync(join(marker, ".."), { recursive: true, mode: 0o700 });
59
+ writeFileSync(marker, `${new Date().toISOString()} ${Math.round(ms)}\n`, { mode: 0o600 });
60
+ }
61
+ catch {
62
+ }
63
+ return text;
64
+ }
65
+ export function median(xs) {
66
+ if (!xs.length)
67
+ return undefined;
68
+ const s = [...xs].sort((a, b) => a - b);
69
+ const m = s.length >> 1;
70
+ return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
71
+ }
package/llms.txt CHANGED
@@ -1,4 +1,4 @@
1
- # Promigence — promigence@0.1.0-beta.2
1
+ # Promigence — promigence@0.1.0-beta.3
2
2
 
3
3
  > Verified, reproducible, forkable execution environments for agents. The CLI is the
4
4
  > `promigence` command (package `promigence`, which is also the TypeScript SDK).
@@ -36,6 +36,7 @@
36
36
  - `promigence me` — plan, concurrency in use, trial credit, spend, limits
37
37
  - `promigence billing card` — add a card: prints a checkout URL (trial credits kept)
38
38
  - `promigence doctor` — check client, auth and API
39
+ - `promigence ping` — your distance to our servers
39
40
  - `promigence init` — write promigence.toml
40
41
  - `promigence examples` — what forks with no build (no auth)
41
42
  - `promigence templates` — the templates: ready-made starting points to fork (no auth)
@@ -125,7 +126,7 @@
125
126
 
126
127
  ---
127
128
 
128
- # Promigence — agent guide (promigence@0.1.0-beta.2)
129
+ # Promigence — agent guide (promigence@0.1.0-beta.3)
129
130
 
130
131
  Promigence is an **environment runtime**: verified, reproducible, forkable execution
131
132
  environments, billed per second (vCPU + memory). The model is one line:
@@ -195,4 +196,4 @@ Highest-precedence code wins: 2 > 5 > 6 > 7/8/9/10 > 11 > 3 > 12 > 4 > 1 > 0.
195
196
  · `promigence <command> --help` · https://promigence.ai
196
197
  Same surface as MCP tools: `promigence mcp` (stdio; `claude mcp add promigence -- promigence mcp`).
197
198
 
198
- Live in v0.1.0-beta.2: login · signup · logout · auth login · keys list/create/revoke · org list/create/switch/members/invite/invites/uninvite/resend/role/remove/leave/join/audit/rename/delete · project list/create/rename/delete/cap/move · me · billing card/usage/portal · doctor · init · examples · templates +show · agent-guide · mcp · snapshot create/status/list/inspect/verify/export/delete/builds/alias · image resolve · github connect/status/disconnect/untrack · repos · gitlab connect/status/disconnect/untrack · run +quote/report/status/cancel/kill/replay/open/captures/list · fork · exec +output/ps/stdin/kill · files put/get/ls/search/mkdir/mv/cp/chmod/rm · agent start · sandbox kill/network/shell/ssh/list/pause/resume/snapshot/info/extend/autopause/probe · port expose/list/revoke · secrets set/list/delete · bench burst · runners github/gitlab · status · referrals +apply · pricing · credits +claim
199
+ Live in v0.1.0-beta.3: login · signup · logout · auth login · keys list/create/revoke · org list/create/switch/members/invite/invites/uninvite/resend/role/remove/leave/join/audit/rename/delete · project list/create/rename/delete/cap/move · me · billing card/usage/portal · doctor · ping · init · examples · templates +show · agent-guide · mcp · snapshot create/status/list/inspect/verify/export/delete/builds/alias · image resolve · github connect/status/disconnect/untrack · repos · gitlab connect/status/disconnect/untrack · run +quote/report/status/cancel/kill/replay/open/captures/list · fork · exec +output/ps/stdin/kill · files put/get/ls/search/mkdir/mv/cp/chmod/rm · agent start · sandbox kill/network/shell/ssh/list/pause/resume/snapshot/info/extend/autopause/probe · port expose/list/revoke · secrets set/list/delete · bench burst · runners github/gitlab · status · referrals +apply · pricing · credits +claim
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promigence",
3
- "version": "0.1.0-beta.2",
3
+ "version": "0.1.0-beta.3",
4
4
  "description": "The fastest sandboxes for AI agents and code. Snapshot an environment once, fork it as often as you need, run commands in every copy, and get a report that tells environment failures from task failures. Billed per second, with a quote before a run and a hard spend cap. One package: the promigence command and the TypeScript SDK.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://www.promigence.ai",