@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/CHANGELOG.md +33 -0
- package/LICENSE +201 -201
- package/dist/{chunk-IBSN34ZI.js → chunk-Z5IPYRO3.js} +192 -21
- package/dist/chunk-Z5IPYRO3.js.map +1 -0
- package/dist/cli.cjs +190 -19
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +190 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +125 -3
- package/dist/index.d.ts +125 -3
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-IBSN34ZI.js.map +0 -1
package/dist/cli.cjs
CHANGED
|
@@ -6,7 +6,7 @@ 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
12
|
var CONVILYN_ERROR_BRAND = /* @__PURE__ */ Symbol.for("@convilyn/sdk.ConvilynError");
|
|
@@ -118,14 +118,22 @@ var GoalJobTimeoutError = class extends ConvilynError {
|
|
|
118
118
|
jobSpecId;
|
|
119
119
|
elapsed;
|
|
120
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;
|
|
121
127
|
constructor(opts) {
|
|
128
|
+
const reason = opts.reason ?? "total";
|
|
122
129
|
super(
|
|
123
|
-
`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)`
|
|
124
131
|
);
|
|
125
132
|
this.name = "GoalJobTimeoutError";
|
|
126
133
|
this.jobSpecId = opts.jobSpecId;
|
|
127
134
|
this.elapsed = opts.elapsed;
|
|
128
135
|
this.timeout = opts.timeout;
|
|
136
|
+
this.reason = reason;
|
|
129
137
|
}
|
|
130
138
|
};
|
|
131
139
|
var WebSocketError = class extends ConvilynError {
|
|
@@ -725,6 +733,29 @@ function parseGoalJob(wire) {
|
|
|
725
733
|
completedAt: toDateOrNull(wire.completedAt)
|
|
726
734
|
};
|
|
727
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
|
+
}
|
|
728
759
|
function parseGoalEvent(wire) {
|
|
729
760
|
return {
|
|
730
761
|
type: wire.type,
|
|
@@ -777,6 +808,27 @@ function parseWorkflowSearchPage(wire) {
|
|
|
777
808
|
cursor: wire.cursor ?? null
|
|
778
809
|
};
|
|
779
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
|
+
}
|
|
780
832
|
function parseLikeResponse(wire) {
|
|
781
833
|
return {
|
|
782
834
|
liked: Boolean(wire.liked),
|
|
@@ -862,6 +914,52 @@ var Account = class {
|
|
|
862
914
|
}
|
|
863
915
|
};
|
|
864
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
|
+
|
|
865
963
|
// src/types.ts
|
|
866
964
|
var CONVERT_JOB_TERMINAL_STATUSES = ["completed", "failed"];
|
|
867
965
|
function isConvertJobTerminal(job) {
|
|
@@ -997,17 +1095,7 @@ var Convert = class {
|
|
|
997
1095
|
/** Download the first result file to `to` (Node) and return that path. */
|
|
998
1096
|
async downloadTo(job, { to }) {
|
|
999
1097
|
const url = await this.downloadUrl(job);
|
|
1000
|
-
|
|
1001
|
-
const st = await lstat(to).catch(() => null);
|
|
1002
|
-
if (st?.isSymbolicLink()) {
|
|
1003
|
-
throw new Error(
|
|
1004
|
-
`Refusing to write to ${JSON.stringify(to)}: target is a symlink. Remove it or choose a non-symlink destination.`
|
|
1005
|
-
);
|
|
1006
|
-
}
|
|
1007
|
-
const response = await this.#http.externalGet(url);
|
|
1008
|
-
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
1009
|
-
await writeFile(to, bytes);
|
|
1010
|
-
return to;
|
|
1098
|
+
return downloadUrlToPath(this.#http, url, to);
|
|
1011
1099
|
}
|
|
1012
1100
|
// ── private ────────────────────────────────────────────────────
|
|
1013
1101
|
#resolveSource(options) {
|
|
@@ -1162,6 +1250,19 @@ var Files = class {
|
|
|
1162
1250
|
const contentType = options.contentType ?? guessContentType(filename);
|
|
1163
1251
|
return this.#uploadWithBody(bytes, filename, contentType);
|
|
1164
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
|
+
}
|
|
1165
1266
|
async #uploadWithBody(body, filename, contentType) {
|
|
1166
1267
|
const size = body.byteLength;
|
|
1167
1268
|
const presign = await this.#presign(filename, size, contentType);
|
|
@@ -1362,7 +1463,8 @@ var Goals = class {
|
|
|
1362
1463
|
return this.#waitLoop(
|
|
1363
1464
|
jobSpecId,
|
|
1364
1465
|
options.timeout ?? DEFAULT_POLL_TIMEOUT2,
|
|
1365
|
-
options.pollInterval ?? DEFAULT_POLL_INTERVAL2
|
|
1466
|
+
options.pollInterval ?? DEFAULT_POLL_INTERVAL2,
|
|
1467
|
+
options.idleTimeout ?? null
|
|
1366
1468
|
);
|
|
1367
1469
|
}
|
|
1368
1470
|
/** Shortcut: {@link start} then {@link wait}. */
|
|
@@ -1370,7 +1472,8 @@ var Goals = class {
|
|
|
1370
1472
|
const job = await this.start(options);
|
|
1371
1473
|
return this.wait(job.jobSpecId, {
|
|
1372
1474
|
timeout: options.timeout,
|
|
1373
|
-
pollInterval: options.pollInterval
|
|
1475
|
+
pollInterval: options.pollInterval,
|
|
1476
|
+
idleTimeout: options.idleTimeout
|
|
1374
1477
|
});
|
|
1375
1478
|
}
|
|
1376
1479
|
/** Answer a single slot the agent is waiting on (sugar over {@link fillSlots}). */
|
|
@@ -1400,9 +1503,17 @@ var Goals = class {
|
|
|
1400
1503
|
});
|
|
1401
1504
|
return parseGoalJob(await response.json());
|
|
1402
1505
|
}
|
|
1403
|
-
/**
|
|
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
|
+
*/
|
|
1404
1515
|
async confirm(jobSpecId, options = {}) {
|
|
1405
|
-
const json = options.expectedVersion != null ? { expectedVersion: options.expectedVersion } :
|
|
1516
|
+
const json = options.expectedVersion != null ? { expectedVersion: options.expectedVersion } : {};
|
|
1406
1517
|
await this.#http.request("POST", `/api/v1/jobs/goal/${jobSpecId}/confirm`, { json });
|
|
1407
1518
|
return this.#pollOnce(jobSpecId);
|
|
1408
1519
|
}
|
|
@@ -1420,6 +1531,41 @@ var Goals = class {
|
|
|
1420
1531
|
await this.#http.request("POST", `/api/v1/jobs/goal/${jobSpecId}/retry`, { json: body });
|
|
1421
1532
|
return this.#pollOnce(jobSpecId);
|
|
1422
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
|
+
}
|
|
1423
1569
|
/**
|
|
1424
1570
|
* Stream goal execution events for a job. Opens a WebSocket, subscribes,
|
|
1425
1571
|
* then yields each {@link GoalEvent} in order; the iterator ends (and the
|
|
@@ -1474,11 +1620,13 @@ var Goals = class {
|
|
|
1474
1620
|
const response = await this.#http.request("GET", `/api/v1/jobs/goal/${jobSpecId}`);
|
|
1475
1621
|
return parseGoalJob(await response.json());
|
|
1476
1622
|
}
|
|
1477
|
-
async #waitLoop(jobSpecId, timeout, initialInterval) {
|
|
1623
|
+
async #waitLoop(jobSpecId, timeout, initialInterval, idleTimeout) {
|
|
1478
1624
|
const start = this.#now() / 1e3;
|
|
1479
1625
|
let interval = initialInterval;
|
|
1480
1626
|
let staleCount = 0;
|
|
1481
1627
|
let lastProgress = -1;
|
|
1628
|
+
let lastStatus = null;
|
|
1629
|
+
let lastChange = start;
|
|
1482
1630
|
for (; ; ) {
|
|
1483
1631
|
const job = await this.#pollOnce(jobSpecId);
|
|
1484
1632
|
if (isGoalJobTerminal(job)) {
|
|
@@ -1487,6 +1635,10 @@ var Goals = class {
|
|
|
1487
1635
|
if (goalJobNeedsInput(job)) {
|
|
1488
1636
|
return job;
|
|
1489
1637
|
}
|
|
1638
|
+
if (job.status !== lastStatus) {
|
|
1639
|
+
lastStatus = job.status;
|
|
1640
|
+
lastChange = this.#now() / 1e3;
|
|
1641
|
+
}
|
|
1490
1642
|
if (job.progress === lastProgress) {
|
|
1491
1643
|
staleCount += 1;
|
|
1492
1644
|
if (staleCount >= STALE_PROGRESS_BACKOFF_AFTER2) {
|
|
@@ -1495,12 +1647,17 @@ var Goals = class {
|
|
|
1495
1647
|
}
|
|
1496
1648
|
} else {
|
|
1497
1649
|
lastProgress = job.progress;
|
|
1650
|
+
lastChange = this.#now() / 1e3;
|
|
1498
1651
|
staleCount = 0;
|
|
1499
1652
|
}
|
|
1500
|
-
const
|
|
1653
|
+
const now = this.#now() / 1e3;
|
|
1654
|
+
const elapsed = now - start;
|
|
1501
1655
|
if (elapsed + interval > timeout) {
|
|
1502
1656
|
throw new GoalJobTimeoutError({ jobSpecId, elapsed, timeout });
|
|
1503
1657
|
}
|
|
1658
|
+
if (idleTimeout != null && now - lastChange + interval > idleTimeout) {
|
|
1659
|
+
throw new GoalJobTimeoutError({ jobSpecId, elapsed, timeout: idleTimeout, reason: "idle" });
|
|
1660
|
+
}
|
|
1504
1661
|
await this.#sleep(interval);
|
|
1505
1662
|
}
|
|
1506
1663
|
}
|
|
@@ -1548,6 +1705,20 @@ var Workflows = class {
|
|
|
1548
1705
|
constructor(http) {
|
|
1549
1706
|
this.#http = http;
|
|
1550
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
|
+
}
|
|
1551
1722
|
/** Browse the public community listing (one page + a `cursor`). */
|
|
1552
1723
|
async search(options = {}) {
|
|
1553
1724
|
const params = {
|