@clickraft/cli 0.8.6 → 0.8.8

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 CHANGED
@@ -1,4 +1,4 @@
1
- ## [0.8.6](https://github.com/clickraft/cli/compare/v0.8.5...v0.8.6) (2026-05-31)
1
+ ## [0.8.8](https://github.com/clickraft/cli/compare/v0.8.7...v0.8.8) (2026-05-31)
2
2
 
3
3
  # Changelog
4
4
 
package/dist/cli.js CHANGED
@@ -14,6 +14,8 @@ import { request as undiciRequest } from "undici";
14
14
  import { z as z6 } from "zod";
15
15
  import { createHash, randomUUID } from "crypto";
16
16
  import { z as z5 } from "zod";
17
+ import { createHash as createHash2 } from "crypto";
18
+ import { request as undiciRequest2 } from "undici";
17
19
  var TOKEN_FIELD_PATTERN = /"(access_token|refresh_token|device_code|user_code|code_verifier|client_secret)"\s*:\s*"[^"]*"/gi;
18
20
  var BARE_TOKEN_PATTERN = /\bck_(?:live|dev|test)_[A-Za-z0-9_-]+/gi;
19
21
  function redactTokenLikeFields(input) {
@@ -677,6 +679,178 @@ async function patch(params) {
677
679
  workflow: data.workflow
678
680
  };
679
681
  }
