trajectories-sh 1.21.1 → 1.23.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.
@@ -0,0 +1,154 @@
1
+ // src/screenshot-chunks.ts
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "fs";
3
+ import { dirname, extname, join } from "path";
4
+ var CHUNK_DIR = "_screenshot_chunks";
5
+ var MANIFEST_NAME = "manifest.json";
6
+ var MANIFEST_VERSION = 1;
7
+ var CHUNK_TARGET_BYTES = 4 * 1024 * 1024;
8
+ var IMAGE_MIME_BY_EXT = {
9
+ ".webp": "image/webp",
10
+ ".png": "image/png",
11
+ ".jpg": "image/jpeg",
12
+ ".jpeg": "image/jpeg",
13
+ ".gif": "image/gif"
14
+ };
15
+ function mimeFor(path) {
16
+ return IMAGE_MIME_BY_EXT[extname(path).toLowerCase()] ?? null;
17
+ }
18
+ function referencedImagePaths(trajectory) {
19
+ const out = [];
20
+ const seen = /* @__PURE__ */ new Set();
21
+ const collect = (parts) => {
22
+ if (!Array.isArray(parts)) return;
23
+ for (const part of parts) {
24
+ if (!part || typeof part !== "object" || part.type !== "image") continue;
25
+ const path = part.source?.path;
26
+ if (typeof path === "string" && path && !seen.has(path)) {
27
+ seen.add(path);
28
+ out.push(path);
29
+ }
30
+ }
31
+ };
32
+ for (const step of Array.isArray(trajectory?.steps) ? trajectory.steps : []) {
33
+ if (!step || typeof step !== "object") continue;
34
+ collect(step.message);
35
+ for (const r of Array.isArray(step.observation?.results) ? step.observation.results : []) {
36
+ if (r && typeof r === "object") collect(r.content);
37
+ }
38
+ }
39
+ return out;
40
+ }
41
+ function buildScreenshotChunks(jobDir, outDir, opts = {}) {
42
+ const targetBytes = opts.targetBytes ?? CHUNK_TARGET_BYTES;
43
+ const files = [];
44
+ const packedRelPaths = /* @__PURE__ */ new Set();
45
+ let packedBytes = 0;
46
+ let imagesPacked = 0;
47
+ let trialsPacked = 0;
48
+ for (const entry of readdirSync(jobDir, { withFileTypes: true })) {
49
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
50
+ const trial = entry.name;
51
+ const trajPath = join(jobDir, trial, "agent", "trajectory.json");
52
+ if (!existsSync(trajPath)) continue;
53
+ let paths;
54
+ try {
55
+ paths = referencedImagePaths(JSON.parse(readFileSync(trajPath, "utf-8")));
56
+ } catch {
57
+ continue;
58
+ }
59
+ const images = paths.map((p) => ({ p, abs: join(jobDir, trial, "agent", p), mime: mimeFor(p) })).filter((x) => x.mime && existsSync(x.abs));
60
+ if (images.length === 0) continue;
61
+ const relDir = `${trial}/${CHUNK_DIR}/agent`;
62
+ const absDir = join(outDir, relDir);
63
+ mkdirSync(absDir, { recursive: true });
64
+ const manifest = { version: MANIFEST_VERSION, chunks: [] };
65
+ let current = null;
66
+ const seal = () => {
67
+ if (!current) return;
68
+ const abs = join(absDir, current.name);
69
+ writeFileSync(abs, Buffer.concat(current.parts));
70
+ manifest.chunks.push({ name: current.name, size: current.size, entries: current.entries });
71
+ files.push({ absPath: abs, relPath: `${relDir}/${current.name}`, size: current.size });
72
+ current = null;
73
+ };
74
+ for (const { p, abs, mime } of images) {
75
+ const data = readFileSync(abs);
76
+ if (current && current.size + data.length > targetBytes) seal();
77
+ if (!current) {
78
+ current = { name: `chunk-${String(manifest.chunks.length).padStart(3, "0")}.bin`, parts: [], entries: [], size: 0 };
79
+ }
80
+ current.entries.push([p, current.size, data.length, mime]);
81
+ current.parts.push(data);
82
+ current.size += data.length;
83
+ packedRelPaths.add(`${trial}/agent/${p}`);
84
+ packedBytes += data.length;
85
+ imagesPacked++;
86
+ }
87
+ seal();
88
+ const manifestAbs = join(absDir, MANIFEST_NAME);
89
+ const manifestBytes = Buffer.from(JSON.stringify(manifest));
90
+ writeFileSync(manifestAbs, manifestBytes);
91
+ files.push({ absPath: manifestAbs, relPath: `${relDir}/${MANIFEST_NAME}`, size: manifestBytes.length });
92
+ trialsPacked++;
93
+ }
94
+ return { files, packedRelPaths, packedBytes, imagesPacked, trialsPacked };
95
+ }
96
+ function unpackScreenshotChunks(jobDir) {
97
+ let imagesWritten = 0;
98
+ let trialsUnpacked = 0;
99
+ if (!existsSync(jobDir)) return { imagesWritten, trialsUnpacked };
100
+ for (const entry of readdirSync(jobDir, { withFileTypes: true })) {
101
+ if (!entry.isDirectory()) continue;
102
+ const chunkRoot = join(jobDir, entry.name, CHUNK_DIR);
103
+ if (!existsSync(chunkRoot)) continue;
104
+ const agentDir = join(chunkRoot, "agent");
105
+ const manifestPath = join(agentDir, MANIFEST_NAME);
106
+ let clean = true;
107
+ if (existsSync(manifestPath)) {
108
+ let manifest = null;
109
+ try {
110
+ manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
111
+ } catch {
112
+ manifest = null;
113
+ }
114
+ if (manifest?.version === MANIFEST_VERSION && Array.isArray(manifest.chunks)) {
115
+ let wrote = false;
116
+ for (const chunk of manifest.chunks) {
117
+ const chunkPath = join(agentDir, chunk.name);
118
+ if (!existsSync(chunkPath)) {
119
+ console.warn(` \u26A0 ${entry.name}: chunk ${chunk.name} missing; its screenshots were not restored`);
120
+ clean = false;
121
+ continue;
122
+ }
123
+ const data = readFileSync(chunkPath);
124
+ for (const [p, offset, length] of chunk.entries) {
125
+ if (p.includes("..") || p.startsWith("/")) continue;
126
+ const dest = join(jobDir, entry.name, "agent", p);
127
+ if (existsSync(dest)) continue;
128
+ if (offset + length > data.length) {
129
+ console.warn(` \u26A0 ${entry.name}: ${p} extends past the end of ${chunk.name}; skipped`);
130
+ clean = false;
131
+ continue;
132
+ }
133
+ mkdirSync(dirname(dest), { recursive: true });
134
+ writeFileSync(dest, data.subarray(offset, offset + length));
135
+ imagesWritten++;
136
+ wrote = true;
137
+ }
138
+ }
139
+ if (wrote) trialsUnpacked++;
140
+ } else {
141
+ console.warn(` \u26A0 ${entry.name}: unreadable screenshot chunk manifest; screenshots were not restored`);
142
+ clean = false;
143
+ }
144
+ }
145
+ if (clean) rmSync(chunkRoot, { recursive: true, force: true });
146
+ }
147
+ return { imagesWritten, trialsUnpacked };
148
+ }
149
+
150
+ export {
151
+ CHUNK_DIR,
152
+ buildScreenshotChunks,
153
+ unpackScreenshotChunks
154
+ };
@@ -1,3 +1,7 @@
1
+ import {
2
+ CHUNK_DIR,
3
+ unpackScreenshotChunks
4
+ } from "./chunk-35B24E3O.js";
1
5
  import {
2
6
  getApiUrl,
3
7
  getAuthHeader
@@ -57,8 +61,13 @@ async function downloadJob(input, outputDir) {
57
61
  }
58
62
  const { slug, files } = await listResp.json();
59
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
+ );
60
67
  console.log(` Job: ${slug} (${jobId})`);
