agnes-ai-cli 0.1.1 → 0.1.2

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/README.md CHANGED
@@ -32,6 +32,8 @@ agnes video keyframes --image ./frame-a.png --image ./frame-b.png --prompt "morp
32
32
  agnes video poll <task-id>
33
33
  ```
34
34
 
35
+ Local media inputs are uploaded to a temporary public URL automatically. The default bridge tries x0.at first, then falls back to tmpfiles, Uguu, and Litterbox.
36
+
35
37
  ## JS API
36
38
 
37
39
  ```js
package/dist/cli.js CHANGED
@@ -72,7 +72,7 @@ Use this group when Agnes requires a public image URL and your source asset is a
72
72
  .command("url")
73
73
  .description("Return a public URL for a local file or pass through an existing URL")
74
74
  .argument("<file-or-url>", "Local file path or http(s) URL")
75
- .option("--ttl <ttl>", "Litterbox TTL (1h, 12h, 24h, 72h)", "1h")
75
+ .option("--ttl <ttl>", "Temporary upload TTL (1h, 12h, 24h, 72h)", "1h")
76
76
  .option("--json", "Output JSON")
77
77
  .action(async (input, options) => {
78
78
  const result = await client.media.toPublicUrl(input, { ttl: ttlSchema.parse(options.ttl) });
package/dist/config.d.ts CHANGED
@@ -40,7 +40,7 @@ export interface AuthCheckResult {
40
40
  export interface PublicUrlResult {
41
41
  ok: true;
42
42
  url: string;
43
- source: "passthrough" | "litterbox" | "provider";
43
+ source: "passthrough" | "litterbox" | "temporary" | "provider";
44
44
  }
45
45
  export type AgnesStatus = "queued" | "in_progress" | "completed" | "failed" | "timed_out";
46
46
  export interface NormalizedVideoTask {
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import type { AgnesClientConfig } from "./config.js";
2
2
  export { checkAuth } from "./auth/check.js";
3
3
  export { toPublicUrl } from "./media/toPublicUrl.js";
4
4
  export { LitterboxMediaUrlProvider } from "./media/litterbox.js";
5
+ export { TemporaryMediaUrlProvider, TmpfilesMediaUrlProvider, UguuMediaUrlProvider, X0MediaUrlProvider } from "./media/temporary-upload.js";
5
6
  export type { AgnesClientConfig, AuthCheckResult, PublicUrlResult, ImageGenerationResult, NormalizedVideoTask, NormalizedVideoResult, Ttl, } from "./config.js";
6
7
  export type { ImageGenerateOptions } from "./image/normalizeImageRequest.js";
7
8
  export type { TextCompleteOptions, TextCompletionResult } from "./text/complete.js";
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import { pollVideo } from "./video/pollVideo.js";
7
7
  export { checkAuth } from "./auth/check.js";
8
8
  export { toPublicUrl } from "./media/toPublicUrl.js";
9
9
  export { LitterboxMediaUrlProvider } from "./media/litterbox.js";
10
+ export { TemporaryMediaUrlProvider, TmpfilesMediaUrlProvider, UguuMediaUrlProvider, X0MediaUrlProvider } from "./media/temporary-upload.js";
10
11
  export function createAgnesClient(config = {}) {
11
12
  return {
12
13
  auth: {
@@ -1,9 +1,15 @@
1
1
  import type { FetchLike, Ttl } from "../config.js";
2
+ type LitterboxMediaUrlProviderOptions = {
3
+ maxAttempts?: number;
4
+ retryDelayMs?: number;
5
+ };
2
6
  export declare class LitterboxMediaUrlProvider {
3
7
  private readonly fetchImpl;
4
- constructor(fetchImpl?: FetchLike);
8
+ private readonly options;
9
+ constructor(fetchImpl?: FetchLike, options?: LitterboxMediaUrlProviderOptions);
5
10
  upload(localPath: string, options?: {
6
11
  ttl?: Ttl;
7
12
  }): Promise<string>;
8
13
  private postFormWithRetry;
9
14
  }
15
+ export {};
@@ -14,8 +14,10 @@ const MIME_TYPES = {
14
14
  };
15
15
  export class LitterboxMediaUrlProvider {
16
16
  fetchImpl;
17
- constructor(fetchImpl = fetch) {
17
+ options;
18
+ constructor(fetchImpl = fetch, options = {}) {
18
19
  this.fetchImpl = fetchImpl;
20
+ this.options = options;
19
21
  }
20
22
  async upload(localPath, options = {}) {
21
23
  const ttl = options.ttl ?? "1h";
@@ -40,7 +42,9 @@ export class LitterboxMediaUrlProvider {
40
42
  }
41
43
  async postFormWithRetry(form) {
42
44
  let lastError;
43
- for (let attempt = 0; attempt <= 2; attempt += 1) {
45
+ const maxAttempts = this.options.maxAttempts ?? 3;
46
+ const retryDelayMs = this.options.retryDelayMs ?? 500;
47
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
44
48
  try {
45
49
  const response = await this.fetchImpl(LITTERBOX_ENDPOINT, {
46
50
  method: "POST",
@@ -51,12 +55,12 @@ export class LitterboxMediaUrlProvider {
51
55
  }
52
56
  catch (error) {
53
57
  lastError = error;
54
- if (attempt === 2) {
58
+ if (attempt === maxAttempts) {
55
59
  throw new AgnesCliError("UPLOAD_FAILED", "Litterbox upload failed before a response was received.", {
56
60
  cause: error instanceof Error ? error.message : String(error),
57
61
  });
58
62
  }
59
- await sleep(500);
63
+ await sleep(retryDelayMs);
60
64
  }
61
65
  }
62
66
  throw new AgnesCliError("UPLOAD_FAILED", "Litterbox upload failed before a response was received.", {
@@ -0,0 +1,25 @@
1
+ import type { FetchLike, MediaUrlProvider, Ttl } from "../config.js";
2
+ export declare class TemporaryMediaUrlProvider implements MediaUrlProvider {
3
+ private readonly providers;
4
+ constructor(fetchImpl?: FetchLike);
5
+ upload(localPath: string, options?: {
6
+ ttl?: Ttl;
7
+ }): Promise<string>;
8
+ }
9
+ export declare class X0MediaUrlProvider implements MediaUrlProvider {
10
+ private readonly fetchImpl;
11
+ constructor(fetchImpl?: FetchLike);
12
+ upload(localPath: string): Promise<string>;
13
+ }
14
+ export declare class UguuMediaUrlProvider implements MediaUrlProvider {
15
+ private readonly fetchImpl;
16
+ constructor(fetchImpl?: FetchLike);
17
+ upload(localPath: string): Promise<string>;
18
+ }
19
+ export declare class TmpfilesMediaUrlProvider implements MediaUrlProvider {
20
+ private readonly fetchImpl;
21
+ constructor(fetchImpl?: FetchLike);
22
+ upload(localPath: string, options?: {
23
+ ttl?: Ttl;
24
+ }): Promise<string>;
25
+ }
@@ -0,0 +1,176 @@
1
+ import { promises as fs } from "node:fs";
2
+ import { basename, extname } from "node:path";
3
+ import { AgnesCliError } from "../errors.js";
4
+ import { LitterboxMediaUrlProvider } from "./litterbox.js";
5
+ const X0_ENDPOINT = "https://x0.at/";
6
+ const UGUU_ENDPOINT = "https://uguu.se/upload";
7
+ const TMPFILES_ENDPOINT = "https://tmpfiles.org/api/v1/upload";
8
+ const MIME_TYPES = {
9
+ ".jpg": "image/jpeg",
10
+ ".jpeg": "image/jpeg",
11
+ ".png": "image/png",
12
+ ".webp": "image/webp",
13
+ ".gif": "image/gif",
14
+ ".mp4": "video/mp4",
15
+ ".mov": "video/quicktime",
16
+ ".webm": "video/webm",
17
+ };
18
+ const TTL_SECONDS = {
19
+ "1h": "3600",
20
+ "12h": "43200",
21
+ "24h": "86400",
22
+ "72h": "259200",
23
+ };
24
+ export class TemporaryMediaUrlProvider {
25
+ providers;
26
+ constructor(fetchImpl = fetch) {
27
+ this.providers = [
28
+ { name: "x0.at", provider: new X0MediaUrlProvider(fetchImpl) },
29
+ { name: "tmpfiles", provider: new TmpfilesMediaUrlProvider(fetchImpl) },
30
+ { name: "Uguu", provider: new UguuMediaUrlProvider(fetchImpl) },
31
+ { name: "Litterbox", provider: new LitterboxMediaUrlProvider(fetchImpl, { maxAttempts: 1 }) },
32
+ ];
33
+ }
34
+ async upload(localPath, options = {}) {
35
+ const failures = [];
36
+ for (const { name, provider } of this.providers) {
37
+ try {
38
+ return await provider.upload(localPath, options);
39
+ }
40
+ catch (error) {
41
+ failures.push(`${name}: ${error instanceof Error ? error.message : String(error)}`);
42
+ }
43
+ }
44
+ throw new AgnesCliError("UPLOAD_FAILED", "Temporary upload failed after trying x0.at, tmpfiles, Uguu, and Litterbox.", {
45
+ failures,
46
+ });
47
+ }
48
+ }
49
+ export class X0MediaUrlProvider {
50
+ fetchImpl;
51
+ constructor(fetchImpl = fetch) {
52
+ this.fetchImpl = fetchImpl;
53
+ }
54
+ async upload(localPath) {
55
+ const form = await createFileForm(localPath, "file");
56
+ const { response, text } = await postForm(this.fetchImpl, X0_ENDPOINT, form);
57
+ if (!response.ok) {
58
+ throw new AgnesCliError("UPLOAD_FAILED", `x0.at upload failed with HTTP ${response.status}.`, {
59
+ status: response.status,
60
+ body: text,
61
+ });
62
+ }
63
+ const url = extractPlainTextUrl(text);
64
+ if (!url) {
65
+ throw new AgnesCliError("UPLOAD_FAILED", `x0.at did not return a public URL: ${text}`);
66
+ }
67
+ return url;
68
+ }
69
+ }
70
+ export class UguuMediaUrlProvider {
71
+ fetchImpl;
72
+ constructor(fetchImpl = fetch) {
73
+ this.fetchImpl = fetchImpl;
74
+ }
75
+ async upload(localPath) {
76
+ const form = await createFileForm(localPath, "files[]");
77
+ const { response, text } = await postForm(this.fetchImpl, UGUU_ENDPOINT, form);
78
+ if (!response.ok) {
79
+ throw new AgnesCliError("UPLOAD_FAILED", `Uguu upload failed with HTTP ${response.status}.`, {
80
+ status: response.status,
81
+ body: text,
82
+ });
83
+ }
84
+ const raw = parseUploadJson(text, "Uguu");
85
+ const url = extractUguuUrl(raw);
86
+ if (!url) {
87
+ throw new AgnesCliError("UPLOAD_FAILED", `Uguu did not return a public URL: ${text}`);
88
+ }
89
+ return url;
90
+ }
91
+ }
92
+ export class TmpfilesMediaUrlProvider {
93
+ fetchImpl;
94
+ constructor(fetchImpl = fetch) {
95
+ this.fetchImpl = fetchImpl;
96
+ }
97
+ async upload(localPath, options = {}) {
98
+ const form = await createFileForm(localPath, "file");
99
+ form.set("expire", TTL_SECONDS[options.ttl ?? "1h"]);
100
+ const { response, text } = await postForm(this.fetchImpl, TMPFILES_ENDPOINT, form);
101
+ if (!response.ok) {
102
+ throw new AgnesCliError("UPLOAD_FAILED", `tmpfiles upload failed with HTTP ${response.status}.`, {
103
+ status: response.status,
104
+ body: text,
105
+ });
106
+ }
107
+ const raw = parseUploadJson(text, "tmpfiles");
108
+ const url = extractTmpfilesUrl(raw);
109
+ if (!url) {
110
+ throw new AgnesCliError("UPLOAD_FAILED", `tmpfiles did not return a public URL: ${text}`);
111
+ }
112
+ return toTmpfilesDirectUrl(url);
113
+ }
114
+ }
115
+ async function createFileForm(localPath, fieldName) {
116
+ const file = await fs.readFile(localPath);
117
+ const form = new FormData();
118
+ const mimeType = MIME_TYPES[extname(localPath).toLowerCase()] ?? "application/octet-stream";
119
+ form.set(fieldName, new Blob([file], { type: mimeType }), basename(localPath));
120
+ return form;
121
+ }
122
+ async function postForm(fetchImpl, endpoint, form) {
123
+ try {
124
+ const response = await fetchImpl(endpoint, {
125
+ method: "POST",
126
+ body: form,
127
+ });
128
+ return { response, text: (await response.text()).trim() };
129
+ }
130
+ catch (error) {
131
+ throw new AgnesCliError("UPLOAD_FAILED", `${new URL(endpoint).hostname} upload failed before a response was received.`, {
132
+ cause: error instanceof Error ? error.message : String(error),
133
+ });
134
+ }
135
+ }
136
+ function parseUploadJson(text, providerName) {
137
+ try {
138
+ return JSON.parse(text);
139
+ }
140
+ catch {
141
+ throw new AgnesCliError("UPLOAD_FAILED", `${providerName} did not return JSON: ${text}`);
142
+ }
143
+ }
144
+ function extractPlainTextUrl(text) {
145
+ const url = text.match(/https?:\/\/[^\s"'<>]+/)?.[0];
146
+ return url && /^https?:\/\//.test(url) ? url : undefined;
147
+ }
148
+ function extractUguuUrl(raw) {
149
+ if (!raw || typeof raw !== "object")
150
+ return undefined;
151
+ const files = raw.files;
152
+ if (!Array.isArray(files))
153
+ return undefined;
154
+ const first = files[0];
155
+ if (!first || typeof first !== "object")
156
+ return undefined;
157
+ const url = first.url;
158
+ return typeof url === "string" && /^https?:\/\//.test(url) ? url : undefined;
159
+ }
160
+ function extractTmpfilesUrl(raw) {
161
+ if (!raw || typeof raw !== "object")
162
+ return undefined;
163
+ const data = raw.data;
164
+ if (!data || typeof data !== "object")
165
+ return undefined;
166
+ const url = data.url;
167
+ return typeof url === "string" && /^https?:\/\//.test(url) ? url : undefined;
168
+ }
169
+ function toTmpfilesDirectUrl(url) {
170
+ const parsed = new URL(url);
171
+ if (parsed.hostname !== "tmpfiles.org" || parsed.pathname.startsWith("/dl/")) {
172
+ return url;
173
+ }
174
+ parsed.pathname = `/dl${parsed.pathname}`;
175
+ return parsed.toString();
176
+ }
@@ -2,7 +2,7 @@ import { access, mkdtemp, rm, writeFile } from "node:fs/promises";
2
2
  import { constants } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { extname, join } from "node:path";
5
- import { LitterboxMediaUrlProvider } from "./litterbox.js";
5
+ import { TemporaryMediaUrlProvider } from "./temporary-upload.js";
6
6
  import { resolveConfig } from "../config.js";
7
7
  import { AgnesCliError } from "../errors.js";
8
8
  const MIME_EXTENSIONS = {
@@ -20,11 +20,11 @@ export async function toPublicUrl(input, options = {}, config = {}) {
20
20
  }
21
21
  const resolved = resolveConfig(config);
22
22
  const provider = resolved.mediaProvider ??
23
- new LitterboxMediaUrlProvider(resolved.fetchImpl);
23
+ new TemporaryMediaUrlProvider(resolved.fetchImpl);
24
24
  const materialized = await materializeUploadInput(input, resolved.fetchImpl);
25
25
  try {
26
26
  const url = await provider.upload(materialized.path, { ttl: options.ttl ?? resolved.defaultMediaTtl });
27
- return { ok: true, url, source: resolved.mediaProvider ? "provider" : "litterbox" };
27
+ return { ok: true, url, source: resolved.mediaProvider ? "provider" : "temporary" };
28
28
  }
29
29
  finally {
30
30
  if (materialized.cleanupDir) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agnes-ai-cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "CLI and JS API for Agnes text, image, and video workflows.",
5
5
  "type": "module",
6
6
  "bin": {