trajectories-sh 1.23.1 → 1.24.0

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.
@@ -5,7 +5,7 @@ import {
5
5
  isLoggedIn,
6
6
  saveApiKey,
7
7
  saveSession
8
- } from "./chunk-Q2ZEN43L.js";
8
+ } from "./chunk-ERO5HP43.js";
9
9
 
10
10
  // src/auth.ts
11
11
  import { createServer } from "http";
@@ -149,6 +149,7 @@ function unpackScreenshotChunks(jobDir) {
149
149
 
150
150
  export {
151
151
  CHUNK_DIR,
152
+ mimeFor,
152
153
  buildScreenshotChunks,
153
154
  unpackScreenshotChunks
154
155
  };
@@ -29,7 +29,7 @@ function getAccessToken() {
29
29
  return loadConfig().access_token;
30
30
  }
31
31
  async function getAuthHeader() {
32
- const envKey = process.env.TRAJECTORIES_API_KEY;
32
+ const envKey = process.env.TRAJECTORIES_API_KEY || process.env.TRAJECTORIES_TOKEN;
33
33
  if (envKey && envKey.trim()) return `Bearer ${envKey.trim()}`;
34
34
  const config = loadConfig();
35
35
  if (config.api_key) return `Bearer ${config.api_key}`;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getApiUrl,
3
3
  getAuthHeader
4
- } from "./chunk-Q2ZEN43L.js";
4
+ } from "./chunk-ERO5HP43.js";
5
5
 
6
6
  // src/quota.ts