61
- console.log(` ${files.length} files, ${(totalBytes / 1024 / 1024).toFixed(1)} MB`);
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
+ );
62
71
  mkdirSync(outputDir, { recursive: true });
63
72
  const { ProgressBar } = await import("./progress-H7WNKHDR.js");
64
73
  const bar = new ProgressBar({ total: files.length, totalBytes, label: "\u2193" });
@@ -123,6 +132,12 @@ async function downloadJob(input, outputDir) {
123
132
  await Promise.all(pending);
124
133
  }
125
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
+ }
126
141
  return { files: downloaded, totalSize };
127
142
  }
128
143
 
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ 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.20.0";
9
+ var version = "1.23.0";
10
10
  try {
11
11
  const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
12
12
  version = pkg.version;
@@ -98,7 +98,7 @@ async function uploadTrajectoryAction(pathArg, opts) {
98
98
  );
99
99
  }
100
100
  if (isZip) {
101
- const { uploadZip } = await import("./upload-4QD3JFJE.js");
101
+ const { uploadZip } = await import("./upload-CIW2CHKX.js");
102
102
  const slug2 = opts.slug ?? pathArg.replace(/\.zip$/i, "").replace(/\/+$/, "").split("/").pop();
103
103
  console.log(chalk.bold(`
104
104
  Uploading ZIP ${pathArg} \u2192 ${chalk.cyan(slug2)}
@@ -127,7 +127,7 @@ async function uploadTrajectoryAction(pathArg, opts) {
127
127
  Uploading trajectory ${pathArg} \u2192 ${chalk.cyan(slug)}
128
128
  `));
129
129
  try {
130
- const { uploadJob } = await import("./upload-4QD3JFJE.js");
130
+ const { uploadJob } = await import("./upload-CIW2CHKX.js");
131
131
  const result = await uploadJob(pathArg, {
132
132
  slug,
133
133
  name: opts.name
@@ -200,7 +200,7 @@ async function downloadTrajectoryAction(url, opts) {
200
200
  Downloading trajectory...
201
201
  `));
202
202
  try {
203
- const { downloadJob, parseJobId } = await import("./download-4L7DZO3X.js");
203
+ const { downloadJob, parseJobId } = await import("./download-M6UVD5S5.js");
204
204
  const outputDir = opts.output ?? parseJobId(url).replace(/^org:/, "").replace(/:/g, "-");
205
205
  const result = await downloadJob(url, outputDir);
206
206
  console.log(chalk.green.bold(` \u2713 Downloaded ${result.files} files`));
@@ -216,7 +216,7 @@ async function downloadTrajectoryAction(url, opts) {
216
216
  }
217
217
  async function deleteTrajectoryAction(url, opts) {
218
218
  const chalk = (await import("chalk")).default;
219
- const { parseJobId } = await import("./download-4L7DZO3X.js");
219
+ const { parseJobId } = await import("./download-M6UVD5S5.js");
220
220
  const { getApiUrl, getAuthHeader } = await import("./config-4XJRECIW.js");
221
221
  let jobId = parseJobId(url);
222
222
  if (jobId.startsWith("org:")) {
@@ -284,7 +284,7 @@ function printCommentError(error, json) {
284
284
  }
285
285
  async function addCommentAction(targetInput, opts) {
286
286
  try {
287
- const { addComment, formatComment, parseCommentTarget, readCommentBody } = await import("./comments-RDCVHCZ7.js");
287
+ const { addComment, formatComment, parseCommentTarget, readCommentBody } = await import("./comments-PK7DDP36.js");
288
288
  const target = parseCommentTarget(targetInput, opts);
289
289
  const body = await readCommentBody(opts);
290
290
  const comment2 = await addComment(target, body);
@@ -300,7 +300,7 @@ ${formatComment(comment2)}
300
300
  }
301
301
  async function listCommentsAction(targetInput, opts) {
302
302
  try {
303
- const { formatComment, listComments, parseCommentTarget } = await import("./comments-RDCVHCZ7.js");
303
+ const { formatComment, listComments, parseCommentTarget } = await import("./comments-PK7DDP36.js");
304
304
  const target = parseCommentTarget(targetInput, opts);
305
305
  const comments = await listComments(target);
306
306
  if (opts.json) {
@@ -318,7 +318,7 @@ ${comments.map(formatComment).join("\n\n")}
318
318
  }
319
319
  async function updateCommentAction(commentId, opts) {
320
320
  try {
321
- const { formatComment, readCommentBody, updateComment } = await import("./comments-RDCVHCZ7.js");
321
+ const { formatComment, readCommentBody, updateComment } = await import("./comments-PK7DDP36.js");
322
322
  const body = await readCommentBody(opts);
323
323
  const comment2 = await updateComment(commentId, body);
324
324
  if (opts.json) console.log(JSON.stringify(comment2));
@@ -349,7 +349,7 @@ async function deleteCommentAction(commentId, opts) {
349
349
  return;
350
350
  }
351
351
  }
352
- const { deleteComment } = await import("./comments-RDCVHCZ7.js");
352
+ const { deleteComment } = await import("./comments-PK7DDP36.js");
353
353
  await deleteComment(commentId);
354
354
  const result = { status: "deleted", comment_id: commentId };
355
355
  if (opts.json) console.log(JSON.stringify(result));
@@ -382,6 +382,210 @@ addCommentBodyOptions(
382
382
  comment.command("update <comment-id>").description("Update the body of one of your comments").option("--json", "Print machine-readable JSON")
383
383
  ).action(updateCommentAction);
384
384
  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
+ async function resolveProject(ref) {
386
+ const { listProjects, resolveProjectRef } = await import("./projects-ESYLKFUR.js");
387
+ const projects = await listProjects();
388
+ return { ...resolveProjectRef(ref, projects), projects };
389
+ }
390
+ async function listProjectsAction(opts) {
391
+ try {
392
+ const { listProjects } = await import("./projects-ESYLKFUR.js");
393
+ const projects = await listProjects();
394
+ if (opts.json) {
395
+ console.log(JSON.stringify(projects, null, 2));
396
+ return;
397
+ }
398
+ if (projects.length === 0) {
399
+ console.log("\n No projects yet. Create one with: trajectories project create <name>\n");
400
+ return;
401
+ }
402
+ const chalk = (await import("chalk")).default;
403
+ console.log(`
404
+ ${chalk.bold(`Projects (${projects.length})`)}
405
+ `);
406
+ for (const p of projects) {
407
+ const sets = `${p.n_jobs} set${p.n_jobs === 1 ? "" : "s"}`;
408
+ const trials = `${p.n_trials} trajector${p.n_trials === 1 ? "y" : "ies"}`;
409
+ const rate = p.pass_rate != null ? ` \xB7 ${(p.pass_rate * 100).toFixed(0)}%` : "";
410
+ console.log(` ${chalk.bold(p.name)} ${chalk.dim(`(${p.visibility})`)}`);
411
+ console.log(` ${chalk.dim(p.id)}`);
412
+ console.log(` ${chalk.dim(`${sets} \xB7 ${trials}${rate}`)}`);
413
+ if (p.description) console.log(` ${chalk.dim(p.description)}`);
414
+ console.log("");
415
+ }
416
+ } catch (error) {
417
+ printCommentError(error, Boolean(opts.json));
418
+ }
419
+ }
420
+ async function showProjectAction(ref, opts) {
421
+ try {
422
+ const { getProject } = await import("./projects-ESYLKFUR.js");
423
+ const { id } = await resolveProject(ref);
424
+ const project2 = await getProject(id);
425
+ if (opts.json) {
426
+ console.log(JSON.stringify(project2, null, 2));
427
+ return;
428
+ }
429
+ const chalk = (await import("chalk")).default;
430
+ console.log(`
431
+ ${chalk.bold(project2.name)} ${chalk.dim(`(${project2.visibility})`)}`);
432
+ console.log(` ${chalk.dim(project2.id)}
433
+ `);
434
+ if (project2.description) console.log(` ${project2.description}
435
+ `);
436
+ console.log(
437
+ ` ${project2.n_jobs} trajectory set${project2.n_jobs === 1 ? "" : "s"} \xB7 ${project2.n_trials} trajector${project2.n_trials === 1 ? "y" : "ies"}` + (project2.pass_rate != null ? ` \xB7 ${(project2.pass_rate * 100).toFixed(0)}% pass` : "") + "\n"
438
+ );
439
+ for (const job of project2.jobs || []) {
440
+ const trials = job.n_trials != null ? chalk.dim(` (${job.n_trials})`) : "";
441
+ console.log(` ${job.name || job.slug || job.id}${trials}`);
442
+ console.log(` ${chalk.dim(job.id)}`);
443
+ }
444
+ console.log("");
445
+ } catch (error) {
446
+ printCommentError(error, Boolean(opts.json));
447
+ }
448
+ }
449
+ async function createProjectAction(name, opts) {
450
+ try {
451
+ const { createProject } = await import("./projects-ESYLKFUR.js");
452
+ const project2 = await createProject({
453
+ name,
454
+ description: opts.description,
455
+ color: opts.color,
456
+ slug: opts.slug,
457
+ visibility: opts.visibility
458
+ });
459
+ if (opts.json) {
460
+ console.log(JSON.stringify(project2));
461
+ return;
462
+ }
463
+ console.log(`
464
+ \u2713 Created project "${project2.name}" (${project2.visibility})`);
465
+ console.log(` ${project2.id}
466
+ `);
467
+ } catch (error) {
468
+ printCommentError(error, Boolean(opts.json));
469
+ }
470
+ }
471
+ async function updateProjectAction(ref, opts) {
472
+ try {
473
+ const patch = {};
474
+ if (opts.name) patch.name = opts.name;
475
+ if (opts.description !== void 0) patch.description = opts.description;
476
+ if (opts.color) patch.color = opts.color;
477
+ if (opts.slug) patch.slug = opts.slug;
478
+ if (opts.visibility) patch.visibility = opts.visibility;
479
+ if (Object.keys(patch).length === 0) {
480
+ throw new Error("Nothing to update. Pass --name, --description, --color, --slug or --visibility");
481
+ }
482
+ const { updateProject } = await import("./projects-ESYLKFUR.js");
483
+ const { id } = await resolveProject(ref);
484
+ const project2 = await updateProject(id, patch);
485
+ if (opts.json) console.log(JSON.stringify(project2));
486
+ else console.log(`
487
+ \u2713 Updated "${project2.name}"
488
+ `);
489
+ } catch (error) {
490
+ printCommentError(error, Boolean(opts.json));
491
+ }
492
+ }
493
+ async function deleteProjectAction(ref, opts) {
494
+ try {
495
+ const { deleteProject } = await import("./projects-ESYLKFUR.js");
496
+ const { id, project: project2 } = await resolveProject(ref);
497
+ const label = project2?.name ? `"${project2.name}"` : id;
498
+ if (!opts.yes) {
499
+ if (!process.stdin.isTTY) {
500
+ throw new Error("Refusing non-interactive deletion without --yes");
501
+ }
502
+ const readline = await import("readline");
503
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
504
+ const answer = await new Promise((resolve) => {
505
+ rl.question(
506
+ ` Delete project ${label}? Its trajectory sets are ungrouped, not deleted. (y/N) `,
507
+ resolve
508
+ );
509
+ });
510
+ rl.close();
511
+ if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
512
+ if (opts.json) console.log(JSON.stringify({ status: "cancelled", project_id: id }));
513
+ else console.log(" Cancelled.");
514
+ return;
515
+ }
516
+ }
517
+ await deleteProject(id);
518
+ if (opts.json) console.log(JSON.stringify({ status: "deleted", project_id: id }));
519
+ else console.log(` \u2713 Deleted project ${label}`);
520
+ } catch (error) {
521
+ printCommentError(error, Boolean(opts.json));
522
+ }
523
+ }
524
+ async function projectAddAction(ref, sets, opts) {
525
+ try {
526
+ const { assignJobs } = await import("./projects-ESYLKFUR.js");
527
+ const { id, project: project2 } = await resolveProject(ref);
528
+ const result = await assignJobs(id, sets, "add");
529
+ if (opts.json) {
530
+ console.log(JSON.stringify(result));
531
+ return;
532
+ }
533
+ const label = project2?.name ? `"${project2.name}"` : id;
534
+ console.log(
535
+ `
536
+ \u2713 Filed ${result.assigned} trajectory set${result.assigned === 1 ? "" : "s"} into ${label}
537
+ `
538
+ );
539
+ } catch (error) {
540
+ printCommentError(error, Boolean(opts.json));
541
+ }
542
+ }
543
+ async function projectRemoveAction(refOrSet, sets, opts) {
544
+ try {
545
+ const { assignJobs } = await import("./projects-ESYLKFUR.js");
546
+ if (opts.all) {
547
+ const result2 = await assignJobs(null, [refOrSet, ...sets], "remove");
548
+ if (opts.json) console.log(JSON.stringify(result2));
549
+ else
550
+ console.log(
551
+ `
552
+ \u2713 Removed ${result2.assigned} trajectory set${result2.assigned === 1 ? "" : "s"} from all projects
553
+ `
554
+ );
555
+ return;
556
+ }
557
+ if (sets.length === 0) {
558
+ throw new Error(
559
+ "Specify which trajectory sets to remove, or pass --all to unfile them from every project"
560
+ );
561
+ }
562
+ const { id, project: project2 } = await resolveProject(refOrSet);
563
+ const result = await assignJobs(id, sets, "remove");
564
+ if (opts.json) {
565
+ console.log(JSON.stringify(result));
566
+ return;
567
+ }
568
+ const label = project2?.name ? `"${project2.name}"` : id;
569
+ console.log(
570
+ `
571
+ \u2713 Removed ${result.assigned} trajectory set${result.assigned === 1 ? "" : "s"} from ${label}
572
+ `
573
+ );
574
+ } catch (error) {
575
+ printCommentError(error, Boolean(opts.json));
576
+ }
577
+ }
578
+ var project = program.command("project").description("Manage projects and the trajectory sets filed into them");
579
+ project.command("list").description("List projects in your organization").option("--json", "Print machine-readable JSON").action(listProjectsAction);
580
+ project.command("show <project>").description("Show a project and the trajectory sets in it (id, URL, slug or name)").option("--json", "Print machine-readable JSON").action(showProjectAction);
581
+ project.command("create <name>").description("Create a project").option("-d, --description <text>", "Project description").option(
582
+ "-c, --color <color>",
583
+ "Palette key: slate, blue, green, amber, purple, pink, teal, red"
584
+ ).option("--slug <slug>", "URL slug (defaults to a slugified name)").option("--visibility <visibility>", "public, unlisted (default) or private").option("--json", "Print machine-readable JSON").action(createProjectAction);
585
+ project.command("update <project>").description("Rename a project or change its description, colour, slug or visibility").option("-n, --name <name>", "New name").option("-d, --description <text>", "New description").option("-c, --color <color>", "New palette key").option("--slug <slug>", "New URL slug").option("--visibility <visibility>", "public, unlisted or private").option("--json", "Print machine-readable JSON").action(updateProjectAction);
586
+ project.command("delete <project>").description("Delete a project (its trajectory sets are ungrouped, not deleted)").option("-y, --yes", "Skip confirmation prompt").option("--json", "Print machine-readable JSON").action(deleteProjectAction);
587
+ project.command("add <project> <sets...>").description("File one or more trajectory sets into a project (URLs or ids)").option("--json", "Print machine-readable JSON").action(projectAddAction);
588
+ 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);
385
589
  program.command("whoami").description("Show current authentication status").action(async () => {
386
590
  const chalk = (await import("chalk")).default;
387
591
  const { status } = await import("./auth-4UEZSE4C.js");
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  parseJobId
3
- } from "./chunk-PF2EMA6Q.js";
3
+ } from "./chunk-IUSBK45Q.js";
4
+ import "./chunk-35B24E3O.js";
4
5
  import {
5
6
  getApiUrl,
6
7
  getAuthHeader
@@ -2,7 +2,8 @@ import {
2
2
  DOWNLOAD_TIMEOUT_MS,
3
3
  downloadJob,
4
4
  parseJobId
5
- } from "./chunk-PF2EMA6Q.js";
5
+ } from "./chunk-IUSBK45Q.js";
6
+ import "./chunk-35B24E3O.js";
6
7
  import "./chunk-Q2ZEN43L.js";
7
8
  export {
8
9
  DOWNLOAD_TIMEOUT_MS,
@@ -0,0 +1,148 @@
1
+ import {
2
+ parseJobId
3
+ } from "./chunk-IUSBK45Q.js";
4
+ import "./chunk-35B24E3O.js";
5
+ import {
6
+ getApiUrl,
7
+ getAuthHeader
8
+ } from "./chunk-Q2ZEN43L.js";
9
+
10
+ // src/projects.ts
11
+ var PROJECT_COLORS = [
12
+ "slate",
13
+ "blue",
14
+ "green",
15
+ "amber",
16
+ "purple",
17
+ "pink",
18
+ "teal",
19
+ "red"
20
+ ];
21
+ var PROJECT_VISIBILITIES = ["public", "unlisted", "private"];
22
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
23
+ async function apiRequest(path, init = {}) {
24
+ const auth = await getAuthHeader();
25
+ if (!auth) {
26
+ throw new Error("Not authenticated. Run: trajectories auth login");
27
+ }
28
+ const headers = {
29
+ ...init.headers,
30
+ Authorization: auth
31
+ };
32
+ const response = await fetch(`${getApiUrl()}${path}`, { ...init, headers });
33
+ if (!response.ok) {
34
+ const responseText = await response.text();
35
+ let detail = responseText;
36
+ try {
37
+ const payload = JSON.parse(responseText);
38
+ if (typeof payload?.detail === "string") detail = payload.detail;
39
+ } catch {
40
+ }
41
+ const message = detail || `Request failed (${response.status})`;
42
+ const error = new Error(message);
43
+ error.status = response.status;
44
+ throw error;
45
+ }
46
+ if (response.status === 204) return void 0;
47
+ return response.json();
48
+ }
49
+ async function listProjects() {
50
+ return apiRequest("/api/org/projects");
51
+ }
52
+ async function getProject(projectId) {
53
+ return apiRequest(`/api/org/projects/${encodeURIComponent(projectId)}`);
54
+ }
55
+ function resolveProjectRef(ref, projects) {
56
+ const trimmed = (ref || "").trim();
57
+ if (!trimmed) throw new Error("A project is required");
58
+ const urlMatch = trimmed.match(/\/projects\/([0-9a-f-]{36})/i);
59
+ if (urlMatch) return { id: urlMatch[1], project: projects.find((p) => p.id === urlMatch[1]) };
60
+ if (UUID_RE.test(trimmed) && !trimmed.includes("/")) {
61
+ return { id: trimmed, project: projects.find((p) => p.id === trimmed) };
62
+ }
63
+ const needle = trimmed.toLowerCase();
64
+ const bySlug = projects.filter((p) => (p.slug || "").toLowerCase() === needle);
65
+ if (bySlug.length === 1) return { id: bySlug[0].id, project: bySlug[0] };
66
+ const byName = projects.filter((p) => (p.name || "").toLowerCase() === needle);
67
+ if (byName.length === 1) return { id: byName[0].id, project: byName[0] };
68
+ if (byName.length > 1) {
69
+ throw new Error(
70
+ `${byName.length} projects are named "${trimmed}". Use the project id or URL instead:
71
+ ` + byName.map((p) => ` ${p.id} ${p.name}`).join("\n")
72
+ );
73
+ }
74
+ const partial = projects.filter((p) => (p.name || "").toLowerCase().includes(needle));
75
+ if (partial.length === 1) return { id: partial[0].id, project: partial[0] };
76
+ if (partial.length > 1) {
77
+ throw new Error(
78
+ `"${trimmed}" matches ${partial.length} projects. Be more specific, or use the id:
79
+ ` + partial.map((p) => ` ${p.id} ${p.name}`).join("\n")
80
+ );
81
+ }
82
+ throw new Error(
83
+ `No project matching "${trimmed}". Run: trajectories project list`
84
+ );
85
+ }
86
+ async function createProject(input) {
87
+ const color = input.color ?? "slate";
88
+ if (!PROJECT_COLORS.includes(color)) {
89
+ throw new Error(`--color must be one of: ${PROJECT_COLORS.join(", ")}`);
90
+ }
91
+ const visibility = input.visibility ?? "unlisted";
92
+ if (!PROJECT_VISIBILITIES.includes(visibility)) {
93
+ throw new Error(`--visibility must be one of: ${PROJECT_VISIBILITIES.join(", ")}`);
94
+ }
95
+ return apiRequest("/api/org/projects", {
96
+ method: "POST",
97
+ headers: { "Content-Type": "application/json" },
98
+ body: JSON.stringify({
99
+ name: input.name,
100
+ description: input.description ?? "",
101
+ color,
102
+ visibility,
103
+ ...input.slug ? { slug: input.slug } : {}
104
+ })
105
+ });
106
+ }
107
+ async function updateProject(projectId, patch) {
108
+ if (patch.color && !PROJECT_COLORS.includes(patch.color)) {
109
+ throw new Error(`--color must be one of: ${PROJECT_COLORS.join(", ")}`);
110
+ }
111
+ if (patch.visibility && !PROJECT_VISIBILITIES.includes(patch.visibility)) {
112
+ throw new Error(`--visibility must be one of: ${PROJECT_VISIBILITIES.join(", ")}`);
113
+ }
114
+ return apiRequest(`/api/org/projects/${encodeURIComponent(projectId)}`, {
115
+ method: "PUT",
116
+ headers: { "Content-Type": "application/json" },
117
+ body: JSON.stringify(patch)
118
+ });
119
+ }
120
+ async function deleteProject(projectId) {
121
+ await apiRequest(`/api/org/projects/${encodeURIComponent(projectId)}`, {
122
+ method: "DELETE"
123
+ });
124
+ }
125
+ async function assignJobs(projectId, jobRefs, action) {
126
+ const jobIds = jobRefs.map((ref) => parseJobId(ref));
127
+ const unique = [...new Set(jobIds)];
128
+ if (unique.length === 0) throw new Error("At least one trajectory set is required");
129
+ if (unique.length > 500) {
130
+ throw new Error(`Cannot file more than 500 trajectory sets at once (got ${unique.length})`);
131
+ }
132
+ return apiRequest("/api/org/projects/assign", {
133
+ method: "POST",
134
+ headers: { "Content-Type": "application/json" },
135
+ body: JSON.stringify({ job_ids: unique, project_id: projectId, action })
136
+ });
137
+ }
138
+ export {
139
+ PROJECT_COLORS,
140
+ PROJECT_VISIBILITIES,
141
+ assignJobs,
142
+ createProject,
143
+ deleteProject,
144
+ getProject,
145
+ listProjects,
146
+ resolveProjectRef,
147
+ updateProject
148
+ };
@@ -1,110 +1,19 @@
1
1
  import {
2
2
  preflightQuota
3
3
  } from "./chunk-2ELJH3IV.js";
4
+ import {
5
+ buildScreenshotChunks
6
+ } from "./chunk-35B24E3O.js";
4
7
  import {
5
8
  getApiUrl,
6
9
  getAuthHeader
7
10
  } from "./chunk-Q2ZEN43L.js";
8
11
 
9
12
  // src/upload.ts
10
- import { readdirSync as readdirSync2, statSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, copyFileSync, unlinkSync } from "fs";
11
- import { join as join2, relative, dirname } from "path";
13
+ import { readdirSync, statSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, unlinkSync } from "fs";
14
+ import { join, relative, dirname } from "path";
12
15
  import { tmpdir } from "os";
13
16
  import { randomBytes, createHash } from "crypto";
14
-
15
- // src/screenshot-chunks.ts
16
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
17
- import { extname, join } from "path";
18
- var CHUNK_DIR = "_screenshot_chunks";
19
- var MANIFEST_NAME = "manifest.json";
20
- var MANIFEST_VERSION = 1;
21
- var CHUNK_TARGET_BYTES = 4 * 1024 * 1024;
22
- var IMAGE_MIME_BY_EXT = {
23
- ".webp": "image/webp",
24
- ".png": "image/png",
25
- ".jpg": "image/jpeg",
26
- ".jpeg": "image/jpeg",
27
- ".gif": "image/gif"
28
- };
29
- function mimeFor(path) {
30
- return IMAGE_MIME_BY_EXT[extname(path).toLowerCase()] ?? null;
31
- }
32
- function referencedImagePaths(trajectory) {
33
- const out = [];
34
- const seen = /* @__PURE__ */ new Set();
35
- const collect = (parts) => {
36
- if (!Array.isArray(parts)) return;
37
- for (const part of parts) {
38
- if (!part || typeof part !== "object" || part.type !== "image") continue;
39
- const path = part.source?.path;
40
- if (typeof path === "string" && path && !seen.has(path)) {
41
- seen.add(path);
42
- out.push(path);
43
- }
44
- }
45
- };
46
- for (const step of Array.isArray(trajectory?.steps) ? trajectory.steps : []) {
47
- if (!step || typeof step !== "object") continue;
48
- collect(step.message);
49
- for (const r of Array.isArray(step.observation?.results) ? step.observation.results : []) {
50
- if (r && typeof r === "object") collect(r.content);
51
- }
52
- }
53
- return out;
54
- }
55
- function buildScreenshotChunks(jobDir, outDir, opts = {}) {
56
- const targetBytes = opts.targetBytes ?? CHUNK_TARGET_BYTES;
57
- const files = [];
58
- let imagesPacked = 0;
59
- let trialsPacked = 0;
60
- for (const entry of readdirSync(jobDir, { withFileTypes: true })) {
61
- if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
62
- const trial = entry.name;
63
- const trajPath = join(jobDir, trial, "agent", "trajectory.json");
64
- if (!existsSync(trajPath)) continue;
65
- let paths;
66
- try {
67
- paths = referencedImagePaths(JSON.parse(readFileSync(trajPath, "utf-8")));
68
- } catch {
69
- continue;
70
- }
71
- const images = paths.map((p) => ({ p, abs: join(jobDir, trial, "agent", p), mime: mimeFor(p) })).filter((x) => x.mime && existsSync(x.abs));
72
- if (images.length === 0) continue;
73
- const relDir = `${trial}/${CHUNK_DIR}/agent`;
74
- const absDir = join(outDir, relDir);
75
- mkdirSync(absDir, { recursive: true });
76
- const manifest = { version: MANIFEST_VERSION, chunks: [] };
77
- let current = null;
78
- const seal = () => {
79
- if (!current) return;
80
- const abs = join(absDir, current.name);
81
- writeFileSync(abs, Buffer.concat(current.parts));
82
- manifest.chunks.push({ name: current.name, size: current.size, entries: current.entries });
83
- files.push({ absPath: abs, relPath: `${relDir}/${current.name}`, size: current.size });
84
- current = null;
85
- };
86
- for (const { p, abs, mime } of images) {
87
- const data = readFileSync(abs);
88
- if (current && current.size + data.length > targetBytes) seal();
89
- if (!current) {
90
- current = { name: `chunk-${String(manifest.chunks.length).padStart(3, "0")}.bin`, parts: [], entries: [], size: 0 };
91
- }
92
- current.entries.push([p, current.size, data.length, mime]);
93
- current.parts.push(data);
94
- current.size += data.length;
95
- imagesPacked++;
96
- }
97
- seal();
98
- const manifestAbs = join(absDir, MANIFEST_NAME);
99
- const manifestBytes = Buffer.from(JSON.stringify(manifest));
100
- writeFileSync(manifestAbs, manifestBytes);
101
- files.push({ absPath: manifestAbs, relPath: `${relDir}/${MANIFEST_NAME}`, size: manifestBytes.length });
102
- trialsPacked++;
103
- }
104
- return { files, imagesPacked, trialsPacked };
105
- }
106
-
107
- // src/upload.ts
108
17
  var SKIP = /* @__PURE__ */ new Set(["__pycache__", ".DS_Store", ".git", "node_modules"]);
109
18
  var MAX_FILE_SIZE = 50 * 1024 * 1024;
110
19
  var CONCURRENCY = Number(process.env.TRAJECTORIES_CLI_CONCURRENCY ?? "24");
@@ -115,9 +24,9 @@ function collectFiles(dir) {
115
24
  const files = [];
116
25
  const skipped = [];
117
26
  function walk(current) {
118
- for (const entry of readdirSync2(current, { withFileTypes: true })) {
27
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
119
28
  if (SKIP.has(entry.name)) continue;
120
- const full = join2(current, entry.name);
29
+ const full = join(current, entry.name);
121
30
  if (entry.isDirectory()) {
122
31
  walk(full);
123
32
  } else if (entry.isFile()) {
@@ -266,13 +175,13 @@ async function convertToWebP(dir) {
266
175
  console.log(" \u26A0 sharp not available, skipping WebP conversion");
267
176
  return { convertedDir: dir, pngsConverted: 0, savedBytes: 0 };
268
177
  }
269
- const tmpDir = join2(tmpdir(), `trajectories-webp-${randomBytes(4).toString("hex")}`);
178
+ const tmpDir = join(tmpdir(), `trajectories-webp-${randomBytes(4).toString("hex")}`);
270
179
  function copyDir(src, dest) {
271
- mkdirSync2(dest, { recursive: true });
272
- for (const entry of readdirSync2(src, { withFileTypes: true })) {
180
+ mkdirSync(dest, { recursive: true });
181
+ for (const entry of readdirSync(src, { withFileTypes: true })) {
273
182
  if (SKIP.has(entry.name)) continue;
274
- const srcPath = join2(src, entry.name);
275
- const destPath = join2(dest, entry.name);
183
+ const srcPath = join(src, entry.name);
184
+ const destPath = join(dest, entry.name);
276
185
  if (entry.isDirectory()) {
277
186
  copyDir(srcPath, destPath);
278
187
  } else {
@@ -300,7 +209,7 @@ async function convertToWebP(dir) {
300
209
  let processed = 0;
301
210
  const total = pngs.length;
302
211
  async function convertOne(f) {
303
- const srcPath = join2(tmpDir, f.relPath);
212
+ const srcPath = join(tmpDir, f.relPath);
304
213
  const webpPath = srcPath.replace(/\.png$/i, ".webp");
305
214
  try {
306
215
  await pngToWebp(srcPath, webpPath);
@@ -332,10 +241,10 @@ async function convertToWebP(dir) {
332
241
  const trajFiles = collectFiles(tmpDir).files.filter((f) => f.relPath.endsWith("trajectory.json"));
333
242
  for (const tf of trajFiles) {
334
243
  try {
335
- let content = readFileSync2(tf.absPath, "utf-8");
244
+ let content = readFileSync(tf.absPath, "utf-8");
336
245
  const original = content;
337
246
  content = content.replace(/screenshots\/([^"]+)\.png/g, (match, name) => {
338
- const webpPath = join2(dirname(tf.absPath), "screenshots", `${name}.webp`);
247
+ const webpPath = join(dirname(tf.absPath), "screenshots", `${name}.webp`);
339
248
  try {
340
249
  statSync(webpPath);
341
250
  return `screenshots/${name}.webp`;
@@ -344,7 +253,7 @@ async function convertToWebP(dir) {
344
253
  }
345
254
  });
346
255
  if (content !== original) {
347
- writeFileSync2(tf.absPath, content);
256
+ writeFileSync(tf.absPath, content);
348
257
  }
349
258
  } catch {
350
259
  }
@@ -390,7 +299,7 @@ function validateAtifFormat(dir) {
390
299
  errors.push("Missing result.json at the job root");
391
300
  } else {
392
301
  try {
393
- const jobResult = JSON.parse(readFileSync2(join2(dir, "result.json"), "utf-8"));
302
+ const jobResult = JSON.parse(readFileSync(join(dir, "result.json"), "utf-8"));
394
303
  const jobRequired = [
395
304
  ["n_total_trials", "n_total_trials (number of trials)"],
396
305
  ["started_at", "started_at (ISO timestamp)"],
@@ -426,9 +335,9 @@ function validateAtifFormat(dir) {
426
335
  return errors;
427
336
  }
428
337
  for (const trial of trialDirs) {
429
- const trialResultPath = join2(dir, trial, "result.json");
338
+ const trialResultPath = join(dir, trial, "result.json");
430
339
  try {
431
- const data = JSON.parse(readFileSync2(trialResultPath, "utf-8"));
340
+ const data = JSON.parse(readFileSync(trialResultPath, "utf-8"));
432
341
  const required = [
433
342
  ["task_name", "task_name (string)"],
434
343
  ["started_at", "started_at (ISO timestamp)"],
@@ -458,7 +367,7 @@ function validateAtifFormat(dir) {
458
367
  errors.push(`Trial "${trial}": missing agent/trajectory.json`);
459
368
  } else {
460
369
  try {
461
- const trajData = JSON.parse(readFileSync2(join2(dir, trajectoryRelPath), "utf-8"));
370
+ const trajData = JSON.parse(readFileSync(join(dir, trajectoryRelPath), "utf-8"));
462
371
  const steps = trajData.steps ?? [];
463
372
  const missingImages = [];
464
373
  for (const step of steps) {
@@ -539,16 +448,19 @@ ${samples}${more}
539
448
  const t0 = Date.now();
540
449
  const contentHash = await computeContentHash(files);
541
450
  console.log(` Hash: ${contentHash.slice(0, 16)}... (${((Date.now() - t0) / 1e3).toFixed(1)}s)`);
542
- const chunkDir = join2(tmpdir(), `trajectories-chunks-${randomBytes(4).toString("hex")}`);
451
+ const chunkDir = join(tmpdir(), `trajectories-chunks-${randomBytes(4).toString("hex")}`);
543
452
  const chunkBuild = buildScreenshotChunks(convertedDir, chunkDir);
544
- const uploadFiles = [...files, ...chunkBuild.files];
545
- const chunkBytes = chunkBuild.files.reduce((s, f) => s + f.size, 0);
453
+ const uploadFiles = [
454
+ ...files.filter((f) => !chunkBuild.packedRelPaths.has(f.relPath.split("\\").join("/"))),
455
+ ...chunkBuild.files
456
+ ];
457
+ const uploadBytes = uploadFiles.reduce((s, f) => s + f.size, 0);
546
458
  if (chunkBuild.imagesPacked > 0) {
547
459
  console.log(
548
- ` \u2713 Packed ${chunkBuild.imagesPacked} screenshots into ${chunkBuild.files.length - chunkBuild.trialsPacked} chunks (${(chunkBytes / 1024 / 1024).toFixed(1)} MB) across ${chunkBuild.trialsPacked} trial${chunkBuild.trialsPacked === 1 ? "" : "s"}`
460
+ ` \u2713 Packed ${chunkBuild.imagesPacked} screenshots into ${chunkBuild.files.length - chunkBuild.trialsPacked} chunks (${(chunkBuild.packedBytes / 1024 / 1024).toFixed(1)} MB) across ${chunkBuild.trialsPacked} trial${chunkBuild.trialsPacked === 1 ? "" : "s"} \u2014 ${uploadFiles.length} uploads`
549
461
  );
550
462
  }
551
- const initResp = await apiPost("/api/cli/push/init", { slug, name, visibility, estimated_bytes: totalSize + chunkBytes, content_hash: contentHash });
463
+ const initResp = await apiPost("/api/cli/push/init", { slug, name, visibility, estimated_bytes: uploadBytes, content_hash: contentHash });
552
464
  if (!initResp.ok) {
553
465
  throw new Error(`Init failed: ${await initResp.text()}`);
554
466
  }
@@ -709,7 +621,7 @@ async function fetchSignedUrlsForBatch(jobId, paths) {
709
621
  return { urlMap, errors };
710
622
  }
711
623
  async function uploadOneFileWithError(file, signedUrl) {
712
- const data = readFileSync2(file.absPath);
624
+ const data = readFileSync(file.absPath);
713
625
  let lastError = "unknown error";
714
626
  const MAX_ATTEMPTS = 8;
715
627
  for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trajectories-sh",
3
- "version": "1.21.1",
3
+ "version": "1.23.0",
4
4
  "description": "CLI for uploading trajectory jobs to trajectories.sh",
5
5
  "type": "module",
6
6
  "bin": {