agnes-ai-cli 0.1.0 → 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
@@ -4,28 +4,16 @@ CLI and JS API for Agnes text, image, and video workflows.
4
4
 
5
5
  ## Install
6
6
 
7
- Current verified distribution path:
7
+ Install globally:
8
8
 
9
9
  ```bash
10
- npm install -g github:jomeswang/agnes-ai-cli
11
- ```
12
-
13
- Ad hoc execution without a global install:
14
-
15
- ```bash
16
- npx -y github:jomeswang/agnes-ai-cli --help
17
- ```
18
-
19
- Planned npm package name:
20
-
21
- ```bash
22
- agnes-ai-cli
10
+ npm install -g agnes-ai-cli
23
11
  ```
24
12
 
25
- Once the npm publish path is cleared, the package will also be installable as:
13
+ Run without installing:
26
14
 
27
15
  ```bash
28
- npm install -g agnes-ai-cli
16
+ npx -y agnes-ai-cli --help
29
17
  ```
30
18
 
31
19
  ## CLI
@@ -44,6 +32,8 @@ agnes video keyframes --image ./frame-a.png --image ./frame-b.png --prompt "morp
44
32
  agnes video poll <task-id>
45
33
  ```
46
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
+
47
37
  ## JS API
48
38
 
49
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
+ }
@@ -1,21 +1,112 @@
1
- import { access } from "node:fs/promises";
1
+ import { access, mkdtemp, rm, writeFile } from "node:fs/promises";
2
2
  import { constants } from "node:fs";
3
- import { LitterboxMediaUrlProvider } from "./litterbox.js";
3
+ import { tmpdir } from "node:os";
4
+ import { extname, join } from "node:path";
5
+ import { TemporaryMediaUrlProvider } from "./temporary-upload.js";
4
6
  import { resolveConfig } from "../config.js";
5
7
  import { AgnesCliError } from "../errors.js";
8
+ const MIME_EXTENSIONS = {
9
+ "image/jpeg": ".jpg",
10
+ "image/png": ".png",
11
+ "image/webp": ".webp",
12
+ "image/gif": ".gif",
13
+ "video/mp4": ".mp4",
14
+ "video/quicktime": ".mov",
15
+ "video/webm": ".webm",
16
+ };
6
17
  export async function toPublicUrl(input, options = {}, config = {}) {
7
- if (/^https?:\/\//.test(input)) {
18
+ if (isPublicHttpUrl(input)) {
8
19
  return { ok: true, url: input, source: "passthrough" };
9
20
  }
21
+ const resolved = resolveConfig(config);
22
+ const provider = resolved.mediaProvider ??
23
+ new TemporaryMediaUrlProvider(resolved.fetchImpl);
24
+ const materialized = await materializeUploadInput(input, resolved.fetchImpl);
25
+ try {
26
+ const url = await provider.upload(materialized.path, { ttl: options.ttl ?? resolved.defaultMediaTtl });
27
+ return { ok: true, url, source: resolved.mediaProvider ? "provider" : "temporary" };
28
+ }
29
+ finally {
30
+ if (materialized.cleanupDir) {
31
+ await rm(materialized.cleanupDir, { recursive: true, force: true });
32
+ }
33
+ }
34
+ }
35
+ function isPublicHttpUrl(input) {
36
+ if (!/^https?:\/\//.test(input))
37
+ return false;
38
+ const url = new URL(input);
39
+ const host = url.hostname.toLowerCase().replace(/^\[|\]$/g, "");
40
+ return !isPrivateHost(host);
41
+ }
42
+ function isPrivateHost(host) {
43
+ if (host === "localhost" || host.endsWith(".localhost") || host === "::1") {
44
+ return true;
45
+ }
46
+ if (/^127\./.test(host) || /^10\./.test(host) || /^192\.168\./.test(host) || /^169\.254\./.test(host)) {
47
+ return true;
48
+ }
49
+ const match = host.match(/^172\.(\d{1,3})\./);
50
+ if (match) {
51
+ const secondOctet = Number(match[1]);
52
+ return secondOctet >= 16 && secondOctet <= 31;
53
+ }
54
+ return host === "0.0.0.0" || (host.includes(":") && (host.startsWith("fc") || host.startsWith("fd") || host.startsWith("fe80:")));
55
+ }
56
+ async function materializeUploadInput(input, fetchImpl) {
57
+ if (input.startsWith("data:")) {
58
+ return writeTempMedia(await decodeDataUrl(input));
59
+ }
60
+ if (/^https?:\/\//.test(input)) {
61
+ return writeTempMedia(await fetchPrivateUrl(input, fetchImpl));
62
+ }
10
63
  try {
11
64
  await access(input, constants.R_OK);
12
65
  }
13
66
  catch {
14
- throw new AgnesCliError("INPUT_NOT_FOUND", `Input must be an existing file path or an http(s) URL: ${input}`);
67
+ throw new AgnesCliError("INPUT_NOT_FOUND", `Input must be an existing file path, a data URL, or an http(s) URL: ${input}`);
15
68
  }
16
- const resolved = resolveConfig(config);
17
- const provider = resolved.mediaProvider ??
18
- new LitterboxMediaUrlProvider(resolved.fetchImpl);
19
- const url = await provider.upload(input, { ttl: options.ttl ?? resolved.defaultMediaTtl });
20
- return { ok: true, url, source: resolved.mediaProvider ? "provider" : "litterbox" };
69
+ return { path: input };
70
+ }
71
+ async function fetchPrivateUrl(input, fetchImpl) {
72
+ const response = await fetchImpl(input);
73
+ if (!response.ok) {
74
+ throw new AgnesCliError("INPUT_FETCH_FAILED", `Input URL fetch failed with HTTP ${response.status}: ${input}`, {
75
+ status: response.status,
76
+ url: input,
77
+ });
78
+ }
79
+ const bytes = Buffer.from(await response.arrayBuffer());
80
+ const mimeType = normalizeMimeType(response.headers.get("content-type") ?? undefined);
81
+ const extension = extname(new URL(input).pathname);
82
+ return { bytes, mimeType, extension };
83
+ }
84
+ function decodeDataUrl(input) {
85
+ const match = input.match(/^data:([^;,]+)?((?:;[^,]+)*?),(.*)$/s);
86
+ if (!match) {
87
+ throw new AgnesCliError("INPUT_INVALID", "Data URL input is malformed.");
88
+ }
89
+ const mimeType = normalizeMimeType(match[1]);
90
+ const metadata = match[2] ?? "";
91
+ const body = match[3] ?? "";
92
+ const bytes = metadata.includes(";base64")
93
+ ? Buffer.from(body, "base64")
94
+ : Buffer.from(decodeURIComponent(body));
95
+ return { bytes, mimeType };
96
+ }
97
+ async function writeTempMedia(input) {
98
+ const cleanupDir = await mkdtemp(join(tmpdir(), "agnes-media-"));
99
+ const extension = normalizeExtension(input.extension) ?? (input.mimeType ? MIME_EXTENSIONS[input.mimeType] : undefined) ?? ".bin";
100
+ const path = join(cleanupDir, `input${extension}`);
101
+ await writeFile(path, input.bytes);
102
+ return { path, cleanupDir };
103
+ }
104
+ function normalizeMimeType(value) {
105
+ return value?.split(";")[0]?.trim().toLowerCase() || undefined;
106
+ }
107
+ function normalizeExtension(value) {
108
+ if (!value)
109
+ return undefined;
110
+ const normalized = value.toLowerCase();
111
+ return /^\.[a-z0-9]+$/.test(normalized) ? normalized : undefined;
21
112
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agnes-ai-cli",
3
- "version": "0.1.0",
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": {