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