@clickraft/cli 0.8.3 → 0.8.5

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/index.js CHANGED
@@ -10,6 +10,10 @@ import { z } from "zod";
10
10
  import { z as z2 } from "zod";
11
11
  import { z as z3 } from "zod";
12
12
  import { z as z4 } from "zod";
13
+ import { request as undiciRequest } from "undici";
14
+ import { z as z6 } from "zod";
15
+ import { createHash, randomUUID } from "crypto";
16
+ import { z as z5 } from "zod";
13
17
  var TOKEN_FIELD_PATTERN = /"(access_token|refresh_token|device_code|user_code|code_verifier|client_secret)"\s*:\s*"[^"]*"/gi;
14
18
  var BARE_TOKEN_PATTERN = /\bck_(?:live|dev|test)_[A-Za-z0-9_-]+/gi;
15
19
  function redactTokenLikeFields(input) {
@@ -81,441 +85,127 @@ var TokenRowSchema = z4.object({
81
85
  createdAt: z4.string()
82
86
  }).strict();
83
87
  var TokensListResponseSchema = z4.array(TokenRowSchema);
84
-
85
- // src/api/client.ts
86
- import { request as undiciRequest } from "undici";
87
- import { z as z6 } from "zod";
88
-
89
- // src/errors/codes.ts
90
- var LOGIN_HINT = "Run `clickraft login` to authenticate.";
91
- var RELOGIN_HINT = "Run `clickraft login` to refresh.";
92
- var EXIT_CODE_MAP = {
93
- // --- 37 Phase 8 Q7 codes ----------------------------------------------------
94
- AUTH_TOKEN_MISSING: {
95
- exitCode: 3,
96
- category: "auth",
97
- retryable: false,
98
- hint: "Set `CLICKRAFT_TOKEN` or run `clickraft login`."
99
- },
100
- AUTH_TOKEN_INVALID: {
101
- exitCode: 3,
102
- category: "auth",
103
- retryable: false,
104
- hint: "The token is malformed or unknown. Run `clickraft login`."
105
- },
106
- AUTH_TOKEN_EXPIRED: {
107
- exitCode: 3,
108
- category: "auth",
109
- retryable: false,
110
- hint: RELOGIN_HINT
111
- },
112
- AUTH_TOKEN_REVOKED: {
113
- exitCode: 3,
114
- category: "auth",
115
- retryable: false,
116
- hint: "Token was revoked. Run `clickraft login` to mint a new one."
117
- },
118
- AUTH_TOKEN_SCOPE_INSUFFICIENT: {
119
- exitCode: 4,
120
- category: "permission",
121
- retryable: false,
122
- hint: "This token does not have the required scope. Re-run `clickraft login` selecting the needed scopes."
123
- },
124
- AUTH_ORG_MISMATCH: {
125
- exitCode: 4,
126
- category: "permission",
127
- retryable: false,
128
- hint: "This token's org no longer exists or you have been removed."
129
- },
130
- AUTH_DEVICE_CODE_PENDING: {
131
- exitCode: 0,
132
- category: "success",
133
- retryable: false,
134
- hint: "Continue polling."
135
- },
136
- AUTH_DEVICE_CODE_EXPIRED: {
137
- exitCode: 3,
138
- category: "auth",
139
- retryable: false,
140
- hint: "Device code expired. Re-run `clickraft login`."
141
- },
142
- AUTH_DEVICE_CODE_SLOW_DOWN: {
143
- exitCode: 0,
144
- category: "success",
145
- retryable: false,
146
- hint: "Increase polling interval (Retry-After)."
147
- },
148
- IDEMPOTENCY_KEY_MISSING: {
149
- exitCode: 2,
150
- category: "usage",
151
- retryable: false,
152
- hint: "Add `X-Idempotency-Key` header (or `--idempotency-key`)."
153
- },
154
- IDEMPOTENCY_KEY_CONFLICT: {
155
- exitCode: 7,
156
- category: "conflict",
157
- retryable: false,
158
- hint: "Same key was used with a different request body. Use a new key."
159
- },
160
- PLAN_FEATURE_LOCKED: {
161
- exitCode: 4,
162
- category: "permission",
163
- retryable: false,
164
- hint: "API access is not enabled on your plan. Visit billing settings."
165
- },
166
- PLAN_QUOTA_EXCEEDED: {
167
- exitCode: 5,
168
- category: "quota",
169
- retryable: false,
170
- hint: "Daily API quota exhausted. Wait, or upgrade your plan."
171
- },
172
- INSUFFICIENT_CREDITS: {
173
- exitCode: 5,
174
- category: "quota",
175
- retryable: false,
176
- hint: "Top up credits to retry."
177
- },
178
- CREDIT_DEDUCTION_FAILED: {
179
- exitCode: 1,
180
- category: "runtime",
181
- retryable: true,
182
- hint: "Could not charge credits. Retry; no charge was made."
183
- },
184
- RATE_LIMITED: {
185
- exitCode: 5,
186
- category: "quota",
187
- retryable: true,
188
- hint: "Slow down. Honor Retry-After."
189
- },
190
- INPUT_INVALID_FORMAT: {
191
- exitCode: 2,
192
- category: "usage",
193
- retryable: false,
194
- hint: "Check input format and try again."
195
- },
196
- INPUT_TOO_LONG: {
197
- exitCode: 2,
198
- category: "usage",
199
- retryable: false,
200
- hint: "Field exceeded max length."
201
- },
202
- WORKFLOW_VALIDATION_FAILED: {
203
- exitCode: 2,
204
- category: "usage",
205
- retryable: false,
206
- hint: "Run `workflow validate` for the full list of errors."
207
- },
208
- WORKFLOW_NODE_TYPE_UNKNOWN: {
209
- exitCode: 2,
210
- category: "usage",
211
- retryable: false,
212
- hint: "Run `clickraft nodes list` for the current set of node types."
213
- },
214
- CANVAS_REV_MISMATCH: {
215
- exitCode: 7,
216
- category: "conflict",
217
- retryable: false,
218
- hint: "The workflow was updated elsewhere. Re-fetch and retry with the current canvas_rev."
219
- },
220
- CANVAS_REV_REQUIRED: {
221
- exitCode: 2,
222
- category: "usage",
223
- retryable: false,
224
- hint: "Include --canvas-rev or use --auto-rev."
225
- },
226
- WORKER_UNREACHABLE: {
227
- exitCode: 6,
228
- category: "network",
229
- retryable: true,
230
- hint: "Collaboration service temporarily unavailable. Retry."
231
- },
232
- IDEMPOTENCY_IN_PROGRESS: {
233
- exitCode: 7,
234
- category: "conflict",
235
- retryable: false,
236
- hint: "A request with this idempotency key is currently in-flight. Retry in a moment."
237
- },
238
- TOKEN_NAME_INVALID: {
239
- exitCode: 2,
240
- category: "usage",
241
- retryable: false,
242
- hint: "Agent token name is malformed."
243
- },
244
- WORKFLOW_NOT_FOUND: {
245
- exitCode: 2,
246
- category: "usage",
247
- retryable: false,
248
- hint: "No workflow with that ID in your org."
249
- },
250
- NOT_FOUND: {
251
- exitCode: 2,
252
- category: "usage",
253
- retryable: false,
254
- hint: "Resource not found."
255
- },
256
- NODE_TYPE_NOT_FOUND: {
257
- exitCode: 2,
258
- category: "usage",
259
- retryable: false,
260
- hint: "Run `clickraft nodes list` for the current set of node types."
261
- },
262
- FEATURE_DISABLED: {
263
- exitCode: 2,
264
- category: "usage",
265
- retryable: false,
266
- hint: "This endpoint is not enabled. Contact support if you believe this is in error."
267
- },
268
- FORBIDDEN: {
269
- exitCode: 4,
270
- category: "permission",
271
- retryable: false,
272
- hint: "Your role does not permit this action."
273
- },
274
- MODEL_NOT_FOUND: {
275
- exitCode: 2,
276
- category: "usage",
277
- retryable: false,
278
- hint: "Unknown `modelSlug`. Run `brand-model list` for the current set."
279
- },
280
- PROVIDER_UNAVAILABLE: {
281
- exitCode: 6,
282
- category: "network",
283
- retryable: true,
284
- hint: "Provider is temporarily unavailable. Retry shortly."
285
- },
286
- STORAGE_UPLOAD_FAILED: {
287
- exitCode: 1,
288
- category: "runtime",
289
- retryable: true,
290
- hint: "Upload failed mid-stream. Retry."
291
- },
292
- STORAGE_QUOTA_EXCEEDED: {
293
- exitCode: 5,
294
- category: "quota",
295
- retryable: false,
296
- hint: "Org storage quota exceeded. Free space or upgrade."
297
- },
298
- GEN_CONTENT_REFUSAL: {
299
- exitCode: 1,
300
- category: "runtime",
301
- retryable: false,
302
- hint: "The model declined the request via safety policy."
303
- },
304
- GEN_OUTPUT_SCHEMA_MISMATCH: {
305
- exitCode: 1,
306
- category: "runtime",
307
- retryable: true,
308
- hint: "Provider returned malformed output. Full refund issued. Retry."
309
- },
310
- GEN_EMPTY_OUTPUT: {
311
- exitCode: 1,
312
- category: "runtime",
313
- retryable: true,
314
- hint: "Provider returned no usable output. Full refund issued. Retry."
315
- },
316
- GEN_ITEM_COUNT_MISMATCH: {
317
- exitCode: 0,
318
- category: "success",
319
- retryable: false,
320
- hint: "Partial output kept; no refund."
321
- },
322
- AGENT_FINGERPRINT_REJECTED: {
323
- exitCode: 4,
324
- category: "permission",
325
- retryable: false,
326
- hint: "This client identification is not permitted. Contact support."
327
- },
328
- INTERNAL_ERROR: {
329
- exitCode: 1,
330
- category: "runtime",
331
- retryable: true,
332
- hint: "Server error. Retry; if it persists, contact support with the request ID."
333
- },
334
- CREDENTIALS_LOCKED: {
335
- exitCode: 1,
336
- category: "runtime",
337
- retryable: true,
338
- hint: "Another CLI invocation holds the credentials lock. Retry shortly."
339
- },
340
- CREDENTIALS_PERMISSIONS_LOOSE: {
341
- exitCode: 0,
342
- category: "success",
343
- retryable: false,
344
- hint: "Credentials file mode is wider than 0600; continuing."
345
- },
346
- // --- 5 additional codes introduced by 1A.1 ---------------------------------
347
- AUTH_ACCESS_DENIED: {
348
- exitCode: 3,
349
- category: "auth",
350
- retryable: false,
351
- hint: "You denied the OAuth grant. Re-run `clickraft login` to retry."
352
- },
353
- AUTH_FLOW_CANCELLED: {
354
- exitCode: 3,
355
- category: "auth",
356
- retryable: false,
357
- hint: "Authentication was cancelled. Re-run `clickraft login`."
358
- },
359
- CREDENTIALS_CORRUPT: {
360
- exitCode: 1,
361
- category: "runtime",
362
- retryable: false,
363
- hint: "Credentials file is unreadable. Remove it and run `clickraft login`."
364
- },
365
- CREDENTIALS_NOT_FOUND: {
366
- exitCode: 3,
367
- category: "auth",
368
- retryable: false,
369
- hint: LOGIN_HINT
370
- },
371
- CREDENTIALS_PROFILE_NOT_FOUND: {
372
- exitCode: 2,
373
- category: "usage",
374
- retryable: false,
375
- hint: "No such profile. Use `clickraft auth profiles` to list available profiles."
376
- },
377
- // --- 8 brand model resolution codes -----------------------------------------
378
- BRAND_MODEL_NOT_FOUND: {
379
- exitCode: 1,
380
- category: "runtime",
381
- retryable: false,
382
- hint: "No brand model with that ID. Run `clickraft brand-model list`."
383
- },
384
- BRAND_MODEL_NOT_A_MODEL: {
385
- exitCode: 1,
386
- category: "runtime",
387
- retryable: false,
388
- hint: "The specified asset is not a brand model."
389
- },
390
- BRAND_MODEL_NOT_ACTIVE: {
391
- exitCode: 1,
392
- category: "runtime",
393
- retryable: false,
394
- hint: "Brand model is not active. Check its status in the dashboard."
395
- },
396
- BRAND_MODEL_NO_REFERENCES: {
397
- exitCode: 1,
398
- category: "runtime",
399
- retryable: false,
400
- hint: "Brand model has no reference images. Upload references first."
401
- },
402
- BRAND_MODEL_POSE_NOT_FOUND: {
403
- exitCode: 1,
404
- category: "runtime",
405
- retryable: false,
406
- hint: "Requested pose not found. Valid: front, 3/4-right, right, left, 3/4-left, back, approved."
407
- },
408
- BRAND_MODEL_DUPLICATE_ID: {
409
- exitCode: 2,
410
- category: "usage",
411
- retryable: false,
412
- hint: "Same brand model ID appears more than once."
413
- },
414
- MODEL_DOES_NOT_SUPPORT_REFERENCES: {
415
- exitCode: 2,
416
- category: "usage",
417
- retryable: false,
418
- hint: "This model does not support brand model references."
419
- },
420
- REFERENCE_LIMIT_EXCEEDED: {
421
- exitCode: 2,
422
- category: "usage",
423
- retryable: false,
424
- hint: "Too many reference images. Check model limits."
425
- },
426
- // --- 8 product reference resolver codes -------------------------------------
427
- PRODUCT_NOT_FOUND: {
428
- exitCode: 1,
429
- category: "runtime",
430
- retryable: false,
431
- hint: "No product with that ID in your org. Run `clickraft product list`."
432
- },
433
- PRODUCT_DUPLICATE_REFERENCE: {
434
- exitCode: 2,
435
- category: "usage",
436
- retryable: false,
437
- hint: "Same (productId, imageId) tuple appears twice in the request."
438
- },
439
- PRODUCT_IMAGE_NOT_FOUND: {
440
- exitCode: 1,
441
- category: "runtime",
442
- retryable: false,
443
- hint: "imageId does not exist in the product images."
444
- },
445
- PRODUCT_IMAGE_MISMATCH: {
446
- exitCode: 2,
447
- category: "usage",
448
- retryable: false,
449
- hint: "imageId belongs to a different product."
450
- },
451
- PRODUCT_SYNC_ERROR: {
452
- exitCode: 1,
453
- category: "runtime",
454
- retryable: false,
455
- hint: "Product sync status is 'error'. Re-sync or use a different product."
456
- },
457
- PRODUCT_DELETED_UPSTREAM: {
458
- exitCode: 1,
459
- category: "runtime",
460
- retryable: false,
461
- hint: "Product was deleted upstream. Remove it or re-sync."
462
- },
463
- PRODUCT_NO_PRIMARY_IMAGE: {
464
- exitCode: 1,
465
- category: "runtime",
466
- retryable: false,
467
- hint: "Product has no primary image. Specify --product <id>:<imageId> explicitly."
468
- },
469
- PRODUCT_SOURCE_MANAGED: {
470
- exitCode: 4,
471
- category: "permission",
472
- retryable: false,
473
- hint: "Product is managed by an external integration."
474
- },
475
- // --- 4 CLI-synthetic codes --------------------------------------------------
476
- NETWORK_ERROR: {
477
- exitCode: 6,
478
- category: "network",
479
- retryable: true,
480
- hint: "Network connection failed. Check connectivity and retry."
481
- },
482
- TIMEOUT: {
483
- exitCode: 6,
484
- category: "network",
485
- retryable: true,
486
- hint: "Request timed out. Retry; consider increasing the timeout."
487
- },
488
- PARSE_ERROR: {
489
- exitCode: 1,
490
- category: "runtime",
491
- retryable: false,
492
- hint: "Server returned an unexpected response. Contact support with the request ID."
493
- },
494
- SCHEMA_MISMATCH: {
495
- exitCode: 1,
496
- category: "runtime",
497
- retryable: false,
498
- hint: "Server response did not match the expected schema. Update the CLI."
88
+ var SERVER_ERROR_CODES = [
89
+ "AUTH_TOKEN_MISSING",
90
+ "AUTH_TOKEN_INVALID",
91
+ "AUTH_TOKEN_EXPIRED",
92
+ "AUTH_TOKEN_REVOKED",
93
+ "AUTH_TOKEN_SCOPE_INSUFFICIENT",
94
+ "AUTH_ORG_MISMATCH",
95
+ "AUTH_DEVICE_CODE_PENDING",
96
+ "AUTH_DEVICE_CODE_EXPIRED",
97
+ "AUTH_DEVICE_CODE_SLOW_DOWN",
98
+ "IDEMPOTENCY_KEY_MISSING",
99
+ "IDEMPOTENCY_KEY_CONFLICT",
100
+ "PLAN_FEATURE_LOCKED",
101
+ "PLAN_QUOTA_EXCEEDED",
102
+ "INSUFFICIENT_CREDITS",
103
+ "CREDIT_DEDUCTION_FAILED",
104
+ "RATE_LIMITED",
105
+ "INPUT_INVALID_FORMAT",
106
+ "INPUT_TOO_LONG",
107
+ "WORKFLOW_VALIDATION_FAILED",
108
+ "WORKFLOW_NODE_TYPE_UNKNOWN",
109
+ "CANVAS_REV_MISMATCH",
110
+ "CANVAS_REV_REQUIRED",
111
+ "WORKER_UNREACHABLE",
112
+ "IDEMPOTENCY_IN_PROGRESS",
113
+ "TOKEN_NAME_INVALID",
114
+ "WORKFLOW_NOT_FOUND",
115
+ "NOT_FOUND",
116
+ "NODE_TYPE_NOT_FOUND",
117
+ "FEATURE_DISABLED",
118
+ "FORBIDDEN",
119
+ "MODEL_NOT_FOUND",
120
+ "PROVIDER_UNAVAILABLE",
121
+ "STORAGE_UPLOAD_FAILED",
122
+ "STORAGE_QUOTA_EXCEEDED",
123
+ "GEN_CONTENT_REFUSAL",
124
+ "GEN_OUTPUT_SCHEMA_MISMATCH",
125
+ "GEN_EMPTY_OUTPUT",
126
+ "GEN_ITEM_COUNT_MISMATCH",
127
+ "AGENT_FINGERPRINT_REJECTED",
128
+ "INTERNAL_ERROR",
129
+ "BRAND_MODEL_NOT_FOUND",
130
+ "BRAND_MODEL_NOT_A_MODEL",
131
+ "BRAND_MODEL_NOT_ACTIVE",
132
+ "BRAND_MODEL_NO_REFERENCES",
133
+ "BRAND_MODEL_POSE_NOT_FOUND",
134
+ "BRAND_MODEL_DUPLICATE_ID",
135
+ "MODEL_DOES_NOT_SUPPORT_REFERENCES",
136
+ "REFERENCE_LIMIT_EXCEEDED",
137
+ "PRODUCT_NOT_FOUND",
138
+ "PRODUCT_DUPLICATE_REFERENCE",
139
+ "PRODUCT_IMAGE_NOT_FOUND",
140
+ "PRODUCT_IMAGE_MISMATCH",
141
+ "PRODUCT_SYNC_ERROR",
142
+ "PRODUCT_DELETED_UPSTREAM",
143
+ "PRODUCT_NO_PRIMARY_IMAGE",
144
+ "PRODUCT_SOURCE_MANAGED"
145
+ ];
146
+ var KNOWN_SERVER_CODES = new Set(SERVER_ERROR_CODES);
147
+ var ApiError = class extends Error {
148
+ code;
149
+ meta;
150
+ cause;
151
+ constructor(code, message, meta, cause) {
152
+ super(message);
153
+ this.name = "ApiError";
154
+ this.code = code;
155
+ this.meta = meta;
156
+ this.cause = cause;
499
157
  }
500
158
  };
501
- var CLI_ONLY_CODES = /* @__PURE__ */ new Set([
502
- "NETWORK_ERROR",
503
- "TIMEOUT",
504
- "PARSE_ERROR",
505
- "SCHEMA_MISMATCH"
506
- ]);
507
- var KNOWN_SERVER_CODES = new Set(
508
- Object.keys(EXIT_CODE_MAP).filter((k) => !CLI_ONLY_CODES.has(k))
509
- );
510
- function getExitCode(code) {
511
- return EXIT_CODE_MAP[code]?.exitCode ?? 1;
159
+ async function longPoll(opts) {
160
+ const now = opts.now ?? Date.now;
161
+ const started = now();
162
+ for (; ; ) {
163
+ if (opts.signal?.aborted) {
164
+ throw new ApiError("TIMEOUT", "Wait was aborted.", { retryable: true });
165
+ }
166
+ const elapsedBefore = now() - started;
167
+ if (elapsedBefore >= opts.timeoutMs) {
168
+ throw new ApiError(
169
+ "TIMEOUT",
170
+ `Wait exceeded ${opts.timeoutMs}ms without reaching a terminal state.`,
171
+ { retryable: true }
172
+ );
173
+ }
174
+ const remainingMs = opts.timeoutMs - elapsedBefore;
175
+ const iterationSignal = composeIterationSignal(opts.signal, remainingMs);
176
+ let result;
177
+ try {
178
+ result = await opts.fetcher({ signal: iterationSignal });
179
+ } catch (err) {
180
+ if (opts.signal?.aborted || now() - started >= opts.timeoutMs) {
181
+ throw new ApiError(
182
+ "TIMEOUT",
183
+ `Wait exceeded ${opts.timeoutMs}ms without reaching a terminal state.`,
184
+ { retryable: true }
185
+ );
186
+ }
187
+ throw err;
188
+ }
189
+ if (opts.signal?.aborted) {
190
+ throw new ApiError("TIMEOUT", "Wait was aborted.", { retryable: true });
191
+ }
192
+ const elapsedAfter = now() - started;
193
+ if (elapsedAfter >= opts.timeoutMs) {
194
+ throw new ApiError(
195
+ "TIMEOUT",
196
+ `Wait exceeded ${opts.timeoutMs}ms without reaching a terminal state.`,
197
+ { retryable: true }
198
+ );
199
+ }
200
+ if (opts.isTerminal(result)) {
201
+ return result;
202
+ }
203
+ }
512
204
  }
513
- function toEnvelopeCode(code) {
514
- return code.startsWith("E_") ? code : `E_${code}`;
205
+ function composeIterationSignal(userSignal, remainingMs) {
206
+ const deadlineSignal = AbortSignal.timeout(Math.max(remainingMs, 0));
207
+ return userSignal ? AbortSignal.any([userSignal, deadlineSignal]) : deadlineSignal;
515
208
  }
516
- var SIGINT_EXIT_CODE = 130;
517
-
518
- // src/errors/schema-mismatch.ts
519
209
  var MAX_ISSUES = 5;
520
210
  function formatSchemaIssues(error) {
521
211
  const issues = error.issues;
@@ -539,9 +229,6 @@ function formatPath(path) {
539
229
  if (path.length === 0) return "(root)";
540
230
  return path.map(String).join(".");
541
231
  }
542
-
543
- // src/api/idempotency.ts
544
- import { createHash, randomUUID } from "crypto";
545
232
  function generateIdempotencyKey() {
546
233
  return randomUUID();
547
234
  }
@@ -549,8 +236,6 @@ function generateUploadIdempotencyKey(content, scope) {
549
236
  const hash = createHash("sha256").update(content).digest("hex");
550
237
  return `upload-${scope}-${hash}`;
551
238
  }
552
-
553
- // src/api/retry.ts
554
239
  var DEFAULT_RETRY_OPTIONS = {
555
240
  maxAttempts: 3,
556
241
  baseDelayMs: 200,
@@ -564,7 +249,7 @@ function computeBackoffMs(attempt, baseDelayMs, maxDelayMs, jitterFraction) {
564
249
  return Math.max(0, Math.round(exp + jitter));
565
250
  }
566
251
  function decideRetry(params) {
567
- const { attempt, options, serverRetryAfterMs, isNetworkError, isRetryable } = params;
252
+ const { attempt, options, serverRetryAfterMs, isNetworkError, isRetryable, onWarning: onWarning2 } = params;
568
253
  if (options.signal?.aborted) {
569
254
  return { shouldRetry: false, delayMs: 0, reason: "aborted" };
570
255
  }
@@ -577,7 +262,7 @@ function decideRetry(params) {
577
262
  if (typeof serverRetryAfterMs === "number" && serverRetryAfterMs >= 0) {
578
263
  let delayMs2 = serverRetryAfterMs;
579
264
  if (delayMs2 > ABSOLUTE_MAX_RETRY_AFTER_MS) {
580
- console.warn(`Server Retry-After exceeded ${ABSOLUTE_MAX_RETRY_AFTER_MS}ms; clipping.`);
265
+ onWarning2?.(`Server Retry-After exceeded ${ABSOLUTE_MAX_RETRY_AFTER_MS}ms; clipping.`);
581
266
  delayMs2 = ABSOLUTE_MAX_RETRY_AFTER_MS;
582
267
  }
583
268
  return { shouldRetry: true, delayMs: delayMs2, reason: "rate_limited" };
@@ -617,9 +302,6 @@ function signalToAbortError(signal) {
617
302
  err.name = "AbortError";
618
303
  return err;
619
304
  }
620
-
621
- // src/api/types.ts
622
- import { z as z5 } from "zod";
623
305
  var ServerSuccessSchema = (payload) => z5.object({ data: payload }).strict();
624
306
  var ServerErrorBodySchema = z5.object({
625
307
  error: z5.object({
@@ -630,20 +312,6 @@ var ServerErrorBodySchema = z5.object({
630
312
  details: z5.unknown().optional()
631
313
  }).strict()
632
314
  }).strict();
633
-
634
- // src/api/client.ts
635
- var ApiError = class extends Error {
636
- code;
637
- meta;
638
- cause;
639
- constructor(code, message, meta, cause) {
640
- super(message);
641
- this.name = "ApiError";
642
- this.code = code;
643
- this.meta = meta;
644
- this.cause = cause;
645
- }
646
- };
647
315
  var REDACT_BODY_LIMIT = 1024;
648
316
  var REDACT_INPUT_CAP = 100 * 1024;
649
317
  var DEFAULT_USER_AGENT = "clickraft-cli";
@@ -700,7 +368,8 @@ var ApiClient = class {
700
368
  options: { ...retry, signal },
701
369
  serverRetryAfterMs: err.meta.retryAfterMs,
702
370
  isNetworkError,
703
- isRetryable
371
+ isRetryable,
372
+ onWarning: this.options.onWarning
704
373
  });
705
374
  if (!decision.shouldRetry) throw err;
706
375
  try {
@@ -788,178 +457,613 @@ var ApiClient = class {
788
457
  bodyText,
789
458
  statusCode,
790
459
  requestId,
791
- retryAfterHeader: pickHeader(responseHeaders, "retry-after")
460
+ retryAfterHeader: pickHeader(responseHeaders, "retry-after"),
461
+ onWarning: this.options.onWarning,
462
+ isRetryableServerCode: this.options.isRetryableServerCode
792
463
  });
793
464
  }
794
465
  };
795
- function parseSuccessBody(args) {
796
- let parsed;
797
- try {
798
- parsed = JSON.parse(args.bodyText);
799
- } catch (err) {
800
- throw new ApiError(
801
- "PARSE_ERROR",
802
- "Server returned a non-JSON response.",
803
- {
804
- status: args.statusCode,
805
- requestId: args.requestId,
806
- retryable: false,
807
- redactedBody: redactAndTruncate(args.bodyText)
808
- },
809
- err
810
- );
811
- }
812
- const envelope = ServerSuccessSchema(args.responseSchema ?? z6.unknown()).safeParse(parsed);
813
- if (!envelope.success) {
814
- const issues = formatSchemaIssues(envelope.error);
815
- throw new ApiError(
816
- "SCHEMA_MISMATCH",
817
- `Server response did not match the expected schema. Issues: ${issues}`,
818
- {
819
- status: args.statusCode,
820
- requestId: args.requestId,
821
- retryable: false,
822
- redactedBody: redactAndTruncate(args.bodyText)
823
- }
824
- );
825
- }
826
- return envelope.data.data;
827
- }
828
- function parseErrorBody(args) {
829
- const redactedBody = redactAndTruncate(args.bodyText);
830
- let parsed;
831
- try {
832
- parsed = JSON.parse(args.bodyText);
833
- } catch {
834
- return new ApiError(
835
- "PARSE_ERROR",
836
- `Server returned a ${args.statusCode} response with a non-JSON body.`,
837
- {
838
- status: args.statusCode,
839
- requestId: args.requestId,
840
- retryable: args.statusCode >= 500,
841
- redactedBody
842
- }
843
- );
844
- }
845
- const envelope = ServerErrorBodySchema.safeParse(parsed);
846
- if (!envelope.success) {
847
- return new ApiError(
848
- "PARSE_ERROR",
849
- `Server returned ${args.statusCode} but body did not match the error envelope.`,
850
- {
851
- status: args.statusCode,
852
- requestId: args.requestId,
853
- retryable: args.statusCode >= 500,
854
- redactedBody
855
- }
856
- );
857
- }
858
- return apiErrorFromServerBody({
859
- body: envelope.data,
860
- statusCode: args.statusCode,
861
- requestId: args.requestId,
862
- retryAfterHeader: args.retryAfterHeader,
863
- redactedBody
864
- });
865
- }
866
- function apiErrorFromServerBody(args) {
867
- const { error } = args.body;
868
- let code;
869
- let safeMessage;
870
- if (KNOWN_SERVER_CODES.has(error.code)) {
871
- code = error.code;
872
- safeMessage = redactTokenLikeFields(error.message);
873
- } else {
874
- const redactedRawCode = redactTokenLikeFields(error.code);
875
- console.warn(`Unknown server error code: ${redactedRawCode}. Treating as INTERNAL_ERROR.`);
876
- code = "INTERNAL_ERROR";
877
- safeMessage = `Unknown server error code: ${redactedRawCode}.`;
878
- }
879
- const mapping = EXIT_CODE_MAP[code];
880
- const retryable = mapping?.retryable ?? (args.statusCode >= 500 || args.statusCode === 429);
881
- const retryAfterMs = typeof error.retryAfter === "number" ? error.retryAfter * 1e3 : parseRetryAfterHeader(args.retryAfterHeader);
882
- return new ApiError(code, safeMessage, {
883
- status: args.statusCode,
884
- requestId: error.correlationId ?? args.requestId,
885
- retryable,
886
- retryAfterMs,
887
- redactedBody: args.redactedBody,
888
- details: error.details
889
- });
890
- }
891
- function parseRetryAfterHeader(headerValue) {
892
- if (!headerValue) return void 0;
893
- const trimmed = headerValue.trim();
894
- if (/^\d+$/.test(trimmed)) {
895
- return Number.parseInt(trimmed, 10) * 1e3;
896
- }
897
- if (/[A-Za-z]/.test(trimmed)) {
898
- const parsed = Date.parse(trimmed);
899
- if (!Number.isNaN(parsed)) {
900
- return Math.max(0, parsed - Date.now());
901
- }
902
- }
903
- return void 0;
904
- }
905
- function buildUrl(baseUrl, path, query) {
906
- const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
907
- const relative = path.startsWith("/") ? path.slice(1) : path;
908
- const url = new URL(relative, base);
909
- if (query) {
910
- for (const [key, value] of Object.entries(query)) {
911
- if (value === void 0) continue;
912
- if (Array.isArray(value)) {
913
- for (const item of value) url.searchParams.append(key, item);
914
- continue;
915
- }
916
- url.searchParams.append(key, String(value));
917
- }
918
- }
919
- return url.toString();
920
- }
921
- function serializeBody(opts) {
922
- if (opts.body === void 0) return void 0;
923
- if (opts.method === "GET") return void 0;
924
- return JSON.stringify(opts.body);
925
- }
926
- function composeSignals(signals) {
927
- const present = signals.filter((s) => s !== void 0);
928
- if (present.length === 0) return new AbortController().signal;
929
- if (present.length === 1) return present[0];
930
- const any = AbortSignal.any;
931
- if (typeof any === "function") return any(present);
932
- const ctrl = new AbortController();
933
- for (const s of present) {
934
- if (s.aborted) {
935
- ctrl.abort(s.reason);
936
- break;
937
- }
938
- s.addEventListener("abort", () => ctrl.abort(s.reason), { once: true });
939
- }
940
- return ctrl.signal;
941
- }
942
- function pickHeader(headers, name) {
943
- const value = headers[name] ?? headers[name.toLowerCase()];
944
- if (Array.isArray(value)) return value[0];
945
- return value;
946
- }
947
- function redactAndTruncate(body) {
948
- const input = body.length > REDACT_INPUT_CAP ? body.slice(0, REDACT_INPUT_CAP) : body;
949
- const redacted = redactTokenLikeFields(input);
950
- return redacted.length > REDACT_BODY_LIMIT ? `${redacted.slice(0, REDACT_BODY_LIMIT)}\u2026` : redacted;
951
- }
952
- function isAbortError(err) {
953
- if (!(err instanceof Error)) return false;
954
- return err.name === "AbortError" || /aborted/i.test(err.message);
466
+ function createClient(options) {
467
+ return new ApiClient(options);
468
+ }
469
+ function parseSuccessBody(args) {
470
+ let parsed;
471
+ try {
472
+ parsed = JSON.parse(args.bodyText);
473
+ } catch (err) {
474
+ throw new ApiError(
475
+ "PARSE_ERROR",
476
+ "Server returned a non-JSON response.",
477
+ {
478
+ status: args.statusCode,
479
+ requestId: args.requestId,
480
+ retryable: false,
481
+ redactedBody: redactAndTruncate(args.bodyText)
482
+ },
483
+ err
484
+ );
485
+ }
486
+ const envelope = ServerSuccessSchema(args.responseSchema ?? z6.unknown()).safeParse(parsed);
487
+ if (!envelope.success) {
488
+ const issues = formatSchemaIssues(envelope.error);
489
+ throw new ApiError(
490
+ "SCHEMA_MISMATCH",
491
+ `Server response did not match the expected schema. Issues: ${issues}`,
492
+ {
493
+ status: args.statusCode,
494
+ requestId: args.requestId,
495
+ retryable: false,
496
+ redactedBody: redactAndTruncate(args.bodyText)
497
+ }
498
+ );
499
+ }
500
+ return envelope.data.data;
501
+ }
502
+ function parseErrorBody(args) {
503
+ const redactedBody = redactAndTruncate(args.bodyText);
504
+ let parsed;
505
+ try {
506
+ parsed = JSON.parse(args.bodyText);
507
+ } catch {
508
+ return new ApiError(
509
+ "PARSE_ERROR",
510
+ `Server returned a ${args.statusCode} response with a non-JSON body.`,
511
+ {
512
+ status: args.statusCode,
513
+ requestId: args.requestId,
514
+ retryable: args.statusCode >= 500,
515
+ redactedBody
516
+ }
517
+ );
518
+ }
519
+ const envelope = ServerErrorBodySchema.safeParse(parsed);
520
+ if (!envelope.success) {
521
+ return new ApiError(
522
+ "PARSE_ERROR",
523
+ `Server returned ${args.statusCode} but body did not match the error envelope.`,
524
+ {
525
+ status: args.statusCode,
526
+ requestId: args.requestId,
527
+ retryable: args.statusCode >= 500,
528
+ redactedBody
529
+ }
530
+ );
531
+ }
532
+ return apiErrorFromServerBody({
533
+ body: envelope.data,
534
+ statusCode: args.statusCode,
535
+ requestId: args.requestId,
536
+ retryAfterHeader: args.retryAfterHeader,
537
+ redactedBody,
538
+ onWarning: args.onWarning,
539
+ isRetryableServerCode: args.isRetryableServerCode
540
+ });
541
+ }
542
+ function apiErrorFromServerBody(args) {
543
+ const { error } = args.body;
544
+ let code;
545
+ let safeMessage;
546
+ if (KNOWN_SERVER_CODES.has(error.code)) {
547
+ code = error.code;
548
+ safeMessage = redactTokenLikeFields(error.message);
549
+ } else {
550
+ const redactedRawCode = redactTokenLikeFields(error.code);
551
+ args.onWarning?.(`Unknown server error code: ${redactedRawCode}. Treating as INTERNAL_ERROR.`);
552
+ code = "INTERNAL_ERROR";
553
+ safeMessage = `Unknown server error code: ${redactedRawCode}.`;
554
+ }
555
+ const retryable = args.isRetryableServerCode?.(code) ?? (args.statusCode >= 500 || args.statusCode === 429);
556
+ const retryAfterMs = typeof error.retryAfter === "number" ? error.retryAfter * 1e3 : parseRetryAfterHeader(args.retryAfterHeader);
557
+ return new ApiError(code, safeMessage, {
558
+ status: args.statusCode,
559
+ requestId: error.correlationId ?? args.requestId,
560
+ retryable,
561
+ retryAfterMs,
562
+ redactedBody: args.redactedBody,
563
+ details: error.details
564
+ });
565
+ }
566
+ function parseRetryAfterHeader(headerValue) {
567
+ if (!headerValue) return void 0;
568
+ const trimmed = headerValue.trim();
569
+ if (/^\d+$/.test(trimmed)) {
570
+ return Number.parseInt(trimmed, 10) * 1e3;
571
+ }
572
+ if (/[A-Za-z]/.test(trimmed)) {
573
+ const parsed = Date.parse(trimmed);
574
+ if (!Number.isNaN(parsed)) {
575
+ return Math.max(0, parsed - Date.now());
576
+ }
577
+ }
578
+ return void 0;
579
+ }
580
+ function buildUrl(baseUrl, path, query) {
581
+ const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
582
+ const relative = path.startsWith("/") ? path.slice(1) : path;
583
+ const url = new URL(relative, base);
584
+ if (query) {
585
+ for (const [key, value] of Object.entries(query)) {
586
+ if (value === void 0) continue;
587
+ if (Array.isArray(value)) {
588
+ for (const item of value) url.searchParams.append(key, item);
589
+ continue;
590
+ }
591
+ url.searchParams.append(key, String(value));
592
+ }
593
+ }
594
+ return url.toString();
595
+ }
596
+ function serializeBody(opts) {
597
+ if (opts.body === void 0) return void 0;
598
+ if (opts.method === "GET") return void 0;
599
+ return JSON.stringify(opts.body);
600
+ }
601
+ function composeSignals(signals) {
602
+ const present = signals.filter((s) => s !== void 0);
603
+ if (present.length === 0) return new AbortController().signal;
604
+ if (present.length === 1) return present[0];
605
+ const any = AbortSignal.any;
606
+ if (typeof any === "function") return any(present);
607
+ const ctrl = new AbortController();
608
+ for (const s of present) {
609
+ if (s.aborted) {
610
+ ctrl.abort(s.reason);
611
+ break;
612
+ }
613
+ s.addEventListener("abort", () => ctrl.abort(s.reason), { once: true });
614
+ }
615
+ return ctrl.signal;
616
+ }
617
+ function pickHeader(headers, name) {
618
+ const value = headers[name] ?? headers[name.toLowerCase()];
619
+ if (Array.isArray(value)) return value[0];
620
+ return value;
621
+ }
622
+ function redactAndTruncate(body) {
623
+ const input = body.length > REDACT_INPUT_CAP ? body.slice(0, REDACT_INPUT_CAP) : body;
624
+ const redacted = redactTokenLikeFields(input);
625
+ return redacted.length > REDACT_BODY_LIMIT ? `${redacted.slice(0, REDACT_BODY_LIMIT)}\u2026` : redacted;
626
+ }
627
+ function isAbortError(err) {
628
+ if (!(err instanceof Error)) return false;
629
+ return err.name === "AbortError" || /aborted/i.test(err.message);
630
+ }
631
+ function toAbortApiError(signal, cause) {
632
+ const reasonMessage = signal.reason instanceof Error ? signal.reason.message : "Request was aborted.";
633
+ return new ApiError("TIMEOUT", reasonMessage, { retryable: true }, cause);
634
+ }
635
+ function errorMessage(err) {
636
+ if (err instanceof Error) return err.message;
637
+ return String(err);
638
+ }
639
+
640
+ // src/errors/codes.ts
641
+ var LOGIN_HINT = "Run `clickraft login` to authenticate.";
642
+ var RELOGIN_HINT = "Run `clickraft login` to refresh.";
643
+ var EXIT_CODE_MAP = {
644
+ // --- 37 Phase 8 Q7 codes ----------------------------------------------------
645
+ AUTH_TOKEN_MISSING: {
646
+ exitCode: 3,
647
+ category: "auth",
648
+ retryable: false,
649
+ hint: "Set `CLICKRAFT_TOKEN` or run `clickraft login`."
650
+ },
651
+ AUTH_TOKEN_INVALID: {
652
+ exitCode: 3,
653
+ category: "auth",
654
+ retryable: false,
655
+ hint: "The token is malformed or unknown. Run `clickraft login`."
656
+ },
657
+ AUTH_TOKEN_EXPIRED: {
658
+ exitCode: 3,
659
+ category: "auth",
660
+ retryable: false,
661
+ hint: RELOGIN_HINT
662
+ },
663
+ AUTH_TOKEN_REVOKED: {
664
+ exitCode: 3,
665
+ category: "auth",
666
+ retryable: false,
667
+ hint: "Token was revoked. Run `clickraft login` to mint a new one."
668
+ },
669
+ AUTH_TOKEN_SCOPE_INSUFFICIENT: {
670
+ exitCode: 4,
671
+ category: "permission",
672
+ retryable: false,
673
+ hint: "This token does not have the required scope. Re-run `clickraft login` selecting the needed scopes."
674
+ },
675
+ AUTH_ORG_MISMATCH: {
676
+ exitCode: 4,
677
+ category: "permission",
678
+ retryable: false,
679
+ hint: "This token's org no longer exists or you have been removed."
680
+ },
681
+ AUTH_DEVICE_CODE_PENDING: {
682
+ exitCode: 0,
683
+ category: "success",
684
+ retryable: false,
685
+ hint: "Continue polling."
686
+ },
687
+ AUTH_DEVICE_CODE_EXPIRED: {
688
+ exitCode: 3,
689
+ category: "auth",
690
+ retryable: false,
691
+ hint: "Device code expired. Re-run `clickraft login`."
692
+ },
693
+ AUTH_DEVICE_CODE_SLOW_DOWN: {
694
+ exitCode: 0,
695
+ category: "success",
696
+ retryable: false,
697
+ hint: "Increase polling interval (Retry-After)."
698
+ },
699
+ IDEMPOTENCY_KEY_MISSING: {
700
+ exitCode: 2,
701
+ category: "usage",
702
+ retryable: false,
703
+ hint: "Add `X-Idempotency-Key` header (or `--idempotency-key`)."
704
+ },
705
+ IDEMPOTENCY_KEY_CONFLICT: {
706
+ exitCode: 7,
707
+ category: "conflict",
708
+ retryable: false,
709
+ hint: "Same key was used with a different request body. Use a new key."
710
+ },
711
+ PLAN_FEATURE_LOCKED: {
712
+ exitCode: 4,
713
+ category: "permission",
714
+ retryable: false,
715
+ hint: "API access is not enabled on your plan. Visit billing settings."
716
+ },
717
+ PLAN_QUOTA_EXCEEDED: {
718
+ exitCode: 5,
719
+ category: "quota",
720
+ retryable: false,
721
+ hint: "Daily API quota exhausted. Wait, or upgrade your plan."
722
+ },
723
+ INSUFFICIENT_CREDITS: {
724
+ exitCode: 5,
725
+ category: "quota",
726
+ retryable: false,
727
+ hint: "Top up credits to retry."
728
+ },
729
+ CREDIT_DEDUCTION_FAILED: {
730
+ exitCode: 1,
731
+ category: "runtime",
732
+ retryable: true,
733
+ hint: "Could not charge credits. Retry; no charge was made."
734
+ },
735
+ RATE_LIMITED: {
736
+ exitCode: 5,
737
+ category: "quota",
738
+ retryable: true,
739
+ hint: "Slow down. Honor Retry-After."
740
+ },
741
+ INPUT_INVALID_FORMAT: {
742
+ exitCode: 2,
743
+ category: "usage",
744
+ retryable: false,
745
+ hint: "Check input format and try again."
746
+ },
747
+ INPUT_TOO_LONG: {
748
+ exitCode: 2,
749
+ category: "usage",
750
+ retryable: false,
751
+ hint: "Field exceeded max length."
752
+ },
753
+ WORKFLOW_VALIDATION_FAILED: {
754
+ exitCode: 2,
755
+ category: "usage",
756
+ retryable: false,
757
+ hint: "Run `workflow validate` for the full list of errors."
758
+ },
759
+ WORKFLOW_NODE_TYPE_UNKNOWN: {
760
+ exitCode: 2,
761
+ category: "usage",
762
+ retryable: false,
763
+ hint: "Run `clickraft nodes list` for the current set of node types."
764
+ },
765
+ CANVAS_REV_MISMATCH: {
766
+ exitCode: 7,
767
+ category: "conflict",
768
+ retryable: false,
769
+ hint: "The workflow was updated elsewhere. Re-fetch and retry with the current canvas_rev."
770
+ },
771
+ CANVAS_REV_REQUIRED: {
772
+ exitCode: 2,
773
+ category: "usage",
774
+ retryable: false,
775
+ hint: "Include --canvas-rev or use --auto-rev."
776
+ },
777
+ WORKER_UNREACHABLE: {
778
+ exitCode: 6,
779
+ category: "network",
780
+ retryable: true,
781
+ hint: "Collaboration service temporarily unavailable. Retry."
782
+ },
783
+ IDEMPOTENCY_IN_PROGRESS: {
784
+ exitCode: 7,
785
+ category: "conflict",
786
+ retryable: false,
787
+ hint: "A request with this idempotency key is currently in-flight. Retry in a moment."
788
+ },
789
+ TOKEN_NAME_INVALID: {
790
+ exitCode: 2,
791
+ category: "usage",
792
+ retryable: false,
793
+ hint: "Agent token name is malformed."
794
+ },
795
+ WORKFLOW_NOT_FOUND: {
796
+ exitCode: 2,
797
+ category: "usage",
798
+ retryable: false,
799
+ hint: "No workflow with that ID in your org."
800
+ },
801
+ NOT_FOUND: {
802
+ exitCode: 2,
803
+ category: "usage",
804
+ retryable: false,
805
+ hint: "Resource not found."
806
+ },
807
+ NODE_TYPE_NOT_FOUND: {
808
+ exitCode: 2,
809
+ category: "usage",
810
+ retryable: false,
811
+ hint: "Run `clickraft nodes list` for the current set of node types."
812
+ },
813
+ FEATURE_DISABLED: {
814
+ exitCode: 2,
815
+ category: "usage",
816
+ retryable: false,
817
+ hint: "This endpoint is not enabled. Contact support if you believe this is in error."
818
+ },
819
+ FORBIDDEN: {
820
+ exitCode: 4,
821
+ category: "permission",
822
+ retryable: false,
823
+ hint: "Your role does not permit this action."
824
+ },
825
+ MODEL_NOT_FOUND: {
826
+ exitCode: 2,
827
+ category: "usage",
828
+ retryable: false,
829
+ hint: "Unknown `modelSlug`. Run `brand-model list` for the current set."
830
+ },
831
+ PROVIDER_UNAVAILABLE: {
832
+ exitCode: 6,
833
+ category: "network",
834
+ retryable: true,
835
+ hint: "Provider is temporarily unavailable. Retry shortly."
836
+ },
837
+ STORAGE_UPLOAD_FAILED: {
838
+ exitCode: 1,
839
+ category: "runtime",
840
+ retryable: true,
841
+ hint: "Upload failed mid-stream. Retry."
842
+ },
843
+ STORAGE_QUOTA_EXCEEDED: {
844
+ exitCode: 5,
845
+ category: "quota",
846
+ retryable: false,
847
+ hint: "Org storage quota exceeded. Free space or upgrade."
848
+ },
849
+ GEN_CONTENT_REFUSAL: {
850
+ exitCode: 1,
851
+ category: "runtime",
852
+ retryable: false,
853
+ hint: "The model declined the request via safety policy."
854
+ },
855
+ GEN_OUTPUT_SCHEMA_MISMATCH: {
856
+ exitCode: 1,
857
+ category: "runtime",
858
+ retryable: true,
859
+ hint: "Provider returned malformed output. Full refund issued. Retry."
860
+ },
861
+ GEN_EMPTY_OUTPUT: {
862
+ exitCode: 1,
863
+ category: "runtime",
864
+ retryable: true,
865
+ hint: "Provider returned no usable output. Full refund issued. Retry."
866
+ },
867
+ GEN_ITEM_COUNT_MISMATCH: {
868
+ exitCode: 0,
869
+ category: "success",
870
+ retryable: false,
871
+ hint: "Partial output kept; no refund."
872
+ },
873
+ AGENT_FINGERPRINT_REJECTED: {
874
+ exitCode: 4,
875
+ category: "permission",
876
+ retryable: false,
877
+ hint: "This client identification is not permitted. Contact support."
878
+ },
879
+ INTERNAL_ERROR: {
880
+ exitCode: 1,
881
+ category: "runtime",
882
+ retryable: true,
883
+ hint: "Server error. Retry; if it persists, contact support with the request ID."
884
+ },
885
+ CREDENTIALS_LOCKED: {
886
+ exitCode: 1,
887
+ category: "runtime",
888
+ retryable: true,
889
+ hint: "Another CLI invocation holds the credentials lock. Retry shortly."
890
+ },
891
+ CREDENTIALS_PERMISSIONS_LOOSE: {
892
+ exitCode: 0,
893
+ category: "success",
894
+ retryable: false,
895
+ hint: "Credentials file mode is wider than 0600; continuing."
896
+ },
897
+ // --- 5 additional codes introduced by 1A.1 ---------------------------------
898
+ AUTH_ACCESS_DENIED: {
899
+ exitCode: 3,
900
+ category: "auth",
901
+ retryable: false,
902
+ hint: "You denied the OAuth grant. Re-run `clickraft login` to retry."
903
+ },
904
+ AUTH_FLOW_CANCELLED: {
905
+ exitCode: 3,
906
+ category: "auth",
907
+ retryable: false,
908
+ hint: "Authentication was cancelled. Re-run `clickraft login`."
909
+ },
910
+ CREDENTIALS_CORRUPT: {
911
+ exitCode: 1,
912
+ category: "runtime",
913
+ retryable: false,
914
+ hint: "Credentials file is unreadable. Remove it and run `clickraft login`."
915
+ },
916
+ CREDENTIALS_NOT_FOUND: {
917
+ exitCode: 3,
918
+ category: "auth",
919
+ retryable: false,
920
+ hint: LOGIN_HINT
921
+ },
922
+ CREDENTIALS_PROFILE_NOT_FOUND: {
923
+ exitCode: 2,
924
+ category: "usage",
925
+ retryable: false,
926
+ hint: "No such profile. Use `clickraft auth profiles` to list available profiles."
927
+ },
928
+ // --- 8 brand model resolution codes -----------------------------------------
929
+ BRAND_MODEL_NOT_FOUND: {
930
+ exitCode: 1,
931
+ category: "runtime",
932
+ retryable: false,
933
+ hint: "No brand model with that ID. Run `clickraft brand-model list`."
934
+ },
935
+ BRAND_MODEL_NOT_A_MODEL: {
936
+ exitCode: 1,
937
+ category: "runtime",
938
+ retryable: false,
939
+ hint: "The specified asset is not a brand model."
940
+ },
941
+ BRAND_MODEL_NOT_ACTIVE: {
942
+ exitCode: 1,
943
+ category: "runtime",
944
+ retryable: false,
945
+ hint: "Brand model is not active. Check its status in the dashboard."
946
+ },
947
+ BRAND_MODEL_NO_REFERENCES: {
948
+ exitCode: 1,
949
+ category: "runtime",
950
+ retryable: false,
951
+ hint: "Brand model has no reference images. Upload references first."
952
+ },
953
+ BRAND_MODEL_POSE_NOT_FOUND: {
954
+ exitCode: 1,
955
+ category: "runtime",
956
+ retryable: false,
957
+ hint: "Requested pose not found. Valid: front, 3/4-right, right, left, 3/4-left, back, approved."
958
+ },
959
+ BRAND_MODEL_DUPLICATE_ID: {
960
+ exitCode: 2,
961
+ category: "usage",
962
+ retryable: false,
963
+ hint: "Same brand model ID appears more than once."
964
+ },
965
+ MODEL_DOES_NOT_SUPPORT_REFERENCES: {
966
+ exitCode: 2,
967
+ category: "usage",
968
+ retryable: false,
969
+ hint: "This model does not support brand model references."
970
+ },
971
+ REFERENCE_LIMIT_EXCEEDED: {
972
+ exitCode: 2,
973
+ category: "usage",
974
+ retryable: false,
975
+ hint: "Too many reference images. Check model limits."
976
+ },
977
+ // --- 8 product reference resolver codes -------------------------------------
978
+ PRODUCT_NOT_FOUND: {
979
+ exitCode: 1,
980
+ category: "runtime",
981
+ retryable: false,
982
+ hint: "No product with that ID in your org. Run `clickraft product list`."
983
+ },
984
+ PRODUCT_DUPLICATE_REFERENCE: {
985
+ exitCode: 2,
986
+ category: "usage",
987
+ retryable: false,
988
+ hint: "Same (productId, imageId) tuple appears twice in the request."
989
+ },
990
+ PRODUCT_IMAGE_NOT_FOUND: {
991
+ exitCode: 1,
992
+ category: "runtime",
993
+ retryable: false,
994
+ hint: "imageId does not exist in the product images."
995
+ },
996
+ PRODUCT_IMAGE_MISMATCH: {
997
+ exitCode: 2,
998
+ category: "usage",
999
+ retryable: false,
1000
+ hint: "imageId belongs to a different product."
1001
+ },
1002
+ PRODUCT_SYNC_ERROR: {
1003
+ exitCode: 1,
1004
+ category: "runtime",
1005
+ retryable: false,
1006
+ hint: "Product sync status is 'error'. Re-sync or use a different product."
1007
+ },
1008
+ PRODUCT_DELETED_UPSTREAM: {
1009
+ exitCode: 1,
1010
+ category: "runtime",
1011
+ retryable: false,
1012
+ hint: "Product was deleted upstream. Remove it or re-sync."
1013
+ },
1014
+ PRODUCT_NO_PRIMARY_IMAGE: {
1015
+ exitCode: 1,
1016
+ category: "runtime",
1017
+ retryable: false,
1018
+ hint: "Product has no primary image. Specify --product <id>:<imageId> explicitly."
1019
+ },
1020
+ PRODUCT_SOURCE_MANAGED: {
1021
+ exitCode: 4,
1022
+ category: "permission",
1023
+ retryable: false,
1024
+ hint: "Product is managed by an external integration."
1025
+ },
1026
+ // --- 4 CLI-synthetic codes --------------------------------------------------
1027
+ NETWORK_ERROR: {
1028
+ exitCode: 6,
1029
+ category: "network",
1030
+ retryable: true,
1031
+ hint: "Network connection failed. Check connectivity and retry."
1032
+ },
1033
+ TIMEOUT: {
1034
+ exitCode: 6,
1035
+ category: "network",
1036
+ retryable: true,
1037
+ hint: "Request timed out. Retry; consider increasing the timeout."
1038
+ },
1039
+ PARSE_ERROR: {
1040
+ exitCode: 1,
1041
+ category: "runtime",
1042
+ retryable: false,
1043
+ hint: "Server returned an unexpected response. Contact support with the request ID."
1044
+ },
1045
+ SCHEMA_MISMATCH: {
1046
+ exitCode: 1,
1047
+ category: "runtime",
1048
+ retryable: false,
1049
+ hint: "Server response did not match the expected schema. Update the CLI."
1050
+ }
1051
+ };
1052
+ function getExitCode(code) {
1053
+ return EXIT_CODE_MAP[code]?.exitCode ?? 1;
955
1054
  }
956
- function toAbortApiError(signal, cause) {
957
- const reasonMessage = signal.reason instanceof Error ? signal.reason.message : "Request was aborted.";
958
- return new ApiError("TIMEOUT", reasonMessage, { retryable: true }, cause);
1055
+ function toEnvelopeCode(code) {
1056
+ return code.startsWith("E_") ? code : `E_${code}`;
959
1057
  }
960
- function errorMessage(err) {
961
- if (err instanceof Error) return err.message;
962
- return String(err);
1058
+ var SIGINT_EXIT_CODE = 130;
1059
+
1060
+ // src/api/client.ts
1061
+ var isRetryableServerCode = (code) => EXIT_CODE_MAP[code].retryable;
1062
+ var onWarning = (msg) => {
1063
+ console.warn(msg);
1064
+ };
1065
+ function createApiClient(opts) {
1066
+ return createClient({ ...opts, onWarning, isRetryableServerCode });
963
1067
  }
964
1068
 
965
1069
  // src/auth/errors.ts
@@ -1388,7 +1492,7 @@ async function getBalance(options) {
1388
1492
  if (!config.accessToken) {
1389
1493
  throw new AuthError("AUTH_TOKEN_MISSING", `Not authenticated. Run \`clickraft login\` first.`);
1390
1494
  }
1391
- const factory = options.apiClientFactory ?? ((apiBaseUrl, accessToken) => new ApiClient({
1495
+ const factory = options.apiClientFactory ?? ((apiBaseUrl, accessToken) => createApiClient({
1392
1496
  apiBaseUrl,
1393
1497
  accessToken,
1394
1498
  signal: options.signal,
@@ -1414,7 +1518,7 @@ async function listBrandModels(options) {
1414
1518
  if (!config.accessToken) {
1415
1519
  throw new AuthError("AUTH_TOKEN_MISSING", `Not authenticated. Run \`clickraft login\` first.`);
1416
1520
  }
1417
- const factory = options.apiClientFactory ?? ((apiBaseUrl, accessToken) => new ApiClient({
1521
+ const factory = options.apiClientFactory ?? ((apiBaseUrl, accessToken) => createApiClient({
1418
1522
  apiBaseUrl,
1419
1523
  accessToken,
1420
1524
  signal: options.signal,
@@ -1579,7 +1683,7 @@ async function buildClient(opts) {
1579
1683
  flagApiBaseUrl: opts.apiBaseUrl,
1580
1684
  flagToken: opts.token
1581
1685
  });
1582
- return new ApiClient({
1686
+ return createApiClient({
1583
1687
  apiBaseUrl: cfg.apiBaseUrl,
1584
1688
  accessToken: cfg.accessToken,
1585
1689
  signal: opts.signal,
@@ -1746,7 +1850,7 @@ async function buildGenerateClient(opts) {
1746
1850
  flagApiBaseUrl: opts.apiBaseUrl,
1747
1851
  flagToken: opts.token
1748
1852
  });
1749
- return new ApiClient({
1853
+ return createApiClient({
1750
1854
  apiBaseUrl: cfg.apiBaseUrl,
1751
1855
  accessToken: cfg.accessToken,
1752
1856
  signal: opts.signal,
@@ -1757,58 +1861,6 @@ function isUrlLike(value) {
1757
1861
  return value.startsWith("http://") || value.startsWith("https://");
1758
1862
  }
1759
1863
 
1760
- // src/cli/long-poll.ts
1761
- async function longPoll(opts) {
1762
- const now = opts.now ?? Date.now;
1763
- const started = now();
1764
- for (; ; ) {
1765
- if (opts.signal?.aborted) {
1766
- throw new ApiError("TIMEOUT", "Wait was aborted.", { retryable: true });
1767
- }
1768
- const elapsedBefore = now() - started;
1769
- if (elapsedBefore >= opts.timeoutMs) {
1770
- throw new ApiError(
1771
- "TIMEOUT",
1772
- `Wait exceeded ${opts.timeoutMs}ms without reaching a terminal state.`,
1773
- { retryable: true }
1774
- );
1775
- }
1776
- const remainingMs = opts.timeoutMs - elapsedBefore;
1777
- const iterationSignal = composeIterationSignal(opts.signal, remainingMs);
1778
- let result;
1779
- try {
1780
- result = await opts.fetcher({ signal: iterationSignal });
1781
- } catch (err) {
1782
- if (opts.signal?.aborted || now() - started >= opts.timeoutMs) {
1783
- throw new ApiError(
1784
- "TIMEOUT",
1785
- `Wait exceeded ${opts.timeoutMs}ms without reaching a terminal state.`,
1786
- { retryable: true }
1787
- );
1788
- }
1789
- throw err;
1790
- }
1791
- if (opts.signal?.aborted) {
1792
- throw new ApiError("TIMEOUT", "Wait was aborted.", { retryable: true });
1793
- }
1794
- const elapsedAfter = now() - started;
1795
- if (elapsedAfter >= opts.timeoutMs) {
1796
- throw new ApiError(
1797
- "TIMEOUT",
1798
- `Wait exceeded ${opts.timeoutMs}ms without reaching a terminal state.`,
1799
- { retryable: true }
1800
- );
1801
- }
1802
- if (opts.isTerminal(result)) {
1803
- return result;
1804
- }
1805
- }
1806
- }
1807
- function composeIterationSignal(userSignal, remainingMs) {
1808
- const deadlineSignal = AbortSignal.timeout(Math.max(remainingMs, 0));
1809
- return userSignal ? AbortSignal.any([userSignal, deadlineSignal]) : deadlineSignal;
1810
- }
1811
-
1812
1864
  // src/commands/generate/get.ts
1813
1865
  async function generateGet(opts) {
1814
1866
  const client = opts.client ?? await buildGenerateClient(opts);
@@ -2570,7 +2622,7 @@ async function logout(options) {
2570
2622
  let serverRevoked = false;
2571
2623
  let revokeFailureMessage;
2572
2624
  if (options.revoke) {
2573
- const factory = options.apiClientFactory ?? ((apiBaseUrl, accessToken) => new ApiClient({
2625
+ const factory = options.apiClientFactory ?? ((apiBaseUrl, accessToken) => createApiClient({
2574
2626
  apiBaseUrl,
2575
2627
  accessToken,
2576
2628
  signal: options.signal,
@@ -2655,7 +2707,7 @@ async function buildModelClient(opts) {
2655
2707
  flagApiBaseUrl: opts.apiBaseUrl,
2656
2708
  flagToken: opts.token
2657
2709
  });
2658
- return new ApiClient({
2710
+ return createApiClient({
2659
2711
  apiBaseUrl: cfg.apiBaseUrl,
2660
2712
  accessToken: cfg.accessToken,
2661
2713
  signal: opts.signal,
@@ -2708,7 +2760,7 @@ async function buildNodesClient(opts) {
2708
2760
  flagApiBaseUrl: opts.apiBaseUrl,
2709
2761
  flagToken: opts.token
2710
2762
  });
2711
- return new ApiClient({
2763
+ return createApiClient({
2712
2764
  apiBaseUrl: cfg.apiBaseUrl,
2713
2765
  accessToken: cfg.accessToken,
2714
2766
  signal: opts.signal,
@@ -2752,7 +2804,7 @@ async function listProducts(options) {
2752
2804
  if (!config.accessToken) {
2753
2805
  throw new AuthError("AUTH_TOKEN_MISSING", `Not authenticated. Run \`clickraft login\` first.`);
2754
2806
  }
2755
- const factory = options.apiClientFactory ?? ((apiBaseUrl, accessToken) => new ApiClient({
2807
+ const factory = options.apiClientFactory ?? ((apiBaseUrl, accessToken) => createApiClient({
2756
2808
  apiBaseUrl,
2757
2809
  accessToken,
2758
2810
  signal: options.signal,
@@ -2861,7 +2913,7 @@ async function buildTemplateClient(opts) {
2861
2913
  flagApiBaseUrl: opts.apiBaseUrl,
2862
2914
  flagToken: opts.token
2863
2915
  });
2864
- return new ApiClient({
2916
+ return createApiClient({
2865
2917
  apiBaseUrl: cfg.apiBaseUrl,
2866
2918
  accessToken: cfg.accessToken,
2867
2919
  signal: opts.signal,
@@ -2955,7 +3007,7 @@ async function listTokens(options) {
2955
3007
  if (!config.accessToken) {
2956
3008
  throw new AuthError("AUTH_TOKEN_MISSING", `Not authenticated. Run \`clickraft login\` first.`);
2957
3009
  }
2958
- const factory = options.apiClientFactory ?? ((apiBaseUrl, accessToken) => new ApiClient({
3010
+ const factory = options.apiClientFactory ?? ((apiBaseUrl, accessToken) => createApiClient({
2959
3011
  apiBaseUrl,
2960
3012
  accessToken,
2961
3013
  signal: options.signal,
@@ -3027,7 +3079,7 @@ async function buildWorkflowClient(opts) {
3027
3079
  flagApiBaseUrl: opts.apiBaseUrl,
3028
3080
  flagToken: opts.token
3029
3081
  });
3030
- return new ApiClient({
3082
+ return createApiClient({
3031
3083
  apiBaseUrl: cfg.apiBaseUrl,
3032
3084
  accessToken: cfg.accessToken,
3033
3085
  signal: opts.signal,