agnes-ai-cli 0.1.0 → 0.1.1

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
@@ -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 { tmpdir } from "node:os";
4
+ import { extname, join } from "node:path";
3
5
  import { LitterboxMediaUrlProvider } from "./litterbox.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 LitterboxMediaUrlProvider(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" : "litterbox" };
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.1",
4
4
  "description": "CLI and JS API for Agnes text, image, and video workflows.",
5
5
  "type": "module",
6
6
  "bin": {