7
7
  async function fetchOrgUsage() {
@@ -0,0 +1,242 @@
1
+ import {
2
+ CHUNK_DIR,
3
+ mimeFor,
4
+ unpackScreenshotChunks
5
+ } from "./chunk-72L6XPK6.js";
6
+ import {
7
+ getApiUrl,
8
+ getAuthHeader
9
+ } from "./chunk-ERO5HP43.js";
10
+
11
+ // src/download.ts
12
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
13
+ import { join, dirname } from "path";
14
+ var UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
15
+ var BATCH_SIZE = 100;
16
+ var CONCURRENCY = 25;
17
+ var DOWNLOAD_TIMEOUT_MS = 18e4;
18
+ var EXIT = {
19
+ OK: 0,
20
+ ERROR: 1,
21
+ USAGE: 2,
22
+ AUTH_REQUIRED: 3,
23
+ NOT_FOUND: 4
24
+ };
25
+ var CliError = class extends Error {
26
+ constructor(message, exitCode = EXIT.ERROR) {
27
+ super(message);
28
+ this.exitCode = exitCode;
29
+ }
30
+ exitCode;
31
+ };
32
+ async function httpError(resp, what) {
33
+ let detail = "";
34
+ try {
35
+ const body = await resp.json();
36
+ detail = typeof body.detail === "string" ? body.detail : "";
37
+ if (resp.status === 401 || resp.status === 403) {
38
+ return new CliError(
39
+ `${what}: authentication required (${resp.status}). ${body.how_to_authenticate?.cli ?? "Set TRAJECTORIES_API_KEY in the environment."}`,
40
+ EXIT.AUTH_REQUIRED
41
+ );
42
+ }
43
+ if (resp.status === 404) {
44
+ return new CliError(
45
+ `${what}: not found (404)${detail ? `: ${detail}` : ""}. If it is private, set TRAJECTORIES_API_KEY for an org member's key.`,
46
+ EXIT.NOT_FOUND
47
+ );
48
+ }
49
+ } catch {
50
+ }
51
+ return new CliError(`${what}: HTTP ${resp.status}${detail ? ` ${detail}` : ""}`, EXIT.ERROR);
52
+ }
53
+ function parseJobId(input) {
54
+ const trimmed = input.trim();
55
+ if (UUID_RE.test(trimmed) && !trimmed.includes("/")) {
56
+ return trimmed.match(UUID_RE)[0];
57
+ }
58
+ const shortMatch = trimmed.match(/\/t\/([0-9a-f-]{36})/i);
59
+ if (shortMatch) return shortMatch[1];
60
+ const trajMatch = trimmed.match(/\/trajectories\/([0-9a-f-]{36})/i);
61
+ if (trajMatch) return trajMatch[1];
62
+ const trajViewerMatch = trimmed.match(/\/traj\/([0-9a-f-]{36})\/([^/]+)/i);
63
+ if (trajViewerMatch) {
64
+ return `org:${trajViewerMatch[1]}:${trajViewerMatch[2]}`;
65
+ }
66
+ const anyUuid = trimmed.match(UUID_RE);
67
+ if (anyUuid) return anyUuid[0];
68
+ throw new CliError(`Could not find a trajectory job ID in: ${input}`, EXIT.USAGE);
69
+ }
70
+ function parseProjectId(input) {
71
+ const trimmed = input.trim();
72
+ const m = trimmed.match(/\/projects\/([0-9a-f-]{36})/i) ?? (trimmed.includes("/") ? null : trimmed.match(UUID_RE));
73
+ if (!m) throw new CliError(`Could not find a project ID in: ${input}`, EXIT.USAGE);
74
+ return m[1] ?? m[0];
75
+ }
76
+ function isScreenshot(path) {
77
+ if (path.includes(`/${CHUNK_DIR}/`)) return true;
78
+ const parts = path.split("/");
79
+ return parts.length >= 3 && parts[1] === "agent" && mimeFor(parts[parts.length - 1]) != null;
80
+ }
81
+ async function planJob(input, options) {
82
+ const apiUrl = getApiUrl();
83
+ const auth = await getAuthHeader();
84
+ const headers = {};
85
+ if (auth) headers["Authorization"] = auth;
86
+ let jobId = parseJobId(input);
87
+ if (jobId.startsWith("org:")) {
88
+ const [, orgId, slug2] = jobId.split(":");
89
+ const configResp = await fetch(`${apiUrl}/api/viewer/${orgId}/${slug2}/api/jobs`, {
90
+ headers,
91
+ signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS)
92
+ });
93
+ if (!configResp.ok) throw await httpError(configResp, `Resolve job ${orgId}/${slug2}`);
94
+ const items = (await configResp.json()).items || [];
95
+ if (!items[0]?.id) throw new CliError(`Could not resolve job ID for ${orgId}/${slug2}`, EXIT.NOT_FOUND);
96
+ jobId = items[0].id;
97
+ }
98
+ const listResp = await fetch(`${apiUrl}/api/cli/download/${jobId}/files`, {
99
+ headers,
100
+ signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS)
101
+ });
102
+ if (!listResp.ok) throw await httpError(listResp, `List files for ${jobId}`);
103
+ const { slug, files: all } = await listResp.json();
104
+ const files = options.includeScreenshots ? all : all.filter((f) => !isScreenshot(f.path));
105
+ const trials = new Set(files.map((f) => f.path.split("/")[0]).filter((p) => p && !p.endsWith(".json") && !p.endsWith(".log")));
106
+ return {
107
+ jobId,
108
+ slug,
109
+ files,
110
+ bytes: files.reduce((s, f) => s + f.size, 0),
111
+ trials: trials.size,
112
+ trajectoryFiles: files.filter((f) => f.path.endsWith("/agent/trajectory.json")).length,
113
+ screenshotsSkipped: all.length - files.length
114
+ };
115
+ }
116
+ async function downloadPlannedJob(plan, outputDir, options) {
117
+ const apiUrl = getApiUrl();
118
+ const auth = await getAuthHeader();
119
+ const headers = {};
120
+ if (auth) headers["Authorization"] = auth;
121
+ const { jobId, files } = plan;
122
+ const log = options.quiet ? () => {
123
+ } : (m) => console.log(m);
124
+ const packedTrials = new Set(files.filter((f) => f.path.includes(`/${CHUNK_DIR}/`)).map((f) => f.path.split("/")[0]));
125
+ log(` Job: ${plan.slug} (${jobId})`);
126
+ log(
127
+ ` ${files.length} files, ${(plan.bytes / 1024 / 1024).toFixed(1)} MB` + (packedTrials.size > 0 ? ` (screenshots of ${packedTrials.size} trial${packedTrials.size === 1 ? "" : "s"} packed)` : "") + (plan.screenshotsSkipped > 0 ? ` (${plan.screenshotsSkipped} screenshot files skipped; --include-screenshots to fetch)` : "")
128
+ );
129
+ mkdirSync(outputDir, { recursive: true });
130
+ const bar = options.quiet ? { tick: (_n = 0) => {
131
+ }, done: () => {
132
+ } } : new (await import("./progress-H7WNKHDR.js")).ProgressBar({ total: files.length, totalBytes: plan.bytes, label: "\u2193" });
133
+ let downloaded = 0;
134
+ let downloadedBytes = 0;
135
+ const failed = [];
136
+ for (let batchStart = 0; batchStart < files.length; batchStart += BATCH_SIZE) {
137
+ const batch = files.slice(batchStart, batchStart + BATCH_SIZE);
138
+ const urlResp = await fetch(`${apiUrl}/api/cli/download/${jobId}/signed-urls`, {
139
+ method: "POST",
140
+ headers: { ...headers, "Content-Type": "application/json" },
141
+ body: JSON.stringify({ paths: batch.map((f) => f.path) }),
142
+ signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS)
143
+ });
144
+ const urlMap = /* @__PURE__ */ new Map();
145
+ if (urlResp.ok) {
146
+ const { urls } = await urlResp.json();
147
+ for (const u of urls) if (u.signed_url) urlMap.set(u.path, u.signed_url);
148
+ }
149
+ let next = 0;
150
+ const worker = async () => {
151
+ while (next < batch.length) {
152
+ const file = batch[next++];
153
+ const signedUrl = urlMap.get(file.path);
154
+ const downloadUrl = signedUrl ?? `${apiUrl}/api/cli/download/${jobId}/file?path=${encodeURIComponent(file.path)}`;
155
+ const downloadHeaders = signedUrl ? {} : headers;
156
+ let ok = false;
157
+ for (let attempt = 0; attempt < 3 && !ok; attempt++) {
158
+ try {
159
+ const resp = await fetch(downloadUrl, { headers: downloadHeaders, signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
160
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
161
+ const data = Buffer.from(await resp.arrayBuffer());
162
+ const outPath = join(outputDir, file.path);
163
+ mkdirSync(dirname(outPath), { recursive: true });
164
+ writeFileSync(outPath, data);
165
+ downloaded++;
166
+ downloadedBytes += data.length;
167
+ bar.tick(data.length);
168
+ ok = true;
169
+ } catch {
170
+ if (attempt < 2) await new Promise((r) => setTimeout(r, 1e3 * (attempt + 1)));
171
+ }
172
+ }
173
+ if (!ok) {
174
+ failed.push(file.path);
175
+ bar.tick(0);
176
+ }
177
+ }
178
+ };
179
+ await Promise.all(Array.from({ length: Math.min(CONCURRENCY, batch.length) }, worker));
180
+ }
181
+ bar.done();
182
+ let screenshotsUnpacked = 0;
183
+ if (packedTrials.size > 0) {
184
+ const unpacked = unpackScreenshotChunks(outputDir);
185
+ screenshotsUnpacked = unpacked.imagesWritten;
186
+ if (unpacked.imagesWritten > 0) {
187
+ log(` \u2713 Unpacked ${unpacked.imagesWritten} screenshots across ${unpacked.trialsUnpacked} trial${unpacked.trialsUnpacked === 1 ? "" : "s"}`);
188
+ }
189
+ }
190
+ return { ...plan, outputDir, downloaded, downloadedBytes, failed, screenshotsUnpacked };
191
+ }
192
+ async function downloadJob(input, outputDir, options = { includeScreenshots: true, quiet: false }) {
193
+ const plan = await planJob(input, options);
194
+ return downloadPlannedJob(plan, outputDir, options);
195
+ }
196
+ function writeJsonl(rootDir, jobDirs, outFile) {
197
+ const lines = [];
198
+ for (const job of jobDirs) {
199
+ if (!existsSync(job.dir)) continue;
200
+ for (const trial of readdirSync(job.dir, { withFileTypes: true })) {
201
+ if (!trial.isDirectory()) continue;
202
+ const trajPath = join(job.dir, trial.name, "agent", "trajectory.json");
203
+ if (!existsSync(trajPath)) continue;
204
+ let result = null;
205
+ const resultPath = join(job.dir, trial.name, "result.json");
206
+ if (existsSync(resultPath)) {
207
+ try {
208
+ result = JSON.parse(readFileSync(resultPath, "utf-8"));
209
+ } catch {
210
+ }
211
+ }
212
+ const trajectory = JSON.parse(readFileSync(trajPath, "utf-8"));
213
+ lines.push(JSON.stringify({ job_id: job.jobId, job_slug: job.slug, trial_name: trial.name, result, trajectory }));
214
+ }
215
+ }
216
+ mkdirSync(dirname(outFile), { recursive: true });
217
+ writeFileSync(outFile, lines.map((l) => l + "\n").join(""));
218
+ return lines.length;
219
+ }
220
+ function dirBytes(dir) {
221
+ if (!existsSync(dir)) return 0;
222
+ let total = 0;
223
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
224
+ const p = join(dir, e.name);
225
+ total += e.isDirectory() ? dirBytes(p) : statSync(p).size;
226
+ }
227
+ return total;
228
+ }
229
+
230
+ export {
231
+ DOWNLOAD_TIMEOUT_MS,
232
+ EXIT,
233
+ CliError,
234
+ httpError,
235
+ parseJobId,
236
+ parseProjectId,
237
+ planJob,
238
+ downloadPlannedJob,
239
+ downloadJob,
240
+ writeJsonl,
241
+ dirBytes
242
+ };
package/dist/cli.js CHANGED
@@ -6,14 +6,34 @@ import { readFileSync } from "fs";
6
6
  import { fileURLToPath } from "url";
