@convilyn/sdk 0.2.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/CHANGELOG.md +66 -0
- package/LICENSE +201 -201
- package/README.md +1 -1
- package/dist/{chunk-UJHZKIP6.js → chunk-Z5IPYRO3.js} +206 -23
- package/dist/chunk-Z5IPYRO3.js.map +1 -0
- package/dist/cli.cjs +200 -20
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +204 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +144 -3
- package/dist/index.d.ts +144 -3
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-UJHZKIP6.js.map +0 -1
package/dist/cli.cjs
CHANGED
|
@@ -6,10 +6,13 @@ var fs = require('fs');
|
|
|
6
6
|
var path = require('path');
|
|
7
7
|
|
|
8
8
|
// src/version.ts
|
|
9
|
-
var VERSION = "0.
|
|
9
|
+
var VERSION = "0.4.0";
|
|
10
10
|
|
|
11
11
|
// src/errors.ts
|
|
12
|
+
var CONVILYN_ERROR_BRAND = /* @__PURE__ */ Symbol.for("@convilyn/sdk.ConvilynError");
|
|
12
13
|
var ConvilynError = class extends Error {
|
|
14
|
+
/** @internal brand marker — see {@link isConvilynError} */
|
|
15
|
+
[CONVILYN_ERROR_BRAND] = true;
|
|
13
16
|
constructor(message) {
|
|
14
17
|
super(message);
|
|
15
18
|
this.name = "ConvilynError";
|
|
@@ -115,14 +118,22 @@ var GoalJobTimeoutError = class extends ConvilynError {
|
|
|
115
118
|
jobSpecId;
|
|
116
119
|
elapsed;
|
|
117
120
|
timeout;
|
|
121
|
+
/**
|
|
122
|
+
* `'total'` — the overall `timeout` budget lapsed; `'idle'` —
|
|
123
|
+
* `idleTimeout` lapsed with no status/progress change (the job may still
|
|
124
|
+
* be healthy on a long phase; `retrieve()` before assuming failure).
|
|
125
|
+
*/
|
|
126
|
+
reason;
|
|
118
127
|
constructor(opts) {
|
|
128
|
+
const reason = opts.reason ?? "total";
|
|
119
129
|
super(
|
|
120
|
-
`GoalJob ${opts.jobSpecId} did not reach a terminal status within ${opts.timeout}s (elapsed ${opts.elapsed.toFixed(1)}s)`
|
|
130
|
+
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)`
|
|
121
131
|
);
|
|
122
132
|
this.name = "GoalJobTimeoutError";
|
|
123
133
|
this.jobSpecId = opts.jobSpecId;
|
|
124
134
|
this.elapsed = opts.elapsed;
|
|
125
135
|
this.timeout = opts.timeout;
|
|
136
|
+
this.reason = reason;
|
|
126
137
|
}
|
|
127
138
|
};
|
|
128
139
|
var WebSocketError = class extends ConvilynError {
|
|
@@ -722,6 +733,29 @@ function parseGoalJob(wire) {
|
|
|
722
733
|
completedAt: toDateOrNull(wire.completedAt)
|
|
723
734
|
};
|
|
724
735
|
}
|
|
736
|
+
function parseArtifact(wire) {
|
|
737
|
+
return {
|
|
738
|
+
artifactId: String(wire.artifactId),
|
|
739
|
+
fileName: String(wire.fileName),
|
|
740
|
+
mimeType: String(wire.mimeType),
|
|
741
|
+
sizeBytes: Number(wire.sizeBytes ?? 0),
|
|
742
|
+
downloadUrl: wire.downloadUrl ?? null,
|
|
743
|
+
artifactType: wire.artifactType ?? null,
|
|
744
|
+
platform: wire.platform ?? null,
|
|
745
|
+
metadata: isRecord2(wire.metadata) ? wire.metadata : null,
|
|
746
|
+
isPrimary: Boolean(wire.isPrimary ?? false),
|
|
747
|
+
description: String(wire.description ?? "")
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
function parseArtifactDownload(wire) {
|
|
751
|
+
return {
|
|
752
|
+
downloadUrl: String(wire.downloadUrl),
|
|
753
|
+
fileName: String(wire.fileName),
|
|
754
|
+
sizeBytes: Number(wire.sizeBytes ?? 0),
|
|
755
|
+
mimeType: String(wire.mimeType),
|
|
756
|
+
expiresAt: toDate(wire.expiresAt)
|
|
757
|
+
};
|
|
758
|
+
}
|
|
725
759
|
function parseGoalEvent(wire) {
|
|
726
760
|
return {
|
|
727
761
|
type: wire.type,
|
|
@@ -774,6 +808,27 @@ function parseWorkflowSearchPage(wire) {
|
|
|
774
808
|
cursor: wire.cursor ?? null
|
|
775
809
|
};
|
|
776
810
|
}
|
|
811
|
+
function parseCatalogWorkflow(wire) {
|
|
812
|
+
return {
|
|
813
|
+
workflowId: String(wire.workflowId),
|
|
814
|
+
name: String(wire.name),
|
|
815
|
+
description: String(wire.description ?? ""),
|
|
816
|
+
icon: wire.icon ?? null,
|
|
817
|
+
supportedInputTypes: Array.isArray(wire.supportedInputTypes) ? wire.supportedInputTypes.map(String) : [],
|
|
818
|
+
supportedInputFormats: Array.isArray(wire.supportedInputFormats) ? wire.supportedInputFormats.map(String) : null,
|
|
819
|
+
category: String(wire.category ?? "goal_lane"),
|
|
820
|
+
requiredSlotCount: Number(wire.requiredSlotCount ?? 0),
|
|
821
|
+
subcategory: wire.subcategory ?? null,
|
|
822
|
+
skuGroup: wire.skuGroup ?? null,
|
|
823
|
+
status: String(wire.status ?? "active"),
|
|
824
|
+
supportedLocales: Array.isArray(wire.supportedLocales) ? wire.supportedLocales.map(String) : null,
|
|
825
|
+
maxInputSizeBytes: wire.maxInputSizeBytes ?? null,
|
|
826
|
+
minFileCount: wire.minFileCount ?? null,
|
|
827
|
+
tier: wire.tier ?? null,
|
|
828
|
+
// Tri-state: only an explicit false blocks a Free-tier run; absent stays null.
|
|
829
|
+
freeTierAllowed: wire.freeTierAllowed ?? null
|
|
830
|
+
};
|
|
831
|
+
}
|
|
777
832
|
function parseLikeResponse(wire) {
|
|
778
833
|
return {
|
|
779
834
|
liked: Boolean(wire.liked),
|
|
@@ -859,6 +914,52 @@ var Account = class {
|
|
|
859
914
|
}
|
|
860
915
|
};
|
|
861
916
|
|
|
917
|
+
// src/internal/download.ts
|
|
918
|
+
var MAX_DOWNLOAD_BYTES = 2 * 1024 ** 3;
|
|
919
|
+
async function downloadUrlToPath(http, url, to, maxBytes = MAX_DOWNLOAD_BYTES) {
|
|
920
|
+
const { lstat, open, unlink } = await import('fs/promises');
|
|
921
|
+
const st = await lstat(to).catch(() => null);
|
|
922
|
+
if (st?.isSymbolicLink()) {
|
|
923
|
+
throw new Error(
|
|
924
|
+
`Refusing to write to ${JSON.stringify(to)}: target is a symlink. Remove it or choose a non-symlink destination.`
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
const response = await http.externalGet(url);
|
|
928
|
+
const body = response.body;
|
|
929
|
+
const handle = await open(to, "w");
|
|
930
|
+
let written = 0;
|
|
931
|
+
try {
|
|
932
|
+
if (body == null) {
|
|
933
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
934
|
+
written = bytes.byteLength;
|
|
935
|
+
if (written > maxBytes) {
|
|
936
|
+
throw new Error(`Download exceeds the ${maxBytes}-byte cap`);
|
|
937
|
+
}
|
|
938
|
+
await handle.write(bytes);
|
|
939
|
+
} else {
|
|
940
|
+
const reader = body.getReader();
|
|
941
|
+
for (; ; ) {
|
|
942
|
+
const { done, value } = await reader.read();
|
|
943
|
+
if (done) {
|
|
944
|
+
break;
|
|
945
|
+
}
|
|
946
|
+
written += value.byteLength;
|
|
947
|
+
if (written > maxBytes) {
|
|
948
|
+
await reader.cancel().catch(() => void 0);
|
|
949
|
+
throw new Error(`Download exceeds the ${maxBytes}-byte cap`);
|
|
950
|
+
}
|
|
951
|
+
await handle.write(value);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
} catch (error) {
|
|
955
|
+
await handle.close().catch(() => void 0);
|
|
956
|
+
await unlink(to).catch(() => void 0);
|
|
957
|
+
throw error;
|
|
958
|
+
}
|
|
959
|
+
await handle.close();
|
|
960
|
+
return to;
|
|
961
|
+
}
|
|
962
|
+
|
|
862
963
|
// src/types.ts
|
|
863
964
|
var CONVERT_JOB_TERMINAL_STATUSES = ["completed", "failed"];
|
|
864
965
|
function isConvertJobTerminal(job) {
|
|
@@ -994,17 +1095,7 @@ var Convert = class {
|
|
|
994
1095
|
/** Download the first result file to `to` (Node) and return that path. */
|
|
995
1096
|
async downloadTo(job, { to }) {
|
|
996
1097
|
const url = await this.downloadUrl(job);
|
|
997
|
-
|
|
998
|
-
const st = await lstat(to).catch(() => null);
|
|
999
|
-
if (st?.isSymbolicLink()) {
|
|
1000
|
-
throw new Error(
|
|
1001
|
-
`Refusing to write to ${JSON.stringify(to)}: target is a symlink. Remove it or choose a non-symlink destination.`
|
|
1002
|
-
);
|
|
1003
|
-
}
|
|
1004
|
-
const response = await this.#http.externalGet(url);
|
|
1005
|
-
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
1006
|
-
await writeFile(to, bytes);
|
|
1007
|
-
return to;
|
|
1098
|
+
return downloadUrlToPath(this.#http, url, to);
|
|
1008
1099
|
}
|
|
1009
1100
|
// ── private ────────────────────────────────────────────────────
|
|
1010
1101
|
#resolveSource(options) {
|
|
@@ -1159,6 +1250,19 @@ var Files = class {
|
|
|
1159
1250
|
const contentType = options.contentType ?? guessContentType(filename);
|
|
1160
1251
|
return this.#uploadWithBody(bytes, filename, contentType);
|
|
1161
1252
|
}
|
|
1253
|
+
/**
|
|
1254
|
+
* Delete an uploaded file's cloud copy (storage object + metadata record).
|
|
1255
|
+
*
|
|
1256
|
+
* Only the uploader can delete; a missing or unowned `fileId` surfaces as
|
|
1257
|
+
* a 404 `APIError` and a file attached to a still-running job as a 409
|
|
1258
|
+
* (`FILE_IN_USE`). The platform deletes input files automatically ~1 hour
|
|
1259
|
+
* after upload anyway — call this when you want the cloud copy gone
|
|
1260
|
+
* deterministically the moment your workflow is done (e.g. privacy-
|
|
1261
|
+
* sensitive documents on an edge device). Port of Python `files.delete`.
|
|
1262
|
+
*/
|
|
1263
|
+
async delete(fileId) {
|
|
1264
|
+
await this.#http.request("DELETE", `/api/v1/files/${fileId}`);
|
|
1265
|
+
}
|
|
1162
1266
|
async #uploadWithBody(body, filename, contentType) {
|
|
1163
1267
|
const size = body.byteLength;
|
|
1164
1268
|
const presign = await this.#presign(filename, size, contentType);
|
|
@@ -1359,7 +1463,8 @@ var Goals = class {
|
|
|
1359
1463
|
return this.#waitLoop(
|
|
1360
1464
|
jobSpecId,
|
|
1361
1465
|
options.timeout ?? DEFAULT_POLL_TIMEOUT2,
|
|
1362
|
-
options.pollInterval ?? DEFAULT_POLL_INTERVAL2
|
|
1466
|
+
options.pollInterval ?? DEFAULT_POLL_INTERVAL2,
|
|
1467
|
+
options.idleTimeout ?? null
|
|
1363
1468
|
);
|
|
1364
1469
|
}
|
|
1365
1470
|
/** Shortcut: {@link start} then {@link wait}. */
|
|
@@ -1367,7 +1472,8 @@ var Goals = class {
|
|
|
1367
1472
|
const job = await this.start(options);
|
|
1368
1473
|
return this.wait(job.jobSpecId, {
|
|
1369
1474
|
timeout: options.timeout,
|
|
1370
|
-
pollInterval: options.pollInterval
|
|
1475
|
+
pollInterval: options.pollInterval,
|
|
1476
|
+
idleTimeout: options.idleTimeout
|
|
1371
1477
|
});
|
|
1372
1478
|
}
|
|
1373
1479
|
/** Answer a single slot the agent is waiting on (sugar over {@link fillSlots}). */
|
|
@@ -1397,9 +1503,17 @@ var Goals = class {
|
|
|
1397
1503
|
});
|
|
1398
1504
|
return parseGoalJob(await response.json());
|
|
1399
1505
|
}
|
|
1400
|
-
/**
|
|
1506
|
+
/**
|
|
1507
|
+
* Confirm a slots-filled job, queueing it for execution.
|
|
1508
|
+
*
|
|
1509
|
+
* `expectedVersion` is optional — the server re-reads the current version
|
|
1510
|
+
* at submit time regardless, so most callers should simply omit it. The
|
|
1511
|
+
* request always carries a JSON object (`{}` when empty): older backend
|
|
1512
|
+
* builds declare the confirm body as a required parameter and reject a
|
|
1513
|
+
* body-less POST with 422 before the handler runs.
|
|
1514
|
+
*/
|
|
1401
1515
|
async confirm(jobSpecId, options = {}) {
|
|
1402
|
-
const json = options.expectedVersion != null ? { expectedVersion: options.expectedVersion } :
|
|
1516
|
+
const json = options.expectedVersion != null ? { expectedVersion: options.expectedVersion } : {};
|
|
1403
1517
|
await this.#http.request("POST", `/api/v1/jobs/goal/${jobSpecId}/confirm`, { json });
|
|
1404
1518
|
return this.#pollOnce(jobSpecId);
|
|
1405
1519
|
}
|
|
@@ -1417,6 +1531,41 @@ var Goals = class {
|
|
|
1417
1531
|
await this.#http.request("POST", `/api/v1/jobs/goal/${jobSpecId}/retry`, { json: body });
|
|
1418
1532
|
return this.#pollOnce(jobSpecId);
|
|
1419
1533
|
}
|
|
1534
|
+
/**
|
|
1535
|
+
* List a completed job's output artifacts with fresh download URLs.
|
|
1536
|
+
*
|
|
1537
|
+
* Available once the job is terminal-successful (`completed` or `partial`);
|
|
1538
|
+
* any other status surfaces the backend's 400 as an `APIError`. Each
|
|
1539
|
+
* artifact's `downloadUrl` is presigned and valid for one hour — re-call
|
|
1540
|
+
* this method for fresh URLs rather than persisting one.
|
|
1541
|
+
*/
|
|
1542
|
+
async artifacts(jobSpecId) {
|
|
1543
|
+
const response = await this.#http.request("GET", `/api/v1/jobs/goal/${jobSpecId}/artifacts`);
|
|
1544
|
+
const payload = await response.json();
|
|
1545
|
+
const rows = Array.isArray(payload.artifacts) ? payload.artifacts : [];
|
|
1546
|
+
return rows.map((row) => parseArtifact(row));
|
|
1547
|
+
}
|
|
1548
|
+
/**
|
|
1549
|
+
* Mint a fresh presigned download URL for one artifact. Prefer this over
|
|
1550
|
+
* reusing a stale {@link Artifact.downloadUrl} when more than an hour may
|
|
1551
|
+
* have passed since {@link artifacts}.
|
|
1552
|
+
*/
|
|
1553
|
+
async downloadArtifactUrl(jobSpecId, artifactId) {
|
|
1554
|
+
const response = await this.#http.request(
|
|
1555
|
+
"GET",
|
|
1556
|
+
`/api/v1/jobs/goal/${jobSpecId}/artifacts/${artifactId}/download`
|
|
1557
|
+
);
|
|
1558
|
+
return parseArtifactDownload(await response.json());
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Download one artifact to `to` (Node) and return that path. Mints a fresh
|
|
1562
|
+
* presigned URL first, then streams the body to disk (shares the size-cap
|
|
1563
|
+
* and symlink-refusal contract with `convert.downloadTo`).
|
|
1564
|
+
*/
|
|
1565
|
+
async downloadArtifactTo(jobSpecId, artifactId, { to }) {
|
|
1566
|
+
const info = await this.downloadArtifactUrl(jobSpecId, artifactId);
|
|
1567
|
+
return downloadUrlToPath(this.#http, info.downloadUrl, to);
|
|
1568
|
+
}
|
|
1420
1569
|
/**
|
|
1421
1570
|
* Stream goal execution events for a job. Opens a WebSocket, subscribes,
|
|
1422
1571
|
* then yields each {@link GoalEvent} in order; the iterator ends (and the
|
|
@@ -1471,11 +1620,13 @@ var Goals = class {
|
|
|
1471
1620
|
const response = await this.#http.request("GET", `/api/v1/jobs/goal/${jobSpecId}`);
|
|
1472
1621
|
return parseGoalJob(await response.json());
|
|
1473
1622
|
}
|
|
1474
|
-
async #waitLoop(jobSpecId, timeout, initialInterval) {
|
|
1623
|
+
async #waitLoop(jobSpecId, timeout, initialInterval, idleTimeout) {
|
|
1475
1624
|
const start = this.#now() / 1e3;
|
|
1476
1625
|
let interval = initialInterval;
|
|
1477
1626
|
let staleCount = 0;
|
|
1478
1627
|
let lastProgress = -1;
|
|
1628
|
+
let lastStatus = null;
|
|
1629
|
+
let lastChange = start;
|
|
1479
1630
|
for (; ; ) {
|
|
1480
1631
|
const job = await this.#pollOnce(jobSpecId);
|
|
1481
1632
|
if (isGoalJobTerminal(job)) {
|
|
@@ -1484,6 +1635,10 @@ var Goals = class {
|
|
|
1484
1635
|
if (goalJobNeedsInput(job)) {
|
|
1485
1636
|
return job;
|
|
1486
1637
|
}
|
|
1638
|
+
if (job.status !== lastStatus) {
|
|
1639
|
+
lastStatus = job.status;
|
|
1640
|
+
lastChange = this.#now() / 1e3;
|
|
1641
|
+
}
|
|
1487
1642
|
if (job.progress === lastProgress) {
|
|
1488
1643
|
staleCount += 1;
|
|
1489
1644
|
if (staleCount >= STALE_PROGRESS_BACKOFF_AFTER2) {
|
|
@@ -1492,12 +1647,17 @@ var Goals = class {
|
|
|
1492
1647
|
}
|
|
1493
1648
|
} else {
|
|
1494
1649
|
lastProgress = job.progress;
|
|
1650
|
+
lastChange = this.#now() / 1e3;
|
|
1495
1651
|
staleCount = 0;
|
|
1496
1652
|
}
|
|
1497
|
-
const
|
|
1653
|
+
const now = this.#now() / 1e3;
|
|
1654
|
+
const elapsed = now - start;
|
|
1498
1655
|
if (elapsed + interval > timeout) {
|
|
1499
1656
|
throw new GoalJobTimeoutError({ jobSpecId, elapsed, timeout });
|
|
1500
1657
|
}
|
|
1658
|
+
if (idleTimeout != null && now - lastChange + interval > idleTimeout) {
|
|
1659
|
+
throw new GoalJobTimeoutError({ jobSpecId, elapsed, timeout: idleTimeout, reason: "idle" });
|
|
1660
|
+
}
|
|
1501
1661
|
await this.#sleep(interval);
|
|
1502
1662
|
}
|
|
1503
1663
|
}
|
|
@@ -1545,6 +1705,20 @@ var Workflows = class {
|
|
|
1545
1705
|
constructor(http) {
|
|
1546
1706
|
this.#http = http;
|
|
1547
1707
|
}
|
|
1708
|
+
/**
|
|
1709
|
+
* List the platform's built-in AI-workflow catalog.
|
|
1710
|
+
*
|
|
1711
|
+
* Distinct from {@link search}, which browses the user-published community
|
|
1712
|
+
* marketplace — the catalog is the curated set of built-in workflows
|
|
1713
|
+
* runnable directly via `goals.start({ workflowId })`. Anonymous callers
|
|
1714
|
+
* are allowed; deprecated entries are filtered out server-side.
|
|
1715
|
+
*/
|
|
1716
|
+
async catalog() {
|
|
1717
|
+
const response = await this.#http.request("GET", "/api/v1/workflows/catalog");
|
|
1718
|
+
const payload = await response.json();
|
|
1719
|
+
const rows = Array.isArray(payload.workflows) ? payload.workflows : [];
|
|
1720
|
+
return rows.map((row) => parseCatalogWorkflow(row));
|
|
1721
|
+
}
|
|
1548
1722
|
/** Browse the public community listing (one page + a `cursor`). */
|
|
1549
1723
|
async search(options = {}) {
|
|
1550
1724
|
const params = {
|
|
@@ -1615,6 +1789,9 @@ var Convilyn = class {
|
|
|
1615
1789
|
#http;
|
|
1616
1790
|
constructor(options = {}) {
|
|
1617
1791
|
const auth = options.auth ?? resolveAuth(options.apiKey);
|
|
1792
|
+
if (options.timeout !== void 0 && options.timeout <= 0) {
|
|
1793
|
+
throw new TypeError(`timeout must be a positive number of seconds (got ${options.timeout})`);
|
|
1794
|
+
}
|
|
1618
1795
|
this.#http = new HttpClient({
|
|
1619
1796
|
auth,
|
|
1620
1797
|
baseUrl: options.baseUrl,
|
|
@@ -1646,7 +1823,10 @@ function resolveRetryPolicy(maxRetries, retryPolicy) {
|
|
|
1646
1823
|
if (maxRetries == null) {
|
|
1647
1824
|
return new ExponentialBackoffRetry();
|
|
1648
1825
|
}
|
|
1649
|
-
if (maxRetries
|
|
1826
|
+
if (maxRetries < 0) {
|
|
1827
|
+
throw new TypeError(`maxRetries must be >= 0 (got ${maxRetries})`);
|
|
1828
|
+
}
|
|
1829
|
+
if (maxRetries === 0) {
|
|
1650
1830
|
return new NoRetry();
|
|
1651
1831
|
}
|
|
1652
1832
|
return new ExponentialBackoffRetry({ maxAttempts: maxRetries });
|