@convilyn/sdk 0.3.0 → 0.4.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.
@@ -111,14 +111,22 @@ var GoalJobTimeoutError = class extends ConvilynError {
111
111
  jobSpecId;
112
112
  elapsed;
113
113
  timeout;
114
+ /**
115
+ * `'total'` — the overall `timeout` budget lapsed; `'idle'` —
116
+ * `idleTimeout` lapsed with no status/progress change (the job may still
117
+ * be healthy on a long phase; `retrieve()` before assuming failure).
118
+ */
119
+ reason;
114
120
  constructor(opts) {
121
+ const reason = opts.reason ?? "total";
115
122
  super(
116
- `GoalJob ${opts.jobSpecId} did not reach a terminal status within ${opts.timeout}s (elapsed ${opts.elapsed.toFixed(1)}s)`
123
+ reason === "idle" ? `GoalJob ${opts.jobSpecId} showed no status/progress change for ${opts.timeout}s (elapsed ${opts.elapsed.toFixed(1)}s total)` : `GoalJob ${opts.jobSpecId} did not reach a terminal status within ${opts.timeout}s (elapsed ${opts.elapsed.toFixed(1)}s)`
117
124
  );
118
125
  this.name = "GoalJobTimeoutError";
119
126
  this.jobSpecId = opts.jobSpecId;
120
127
  this.elapsed = opts.elapsed;
121
128
  this.timeout = opts.timeout;
129
+ this.reason = reason;
122
130
  }
123
131
  };
124
132
  var WebSocketError = class extends ConvilynError {
@@ -330,7 +338,7 @@ function emitSoftLimitWarning(opts) {
330
338
  }
331
339
 
332
340
  // src/version.ts
333
- var VERSION = "0.3.0";
341
+ var VERSION = "0.4.0";
334
342
 
335
343
  // src/internal/auth.ts
336
344
  var ENV_API_KEY = "CONVILYN_API_KEY";
@@ -761,6 +769,29 @@ function parseGoalJob(wire) {
761
769
  completedAt: toDateOrNull(wire.completedAt)
762
770
  };
763
771
  }
