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 +4 -16
- package/dist/media/toPublicUrl.js +99 -8
- package/package.json +1 -1
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
|
-
|
|
7
|
+
Install globally:
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm install -g
|
|
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
|
-
|
|
13
|
+
Run without installing:
|
|
26
14
|
|
|
27
15
|
```bash
|
|
28
|
-
|
|
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 (
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
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
|
}
|