682
+ async function uploadAsset(params) {
683
+ const contentType = detectContentType(params.filename, params.content);
684
+ const sha = sha256Hex(params.content);
685
+ const contentHash = `sha256:${sha}`;
686
+ const idempotencyKey = generateUploadIdempotencyKey(params.content, params.scope);
687
+ const response = await params.client.request({
688
+ method: "POST",
689
+ path: "/assets",
690
+ body: {
691
+ filename: params.filename,
692
+ contentType,
693
+ sizeBytes: params.content.length,
694
+ contentHash
695
+ },
696
+ idempotencyKey,
697
+ responseSchema: params.schema,
698
+ signal: params.signal
699
+ });
700
+ if (!response.deduplicated) {
701
+ const first = await tryPut({
702
+ uploadUrl: response.uploadUrl,
703
+ content: params.content,
704
+ contentType,
705
+ signal: params.signal,
706
+ dispatcher: params.putDispatcher
707
+ });
708
+ if (!first.ok) {
709
+ if (first.kind === "fatal") {
710
+ throw new ApiError(
711
+ "STORAGE_UPLOAD_FAILED",
712
+ `Upload to signed URL failed for ${params.filename} (HTTP ${first.status ?? "unknown"}).`,
713
+ { retryable: false }
714
+ );
715
+ }
716
+ const retry = await tryPut({
717
+ uploadUrl: response.uploadUrl,
718
+ content: params.content,
719
+ contentType,
720
+ signal: params.signal,
721
+ dispatcher: params.putDispatcher
722
+ });
723
+ if (!retry.ok) {
724
+ throw new ApiError(
725
+ "STORAGE_UPLOAD_FAILED",
726
+ `Upload to signed URL failed for ${params.filename} after one retry.`,
727
+ { retryable: true }
728
+ );
729
+ }
730
+ }
731
+ }
732
+ return {
733
+ assetId: response.assetId,
734
+ publicUrl: response.publicUrl,
735
+ deduplicated: response.deduplicated,
736
+ bytes: params.content.length,
737
+ contentHash
738
+ };
739
+ }
740
+ async function tryPut(args) {
741
+ try {
742
+ const response = await undiciRequest2(args.uploadUrl, {
743
+ method: "PUT",
744
+ body: args.content,
745
+ headers: {
746
+ "content-type": args.contentType,
747
+ "content-length": String(args.content.length)
748
+ },
749
+ signal: args.signal,
750
+ dispatcher: args.dispatcher
751
+ });
752
+ await response.body.text();
753
+ const status = response.statusCode;
754
+ if (status >= 200 && status < 300) return { ok: true };
755
+ if (status >= 400 && status < 500) return { ok: false, kind: "fatal", status };
756
+ return { ok: false, kind: "retryable", status };
757
+ } catch {
758
+ return { ok: false, kind: "retryable" };
759
+ }
760
+ }
761
+ function sha256Hex(content) {
762
+ return createHash2("sha256").update(content).digest("hex");
763
+ }
764
+ var MAGIC_NUMBERS = [
765
+ { contentType: "image/png", prefix: [137, 80, 78, 71, 13, 10, 26, 10] },
766
+ { contentType: "image/jpeg", prefix: [255, 216, 255] },
767
+ { contentType: "image/webp", prefix: [82, 73, 70, 70] }
768
+ // also matches WAV — disambiguated below
769
+ ];
770
+ var EXTENSION_FALLBACK = {
771
+ png: "image/png",
772
+ jpg: "image/jpeg",
773
+ jpeg: "image/jpeg",
774
+ gif: "image/gif",
775
+ webp: "image/webp",
776
+ mp4: "video/mp4",
777
+ mov: "video/quicktime",
778
+ mp3: "audio/mpeg",
779
+ wav: "audio/wav",
780
+ pdf: "application/pdf",
781
+ txt: "text/plain",
782
+ json: "application/json"
783
+ };
784
+ function detectContentType(filename, content) {
785
+ if (content.length >= 6) {
786
+ const head = content.subarray(0, 6).toString("ascii");
787
+ if (head === "GIF87a" || head === "GIF89a") return "image/gif";
788
+ }
789
+ if (content.length >= 12 && content.subarray(4, 8).toString("ascii") === "ftyp") {
790
+ return "video/mp4";
791
+ }
792
+ for (const { contentType, prefix } of MAGIC_NUMBERS) {
793
+ if (matchesPrefix(content, prefix)) {
794
+ if (contentType === "image/webp") {
795
+ const formatId = content.subarray(8, 12).toString("ascii");
796
+ if (formatId === "WEBP") return "image/webp";
797
+ if (formatId === "WAVE") return "audio/wav";
798
+ break;
799
+ }
800
+ return contentType;
801
+ }
802
+ }
803
+ const ext = filename.toLowerCase().split(".").pop();
804
+ if (ext && EXTENSION_FALLBACK[ext]) {
805
+ return EXTENSION_FALLBACK[ext];
806
+ }
807
+ return "application/octet-stream";
808
+ }
809
+ function matchesPrefix(content, prefix) {
810
+ if (content.length < prefix.length) return false;
811
+ for (let i = 0; i < prefix.length; i++) {
812
+ if (content[i] !== prefix[i]) return false;
813
+ }
814
+ return true;
815
+ }
816
+ function waitForGeneration(params) {
817
+ return longPoll({
818
+ // Forward the iteration signal so the in-flight server hold is aborted as
819
+ // soon as the overall deadline expires — otherwise `--timeout 1` would still
820
+ // block for the full server long-poll round.
821
+ fetcher: (args) => params.client.request({
822
+ method: "GET",
823
+ path: `/jobs/${encodeURIComponent(params.jobId)}`,
824
+ query: { wait: params.serverWaitSeconds },
825
+ responseSchema: params.resultSchema,
826
+ signal: args?.signal
827
+ }),
828
+ isTerminal: params.isTerminal,
829
+ timeoutMs: params.timeoutMs,
830
+ signal: params.signal
831
+ });
832
+ }
833
+ async function createGeneration(params) {
834
+ const created = await params.client.request({
835
+ method: "POST",
836
+ path: "/jobs",
837
+ body: params.body,
838
+ responseSchema: params.createSchema,
839
+ signal: params.signal
840
+ });
841
+ if (!params.wait) {
842
+ return params.projectCreated(created, params.modelSlug);
843
+ }
844
+ return waitForGeneration({
845
+ client: params.client,
846
+ jobId: created.jobId,
847
+ timeoutMs: params.timeoutMs,
848
+ serverWaitSeconds: params.serverWaitSeconds,
849
+ signal: params.signal,
850
+ resultSchema: params.resultSchema,
851
+ isTerminal: params.isTerminal
852
+ });
853
+ }
680
854
 
681
855
  // src/errors/codes.ts
682
856
  var LOGIN_HINT = "Run `clickraft login` to authenticate.";