772
+ function parseArtifact(wire) {
773
+ return {
774
+ artifactId: String(wire.artifactId),
775
+ fileName: String(wire.fileName),
776
+ mimeType: String(wire.mimeType),
777
+ sizeBytes: Number(wire.sizeBytes ?? 0),
778
+ downloadUrl: wire.downloadUrl ?? null,
779
+ artifactType: wire.artifactType ?? null,
780
+ platform: wire.platform ?? null,
781
+ metadata: isRecord2(wire.metadata) ? wire.metadata : null,
782
+ isPrimary: Boolean(wire.isPrimary ?? false),
783
+ description: String(wire.description ?? "")
784
+ };
785
+ }
786
+ function parseArtifactDownload(wire) {
787
+ return {
788
+ downloadUrl: String(wire.downloadUrl),
789
+ fileName: String(wire.fileName),
790
+ sizeBytes: Number(wire.sizeBytes ?? 0),
791
+ mimeType: String(wire.mimeType),
792
+ expiresAt: toDate(wire.expiresAt)
793
+ };
794
+ }
764
795
  function parseGoalEvent(wire) {
765
796
  return {
766
797
  type: wire.type,
@@ -813,6 +844,27 @@ function parseWorkflowSearchPage(wire) {
813
844
  cursor: wire.cursor ?? null
814
845
  };
815
846
  }
847
+ function parseCatalogWorkflow(wire) {
848
+ return {
849
+ workflowId: String(wire.workflowId),
850
+ name: String(wire.name),
851
+ description: String(wire.description ?? ""),
852
+ icon: wire.icon ?? null,
853
+ supportedInputTypes: Array.isArray(wire.supportedInputTypes) ? wire.supportedInputTypes.map(String) : [],
854
+ supportedInputFormats: Array.isArray(wire.supportedInputFormats) ? wire.supportedInputFormats.map(String) : null,
855
+ category: String(wire.category ?? "goal_lane"),
856
+ requiredSlotCount: Number(wire.requiredSlotCount ?? 0),
857
+ subcategory: wire.subcategory ?? null,
858
+ skuGroup: wire.skuGroup ?? null,
859
+ status: String(wire.status ?? "active"),
860
+ supportedLocales: Array.isArray(wire.supportedLocales) ? wire.supportedLocales.map(String) : null,
861
+ maxInputSizeBytes: wire.maxInputSizeBytes ?? null,
862
+ minFileCount: wire.minFileCount ?? null,
863
+ tier: wire.tier ?? null,
864
+ // Tri-state: only an explicit false blocks a Free-tier run; absent stays null.
865
+ freeTierAllowed: wire.freeTierAllowed ?? null
866
+ };
867
+ }
816
868
  function parseLikeResponse(wire) {
817
869
  return {
818
870
  liked: Boolean(wire.liked),
@@ -898,6 +950,52 @@ var Account = class {
898
950
  }
899
951
  };
900
952
 
953
+ // src/internal/download.ts
954
+ var MAX_DOWNLOAD_BYTES = 2 * 1024 ** 3;
955
+ async function downloadUrlToPath(http, url, to, maxBytes = MAX_DOWNLOAD_BYTES) {
956
+ const { lstat, open, unlink } = await import('fs/promises');
957
+ const st = await lstat(to).catch(() => null);
958
+ if (st?.isSymbolicLink()) {
959
+ throw new Error(
960
+ `Refusing to write to ${JSON.stringify(to)}: target is a symlink. Remove it or choose a non-symlink destination.`
961
+ );
962
+ }
963
+ const response = await http.externalGet(url);
964
+ const body = response.body;
965
+ const handle = await open(to, "w");
966
+ let written = 0;
967
+ try {
968
+ if (body == null) {
969
+ const bytes = new Uint8Array(await response.arrayBuffer());
970
+ written = bytes.byteLength;
971
+ if (written > maxBytes) {
972
+ throw new Error(`Download exceeds the ${maxBytes}-byte cap`);
973
+ }
974
+ await handle.write(bytes);
975
+ } else {
976
+ const reader = body.getReader();
977
+ for (; ; ) {
978
+ const { done, value } = await reader.read();
979
+ if (done) {
980
+ break;
981
+ }
982
+ written += value.byteLength;
983
+ if (written > maxBytes) {
984
+ await reader.cancel().catch(() => void 0);
985
+ throw new Error(`Download exceeds the ${maxBytes}-byte cap`);
986
+ }
987
+ await handle.write(value);
988
+ }
989
+ }
990
+ } catch (error) {
991
+ await handle.close().catch(() => void 0);
992
+ await unlink(to).catch(() => void 0);
993
+ throw error;
994
+ }
995
+ await handle.close();
996
+ return to;
997
+ }
998
+
901
999
  // src/resources/convert.ts
902
1000
  var DEFAULT_POLL_INTERVAL = 1;
903
1001
  var DEFAULT_POLL_TIMEOUT = 300;
@@ -993,17 +1091,7 @@ var Convert = class {
993
1091
  /** Download the first result file to `to` (Node) and return that path. */
994
1092
  async downloadTo(job, { to }) {
995
1093
  const url = await this.downloadUrl(job);
996
- const { lstat, writeFile } = await import('fs/promises');
997
- const st = await lstat(to).catch(() => null);
998
- if (st?.isSymbolicLink()) {
999
- throw new Error(
1000
- `Refusing to write to ${JSON.stringify(to)}: target is a symlink. Remove it or choose a non-symlink destination.`
1001
- );
1002
- }
1003
- const response = await this.#http.externalGet(url);
1004
- const bytes = new Uint8Array(await response.arrayBuffer());
1005
- await writeFile(to, bytes);
1006
- return to;
1094
+ return downloadUrlToPath(this.#http, url, to);
1007
1095
  }
1008
1096
  // ── private ────────────────────────────────────────────────────
1009
1097
  #resolveSource(options) {
@@ -1158,6 +1246,19 @@ var Files = class {
1158
1246
  const contentType = options.contentType ?? guessContentType(filename);
1159
1247
  return this.#uploadWithBody(bytes, filename, contentType);
1160
1248
  }
1249
+ /**
1250
+ * Delete an uploaded file's cloud copy (storage object + metadata record).
1251
+ *
1252
+ * Only the uploader can delete; a missing or unowned `fileId` surfaces as
1253
+ * a 404 `APIError` and a file attached to a still-running job as a 409
1254
+ * (`FILE_IN_USE`). The platform deletes input files automatically ~1 hour
1255
+ * after upload anyway — call this when you want the cloud copy gone
1256
+ * deterministically the moment your workflow is done (e.g. privacy-
1257
+ * sensitive documents on an edge device). Port of Python `files.delete`.
1258
+ */
1259
+ async delete(fileId) {
1260
+ await this.#http.request("DELETE", `/api/v1/files/${fileId}`);
1261
+ }
1161
1262
  async #uploadWithBody(body, filename, contentType) {
1162
1263
  const size = body.byteLength;
1163
1264
  const presign = await this.#presign(filename, size, contentType);
@@ -1358,7 +1459,8 @@ var Goals = class {
1358
1459
  return this.#waitLoop(
1359
1460
  jobSpecId,
1360
1461
  options.timeout ?? DEFAULT_POLL_TIMEOUT2,
1361
- options.pollInterval ?? DEFAULT_POLL_INTERVAL2
1462
+ options.pollInterval ?? DEFAULT_POLL_INTERVAL2,
1463
+ options.idleTimeout ?? null
1362
1464
  );
1363
1465
  }
1364
1466
  /** Shortcut: {@link start} then {@link wait}. */
@@ -1366,7 +1468,8 @@ var Goals = class {
1366
1468
  const job = await this.start(options);
1367
1469
  return this.wait(job.jobSpecId, {
1368
1470
  timeout: options.timeout,
1369
- pollInterval: options.pollInterval
1471
+ pollInterval: options.pollInterval,
1472
+ idleTimeout: options.idleTimeout
1370
1473
  });
1371
1474
  }
1372
1475
  /** Answer a single slot the agent is waiting on (sugar over {@link fillSlots}). */
@@ -1396,9 +1499,17 @@ var Goals = class {
1396
1499
  });
1397
1500
  return parseGoalJob(await response.json());
1398
1501
  }
1399
- /** Confirm a slots-filled job, queueing it for execution. */
1502
+ /**
1503
+ * Confirm a slots-filled job, queueing it for execution.
1504
+ *
1505
+ * `expectedVersion` is optional — the server re-reads the current version
1506
+ * at submit time regardless, so most callers should simply omit it. The
1507
+ * request always carries a JSON object (`{}` when empty): older backend
1508
+ * builds declare the confirm body as a required parameter and reject a
1509
+ * body-less POST with 422 before the handler runs.
1510
+ */
1400
1511
  async confirm(jobSpecId, options = {}) {
1401
- const json = options.expectedVersion != null ? { expectedVersion: options.expectedVersion } : void 0;
1512
+ const json = options.expectedVersion != null ? { expectedVersion: options.expectedVersion } : {};
1402
1513
  await this.#http.request("POST", `/api/v1/jobs/goal/${jobSpecId}/confirm`, { json });
1403
1514
  return this.#pollOnce(jobSpecId);
1404
1515
  }
@@ -1416,6 +1527,41 @@ var Goals = class {
1416
1527
  await this.#http.request("POST", `/api/v1/jobs/goal/${jobSpecId}/retry`, { json: body });
1417
1528
  return this.#pollOnce(jobSpecId);
1418
1529
  }
1530
+ /**
1531
+ * List a completed job's output artifacts with fresh download URLs.
1532
+ *
1533
+ * Available once the job is terminal-successful (`completed` or `partial`);
1534
+ * any other status surfaces the backend's 400 as an `APIError`. Each
1535
+ * artifact's `downloadUrl` is presigned and valid for one hour — re-call
1536
+ * this method for fresh URLs rather than persisting one.
1537
+ */
1538
+ async artifacts(jobSpecId) {
1539
+ const response = await this.#http.request("GET", `/api/v1/jobs/goal/${jobSpecId}/artifacts`);
1540
+ const payload = await response.json();
1541
+ const rows = Array.isArray(payload.artifacts) ? payload.artifacts : [];
1542
+ return rows.map((row) => parseArtifact(row));
1543
+ }
1544
+ /**
1545
+ * Mint a fresh presigned download URL for one artifact. Prefer this over
1546
+ * reusing a stale {@link Artifact.downloadUrl} when more than an hour may
1547
+ * have passed since {@link artifacts}.
1548
+ */
1549
+ async downloadArtifactUrl(jobSpecId, artifactId) {
1550
+ const response = await this.#http.request(
1551
+ "GET",
1552
+ `/api/v1/jobs/goal/${jobSpecId}/artifacts/${artifactId}/download`
1553
+ );
1554
+ return parseArtifactDownload(await response.json());
1555
+ }
1556
+ /**
1557
+ * Download one artifact to `to` (Node) and return that path. Mints a fresh
1558
+ * presigned URL first, then streams the body to disk (shares the size-cap
1559
+ * and symlink-refusal contract with `convert.downloadTo`).
1560
+ */
1561
+ async downloadArtifactTo(jobSpecId, artifactId, { to }) {
1562
+ const info = await this.downloadArtifactUrl(jobSpecId, artifactId);
1563
+ return downloadUrlToPath(this.#http, info.downloadUrl, to);
1564
+ }
1419
1565
  /**
1420
1566
  * Stream goal execution events for a job. Opens a WebSocket, subscribes,
1421
1567
  * then yields each {@link GoalEvent} in order; the iterator ends (and the
@@ -1470,11 +1616,13 @@ var Goals = class {
1470
1616
  const response = await this.#http.request("GET", `/api/v1/jobs/goal/${jobSpecId}`);
1471
1617
  return parseGoalJob(await response.json());
1472
1618
  }
1473
- async #waitLoop(jobSpecId, timeout, initialInterval) {
1619
+ async #waitLoop(jobSpecId, timeout, initialInterval, idleTimeout) {
1474
1620
  const start = this.#now() / 1e3;
1475
1621
  let interval = initialInterval;
1476
1622
  let staleCount = 0;
1477
1623
  let lastProgress = -1;
1624
+ let lastStatus = null;
1625
+ let lastChange = start;
1478
1626
  for (; ; ) {
1479
1627
  const job = await this.#pollOnce(jobSpecId);
1480
1628
  if (isGoalJobTerminal(job)) {
@@ -1483,6 +1631,10 @@ var Goals = class {
1483
1631
  if (goalJobNeedsInput(job)) {
1484
1632
  return job;
1485
1633
  }
1634
+ if (job.status !== lastStatus) {
1635
+ lastStatus = job.status;
1636
+ lastChange = this.#now() / 1e3;
1637
+ }
1486
1638
  if (job.progress === lastProgress) {
1487
1639
  staleCount += 1;
1488
1640
  if (staleCount >= STALE_PROGRESS_BACKOFF_AFTER2) {
@@ -1491,12 +1643,17 @@ var Goals = class {
1491
1643
  }
1492
1644
  } else {
1493
1645
  lastProgress = job.progress;
1646
+ lastChange = this.#now() / 1e3;
1494
1647
  staleCount = 0;
1495
1648
  }
1496
- const elapsed = this.#now() / 1e3 - start;
1649
+ const now = this.#now() / 1e3;
1650
+ const elapsed = now - start;
1497
1651
  if (elapsed + interval > timeout) {
1498
1652
  throw new GoalJobTimeoutError({ jobSpecId, elapsed, timeout });
1499
1653
  }
1654
+ if (idleTimeout != null && now - lastChange + interval > idleTimeout) {
1655
+ throw new GoalJobTimeoutError({ jobSpecId, elapsed, timeout: idleTimeout, reason: "idle" });
1656
+ }
1500
1657
  await this.#sleep(interval);
1501
1658
  }
1502
1659
  }
@@ -1544,6 +1701,20 @@ var Workflows = class {
1544
1701
  constructor(http) {
1545
1702
  this.#http = http;
1546
1703
  }
1704
+ /**
1705
+ * List the platform's built-in AI-workflow catalog.
1706
+ *
1707
+ * Distinct from {@link search}, which browses the user-published community
1708
+ * marketplace — the catalog is the curated set of built-in workflows
1709
+ * runnable directly via `goals.start({ workflowId })`. Anonymous callers
1710
+ * are allowed; deprecated entries are filtered out server-side.
1711
+ */
1712
+ async catalog() {
1713
+ const response = await this.#http.request("GET", "/api/v1/workflows/catalog");
1714
+ const payload = await response.json();
1715
+ const rows = Array.isArray(payload.workflows) ? payload.workflows : [];
1716
+ return rows.map((row) => parseCatalogWorkflow(row));
1717
+ }
1547
1718
  /** Browse the public community listing (one page + a `cursor`). */
1548
1719
  async search(options = {}) {
1549
1720
  const params = {
@@ -1658,5 +1829,5 @@ function resolveRetryPolicy(maxRetries, retryPolicy) {
1658
1829
  }
1659
1830
 
1660
1831
  export { APIError, AuthError, AutoThrottleConfig, CONVERT_JOB_TERMINAL_STATUSES, Convilyn, ConvilynError, DEFAULT_BASE_URL, ExponentialBackoffRetry, GOAL_EVENT_TERMINAL_TYPES, GOAL_EVENT_TYPES, GOAL_JOB_TERMINAL_STATUSES, GoalJobFailedError, GoalJobTimeoutError, HttpClient, JobFailedError, JobTimeoutError, NoRetry, PlanRequiredError, QuotaExceededError, RateLimitError, RetryExhaustedError, S3UploadError, VERSION, WebSocketError, goalJobNeedsInput, guessContentType, isConvertJobTerminal, isConvilynError, isGoalEventTerminal, isGoalEventType, isGoalJobTerminal, resolveAuth };
1661
- //# sourceMappingURL=chunk-IBSN34ZI.js.map
1662
- //# sourceMappingURL=chunk-IBSN34ZI.js.map
1832
+ //# sourceMappingURL=chunk-Z5IPYRO3.js.map
1833
+ //# sourceMappingURL=chunk-Z5IPYRO3.js.map