trajectories-sh 1.24.1 → 1.25.1

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.
@@ -1,7 +1,6 @@
1
1
  import {
2
2
  parseJobId
3
- } from "./chunk-LOJZPDGI.js";
4
- import "./chunk-72L6XPK6.js";
3
+ } from "./chunk-MMQI3LNY.js";
5
4
  import {
6
5
  getApiUrl,
7
6
  getAuthHeader
@@ -159,12 +158,13 @@ function formatComment(comment) {
159
158
  return `[${comment.id}] ${author} \xB7 ${scope}${when ? ` \xB7 ${when}` : ""}
160
159
  ${comment.body}`;
161
160
  }
161
+
162
162
  export {
163
- addComment,
164
- deleteComment,
165
- formatComment,
166
- listComments,
167
163
  parseCommentTarget,
168
164
  readCommentBody,
169
- updateComment
165
+ addComment,
166
+ listComments,
167
+ updateComment,
168
+ deleteComment,
169
+ formatComment
170
170
  };
@@ -15,6 +15,16 @@ 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
15
  var BATCH_SIZE = 100;
16
16
  var CONCURRENCY = 25;
17
17
  var DOWNLOAD_TIMEOUT_MS = 18e4;
18
+ var MAX_ATTEMPTS = 7;
19
+ function retryDelayMs(attempt, status = 0, retryAfter = null) {
20
+ if (retryAfter) {
21
+ const secs = Number(retryAfter);
22
+ const ms = Number.isFinite(secs) ? secs * 1e3 : Date.parse(retryAfter) - Date.now();
23
+ if (Number.isFinite(ms) && ms > 0) return Math.min(ms, 3e4);
24
+ }
25
+ const base = Math.min(16e3, 500 * 2 ** attempt) * (status === 429 ? 2 : 1);
26
+ return Math.round(base / 2 + Math.random() * (base / 2));
27
+ }
18
28
  var EXIT = {
19
29
  OK: 0,
20
30
  ERROR: 1,
@@ -154,10 +164,14 @@ async function downloadPlannedJob(plan, outputDir, options) {
154
164
  const downloadUrl = signedUrl ?? `${apiUrl}/api/cli/download/${jobId}/file?path=${encodeURIComponent(file.path)}`;
155
165
  const downloadHeaders = signedUrl ? {} : headers;
156
166
  let ok = false;
157
- for (let attempt = 0; attempt < 3 && !ok; attempt++) {
167
+ for (let attempt = 0; attempt < MAX_ATTEMPTS && !ok; attempt++) {
168
+ let wait = 0;
158
169
  try {
159
170
  const resp = await fetch(downloadUrl, { headers: downloadHeaders, signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
160
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
171
+ if (!resp.ok) {
172
+ wait = retryDelayMs(attempt, resp.status, resp.headers.get("retry-after"));
173
+ throw new Error(`HTTP ${resp.status}`);
174
+ }
161
175
  const data = Buffer.from(await resp.arrayBuffer());
162
176
  const outPath = join(outputDir, file.path);
163
177
  mkdirSync(dirname(outPath), { recursive: true });
@@ -167,7 +181,7 @@ async function downloadPlannedJob(plan, outputDir, options) {
167
181
  bar.tick(data.length);
168
182
  ok = true;
169
183
  } catch {
170
- if (attempt < 2) await new Promise((r) => setTimeout(r, 1e3 * (attempt + 1)));
184
+ if (attempt < MAX_ATTEMPTS - 1) await new Promise((r) => setTimeout(r, wait || retryDelayMs(attempt)));
171
185
  }
172
186
  }
173
187
  if (!ok) {
@@ -229,6 +243,7 @@ function dirBytes(dir) {
229
243
 
230
244
  export {
231
245
  DOWNLOAD_TIMEOUT_MS,
246
+ retryDelayMs,
232
247
  EXIT,
233
248
  CliError,
234
249
  httpError,
package/dist/cli.js CHANGED
@@ -99,7 +99,7 @@ async function contractRequest(path, init = {}) {
99
99
  signal: AbortSignal.timeout(12e4)
100
100
  });
101
101
  if (!response.ok) {
102
- const body = await response.json().catch(() => ({ detail: "Contract request failed" }));
102
+ const body = await response.json().catch(() => ({ detail: "Agreement request failed" }));
103
103
  throw new ContractError(response.status, body.detail);
104
104
  }
105
105
  if (response.headers.get("content-type")?.includes("application/pdf"))
@@ -110,8 +110,8 @@ function errorExit(status) {
110
110
  return status === 401 ? 3 : status === 403 ? 4 : status === 409 ? 5 : status === 422 ? 2 : 1;
111
111
  }
112
112
  function registerContracts(program2) {
113
- const group = program2.command("contracts").description(
114
- "Draft, issue and manage private Refresh contracts. Never signs for a recipient."
113
+ const group = program2.command("agreements").alias("contracts").description(
114
+ "Draft, issue and manage private Refresh agreements. Never signs for a recipient."
115
115
  );
116
116
  const wrap = (fn) => async (...args) => {
117
117
  try {
@@ -137,7 +137,7 @@ function registerContracts(program2) {
137
137
  ).requiredOption("--file <path>").requiredOption("--output <path>")
138
138
  ).action(
139
139
  wrap(async (opts) => {
140
- const { previewContractFile } = await import("./contracts-preview-3J2PLT3U.js");
140
+ const { previewContractFile } = await import("./contracts-preview-3DFBYKZV.js");
141
141
  return previewContractFile(opts.file, opts.output);
142
142
  })
143
143
  );
@@ -267,6 +267,330 @@ function registerContracts(program2) {
267
267
  );
268
268
  }
269
269
 
270
+ // src/assets.ts
271
+ import { createHash, randomUUID } from "crypto";
272
+ import { createReadStream, createWriteStream } from "fs";
273
+ import { open, readFile as readFile2, stat, unlink } from "fs/promises";
274
+ import { basename, resolve } from "path";
275
+ import { Readable, Transform } from "stream";
276
+ import { pipeline } from "stream/promises";
277
+ var AssetError = class extends Error {
278
+ constructor(message, status = 0) {
279
+ super(message);
280
+ this.status = status;
281
+ }
282
+ status;
283
+ };
284
+ function uuid(value) {
285
+ if (!/^[a-f\d]{8}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{12}$/i.test(value))
286
+ throw new Error(`Expected a stable UUID, received: ${value}`);
287
+ return value;
288
+ }
289
+ async function assetRequest(path, init = {}) {
290
+ const authorization = await getAuthHeader();
291
+ if (!authorization)
292
+ throw new AssetError(
293
+ "Authentication required. Run trajectories auth login or set TRAJECTORIES_API_KEY.",
294
+ 401
295
+ );
296
+ const response = await fetch(`${getApiUrl()}/api/assets${path}`, {
297
+ ...init,
298
+ signal: init.signal || AbortSignal.timeout(18e4),
299
+ headers: {
300
+ Authorization: authorization,
301
+ ...typeof init.body === "string" ? { "Content-Type": "application/json" } : {},
302
+ ...init.headers
303
+ }
304
+ });
305
+ if (!response.ok) {
306
+ const payload = await response.json().catch(() => ({}));
307
+ throw new AssetError(
308
+ typeof payload.detail === "string" ? payload.detail : `Asset API returned ${response.status}; inspect input fields and retry.`,
309
+ response.status
310
+ );
311
+ }
312
+ return response.json();
313
+ }
314
+ async function retry(fn, delay = 500) {
315
+ for (let attempt = 0; ; attempt++) {
316
+ try {
317
+ return await fn();
318
+ } catch (error) {
319
+ if (attempt >= 3 || error instanceof AssetError && error.status < 500 && error.status !== 429)
320
+ throw error;
321
+ await new Promise((resolve2) => setTimeout(resolve2, delay * 2 ** attempt));
322
+ }
323
+ }
324
+ }
325
+ async function hashFile(path) {
326
+ const digest = createHash("sha256");
327
+ for await (const data of createReadStream(path)) digest.update(data);
328
+ return digest.digest("hex");
329
+ }
330
+ async function uploadAsset(asset, path, options, report = () => {
331
+ }) {
332
+ uuid(asset);
333
+ const file = await stat(path);
334
+ if (!file.isFile() || !path.endsWith(".zip") || file.size === 0)
335
+ throw new Error(
336
+ "Upload a non-empty .zip file; directories are not executed or packaged automatically."
337
+ );
338
+ const config = await assetRequest("/config");
339
+ if (file.size > config.max_bytes)
340
+ throw new Error(
341
+ `ZIP exceeds the server limit (${config.max_bytes} bytes). Split it or contact your administrator.`
342
+ );
343
+ const sha256 = await hashFile(path);
344
+ const role = options.role || "bundle";
345
+ if (!["bundle", "sample"].includes(role))
346
+ throw new Error("--role must be bundle or sample");
347
+ const key = options.idempotencyKey ? uuid(options.idempotencyKey) : randomUUID();
348
+ report({
349
+ event: "upload.start",
350
+ asset_id: asset,
351
+ idempotency_key: key,
352
+ sha256
353
+ });
354
+ let version2 = options.version ? await assetRequest(`/${asset}/versions/${uuid(options.version)}`) : await retry(
355
+ () => assetRequest(`/${asset}/versions`, {
356
+ method: "POST",
357
+ headers: { "Idempotency-Key": key },
358
+ body: JSON.stringify({
359
+ role,
360
+ filename: basename(path),
361
+ size: file.size,
362
+ sha256
363
+ })
364
+ })
365
+ );
366
+ report({
367
+ event: "upload.resume",
368
+ asset_id: asset,
369
+ version_id: version2.id,
370
+ command: `trajectories assets upload ${asset} ${JSON.stringify(path)} --role ${role} --version ${version2.id} --json`
371
+ });
372
+ if (version2.sha256 !== sha256 || version2.size !== file.size || version2.role !== role)
373
+ throw new Error(
374
+ "Resume file does not match original size, role or SHA-256; use the unchanged ZIP."
375
+ );
376
+ if (version2.status === "ready") return version2;
377
+ if (version2.status === "expired")
378
+ throw new Error(
379
+ "Upload expired; start another upload without --version and with a new idempotency key."
380
+ );
381
+ version2 = await assetRequest(`/${asset}/versions/${version2.id}`);
382
+ const completed = new Set(version2.parts?.map((p) => p.part));
383
+ const count = Math.ceil(file.size / version2.chunk_bytes);
384
+ const handle = await open(path, "r");
385
+ try {
386
+ for (let part = 0; part < count; part++) {
387
+ if (!completed.has(part)) {
388
+ const data = Buffer.alloc(
389
+ Math.min(version2.chunk_bytes, file.size - part * version2.chunk_bytes)
390
+ );
391
+ const read = await handle.read(
392
+ data,
393
+ 0,
394
+ data.length,
395
+ part * version2.chunk_bytes
396
+ );
397
+ if (read.bytesRead !== data.length)
398
+ throw new Error(
399
+ "File changed while uploading. Restore the original file before resuming."
400
+ );
401
+ const checksum = createHash("sha256").update(data).digest("hex");
402
+ await retry(
403
+ () => assetRequest(`/${asset}/versions/${version2.id}/parts/${part}`, {
404
+ method: "PUT",
405
+ headers: {
406
+ "Content-Type": "application/octet-stream",
407
+ "X-Content-SHA256": checksum
408
+ },
409
+ body: data
410
+ })
411
+ );
412
+ }
413
+ report({
414
+ event: "upload.progress",
415
+ version_id: version2.id,
416
+ completed_parts: part + 1,
417
+ total_parts: count
418
+ });
419
+ }
420
+ } finally {
421
+ await handle.close();
422
+ }
423
+ return retry(
424
+ () => assetRequest(`/${asset}/versions/${version2.id}/complete`, {
425
+ method: "POST"
426
+ })
427
+ );
428
+ }
429
+ async function downloadAsset(asset, version2, output2) {
430
+ uuid(asset);
431
+ uuid(version2);
432
+ const result = await assetRequest(`/${asset}/versions/${version2}/download`, { method: "POST" });
433
+ const response = await fetch(result.url, {
434
+ signal: AbortSignal.timeout(18e4)
435
+ });
436
+ if (!response.ok || !response.body)
437
+ throw new Error(
438
+ `Download failed (${response.status}); retry to obtain a new short-lived link.`
439
+ );
440
+ const path = resolve(output2);
441
+ const digest = createHash("sha256");
442
+ let size = 0;
443
+ let created = false;
444
+ const handle = await open(path, "wx", 384);
445
+ created = true;
446
+ try {
447
+ const checker = new Transform({
448
+ transform(chunk, _encoding, callback) {
449
+ size += chunk.length;
450
+ if (size > result.size) {
451
+ callback(new Error("Download exceeds manifest size"));
452
+ return;
453
+ }
454
+ digest.update(chunk);
455
+ callback(null, chunk);
456
+ }
457
+ });
458
+ await pipeline(
459
+ Readable.fromWeb(response.body),
460
+ checker,
461
+ createWriteStream(path, { fd: handle.fd, autoClose: false })
462
+ );
463
+ if (size !== result.size || digest.digest("hex") !== result.sha256)
464
+ throw new Error(
465
+ "Download checksum mismatch. Incomplete output removed; retry."
466
+ );
467
+ return {
468
+ asset_id: asset,
469
+ version_id: version2,
470
+ path,
471
+ size,
472
+ sha256: result.sha256
473
+ };
474
+ } catch (error) {
475
+ if (created) await unlink(path).catch(() => {
476
+ });
477
+ throw error;
478
+ } finally {
479
+ await handle.close();
480
+ }
481
+ }
482
+ function action(fn) {
483
+ return async (...args) => {
484
+ const options = args.at(-1)?.opts?.() || {};
485
+ try {
486
+ const result = await fn(...args);
487
+ console.log(JSON.stringify(result, null, options.json ? void 0 : 2));
488
+ } catch (error) {
489
+ const message = error instanceof Error ? error.message : String(error);
490
+ const status = error instanceof AssetError ? error.status : 0;
491
+ console.error(
492
+ options.json ? JSON.stringify({
493
+ error: {
494
+ message,
495
+ status,
496
+ retryable: status >= 500 || status === 429
497
+ }
498
+ }) : message
499
+ );
500
+ process.exitCode = status === 401 || status === 403 ? 3 : status === 404 ? 4 : 1;
501
+ }
502
+ };
503
+ }
504
+ function registerAssets(program2) {
505
+ const assets = program2.command("assets").description(
506
+ "Private organization asset library; UI and CLI share one API"
507
+ );
508
+ assets.command("config").description("Inspect quotas, organization and staff access").option("--json", "Machine-readable JSON").action(action(() => assetRequest("/config")));
509
+ assets.command("list").description(
510
+ "List/filter one cursor page (staff inventory requires explicit access)"
511
+ ).option("--query <text>").option("--kind <type>").option("--tag <tag>").option("--archived").option("--cursor <uuid>").option("--limit <count>", "Page size, 1\u2013100", "30").option("--staff", "Cross-organization staff inventory").option("--json").action(
512
+ action(async (options) => {
513
+ const params = new URLSearchParams({
514
+ status: options.archived ? "archived" : "active",
515
+ limit: options.limit
516
+ });
517
+ for (const [key, value] of Object.entries({
518
+ q: options.query,
519
+ kind: options.kind,
520
+ tag: options.tag,
521
+ cursor: options.cursor
522
+ }))
523
+ if (value) params.set(key, String(value));
524
+ return assetRequest(
525
+ `${options.staff ? "/staff/inventory" : ""}?${params}`
526
+ );
527
+ })
528
+ );
529
+ assets.command("create").description(
530
+ "Create metadata from a JSON file; authorized Refresh staff may review uploaded material"
531
+ ).requiredOption("--metadata <file>").option("--idempotency-key <uuid>").option("--json").action(
532
+ action(async (options) => {
533
+ const payload = JSON.parse(await readFile2(options.metadata, "utf8"));
534
+ const key = options.idempotencyKey ? uuid(options.idempotencyKey) : randomUUID();
535
+ return retry(
536
+ () => assetRequest("", {
537
+ method: "POST",
538
+ headers: { "Idempotency-Key": key },
539
+ body: JSON.stringify(payload)
540
+ })
541
+ );
542
+ })
543
+ );
544
+ assets.command("inspect <asset-id>").option("--json").action(action((id) => assetRequest(`/${uuid(id)}`)));
545
+ assets.command("update <asset-id>").description("Replace editable metadata, preserving immutable versions").requiredOption("--metadata <file>").option("--json").action(
546
+ action(
547
+ async (id, options) => assetRequest(`/${uuid(id)}`, {
548
+ method: "PUT",
549
+ body: JSON.stringify(
550
+ JSON.parse(await readFile2(options.metadata, "utf8"))
551
+ )
552
+ })
553
+ )
554
+ );
555
+ assets.command("upload <asset-id> <zip>").description(
556
+ "Upload or resume a verified ZIP; progress/resume information goes to stderr"
557
+ ).option("--role <role>", "bundle or sample", "bundle").option("--version <uuid>", "Resume an existing upload").option("--idempotency-key <uuid>").option("--json").action(
558
+ action(
559
+ (id, path, options) => uploadAsset(
560
+ id,
561
+ path,
562
+ options,
563
+ (event) => console.error(JSON.stringify(event))
564
+ )
565
+ )
566
+ );
567
+ assets.command("status <asset-id> <version-id>").description(
568
+ "Inspect upload parts, status, checksum and published file manifest"
569
+ ).option("--json").action(
570
+ action(
571
+ (id, version2) => assetRequest(`/${uuid(id)}/versions/${uuid(version2)}`)
572
+ )
573
+ );
574
+ assets.command("download <asset-id> <version-id>").description(
575
+ "Download a version with SHA-256 verification; never overwrites a file"
576
+ ).requiredOption("--out <file>").option("--json").action(
577
+ action((id, version2, options) => downloadAsset(id, version2, options.out))
578
+ );
579
+ assets.command("archive <asset-id>").option("--json").action(
580
+ action((id) => assetRequest(`/${uuid(id)}/archive`, { method: "POST" }))
581
+ );
582
+ assets.command("delete <asset-id>").description("Permanently revoke access and clean stored files").requiredOption(
583
+ "--confirm <asset-id>",
584
+ "Repeat exact asset ID to confirm irreversible deletion"
585
+ ).option("--json").action(
586
+ action((id, options) => {
587
+ if (options.confirm !== id)
588
+ throw new Error("--confirm must exactly match the asset ID");
589
+ return assetRequest(`/${uuid(id)}`, { method: "DELETE" });
590
+ })
591
+ );
592
+ }
593
+
270
594
  // src/cli.ts
271
595
  import { readFileSync } from "fs";
272
596
  import { fileURLToPath } from "url";
@@ -384,7 +708,7 @@ async function uploadTrajectoryAction(pathArg, opts) {
384
708
  );
385
709
  }
386
710
  if (isZip) {
387
- const { uploadZip } = await import("./upload-6HQ5A2H7.js");
711
+ const { uploadZip } = await import("./upload-OW7RWQVX.js");
388
712
  const slug2 = opts.slug ?? pathArg.replace(/\.zip$/i, "").replace(/\/+$/, "").split("/").pop();
389
713
  console.log(chalk.bold(`
390
714
  Uploading ZIP ${pathArg} \u2192 ${chalk.cyan(slug2)}
@@ -413,7 +737,7 @@ async function uploadTrajectoryAction(pathArg, opts) {
413
737
  Uploading trajectory ${pathArg} \u2192 ${chalk.cyan(slug)}
414
738
  `));
415
739
  try {
416
- const { uploadJob } = await import("./upload-6HQ5A2H7.js");
740
+ const { uploadJob } = await import("./upload-OW7RWQVX.js");
417
741
  const result = await uploadJob(pathArg, {
418
742
  slug,
419
743
  name: opts.name
@@ -501,7 +825,7 @@ async function downloadTrajectoryAction(url, opts) {
501
825
  const chalk = (await import("chalk")).default;
502
826
  const json = Boolean(opts.json);
503
827
  try {
504
- const { planJob, downloadPlannedJob, parseJobId } = await import("./download-BWJDAVMM.js");
828
+ const { planJob, downloadPlannedJob, parseJobId } = await import("./download-A3LEBXH5.js");
505
829
  const options = downloadOptions(opts, true);
506
830
  const plan = await planJob(url, options);
507
831
  const outputDir = opts.out ?? opts.output ?? parseJobId(url).replace(/^org:/, "").replace(/:/g, "-");
@@ -539,7 +863,7 @@ async function downloadProjectAction(url, opts) {
539
863
  const chalk = (await import("chalk")).default;
540
864
  const json = Boolean(opts.json);
541
865
  try {
542
- const { planProject, downloadProject, defaultProjectDir } = await import("./download-project-XN4OPF3M.js");
866
+ const { planProject, downloadProject, defaultProjectDir } = await import("./download-project-66IARJWR.js");
543
867
  const options = { ...downloadOptions(opts, false), format: opts.format === "jsonl" ? "jsonl" : "json" };
544
868
  const plan = await planProject(url, options);
545
869
  const outputDir = opts.out ?? defaultProjectDir(plan.project);
@@ -585,7 +909,7 @@ async function downloadProjectAction(url, opts) {
585
909
  }
586
910
  async function deleteTrajectoryAction(url, opts) {
587
911
  const chalk = (await import("chalk")).default;
588
- const { parseJobId } = await import("./download-BWJDAVMM.js");
912
+ const { parseJobId } = await import("./download-A3LEBXH5.js");
589
913
  const { getApiUrl: getApiUrl2, getAuthHeader: getAuthHeader2 } = await import("./config-QS2IT5DI.js");
590
914
  let jobId = parseJobId(url);
591
915
  if (jobId.startsWith("org:")) {
@@ -616,8 +940,8 @@ async function deleteTrajectoryAction(url, opts) {
616
940
  if (!opts.yes) {
617
941
  const readline = await import("readline");
618
942
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
619
- const answer = await new Promise((resolve) => {
620
- rl.question(chalk.yellow(" Are you sure? (y/N) "), resolve);
943
+ const answer = await new Promise((resolve2) => {
944
+ rl.question(chalk.yellow(" Are you sure? (y/N) "), resolve2);
621
945
  });
622
946
  rl.close();
623
947
  if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
@@ -653,7 +977,7 @@ function printCommentError(error, json) {
653
977
  }
654
978
  async function addCommentAction(targetInput, opts) {
655
979
  try {
656
- const { addComment, formatComment, parseCommentTarget, readCommentBody } = await import("./comments-M3EX3VOU.js");
980
+ const { addComment, formatComment, parseCommentTarget, readCommentBody } = await import("./comments-3UOERFNJ.js");
657
981
  const target = parseCommentTarget(targetInput, opts);
658
982
  const body = await readCommentBody(opts);
659
983
  const comment2 = await addComment(target, body);
@@ -669,7 +993,7 @@ ${formatComment(comment2)}
669
993
  }
670
994
  async function listCommentsAction(targetInput, opts) {
671
995
  try {
672
- const { formatComment, listComments, parseCommentTarget } = await import("./comments-M3EX3VOU.js");
996
+ const { formatComment, listComments, parseCommentTarget } = await import("./comments-3UOERFNJ.js");
673
997
  const target = parseCommentTarget(targetInput, opts);
674
998
  const comments = await listComments(target);
675
999
  if (opts.json) {
@@ -687,7 +1011,7 @@ ${comments.map(formatComment).join("\n\n")}
687
1011
  }
688
1012
  async function updateCommentAction(commentId, opts) {
689
1013
  try {
690
- const { formatComment, readCommentBody, updateComment } = await import("./comments-M3EX3VOU.js");
1014
+ const { formatComment, readCommentBody, updateComment } = await import("./comments-3UOERFNJ.js");
691
1015
  const body = await readCommentBody(opts);
692
1016
  const comment2 = await updateComment(commentId, body);
693
1017
  if (opts.json) console.log(JSON.stringify(comment2));
@@ -708,8 +1032,8 @@ async function deleteCommentAction(commentId, opts) {
708
1032
  }
709
1033
  const readline = await import("readline");
710
1034
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
711
- const answer = await new Promise((resolve) => {
712
- rl.question(` Delete comment ${commentId}? (y/N) `, resolve);
1035
+ const answer = await new Promise((resolve2) => {
1036
+ rl.question(` Delete comment ${commentId}? (y/N) `, resolve2);
713
1037
  });
714
1038
  rl.close();
715
1039
  if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
@@ -718,7 +1042,7 @@ async function deleteCommentAction(commentId, opts) {
718
1042
  return;
719
1043
  }
720
1044
  }
721
- const { deleteComment } = await import("./comments-M3EX3VOU.js");
1045
+ const { deleteComment } = await import("./comments-3UOERFNJ.js");
722
1046
  await deleteComment(commentId);
723
1047
  const result = { status: "deleted", comment_id: commentId };
724
1048
  if (opts.json) console.log(JSON.stringify(result));
@@ -750,6 +1074,45 @@ Example:
750
1074
  ).action(downloadTrajectoryAction);
751
1075
  var del = program.command("delete").description("Delete trajectories");
752
1076
  del.command("trajectory <url>").description("Delete a trajectory job from trajectories.sh").option("-y, --yes", "Skip confirmation prompt").action(deleteTrajectoryAction);
1077
+ var output = program.command("output").description("Attach agent output images/videos to trajectories");
1078
+ output.command("upload <target> <files...>").description("Upload output media (50 MiB per file; target is a job URL or UUID)").option("-t, --trial <trial>", "Attach to this trial UUID or name (or use ?trial= in the URL)").option("--json", "Print results as JSON, including successes if a later file fails").action(async (targetInput, files, opts) => {
1079
+ const uploaded = [];
1080
+ try {
1081
+ const { parseOutputTarget, validateOutputFile, uploadOutput } = await import("./outputs-TID2NORZ.js");
1082
+ const target = parseOutputTarget(targetInput, opts.trial);
1083
+ if (files.length > 24) throw new Error("Upload at most 24 files per trajectory");
1084
+ files.forEach(validateOutputFile);
1085
+ for (const file of files) {
1086
+ const result = await uploadOutput(target, file);
1087
+ uploaded.push(result);
1088
+ if (!opts.json) console.log(` \u2713 ${result.filename} attached (${result.id})`);
1089
+ }
1090
+ if (opts.json) console.log(JSON.stringify({ uploaded }));
1091
+ } catch (error) {
1092
+ if (opts.json) console.error(JSON.stringify({ uploaded, error: { message: error.message, status: error.status ?? null } }));
1093
+ else console.error(` \u2717 ${error.message}
1094
+ ${uploaded.length} file(s) attached. Check output list before retrying.`);
1095
+ process.exitCode = 1;
1096
+ }
1097
+ });
1098
+ output.command("list <target>").option("-t, --trial <trial>", "Trial UUID or name").option("--json", "Print machine-readable JSON").action(async (input, opts) => {
1099
+ try {
1100
+ const { listOutputs, parseOutputTarget } = await import("./outputs-TID2NORZ.js");
1101
+ const rows = await listOutputs(parseOutputTarget(input, opts.trial));
1102
+ console.log(opts.json ? JSON.stringify(rows) : rows.map((r) => `${r.id} ${r.filename} ${r.media_type}`).join("\n") || "No outputs attached.");
1103
+ } catch (error) {
1104
+ printCommentError(error, Boolean(opts.json));
1105
+ }
1106
+ });
1107
+ output.command("delete <target> <output-id>").description("Remove an output attachment from a trajectory in your organization").option("--json", "Print machine-readable JSON").action(async (input, id, opts) => {
1108
+ try {
1109
+ const { deleteOutput, parseOutputTarget } = await import("./outputs-TID2NORZ.js");
1110
+ const result = await deleteOutput(parseOutputTarget(input), id);
1111
+ console.log(opts.json ? JSON.stringify(result) : "Output deleted.");
1112
+ } catch (error) {
1113
+ printCommentError(error, Boolean(opts.json));
1114
+ }
1115
+ });
753
1116
  var comment = program.command("comment").description("Manage trajectory comments");
754
1117
  function addCommentTargetOptions(command) {
755
1118
  return command.option("-t, --trial <trial>", "Trial UUID or trial name").option("-s, --step <step>", "ATIF step ID (not an array index)").option("--phase <phase>", "Step phase: action or result").option("--json", "Print machine-readable JSON");
@@ -768,13 +1131,13 @@ addCommentBodyOptions(
768
1131
  ).action(updateCommentAction);
769
1132
  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);
770
1133
  async function resolveProject(ref) {
771
- const { listProjects, resolveProjectRef } = await import("./projects-END2CFCS.js");
1134
+ const { listProjects, resolveProjectRef } = await import("./projects-DDPO354C.js");
772
1135
  const projects = await listProjects();
773
1136
  return { ...resolveProjectRef(ref, projects), projects };
774
1137
  }
775
1138
  async function listProjectsAction(opts) {
776
1139
  try {
777
- const { listProjects } = await import("./projects-END2CFCS.js");
1140
+ const { listProjects } = await import("./projects-DDPO354C.js");
778
1141
  const projects = await listProjects();
779
1142
  if (opts.json) {
780
1143
  console.log(JSON.stringify(projects, null, 2));
@@ -804,7 +1167,7 @@ async function listProjectsAction(opts) {
804
1167
  }
805
1168
  async function showProjectAction(ref, opts) {
806
1169
  try {
807
- const { getProject } = await import("./projects-END2CFCS.js");
1170
+ const { getProject } = await import("./projects-DDPO354C.js");
808
1171
  const { id } = await resolveProject(ref);
809
1172
  const project2 = await getProject(id);
810
1173
  if (opts.json) {
@@ -833,7 +1196,7 @@ async function showProjectAction(ref, opts) {
833
1196
  }
834
1197
  async function createProjectAction(name, opts) {
835
1198
  try {
836
- const { createProject } = await import("./projects-END2CFCS.js");
1199
+ const { createProject } = await import("./projects-DDPO354C.js");
837
1200
  const project2 = await createProject({
838
1201
  name,
839
1202
  description: opts.description,
@@ -864,7 +1227,7 @@ async function updateProjectAction(ref, opts) {
864
1227
  if (Object.keys(patch).length === 0) {
865
1228
  throw new Error("Nothing to update. Pass --name, --description, --color, --slug or --visibility");
866
1229
  }
867
- const { updateProject } = await import("./projects-END2CFCS.js");
1230
+ const { updateProject } = await import("./projects-DDPO354C.js");
868
1231
  const { id } = await resolveProject(ref);
869
1232
  const project2 = await updateProject(id, patch);
870
1233
  if (opts.json) console.log(JSON.stringify(project2));
@@ -877,7 +1240,7 @@ async function updateProjectAction(ref, opts) {
877
1240
  }
878
1241
  async function deleteProjectAction(ref, opts) {
879
1242
  try {
880
- const { deleteProject } = await import("./projects-END2CFCS.js");
1243
+ const { deleteProject } = await import("./projects-DDPO354C.js");
881
1244
  const { id, project: project2 } = await resolveProject(ref);
882
1245
  const label = project2?.name ? `"${project2.name}"` : id;
883
1246
  if (!opts.yes) {
@@ -886,10 +1249,10 @@ async function deleteProjectAction(ref, opts) {
886
1249
  }
887
1250
  const readline = await import("readline");
888
1251
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
889
- const answer = await new Promise((resolve) => {
1252
+ const answer = await new Promise((resolve2) => {
890
1253
  rl.question(
891
1254
  ` Delete project ${label}? Its trajectory sets are ungrouped, not deleted. (y/N) `,
892
- resolve
1255
+ resolve2
893
1256
  );
894
1257
  });
895
1258
  rl.close();
@@ -908,7 +1271,7 @@ async function deleteProjectAction(ref, opts) {
908
1271
  }
909
1272
  async function projectAddAction(ref, sets, opts) {
910
1273
  try {
911
- const { assignJobs } = await import("./projects-END2CFCS.js");
1274
+ const { assignJobs } = await import("./projects-DDPO354C.js");
912
1275
  const { id, project: project2 } = await resolveProject(ref);
913
1276
  const result = await assignJobs(id, sets, "add");
914
1277
  if (opts.json) {
@@ -927,7 +1290,7 @@ async function projectAddAction(ref, sets, opts) {
927
1290
  }
928
1291
  async function projectRemoveAction(refOrSet, sets, opts) {
929
1292
  try {
930
- const { assignJobs } = await import("./projects-END2CFCS.js");
1293
+ const { assignJobs } = await import("./projects-DDPO354C.js");
931
1294
  if (opts.all) {
932
1295
  const result2 = await assignJobs(null, [refOrSet, ...sets], "remove");
933
1296
  if (opts.json) console.log(JSON.stringify(result2));
@@ -990,4 +1353,13 @@ if (process.argv.length <= 2) {
990
1353
  process.exit(1);
991
1354
  }
992
1355
  registerContracts(program);
1356
+ registerAssets(program);
1357
+ try {
1358
+ const { registerWatch } = await import("./register-A5MUHXPT.js");
1359
+ registerWatch(program);
1360
+ } catch (e) {
1361
+ if (process.env.TRAJECTORIES_DEBUG) {
1362
+ console.error(`watch command unavailable: ${e?.message ?? e}`);
1363
+ }
1364
+ }
993
1365
  await program.parseAsync();
@@ -0,0 +1,21 @@
1
+ import {
2
+ addComment,
3
+ deleteComment,
4
+ formatComment,
5
+ listComments,
6
+ parseCommentTarget,
7
+ readCommentBody,
8
+ updateComment
9
+ } from "./chunk-7FAZJWE4.js";
10
+ import "./chunk-MMQI3LNY.js";
11
+ import "./chunk-72L6XPK6.js";
12
+ import "./chunk-ERO5HP43.js";
13
+ export {
14
+ addComment,
15
+ deleteComment,
16
+ formatComment,
17
+ listComments,
18
+ parseCommentTarget,
19
+ readCommentBody,
20
+ updateComment
21
+ };
@@ -32,6 +32,8 @@ function validate(value) {
32
32
  if (!value || typeof value !== "object")
33
33
  throw new Error("Contract must be a JSON object.");
34
34
  const d = value;
35
+ if (d.kind !== void 0 && !["collaboration", "nda_only"].includes(String(d.kind)))
36
+ throw new Error("Invalid document kind.");
35
37
  for (const key of [
36
38
  "title",
37
39
  "recipient_name",
@@ -63,6 +65,8 @@ function validate(value) {
63
65
  if (typeof item[key] !== "string")
64
66
  throw new Error(`Deliverable ${key} must be a string.`);
65
67
  }
68
+ if (d.kind === "nda_only" && (!d.nda_required || d.amount_minor !== 0 || d.deliverables.length || d.payment_terms || d.submission_instructions || d.agreement_terms))
69
+ throw new Error("NDA-only drafts require an NDA and cannot contain paid scope or collaborator agreement terms.");
66
70
  }
67
71
  function paymentDetails(d) {
68
72
  const terms = d.payment_terms.trim();
@@ -83,8 +87,12 @@ function paymentDetails(d) {
83
87
  return terms;
84
88
  }
85
89
  function renderContractPreview(value) {
90
+ if (value && typeof value === "object" && "kind" in value && value.kind === "nda_only") {
91
+ value = { recipient_name: "", recipient_email: "", currency: "USD", amount_minor: 0, pricing: "fixed", payment_terms: "", submission_instructions: "", agreement_terms: "", deliverables: [], nda_required: true, nda_terms: "", terms_approved: false, ...value };
92
+ }
86
93
  validate(value);
87
94
  const d = value;
95
+ const ndaOnly = d.kind === "nda_only";
88
96
  const amount = new Intl.NumberFormat("en-US", {
89
97
  style: "currency",
90
98
  currency: d.currency
@@ -92,6 +100,7 @@ function renderContractPreview(value) {
92
100
  const basis = d.pricing === "fixed" ? "Fixed total" : `Per ${d.pricing_unit || "accepted item"}`;
93
101
  const pending = [
94
102
  ...d.nda_required && !d.nda_terms.trim() ? ["Approved NDA terms"] : [],
103
+ ...ndaOnly && !d.terms_approved ? ["Explicit NDA terms approval"] : [],
95
104
  ...d.deliverables.some((x) => !x.acceptance.trim()) ? ["Task success / acceptance criteria"] : []
96
105
  ];
97
106
  const paragraph = (text, fallback = "Not specified") => `<p class="prose">${escape(text.trim() || fallback)}</p>`;
@@ -110,12 +119,12 @@ input{display:block;width:100%;padding:10px;margin:8px 0;border:1px solid #193c3
110
119
  .single-deliverable{list-style:none;padding-left:0}.single-deliverable>li{padding-left:0}.acceptance-details{margin-top:8px}.acceptance-list{list-style:disc outside;padding-left:1.4rem;margin:8px 0 0}.acceptance-list>li{padding-left:.2rem;margin:8px 0}
111
120
  ${contractFontCss}
112
121
  body{font-family:Geist,Arial,sans-serif;color:#0b0b0b}main{max-width:960px}h1,h2,.amount{font-family:'Instrument Serif',Georgia,serif}.brand,.cover-wordmark{font-family:'Instrument Serif',Georgia,serif}h1,h2{font-weight:400;letter-spacing:-.005em}.cover-card h1{font-size:52px}.cover-photo{height:clamp(380px,56vh,560px)}.cover-card{margin-top:-144px}.contract-cover{margin-bottom:48px}.grid{margin:0 32px;grid-template-columns:minmax(0,1fr) 240px;gap:40px}.contract-section{padding:0;margin:0 0 48px;scroll-margin-top:24px}.contract-section h2{font-size:34px;line-height:1.25;margin:0 0 20px}.contract-section .prose{font-size:14px;line-height:1.8}.contract-summary{border-left:1px solid #0b0b0b1a;padding-left:24px}.muted{color:#6b6a66}@media(max-width:760px){.grid{margin:0 12px;grid-template-columns:1fr;gap:36px}.contract-summary{border-left:0;border-bottom:1px solid #0b0b0b1a;padding:0 0 28px}.contract-section h2{font-size:30px}.cover-card h1{font-size:38px}.cover-photo{height:300px}.cover-card{margin-top:-90px}}@media print{.grid{display:block}.contract-summary{border-left:0;padding-left:0}.contract-section{break-inside:avoid}}
113
- </style></head><body><main><nav><span class="brand"><img src="${logoImage}" alt="" width="25" height="25">Refresh</span><span class="muted">Contractor draft</span><button id="save-pdf">Save as PDF</button></nav><div class="banner">Local draft preview \xB7 Not issued or sent \xB7 Signing disabled</div><header class="contract-cover"><div class="cover-photo"><div class="cover-lockup"><img src="${logoImage}" alt="" width="44" height="44"><span><span class="cover-wordmark">Refresh</span><span class="cover-tagline">Environments &amp; Data</span></span></div></div><div class="cover-card"><span class="badge">Draft</span><h1>${escape(d.title)}</h1><p class="cover-meta">Prepared for ${escape(d.recipient_name)}<span class="muted">${escape(d.recipient_email)}</span></p></div></header><div class="grid"><div>
114
- <section class="contract-section"><h2>What you will deliver</h2><ol class="deliverables-list${d.deliverables.length === 1 ? " single-deliverable" : ""}">${d.deliverables.map((item) => `<li><h3>${escape(item.title)}</h3><ul class="deliverable-details"><li class="prose">${escape(item.description)}</li></ul><div class="acceptance-details"><strong>Acceptance:</strong> ${acceptanceItems(item.acceptance).length > 1 ? `<ul class="acceptance-list">${acceptanceItems(item.acceptance).map((text) => `<li class="prose">${escape(text)}</li>`).join("")}</ul>` : escape(item.acceptance || "To be agreed")}</div>${item.due.trim() ? `<p class="muted">Due: ${escape(item.due)}</p>` : ""}</li>`).join("")}</ol></section>
122
+ </style></head><body><main><nav><span class="brand"><img src="${logoImage}" alt="" width="25" height="25">Refresh</span><span class="muted">${ndaOnly ? "NDA draft" : "Contractor draft"}</span><button id="save-pdf">Save as PDF</button></nav><div class="banner">Local draft preview \xB7 Not issued or sent \xB7 Signing disabled</div><header class="contract-cover"><div class="cover-photo"><div class="cover-lockup"><img src="${logoImage}" alt="" width="44" height="44"><span><span class="cover-wordmark">Refresh</span><span class="cover-tagline">Environments &amp; Data</span></span></div></div><div class="cover-card"><span class="badge">Draft</span><h1>${escape(d.title)}</h1><p class="cover-meta">Prepared for ${escape(d.recipient_name)}<span class="muted">${escape(d.recipient_email)}</span></p></div></header><div class="grid"><div>
123
+ ${!ndaOnly ? `<section class="contract-section"><h2>What you will deliver</h2><ol class="deliverables-list${d.deliverables.length === 1 ? " single-deliverable" : ""}">${d.deliverables.map((item) => `<li><h3>${escape(item.title)}</h3><ul class="deliverable-details"><li class="prose">${escape(item.description)}</li></ul><div class="acceptance-details"><strong>Acceptance:</strong> ${acceptanceItems(item.acceptance).length > 1 ? `<ul class="acceptance-list">${acceptanceItems(item.acceptance).map((text) => `<li class="prose">${escape(text)}</li>`).join("")}</ul>` : escape(item.acceptance || "To be agreed")}</div>${item.due.trim() ? `<p class="muted">Due: ${escape(item.due)}</p>` : ""}</li>`).join("")}</ol></section>
115
124
  <section class="contract-section"><h2>How to submit</h2>${paragraph(d.submission_instructions)}</section>
116
- <section class="contract-section" id="agreement"><h2>Collaborator Agreement</h2>${d.agreement_terms.trim() ? paragraph(d.agreement_terms) : ""}<p class="muted">By clicking Sign, I agree to this document and intend my typed name to be my signature.</p><label>Full name<input disabled placeholder="Type your full name" aria-label="Full name"></label><button disabled>Sign</button></section>
125
+ <section class="contract-section" id="agreement"><h2>Collaborator Agreement</h2>${d.agreement_terms.trim() ? paragraph(d.agreement_terms) : ""}<p class="muted">By clicking Sign, I agree to this document and intend my typed name to be my signature.</p><label>Full name<input disabled placeholder="Type your full name" aria-label="Full name"></label><button disabled>Sign</button></section>` : ""}
117
126
  ${d.nda_required ? `<section class="contract-section" id="nda"><h2>Non-disclosure agreement</h2>${d.nda_terms.trim() ? paragraph(d.nda_terms) : ""}<p class="muted">By clicking Sign, I agree to this document and intend my typed name to be my signature.</p><label>Full name<input disabled placeholder="Type your full name" aria-label="Full name"></label><button disabled>Sign</button></section>` : ""}
118
- </div><aside class="contract-summary summary"><span class="muted">Compensation</span><p class="amount">${escape(amount)}</p><p class="muted">${escape(basis)}</p>${paymentDetails(d) ? paragraph(paymentDetails(d)) : ""}<hr><a href="#agreement">Agreement</a><p class="muted">Not issued</p>${d.nda_required ? '<a href="#nda">NDA</a><p class="muted">Required \xB7 Not issued</p>' : ""}</aside></div></main><script>${printScript}</script></body></html>`
127
+ </div><aside class="contract-summary summary">${!ndaOnly ? `<span class="muted">Compensation</span><p class="amount">${escape(amount)}</p><p class="muted">${escape(basis)}</p>${paymentDetails(d) ? paragraph(paymentDetails(d)) : ""}<hr><a href="#agreement">Agreement</a><p class="muted">Not issued</p>` : ""}${d.nda_required ? '<a href="#nda">NDA</a><p class="muted">Required \xB7 Not issued</p>' : ""}</aside></div></main><script>${printScript}</script></body></html>`
119
128
  };
120
129
  }
121
130
  async function previewContractFile(file, output) {
@@ -9,8 +9,9 @@ import {
9
9
  parseJobId,
10
10
  parseProjectId,
11
11
  planJob,
12
+ retryDelayMs,
12
13
  writeJsonl
13
- } from "./chunk-LOJZPDGI.js";
14
+ } from "./chunk-MMQI3LNY.js";
14
15
  import "./chunk-72L6XPK6.js";
15
16
  import "./chunk-ERO5HP43.js";
16
17
  export {
@@ -24,5 +25,6 @@ export {
24
25
  parseJobId,
25
26
  parseProjectId,
26
27
  planJob,
28
+ retryDelayMs,
27
29
  writeJsonl
28
30
  };
@@ -7,7 +7,7 @@ import {
7
7
  parseProjectId,
8
8
  planJob,
9
9
  writeJsonl
10
- } from "./chunk-LOJZPDGI.js";
10
+ } from "./chunk-MMQI3LNY.js";
11
11
  import "./chunk-72L6XPK6.js";
12
12
  import {
13
13
  getApiUrl,
@@ -0,0 +1,86 @@
1
+ import {
2
+ parseCommentTarget
3
+ } from "./chunk-7FAZJWE4.js";
4
+ import "./chunk-MMQI3LNY.js";
5
+ import "./chunk-72L6XPK6.js";
6
+ import {
7
+ getApiUrl,
8
+ getAuthHeader
9
+ } from "./chunk-ERO5HP43.js";
10
+
11
+ // src/outputs.ts
12
+ import { readFileSync, statSync } from "fs";
13
+ import { basename } from "path";
14
+ var MAX_OUTPUT_BYTES = 50 * 1024 * 1024;
15
+ function parseOutputTarget(input, trial) {
16
+ let target = input;
17
+ try {
18
+ const url = new URL(input);
19
+ url.searchParams.delete("step");
20
+ url.searchParams.delete("phase");
21
+ target = url.toString();
22
+ } catch {
23
+ }
24
+ const { jobId, trialId, trialName } = parseCommentTarget(target, { trial });
25
+ return { jobId, trialId, trialName };
26
+ }
27
+ function pathFor(target) {
28
+ return `/api/org/trajectories/${encodeURIComponent(target.jobId)}/outputs`;
29
+ }
30
+ function scope(target) {
31
+ const params = new URLSearchParams();
32
+ if (target.trialId) params.set("trial_id", target.trialId);
33
+ if (target.trialName) params.set("trial_name", target.trialName);
34
+ return params;
35
+ }
36
+ async function request(path, init = {}, requireAuth = false) {
37
+ const auth = await getAuthHeader();
38
+ if (requireAuth && !auth) throw new Error("Not authenticated. Run: trajectories auth login");
39
+ const response = await fetch(`${getApiUrl()}${path}`, {
40
+ ...init,
41
+ headers: { ...init.headers, ...auth ? { Authorization: auth } : {} }
42
+ });
43
+ if (!response.ok) {
44
+ let detail = `Request failed (${response.status})`;
45
+ try {
46
+ const body = await response.json();
47
+ if (typeof body.detail === "string") detail = body.detail;
48
+ } catch {
49
+ }
50
+ throw Object.assign(new Error(detail), { status: response.status });
51
+ }
52
+ return response.json();
53
+ }
54
+ function validateOutputFile(file) {
55
+ const stat = statSync(file);
56
+ if (!stat.isFile() || stat.size === 0 || stat.size > MAX_OUTPUT_BYTES) {
57
+ throw new Error(`${file}: output must be a nonempty file of at most 50 MiB`);
58
+ }
59
+ if (!/\.(png|jpe?g|webp|gif|mp4|webm)$/i.test(file)) {
60
+ throw new Error(`${file}: use PNG, JPEG, WebP, GIF, MP4, or WebM`);
61
+ }
62
+ }
63
+ async function uploadOutput(target, file) {
64
+ validateOutputFile(file);
65
+ const params = scope(target);
66
+ params.set("filename", basename(file));
67
+ return request(`${pathFor(target)}?${params}`, {
68
+ method: "POST",
69
+ headers: { "Content-Type": "application/octet-stream" },
70
+ body: new Uint8Array(readFileSync(file))
71
+ }, true);
72
+ }
73
+ function listOutputs(target) {
74
+ return request(`${pathFor(target)}?${scope(target)}`);
75
+ }
76
+ function deleteOutput(target, id) {
77
+ return request(`${pathFor(target)}/${encodeURIComponent(id)}`, { method: "DELETE" }, true);
78
+ }
79
+ export {
80
+ MAX_OUTPUT_BYTES,
81
+ deleteOutput,
82
+ listOutputs,
83
+ parseOutputTarget,
84
+ uploadOutput,
85
+ validateOutputFile
86
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  parseJobId
3
- } from "./chunk-LOJZPDGI.js";
3
+ } from "./chunk-MMQI3LNY.js";
4
4
  import "./chunk-72L6XPK6.js";
5
5
  import {
6
6
  getApiUrl,
@@ -0,0 +1,46 @@
1
+ // ../watch/src/register.mjs
2
+ import { spawn } from "child_process";
3
+ import { existsSync } from "fs";
4
+ import { dirname, join } from "path";
5
+ import { fileURLToPath } from "url";
6
+ var here = dirname(fileURLToPath(import.meta.url));
7
+ var CANDIDATES = [
8
+ join(here, "server.mjs"),
9
+ join(here, "..", "..", "watch", "src", "server.mjs"),
10
+ join(here, "..", "watch", "src", "server.mjs")
11
+ ];
12
+ function serverPath() {
13
+ const found = CANDIDATES.find((path) => existsSync(path));
14
+ if (found) return found;
15
+ throw new Error("trajectories watch: could not find server.mjs. Looked in:\n " + CANDIDATES.join("\n "));
16
+ }
17
+ function runWatch(args = [], { stdio = "inherit" } = {}) {
18
+ const child = spawn(process.execPath, [serverPath(), ...args], { stdio });
19
+ return new Promise((done) => {
20
+ child.on("exit", (code) => done(code ?? 0));
21
+ });
22
+ }
23
+ function registerWatch(program) {
24
+ program.command("watch [job]").description("Watch a Harbor trial locally while it runs, or replay a finished one").option("--root <dir>", "Directory holding the jobs directory (default: the current directory)").option("--jobs <dir>", "The jobs directory itself (default: harbor/jobs, then jobs)").option("--port <n>", "Local port (default: 7654)").option("--stale <n>", "Seconds without a trajectory write before a run counts as died (default: 180)").option("--env-file <path>", "Read DAYTONA_API_KEY out of this env-style file").option("--setup", "Ask the first-run questions again, then carry on").option("--no-color", "Never colour the output (NO_COLOR does the same)").option("--url", "Print the live screen address and exit; non-zero when there is none").action(async (job, opts) => {
25
+ const args = [];
26
+ if (job) args.push(job);
27
+ for (const [name, flag] of [
28
+ ["root", "--root"],
29
+ ["jobs", "--jobs"],
30
+ ["port", "--port"],
31
+ ["stale", "--stale"],
32
+ ["envFile", "--env-file"]
33
+ ]) {
34
+ if (opts[name] !== void 0) args.push(flag, String(opts[name]));
35
+ }
36
+ if (opts.setup) args.push("--setup");
37
+ if (opts.color === false) args.push("--no-color");
38
+ if (opts.url) args.push("--url");
39
+ process.exitCode = await runWatch(args);
40
+ });
41
+ return program;
42
+ }
43
+ export {
44
+ registerWatch,
45
+ runWatch
46
+ };
@@ -9,9 +9,27 @@ import {
9
9
  getAuthHeader
10
10
  } from "./chunk-ERO5HP43.js";
11
11
 
12
+ // src/converted-image-paths.ts
13
+ import { dirname, resolve } from "path";
14
+ function rewriteConvertedImagePaths(value, trajectoryPath, converted) {
15
+ if (!value || typeof value !== "object") return;
16
+ if (Array.isArray(value)) {
17
+ for (const item of value) rewriteConvertedImagePaths(item, trajectoryPath, converted);
18
+ return;
19
+ }
20
+ const object = value;
21
+ if (object.type === "image" && object.source && typeof object.source === "object") {
22
+ const source = object.source;
23
+ if (typeof source.path === "string" && converted.has(resolve(dirname(trajectoryPath), source.path))) {
24
+ source.path = source.path.replace(/\.png$/i, ".webp");
25
+ }
26
+ }
27
+ for (const item of Object.values(object)) rewriteConvertedImagePaths(item, trajectoryPath, converted);
28
+ }
29
+
12
30
  // src/upload.ts
13
31
  import { readdirSync, statSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, unlinkSync } from "fs";
14
- import { join, relative, dirname } from "path";
32
+ import { join, relative } from "path";
15
33
  import { tmpdir } from "os";
16
34
  import { randomBytes, createHash } from "crypto";
17
35
  var SKIP = /* @__PURE__ */ new Set(["__pycache__", ".DS_Store", ".git", "node_modules"]);
@@ -78,7 +96,7 @@ async function uploadZip(zipPath, opts) {
78
96
  console.log(" Uploading ZIP via resumable upload...");
79
97
  const { createReadStream } = await import("fs");
80
98
  const tus = await import("tus-js-client");
81
- await new Promise((resolve, reject) => {
99
+ await new Promise((resolve2, reject) => {
82
100
  const fileStream = createReadStream(zipPath);
83
101
  const upload = new tus.Upload(fileStream, {
84
102
  endpoint: `${uploadInfo.supabase_url}/storage/v1/upload/resumable`,
@@ -107,7 +125,7 @@ async function uploadZip(zipPath, opts) {
107
125
  },
108
126
  onSuccess() {
109
127
  console.log(` \u2191 100% \u2014 ZIP uploaded to storage.`);
110
- resolve();
128
+ resolve2();
111
129
  }
112
130
  });
113
131
  upload.start();
@@ -204,6 +222,7 @@ async function convertToWebP(dir) {
204
222
  const prevConcurrency = sharp.concurrency();
205
223
  sharp.concurrency(1);
206
224
  sharp.cache({ memory: 0, files: 0, items: 0 });
225
+ const convertedPaths = /* @__PURE__ */ new Map();
207
226
  let converted = 0;
208
227
  let savedBytes = 0;
209
228
  let processed = 0;
@@ -216,6 +235,7 @@ async function convertToWebP(dir) {
216
235
  const origSize = statSync(srcPath).size;
217
236
  const newSize = statSync(webpPath).size;
218
237
  if (newSize < origSize) {
238
+ convertedPaths.set(srcPath, webpPath);
219
239
  unlinkSync(srcPath);
220
240
  savedBytes += origSize - newSize;
221
241
  converted++;
@@ -241,20 +261,11 @@ async function convertToWebP(dir) {
241
261
  const trajFiles = collectFiles(tmpDir).files.filter((f) => f.relPath.endsWith("trajectory.json"));
242
262
  for (const tf of trajFiles) {
243
263
  try {
244
- let content = readFileSync(tf.absPath, "utf-8");
245
- const original = content;
246
- content = content.replace(/screenshots\/([^"]+)\.png/g, (match, name) => {
247
- const webpPath = join(dirname(tf.absPath), "screenshots", `${name}.webp`);
248
- try {
249
- statSync(webpPath);
250
- return `screenshots/${name}.webp`;
251
- } catch {
252
- return match;
253
- }
254
- });
255
- if (content !== original) {
256
- writeFileSync(tf.absPath, content);
257
- }
264
+ const original = readFileSync(tf.absPath, "utf-8");
265
+ const trajectory = JSON.parse(original);
266
+ const before = JSON.stringify(trajectory);
267
+ rewriteConvertedImagePaths(trajectory, tf.absPath, convertedPaths);
268
+ if (JSON.stringify(trajectory) !== before) writeFileSync(tf.absPath, JSON.stringify(trajectory));
258
269
  } catch {
259
270
  }
260
271
  }
@@ -673,6 +684,7 @@ function truncate(s, n) {
673
684
  }
674
685
  export {
675
686
  collectFiles,
687
+ convertToWebP,
676
688
  uploadJob,
677
689
  uploadZip
678
690
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trajectories-sh",
3
- "version": "1.24.1",
3
+ "version": "1.25.1",
4
4
  "description": "CLI for uploading trajectory jobs to trajectories.sh",
5
5
  "type": "module",
6
6
  "bin": {