trajectories-sh 1.18.0 → 1.18.2

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.
@@ -0,0 +1,55 @@
1
+ import {
2
+ getApiUrl,
3
+ getAuthHeader
4
+ } from "./chunk-Q2ZEN43L.js";
5
+
6
+ // src/quota.ts
7
+ async function fetchOrgUsage() {
8
+ const auth = await getAuthHeader();
9
+ if (!auth) return null;
10
+ try {
11
+ const resp = await fetch(`${getApiUrl()}/api/cli/usage`, {
12
+ headers: { Authorization: auth },
13
+ signal: AbortSignal.timeout(1e4)
14
+ });
15
+ if (!resp.ok) return null;
16
+ return await resp.json();
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
21
+ function planUpgradeLabel(plan) {
22
+ return plan === "free" ? "Team" : "Premiere";
23
+ }
24
+ async function preflightQuota(check) {
25
+ const usage = await fetchOrgUsage();
26
+ if (!usage) return;
27
+ if (usage.plan === "premiere") return;
28
+ const plan = planUpgradeLabel(usage.plan);
29
+ if (check.kind === "trajectory") {
30
+ if (usage.trajectory_count >= usage.trajectory_limit) {
31
+ throw new Error(
32
+ `Trajectory limit reached (${usage.trajectory_count.toLocaleString()}/${usage.trajectory_limit.toLocaleString()}). Upgrade to ${plan} for more: https://trajectories.sh/pricing`
33
+ );
34
+ }
35
+ const projected = usage.storage_bytes_used + check.projectedAddedBytes;
36
+ if (projected > usage.storage_limit_bytes) {
37
+ const usedGB = (usage.storage_bytes_used / 1024 ** 3).toFixed(1);
38
+ const uploadGB = (check.projectedAddedBytes / 1024 ** 3).toFixed(1);
39
+ const limitGB = (usage.storage_limit_bytes / 1024 ** 3).toFixed(0);
40
+ throw new Error(
41
+ `Storage limit would be exceeded (${usedGB} GB used + ${uploadGB} GB upload > ${limitGB} GB limit). Upgrade to ${plan} for more: https://trajectories.sh/pricing`
42
+ );
43
+ }
44
+ return;
45
+ }
46
+ if (usage.task_count + check.newTaskCount > usage.task_limit) {
47
+ throw new Error(
48
+ `Task limit reached (${usage.task_count.toLocaleString()}/${usage.task_limit.toLocaleString()}). Upgrade to ${plan} for more: https://trajectories.sh/pricing`
49
+ );
50
+ }
51
+ }
52
+
53
+ export {
54
+ preflightQuota
55
+ };
package/dist/cli.js CHANGED
@@ -64,11 +64,18 @@ auth.command("status").description("Show current authentication status").action(
64
64
  console.log(chalk.dim(" Not logged in. Run: trajectories auth login"));
65
65
  }
66
66
  });
