trajectories-sh 1.23.0 → 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.
- package/dist/{auth-4UEZSE4C.js → auth-NNBHKNIF.js} +1 -1
- package/dist/{chunk-35B24E3O.js → chunk-72L6XPK6.js} +1 -0
- package/dist/{chunk-Q2ZEN43L.js → chunk-ERO5HP43.js} +1 -1
- package/dist/{chunk-2ELJH3IV.js → chunk-HHUWMQ5S.js} +1 -1
- package/dist/chunk-LOJZPDGI.js +242 -0
- package/dist/cli.js +164 -40
- package/dist/{comments-PK7DDP36.js → comments-M3EX3VOU.js} +3 -3
- package/dist/{config-4XJRECIW.js → config-QS2IT5DI.js} +1 -1
- package/dist/download-BWJDAVMM.js +28 -0
- package/dist/download-project-XN4OPF3M.js +70 -0
- package/dist/{projects-ESYLKFUR.js → projects-END2CFCS.js} +3 -3
- package/dist/{push-task-OZTAROKW.js → push-task-JGGRVTJ3.js} +2 -2
- package/dist/{upload-CIW2CHKX.js → upload-6HQ5A2H7.js} +3 -3
- package/package.json +1 -1
- package/dist/chunk-IUSBK45Q.js +0 -148
- package/dist/download-M6UVD5S5.js +0 -12
|
@@ -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}`;
|
|
@@ -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.
|
|
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.
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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 {
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
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-
|
|
220
|
-
const { getApiUrl, getAuthHeader } = await import("./config-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
592
|
-
const { getApiUrl } = await import("./config-
|
|
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"));
|
|
@@ -599,4 +718,9 @@ program.command("whoami").description("Show current authentication status").acti
|
|
|
599
718
|
console.log(chalk.dim(" Not logged in. Run: trajectories auth login"));
|
|
600
719
|
}
|
|
601
720
|
});
|
|
721
|
+
program.showHelpAfterError("(run: trajectories help)");
|
|
722
|
+
if (process.argv.length <= 2) {
|
|
723
|
+
program.outputHelp();
|
|
724
|
+
process.exit(1);
|
|
725
|
+
}
|
|
602
726
|
await program.parseAsync();
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
parseJobId
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-LOJZPDGI.js";
|
|
4
|
+
import "./chunk-72L6XPK6.js";
|
|
5
5
|
import {
|
|
6
6
|
getApiUrl,
|
|
7
7
|
getAuthHeader
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-ERO5HP43.js";
|
|
9
9
|
|
|
10
10
|
// src/comments.ts
|
|
11
11
|
import { readFileSync } from "fs";
|
|
@@ -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-
|
|
4
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-LOJZPDGI.js";
|
|
4
|
+
import "./chunk-72L6XPK6.js";
|
|
5
5
|
import {
|
|
6
6
|
getApiUrl,
|
|
7
7
|
getAuthHeader
|
|
8
|
-
} from "./chunk-
|
|
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-
|
|
3
|
+
} from "./chunk-HHUWMQ5S.js";
|
|
4
4
|
import {
|
|
5
5
|
getApiUrl,
|
|
6
6
|
getAuthHeader
|
|
7
|
-
} from "./chunk-
|
|
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-
|
|
3
|
+
} from "./chunk-HHUWMQ5S.js";
|
|
4
4
|
import {
|
|
5
5
|
buildScreenshotChunks
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-72L6XPK6.js";
|
|
7
7
|
import {
|
|
8
8
|
getApiUrl,
|
|
9
9
|
getAuthHeader
|
|
10
|
-
} from "./chunk-
|
|
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
package/dist/chunk-IUSBK45Q.js
DELETED
|
@@ -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
|
-
};
|