@vee3/upload 0.1.0

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/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@vee3/upload",
3
+ "version": "0.1.0",
4
+ "description": "Upload local files to Vee3.",
5
+ "type": "module",
6
+ "bin": {
7
+ "vee3-upload": "./src/index.js"
8
+ },
9
+ "files": ["src"],
10
+ "engines": {
11
+ "node": ">=18"
12
+ },
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/vee3io/Vee3.git",
19
+ "directory": "packages/upload"
20
+ },
21
+ "keywords": ["vee3", "upload", "cli", "mcp"],
22
+ "license": "MIT"
23
+ }
package/src/api.js ADDED
@@ -0,0 +1,34 @@
1
+ export const DEFAULT_API_BASE_URL = "https://api.vee3.io";
2
+
3
+ async function formatApiError(response) {
4
+ const responseText = await response.text();
5
+ try {
6
+ const payload = JSON.parse(responseText);
7
+ if (payload && typeof payload.error === "object") {
8
+ const requestId = payload.error.request_id ? ` (request_id=${payload.error.request_id})` : "";
9
+ return `${payload.error.code}: ${payload.error.message}${requestId}`;
10
+ }
11
+ } catch {
12
+ // Fall through to raw response text.
13
+ }
14
+ return responseText || `HTTP ${response.status}`;
15
+ }
16
+
17
+ export async function resolveUpload({ apiBaseUrl, uploadCode, contentType }) {
18
+ const response = await fetch(`${apiBaseUrl.replace(/\/$/, "")}/v1/agent-uploads/resolve`, {
19
+ method: "POST",
20
+ headers: {
21
+ "Content-Type": "application/json"
22
+ },
23
+ body: JSON.stringify({
24
+ upload_code: uploadCode,
25
+ content_type: contentType
26
+ })
27
+ });
28
+
29
+ if (!response.ok) {
30
+ throw new Error(await formatApiError(response));
31
+ }
32
+
33
+ return await response.json();
34
+ }
@@ -0,0 +1,90 @@
1
+ import { open } from "node:fs/promises";
2
+ import { extname } from "node:path";
3
+
4
+ const extensionContentTypes = new Map([
5
+ [".jpg", "image/jpeg"],
6
+ [".jpeg", "image/jpeg"],
7
+ [".png", "image/png"],
8
+ [".gif", "image/gif"],
9
+ [".webp", "image/webp"],
10
+ [".bmp", "image/bmp"],
11
+ [".tif", "image/tiff"],
12
+ [".tiff", "image/tiff"],
13
+ [".svg", "image/svg+xml"],
14
+ [".mp4", "video/mp4"],
15
+ [".m4v", "video/mp4"],
16
+ [".webm", "video/webm"],
17
+ [".mov", "video/quicktime"],
18
+ [".avi", "video/x-msvideo"],
19
+ [".mkv", "video/x-matroska"],
20
+ [".mpeg", "video/mpeg"],
21
+ [".mpg", "video/mpeg"],
22
+ [".3gp", "video/3gpp"],
23
+ [".mp3", "audio/mpeg"],
24
+ [".m4a", "audio/mp4"],
25
+ [".wav", "audio/wav"],
26
+ [".ogg", "audio/ogg"],
27
+ [".pdf", "application/pdf"],
28
+ [".zip", "application/zip"],
29
+ [".json", "application/json"],
30
+ [".txt", "text/plain"],
31
+ [".csv", "text/csv"]
32
+ ]);
33
+
34
+ function matchesBytes(bytes, expectedBytes, offset = 0) {
35
+ if (bytes.length < offset + expectedBytes.length) {
36
+ return false;
37
+ }
38
+ return expectedBytes.every((byte, index) => bytes[offset + index] === byte);
39
+ }
40
+
41
+ function readAscii(bytes, start, end) {
42
+ return Buffer.from(bytes.subarray(start, end)).toString("ascii");
43
+ }
44
+
45
+ async function readSignature(filePath) {
46
+ const file = await open(filePath, "r");
47
+ try {
48
+ const buffer = Buffer.alloc(64);
49
+ const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
50
+ return buffer.subarray(0, bytesRead);
51
+ } finally {
52
+ await file.close();
53
+ }
54
+ }
55
+
56
+ export async function detectContentType(filePath) {
57
+ const signature = await readSignature(filePath);
58
+
59
+ if (matchesBytes(signature, [0xff, 0xd8, 0xff])) {
60
+ return "image/jpeg";
61
+ }
62
+ if (matchesBytes(signature, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) {
63
+ return "image/png";
64
+ }
65
+ if (readAscii(signature, 0, 6) === "GIF87a" || readAscii(signature, 0, 6) === "GIF89a") {
66
+ return "image/gif";
67
+ }
68
+ if (readAscii(signature, 0, 4) === "RIFF" && readAscii(signature, 8, 12) === "WEBP") {
69
+ return "image/webp";
70
+ }
71
+ if (readAscii(signature, 0, 4) === "%PDF") {
72
+ return "application/pdf";
73
+ }
74
+ if (matchesBytes(signature, [0x50, 0x4b, 0x03, 0x04])) {
75
+ return "application/zip";
76
+ }
77
+ if (readAscii(signature, 0, 4) === "RIFF" && readAscii(signature, 8, 12) === "AVI ") {
78
+ return "video/x-msvideo";
79
+ }
80
+ if (matchesBytes(signature, [0x1a, 0x45, 0xdf, 0xa3])) {
81
+ return extname(filePath).toLowerCase() === ".mkv" ? "video/x-matroska" : "video/webm";
82
+ }
83
+ if (readAscii(signature, 4, 8) === "ftyp") {
84
+ const brand = readAscii(signature, 8, 12);
85
+ return brand === "qt " ? "video/quicktime" : "video/mp4";
86
+ }
87
+
88
+ const extension = extname(filePath).toLowerCase();
89
+ return extensionContentTypes.get(extension) ?? "application/octet-stream";
90
+ }
package/src/index.js ADDED
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { stat } from "node:fs/promises";
4
+ import { DEFAULT_API_BASE_URL, resolveUpload } from "./api.js";
5
+ import { detectContentType } from "./detectContentType.js";
6
+ import { uploadFileToSignedUrl } from "./uploadFile.js";
7
+
8
+ function printUsage() {
9
+ console.error(
10
+ "Usage: vee3-upload <upload_code> <file_path> [--api-base-url https://api.vee3.io] [--json]"
11
+ );
12
+ }
13
+
14
+ function parseArguments(argumentsList) {
15
+ const positionalArguments = [];
16
+ let apiBaseUrl = DEFAULT_API_BASE_URL;
17
+ let outputJson = false;
18
+
19
+ for (let index = 0; index < argumentsList.length; index += 1) {
20
+ const argument = argumentsList[index];
21
+ if (argument === "--api-base-url") {
22
+ const nextArgument = argumentsList[index + 1];
23
+ if (!nextArgument) {
24
+ throw new Error("--api-base-url requires a value");
25
+ }
26
+ apiBaseUrl = nextArgument;
27
+ index += 1;
28
+ continue;
29
+ }
30
+ if (argument === "--json") {
31
+ outputJson = true;
32
+ continue;
33
+ }
34
+ positionalArguments.push(argument);
35
+ }
36
+
37
+ if (positionalArguments.length !== 2) {
38
+ throw new Error("Expected upload_code and file_path");
39
+ }
40
+
41
+ return {
42
+ uploadCode: positionalArguments[0],
43
+ filePath: positionalArguments[1],
44
+ apiBaseUrl,
45
+ outputJson
46
+ };
47
+ }
48
+
49
+ async function main() {
50
+ const { uploadCode, filePath, apiBaseUrl, outputJson } = parseArguments(
51
+ process.argv.slice(2)
52
+ );
53
+ const fileStat = await stat(filePath);
54
+ if (!fileStat.isFile()) {
55
+ throw new Error(`File path is not a file: ${filePath}`);
56
+ }
57
+
58
+ const contentType = await detectContentType(filePath);
59
+ const resolvedUpload = await resolveUpload({
60
+ apiBaseUrl,
61
+ uploadCode,
62
+ contentType
63
+ });
64
+
65
+ if (fileStat.size > resolvedUpload.max_bytes) {
66
+ throw new Error(
67
+ `File is too large: ${fileStat.size} bytes exceeds ${resolvedUpload.max_bytes} bytes`
68
+ );
69
+ }
70
+
71
+ await uploadFileToSignedUrl({
72
+ uploadUrl: resolvedUpload.upload_url,
73
+ filePath,
74
+ fileSizeBytes: fileStat.size,
75
+ requiredHeaders: resolvedUpload.required_headers
76
+ });
77
+
78
+ if (outputJson) {
79
+ console.log(
80
+ JSON.stringify({
81
+ upload_id: resolvedUpload.upload_id,
82
+ content_type: contentType,
83
+ size_bytes: fileStat.size
84
+ })
85
+ );
86
+ return;
87
+ }
88
+
89
+ console.log(resolvedUpload.upload_id);
90
+ }
91
+
92
+ main().catch((error) => {
93
+ printUsage();
94
+ console.error(error instanceof Error ? error.message : String(error));
95
+ process.exit(1);
96
+ });
@@ -0,0 +1,25 @@
1
+ import { createReadStream } from "node:fs";
2
+
3
+ export async function uploadFileToSignedUrl({
4
+ uploadUrl,
5
+ filePath,
6
+ fileSizeBytes,
7
+ requiredHeaders
8
+ }) {
9
+ const headers = {
10
+ ...requiredHeaders,
11
+ "Content-Length": String(fileSizeBytes)
12
+ };
13
+
14
+ const response = await fetch(uploadUrl, {
15
+ method: "PUT",
16
+ headers,
17
+ body: createReadStream(filePath),
18
+ duplex: "half"
19
+ });
20
+
21
+ if (!response.ok) {
22
+ const responseText = await response.text();
23
+ throw new Error(responseText || `Upload failed with HTTP ${response.status}`);
24
+ }
25
+ }