67
- async function uploadTrajectoryAction(directory, opts) {
67
+ async function uploadTrajectoryAction(pathArg, opts) {
68
68
  const chalk = (await import("chalk")).default;
69
69
  const { existsSync, statSync } = await import("fs");
70
- if (!existsSync(directory) || !statSync(directory).isDirectory()) {
71
- console.error(chalk.red(` \u2717 Directory not found: ${directory}`));
70
+ const exists = existsSync(pathArg);
71
+ const isZip = pathArg.toLowerCase().endsWith(".zip");
72
+ const isDir = exists && statSync(pathArg).isDirectory();
73
+ if (!exists) {
74
+ console.error(chalk.red(` \u2717 Path not found: ${pathArg}`));
75
+ process.exit(1);
76
+ }
77
+ if (!isDir && !isZip) {
78
+ console.error(chalk.red(` \u2717 ${pathArg} is not a directory or .zip file`));
72
79
  process.exit(1);
73
80
  }
74
81
  if (opts.apiKey) {
@@ -90,13 +97,38 @@ async function uploadTrajectoryAction(directory, opts) {
90
97
  )
91
98
  );
92
99
  }
93
- const slug = opts.slug ?? directory.replace(/\/+$/, "").split("/").pop();
100
+ if (isZip) {
101
+ const { uploadZip } = await import("./upload-3CRW3BJ6.js");
102
+ const slug2 = opts.slug ?? pathArg.replace(/\.zip$/i, "").replace(/\/+$/, "").split("/").pop();
103
+ console.log(chalk.bold(`
104
+ Uploading ZIP ${pathArg} \u2192 ${chalk.cyan(slug2)}
105
+ `));
106
+ try {
107
+ const results = await uploadZip(pathArg, { slug: slug2, name: opts.name });
108
+ for (const r of results) {
109
+ console.log(chalk.green.bold(` \u2713 ${r.slug}`));
110
+ console.log(` Trials: ${r.nTrials}`);
111
+ if (r.meanReward != null) {
112
+ console.log(` Pass rate: ${(r.meanReward * 100).toFixed(0)}%`);
113
+ }
114
+ console.log(` Viewer: ${r.viewerUrl}`);
115
+ }
116
+ console.log("");
117
+ } catch (e) {
118
+ console.error(chalk.red(`
119
+ \u2717 ${e.message}
120
+ `));
121
+ process.exit(1);
122
+ }
123
+ return;
124
+ }
125
+ const slug = opts.slug ?? pathArg.replace(/\/+$/, "").split("/").pop();
94
126
  console.log(chalk.bold(`
95
- Uploading trajectory ${directory} \u2192 ${chalk.cyan(slug)}
127
+ Uploading trajectory ${pathArg} \u2192 ${chalk.cyan(slug)}
96
128
  `));
97
129
  try {
98
- const { uploadJob } = await import("./upload-NR7SFMZZ.js");
99
- const result = await uploadJob(directory, {
130
+ const { uploadJob } = await import("./upload-3CRW3BJ6.js");
131
+ const result = await uploadJob(pathArg, {
100
132
  slug,
101
133
  name: opts.name
102
134
  });
@@ -141,7 +173,7 @@ async function uploadTaskAction(directory, opts) {
141
173
  Uploading task ${directory} \u2192 ${chalk.cyan(name)}
142
174
  `));
143
175
  try {
144
- const { pushTask } = await import("./push-task-IZZNF3ZA.js");
176
+ const { pushTask } = await import("./push-task-OZTAROKW.js");
145
177
  const result = await pushTask(directory, {
146
178
  name: opts.name,
147
179
  visibility: opts.visibility
@@ -242,7 +274,7 @@ async function deleteTrajectoryAction(url, opts) {
242
274
  }
243
275
  }
244
276
  var upload = program.command("upload").description("Upload trajectories or tasks");
245
- upload.command("trajectory <directory>").description("Upload a trajectory job directory (always uploaded as unlisted)").option("-s, --slug <slug>", "URL slug (default: directory name)").option("-n, --name <name>", "Display name (default: slug)").option("--api-key <key>", "API key (or set TRAJECTORIES_API_KEY in your env)").option("-v, --visibility <vis>", "(deprecated, ignored \u2014 set visibility in the UI)").action(uploadTrajectoryAction);
277
+ upload.command("trajectory <path>").description("Upload a trajectory job directory or ZIP file (always uploaded as unlisted)").option("-s, --slug <slug>", "URL slug (default: directory name)").option("-n, --name <name>", "Display name (default: slug)").option("--api-key <key>", "API key (or set TRAJECTORIES_API_KEY in your env)").option("-v, --visibility <vis>", "(deprecated, ignored \u2014 set visibility in the UI)").action(uploadTrajectoryAction);
246
278
  upload.command("task <directory>").description("Upload a benchmark task directory").option("-n, --name <name>", "Task name (default: directory name)").option("--api-key <key>", "API key (or set TRAJECTORIES_API_KEY in your env)").option("-v, --visibility <vis>", "public or private", "public").action(uploadTaskAction);
247
279
  var download = program.command("download").description("Download trajectories");
248
280
  download.command("trajectory <url>").description("Download a trajectory job from trajectories.sh").option("-o, --output <dir>", "Output directory (default: slug name)").action(downloadTrajectoryAction);
@@ -1,3 +1,6 @@
1
+ import {
2
+ preflightQuota
3
+ } from "./chunk-2ELJH3IV.js";
1
4
  import {
2
5
  getApiUrl,
3
6
  getAuthHeader
@@ -116,18 +119,9 @@ async function pushTask(dir, opts) {
116
119
  "No task content found. A task needs at least an instruction.md, Dockerfile, or test file."
117
120
  );
118
121
  }
119
- const apiUrl = getApiUrl();
120
- try {
121
- const usageResp = await fetch(`${apiUrl}/api/cli/usage`, {
122
- headers: { Authorization: auth },
123
- signal: AbortSignal.timeout(1e4)
124
- });
125
- if (usageResp.ok) {
126
- const usage = await usageResp.json();
127
- }
128
- } catch {
129
- }
122
+ await preflightQuota({ kind: "task", newTaskCount: 1 });
130
123
  console.log(" Uploading...");
124
+ const apiUrl = getApiUrl();
131
125
  const resp = await fetch(`${apiUrl}/api/cli/push-task`, {
132
126
  method: "POST",
133
127
  headers: {
@@ -1,3 +1,6 @@
1
+ import {
2
+ preflightQuota
3
+ } from "./chunk-2ELJH3IV.js";
1
4
  import {
2
5
  getApiUrl,
3
6
  getAuthHeader
@@ -10,7 +13,7 @@ import { tmpdir } from "os";
10
13
  import { randomBytes, createHash } from "crypto";
11
14
  var SKIP = /* @__PURE__ */ new Set(["__pycache__", ".DS_Store", ".git", "node_modules"]);
12
15
  var MAX_FILE_SIZE = 50 * 1024 * 1024;
13
- var CONCURRENCY = Number(process.env.TRAJECTORIES_CLI_CONCURRENCY ?? "512");
16
+ var CONCURRENCY = Number(process.env.TRAJECTORIES_CLI_CONCURRENCY ?? "256");
14
17
  var BATCH_SIZE = Number(process.env.TRAJECTORIES_CLI_BATCH_SIZE ?? "500");
15
18
  var WEBP_CONCURRENCY = Number(process.env.TRAJECTORIES_CLI_WEBP_CONCURRENCY ?? "4");
16
19
  var HASH_CONCURRENCY = Number(process.env.TRAJECTORIES_CLI_HASH_CONCURRENCY ?? "16");
@@ -47,6 +50,48 @@ async function apiPost(path, body, timeout = 3e5) {
47
50
  signal: AbortSignal.timeout(timeout)
48
51
  });
49
52
  }
53
+ async function uploadZip(zipPath, opts) {
54
+ const { readFileSync: readFileSync2, statSync: statSync2 } = await import("fs");
55
+ const { getApiUrl: _getApiUrl, getAuthHeader: _getAuthHeader } = await import("./config-4XJRECIW.js");
56
+ const auth = await _getAuthHeader();
57
+ if (!auth) throw new Error("Not authenticated. Run: trajectories auth login");
58
+ const fileStat = statSync2(zipPath);
59
+ const sizeMB = (fileStat.size / (1024 * 1024)).toFixed(1);
60
+ console.log(` ZIP size: ${sizeMB} MB`);
61
+ console.log(" Uploading to server (server-side extraction)...");
62
+ const fileData = readFileSync2(zipPath);
63
+ const formData = new FormData();
64
+ formData.append("file", new Blob([fileData]), zipPath.split("/").pop() ?? "upload.zip");
65
+ if (opts.slug) formData.append("slug", opts.slug);
66
+ if (opts.name) formData.append("name", opts.name);
67
+ formData.append("visibility", "unlisted");
68
+ const apiUrl = _getApiUrl();
69
+ const resp = await fetch(`${apiUrl}/api/cli/push/zip`, {
70
+ method: "POST",
71
+ headers: { Authorization: auth },
72
+ body: formData,
73
+ signal: AbortSignal.timeout(36e5)
74
+ });
75
+ if (!resp.ok) {
76
+ let detail;
77
+ try {
78
+ const body = await resp.json();
79
+ detail = body.detail || JSON.stringify(body);
80
+ } catch {
81
+ detail = await resp.text();
82
+ }
83
+ throw new Error(`ZIP upload failed (${resp.status}): ${detail}`);
84
+ }
85
+ const result = await resp.json();
86
+ const siteUrl = apiUrl.includes("localhost") ? "http://localhost:3000" : apiUrl.replace("api.", "").replace(/\/$/, "");
87
+ return result.jobs.map((j) => ({
88
+ jobId: j.job_id,
89
+ slug: j.slug,
90
+ nTrials: j.n_trials,
91
+ meanReward: j.mean_reward ?? void 0,
92
+ viewerUrl: `${siteUrl}/t/${j.job_id}`
93
+ }));
94
+ }
50
95
  async function convertToWebP(dir) {
51
96
  const { files } = collectFiles(dir);
52
97
  const pngs = files.filter((f) => f.relPath.toLowerCase().endsWith(".png"));
@@ -328,40 +373,11 @@ ${samples}${more}
328
373
  `
329
374
  );
330
375
  }
376
+ await preflightQuota({ kind: "trajectory", projectedAddedBytes: totalSize });
331
377
  process.stdout.write(` Hashing...\r`);
332
378
  const t0 = Date.now();
333
379
  const contentHash = await computeContentHash(files);
334
380
  console.log(` Hash: ${contentHash.slice(0, 16)}... (${((Date.now() - t0) / 1e3).toFixed(1)}s)`);
335
- try {
336
- const auth = await getAuthHeader();
337
- const usageResp = await fetch(`${getApiUrl()}/api/cli/usage`, {
338
- headers: auth ? { Authorization: auth } : {},
339
- signal: AbortSignal.timeout(1e4)
340
- });
341
- if (usageResp.status === 200) {
342
- const usage = await usageResp.json();
343
- if (usage.plan !== "premiere") {
344
- if (usage.trajectory_count >= usage.trajectory_limit) {
345
- const plan = usage.plan === "free" ? "Team" : "Premiere";
346
- throw new Error(
347
- `Trajectory limit reached (${usage.trajectory_count.toLocaleString()}/${usage.trajectory_limit.toLocaleString()}). Upgrade to ${plan} for more: https://trajectories.sh/pricing`
348
- );
349
- }
350
- const projectedStorage = usage.storage_bytes_used + totalSize;
351
- if (projectedStorage > usage.storage_limit_bytes) {
352
- const usedGB = (usage.storage_bytes_used / 1024 ** 3).toFixed(1);
353
- const uploadGB = (totalSize / 1024 ** 3).toFixed(1);
354
- const limitGB = (usage.storage_limit_bytes / 1024 ** 3).toFixed(0);
355
- const plan = usage.plan === "free" ? "Team" : "Premiere";
356
- throw new Error(
357
- `Storage limit would be exceeded (${usedGB} GB used + ${uploadGB} GB upload > ${limitGB} GB limit). Upgrade to ${plan} for more: https://trajectories.sh/pricing`
358
- );
359
- }
360
- }
361
- }
362
- } catch (e) {
363
- if (e.message?.includes("limit")) throw e;
364
- }
365
381
  const initResp = await apiPost("/api/cli/push/init", { slug, name, visibility, estimated_bytes: totalSize, content_hash: contentHash });
366
382
  if (!initResp.ok) {
367
383
  throw new Error(`Init failed: ${await initResp.text()}`);
@@ -514,7 +530,7 @@ async function fetchSignedUrlsForBatch(jobId, paths) {
514
530
  async function uploadOneFileWithError(file, signedUrl) {
515
531
  const data = readFileSync(file.absPath);
516
532
  let lastError = "unknown error";
517
- for (let attempt = 0; attempt < 3; attempt++) {
533
+ for (let attempt = 0; attempt < 5; attempt++) {
518
534
  try {
519
535
  const resp = await fetch(signedUrl, {
520
536
  method: "PUT",
@@ -529,10 +545,14 @@ async function uploadOneFileWithError(file, signedUrl) {
529
545
  if (body) lastError += ` \u2014 ${body.slice(0, 150).replace(/\s+/g, " ")}`;
530
546
  } catch {
531
547
  }
548
+ if (resp.status === 503 || resp.status === 429) {
549
+ await sleep(2e3 * (attempt + 1));
550
+ continue;
551
+ }
532
552
  } catch (e) {
533
553
  lastError = `fetch error: ${e?.name ?? "Error"} ${e?.message ?? e}`;
534
554
  }
535
- if (attempt < 2) await sleep(1e3 * (attempt + 1));
555
+ if (attempt < 4) await sleep(1e3 * (attempt + 1));
536
556
  }
537
557
  return { ok: false, error: lastError };
538
558
  }
@@ -544,5 +564,6 @@ function truncate(s, n) {
544
564
  }
545
565
  export {
546
566
  collectFiles,
547
- uploadJob
567
+ uploadJob,
568
+ uploadZip
548
569
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trajectories-sh",
3
- "version": "1.18.0",
3
+ "version": "1.18.2",
4
4
  "description": "CLI for uploading trajectory jobs to trajectories.sh",
5
5
  "type": "module",
6
6
  "bin": {