@@ -1635,10 +1809,9 @@ var GenerationResultSchema = z9.object({
1635
1809
  }).strict();
1636
1810
 
1637
1811
  // src/commands/upload.ts
1638
- import { createHash as createHash2 } from "crypto";
1639
1812
  import { readFile as readFile2, stat as stat2 } from "fs/promises";
1640
1813
  import { basename } from "path";
1641
- import { request as undiciRequest2 } from "undici";
1814
+ import "undici";
1642
1815
 
1643
1816
  // src/schemas/assets.ts
1644
1817
  import { z as z10 } from "zod";
@@ -1663,60 +1836,16 @@ async function uploadFile(opts) {
1663
1836
  const size = await safeStatSize(opts.filePath);
1664
1837
  validateSize(size);
1665
1838
  const content = await safeReadFile(opts.filePath);
1666
- const contentType = detectContentType(opts.filePath, content);
1667
- const sha = sha256Hex(content);
1668
- const contentHash = `sha256:${sha}`;
1669
- const filename = basename(opts.filePath);
1670
- const idempotencyKey = generateUploadIdempotencyKey(content, scope);
1671
1839
  const client = opts.client ?? await buildClient(opts);
1672
- const response = await postAssetMetadata({
1840
+ return uploadAsset({
1673
1841
  client,
1674
- filename,
1675
- contentType,
1676
- sizeBytes: content.length,
1677
- contentHash,
1678
- idempotencyKey,
1679
- signal: opts.signal
1842
+ content,
1843
+ filename: basename(opts.filePath),
1844
+ scope,
1845
+ signal: opts.signal,
1846
+ putDispatcher: opts.putDispatcher,
1847
+ schema: UploadResponseSchema
1680
1848
  });
1681
- if (!response.deduplicated) {
1682
- const first = await tryPut({
1683
- uploadUrl: response.uploadUrl,
1684
- content,
1685
- contentType,
1686
- signal: opts.signal,
1687
- dispatcher: opts.putDispatcher
1688
- });
1689
- if (!first.ok) {
1690
- if (first.kind === "fatal") {
1691
- throw new ApiError(
1692
- "STORAGE_UPLOAD_FAILED",
1693
- `Upload to signed URL failed for ${filename} (HTTP ${first.status ?? "unknown"}).`,
1694
- { retryable: false }
1695
- );
1696
- }
1697
- const retry = await tryPut({
1698
- uploadUrl: response.uploadUrl,
1699
- content,
1700
- contentType,
1701
- signal: opts.signal,
1702
- dispatcher: opts.putDispatcher
1703
- });
1704
- if (!retry.ok) {
1705
- throw new ApiError(
1706
- "STORAGE_UPLOAD_FAILED",
1707
- `Upload to signed URL failed for ${filename} after one retry.`,
1708
- { retryable: true }
1709
- );
1710
- }
1711
- }
1712
- }
1713
- return {
1714
- assetId: response.assetId,
1715
- publicUrl: response.publicUrl,
1716
- deduplicated: response.deduplicated,
1717
- bytes: content.length,
1718
- contentHash
1719
- };
1720
1849
  }
1721
1850
  async function buildClient(opts) {
1722
1851
  const cfg = await loadConfig({
@@ -1731,42 +1860,6 @@ async function buildClient(opts) {
1731
1860
  cloudflareAccess: cfg.cloudflareAccess
1732
1861
  });
1733
1862
  }
1734
- async function postAssetMetadata(args) {
1735
- return args.client.request({
1736
- method: "POST",
1737
- path: "/assets",
1738
- body: {
1739
- filename: args.filename,
1740
- contentType: args.contentType,
1741
- sizeBytes: args.sizeBytes,
1742
- contentHash: args.contentHash
1743
- },
1744
- idempotencyKey: args.idempotencyKey,
1745
- responseSchema: UploadResponseSchema,
1746
- signal: args.signal
1747
- });
1748
- }
1749
- async function tryPut(args) {
1750
- try {
1751
- const response = await undiciRequest2(args.uploadUrl, {
1752
- method: "PUT",
1753
- body: args.content,
1754
- headers: {
1755
- "content-type": args.contentType,
1756
- "content-length": String(args.content.length)
1757
- },
1758
- signal: args.signal,
1759
- dispatcher: args.dispatcher
1760
- });
1761
- await response.body.text();
1762
- const status = response.statusCode;
1763
- if (status >= 200 && status < 300) return { ok: true };
1764
- if (status >= 400 && status < 500) return { ok: false, kind: "fatal", status };
1765
- return { ok: false, kind: "retryable", status };
1766
- } catch {
1767
- return { ok: false, kind: "retryable" };
1768
- }
1769
- }
1770
1863
  async function safeStatSize(filePath) {
1771
1864
  let stats;
1772
1865
  try {
@@ -1828,61 +1921,6 @@ function validateSize(bytes) {
1828
1921
  );
1829
1922
  }
1830
1923
  }
1831
- function sha256Hex(content) {
1832
- return createHash2("sha256").update(content).digest("hex");
1833
- }
1834
- var MAGIC_NUMBERS = [
1835
- { contentType: "image/png", prefix: [137, 80, 78, 71, 13, 10, 26, 10] },
1836
- { contentType: "image/jpeg", prefix: [255, 216, 255] },
1837
- { contentType: "image/webp", prefix: [82, 73, 70, 70] }
1838
- // also matches WAV — disambiguated below
1839
- ];
1840
- var EXTENSION_FALLBACK = {
1841
- png: "image/png",
1842
- jpg: "image/jpeg",
1843
- jpeg: "image/jpeg",
1844
- gif: "image/gif",
1845
- webp: "image/webp",
1846
- mp4: "video/mp4",
1847
- mov: "video/quicktime",
1848
- mp3: "audio/mpeg",
1849
- wav: "audio/wav",
1850
- pdf: "application/pdf",
1851
- txt: "text/plain",
1852
- json: "application/json"
1853
- };
1854
- function detectContentType(filePath, content) {
1855
- if (content.length >= 6) {
1856
- const head = content.subarray(0, 6).toString("ascii");
1857
- if (head === "GIF87a" || head === "GIF89a") return "image/gif";
1858
- }
1859
- if (content.length >= 12 && content.subarray(4, 8).toString("ascii") === "ftyp") {
1860
- return "video/mp4";
1861
- }
1862
- for (const { contentType, prefix } of MAGIC_NUMBERS) {
1863
- if (matchesPrefix(content, prefix)) {
1864
- if (contentType === "image/webp") {
1865
- const formatId = content.subarray(8, 12).toString("ascii");
1866
- if (formatId === "WEBP") return "image/webp";
1867
- if (formatId === "WAVE") return "audio/wav";
1868
- break;
1869
- }
1870
- return contentType;
1871
- }
1872
- }
1873
- const ext = filePath.toLowerCase().split(".").pop();
1874
- if (ext && EXTENSION_FALLBACK[ext]) {
1875
- return EXTENSION_FALLBACK[ext];
1876
- }
1877
- return "application/octet-stream";
1878
- }
1879
- function matchesPrefix(content, prefix) {
1880
- if (content.length < prefix.length) return false;
1881
- for (let i = 0; i < prefix.length; i++) {
1882
- if (content[i] !== prefix[i]) return false;
1883
- }
1884
- return true;
1885
- }
1886
1924
 
1887
1925
  // src/commands/generate/internal.ts
1888
1926
  async function buildGenerateClient(opts) {
@@ -1902,37 +1940,19 @@ function isUrlLike(value) {
1902
1940
  return value.startsWith("http://") || value.startsWith("https://");
1903
1941
  }
1904
1942
 
1905
- // src/commands/generate/get.ts
1906
- async function generateGet(opts) {
1907
- const client = opts.client ?? await buildGenerateClient(opts);
1908
- return client.request({
1909
- method: "GET",
1910
- path: `/jobs/${encodeURIComponent(opts.jobId)}`,
1911
- query: opts.waitSeconds && opts.waitSeconds > 0 ? { wait: opts.waitSeconds } : void 0,
1912
- responseSchema: GenerationResultSchema,
1913
- signal: opts.signal
1914
- });
1915
- }
1916
-
1917
1943
  // src/commands/generate/wait.ts
1918
1944
  var DEFAULT_WAIT_TIMEOUT_SECONDS = 120;
1919
1945
  var SERVER_LONG_POLL_SECONDS = 30;
1920
1946
  async function generateWait(opts) {
1921
1947
  const client = opts.client ?? await buildGenerateClient(opts);
1922
- const timeoutSeconds = opts.timeoutSeconds ?? DEFAULT_WAIT_TIMEOUT_SECONDS;
1923
- return longPoll({
1924
- // Forward the iteration signal so the in-flight server hold is aborted
1925
- // as soon as the overall deadline expires — otherwise `--timeout 1` would
1926
- // still block for the full 30s server long-poll round.
1927
- fetcher: (args) => generateGet({
1928
- jobId: opts.jobId,
1929
- waitSeconds: SERVER_LONG_POLL_SECONDS,
1930
- client,
1931
- signal: args?.signal
1932
- }),
1933
- isTerminal: (r) => isTerminalStatus(r.status),
1934
- timeoutMs: timeoutSeconds * 1e3,
1935
- signal: opts.signal
1948
+ return waitForGeneration({
1949
+ client,
1950
+ jobId: opts.jobId,
1951
+ timeoutMs: (opts.timeoutSeconds ?? DEFAULT_WAIT_TIMEOUT_SECONDS) * 1e3,
1952
+ serverWaitSeconds: SERVER_LONG_POLL_SECONDS,
1953
+ signal: opts.signal,
1954
+ resultSchema: GenerationResultSchema,
1955
+ isTerminal: (r) => isTerminalStatus(r.status)
1936
1956
  });
1937
1957
  }
1938
1958
 
@@ -1950,22 +1970,18 @@ async function generateCreate(opts) {
1950
1970
  signal: opts.signal
1951
1971
  });
1952
1972
  const body = buildRequestBody({ ...opts, referenceImages });
1953
- const created = await client.request({
1954
- method: "POST",
1955
- path: "/jobs",
1956
- body,
1957
- responseSchema: GenerateCreateResponseSchema,
1958
- signal: opts.signal
1959
- });
1960
- const wait = opts.wait ?? true;
1961
- if (!wait) {
1962
- return projectCreateToResult(created, opts.modelSlug);
1963
- }
1964
- return generateWait({
1965
- jobId: created.jobId,
1966
- timeoutSeconds: opts.timeoutSeconds ?? DEFAULT_WAIT_TIMEOUT_SECONDS,
1973
+ return createGeneration({
1967
1974
  client,
1968
- signal: opts.signal
1975
+ body,
1976
+ modelSlug: opts.modelSlug,
1977
+ wait: opts.wait ?? true,
1978
+ timeoutMs: (opts.timeoutSeconds ?? DEFAULT_WAIT_TIMEOUT_SECONDS) * 1e3,
1979
+ serverWaitSeconds: SERVER_LONG_POLL_SECONDS,
1980
+ signal: opts.signal,
1981
+ createSchema: GenerateCreateResponseSchema,
1982
+ resultSchema: GenerationResultSchema,
1983
+ isTerminal: (r) => isTerminalStatus(r.status),
1984
+ projectCreated: projectCreateToResult
1969
1985
  });
1970
1986
  }
1971
1987
  async function resolveReferenceImages(args) {
@@ -2048,6 +2064,18 @@ function projectCreateToResult(response, modelSlug) {
2048
2064
  };
2049
2065
  }
2050
2066
 
2067
+ // src/commands/generate/get.ts
2068
+ async function generateGet(opts) {
2069
+ const client = opts.client ?? await buildGenerateClient(opts);
2070
+ return client.request({
2071
+ method: "GET",
2072
+ path: `/jobs/${encodeURIComponent(opts.jobId)}`,
2073
+ query: opts.waitSeconds && opts.waitSeconds > 0 ? { wait: opts.waitSeconds } : void 0,
2074
+ responseSchema: GenerationResultSchema,
2075
+ signal: opts.signal
2076
+ });
2077
+ }
2078
+
2051
2079
  // src/cli/args.ts
2052
2080
  var ArgError = class extends Error {
2053
2081
  constructor(message) {