7
7
  import { dirname, join } from "path";
8
8
  var __dirname = dirname(fileURLToPath(import.meta.url));
9
- var version = "1.23.1";
9
+ var version = "1.24.0";
10
10
  try {
11
11
  const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
12
12
  version = pkg.version;
13
13
  } catch {
14
14
  }
15
15
  var program = new Command();
16
- program.name("trajectories").description("trajectories.sh \u2014 Upload and explore trajectories and tasks").version(version);
16
+ program.exitOverride((err) => {
17
+ if (err.code === "commander.helpDisplayed" || err.code === "commander.version" || err.code === "commander.help") process.exit(0);
18
+ process.exit(2);
19
+ });
20
+ program.name("trajectories").description("trajectories.sh \u2014 Upload and explore trajectories and tasks").version(version).addHelpText(
21
+ "after",
22
+ `
23
+ Examples:
24
+ $ npx trajectories-sh@latest download project https://www.trajectories.sh/projects/<id>
25
+ $ npx trajectories-sh@latest download trajectory https://www.trajectories.sh/t/<id> --out ./run
26
+ $ npx trajectories-sh@latest download project <url> --dry-run --json # plan only, machine-readable
27
+
28
+ Auth:
29
+ Public and unlisted data needs none. For private data set TRAJECTORIES_API_KEY
30
+ (alias: TRAJECTORIES_TOKEN) in the environment; nothing prompts when it is set.
31
+
32
+ Exit codes:
33
+ 0 ok \xB7 1 error \xB7 2 bad arguments \xB7 3 authentication required \xB7 4 not found
34
+
35
+ Machine-readable guide: https://www.trajectories.sh/llms.txt`
36
+ );
17
37
  var auth = program.command("auth").description("Manage authentication");
