@convilyn/sdk 0.2.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 +137 -0
- package/LICENSE +201 -0
- package/README.md +282 -0
- package/dist/chunk-UJHZKIP6.js +1650 -0
- package/dist/chunk-UJHZKIP6.js.map +1 -0
- package/dist/cli.cjs +2667 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.js +1019 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +1678 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +893 -0
- package/dist/index.d.ts +893 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/package.json +78 -0
|
@@ -0,0 +1,1650 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var ConvilynError = class extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "ConvilynError";
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
var AuthError = class extends ConvilynError {
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "AuthError";
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
var APIError = class extends ConvilynError {
|
|
15
|
+
statusCode;
|
|
16
|
+
code;
|
|
17
|
+
details;
|
|
18
|
+
constructor(statusCode, code, message, details) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "APIError";
|
|
21
|
+
this.statusCode = statusCode;
|
|
22
|
+
this.code = code;
|
|
23
|
+
this.details = details ?? {};
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var RateLimitError = class extends APIError {
|
|
27
|
+
constructor(statusCode, code, message, details) {
|
|
28
|
+
super(statusCode, code, message, details);
|
|
29
|
+
this.name = "RateLimitError";
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var PlanRequiredError = class extends APIError {
|
|
33
|
+
requiredPlan;
|
|
34
|
+
upgradeUrl;
|
|
35
|
+
constructor(statusCode, code, message, details, opts = {}) {
|
|
36
|
+
super(statusCode, code, message, details);
|
|
37
|
+
this.name = "PlanRequiredError";
|
|
38
|
+
this.requiredPlan = opts.requiredPlan ?? "pro";
|
|
39
|
+
this.upgradeUrl = opts.upgradeUrl ?? null;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
var QuotaExceededError = class extends APIError {
|
|
43
|
+
costCredits;
|
|
44
|
+
balanceCredits;
|
|
45
|
+
topUpUrl;
|
|
46
|
+
constructor(statusCode, code, message, details, opts = {}) {
|
|
47
|
+
super(statusCode, code, message, details);
|
|
48
|
+
this.name = "QuotaExceededError";
|
|
49
|
+
this.costCredits = opts.costCredits ?? null;
|
|
50
|
+
this.balanceCredits = opts.balanceCredits ?? null;
|
|
51
|
+
this.topUpUrl = opts.topUpUrl ?? null;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
var S3UploadError = class extends APIError {
|
|
55
|
+
constructor(statusCode, code, message, details) {
|
|
56
|
+
super(statusCode, code, message, details);
|
|
57
|
+
this.name = "S3UploadError";
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
var RetryExhaustedError = class extends APIError {
|
|
61
|
+
attemptCount;
|
|
62
|
+
constructor(statusCode, code, message, details, opts = {}) {
|
|
63
|
+
super(statusCode, code, message, details);
|
|
64
|
+
this.name = "RetryExhaustedError";
|
|
65
|
+
this.attemptCount = opts.attemptCount ?? 0;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
var JobFailedError = class extends ConvilynError {
|
|
69
|
+
jobId;
|
|
70
|
+
processorType;
|
|
71
|
+
code;
|
|
72
|
+
constructor(opts) {
|
|
73
|
+
super(opts.message);
|
|
74
|
+
this.name = "JobFailedError";
|
|
75
|
+
this.jobId = opts.jobId;
|
|
76
|
+
this.processorType = opts.processorType;
|
|
77
|
+
this.code = opts.code;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
var JobTimeoutError = class extends ConvilynError {
|
|
81
|
+
jobId;
|
|
82
|
+
elapsed;
|
|
83
|
+
timeout;
|
|
84
|
+
constructor(opts) {
|
|
85
|
+
super(
|
|
86
|
+
`Job ${opts.jobId} did not reach a terminal status within ${opts.timeout}s (elapsed ${opts.elapsed.toFixed(1)}s)`
|
|
87
|
+
);
|
|
88
|
+
this.name = "JobTimeoutError";
|
|
89
|
+
this.jobId = opts.jobId;
|
|
90
|
+
this.elapsed = opts.elapsed;
|
|
91
|
+
this.timeout = opts.timeout;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
var GoalJobFailedError = class extends ConvilynError {
|
|
95
|
+
jobSpecId;
|
|
96
|
+
code;
|
|
97
|
+
constructor(opts) {
|
|
98
|
+
super(opts.message ?? "Job failed without a structured error message");
|
|
99
|
+
this.name = "GoalJobFailedError";
|
|
100
|
+
this.jobSpecId = opts.jobSpecId;
|
|
101
|
+
this.code = opts.code ?? "GOAL_JOB_FAILED";
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
var GoalJobTimeoutError = class extends ConvilynError {
|
|
105
|
+
jobSpecId;
|
|
106
|
+
elapsed;
|
|
107
|
+
timeout;
|
|
108
|
+
constructor(opts) {
|
|
109
|
+
super(
|
|
110
|
+
`GoalJob ${opts.jobSpecId} did not reach a terminal status within ${opts.timeout}s (elapsed ${opts.elapsed.toFixed(1)}s)`
|
|
111
|
+
);
|
|
112
|
+
this.name = "GoalJobTimeoutError";
|
|
113
|
+
this.jobSpecId = opts.jobSpecId;
|
|
114
|
+
this.elapsed = opts.elapsed;
|
|
115
|
+
this.timeout = opts.timeout;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
var WebSocketError = class extends ConvilynError {
|
|
119
|
+
payload;
|
|
120
|
+
constructor(message, opts = {}) {
|
|
121
|
+
super(message);
|
|
122
|
+
this.name = "WebSocketError";
|
|
123
|
+
this.payload = opts.payload ?? null;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// src/types.ts
|
|
128
|
+
var CONVERT_JOB_TERMINAL_STATUSES = ["completed", "failed"];
|
|
129
|
+
function isConvertJobTerminal(job) {
|
|
130
|
+
return CONVERT_JOB_TERMINAL_STATUSES.includes(job.status);
|
|
131
|
+
}
|
|
132
|
+
var GOAL_JOB_TERMINAL_STATUSES = [
|
|
133
|
+
"completed",
|
|
134
|
+
"partial",
|
|
135
|
+
"failed",
|
|
136
|
+
"cancelled"
|
|
137
|
+
];
|
|
138
|
+
function isGoalJobTerminal(job) {
|
|
139
|
+
return GOAL_JOB_TERMINAL_STATUSES.includes(job.status);
|
|
140
|
+
}
|
|
141
|
+
function goalJobNeedsInput(job) {
|
|
142
|
+
return job.status === "slots_pending";
|
|
143
|
+
}
|
|
144
|
+
var GOAL_EVENT_TYPES = [
|
|
145
|
+
"tool_started",
|
|
146
|
+
"tool_finished",
|
|
147
|
+
"agent_step_started",
|
|
148
|
+
"agent_step_finished",
|
|
149
|
+
"orchestration_transition",
|
|
150
|
+
"status",
|
|
151
|
+
"progress",
|
|
152
|
+
"completed",
|
|
153
|
+
"failed",
|
|
154
|
+
"slot_needed",
|
|
155
|
+
"keepalive",
|
|
156
|
+
"agent_text",
|
|
157
|
+
"agent_text_done"
|
|
158
|
+
];
|
|
159
|
+
function isGoalEventType(value) {
|
|
160
|
+
return GOAL_EVENT_TYPES.includes(value);
|
|
161
|
+
}
|
|
162
|
+
var GOAL_EVENT_TERMINAL_TYPES = ["completed", "failed", "cancelled"];
|
|
163
|
+
function isGoalEventTerminal(event) {
|
|
164
|
+
return GOAL_EVENT_TERMINAL_TYPES.includes(event.type);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/retry.ts
|
|
168
|
+
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
169
|
+
var DEFAULT_RETRYABLE_STATUSES = /* @__PURE__ */ new Set([
|
|
170
|
+
408,
|
|
171
|
+
429,
|
|
172
|
+
500,
|
|
173
|
+
502,
|
|
174
|
+
503,
|
|
175
|
+
504
|
|
176
|
+
]);
|
|
177
|
+
function parseRetryAfter(response) {
|
|
178
|
+
const raw = response.headers.get("Retry-After") ?? response.headers.get("retry-after");
|
|
179
|
+
if (raw == null) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
const trimmed = raw.trim();
|
|
183
|
+
if (trimmed === "") {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
const seconds = Number(trimmed);
|
|
187
|
+
if (!Number.isFinite(seconds) || seconds < 0) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
return seconds;
|
|
191
|
+
}
|
|
192
|
+
function generateIdempotencyKey() {
|
|
193
|
+
return crypto.randomUUID();
|
|
194
|
+
}
|
|
195
|
+
var ExponentialBackoffRetry = class {
|
|
196
|
+
maxAttempts;
|
|
197
|
+
baseDelay;
|
|
198
|
+
maxDelay;
|
|
199
|
+
jitter;
|
|
200
|
+
retryableStatuses;
|
|
201
|
+
#rng;
|
|
202
|
+
constructor(options = {}) {
|
|
203
|
+
this.maxAttempts = options.maxAttempts ?? 5;
|
|
204
|
+
this.baseDelay = options.baseDelay ?? 0.2;
|
|
205
|
+
this.maxDelay = options.maxDelay ?? 30;
|
|
206
|
+
this.jitter = options.jitter ?? 0.25;
|
|
207
|
+
this.retryableStatuses = options.retryableStatuses ?? DEFAULT_RETRYABLE_STATUSES;
|
|
208
|
+
this.#rng = options.rng ?? Math.random;
|
|
209
|
+
}
|
|
210
|
+
shouldRetry(response, error, attempt) {
|
|
211
|
+
if (attempt >= this.maxAttempts) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
if (error != null) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
if (response == null) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
return this.retryableStatuses.has(response.status);
|
|
221
|
+
}
|
|
222
|
+
nextDelay(response, attempt) {
|
|
223
|
+
const retryAfter = response ? parseRetryAfter(response) : null;
|
|
224
|
+
if (retryAfter != null) {
|
|
225
|
+
return Math.min(retryAfter, this.maxDelay);
|
|
226
|
+
}
|
|
227
|
+
const capped = Math.min(this.baseDelay * 2 ** attempt, this.maxDelay);
|
|
228
|
+
const spread = capped * this.jitter;
|
|
229
|
+
const jittered = capped + (-spread + this.#rng() * (2 * spread));
|
|
230
|
+
return Math.max(0, jittered);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
var NoRetry = class {
|
|
234
|
+
shouldRetry() {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
nextDelay() {
|
|
238
|
+
return 0;
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// src/throttle.ts
|
|
243
|
+
var SOFT_LIMIT_HEADER = "X-Quota-State";
|
|
244
|
+
var SOFT_LIMIT_VALUE = "soft_limit";
|
|
245
|
+
var DEFAULT_MAX_RETRIES = 1;
|
|
246
|
+
var DEFAULT_MAX_SLEEP_SECONDS = 60;
|
|
247
|
+
var DEFAULT_FALLBACK_SLEEP_SECONDS = 5;
|
|
248
|
+
var AutoThrottleConfig = class {
|
|
249
|
+
maxRetries;
|
|
250
|
+
maxSleep;
|
|
251
|
+
fallbackSleep;
|
|
252
|
+
constructor(options = {}) {
|
|
253
|
+
this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
254
|
+
this.maxSleep = options.maxSleep ?? DEFAULT_MAX_SLEEP_SECONDS;
|
|
255
|
+
this.fallbackSleep = options.fallbackSleep ?? DEFAULT_FALLBACK_SLEEP_SECONDS;
|
|
256
|
+
if (this.maxRetries < 0) {
|
|
257
|
+
throw new RangeError("autoThrottle.maxRetries must be >= 0");
|
|
258
|
+
}
|
|
259
|
+
if (this.maxSleep <= 0) {
|
|
260
|
+
throw new RangeError("autoThrottle.maxSleep must be > 0");
|
|
261
|
+
}
|
|
262
|
+
if (this.fallbackSleep < 0) {
|
|
263
|
+
throw new RangeError("autoThrottle.fallbackSleep must be >= 0");
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
function resolveAutoThrottle(value) {
|
|
268
|
+
if (value == null || value === false) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
if (value === true) {
|
|
272
|
+
return new AutoThrottleConfig();
|
|
273
|
+
}
|
|
274
|
+
return value;
|
|
275
|
+
}
|
|
276
|
+
function computeQuotaSleep(error, config) {
|
|
277
|
+
const details = error.details ?? {};
|
|
278
|
+
let sleepS = sleepFromRetryAfter(details);
|
|
279
|
+
if (sleepS == null) {
|
|
280
|
+
sleepS = sleepFromResetAt(details);
|
|
281
|
+
}
|
|
282
|
+
if (sleepS == null) {
|
|
283
|
+
sleepS = config.fallbackSleep;
|
|
284
|
+
}
|
|
285
|
+
if (sleepS > config.maxSleep) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
return Math.max(0, sleepS);
|
|
289
|
+
}
|
|
290
|
+
function sleepFromRetryAfter(details) {
|
|
291
|
+
const raw = details.retry_after_seconds;
|
|
292
|
+
if (raw == null) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
const seconds = typeof raw === "number" ? raw : Number(raw);
|
|
296
|
+
return Number.isFinite(seconds) ? seconds : null;
|
|
297
|
+
}
|
|
298
|
+
function sleepFromResetAt(details) {
|
|
299
|
+
const raw = details.reset_at;
|
|
300
|
+
if (typeof raw !== "string") {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
const resetMs = Date.parse(raw);
|
|
304
|
+
if (Number.isNaN(resetMs)) {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
return (resetMs - Date.now()) / 1e3;
|
|
308
|
+
}
|
|
309
|
+
function responseHasSoftLimitHeader(headers) {
|
|
310
|
+
const raw = headers.get(SOFT_LIMIT_HEADER);
|
|
311
|
+
if (!raw) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
return raw.trim().toLowerCase() === SOFT_LIMIT_VALUE;
|
|
315
|
+
}
|
|
316
|
+
function emitSoftLimitWarning(opts) {
|
|
317
|
+
let message = `Convilyn quota soft_limit reached (source=${opts.source})`;
|
|
318
|
+
if (opts.details) {
|
|
319
|
+
message = `${message}: ${opts.details}`;
|
|
320
|
+
}
|
|
321
|
+
if (typeof process !== "undefined" && typeof process.emitWarning === "function") {
|
|
322
|
+
process.emitWarning(message);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// src/version.ts
|
|
327
|
+
var VERSION = "0.2.0";
|
|
328
|
+
|
|
329
|
+
// src/internal/auth.ts
|
|
330
|
+
var ENV_API_KEY = "CONVILYN_API_KEY";
|
|
331
|
+
var CONSUMER_KEY_PREFIX = "ck_";
|
|
332
|
+
var AUTHOR_KEY_PREFIXES = ["cvl_", "cvi_"];
|
|
333
|
+
function isAuthorKey(key) {
|
|
334
|
+
return AUTHOR_KEY_PREFIXES.some((prefix) => key.startsWith(prefix));
|
|
335
|
+
}
|
|
336
|
+
function maskKey(key) {
|
|
337
|
+
if (key.length <= 8) {
|
|
338
|
+
return "***";
|
|
339
|
+
}
|
|
340
|
+
return `${key.slice(0, 4)}\u2026${key.slice(-2)}`;
|
|
341
|
+
}
|
|
342
|
+
var ApiKey = class {
|
|
343
|
+
#key;
|
|
344
|
+
constructor(key) {
|
|
345
|
+
if (!key || !key.trim()) {
|
|
346
|
+
throw new AuthError("API key must be a non-empty string");
|
|
347
|
+
}
|
|
348
|
+
if (isAuthorKey(key)) {
|
|
349
|
+
throw new AuthError(
|
|
350
|
+
`${maskKey(key)} looks like a Convilyn Author SDK / deploy token, not a consumer API key. The consumer SDK authenticates with a "${CONSUMER_KEY_PREFIX}" key \u2014 mint one under Settings \u2192 API. (Author tokens publish workflows / tools; they do not call the data-plane API.)`
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
this.#key = key;
|
|
354
|
+
}
|
|
355
|
+
headers() {
|
|
356
|
+
return { Authorization: `Bearer ${this.#key}` };
|
|
357
|
+
}
|
|
358
|
+
bearerToken() {
|
|
359
|
+
return this.#key;
|
|
360
|
+
}
|
|
361
|
+
/** Masked so the raw key never leaks via string interpolation. */
|
|
362
|
+
toString() {
|
|
363
|
+
return `ApiKey(${maskKey(this.#key)})`;
|
|
364
|
+
}
|
|
365
|
+
/** Node's `util.inspect` hook so `console.log(apiKey)` is masked too. */
|
|
366
|
+
[/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
|
|
367
|
+
return this.toString();
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
function resolveAuth(apiKey, options = {}) {
|
|
371
|
+
const env = options.env ?? (typeof process !== "undefined" ? process.env : {});
|
|
372
|
+
const resolved = apiKey ?? env[ENV_API_KEY];
|
|
373
|
+
if (!resolved || !resolved.trim()) {
|
|
374
|
+
throw new AuthError(
|
|
375
|
+
`No API key provided. Pass { apiKey } or set the ${ENV_API_KEY} environment variable.`
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
return new ApiKey(resolved);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// src/internal/http.ts
|
|
382
|
+
var DEFAULT_BASE_URL = "https://api.convilyn.corenovus.com";
|
|
383
|
+
var DEFAULT_TIMEOUT = 30;
|
|
384
|
+
function requireHttps(url) {
|
|
385
|
+
let scheme;
|
|
386
|
+
try {
|
|
387
|
+
scheme = new URL(url).protocol.replace(/:$/, "").toLowerCase();
|
|
388
|
+
} catch {
|
|
389
|
+
scheme = "";
|
|
390
|
+
}
|
|
391
|
+
if (scheme !== "https") {
|
|
392
|
+
throw new Error(
|
|
393
|
+
`Refusing to dial non-https URL (scheme=${JSON.stringify(scheme)}). The Convilyn SDK only follows https presigned URLs.`
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
var PLAN_REQUIRED_CODES = /* @__PURE__ */ new Set([
|
|
398
|
+
"TIER_REQUIRED",
|
|
399
|
+
"PRO_TIER_REQUIRED",
|
|
400
|
+
"BUSINESS_TIER_REQUIRED",
|
|
401
|
+
"ENTERPRISE_TIER_REQUIRED"
|
|
402
|
+
]);
|
|
403
|
+
var realSleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1e3));
|
|
404
|
+
var HttpClient = class {
|
|
405
|
+
#auth;
|
|
406
|
+
#baseUrl;
|
|
407
|
+
#timeout;
|
|
408
|
+
#userAgent;
|
|
409
|
+
#retryPolicy;
|
|
410
|
+
#idempotencyEnabled;
|
|
411
|
+
#autoThrottle;
|
|
412
|
+
#fetch;
|
|
413
|
+
#sleep;
|
|
414
|
+
constructor(options) {
|
|
415
|
+
this.#auth = options.auth;
|
|
416
|
+
this.#baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
417
|
+
this.#timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
418
|
+
this.#userAgent = options.userAgent ?? `convilyn-js/${VERSION}`;
|
|
419
|
+
this.#retryPolicy = options.retryPolicy ?? new ExponentialBackoffRetry();
|
|
420
|
+
this.#idempotencyEnabled = options.idempotencyEnabled ?? true;
|
|
421
|
+
this.#autoThrottle = options.autoThrottle ?? null;
|
|
422
|
+
if (options.fetch) {
|
|
423
|
+
this.#fetch = options.fetch;
|
|
424
|
+
} else if (typeof fetch !== "undefined") {
|
|
425
|
+
this.#fetch = (url, init) => fetch(url, init);
|
|
426
|
+
} else {
|
|
427
|
+
throw new Error("No global fetch available; pass options.fetch (Node 18+ or a polyfill)");
|
|
428
|
+
}
|
|
429
|
+
this.#sleep = options.sleep ?? realSleep;
|
|
430
|
+
}
|
|
431
|
+
get baseUrl() {
|
|
432
|
+
return this.#baseUrl;
|
|
433
|
+
}
|
|
434
|
+
/** Read-only handle to the auth strategy (used by the WS transport). */
|
|
435
|
+
get auth() {
|
|
436
|
+
return this.#auth;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Issue a request, applying the retry policy on transient failures and
|
|
440
|
+
* raising a typed error on a non-2xx final response.
|
|
441
|
+
*/
|
|
442
|
+
async request(method, path, options = {}) {
|
|
443
|
+
const response = await this.#requestWithQuotaThrottle(
|
|
444
|
+
method,
|
|
445
|
+
path,
|
|
446
|
+
options,
|
|
447
|
+
this.#autoThrottle ? this.#autoThrottle.maxRetries : 0
|
|
448
|
+
);
|
|
449
|
+
maybeEmitSoftLimit(response);
|
|
450
|
+
return response;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Like {@link request} but never raises on HTTP status — returns the final
|
|
454
|
+
* response so callers (the `convilyn api` CLI escape hatch) can inspect it.
|
|
455
|
+
*/
|
|
456
|
+
async rawRequest(method, path, options = {}) {
|
|
457
|
+
const { response } = await this.#doRequestWithRetry(method, path, options);
|
|
458
|
+
maybeEmitSoftLimit(response);
|
|
459
|
+
return response;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Run {@link #doRequestWithRetry}, re-issuing on a 402 {@link QuotaExceededError}
|
|
463
|
+
* up to `quotaAttemptsRemaining` times, sleeping {@link computeQuotaSleep}
|
|
464
|
+
* between attempts. Transport-level retries (5xx / 429 / 408) are handled
|
|
465
|
+
* inside `#doRequestWithRetry`; this shim targets the 402 quota envelope only.
|
|
466
|
+
*/
|
|
467
|
+
async #requestWithQuotaThrottle(method, path, options, quotaAttemptsRemaining) {
|
|
468
|
+
const { response, attempts } = await this.#doRequestWithRetry(method, path, options);
|
|
469
|
+
if (response.status < 400) {
|
|
470
|
+
return response;
|
|
471
|
+
}
|
|
472
|
+
const decoded = attempts > 1 ? await toRetryExhausted(response, attempts) : await decodeError(response);
|
|
473
|
+
if (this.#autoThrottle != null && quotaAttemptsRemaining > 0 && decoded instanceof QuotaExceededError) {
|
|
474
|
+
const sleepSeconds = computeQuotaSleep(decoded, this.#autoThrottle);
|
|
475
|
+
if (sleepSeconds != null) {
|
|
476
|
+
await this.#sleep(sleepSeconds);
|
|
477
|
+
return this.#requestWithQuotaThrottle(method, path, options, quotaAttemptsRemaining - 1);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
throw decoded;
|
|
481
|
+
}
|
|
482
|
+
async #doRequestWithRetry(method, path, options) {
|
|
483
|
+
const headers = this.#defaultHeaders();
|
|
484
|
+
if (options.headers) {
|
|
485
|
+
Object.assign(headers, options.headers);
|
|
486
|
+
}
|
|
487
|
+
this.#applyIdempotencyKey(method.toUpperCase(), headers);
|
|
488
|
+
let attempt = 0;
|
|
489
|
+
for (; ; ) {
|
|
490
|
+
const response = await this.#fetchOnce(method, path, headers, options);
|
|
491
|
+
if (response.status < 400) {
|
|
492
|
+
return { response, attempts: attempt + 1 };
|
|
493
|
+
}
|
|
494
|
+
const view = { status: response.status, headers: response.headers };
|
|
495
|
+
if (this.#retryPolicy.shouldRetry(view, null, attempt)) {
|
|
496
|
+
await this.#sleep(this.#retryPolicy.nextDelay(view, attempt));
|
|
497
|
+
attempt += 1;
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
return { response, attempts: attempt + 1 };
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
#defaultHeaders() {
|
|
504
|
+
return {
|
|
505
|
+
"User-Agent": this.#userAgent,
|
|
506
|
+
Accept: "application/json",
|
|
507
|
+
...this.#auth.headers()
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
#applyIdempotencyKey(method, headers) {
|
|
511
|
+
if (!this.#idempotencyEnabled) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (!MUTATING_METHODS.has(method)) {
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if ("Idempotency-Key" in headers) {
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
headers["Idempotency-Key"] = generateIdempotencyKey();
|
|
521
|
+
}
|
|
522
|
+
async #fetchOnce(method, path, headers, options) {
|
|
523
|
+
const url = this.#buildUrl(path, options.params);
|
|
524
|
+
if (options.json !== void 0) {
|
|
525
|
+
headers["Content-Type"] = "application/json";
|
|
526
|
+
}
|
|
527
|
+
const init = { method, headers };
|
|
528
|
+
if (options.json !== void 0) {
|
|
529
|
+
init.body = JSON.stringify(options.json);
|
|
530
|
+
} else if (options.content !== void 0) {
|
|
531
|
+
init.body = options.content;
|
|
532
|
+
}
|
|
533
|
+
const controller = new AbortController();
|
|
534
|
+
const timer = setTimeout(() => controller.abort(), this.#timeout * 1e3);
|
|
535
|
+
init.signal = controller.signal;
|
|
536
|
+
try {
|
|
537
|
+
return await this.#fetch(url, init);
|
|
538
|
+
} finally {
|
|
539
|
+
clearTimeout(timer);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
#buildUrl(path, params) {
|
|
543
|
+
const absolute = /^https?:\/\//i.test(path);
|
|
544
|
+
const full = absolute ? path : `${this.#baseUrl}${path.startsWith("/") ? "" : "/"}${path}`;
|
|
545
|
+
if (!params) {
|
|
546
|
+
return full;
|
|
547
|
+
}
|
|
548
|
+
const url = new URL(full);
|
|
549
|
+
for (const [key, value] of Object.entries(params)) {
|
|
550
|
+
if (value == null) {
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
url.searchParams.set(key, String(value));
|
|
554
|
+
}
|
|
555
|
+
return url.toString();
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* PUT to an external absolute URL (e.g. a presigned upload URL). Sends NO
|
|
559
|
+
* Convilyn auth (the URL carries its own signature) and is NOT retried.
|
|
560
|
+
*/
|
|
561
|
+
async externalPut(url, { content, headers }) {
|
|
562
|
+
requireHttps(url);
|
|
563
|
+
const response = await this.#fetch(url, {
|
|
564
|
+
method: "PUT",
|
|
565
|
+
body: content,
|
|
566
|
+
headers: headers ?? {}
|
|
567
|
+
});
|
|
568
|
+
if (response.status >= 400) {
|
|
569
|
+
throw new S3UploadError(
|
|
570
|
+
response.status,
|
|
571
|
+
`S3_HTTP_${response.status}`,
|
|
572
|
+
response.statusText || "External upload failed",
|
|
573
|
+
{ url }
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
return response;
|
|
577
|
+
}
|
|
578
|
+
/** GET an external absolute URL (e.g. a presigned download). No auth. */
|
|
579
|
+
async externalGet(url, { headers } = {}) {
|
|
580
|
+
requireHttps(url);
|
|
581
|
+
const response = await this.#fetch(url, { method: "GET", headers: headers ?? {} });
|
|
582
|
+
if (response.status >= 400) {
|
|
583
|
+
throw new APIError(
|
|
584
|
+
response.status,
|
|
585
|
+
`DOWNLOAD_HTTP_${response.status}`,
|
|
586
|
+
response.statusText || "External download failed",
|
|
587
|
+
{ url }
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
return response;
|
|
591
|
+
}
|
|
592
|
+
/** No persistent connection pool with `fetch`; provided for API symmetry. */
|
|
593
|
+
async close() {
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
function maybeEmitSoftLimit(response) {
|
|
597
|
+
if (responseHasSoftLimitHeader(response.headers)) {
|
|
598
|
+
emitSoftLimitWarning({ source: response.url || "request" });
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function isRecord(value) {
|
|
602
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
603
|
+
}
|
|
604
|
+
function asString(value) {
|
|
605
|
+
return typeof value === "string" ? value : void 0;
|
|
606
|
+
}
|
|
607
|
+
function coerceNumber(value) {
|
|
608
|
+
if (value == null) {
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
if (typeof value === "number") {
|
|
612
|
+
return Number.isFinite(value) ? value : null;
|
|
613
|
+
}
|
|
614
|
+
if (typeof value === "string") {
|
|
615
|
+
const parsed = Number(value);
|
|
616
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
617
|
+
}
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
async function readJson(response) {
|
|
621
|
+
try {
|
|
622
|
+
const payload = await response.json();
|
|
623
|
+
return isRecord(payload) ? payload : {};
|
|
624
|
+
} catch {
|
|
625
|
+
return {};
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
function flattenErrorEnvelope(payload) {
|
|
629
|
+
for (const key of ["detail", "error"]) {
|
|
630
|
+
const nested = payload[key];
|
|
631
|
+
if (isRecord(nested)) {
|
|
632
|
+
return nested;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return payload;
|
|
636
|
+
}
|
|
637
|
+
async function decodeError(response) {
|
|
638
|
+
const status = response.status;
|
|
639
|
+
const inner = flattenErrorEnvelope(await readJson(response));
|
|
640
|
+
const code = asString(inner.code) ?? `HTTP_${status}`;
|
|
641
|
+
const message = asString(inner.message) ?? (response.statusText || "Request failed");
|
|
642
|
+
const details = isRecord(inner.details) ? inner.details : void 0;
|
|
643
|
+
if (status === 429) {
|
|
644
|
+
return new RateLimitError(status, code, message, details);
|
|
645
|
+
}
|
|
646
|
+
if (status === 402) {
|
|
647
|
+
const upgradeUrl = asString(inner.upgrade_url ?? inner.upgradeUrl ?? inner.topUp ?? inner.top_up) ?? null;
|
|
648
|
+
if (PLAN_REQUIRED_CODES.has(code)) {
|
|
649
|
+
return new PlanRequiredError(status, code, message, details, { upgradeUrl });
|
|
650
|
+
}
|
|
651
|
+
if (code === "insufficient_balance" || code === "QUOTA_EXCEEDED") {
|
|
652
|
+
return new QuotaExceededError(status, code, message, details, {
|
|
653
|
+
costCredits: coerceNumber(inner.costCredits ?? inner.cost_credits),
|
|
654
|
+
balanceCredits: coerceNumber(inner.balanceCredits ?? inner.balance_credits),
|
|
655
|
+
topUpUrl: upgradeUrl
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return new APIError(status, code, message, details);
|
|
660
|
+
}
|
|
661
|
+
async function toRetryExhausted(response, attemptCount) {
|
|
662
|
+
const status = response.status;
|
|
663
|
+
const payload = await readJson(response);
|
|
664
|
+
const code = asString(payload.code) ?? `HTTP_${status}`;
|
|
665
|
+
const message = asString(payload.message) ?? (response.statusText || "Request failed");
|
|
666
|
+
const details = isRecord(payload.details) ? payload.details : void 0;
|
|
667
|
+
return new RetryExhaustedError(status, code, message, details, { attemptCount });
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// src/internal/parsers.ts
|
|
671
|
+
var MICRO_U_PER_CREDIT = 1e4;
|
|
672
|
+
function microUToCredits(value) {
|
|
673
|
+
return Number(value ?? 0) / MICRO_U_PER_CREDIT;
|
|
674
|
+
}
|
|
675
|
+
function toDate(value) {
|
|
676
|
+
return new Date(value);
|
|
677
|
+
}
|
|
678
|
+
function toDateOrNull(value) {
|
|
679
|
+
return value == null ? null : toDate(value);
|
|
680
|
+
}
|
|
681
|
+
function isRecord2(value) {
|
|
682
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
683
|
+
}
|
|
684
|
+
function parseFile(wire) {
|
|
685
|
+
return {
|
|
686
|
+
fileId: String(wire.fileId),
|
|
687
|
+
filename: String(wire.fileName),
|
|
688
|
+
size: Number(wire.fileSize),
|
|
689
|
+
contentType: String(wire.mimeType),
|
|
690
|
+
createdAt: toDate(wire.createdAt),
|
|
691
|
+
jobId: wire.jobId ?? null,
|
|
692
|
+
isInput: wire.isInput == null ? true : Boolean(wire.isInput)
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
function parseResultFile(wire) {
|
|
696
|
+
return {
|
|
697
|
+
filename: String(wire.filename),
|
|
698
|
+
size: Number(wire.size),
|
|
699
|
+
mimetype: String(wire.mimetype),
|
|
700
|
+
url: String(wire.url)
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
function parseJobError(wire) {
|
|
704
|
+
return { code: String(wire.code), message: String(wire.message) };
|
|
705
|
+
}
|
|
706
|
+
function parseConvertJob(wire) {
|
|
707
|
+
const resultFiles = wire.resultFiles;
|
|
708
|
+
const error = wire.error;
|
|
709
|
+
return {
|
|
710
|
+
jobId: String(wire.jobId),
|
|
711
|
+
status: wire.status,
|
|
712
|
+
processorType: String(wire.processorType),
|
|
713
|
+
progress: Number(wire.progress ?? 0),
|
|
714
|
+
progressMessage: wire.progressMessage ?? null,
|
|
715
|
+
resultFiles: Array.isArray(resultFiles) ? resultFiles.map((row) => parseResultFile(row)) : null,
|
|
716
|
+
error: error != null ? parseJobError(error) : null,
|
|
717
|
+
retryCount: Number(wire.retryCount ?? 0),
|
|
718
|
+
createdAt: toDate(wire.createdAt),
|
|
719
|
+
updatedAt: toDate(wire.updatedAt),
|
|
720
|
+
startedAt: toDateOrNull(wire.startedAt),
|
|
721
|
+
completedAt: toDateOrNull(wire.completedAt),
|
|
722
|
+
estimatedDuration: wire.estimatedDuration ?? null
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
function parsePendingSlot(wire) {
|
|
726
|
+
return {
|
|
727
|
+
slotId: String(wire.slotId),
|
|
728
|
+
slotType: String(wire.slotType),
|
|
729
|
+
question: String(wire.question),
|
|
730
|
+
options: Array.isArray(wire.options) ? wire.options : null,
|
|
731
|
+
required: wire.required == null ? true : Boolean(wire.required),
|
|
732
|
+
isDisambiguation: Boolean(wire.isDisambiguation ?? false),
|
|
733
|
+
suggestedValue: wire.suggestedValue ?? null,
|
|
734
|
+
suggestedConfidence: wire.suggestedConfidence ?? null
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
function parseGoalJob(wire) {
|
|
738
|
+
return {
|
|
739
|
+
jobSpecId: String(wire.jobSpecId),
|
|
740
|
+
status: wire.status,
|
|
741
|
+
progress: Number(wire.progress ?? 0),
|
|
742
|
+
itemVersion: wire.itemVersion ?? null,
|
|
743
|
+
attemptId: wire.attemptId ?? null,
|
|
744
|
+
goalText: wire.goalText ?? null,
|
|
745
|
+
fileIds: Array.isArray(wire.fileIds) ? wire.fileIds.map(String) : [],
|
|
746
|
+
pendingSlots: Array.isArray(wire.pendingSlots) ? wire.pendingSlots.map((slot) => parsePendingSlot(slot)) : [],
|
|
747
|
+
filledSlots: isRecord2(wire.filledSlots) ? wire.filledSlots : {},
|
|
748
|
+
pendingInterrupts: Array.isArray(wire.pendingInterrupts) ? wire.pendingInterrupts : [],
|
|
749
|
+
agentMessage: wire.agentMessage ?? null,
|
|
750
|
+
errorMessage: wire.errorMessage ?? null,
|
|
751
|
+
errorCode: wire.errorCode ?? null,
|
|
752
|
+
createdAt: toDate(wire.createdAt),
|
|
753
|
+
updatedAt: toDate(wire.updatedAt),
|
|
754
|
+
startedAt: toDateOrNull(wire.startedAt),
|
|
755
|
+
completedAt: toDateOrNull(wire.completedAt)
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
function parseGoalEvent(wire) {
|
|
759
|
+
return {
|
|
760
|
+
type: wire.type,
|
|
761
|
+
schemaVersion: Number(wire.schemaVersion ?? 0),
|
|
762
|
+
jobSpecId: String(wire.jobSpecId),
|
|
763
|
+
emittedAt: toDate(wire.emittedAt),
|
|
764
|
+
seq: Number(wire.seq ?? 0),
|
|
765
|
+
data: isRecord2(wire.data) ? wire.data : {}
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
function parseWorkflowStats(wire) {
|
|
769
|
+
const stats = isRecord2(wire) ? wire : {};
|
|
770
|
+
return {
|
|
771
|
+
runCount: Number(stats.runCount ?? 0),
|
|
772
|
+
forkCount: Number(stats.forkCount ?? 0),
|
|
773
|
+
likeCount: Number(stats.likeCount ?? 0)
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
function parseWorkflow(wire) {
|
|
777
|
+
return {
|
|
778
|
+
workflowId: String(wire.workflowId),
|
|
779
|
+
ownerId: wire.ownerId ?? null,
|
|
780
|
+
specId: String(wire.specId),
|
|
781
|
+
sourceSpecId: wire.sourceSpecId ?? null,
|
|
782
|
+
sourceType: wire.sourceType ?? null,
|
|
783
|
+
name: String(wire.name),
|
|
784
|
+
description: wire.description ?? null,
|
|
785
|
+
visibility: wire.visibility,
|
|
786
|
+
tags: Array.isArray(wire.tags) ? wire.tags.map(String) : [],
|
|
787
|
+
stats: parseWorkflowStats(wire.stats),
|
|
788
|
+
itemVersion: Number(wire.itemVersion ?? 0)
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
function parseWorkflowSummary(wire) {
|
|
792
|
+
return {
|
|
793
|
+
workflowId: String(wire.workflowId),
|
|
794
|
+
specId: String(wire.specId),
|
|
795
|
+
ownerId: wire.ownerId ?? null,
|
|
796
|
+
name: String(wire.name),
|
|
797
|
+
description: wire.description ?? null,
|
|
798
|
+
visibility: wire.visibility,
|
|
799
|
+
tags: Array.isArray(wire.tags) ? wire.tags.map(String) : [],
|
|
800
|
+
stats: parseWorkflowStats(wire.stats)
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
function parseWorkflowSearchPage(wire) {
|
|
804
|
+
const items = wire.items;
|
|
805
|
+
return {
|
|
806
|
+
items: Array.isArray(items) ? items.map((row) => parseWorkflowSummary(row)) : [],
|
|
807
|
+
cursor: wire.cursor ?? null
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
function parseLikeResponse(wire) {
|
|
811
|
+
return {
|
|
812
|
+
liked: Boolean(wire.liked),
|
|
813
|
+
likeCount: Number(wire.likeCount ?? 0)
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
function parseToolCostEstimate(wire) {
|
|
817
|
+
return {
|
|
818
|
+
toolName: String(wire.toolName),
|
|
819
|
+
perInvocationCredits: microUToCredits(wire.perInvocationMicroU)
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
function parseQuotaCheck(wire) {
|
|
823
|
+
return {
|
|
824
|
+
state: wire.state,
|
|
825
|
+
tier: wire.tier,
|
|
826
|
+
estimatedCredits: microUToCredits(wire.estimatedMicroU),
|
|
827
|
+
thresholdCredits: microUToCredits(wire.thresholdMicroU),
|
|
828
|
+
upgradeUrl: wire.upgradeUrl ?? null
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
function parseCostEstimate(wire) {
|
|
832
|
+
const tools = wire.tools;
|
|
833
|
+
return {
|
|
834
|
+
estimatedCredits: microUToCredits(wire.estimatedMicroU),
|
|
835
|
+
estimatedUsd: Number(wire.estimatedUsd ?? 0),
|
|
836
|
+
estimatedTotalCredits: microUToCredits(wire.estimatedTotalMicroU),
|
|
837
|
+
estimatedMinCredits: microUToCredits(wire.estimatedMinMicroU),
|
|
838
|
+
estimatedMaxCredits: microUToCredits(wire.estimatedMaxMicroU),
|
|
839
|
+
tools: Array.isArray(tools) ? tools.map((row) => parseToolCostEstimate(row)) : [],
|
|
840
|
+
quotaCheck: parseQuotaCheck(isRecord2(wire.quotaCheck) ? wire.quotaCheck : {})
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
function planFromTier(tier) {
|
|
844
|
+
return { tier };
|
|
845
|
+
}
|
|
846
|
+
function parseUsageHistoryEntry(wire) {
|
|
847
|
+
return {
|
|
848
|
+
metric: String(wire.metric),
|
|
849
|
+
periodStart: toDate(wire.period_start ?? wire.periodStart),
|
|
850
|
+
periodEnd: toDate(wire.period_end ?? wire.periodEnd),
|
|
851
|
+
used: Number(wire.used ?? 0),
|
|
852
|
+
limit: wire.limit ?? null
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// src/resources/account.ts
|
|
857
|
+
var Account = class {
|
|
858
|
+
#http;
|
|
859
|
+
constructor(http) {
|
|
860
|
+
this.#http = http;
|
|
861
|
+
}
|
|
862
|
+
/** Estimate a workflow's cost (credits) + check the verdict against the plan. */
|
|
863
|
+
async getQuota(options = {}) {
|
|
864
|
+
const body = { toolNames: options.tools ?? [] };
|
|
865
|
+
if (options.maxIterations != null) {
|
|
866
|
+
body.maxIterations = options.maxIterations;
|
|
867
|
+
}
|
|
868
|
+
const response = await this.#http.request("POST", "/api/v1/workflows/cost-preview", {
|
|
869
|
+
json: body
|
|
870
|
+
});
|
|
871
|
+
return parseCostEstimate(await response.json());
|
|
872
|
+
}
|
|
873
|
+
/** Return the caller's current billing plan (derived from the cost-preview tier). */
|
|
874
|
+
async getPlan() {
|
|
875
|
+
const estimate = await this.getQuota({ tools: [], maxIterations: 1 });
|
|
876
|
+
return planFromTier(estimate.quotaCheck.tier);
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* List the caller's past usage periods (one row per metric+period). `since`
|
|
880
|
+
* filters client-side to entries whose `periodStart` is on or after it
|
|
881
|
+
* (the backend does not honour a query param today).
|
|
882
|
+
*/
|
|
883
|
+
async usageHistory(options = {}) {
|
|
884
|
+
const response = await this.#http.request("GET", "/api/v1/payment/usage/history");
|
|
885
|
+
const rows = await response.json();
|
|
886
|
+
const entries = Array.isArray(rows) ? rows.map((row) => parseUsageHistoryEntry(row)) : [];
|
|
887
|
+
if (options.since == null) {
|
|
888
|
+
return entries;
|
|
889
|
+
}
|
|
890
|
+
const since = options.since;
|
|
891
|
+
return entries.filter((entry) => entry.periodStart >= since);
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
// src/resources/convert.ts
|
|
896
|
+
var DEFAULT_POLL_INTERVAL = 1;
|
|
897
|
+
var DEFAULT_POLL_TIMEOUT = 300;
|
|
898
|
+
var MAX_POLL_INTERVAL = 5;
|
|
899
|
+
var STALE_PROGRESS_BACKOFF_AFTER = 3;
|
|
900
|
+
var BACKOFF_FACTOR = 1.5;
|
|
901
|
+
var SOURCE_FORMAT_BY_SUFFIX = {
|
|
902
|
+
docx: "docx",
|
|
903
|
+
doc: "doc",
|
|
904
|
+
pdf: "pdf",
|
|
905
|
+
pptx: "pptx",
|
|
906
|
+
ppt: "ppt",
|
|
907
|
+
xlsx: "xlsx",
|
|
908
|
+
xls: "xls",
|
|
909
|
+
txt: "txt",
|
|
910
|
+
rtf: "rtf",
|
|
911
|
+
html: "html",
|
|
912
|
+
htm: "html",
|
|
913
|
+
md: "md",
|
|
914
|
+
odt: "odt"
|
|
915
|
+
};
|
|
916
|
+
function inferSourceFormat(filename) {
|
|
917
|
+
const dot = filename.lastIndexOf(".");
|
|
918
|
+
if (dot < 0) {
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
return SOURCE_FORMAT_BY_SUFFIX[filename.slice(dot + 1).toLowerCase()] ?? null;
|
|
922
|
+
}
|
|
923
|
+
var realSleep2 = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1e3));
|
|
924
|
+
var Convert = class {
|
|
925
|
+
#http;
|
|
926
|
+
#sleep;
|
|
927
|
+
#now;
|
|
928
|
+
constructor(http, options = {}) {
|
|
929
|
+
this.#http = http;
|
|
930
|
+
this.#sleep = options.sleep ?? realSleep2;
|
|
931
|
+
this.#now = options.now ?? (() => Date.now());
|
|
932
|
+
}
|
|
933
|
+
/** Create a conversion job and return immediately (queued/processing). */
|
|
934
|
+
async create(options) {
|
|
935
|
+
const { fileId, sourceFormat } = this.#resolveSource(options);
|
|
936
|
+
const params = {
|
|
937
|
+
source_file_id: fileId,
|
|
938
|
+
source_format: sourceFormat,
|
|
939
|
+
target_format: options.targetFormat,
|
|
940
|
+
quality: options.quality ?? "standard"
|
|
941
|
+
};
|
|
942
|
+
if (options.pageRange != null) {
|
|
943
|
+
params.page_range = options.pageRange;
|
|
944
|
+
}
|
|
945
|
+
const response = await this.#http.request("POST", "/api/v1/jobs", {
|
|
946
|
+
json: { processor_type: "document_conversion", params }
|
|
947
|
+
});
|
|
948
|
+
return parseConvertJob(await response.json());
|
|
949
|
+
}
|
|
950
|
+
/** Fetch the current state of a job (one-shot poll). */
|
|
951
|
+
async retrieve(jobId) {
|
|
952
|
+
return this.#pollOnce(jobId);
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Poll until the job reaches a terminal status, the timeout expires, or the
|
|
956
|
+
* backend reports failure.
|
|
957
|
+
*
|
|
958
|
+
* @throws {@link JobFailedError} when the terminal status is `failed`.
|
|
959
|
+
* @throws {@link JobTimeoutError} when `timeout` elapses first.
|
|
960
|
+
*/
|
|
961
|
+
async wait(jobId, options = {}) {
|
|
962
|
+
return this.#waitLoop(
|
|
963
|
+
jobId,
|
|
964
|
+
options.timeout ?? DEFAULT_POLL_TIMEOUT,
|
|
965
|
+
options.pollInterval ?? DEFAULT_POLL_INTERVAL
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
/** Shortcut: {@link create} then {@link wait}. */
|
|
969
|
+
async createAndWait(options) {
|
|
970
|
+
const job = await this.create(options);
|
|
971
|
+
return this.wait(job.jobId, { timeout: options.timeout, pollInterval: options.pollInterval });
|
|
972
|
+
}
|
|
973
|
+
/** Presigned download URL for the first result file. */
|
|
974
|
+
async downloadUrl(job) {
|
|
975
|
+
const resolved = await this.#ensureJob(job);
|
|
976
|
+
const first = resolved.resultFiles?.[0];
|
|
977
|
+
if (!first) {
|
|
978
|
+
throw new JobFailedError({
|
|
979
|
+
jobId: resolved.jobId,
|
|
980
|
+
processorType: resolved.processorType,
|
|
981
|
+
code: "NO_RESULT_FILES",
|
|
982
|
+
message: "Job is terminal but no result files were produced"
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
return first.url;
|
|
986
|
+
}
|
|
987
|
+
/** Download the first result file to `to` (Node) and return that path. */
|
|
988
|
+
async downloadTo(job, { to }) {
|
|
989
|
+
const url = await this.downloadUrl(job);
|
|
990
|
+
const { lstat, writeFile } = await import('fs/promises');
|
|
991
|
+
const st = await lstat(to).catch(() => null);
|
|
992
|
+
if (st?.isSymbolicLink()) {
|
|
993
|
+
throw new Error(
|
|
994
|
+
`Refusing to write to ${JSON.stringify(to)}: target is a symlink. Remove it or choose a non-symlink destination.`
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
const response = await this.#http.externalGet(url);
|
|
998
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
999
|
+
await writeFile(to, bytes);
|
|
1000
|
+
return to;
|
|
1001
|
+
}
|
|
1002
|
+
// ── private ────────────────────────────────────────────────────
|
|
1003
|
+
#resolveSource(options) {
|
|
1004
|
+
const { file, fileId, sourceFormat } = options;
|
|
1005
|
+
if (file != null && fileId != null) {
|
|
1006
|
+
throw new TypeError("create() accepts either `file` or `fileId`, not both");
|
|
1007
|
+
}
|
|
1008
|
+
if (file != null) {
|
|
1009
|
+
const inferred = sourceFormat ?? inferSourceFormat(file.filename);
|
|
1010
|
+
if (inferred == null) {
|
|
1011
|
+
throw new Error(
|
|
1012
|
+
`Could not infer sourceFormat from filename ${JSON.stringify(file.filename)}; pass sourceFormat explicitly`
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
return { fileId: file.fileId, sourceFormat: inferred };
|
|
1016
|
+
}
|
|
1017
|
+
if (fileId != null) {
|
|
1018
|
+
if (sourceFormat == null) {
|
|
1019
|
+
throw new Error("sourceFormat is required when only fileId is provided");
|
|
1020
|
+
}
|
|
1021
|
+
return { fileId, sourceFormat };
|
|
1022
|
+
}
|
|
1023
|
+
throw new TypeError("create() requires either `file` or `fileId`");
|
|
1024
|
+
}
|
|
1025
|
+
async #pollOnce(jobId) {
|
|
1026
|
+
const response = await this.#http.request("GET", `/api/v1/jobs/${jobId}`);
|
|
1027
|
+
return parseConvertJob(await response.json());
|
|
1028
|
+
}
|
|
1029
|
+
async #waitLoop(jobId, timeout, initialInterval) {
|
|
1030
|
+
const start = this.#now() / 1e3;
|
|
1031
|
+
let interval = initialInterval;
|
|
1032
|
+
let staleCount = 0;
|
|
1033
|
+
let lastProgress = -1;
|
|
1034
|
+
for (; ; ) {
|
|
1035
|
+
const job = await this.#pollOnce(jobId);
|
|
1036
|
+
if (isConvertJobTerminal(job)) {
|
|
1037
|
+
return this.#finalise(job);
|
|
1038
|
+
}
|
|
1039
|
+
if (job.progress === lastProgress) {
|
|
1040
|
+
staleCount += 1;
|
|
1041
|
+
if (staleCount >= STALE_PROGRESS_BACKOFF_AFTER) {
|
|
1042
|
+
interval = Math.min(interval * BACKOFF_FACTOR, MAX_POLL_INTERVAL);
|
|
1043
|
+
staleCount = 0;
|
|
1044
|
+
}
|
|
1045
|
+
} else {
|
|
1046
|
+
lastProgress = job.progress;
|
|
1047
|
+
staleCount = 0;
|
|
1048
|
+
}
|
|
1049
|
+
const elapsed = this.#now() / 1e3 - start;
|
|
1050
|
+
if (elapsed + interval > timeout) {
|
|
1051
|
+
throw new JobTimeoutError({ jobId, elapsed, timeout });
|
|
1052
|
+
}
|
|
1053
|
+
await this.#sleep(interval);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
#finalise(job) {
|
|
1057
|
+
if (job.status === "failed") {
|
|
1058
|
+
const err = job.error;
|
|
1059
|
+
throw new JobFailedError({
|
|
1060
|
+
jobId: job.jobId,
|
|
1061
|
+
processorType: job.processorType,
|
|
1062
|
+
code: err ? err.code : "UNKNOWN",
|
|
1063
|
+
message: err ? err.message : "Job failed without a structured error"
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
return job;
|
|
1067
|
+
}
|
|
1068
|
+
async #ensureJob(job) {
|
|
1069
|
+
return typeof job === "string" ? this.#pollOnce(job) : job;
|
|
1070
|
+
}
|
|
1071
|
+
};
|
|
1072
|
+
|
|
1073
|
+
// src/resources/files.ts
|
|
1074
|
+
var DEFAULT_CONTENT_TYPE = "application/octet-stream";
|
|
1075
|
+
var MIME_BY_SUFFIX = {
|
|
1076
|
+
pdf: "application/pdf",
|
|
1077
|
+
doc: "application/msword",
|
|
1078
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1079
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
1080
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
1081
|
+
xls: "application/vnd.ms-excel",
|
|
1082
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
1083
|
+
txt: "text/plain",
|
|
1084
|
+
csv: "text/csv",
|
|
1085
|
+
html: "text/html",
|
|
1086
|
+
htm: "text/html",
|
|
1087
|
+
md: "text/markdown",
|
|
1088
|
+
json: "application/json",
|
|
1089
|
+
xml: "application/xml",
|
|
1090
|
+
png: "image/png",
|
|
1091
|
+
jpg: "image/jpeg",
|
|
1092
|
+
jpeg: "image/jpeg",
|
|
1093
|
+
gif: "image/gif",
|
|
1094
|
+
webp: "image/webp",
|
|
1095
|
+
svg: "image/svg+xml",
|
|
1096
|
+
mp4: "video/mp4",
|
|
1097
|
+
mp3: "audio/mpeg",
|
|
1098
|
+
wav: "audio/wav",
|
|
1099
|
+
zip: "application/zip"
|
|
1100
|
+
};
|
|
1101
|
+
function basename(path) {
|
|
1102
|
+
const parts = path.split(/[\\/]/);
|
|
1103
|
+
return parts[parts.length - 1] || path;
|
|
1104
|
+
}
|
|
1105
|
+
function guessContentType(filename) {
|
|
1106
|
+
const dot = filename.lastIndexOf(".");
|
|
1107
|
+
if (dot < 0) {
|
|
1108
|
+
return DEFAULT_CONTENT_TYPE;
|
|
1109
|
+
}
|
|
1110
|
+
const suffix = filename.slice(dot + 1).toLowerCase();
|
|
1111
|
+
return MIME_BY_SUFFIX[suffix] ?? DEFAULT_CONTENT_TYPE;
|
|
1112
|
+
}
|
|
1113
|
+
function toBytes(content) {
|
|
1114
|
+
if (typeof content === "string") {
|
|
1115
|
+
return new TextEncoder().encode(content);
|
|
1116
|
+
}
|
|
1117
|
+
if (content instanceof ArrayBuffer) {
|
|
1118
|
+
return new Uint8Array(content);
|
|
1119
|
+
}
|
|
1120
|
+
return content;
|
|
1121
|
+
}
|
|
1122
|
+
var Files = class {
|
|
1123
|
+
#http;
|
|
1124
|
+
constructor(http) {
|
|
1125
|
+
this.#http = http;
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Upload a file and return its {@link File} record. Supply exactly one of
|
|
1129
|
+
* `path` (Node) or `content` (bytes/string).
|
|
1130
|
+
*/
|
|
1131
|
+
async upload(options) {
|
|
1132
|
+
const { path, content } = options;
|
|
1133
|
+
if (path == null && content == null) {
|
|
1134
|
+
throw new TypeError("upload() requires either `path` or `content`");
|
|
1135
|
+
}
|
|
1136
|
+
if (path != null && content != null) {
|
|
1137
|
+
throw new TypeError("upload() accepts either `path` or `content`, not both");
|
|
1138
|
+
}
|
|
1139
|
+
let bytes;
|
|
1140
|
+
let filename = options.filename;
|
|
1141
|
+
if (path != null) {
|
|
1142
|
+
const { readFile } = await import('fs/promises');
|
|
1143
|
+
const buffer = await readFile(path);
|
|
1144
|
+
bytes = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
1145
|
+
filename ??= basename(path);
|
|
1146
|
+
} else {
|
|
1147
|
+
bytes = toBytes(content);
|
|
1148
|
+
}
|
|
1149
|
+
if (!filename) {
|
|
1150
|
+
throw new Error("filename is required when uploading raw content");
|
|
1151
|
+
}
|
|
1152
|
+
const contentType = options.contentType ?? guessContentType(filename);
|
|
1153
|
+
return this.#uploadWithBody(bytes, filename, contentType);
|
|
1154
|
+
}
|
|
1155
|
+
async #uploadWithBody(body, filename, contentType) {
|
|
1156
|
+
const size = body.byteLength;
|
|
1157
|
+
const presign = await this.#presign(filename, size, contentType);
|
|
1158
|
+
await this.#http.externalPut(presign.uploadUrl, {
|
|
1159
|
+
content: body,
|
|
1160
|
+
headers: { "Content-Type": contentType }
|
|
1161
|
+
});
|
|
1162
|
+
const confirm = await this.#confirm(presign.fileId, filename, size, contentType, presign.s3Key);
|
|
1163
|
+
return parseFile(confirm);
|
|
1164
|
+
}
|
|
1165
|
+
async #presign(filename, size, contentType) {
|
|
1166
|
+
const response = await this.#http.request("POST", "/api/v1/upload/presign", {
|
|
1167
|
+
json: { fileName: filename, fileSize: size, contentType }
|
|
1168
|
+
});
|
|
1169
|
+
const body = await response.json();
|
|
1170
|
+
return {
|
|
1171
|
+
uploadUrl: String(body.uploadUrl),
|
|
1172
|
+
fileId: String(body.fileId),
|
|
1173
|
+
s3Key: String(body.s3Key)
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
async #confirm(fileId, filename, size, contentType, s3Key) {
|
|
1177
|
+
const response = await this.#http.request("POST", "/api/v1/upload/confirm", {
|
|
1178
|
+
json: { fileId, fileName: filename, fileSize: size, contentType, s3Key }
|
|
1179
|
+
});
|
|
1180
|
+
return await response.json();
|
|
1181
|
+
}
|
|
1182
|
+
};
|
|
1183
|
+
|
|
1184
|
+
// src/internal/ws.ts
|
|
1185
|
+
var ENV_WS_URL = "CONVILYN_WS_URL";
|
|
1186
|
+
var WebSocketTransport = class {
|
|
1187
|
+
#WebSocketImpl;
|
|
1188
|
+
#ws = null;
|
|
1189
|
+
#queue = [];
|
|
1190
|
+
#waiters = [];
|
|
1191
|
+
#ended = false;
|
|
1192
|
+
#failure = null;
|
|
1193
|
+
constructor(options = {}) {
|
|
1194
|
+
const impl = options.WebSocketImpl ?? (typeof WebSocket !== "undefined" ? WebSocket : void 0);
|
|
1195
|
+
if (!impl) {
|
|
1196
|
+
throw new Error(
|
|
1197
|
+
"No global WebSocket available; pass a WebSocket implementation (Node 22+ exposes one globally, or use the `ws` package)."
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
this.#WebSocketImpl = impl;
|
|
1201
|
+
}
|
|
1202
|
+
connect(url) {
|
|
1203
|
+
return new Promise((resolve, reject) => {
|
|
1204
|
+
let settled = false;
|
|
1205
|
+
let socket;
|
|
1206
|
+
try {
|
|
1207
|
+
socket = new this.#WebSocketImpl(url);
|
|
1208
|
+
} catch (err) {
|
|
1209
|
+
reject(err);
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
this.#ws = socket;
|
|
1213
|
+
socket.addEventListener("open", () => {
|
|
1214
|
+
settled = true;
|
|
1215
|
+
resolve();
|
|
1216
|
+
});
|
|
1217
|
+
socket.addEventListener("message", (event) => this.#onMessage(event));
|
|
1218
|
+
socket.addEventListener("error", () => {
|
|
1219
|
+
const err = new Error("WebSocket connection error");
|
|
1220
|
+
this.#failure = err;
|
|
1221
|
+
this.#flushWaiters();
|
|
1222
|
+
if (!settled) {
|
|
1223
|
+
settled = true;
|
|
1224
|
+
reject(err);
|
|
1225
|
+
}
|
|
1226
|
+
});
|
|
1227
|
+
socket.addEventListener("close", () => {
|
|
1228
|
+
this.#ended = true;
|
|
1229
|
+
this.#flushWaiters();
|
|
1230
|
+
});
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
send(payload) {
|
|
1234
|
+
if (!this.#ws) {
|
|
1235
|
+
return Promise.reject(new Error("WebSocketTransport.send called before connect"));
|
|
1236
|
+
}
|
|
1237
|
+
this.#ws.send(payload);
|
|
1238
|
+
return Promise.resolve();
|
|
1239
|
+
}
|
|
1240
|
+
recv() {
|
|
1241
|
+
const buffered = this.#queue.shift();
|
|
1242
|
+
if (buffered !== void 0) {
|
|
1243
|
+
return Promise.resolve(buffered);
|
|
1244
|
+
}
|
|
1245
|
+
if (this.#failure != null) {
|
|
1246
|
+
return Promise.reject(this.#failure);
|
|
1247
|
+
}
|
|
1248
|
+
if (this.#ended) {
|
|
1249
|
+
return Promise.reject(new Error("WebSocket closed"));
|
|
1250
|
+
}
|
|
1251
|
+
return new Promise((resolve, reject) => {
|
|
1252
|
+
this.#waiters.push({ resolve, reject });
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
close() {
|
|
1256
|
+
if (this.#ws) {
|
|
1257
|
+
try {
|
|
1258
|
+
this.#ws.close();
|
|
1259
|
+
} catch {
|
|
1260
|
+
}
|
|
1261
|
+
this.#ws = null;
|
|
1262
|
+
}
|
|
1263
|
+
return Promise.resolve();
|
|
1264
|
+
}
|
|
1265
|
+
#onMessage(event) {
|
|
1266
|
+
const data = typeof event.data === "string" ? event.data : event.data instanceof ArrayBuffer ? new TextDecoder().decode(event.data) : String(event.data);
|
|
1267
|
+
const waiter = this.#waiters.shift();
|
|
1268
|
+
if (waiter) {
|
|
1269
|
+
waiter.resolve(data);
|
|
1270
|
+
} else {
|
|
1271
|
+
this.#queue.push(data);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
#flushWaiters() {
|
|
1275
|
+
const reason = this.#failure ?? new Error("WebSocket closed");
|
|
1276
|
+
while (this.#waiters.length > 0) {
|
|
1277
|
+
this.#waiters.shift().reject(reason);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
};
|
|
1281
|
+
function resolveWsUrl(options) {
|
|
1282
|
+
const env = options.env ?? (typeof process !== "undefined" ? process.env : {});
|
|
1283
|
+
for (const candidate of [options.explicit, options.fallback, env[ENV_WS_URL]]) {
|
|
1284
|
+
if (candidate) {
|
|
1285
|
+
return candidate;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
throw new Error(
|
|
1289
|
+
"No WebSocket URL configured. Pass { wsUrl } to events(), set it on the client constructor, or set the CONVILYN_WS_URL environment variable."
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
function buildWsConnectUrl(baseUrl, token) {
|
|
1293
|
+
const separator = baseUrl.includes("?") ? "&" : "?";
|
|
1294
|
+
return `${baseUrl}${separator}token=${token}`;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// src/resources/goals.ts
|
|
1298
|
+
var DEFAULT_POLL_INTERVAL2 = 1;
|
|
1299
|
+
var DEFAULT_POLL_TIMEOUT2 = 300;
|
|
1300
|
+
var MAX_POLL_INTERVAL2 = 5;
|
|
1301
|
+
var STALE_PROGRESS_BACKOFF_AFTER2 = 3;
|
|
1302
|
+
var BACKOFF_FACTOR2 = 1.5;
|
|
1303
|
+
var realSleep3 = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1e3));
|
|
1304
|
+
function errorMessage(error) {
|
|
1305
|
+
return error instanceof Error ? error.message : String(error);
|
|
1306
|
+
}
|
|
1307
|
+
var Goals = class {
|
|
1308
|
+
#http;
|
|
1309
|
+
#wsUrl;
|
|
1310
|
+
#wsTransportFactory;
|
|
1311
|
+
#sleep;
|
|
1312
|
+
#now;
|
|
1313
|
+
constructor(http, options = {}) {
|
|
1314
|
+
this.#http = http;
|
|
1315
|
+
this.#wsUrl = options.wsUrl ?? null;
|
|
1316
|
+
this.#wsTransportFactory = options.wsTransportFactory ?? (() => new WebSocketTransport());
|
|
1317
|
+
this.#sleep = options.sleep ?? realSleep3;
|
|
1318
|
+
this.#now = options.now ?? (() => Date.now());
|
|
1319
|
+
}
|
|
1320
|
+
/** Create a goal job and return its initial {@link GoalJob} state. */
|
|
1321
|
+
async start(options) {
|
|
1322
|
+
validateStartInputs(options);
|
|
1323
|
+
const payload = { fileIds: options.files ?? [] };
|
|
1324
|
+
if (options.workflowId != null) {
|
|
1325
|
+
payload.workflowId = options.workflowId;
|
|
1326
|
+
}
|
|
1327
|
+
if (options.goalText != null) {
|
|
1328
|
+
payload.goalText = options.goalText;
|
|
1329
|
+
}
|
|
1330
|
+
if (options.slots != null) {
|
|
1331
|
+
payload.slotAnswers = Object.entries(options.slots).map(([slotId, value]) => ({
|
|
1332
|
+
slotId,
|
|
1333
|
+
value
|
|
1334
|
+
}));
|
|
1335
|
+
}
|
|
1336
|
+
if (options.llmConfigId != null) {
|
|
1337
|
+
payload.llmConfigId = options.llmConfigId;
|
|
1338
|
+
}
|
|
1339
|
+
const response = await this.#http.request("POST", "/api/v1/jobs/goal", { json: payload });
|
|
1340
|
+
return parseGoalJob(await response.json());
|
|
1341
|
+
}
|
|
1342
|
+
/** Fetch the current state of a goal job. */
|
|
1343
|
+
async retrieve(jobSpecId) {
|
|
1344
|
+
return this.#pollOnce(jobSpecId);
|
|
1345
|
+
}
|
|
1346
|
+
/**
|
|
1347
|
+
* Poll until the job reaches a terminal state OR stops for HITL
|
|
1348
|
+
* (`slots_pending`). `failed` raises {@link GoalJobFailedError}; an elapsed
|
|
1349
|
+
* deadline raises {@link GoalJobTimeoutError}.
|
|
1350
|
+
*/
|
|
1351
|
+
async wait(jobSpecId, options = {}) {
|
|
1352
|
+
return this.#waitLoop(
|
|
1353
|
+
jobSpecId,
|
|
1354
|
+
options.timeout ?? DEFAULT_POLL_TIMEOUT2,
|
|
1355
|
+
options.pollInterval ?? DEFAULT_POLL_INTERVAL2
|
|
1356
|
+
);
|
|
1357
|
+
}
|
|
1358
|
+
/** Shortcut: {@link start} then {@link wait}. */
|
|
1359
|
+
async run(options) {
|
|
1360
|
+
const job = await this.start(options);
|
|
1361
|
+
return this.wait(job.jobSpecId, {
|
|
1362
|
+
timeout: options.timeout,
|
|
1363
|
+
pollInterval: options.pollInterval
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
/** Answer a single slot the agent is waiting on (sugar over {@link fillSlots}). */
|
|
1367
|
+
async fillSlot(jobSpecId, options) {
|
|
1368
|
+
return this.fillSlots(
|
|
1369
|
+
jobSpecId,
|
|
1370
|
+
{ [options.slotId]: options.value },
|
|
1371
|
+
{
|
|
1372
|
+
expectedVersion: options.expectedVersion
|
|
1373
|
+
}
|
|
1374
|
+
);
|
|
1375
|
+
}
|
|
1376
|
+
/** Answer one or more slots in a single PATCH (`{slotId: value}` → wire list). */
|
|
1377
|
+
async fillSlots(jobSpecId, answers, options = {}) {
|
|
1378
|
+
const entries = Object.entries(answers);
|
|
1379
|
+
if (entries.length === 0) {
|
|
1380
|
+
throw new Error("answers must contain at least one slot answer");
|
|
1381
|
+
}
|
|
1382
|
+
const body = {
|
|
1383
|
+
answers: entries.map(([slotId, value]) => ({ slotId, value }))
|
|
1384
|
+
};
|
|
1385
|
+
if (options.expectedVersion != null) {
|
|
1386
|
+
body.expectedVersion = options.expectedVersion;
|
|
1387
|
+
}
|
|
1388
|
+
const response = await this.#http.request("PATCH", `/api/v1/jobs/goal/${jobSpecId}/slots`, {
|
|
1389
|
+
json: body
|
|
1390
|
+
});
|
|
1391
|
+
return parseGoalJob(await response.json());
|
|
1392
|
+
}
|
|
1393
|
+
/** Confirm a slots-filled job, queueing it for execution. */
|
|
1394
|
+
async confirm(jobSpecId, options = {}) {
|
|
1395
|
+
const json = options.expectedVersion != null ? { expectedVersion: options.expectedVersion } : void 0;
|
|
1396
|
+
await this.#http.request("POST", `/api/v1/jobs/goal/${jobSpecId}/confirm`, { json });
|
|
1397
|
+
return this.#pollOnce(jobSpecId);
|
|
1398
|
+
}
|
|
1399
|
+
/** Cancel a running or queued job. */
|
|
1400
|
+
async cancel(jobSpecId) {
|
|
1401
|
+
await this.#http.request("POST", `/api/v1/jobs/goal/${jobSpecId}/cancel`);
|
|
1402
|
+
return this.#pollOnce(jobSpecId);
|
|
1403
|
+
}
|
|
1404
|
+
/** Retry a failed job (resume the thread or start a fresh rerun). */
|
|
1405
|
+
async retry(jobSpecId, options = {}) {
|
|
1406
|
+
const body = { rerunMode: options.rerunMode ?? "retry_same_thread" };
|
|
1407
|
+
if (options.reason != null) {
|
|
1408
|
+
body.reason = options.reason;
|
|
1409
|
+
}
|
|
1410
|
+
await this.#http.request("POST", `/api/v1/jobs/goal/${jobSpecId}/retry`, { json: body });
|
|
1411
|
+
return this.#pollOnce(jobSpecId);
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Stream goal execution events for a job. Opens a WebSocket, subscribes,
|
|
1415
|
+
* then yields each {@link GoalEvent} in order; the iterator ends (and the
|
|
1416
|
+
* socket closes) on a terminal event. No auto-reconnect — the backend does
|
|
1417
|
+
* not replay missed events, so a silent reconnect would hide gaps.
|
|
1418
|
+
*
|
|
1419
|
+
* **WebSocket streaming is not available with a `ck_` consumer key in v1** —
|
|
1420
|
+
* the backend WS gateway does not authenticate `ck_` keys, so the connection
|
|
1421
|
+
* is denied. Use {@link wait} polling (standard HTTP auth, works today) for
|
|
1422
|
+
* the supported real-time path. This method is wired and ready for when the
|
|
1423
|
+
* gateway gains `ck_` support.
|
|
1424
|
+
*
|
|
1425
|
+
* @throws {@link WebSocketError} on connect/subscribe failure, a dropped
|
|
1426
|
+
* stream, or an unparseable/unrecognised envelope.
|
|
1427
|
+
*/
|
|
1428
|
+
async *events(jobSpecId, options = {}) {
|
|
1429
|
+
const url = buildWsConnectUrl(
|
|
1430
|
+
resolveWsUrl({ explicit: options.wsUrl, fallback: this.#wsUrl }),
|
|
1431
|
+
this.#http.auth.bearerToken()
|
|
1432
|
+
);
|
|
1433
|
+
const transport = this.#wsTransportFactory();
|
|
1434
|
+
try {
|
|
1435
|
+
try {
|
|
1436
|
+
await transport.connect(url);
|
|
1437
|
+
await transport.send(JSON.stringify({ action: "subscribe", jobSpecId }));
|
|
1438
|
+
} catch (error) {
|
|
1439
|
+
throw new WebSocketError(
|
|
1440
|
+
`Failed to open event stream for job ${jobSpecId}: ${errorMessage(error)}. Note: the WebSocket gateway does not accept ck_ consumer keys in v1 \u2014 use wait() polling (HTTP auth) instead.`
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1443
|
+
for (; ; ) {
|
|
1444
|
+
let raw;
|
|
1445
|
+
try {
|
|
1446
|
+
raw = await transport.recv();
|
|
1447
|
+
} catch (error) {
|
|
1448
|
+
throw new WebSocketError(
|
|
1449
|
+
`Event stream dropped for job ${jobSpecId}: ${errorMessage(error)}`
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1452
|
+
const event = parseEvent(raw);
|
|
1453
|
+
yield event;
|
|
1454
|
+
if (isGoalEventTerminal(event)) {
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
} finally {
|
|
1459
|
+
await transport.close();
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
// ── private ────────────────────────────────────────────────────
|
|
1463
|
+
async #pollOnce(jobSpecId) {
|
|
1464
|
+
const response = await this.#http.request("GET", `/api/v1/jobs/goal/${jobSpecId}`);
|
|
1465
|
+
return parseGoalJob(await response.json());
|
|
1466
|
+
}
|
|
1467
|
+
async #waitLoop(jobSpecId, timeout, initialInterval) {
|
|
1468
|
+
const start = this.#now() / 1e3;
|
|
1469
|
+
let interval = initialInterval;
|
|
1470
|
+
let staleCount = 0;
|
|
1471
|
+
let lastProgress = -1;
|
|
1472
|
+
for (; ; ) {
|
|
1473
|
+
const job = await this.#pollOnce(jobSpecId);
|
|
1474
|
+
if (isGoalJobTerminal(job)) {
|
|
1475
|
+
return this.#finalise(job);
|
|
1476
|
+
}
|
|
1477
|
+
if (goalJobNeedsInput(job)) {
|
|
1478
|
+
return job;
|
|
1479
|
+
}
|
|
1480
|
+
if (job.progress === lastProgress) {
|
|
1481
|
+
staleCount += 1;
|
|
1482
|
+
if (staleCount >= STALE_PROGRESS_BACKOFF_AFTER2) {
|
|
1483
|
+
interval = Math.min(interval * BACKOFF_FACTOR2, MAX_POLL_INTERVAL2);
|
|
1484
|
+
staleCount = 0;
|
|
1485
|
+
}
|
|
1486
|
+
} else {
|
|
1487
|
+
lastProgress = job.progress;
|
|
1488
|
+
staleCount = 0;
|
|
1489
|
+
}
|
|
1490
|
+
const elapsed = this.#now() / 1e3 - start;
|
|
1491
|
+
if (elapsed + interval > timeout) {
|
|
1492
|
+
throw new GoalJobTimeoutError({ jobSpecId, elapsed, timeout });
|
|
1493
|
+
}
|
|
1494
|
+
await this.#sleep(interval);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
#finalise(job) {
|
|
1498
|
+
if (job.status === "failed") {
|
|
1499
|
+
throw new GoalJobFailedError({
|
|
1500
|
+
jobSpecId: job.jobSpecId,
|
|
1501
|
+
code: job.errorCode,
|
|
1502
|
+
message: job.errorMessage
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
return job;
|
|
1506
|
+
}
|
|
1507
|
+
};
|
|
1508
|
+
function validateStartInputs(options) {
|
|
1509
|
+
const { workflowId, goalText, files } = options;
|
|
1510
|
+
if (workflowId == null && goalText == null) {
|
|
1511
|
+
throw new TypeError("start() requires either `workflowId` or `goalText`");
|
|
1512
|
+
}
|
|
1513
|
+
if (workflowId != null && goalText != null) {
|
|
1514
|
+
throw new TypeError("start() accepts either `workflowId` or `goalText`, not both");
|
|
1515
|
+
}
|
|
1516
|
+
if (workflowId == null && (!files || files.length === 0)) {
|
|
1517
|
+
throw new Error("files is required when `workflowId` is not provided");
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
function parseEvent(raw) {
|
|
1521
|
+
let payload;
|
|
1522
|
+
try {
|
|
1523
|
+
payload = JSON.parse(raw);
|
|
1524
|
+
} catch (error) {
|
|
1525
|
+
throw new WebSocketError(`Server sent non-JSON message: ${errorMessage(error)}`, {
|
|
1526
|
+
payload: raw
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
if (typeof payload !== "object" || payload === null || typeof payload.type !== "string" || typeof payload.jobSpecId !== "string" || !isGoalEventType(payload.type)) {
|
|
1530
|
+
throw new WebSocketError("Server sent an unrecognised event envelope", { payload: raw });
|
|
1531
|
+
}
|
|
1532
|
+
return parseGoalEvent(payload);
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// src/resources/workflows.ts
|
|
1536
|
+
var Workflows = class {
|
|
1537
|
+
#http;
|
|
1538
|
+
constructor(http) {
|
|
1539
|
+
this.#http = http;
|
|
1540
|
+
}
|
|
1541
|
+
/** Browse the public community listing (one page + a `cursor`). */
|
|
1542
|
+
async search(options = {}) {
|
|
1543
|
+
const params = {
|
|
1544
|
+
sort: options.sort ?? "recent",
|
|
1545
|
+
limit: options.limit ?? 20
|
|
1546
|
+
};
|
|
1547
|
+
if (options.tag != null) {
|
|
1548
|
+
params.tag = options.tag;
|
|
1549
|
+
}
|
|
1550
|
+
if (options.cursor != null) {
|
|
1551
|
+
params.cursor = options.cursor;
|
|
1552
|
+
}
|
|
1553
|
+
const response = await this.#http.request("GET", "/api/v1/workflows/community", { params });
|
|
1554
|
+
return parseWorkflowSearchPage(await response.json());
|
|
1555
|
+
}
|
|
1556
|
+
/** Fetch a single workflow by id. */
|
|
1557
|
+
async get(workflowId) {
|
|
1558
|
+
const response = await this.#http.request("GET", `/api/v1/workflows/${workflowId}`);
|
|
1559
|
+
return parseWorkflow(await response.json());
|
|
1560
|
+
}
|
|
1561
|
+
/** Fork a curated/user source into a new private workflow (Pro). */
|
|
1562
|
+
async fork(options) {
|
|
1563
|
+
const body = { sourceSpecId: options.sourceSpecId };
|
|
1564
|
+
if (options.name != null) {
|
|
1565
|
+
body.name = options.name;
|
|
1566
|
+
}
|
|
1567
|
+
const response = await this.#http.request("POST", "/api/v1/workflows/fork", { json: body });
|
|
1568
|
+
return parseWorkflow(await response.json());
|
|
1569
|
+
}
|
|
1570
|
+
/** Flip an owned workflow's visibility to `public` (Pro; optimistic lock). */
|
|
1571
|
+
async publish(workflowId, options) {
|
|
1572
|
+
return this.patch(workflowId, { itemVersion: options.itemVersion, visibility: "public" });
|
|
1573
|
+
}
|
|
1574
|
+
/** Partial update of an owned workflow (the generic PATCH escape hatch). */
|
|
1575
|
+
async patch(workflowId, options) {
|
|
1576
|
+
const body = { itemVersion: options.itemVersion };
|
|
1577
|
+
if (options.name != null) {
|
|
1578
|
+
body.name = options.name;
|
|
1579
|
+
}
|
|
1580
|
+
if (options.description != null) {
|
|
1581
|
+
body.description = options.description;
|
|
1582
|
+
}
|
|
1583
|
+
if (options.visibility != null) {
|
|
1584
|
+
body.visibility = options.visibility;
|
|
1585
|
+
}
|
|
1586
|
+
if (options.tags != null) {
|
|
1587
|
+
body.tags = [...options.tags];
|
|
1588
|
+
}
|
|
1589
|
+
const response = await this.#http.request("PATCH", `/api/v1/workflows/${workflowId}`, {
|
|
1590
|
+
json: body
|
|
1591
|
+
});
|
|
1592
|
+
return parseWorkflow(await response.json());
|
|
1593
|
+
}
|
|
1594
|
+
/** Toggle the caller's like on a public workflow (idempotent toggle). */
|
|
1595
|
+
async like(workflowId) {
|
|
1596
|
+
const response = await this.#http.request("POST", `/api/v1/workflows/${workflowId}/like`);
|
|
1597
|
+
return parseLikeResponse(await response.json());
|
|
1598
|
+
}
|
|
1599
|
+
};
|
|
1600
|
+
|
|
1601
|
+
// src/client.ts
|
|
1602
|
+
var Convilyn = class {
|
|
1603
|
+
files;
|
|
1604
|
+
convert;
|
|
1605
|
+
goals;
|
|
1606
|
+
workflows;
|
|
1607
|
+
account;
|
|
1608
|
+
#http;
|
|
1609
|
+
constructor(options = {}) {
|
|
1610
|
+
const auth = options.auth ?? resolveAuth(options.apiKey);
|
|
1611
|
+
this.#http = new HttpClient({
|
|
1612
|
+
auth,
|
|
1613
|
+
baseUrl: options.baseUrl,
|
|
1614
|
+
timeout: options.timeout,
|
|
1615
|
+
retryPolicy: resolveRetryPolicy(options.maxRetries, options.retryPolicy),
|
|
1616
|
+
idempotencyEnabled: !options.disableIdempotency,
|
|
1617
|
+
autoThrottle: resolveAutoThrottle(options.autoThrottle)
|
|
1618
|
+
});
|
|
1619
|
+
this.files = new Files(this.#http);
|
|
1620
|
+
this.convert = new Convert(this.#http);
|
|
1621
|
+
this.goals = new Goals(this.#http, {
|
|
1622
|
+
wsUrl: options.wsUrl,
|
|
1623
|
+
wsTransportFactory: options.wsTransportFactory
|
|
1624
|
+
});
|
|
1625
|
+
this.workflows = new Workflows(this.#http);
|
|
1626
|
+
this.account = new Account(this.#http);
|
|
1627
|
+
}
|
|
1628
|
+
get baseUrl() {
|
|
1629
|
+
return this.#http.baseUrl;
|
|
1630
|
+
}
|
|
1631
|
+
async close() {
|
|
1632
|
+
await this.#http.close();
|
|
1633
|
+
}
|
|
1634
|
+
};
|
|
1635
|
+
function resolveRetryPolicy(maxRetries, retryPolicy) {
|
|
1636
|
+
if (retryPolicy != null) {
|
|
1637
|
+
return retryPolicy;
|
|
1638
|
+
}
|
|
1639
|
+
if (maxRetries == null) {
|
|
1640
|
+
return new ExponentialBackoffRetry();
|
|
1641
|
+
}
|
|
1642
|
+
if (maxRetries <= 0) {
|
|
1643
|
+
return new NoRetry();
|
|
1644
|
+
}
|
|
1645
|
+
return new ExponentialBackoffRetry({ maxAttempts: maxRetries });
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
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, isGoalEventTerminal, isGoalEventType, isGoalJobTerminal, resolveAuth };
|
|
1649
|
+
//# sourceMappingURL=chunk-UJHZKIP6.js.map
|
|
1650
|
+
//# sourceMappingURL=chunk-UJHZKIP6.js.map
|