@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/dist/cli.js ADDED
@@ -0,0 +1,1019 @@
1
+ #!/usr/bin/env node
2
+ import { VERSION, AuthError, APIError, guessContentType, Convilyn, JobFailedError, JobTimeoutError, HttpClient, resolveAuth, isGoalEventTerminal, WebSocketError, DEFAULT_BASE_URL, GoalJobFailedError, GoalJobTimeoutError, PlanRequiredError, QuotaExceededError } from './chunk-UJHZKIP6.js';
3
+ import { Command, Option } from 'commander';
4
+ import { statSync, readFileSync, writeFileSync } from 'fs';
5
+ import { basename, parse, join } from 'path';
6
+
7
+ // src/cli/context.ts
8
+ var ENV_API_KEY = "CONVILYN_API_KEY";
9
+ var ENV_BASE_URL = "CONVILYN_BASE_URL";
10
+ var ENV_WS_URL = "CONVILYN_WS_URL";
11
+ function buildClient(env) {
12
+ return new Convilyn({
13
+ apiKey: env[ENV_API_KEY],
14
+ baseUrl: env[ENV_BASE_URL],
15
+ wsUrl: env[ENV_WS_URL]
16
+ });
17
+ }
18
+ function buildHttpClient(env) {
19
+ return new HttpClient({
20
+ auth: resolveAuth(env[ENV_API_KEY], { env }),
21
+ baseUrl: env[ENV_BASE_URL]
22
+ });
23
+ }
24
+ function processWriters() {
25
+ return {
26
+ stdout: (text) => process.stdout.write(text),
27
+ stderr: (text) => process.stderr.write(text)
28
+ };
29
+ }
30
+ function printError(writers, env, prefix, error) {
31
+ const detail = env.CONVILYN_DEBUG ? JSON.stringify(error instanceof Error ? { name: error.name, message: error.message } : error) : error instanceof Error ? error.message : String(error);
32
+ writers.stderr(`${prefix}: ${detail}
33
+ `);
34
+ }
35
+
36
+ // src/cli/exitCodes.ts
37
+ var EXIT_OK = 0;
38
+ var EXIT_USAGE = 1;
39
+ var EXIT_API_ERROR = 2;
40
+ var EXIT_JOB_FAILED = 3;
41
+
42
+ // src/cli/output.ts
43
+ var GLYPHS = {
44
+ upload: "\u2191",
45
+ create: "\u25B6",
46
+ wait: "\u2026",
47
+ download: "\u2193",
48
+ ok: "\u2713",
49
+ warn: "!",
50
+ error: "\u2717"
51
+ };
52
+ function sortedJson(value, indent) {
53
+ return JSON.stringify(
54
+ value,
55
+ (_key, val) => {
56
+ if (val && typeof val === "object" && !Array.isArray(val)) {
57
+ const record = val;
58
+ return Object.fromEntries(
59
+ Object.keys(record).sort().map((key) => [key, record[key]])
60
+ );
61
+ }
62
+ return val;
63
+ },
64
+ indent
65
+ );
66
+ }
67
+ function humaniseSize(sizeBytes) {
68
+ if (sizeBytes == null || sizeBytes === 0) {
69
+ return "0 B";
70
+ }
71
+ let remaining = sizeBytes;
72
+ for (const unit of ["B", "KiB", "MiB", "GiB"]) {
73
+ if (remaining < 1024) {
74
+ return unit === "B" ? `${Math.round(remaining)} ${unit}` : `${remaining.toFixed(1)} ${unit}`;
75
+ }
76
+ remaining /= 1024;
77
+ }
78
+ return `${remaining.toFixed(1)} TiB`;
79
+ }
80
+ function formatEvent(kind, fields) {
81
+ if (kind === "upload" && "filename" in fields) {
82
+ const size = fields.size_bytes;
83
+ const sizeStr = size ? ` (${humaniseSize(size)})` : "";
84
+ return `Uploading ${String(fields.filename)}${sizeStr}`;
85
+ }
86
+ if (kind === "create" && "job_id" in fields) {
87
+ return `Created job ${String(fields.job_id)}`;
88
+ }
89
+ if (kind === "wait") {
90
+ const progress = fields.progress;
91
+ return progress != null ? `Converting\u2026 ${String(progress)}%` : "Waiting for job to finish";
92
+ }
93
+ if (kind === "download" && "path" in fields) {
94
+ return `Downloaded ${String(fields.path)}`;
95
+ }
96
+ const pairs = Object.entries(fields).map(([key, value]) => `${key}=${String(value)}`).join(", ");
97
+ return `${kind}: ${pairs}`;
98
+ }
99
+ function formatSummary(payload) {
100
+ const outputPath = payload.output_path;
101
+ const outputSize = payload.output_size_bytes;
102
+ if (outputPath && outputSize != null) {
103
+ return `\u2713 ${outputPath} (${humaniseSize(outputSize)})`;
104
+ }
105
+ if (outputPath) {
106
+ return `\u2713 ${outputPath}`;
107
+ }
108
+ const status = payload.status;
109
+ if (status) {
110
+ return `\u2713 status=${status}`;
111
+ }
112
+ return "\u2713 done";
113
+ }
114
+ var HumanRenderer = class {
115
+ #writers;
116
+ constructor(writers) {
117
+ this.#writers = writers;
118
+ }
119
+ event(kind, fields = {}) {
120
+ const glyph = GLYPHS[kind] ?? "\u2022";
121
+ const message = fields.message ?? formatEvent(kind, fields);
122
+ this.#writers.stderr(`${glyph} ${message}
123
+ `);
124
+ }
125
+ final(payload) {
126
+ const summary = payload.summary ?? formatSummary(payload);
127
+ this.#writers.stdout(`${summary}
128
+ `);
129
+ }
130
+ };
131
+ var JsonRenderer = class {
132
+ #writers;
133
+ constructor(writers) {
134
+ this.#writers = writers;
135
+ }
136
+ event(_kind, _fields) {
137
+ }
138
+ final(payload) {
139
+ this.#writers.stdout(`${sortedJson(payload)}
140
+ `);
141
+ }
142
+ };
143
+ function makeRenderer(jsonOutput, writers) {
144
+ return jsonOutput ? new JsonRenderer(writers) : new HumanRenderer(writers);
145
+ }
146
+
147
+ // src/cli/account.ts
148
+ async function runAccountPlan(options, deps2) {
149
+ const renderer = makeRenderer(Boolean(options.json), deps2.writers);
150
+ return runAccountAction(deps2, renderer, async (client) => {
151
+ const plan = await client.account.getPlan();
152
+ return { command: "account.plan", tier: plan.tier, summary: `tier=${plan.tier}` };
153
+ });
154
+ }
155
+ async function runAccountQuota(options, deps2) {
156
+ const renderer = makeRenderer(Boolean(options.json), deps2.writers);
157
+ return runAccountAction(deps2, renderer, async (client) => {
158
+ const estimate = await client.account.getQuota({
159
+ tools: options.tools ?? [],
160
+ maxIterations: options.maxIter
161
+ });
162
+ const quota = estimate.quotaCheck;
163
+ return {
164
+ command: "account.quota",
165
+ tier: quota.tier,
166
+ state: quota.state,
167
+ estimated_credits: estimate.estimatedCredits,
168
+ estimated_usd: estimate.estimatedUsd,
169
+ threshold_credits: quota.thresholdCredits,
170
+ upgrade_url: quota.upgradeUrl,
171
+ summary: `tier=${quota.tier} state=${quota.state} cost=${estimate.estimatedCredits}/${quota.thresholdCredits} credits`
172
+ };
173
+ });
174
+ }
175
+ async function runAccountAction(deps2, renderer, action) {
176
+ let client;
177
+ try {
178
+ client = (deps2.clientFactory ?? (() => buildClient(deps2.env)))();
179
+ } catch (error) {
180
+ printError(deps2.writers, deps2.env, "Authentication failed", error);
181
+ return EXIT_USAGE;
182
+ }
183
+ try {
184
+ const payload = await action(client);
185
+ renderer.event(String(payload.command), { summary: payload.summary });
186
+ renderer.final(payload);
187
+ return EXIT_OK;
188
+ } catch (error) {
189
+ if (error instanceof PlanRequiredError) {
190
+ deps2.writers.stderr(
191
+ `Plan required: ${error.message} (upgrade: ${error.upgradeUrl ?? "see /pricing"})
192
+ `
193
+ );
194
+ return EXIT_USAGE;
195
+ }
196
+ if (error instanceof QuotaExceededError) {
197
+ deps2.writers.stderr(
198
+ `Quota exceeded: ${error.message} (needs ${error.costCredits ?? "?"} credits, balance ${error.balanceCredits ?? "?"})
199
+ `
200
+ );
201
+ return EXIT_USAGE;
202
+ }
203
+ if (error instanceof AuthError) {
204
+ deps2.writers.stderr(`Authentication failed: ${error.message}
205
+ `);
206
+ return EXIT_USAGE;
207
+ }
208
+ if (error instanceof APIError) {
209
+ deps2.writers.stderr(`API error: ${error.message}
210
+ `);
211
+ return EXIT_API_ERROR;
212
+ }
213
+ throw error;
214
+ } finally {
215
+ try {
216
+ await client.close();
217
+ } catch {
218
+ }
219
+ }
220
+ }
221
+ function parseHeaders(items) {
222
+ const result = {};
223
+ for (const item of items) {
224
+ if (!item.includes(":")) {
225
+ throw new Error(`Bad --header value ${JSON.stringify(item)}; expected "Name: Value"`);
226
+ }
227
+ const index = item.indexOf(":");
228
+ result[item.slice(0, index).trim()] = item.slice(index + 1).trim();
229
+ }
230
+ return result;
231
+ }
232
+ function parseQuery(items) {
233
+ if (items.length === 0) {
234
+ return void 0;
235
+ }
236
+ const result = {};
237
+ for (const item of items) {
238
+ if (!item.includes("=")) {
239
+ throw new Error(`Bad --query value ${JSON.stringify(item)}; expected NAME=VALUE`);
240
+ }
241
+ const index = item.indexOf("=");
242
+ result[item.slice(0, index)] = item.slice(index + 1);
243
+ }
244
+ return result;
245
+ }
246
+ async function resolveBody(dataInline, inputFile, readStdin) {
247
+ let raw;
248
+ if (dataInline != null) {
249
+ raw = dataInline;
250
+ } else if (inputFile != null) {
251
+ raw = inputFile === "-" ? await readStdin() : readFileSync(inputFile, "utf8");
252
+ }
253
+ if (raw == null) {
254
+ return void 0;
255
+ }
256
+ let parsed;
257
+ try {
258
+ parsed = JSON.parse(raw);
259
+ } catch (error) {
260
+ throw new Error(
261
+ `Body is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
262
+ );
263
+ }
264
+ return new TextEncoder().encode(JSON.stringify(parsed));
265
+ }
266
+ async function formatBody(response, jsonOutput) {
267
+ const raw = await response.text();
268
+ if (!raw) {
269
+ return "";
270
+ }
271
+ let parsed;
272
+ try {
273
+ parsed = JSON.parse(raw);
274
+ } catch {
275
+ return raw;
276
+ }
277
+ return jsonOutput ? sortedJson(parsed) : sortedJson(parsed, 2);
278
+ }
279
+ async function renderResponse(response, options, writers) {
280
+ const bodyText = await formatBody(response, Boolean(options.json));
281
+ if (options.output != null) {
282
+ writeFileSync(options.output, bodyText, "utf8");
283
+ return;
284
+ }
285
+ if (options.include) {
286
+ writers.stdout(`HTTP/1.1 ${response.status} ${response.statusText}
287
+ `);
288
+ response.headers.forEach((value, name) => writers.stdout(`${name}: ${value}
289
+ `));
290
+ writers.stdout("\n");
291
+ }
292
+ writers.stdout(`${bodyText}
293
+ `);
294
+ }
295
+ function defaultReadStdin() {
296
+ return new Promise((resolve, reject) => {
297
+ const chunks = [];
298
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
299
+ process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
300
+ process.stdin.on("error", reject);
301
+ });
302
+ }
303
+ async function runApi(method, path, options, deps2) {
304
+ if (options.data != null && options.input != null) {
305
+ deps2.writers.stderr("--data and --input are mutually exclusive\n");
306
+ return EXIT_USAGE;
307
+ }
308
+ let body;
309
+ let headers;
310
+ let params;
311
+ try {
312
+ body = await resolveBody(options.data, options.input, deps2.readStdin ?? defaultReadStdin);
313
+ headers = parseHeaders(options.header ?? []);
314
+ params = parseQuery(options.query ?? []);
315
+ } catch (error) {
316
+ deps2.writers.stderr(`${error instanceof Error ? error.message : String(error)}
317
+ `);
318
+ return EXIT_USAGE;
319
+ }
320
+ let httpClient;
321
+ try {
322
+ httpClient = (deps2.httpFactory ?? (() => buildHttpClient(deps2.env)))();
323
+ } catch (error) {
324
+ if (error instanceof AuthError) {
325
+ deps2.writers.stderr(`Authentication failed: ${error.message}
326
+ `);
327
+ return EXIT_USAGE;
328
+ }
329
+ throw error;
330
+ }
331
+ try {
332
+ let response;
333
+ try {
334
+ response = await httpClient.rawRequest(method.toUpperCase(), path, {
335
+ headers: Object.keys(headers).length > 0 ? headers : void 0,
336
+ params,
337
+ content: body
338
+ });
339
+ } catch (error) {
340
+ if (error instanceof APIError) {
341
+ deps2.writers.stderr(`Transport error: ${error.message}
342
+ `);
343
+ return EXIT_API_ERROR;
344
+ }
345
+ throw error;
346
+ }
347
+ await renderResponse(response, options, deps2.writers);
348
+ return response.status >= 400 ? EXIT_API_ERROR : EXIT_OK;
349
+ } finally {
350
+ try {
351
+ await httpClient.close();
352
+ } catch {
353
+ }
354
+ }
355
+ }
356
+ function resolveOutputPath(inputFile, explicit, targetFormat) {
357
+ if (explicit) {
358
+ return explicit;
359
+ }
360
+ const parsed = parse(inputFile);
361
+ return join(parsed.dir, `${parsed.name}.${targetFormat}`);
362
+ }
363
+ function guessSourceFormat(filename) {
364
+ const dot = filename.lastIndexOf(".");
365
+ if (dot < 0) {
366
+ return null;
367
+ }
368
+ return filename.slice(dot + 1).toLowerCase() || null;
369
+ }
370
+ async function runConvert(options, deps2) {
371
+ const renderer = makeRenderer(Boolean(options.json), deps2.writers);
372
+ const quality = options.quality ?? "standard";
373
+ const output = resolveOutputPath(options.inputFile, options.output, options.targetFormat);
374
+ const source = options.sourceFormat ?? guessSourceFormat(basename(options.inputFile)) ?? void 0;
375
+ if (options.dryRun) {
376
+ emitDryRun(renderer, options.inputFile, source, options.targetFormat, quality, output);
377
+ return EXIT_OK;
378
+ }
379
+ let client;
380
+ try {
381
+ client = (deps2.clientFactory ?? (() => buildClient(deps2.env)))();
382
+ } catch (error) {
383
+ printError(deps2.writers, deps2.env, "Authentication failed", error);
384
+ return EXIT_USAGE;
385
+ }
386
+ try {
387
+ const size = statSync(options.inputFile).size;
388
+ renderer.event("upload", { filename: basename(options.inputFile), size_bytes: size });
389
+ const file = await client.files.upload({ path: options.inputFile });
390
+ renderer.event("create", { message: `Creating conversion \u2192 ${options.targetFormat}` });
391
+ const job = await client.convert.createAndWait({
392
+ file,
393
+ targetFormat: options.targetFormat,
394
+ sourceFormat: source,
395
+ quality
396
+ });
397
+ renderer.event("wait", { progress: job.progress });
398
+ renderer.event("download", { path: output });
399
+ const written = await client.convert.downloadTo(job, { to: output });
400
+ const outputSize = statSync(written).size;
401
+ renderer.final({
402
+ command: "convert",
403
+ input_file: options.inputFile,
404
+ file_id: file.fileId,
405
+ job_id: job.jobId,
406
+ status: job.status,
407
+ output_path: written,
408
+ output_size_bytes: outputSize
409
+ });
410
+ return EXIT_OK;
411
+ } catch (error) {
412
+ return mapError(error, deps2);
413
+ } finally {
414
+ try {
415
+ await client.close();
416
+ } catch {
417
+ }
418
+ }
419
+ }
420
+ function mapError(error, deps2) {
421
+ if (error instanceof JobFailedError) {
422
+ printError(deps2.writers, deps2.env, "Conversion failed", error);
423
+ return EXIT_JOB_FAILED;
424
+ }
425
+ if (error instanceof JobTimeoutError) {
426
+ printError(deps2.writers, deps2.env, "Polling timed out", error);
427
+ return EXIT_API_ERROR;
428
+ }
429
+ if (error instanceof AuthError) {
430
+ printError(deps2.writers, deps2.env, "Authentication failed", error);
431
+ return EXIT_USAGE;
432
+ }
433
+ if (error instanceof APIError) {
434
+ printError(deps2.writers, deps2.env, "API error", error);
435
+ return EXIT_API_ERROR;
436
+ }
437
+ throw error;
438
+ }
439
+ function emitDryRun(renderer, inputFile, source, targetFormat, quality, output) {
440
+ const size = statSync(inputFile).size;
441
+ const contentType = guessContentType(basename(inputFile));
442
+ renderer.event("upload", {
443
+ message: `[dry-run] Would upload: ${basename(inputFile)} (${size} B, ${contentType})`
444
+ });
445
+ renderer.event("create", {
446
+ message: `[dry-run] Would POST /api/v1/jobs: {processor_type=document_conversion, source_format=${source ?? "null"}, target_format=${targetFormat}, quality=${quality}}`
447
+ });
448
+ renderer.event("download", { message: `[dry-run] Would download to: ${output}` });
449
+ renderer.final({
450
+ command: "convert",
451
+ dry_run: true,
452
+ input_file: inputFile,
453
+ source_format: source ?? null,
454
+ target_format: targetFormat,
455
+ quality,
456
+ output_path: output,
457
+ summary: "[dry-run] No API calls made."
458
+ });
459
+ }
460
+
461
+ // src/cli/goals.ts
462
+ var EVENT_GLYPHS = {
463
+ tool_started: "\u25B6",
464
+ tool_finished: "\u2713",
465
+ agent_step_started: "\u25B6",
466
+ agent_step_finished: "\u2713",
467
+ orchestration_transition: "\u2194",
468
+ status: "\u2022",
469
+ progress: "\u2026",
470
+ completed: "\u2713",
471
+ failed: "\u2717",
472
+ slot_needed: "?",
473
+ keepalive: "\xB7",
474
+ agent_text: "\u203A",
475
+ agent_text_done: "\u203A"
476
+ };
477
+ function parseFileIds(raw) {
478
+ if (raw == null) {
479
+ return void 0;
480
+ }
481
+ const cleaned = raw.split(",").map((token) => token.trim()).filter((token) => token.length > 0);
482
+ return cleaned.length > 0 ? cleaned : void 0;
483
+ }
484
+ function parseSlotValue(raw) {
485
+ const stripped = raw.trim();
486
+ if (!stripped) {
487
+ throw new Error("--value cannot be empty");
488
+ }
489
+ try {
490
+ return JSON.parse(stripped);
491
+ } catch {
492
+ return raw;
493
+ }
494
+ }
495
+ function parseSlotPairs(pairs) {
496
+ const slots = {};
497
+ for (const raw of pairs) {
498
+ if (!raw.includes("=")) {
499
+ throw new Error(`--slot expects KEY=VALUE, got ${JSON.stringify(raw)}`);
500
+ }
501
+ const index = raw.indexOf("=");
502
+ const key = raw.slice(0, index).trim();
503
+ if (!key) {
504
+ throw new Error("--slot key cannot be empty");
505
+ }
506
+ slots[key] = parseSlotValue(raw.slice(index + 1));
507
+ }
508
+ return slots;
509
+ }
510
+ function summariseEvent(event) {
511
+ const data = event.data ?? {};
512
+ for (const key of ["name", "tool", "role", "message", "status"]) {
513
+ const value = data[key];
514
+ if (value) {
515
+ return ` ${String(value)}`;
516
+ }
517
+ }
518
+ if (event.type === "progress" && "progress" in data) {
519
+ return ` ${String(data.progress)}%`;
520
+ }
521
+ return "";
522
+ }
523
+ function terminalEventExitCode(event) {
524
+ return event.type === "failed" ? EXIT_JOB_FAILED : EXIT_OK;
525
+ }
526
+ function emitEvent(event, jsonOutput, writers) {
527
+ if (jsonOutput) {
528
+ writers.stdout(`${sortedJson(event)}
529
+ `);
530
+ return;
531
+ }
532
+ const glyph = EVENT_GLYPHS[event.type] ?? "\u2022";
533
+ writers.stderr(`${glyph} ${event.type}${summariseEvent(event)}
534
+ `);
535
+ }
536
+ function jobToPayload(commandName, job) {
537
+ return {
538
+ command: `goals.${commandName}`,
539
+ job_spec_id: job.jobSpecId,
540
+ status: job.status,
541
+ progress: job.progress,
542
+ pending_slots: job.pendingSlots,
543
+ filled_slots: job.filledSlots,
544
+ summary: `\u2713 ${commandName} job=${job.jobSpecId} status=${job.status}`
545
+ };
546
+ }
547
+ async function runGoalsStart(options, deps2) {
548
+ const renderer = makeRenderer(Boolean(options.json), deps2.writers);
549
+ const fileIds = parseFileIds(options.files);
550
+ let slots;
551
+ try {
552
+ slots = parseSlotPairs(options.slot ?? []);
553
+ } catch (error) {
554
+ deps2.writers.stderr(`${error instanceof Error ? error.message : String(error)}
555
+ `);
556
+ return EXIT_USAGE;
557
+ }
558
+ if (options.dryRun) {
559
+ const payload = { fileIds: fileIds ?? [] };
560
+ if (options.workflowId != null) {
561
+ payload.workflowId = options.workflowId;
562
+ }
563
+ if (options.goalText != null) {
564
+ payload.goalText = options.goalText;
565
+ }
566
+ if (Object.keys(slots).length > 0) {
567
+ payload.slotAnswers = Object.entries(slots).map(([slotId, value]) => ({ slotId, value }));
568
+ }
569
+ renderer.event("create", {
570
+ message: `[dry-run] Would POST /api/v1/jobs/goal: ${sortedJson(payload)}`
571
+ });
572
+ renderer.final({
573
+ command: "goals.start",
574
+ dry_run: true,
575
+ payload,
576
+ summary: "[dry-run] No API calls made."
577
+ });
578
+ return EXIT_OK;
579
+ }
580
+ return runGoalAction(
581
+ deps2,
582
+ renderer,
583
+ "start",
584
+ (client) => client.goals.start({
585
+ workflowId: options.workflowId,
586
+ goalText: options.goalText,
587
+ files: fileIds,
588
+ slots: Object.keys(slots).length > 0 ? slots : void 0
589
+ })
590
+ );
591
+ }
592
+ async function runGoalsStatus(jobSpecId, options, deps2) {
593
+ const renderer = makeRenderer(Boolean(options.json), deps2.writers);
594
+ return runGoalAction(
595
+ deps2,
596
+ renderer,
597
+ "status",
598
+ (client) => options.watch ? client.goals.wait(jobSpecId, { timeout: options.timeout }) : client.goals.retrieve(jobSpecId)
599
+ );
600
+ }
601
+ async function runGoalsFillSlot(jobSpecId, options, deps2) {
602
+ const renderer = makeRenderer(Boolean(options.json), deps2.writers);
603
+ let value;
604
+ try {
605
+ value = parseSlotValue(options.value);
606
+ } catch (error) {
607
+ deps2.writers.stderr(`${error instanceof Error ? error.message : String(error)}
608
+ `);
609
+ return EXIT_USAGE;
610
+ }
611
+ return runGoalAction(
612
+ deps2,
613
+ renderer,
614
+ "fill-slot",
615
+ (client) => client.goals.fillSlot(jobSpecId, {
616
+ slotId: options.slotId,
617
+ value,
618
+ expectedVersion: options.expectedVersion
619
+ })
620
+ );
621
+ }
622
+ async function runGoalsConfirm(jobSpecId, options, deps2) {
623
+ const renderer = makeRenderer(Boolean(options.json), deps2.writers);
624
+ return runGoalAction(
625
+ deps2,
626
+ renderer,
627
+ "confirm",
628
+ (client) => client.goals.confirm(jobSpecId, { expectedVersion: options.expectedVersion })
629
+ );
630
+ }
631
+ async function runGoalsCancel(jobSpecId, options, deps2) {
632
+ const renderer = makeRenderer(Boolean(options.json), deps2.writers);
633
+ return runGoalAction(deps2, renderer, "cancel", (client) => client.goals.cancel(jobSpecId));
634
+ }
635
+ async function runGoalsRetry(jobSpecId, options, deps2) {
636
+ const renderer = makeRenderer(Boolean(options.json), deps2.writers);
637
+ return runGoalAction(
638
+ deps2,
639
+ renderer,
640
+ "retry",
641
+ (client) => client.goals.retry(jobSpecId, { rerunMode: options.rerunMode, reason: options.reason })
642
+ );
643
+ }
644
+ async function runGoalsEvents(jobSpecId, options, deps2) {
645
+ let client;
646
+ try {
647
+ client = (deps2.clientFactory ?? (() => buildClient(deps2.env)))();
648
+ } catch (error) {
649
+ if (error instanceof AuthError) {
650
+ deps2.writers.stderr(`Authentication failed: ${error.message}
651
+ `);
652
+ return EXIT_USAGE;
653
+ }
654
+ throw error;
655
+ }
656
+ try {
657
+ for await (const event of client.goals.events(jobSpecId, { wsUrl: options.wsUrl })) {
658
+ emitEvent(event, Boolean(options.json), deps2.writers);
659
+ if (isGoalEventTerminal(event)) {
660
+ return terminalEventExitCode(event);
661
+ }
662
+ }
663
+ deps2.writers.stderr("Event stream closed without a terminal event\n");
664
+ return EXIT_API_ERROR;
665
+ } catch (error) {
666
+ if (error instanceof WebSocketError) {
667
+ deps2.writers.stderr(`WebSocket error: ${error.message}
668
+ `);
669
+ return EXIT_API_ERROR;
670
+ }
671
+ if (error instanceof APIError) {
672
+ deps2.writers.stderr(`API error: ${error.message}
673
+ `);
674
+ return EXIT_API_ERROR;
675
+ }
676
+ if (error instanceof Error && /No WebSocket URL configured/.test(error.message)) {
677
+ deps2.writers.stderr(`Configuration error: ${error.message}
678
+ `);
679
+ return EXIT_USAGE;
680
+ }
681
+ deps2.writers.stderr(
682
+ `Unexpected error: ${error instanceof Error ? error.message : String(error)}
683
+ `
684
+ );
685
+ return EXIT_API_ERROR;
686
+ } finally {
687
+ try {
688
+ await client.close();
689
+ } catch {
690
+ }
691
+ }
692
+ }
693
+ async function runGoalAction(deps2, renderer, commandName, action) {
694
+ let client;
695
+ try {
696
+ client = (deps2.clientFactory ?? (() => buildClient(deps2.env)))();
697
+ } catch (error) {
698
+ printError(deps2.writers, deps2.env, "Authentication failed", error);
699
+ return EXIT_USAGE;
700
+ }
701
+ try {
702
+ const job = await action(client);
703
+ renderer.event(commandName, { status: job.status });
704
+ renderer.final(jobToPayload(commandName, job));
705
+ return EXIT_OK;
706
+ } catch (error) {
707
+ if (error instanceof GoalJobFailedError) {
708
+ printError(deps2.writers, deps2.env, "Goal job failed", error);
709
+ return EXIT_JOB_FAILED;
710
+ }
711
+ if (error instanceof GoalJobTimeoutError) {
712
+ printError(deps2.writers, deps2.env, "Polling timed out", error);
713
+ return EXIT_API_ERROR;
714
+ }
715
+ if (error instanceof AuthError) {
716
+ printError(deps2.writers, deps2.env, "Authentication failed", error);
717
+ return EXIT_USAGE;
718
+ }
719
+ if (error instanceof APIError) {
720
+ printError(deps2.writers, deps2.env, "API error", error);
721
+ return EXIT_API_ERROR;
722
+ }
723
+ throw error;
724
+ } finally {
725
+ try {
726
+ await client.close();
727
+ } catch {
728
+ }
729
+ }
730
+ }
731
+
732
+ // src/cli/doctor.ts
733
+ function maskSecret(value) {
734
+ return value.length <= 10 ? "***" : `${value.slice(0, 6)}\u2026${value.slice(-4)}`;
735
+ }
736
+ function checkNode() {
737
+ const version = process.versions.node;
738
+ const major = Number(version.split(".")[0]);
739
+ if (Number.isFinite(major) && major < 18) {
740
+ return { name: "Node", status: "FAIL", detail: `need \u2265 18, got ${version}` };
741
+ }
742
+ return { name: "Node", status: "OK", detail: version };
743
+ }
744
+ function checkCapability(name, available, options = {}) {
745
+ if (available) {
746
+ return { name, status: "OK", detail: "available" };
747
+ }
748
+ const detail = `not available in this runtime${options.hint ? ` \u2014 ${options.hint}` : ""}`;
749
+ return { name, status: options.optional ? "WARN" : "FAIL", detail };
750
+ }
751
+ function checkApiKey(env) {
752
+ const key = env[ENV_API_KEY];
753
+ if (!key) {
754
+ return {
755
+ name: ENV_API_KEY,
756
+ status: "FAIL",
757
+ detail: `not set \u2014 export ${ENV_API_KEY}=ck_\u2026 first`
758
+ };
759
+ }
760
+ return { name: ENV_API_KEY, status: "OK", detail: maskSecret(key) };
761
+ }
762
+ function checkBaseUrl(env) {
763
+ const url = env[ENV_BASE_URL] || DEFAULT_BASE_URL;
764
+ const isDefault = url === DEFAULT_BASE_URL;
765
+ return { name: ENV_BASE_URL, status: "OK", detail: `${url}${isDefault ? " (default)" : ""}` };
766
+ }
767
+ async function checkBackendHealth(deps2) {
768
+ const base = (deps2.env[ENV_BASE_URL] || DEFAULT_BASE_URL).replace(/\/+$/, "");
769
+ const url = `${base}/api/v1/health`;
770
+ const fetchImpl = deps2.fetchImpl ?? ((target) => fetch(target));
771
+ try {
772
+ const response = await fetchImpl(url);
773
+ if (response.status >= 400) {
774
+ return {
775
+ name: "Backend health",
776
+ status: "FAIL",
777
+ detail: `HTTP ${response.status} ${response.statusText}`
778
+ };
779
+ }
780
+ return {
781
+ name: "Backend health",
782
+ status: "OK",
783
+ detail: `${response.status} ${response.statusText}`
784
+ };
785
+ } catch (error) {
786
+ const detail = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
787
+ return { name: "Backend health", status: "FAIL", detail };
788
+ }
789
+ }
790
+ async function checkAccountTier(deps2) {
791
+ try {
792
+ const client = (deps2.clientFactory ?? (() => buildClient(deps2.env)))();
793
+ try {
794
+ const plan = await client.account.getPlan();
795
+ return { name: "Account tier", status: "OK", detail: `tier=${plan.tier}` };
796
+ } finally {
797
+ try {
798
+ await client.close();
799
+ } catch {
800
+ }
801
+ }
802
+ } catch (error) {
803
+ if (error instanceof APIError) {
804
+ return {
805
+ name: "Account tier",
806
+ status: "WARN",
807
+ detail: `tier query failed: HTTP ${error.statusCode} ${error.code}`
808
+ };
809
+ }
810
+ return {
811
+ name: "Account tier",
812
+ status: "WARN",
813
+ detail: error instanceof Error ? error.message : String(error)
814
+ };
815
+ }
816
+ }
817
+ async function collectChecks(ping, deps2) {
818
+ const checks = [
819
+ checkNode(),
820
+ { name: "convilyn SDK", status: "OK", detail: VERSION },
821
+ checkCapability("fetch", typeof fetch !== "undefined"),
822
+ // Optional: only goals.events() streaming needs a global WebSocket (Node 22+);
823
+ // goals.wait() polling covers the same ground on older runtimes.
824
+ checkCapability("WebSocket", typeof WebSocket !== "undefined", {
825
+ optional: true,
826
+ hint: "needed only for goals.events(); goals.wait() polling works without it"
827
+ }),
828
+ checkCapability(
829
+ "crypto.randomUUID",
830
+ typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
831
+ ),
832
+ checkApiKey(deps2.env),
833
+ checkBaseUrl(deps2.env)
834
+ ];
835
+ if (ping) {
836
+ checks.push(await checkBackendHealth(deps2));
837
+ if (deps2.env[ENV_API_KEY]) {
838
+ checks.push(await checkAccountTier(deps2));
839
+ }
840
+ } else {
841
+ checks.push({
842
+ name: "Backend health",
843
+ status: "SKIP",
844
+ detail: "skipped (re-run with --ping to probe /api/v1/health)"
845
+ });
846
+ }
847
+ return checks;
848
+ }
849
+ function emitChecks(renderer, checks) {
850
+ for (const check of checks) {
851
+ renderer.event(check.status === "OK" ? "ok" : "warn", {
852
+ message: `[${check.status}] ${check.name}: ${check.detail}`
853
+ });
854
+ }
855
+ const failures = checks.filter((check) => check.status === "FAIL");
856
+ renderer.final({
857
+ command: "doctor",
858
+ checks: checks.map((check) => ({
859
+ name: check.name,
860
+ status: check.status,
861
+ detail: check.detail
862
+ })),
863
+ summary: failures.length === 0 ? "All checks passed." : `${failures.length} of ${checks.length} checks failed.`
864
+ });
865
+ }
866
+ async function runDoctor(options, deps2) {
867
+ const renderer = makeRenderer(Boolean(options.json), deps2.writers);
868
+ const checks = await collectChecks(Boolean(options.ping), deps2);
869
+ emitChecks(renderer, checks);
870
+ const failures = checks.filter((check) => check.status === "FAIL");
871
+ if (failures.length === 0) {
872
+ return EXIT_OK;
873
+ }
874
+ const backendFailed = failures.some((check) => check.name === "Backend health");
875
+ return backendFailed ? EXIT_API_ERROR : EXIT_USAGE;
876
+ }
877
+
878
+ // src/cli/main.ts
879
+ function deps() {
880
+ return { writers: processWriters(), env: process.env };
881
+ }
882
+ async function finish(code) {
883
+ process.exitCode = await code;
884
+ }
885
+ function asInt(value) {
886
+ return value != null ? Number(value) : void 0;
887
+ }
888
+ function buildProgram() {
889
+ const program = new Command();
890
+ program.name("convilyn").description("Convert files and run AI workflows from the command line.").version(VERSION, "-v, --version");
891
+ program.command("convert").description("Convert a local file into a different format.").argument("<input-file>", "Path to the file to convert").requiredOption("--to <format>", "Target format token (e.g. pdf, docx, html)").option("-o, --output <path>", "Output path (defaults to <basename>.<format>)").option("--source-format <format>", "Override source-format auto-detection").option("--quality <quality>", "Conversion quality hint", "standard").option("--json", "Emit a single JSON object on stdout").option("--dry-run", "Show what would happen without making any API calls").action(
892
+ async (inputFile, opts) => finish(
893
+ runConvert(
894
+ {
895
+ inputFile,
896
+ targetFormat: opts.to,
897
+ output: opts.output,
898
+ sourceFormat: opts.sourceFormat,
899
+ quality: opts.quality,
900
+ json: opts.json,
901
+ dryRun: opts.dryRun
902
+ },
903
+ deps()
904
+ )
905
+ )
906
+ );
907
+ program.command("doctor").description("Show SDK environment + backend connectivity diagnostics.").option("--json", "Emit a single JSON object on stdout").option("--ping", "Probe the backend health endpoint").action(async (opts) => finish(runDoctor({ json: opts.json, ping: opts.ping }, deps())));
908
+ program.command("api").description("Call any Convilyn API endpoint (gh-style escape hatch).").argument("<method>", `HTTP method`).argument("<path>", "Request path, e.g. /api/v1/jobs/job_xyz").option("--data <json>", "JSON body inline (mutually exclusive with --input)").option("--input <file>", "Read JSON body from FILE ('-' for stdin)").option("--header <header...>", 'Extra header in "Name: Value" form (repeatable)').option("--query <param...>", "Query parameter NAME=VALUE (repeatable)").option("--include", "Print response status + headers + body").option("--json", "Single-line JSON (jq-friendly)").option("-o, --output <file>", "Write response body to FILE").action(
909
+ async (method, path, opts) => finish(
910
+ runApi(
911
+ method,
912
+ path,
913
+ {
914
+ data: opts.data,
915
+ input: opts.input,
916
+ header: opts.header,
917
+ query: opts.query,
918
+ include: opts.include,
919
+ json: opts.json,
920
+ output: opts.output
921
+ },
922
+ deps()
923
+ )
924
+ )
925
+ );
926
+ buildGoalsGroup(program);
927
+ buildAccountGroup(program);
928
+ return program;
929
+ }
930
+ function buildGoalsGroup(program) {
931
+ const goals = program.command("goals").description("Run agentic goal workflows.");
932
+ goals.command("start").option("--workflow-id <id>", "Workflow spec id (mutually exclusive with --goal-text)").option("--goal-text <text>", "Free-text goal (requires --files)").option("--files <ids>", "Comma-separated file ids").option("--slot <kv...>", "Pre-fill a slot KEY=VALUE (repeatable)").option("--json", "Emit the job as JSON on stdout").option("--dry-run", "Print the payload without hitting the network").action(
933
+ async (opts) => finish(
934
+ runGoalsStart(
935
+ {
936
+ workflowId: opts.workflowId,
937
+ goalText: opts.goalText,
938
+ files: opts.files,
939
+ slot: opts.slot,
940
+ json: opts.json,
941
+ dryRun: opts.dryRun
942
+ },
943
+ deps()
944
+ )
945
+ )
946
+ );
947
+ goals.command("status").argument("<job-spec-id>").option("--watch", "Poll until terminal status or HITL stop").option("--timeout <seconds>", "Watch-mode timeout in seconds", "300").option("--json", "Emit the job as JSON on stdout").action(
948
+ async (jobSpecId, opts) => finish(
949
+ runGoalsStatus(
950
+ jobSpecId,
951
+ { watch: opts.watch, timeout: asInt(opts.timeout), json: opts.json },
952
+ deps()
953
+ )
954
+ )
955
+ );
956
+ goals.command("events").argument("<job-spec-id>").option("--json", "Emit each event as NDJSON on stdout").option("--ws-url <url>", "Override the configured WebSocket URL").action(
957
+ async (jobSpecId, opts) => finish(runGoalsEvents(jobSpecId, { json: opts.json, wsUrl: opts.wsUrl }, deps()))
958
+ );
959
+ goals.command("fill-slot").argument("<job-spec-id>").requiredOption("--slot-id <id>", "Slot identifier to answer").requiredOption("--value <value>", "Slot value (JSON when possible, else literal)").option("--expected-version <n>", "Optimistic-locking item version").option("--json", "Emit the updated job as JSON on stdout").action(
960
+ async (jobSpecId, opts) => finish(
961
+ runGoalsFillSlot(
962
+ jobSpecId,
963
+ {
964
+ slotId: opts.slotId,
965
+ value: opts.value,
966
+ expectedVersion: asInt(opts.expectedVersion),
967
+ json: opts.json
968
+ },
969
+ deps()
970
+ )
971
+ )
972
+ );
973
+ goals.command("confirm").argument("<job-spec-id>").option("--expected-version <n>", "Optimistic-locking item version").option("--json", "Emit JSON on stdout").action(
974
+ async (jobSpecId, opts) => finish(
975
+ runGoalsConfirm(
976
+ jobSpecId,
977
+ { expectedVersion: asInt(opts.expectedVersion), json: opts.json },
978
+ deps()
979
+ )
980
+ )
981
+ );
982
+ goals.command("cancel").argument("<job-spec-id>").option("--json", "Emit JSON on stdout").action(
983
+ async (jobSpecId, opts) => finish(runGoalsCancel(jobSpecId, { json: opts.json }, deps()))
984
+ );
985
+ goals.command("retry").argument("<job-spec-id>").addOption(
986
+ new Option("--rerun-mode <mode>", "Resume the thread or start fresh").choices(["retry_same_thread", "fresh_rerun"]).default("retry_same_thread")
987
+ ).option("--reason <reason>", "Optional audit string").option("--json", "Emit JSON on stdout").action(
988
+ async (jobSpecId, opts) => finish(
989
+ runGoalsRetry(
990
+ jobSpecId,
991
+ { rerunMode: opts.rerunMode, reason: opts.reason, json: opts.json },
992
+ deps()
993
+ )
994
+ )
995
+ );
996
+ }
997
+ function buildAccountGroup(program) {
998
+ const account = program.command("account").description("Check your billing plan + quota.");
999
+ account.command("plan").option("--json", "Emit a single JSON object on stdout").action(async (opts) => finish(runAccountPlan({ json: opts.json }, deps())));
1000
+ account.command("quota").option("--tool <tool...>", "Fully-qualified tool id (repeatable)").option("--max-iter <n>", "Iteration cap from the draft spec").option("--json", "Emit a single JSON object on stdout").action(
1001
+ async (opts) => finish(
1002
+ runAccountQuota({ tools: opts.tool, maxIter: asInt(opts.maxIter), json: opts.json }, deps())
1003
+ )
1004
+ );
1005
+ }
1006
+ async function main() {
1007
+ await buildProgram().parseAsync(process.argv);
1008
+ }
1009
+ main().catch((error) => {
1010
+ process.stderr.write(
1011
+ `Unexpected error: ${error instanceof Error ? error.message : String(error)}
1012
+ `
1013
+ );
1014
+ process.exitCode = 2;
1015
+ });
1016
+
1017
+ export { buildProgram };
1018
+ //# sourceMappingURL=cli.js.map
1019
+ //# sourceMappingURL=cli.js.map