18
38
  auth.command("login").description("Log in to trajectories.sh via your browser").option("--api-key <key>", "Use an API key instead (recommended for CI)").action(async (opts) => {
19
39
  const ora = (await import("ora")).default;
@@ -21,7 +41,7 @@ auth.command("login").description("Log in to trajectories.sh via your browser").
21
41
  if (opts.apiKey) {
22
42
  const spinner = ora("Validating API key...").start();
23
43
  try {
24
- const { loginWithApiKey } = await import("./auth-4UEZSE4C.js");
44
+ const { loginWithApiKey } = await import("./auth-NNBHKNIF.js");
25
45
  await loginWithApiKey(opts.apiKey);
26
46
  spinner.succeed("Logged in with API key");
27
47
  } catch (e) {
@@ -33,7 +53,7 @@ auth.command("login").description("Log in to trajectories.sh via your browser").
33
53
  console.log(chalk.bold("\n Opening browser to log in...\n"));
34
54
  console.log(chalk.dim(" If the browser doesn't open, visit:"));
35
55
  try {
36
- const { loginWithBrowser } = await import("./auth-4UEZSE4C.js");
56
+ const { loginWithBrowser } = await import("./auth-NNBHKNIF.js");
37
57
  const { email } = await loginWithBrowser();
38
58
  console.log(chalk.green(`
39
59
  \u2713 Logged in as ${email}
@@ -48,13 +68,13 @@ auth.command("login").description("Log in to trajectories.sh via your browser").
48
68
  });
49
69
  auth.command("logout").description("Log out and remove saved credentials").action(async () => {
50
70
  const chalk = (await import("chalk")).default;
51
- const { logout } = await import("./auth-4UEZSE4C.js");
71
+ const { logout } = await import("./auth-NNBHKNIF.js");
52
72
  logout();
53
73
  console.log(chalk.green(" \u2713 Logged out. Credentials removed."));
54
74
  });
55
75
  auth.command("status").description("Show current authentication status").action(async () => {
56
76
  const chalk = (await import("chalk")).default;
57
- const { status } = await import("./auth-4UEZSE4C.js");
77
+ const { status } = await import("./auth-NNBHKNIF.js");
58
78
  const s = status();
59
79
  if (s.loggedIn) {
60
80
  console.log(chalk.green(" \u2713 Authenticated"));
@@ -81,7 +101,7 @@ async function uploadTrajectoryAction(pathArg, opts) {
81
101
  if (opts.apiKey) {
82
102
  process.env.TRAJECTORIES_API_KEY = String(opts.apiKey);
83
103
  }
84
- const { isLoggedIn } = await import("./config-4XJRECIW.js");
104
+ const { isLoggedIn } = await import("./config-QS2IT5DI.js");
85
105
  if (!isLoggedIn()) {
86
106
  console.error(
87
107
  chalk.red(
@@ -98,7 +118,7 @@ async function uploadTrajectoryAction(pathArg, opts) {
98
118
  );
99
119
  }
100
120
  if (isZip) {
101
- const { uploadZip } = await import("./upload-CIW2CHKX.js");
121
+ const { uploadZip } = await import("./upload-6HQ5A2H7.js");
102
122
  const slug2 = opts.slug ?? pathArg.replace(/\.zip$/i, "").replace(/\/+$/, "").split("/").pop();
103
123
  console.log(chalk.bold(`
104
124
  Uploading ZIP ${pathArg} \u2192 ${chalk.cyan(slug2)}
@@ -127,7 +147,7 @@ async function uploadTrajectoryAction(pathArg, opts) {
127
147
  Uploading trajectory ${pathArg} \u2192 ${chalk.cyan(slug)}
128
148
  `));
129
149
  try {
130
- const { uploadJob } = await import("./upload-CIW2CHKX.js");
150
+ const { uploadJob } = await import("./upload-6HQ5A2H7.js");
131
151
  const result = await uploadJob(pathArg, {
132
152
  slug,
133
153
  name: opts.name
@@ -159,7 +179,7 @@ async function uploadTaskAction(directory, opts) {
159
179
  if (opts.apiKey) {
160
180
  process.env.TRAJECTORIES_API_KEY = String(opts.apiKey);
161
181
  }
162
- const { isLoggedIn } = await import("./config-4XJRECIW.js");
182
+ const { isLoggedIn } = await import("./config-QS2IT5DI.js");
163
183
  if (!isLoggedIn()) {
164
184
  console.error(
165
185
  chalk.red(
@@ -173,7 +193,7 @@ async function uploadTaskAction(directory, opts) {
173
193
  Uploading task ${directory} \u2192 ${chalk.cyan(name)}
174
194
  `));
175
195
  try {
176
- const { pushTask } = await import("./push-task-OZTAROKW.js");
196
+ const { pushTask } = await import("./push-task-JGGRVTJ3.js");
177
197
  const result = await pushTask(directory, {
178
198
  name: opts.name,
179
199
  visibility: opts.visibility
@@ -194,30 +214,113 @@ async function uploadTaskAction(directory, opts) {
194
214
  process.exit(1);
195
215
  }
196
216
  }
217
+ function exitWith(e, json) {
218
+ const code = typeof e?.exitCode === "number" ? e.exitCode : 1;
219
+ if (json) {
220
+ console.log(JSON.stringify({ ok: false, error: String(e?.message ?? e), exit_code: code }));
221
+ } else {
222
+ console.error(`
223
+ \u2717 ${e?.message ?? e}
224
+ `);
225
+ }
226
+ process.exit(code);
227
+ }
228
+ function downloadOptions(opts, defaultScreenshots) {
229
+ return {
230
+ includeScreenshots: opts.includeScreenshots === true ? true : opts.screenshots === false ? false : defaultScreenshots,
231
+ quiet: Boolean(opts.json)
232
+ };
233
+ }
197
234
  async function downloadTrajectoryAction(url, opts) {
198
235
  const chalk = (await import("chalk")).default;
199
- console.log(chalk.bold(`
236
+ const json = Boolean(opts.json);
237
+ try {
238
+ const { planJob, downloadPlannedJob, parseJobId } = await import("./download-BWJDAVMM.js");
239
+ const options = downloadOptions(opts, true);
240
+ const plan = await planJob(url, options);
241
+ const outputDir = opts.out ?? opts.output ?? parseJobId(url).replace(/^org:/, "").replace(/:/g, "-");
242
+ if (opts.dryRun) {
243
+ if (json) {
244
+ console.log(JSON.stringify({ ok: true, dry_run: true, job: { id: plan.jobId, slug: plan.slug, trials: plan.trials, files: plan.files.length, bytes: plan.bytes, trajectory_files: plan.trajectoryFiles, screenshots_skipped: plan.screenshotsSkipped }, out: outputDir }));
245
+ } else {
246
+ console.log(chalk.bold(`
247
+ Would download ${plan.slug}`));
248
+ console.log(` ${plan.files.length} files, ${(plan.bytes / 1024 / 1024).toFixed(1)} MB, ${plan.trials} trials, ${plan.trajectoryFiles} trajectory.json`);
249
+ console.log(` Output: ${outputDir}
250
+ `);
251
+ }
252
+ return;
253
+ }
254
+ if (!json) console.log(chalk.bold(`
200
255
  Downloading trajectory...
201
256
  `));
257
+ const result = await downloadPlannedJob(plan, outputDir, options);
258
+ if (json) {
259
+ console.log(JSON.stringify({ ok: result.failed.length === 0, job: { id: result.jobId, slug: result.slug, trials: result.trials, files: result.downloaded, bytes: result.downloadedBytes, trajectory_files: result.trajectoryFiles, screenshots_unpacked: result.screenshotsUnpacked, failed: result.failed }, out: outputDir }));
260
+ } else {
261
+ console.log(chalk.green.bold(` \u2713 Downloaded ${result.downloaded} files`));
262
+ console.log(` Size: ${(result.downloadedBytes / 1024 / 1024).toFixed(1)} MB`);
263
+ console.log(` Output: ${outputDir}
264
+ `);
265
+ if (result.failed.length) console.error(chalk.yellow(` ${result.failed.length} file(s) failed to download.`));
266
+ }
267
+ if (result.failed.length) process.exit(1);
268
+ } catch (e) {
269
+ exitWith(e, json);
270
+ }
271
+ }
272
+ async function downloadProjectAction(url, opts) {
273
+ const chalk = (await import("chalk")).default;
274
+ const json = Boolean(opts.json);
202
275
  try {
203
- const { downloadJob, parseJobId } = await import("./download-M6UVD5S5.js");
204
- const outputDir = opts.output ?? parseJobId(url).replace(/^org:/, "").replace(/:/g, "-");
205
- const result = await downloadJob(url, outputDir);
206
- console.log(chalk.green.bold(` \u2713 Downloaded ${result.files} files`));
207
- console.log(` Size: ${(result.totalSize / 1024 / 1024).toFixed(1)} MB`);
208
- console.log(` Output: ${outputDir}
276
+ const { planProject, downloadProject, defaultProjectDir } = await import("./download-project-XN4OPF3M.js");
277
+ const options = { ...downloadOptions(opts, false), format: opts.format === "jsonl" ? "jsonl" : "json" };
278
+ const plan = await planProject(url, options);
279
+ const outputDir = opts.out ?? defaultProjectDir(plan.project);
280
+ const summary = (jobs2) => ({
281
+ project: { id: plan.project.id, name: plan.project.name, slug: plan.project.slug, jobs: plan.jobs.length, trials: plan.project.n_trials },
282
+ out: outputDir,
283
+ jobs: jobs2,
284
+ files: jobs2.reduce((s, j) => s + j.files, 0),
285
+ bytes: jobs2.reduce((s, j) => s + j.bytes, 0),
286
+ trajectory_files: jobs2.reduce((s, j) => s + j.trajectory_files, 0)
287
+ });
288
+ if (opts.dryRun) {
289
+ const jobs2 = plan.jobs.map((j) => ({ id: j.jobId, slug: j.slug, trials: j.trials, files: j.files.length, bytes: j.bytes, trajectory_files: j.trajectoryFiles }));
290
+ if (json) {
291
+ console.log(JSON.stringify({ ok: true, dry_run: true, ...summary(jobs2) }));
292
+ } else {
293
+ console.log(chalk.bold(`
294
+ Would download ${plan.project.name ?? plan.project.id}`));
295
+ for (const j of jobs2) console.log(` ${j.slug}: ${j.files} files, ${(j.bytes / 1024 / 1024).toFixed(1)} MB, ${j.trials} trials`);
296
+ console.log(` Total: ${plan.files} files, ${(plan.bytes / 1024 / 1024).toFixed(1)} MB, ${plan.trajectoryFiles} trajectory.json \u2192 ${outputDir}
209
297
  `);
298
+ }
299
+ return;
300
+ }
301
+ if (!json) console.log(chalk.bold(`
302
+ Downloading project ${plan.project.name ?? plan.project.id} (${plan.jobs.length} trajectory sets)...`));
303
+ const result = await downloadProject(plan, outputDir, options);
304
+ const jobs = result.jobs.map((j) => ({ id: j.jobId, slug: j.slug, trials: j.trials, files: j.downloaded, bytes: j.downloadedBytes, trajectory_files: j.trajectoryFiles, failed: j.failed }));
305
+ const failed = result.jobs.reduce((s, j) => s + j.failed.length, 0);
306
+ if (json) {
307
+ console.log(JSON.stringify({ ok: failed === 0, ...summary(jobs), jsonl: result.jsonl }));
308
+ } else {
309
+ console.log(chalk.green.bold(`
310
+ \u2713 Downloaded ${plan.jobs.length} trajectory sets, ${jobs.reduce((s, j) => s + j.files, 0)} files`));
311
+ console.log(` Output: ${outputDir}
312
+ `);
313
+ if (failed) console.error(chalk.yellow(` ${failed} file(s) failed to download.`));
314
+ }
315
+ if (failed) process.exit(1);
210
316
  } catch (e) {
211
- console.error(chalk.red(`
212
- \u2717 ${e.message}
213
- `));
214
- process.exit(1);
317
+ exitWith(e, json);
215
318
  }
216
319
  }
217
320
  async function deleteTrajectoryAction(url, opts) {
218
321
  const chalk = (await import("chalk")).default;
219
- const { parseJobId } = await import("./download-M6UVD5S5.js");
220
- const { getApiUrl, getAuthHeader } = await import("./config-4XJRECIW.js");
322
+ const { parseJobId } = await import("./download-BWJDAVMM.js");
323
+ const { getApiUrl, getAuthHeader } = await import("./config-QS2IT5DI.js");
221
324
  let jobId = parseJobId(url);
222
325
  if (jobId.startsWith("org:")) {
223
326
  console.error(chalk.red("\n \u2717 Please use a direct job URL or UUID for deletion.\n"));
@@ -284,7 +387,7 @@ function printCommentError(error, json) {
284
387
  }
285
388
  async function addCommentAction(targetInput, opts) {
286
389
  try {
287
- const { addComment, formatComment, parseCommentTarget, readCommentBody } = await import("./comments-PK7DDP36.js");
390
+ const { addComment, formatComment, parseCommentTarget, readCommentBody } = await import("./comments-M3EX3VOU.js");
288
391
  const target = parseCommentTarget(targetInput, opts);
289
392
  const body = await readCommentBody(opts);
290
393
  const comment2 = await addComment(target, body);
@@ -300,7 +403,7 @@ ${formatComment(comment2)}
300
403
  }
301
404
  async function listCommentsAction(targetInput, opts) {
302
405
  try {
303
- const { formatComment, listComments, parseCommentTarget } = await import("./comments-PK7DDP36.js");
406
+ const { formatComment, listComments, parseCommentTarget } = await import("./comments-M3EX3VOU.js");
304
407
  const target = parseCommentTarget(targetInput, opts);
305
408
  const comments = await listComments(target);
306
409
  if (opts.json) {
@@ -318,7 +421,7 @@ ${comments.map(formatComment).join("\n\n")}
318
421
  }
319
422
  async function updateCommentAction(commentId, opts) {
320
423
  try {
321
- const { formatComment, readCommentBody, updateComment } = await import("./comments-PK7DDP36.js");
424
+ const { formatComment, readCommentBody, updateComment } = await import("./comments-M3EX3VOU.js");
322
425
  const body = await readCommentBody(opts);
323
426
  const comment2 = await updateComment(commentId, body);
324
427
  if (opts.json) console.log(JSON.stringify(comment2));
@@ -349,7 +452,7 @@ async function deleteCommentAction(commentId, opts) {
349
452
  return;
350
453
  }
351
454
  }
352
- const { deleteComment } = await import("./comments-PK7DDP36.js");
455
+ const { deleteComment } = await import("./comments-M3EX3VOU.js");
353
456
  await deleteComment(commentId);
354
457
  const result = { status: "deleted", comment_id: commentId };
355
458
  if (opts.json) console.log(JSON.stringify(result));
@@ -361,8 +464,24 @@ async function deleteCommentAction(commentId, opts) {
361
464
  var upload = program.command("upload").description("Upload trajectories or tasks");
362
465
  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);
363
466
  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);
364
- var download = program.command("download").description("Download trajectories");
365
- download.command("trajectory <url>").description("Download a trajectory job from trajectories.sh").option("-o, --output <dir>", "Output directory (default: slug name)").action(downloadTrajectoryAction);
467
+ var download = program.command("download").description("Download trajectories (a whole project, or one trajectory set)").addHelpText(
468
+ "after",
469
+ `
470
+ Output layout: <out>/<job-slug>/<trial-name>/agent/trajectory.json (+ result.json, config.json, verifier/, artifacts/)
471
+ Exit codes: 0 ok \xB7 1 error \xB7 2 bad arguments \xB7 3 authentication required \xB7 4 not found`
472
+ );
473
+ download.command("project <url>").description("Download every trajectory set in a project (URL like https://www.trajectories.sh/projects/<id>)").option("-o, --out <dir>", "Output directory (default: project slug)").option("--format <fmt>", "json (files as stored) or jsonl (also write trajectories.jsonl, one trial per line)", "json").option("--include-screenshots", "Also fetch screenshots (large; off by default for projects)").option("--json", "Print one JSON summary line to stdout and nothing else").option("--dry-run", "Print what would be fetched (jobs, trials, files, bytes) and exit without downloading").addHelpText(
474
+ "after",
475
+ `
476
+ Example:
477
+ $ trajectories download project https://www.trajectories.sh/projects/3ddb7729-c33f-473c-90b7-4c0f92d09e85 --out ./alder-ridge --json`
478
+ ).action(downloadProjectAction);
479
+ download.command("trajectory <url>").description("Download one trajectory set (URL like https://www.trajectories.sh/t/<id>)").option("-o, --out <dir>", "Output directory (default: derived from the URL)").option("--output <dir>", "Alias of --out (kept for older scripts)").option("--no-screenshots", "Skip screenshots (they are fetched by default for a single set)").option("--include-screenshots", "Explicitly fetch screenshots (the default)").option("--json", "Print one JSON summary line to stdout and nothing else").option("--dry-run", "Print what would be fetched and exit without downloading").addHelpText(
480
+ "after",
481
+ `
482
+ Example:
483
+ $ trajectories download trajectory https://www.trajectories.sh/t/6e1ea650-a7b7-44e9-bfd2-693bc4fb1758 --out ./run`
484
+ ).action(downloadTrajectoryAction);
366
485
  var del = program.command("delete").description("Delete trajectories");
367
486
  del.command("trajectory <url>").description("Delete a trajectory job from trajectories.sh").option("-y, --yes", "Skip confirmation prompt").action(deleteTrajectoryAction);
368
487
  var comment = program.command("comment").description("Manage trajectory comments");
@@ -383,13 +502,13 @@ addCommentBodyOptions(
383
502
  ).action(updateCommentAction);
384
503
  comment.command("delete <comment-id>").description("Delete one of your comments").option("-y, --yes", "Skip confirmation prompt").option("--json", "Print machine-readable JSON").action(deleteCommentAction);
385
504
  async function resolveProject(ref) {
386
- const { listProjects, resolveProjectRef } = await import("./projects-ESYLKFUR.js");
505
+ const { listProjects, resolveProjectRef } = await import("./projects-END2CFCS.js");
387
506
  const projects = await listProjects();
388
507
  return { ...resolveProjectRef(ref, projects), projects };
389
508
  }
390
509
  async function listProjectsAction(opts) {
391
510
  try {
392
- const { listProjects } = await import("./projects-ESYLKFUR.js");
511
+ const { listProjects } = await import("./projects-END2CFCS.js");
393
512
  const projects = await listProjects();
394
513
  if (opts.json) {
395
514
  console.log(JSON.stringify(projects, null, 2));
@@ -419,7 +538,7 @@ async function listProjectsAction(opts) {
419
538
  }
420
539
  async function showProjectAction(ref, opts) {
421
540
  try {
422
- const { getProject } = await import("./projects-ESYLKFUR.js");
541
+ const { getProject } = await import("./projects-END2CFCS.js");
423
542
  const { id } = await resolveProject(ref);
424
543
  const project2 = await getProject(id);
425
544
  if (opts.json) {
@@ -448,7 +567,7 @@ async function showProjectAction(ref, opts) {
448
567
  }
449
568
  async function createProjectAction(name, opts) {
450
569
  try {
451
- const { createProject } = await import("./projects-ESYLKFUR.js");
570
+ const { createProject } = await import("./projects-END2CFCS.js");
452
571
  const project2 = await createProject({
453
572
  name,
454
573
  description: opts.description,
@@ -479,7 +598,7 @@ async function updateProjectAction(ref, opts) {
479
598
  if (Object.keys(patch).length === 0) {
480
599
  throw new Error("Nothing to update. Pass --name, --description, --color, --slug or --visibility");
481
600
  }
482
- const { updateProject } = await import("./projects-ESYLKFUR.js");
601
+ const { updateProject } = await import("./projects-END2CFCS.js");
483
602
  const { id } = await resolveProject(ref);
484
603
  const project2 = await updateProject(id, patch);
485
604
  if (opts.json) console.log(JSON.stringify(project2));
@@ -492,7 +611,7 @@ async function updateProjectAction(ref, opts) {
492
611
  }
493
612
  async function deleteProjectAction(ref, opts) {
494
613
  try {
495
- const { deleteProject } = await import("./projects-ESYLKFUR.js");
614
+ const { deleteProject } = await import("./projects-END2CFCS.js");
496
615
  const { id, project: project2 } = await resolveProject(ref);
497
616
  const label = project2?.name ? `"${project2.name}"` : id;
498
617
  if (!opts.yes) {
@@ -523,7 +642,7 @@ async function deleteProjectAction(ref, opts) {
523
642
  }
524
643
  async function projectAddAction(ref, sets, opts) {
525
644
  try {
526
- const { assignJobs } = await import("./projects-ESYLKFUR.js");
645
+ const { assignJobs } = await import("./projects-END2CFCS.js");
527
646
  const { id, project: project2 } = await resolveProject(ref);
528
647
  const result = await assignJobs(id, sets, "add");
529
648
  if (opts.json) {
@@ -542,7 +661,7 @@ async function projectAddAction(ref, sets, opts) {
542
661
  }
543
662
  async function projectRemoveAction(refOrSet, sets, opts) {
544
663
  try {
545
- const { assignJobs } = await import("./projects-ESYLKFUR.js");
664
+ const { assignJobs } = await import("./projects-END2CFCS.js");
546
665
  if (opts.all) {
547
666
  const result2 = await assignJobs(null, [refOrSet, ...sets], "remove");
548
667
  if (opts.json) console.log(JSON.stringify(result2));
@@ -588,8 +707,8 @@ project.command("add <project> <sets...>").description("File one or more traject
588
707
  project.command("remove <project|set> [sets...]").description("Remove trajectory sets from a project, or from all with --all").option("--all", "Unfile the given sets from every project").option("--json", "Print machine-readable JSON").action(projectRemoveAction);
589
708
  program.command("whoami").description("Show current authentication status").action(async () => {
590
709
  const chalk = (await import("chalk")).default;
591
- const { status } = await import("./auth-4UEZSE4C.js");
592
- const { getApiUrl } = await import("./config-4XJRECIW.js");
710
+ const { status } = await import("./auth-NNBHKNIF.js");
711
+ const { getApiUrl } = await import("./config-QS2IT5DI.js");
593
712
  const s = status();
594
713
  if (s.loggedIn) {
595
714
  console.log(chalk.green(" \u2713 Authenticated"));
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  parseJobId
3
- } from "./chunk-IUSBK45Q.js";
4
- import "./chunk-35B24E3O.js";
3
+ } from "./chunk-LOJZPDGI.js";
4
+ import "./chunk-72L6XPK6.js";
5
5
  import {
6
6
  getApiUrl,
7
7
  getAuthHeader
8
- } from "./chunk-Q2ZEN43L.js";
8
+ } from "./chunk-ERO5HP43.js";
9
9
 
10
10
  // src/comments.ts
11
11
  import { readFileSync } from "fs";
@@ -11,7 +11,7 @@ import {
11
11
  saveApiKey,
12
12
  saveApiUrl,
13
13
  saveSession
14
- } from "./chunk-Q2ZEN43L.js";
14
+ } from "./chunk-ERO5HP43.js";
15
15
  export {
16
16
  CONFIG_DIR,
17
17
  CONFIG_FILE,
@@ -0,0 +1,28 @@
1
+ import {
2
+ CliError,
3
+ DOWNLOAD_TIMEOUT_MS,
4
+ EXIT,
5
+ dirBytes,
6
+ downloadJob,
7
+ downloadPlannedJob,
8
+ httpError,
9
+ parseJobId,
10
+ parseProjectId,
11
+ planJob,
12
+ writeJsonl
13
+ } from "./chunk-LOJZPDGI.js";
14
+ import "./chunk-72L6XPK6.js";
15
+ import "./chunk-ERO5HP43.js";
16
+ export {
17
+ CliError,
18
+ DOWNLOAD_TIMEOUT_MS,
19
+ EXIT,
20
+ dirBytes,
21
+ downloadJob,
22
+ downloadPlannedJob,
23
+ httpError,
24
+ parseJobId,
25
+ parseProjectId,
26
+ planJob,
27
+ writeJsonl
28
+ };
@@ -0,0 +1,70 @@
1
+ import {
2
+ CliError,
3
+ DOWNLOAD_TIMEOUT_MS,
4
+ EXIT,
5
+ downloadPlannedJob,
6
+ httpError,
7
+ parseProjectId,
8
+ planJob,
9
+ writeJsonl
10
+ } from "./chunk-LOJZPDGI.js";
11
+ import "./chunk-72L6XPK6.js";
12
+ import {
13
+ getApiUrl,
14
+ getAuthHeader
15
+ } from "./chunk-ERO5HP43.js";
16
+
17
+ // src/download-project.ts
18
+ import { join } from "path";
19
+ async function fetchProjectManifest(input) {
20
+ const projectId = parseProjectId(input);
21
+ const auth = await getAuthHeader();
22
+ const headers = { Accept: "application/json" };
23
+ if (auth) headers["Authorization"] = auth;
24
+ const resp = await fetch(`${getApiUrl()}/api/public/projects/${projectId}/export`, {
25
+ headers,
26
+ signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS)
27
+ });
28
+ if (!resp.ok) throw await httpError(resp, `Project ${projectId}`);
29
+ return await resp.json();
30
+ }
31
+ async function planProject(input, options) {
32
+ const project = await fetchProjectManifest(input);
33
+ if (!project.jobs.length) throw new CliError(`Project "${project.name ?? project.id}" has no downloadable trajectory sets.`, EXIT.NOT_FOUND);
34
+ const jobs = [];
35
+ for (const job of project.jobs) jobs.push(await planJob(job.id, options));
36
+ return {
37
+ project,
38
+ jobs,
39
+ bytes: jobs.reduce((s, j) => s + j.bytes, 0),
40
+ files: jobs.reduce((s, j) => s + j.files.length, 0),
41
+ trajectoryFiles: jobs.reduce((s, j) => s + j.trajectoryFiles, 0)
42
+ };
43
+ }
44
+ async function downloadProject(plan, outputDir, options) {
45
+ const log = options.quiet ? () => {
46
+ } : (m) => console.log(m);
47
+ const results = [];
48
+ for (const [i, job] of plan.jobs.entries()) {
49
+ log(`
50
+ [${i + 1}/${plan.jobs.length}] ${job.slug}`);
51
+ results.push(await downloadPlannedJob(job, join(outputDir, job.slug), options));
52
+ }
53
+ let jsonl = null;
54
+ if (options.format === "jsonl") {
55
+ const path = join(outputDir, "trajectories.jsonl");
56
+ const trials = writeJsonl(outputDir, results.map((r) => ({ jobId: r.jobId, slug: r.slug, dir: r.outputDir })), path);
57
+ jsonl = { path, trials };
58
+ log(` \u2713 Wrote ${trials} trials to ${path}`);
59
+ }
60
+ return { project: plan.project, outputDir, jobs: results, jsonl };
61
+ }
62
+ function defaultProjectDir(project) {
63
+ return project.slug || project.id;
64
+ }
65
+ export {
66
+ defaultProjectDir,
67
+ downloadProject,
68
+ fetchProjectManifest,
69
+ planProject
70
+ };
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  parseJobId
3
- } from "./chunk-IUSBK45Q.js";
4
- import "./chunk-35B24E3O.js";
3
+ } from "./chunk-LOJZPDGI.js";
4
+ import "./chunk-72L6XPK6.js";
5
5
  import {
6
6
  getApiUrl,
7
7
  getAuthHeader
8
- } from "./chunk-Q2ZEN43L.js";
8
+ } from "./chunk-ERO5HP43.js";
9
9
 
10
10
  // src/projects.ts
11
11
  var PROJECT_COLORS = [
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  preflightQuota
3
- } from "./chunk-2ELJH3IV.js";
3
+ } from "./chunk-HHUWMQ5S.js";
4
4
  import {
5
5
  getApiUrl,
6
6
  getAuthHeader
7
- } from "./chunk-Q2ZEN43L.js";
7
+ } from "./chunk-ERO5HP43.js";
8
8
 
9
9
  // src/push-task.ts
10
10
  import { readFileSync, existsSync, readdirSync } from "fs";
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  preflightQuota
3
- } from "./chunk-2ELJH3IV.js";
3
+ } from "./chunk-HHUWMQ5S.js";
4
4
  import {
5
5
  buildScreenshotChunks
6
- } from "./chunk-35B24E3O.js";
6
+ } from "./chunk-72L6XPK6.js";
7
7
  import {
8
8
  getApiUrl,
9
9
  getAuthHeader
10
- } from "./chunk-Q2ZEN43L.js";
10
+ } from "./chunk-ERO5HP43.js";
11
11
 
12
12
  // src/upload.ts
13
13
  import { readdirSync, statSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, unlinkSync } from "fs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trajectories-sh",
3
- "version": "1.23.1",
3
+ "version": "1.24.0",
4
4
  "description": "CLI for uploading trajectory jobs to trajectories.sh",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,148 +0,0 @@
1
- import {
2
- CHUNK_DIR,
3
- unpackScreenshotChunks
4
- } from "./chunk-35B24E3O.js";
5
- import {
6
- getApiUrl,
7
- getAuthHeader
8
- } from "./chunk-Q2ZEN43L.js";
9
-
10
- // src/download.ts
11
- import { mkdirSync, writeFileSync } from "fs";
12
- import { join, dirname } from "path";
13
- var UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
14
- var BATCH_SIZE = 100;
15
- var CONCURRENCY = 25;
16
- var DOWNLOAD_TIMEOUT_MS = 18e4;
17
- function parseJobId(input) {
18
- const trimmed = input.trim();
19
- if (UUID_RE.test(trimmed) && !trimmed.includes("/")) {
20
- return trimmed.match(UUID_RE)[0];
21
- }
22
- const shortMatch = trimmed.match(/\/t\/([0-9a-f-]{36})/i);
23
- if (shortMatch) return shortMatch[1];
24
- const trajMatch = trimmed.match(/\/trajectories\/([0-9a-f-]{36})/i);
25
- if (trajMatch) return trajMatch[1];
26
- const trajViewerMatch = trimmed.match(/\/traj\/([0-9a-f-]{36})\/([^/]+)/i);
27
- if (trajViewerMatch) {
28
- return `org:${trajViewerMatch[1]}:${trajViewerMatch[2]}`;
29
- }
30
- const anyUuid = trimmed.match(UUID_RE);
31
- if (anyUuid) return anyUuid[0];
32
- throw new Error(`Could not find a trajectory job ID in: ${input}`);
33
- }
34
- async function downloadJob(input, outputDir) {
35
- const apiUrl = getApiUrl();
36
- const auth = await getAuthHeader();
37
- const headers = {};
38
- if (auth) headers["Authorization"] = auth;
39
- let jobId = parseJobId(input);
40
- if (jobId.startsWith("org:")) {
41
- const [, orgId, slug2] = jobId.split(":");
42
- const configResp = await fetch(`${apiUrl}/api/viewer/${orgId}/${slug2}/api/jobs`, {
43
- headers,
44
- signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS)
45
- });
46
- if (!configResp.ok) throw new Error(`Could not resolve job: ${orgId}/${slug2}`);
47
- const configData = await configResp.json();
48
- const items = configData.items || [];
49
- if (items.length > 0 && items[0].id) {
50
- jobId = items[0].id;
51
- } else {
52
- throw new Error(`Could not resolve job ID for ${orgId}/${slug2}`);
53
- }
54
- }
55
- const listResp = await fetch(`${apiUrl}/api/cli/download/${jobId}/files`, {
56
- headers,
57
- signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS)
58
- });
59
- if (!listResp.ok) {
60
- throw new Error(`Failed to list files: ${await listResp.text()}`);
61
- }
62
- const { slug, files } = await listResp.json();
63
- const totalBytes = files.reduce((s, f) => s + f.size, 0);
64
- const packedTrials = new Set(
65
- files.filter((f) => f.path.includes(`/${CHUNK_DIR}/`)).map((f) => f.path.split("/")[0])
66
- );
67
- console.log(` Job: ${slug} (${jobId})`);
68
- console.log(
69
- ` ${files.length} files, ${(totalBytes / 1024 / 1024).toFixed(1)} MB` + (packedTrials.size > 0 ? ` (screenshots of ${packedTrials.size} trial${packedTrials.size === 1 ? "" : "s"} packed)` : "")
70
- );
71
- mkdirSync(outputDir, { recursive: true });
72
- const { ProgressBar } = await import("./progress-H7WNKHDR.js");
73
- const bar = new ProgressBar({ total: files.length, totalBytes, label: "\u2193" });
74
- let downloaded = 0;
75
- let totalSize = 0;
76
- for (let batchStart = 0; batchStart < files.length; batchStart += BATCH_SIZE) {
77
- const batch = files.slice(batchStart, batchStart + BATCH_SIZE);
78
- const urlResp = await fetch(`${apiUrl}/api/cli/download/${jobId}/signed-urls`, {
79
- method: "POST",
80
- headers: { ...headers, "Content-Type": "application/json" },
81
- body: JSON.stringify({ paths: batch.map((f) => f.path) }),
82
- signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS)
83
- });
84
- let urlMap = /* @__PURE__ */ new Map();
85
- if (urlResp.ok) {
86
- const { urls } = await urlResp.json();
87
- for (const u of urls) {
88
- if (u.signed_url) urlMap.set(u.path, u.signed_url);
89
- }
90
- }
91
- const pending = [];
92
- for (const file of batch) {
93
- const signedUrl = urlMap.get(file.path);
94
- const downloadUrl = signedUrl ?? `${apiUrl}/api/cli/download/${jobId}/file?path=${encodeURIComponent(file.path)}`;
95
- const downloadHeaders = signedUrl ? {} : headers;
96
- const task = (async () => {
97
- for (let attempt = 0; attempt < 3; attempt++) {
98
- try {
99
- const resp = await fetch(downloadUrl, {
100
- headers: downloadHeaders,
101
- signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS)
102
- });
103
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
104
- const data = Buffer.from(await resp.arrayBuffer());
105
- const outPath = join(outputDir, file.path);
106
- mkdirSync(dirname(outPath), { recursive: true });
107
- writeFileSync(outPath, data);
108
- downloaded++;
109
- totalSize += data.length;
110
- bar.tick(data.length);
111
- return;
112
- } catch {
113
- if (attempt < 2) await new Promise((r) => setTimeout(r, 1e3 * (attempt + 1)));
114
- }
115
- }
116
- bar.tick(0);
117
- })();
118
- pending.push(task);
119
- if (pending.length >= CONCURRENCY) {
120
- await Promise.race(pending);
121
- const next = [];
122
- for (const p of pending) {
123
- let settled = false;
124
- p.then(() => settled = true, () => settled = true);
125
- if (!settled) next.push(p);
126
- }
127
- if (next.length === pending.length) await Promise.race(pending);
128
- pending.length = 0;
129
- pending.push(...next);
130
- }
131
- }
132
- await Promise.all(pending);
133
- }
134
- bar.done();
135
- if (packedTrials.size > 0) {
136
- const unpacked = unpackScreenshotChunks(outputDir);
137
- if (unpacked.imagesWritten > 0) {
138
- console.log(` \u2713 Unpacked ${unpacked.imagesWritten} screenshots across ${unpacked.trialsUnpacked} trial${unpacked.trialsUnpacked === 1 ? "" : "s"}`);
139
- }
140
- }
141
- return { files: downloaded, totalSize };
142
- }
143
-
144
- export {
145
- DOWNLOAD_TIMEOUT_MS,
146
- parseJobId,
147
- downloadJob
148
- };
@@ -1,12 +0,0 @@
1
- import {
2
- DOWNLOAD_TIMEOUT_MS,
3
- downloadJob,
4
- parseJobId
5
- } from "./chunk-IUSBK45Q.js";
6
- import "./chunk-35B24E3O.js";
7
- import "./chunk-Q2ZEN43L.js";
8
- export {
9
- DOWNLOAD_TIMEOUT_MS,
10
- downloadJob,
11
- parseJobId
12
- };