@genrupt/cli 0.1.0 → 0.1.3

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.
@@ -0,0 +1,156 @@
1
+ import { CLI_VERSION, DEFAULT_ORIGIN } from "./constants.js";
2
+ import { readConfig } from "./config.js";
3
+ const TELEMETRY_TIMEOUT_MS = 1500;
4
+ const ACCESS_TOKEN_SKEW_MS = 5000;
5
+ function telemetryDisabled() {
6
+ const value = process.env.GENRUPT_DISABLE_TELEMETRY?.trim().toLowerCase();
7
+ return value === "1" || value === "true" || value === "yes";
8
+ }
9
+ function normalizeOrigin(origin) {
10
+ return (origin?.trim() || DEFAULT_ORIGIN).replace(/\/$/, "");
11
+ }
12
+ function isTokenFresh(expiresAt) {
13
+ const expiresAtMs = expiresAt ? new Date(expiresAt).getTime() : Number.NaN;
14
+ return Number.isFinite(expiresAtMs) && expiresAtMs > Date.now() + ACCESS_TOKEN_SKEW_MS;
15
+ }
16
+ async function readTelemetryConfig(originOverride) {
17
+ try {
18
+ const config = await readConfig();
19
+ return {
20
+ origin: normalizeOrigin(originOverride ?? config?.origin),
21
+ accessToken: config && isTokenFresh(config.accessTokenExpiresAt) ? config.accessToken : undefined,
22
+ };
23
+ }
24
+ catch {
25
+ return {
26
+ origin: normalizeOrigin(originOverride),
27
+ };
28
+ }
29
+ }
30
+ function detectPackageManager() {
31
+ const userAgent = process.env.npm_config_user_agent?.trim();
32
+ if (userAgent) {
33
+ return userAgent.split(/\s+/)[0]?.slice(0, 160);
34
+ }
35
+ const execPath = process.env.npm_execpath?.toLowerCase();
36
+ if (execPath?.includes("pnpm"))
37
+ return "pnpm";
38
+ if (execPath?.includes("yarn"))
39
+ return "yarn";
40
+ if (execPath?.includes("npm"))
41
+ return "npm";
42
+ return undefined;
43
+ }
44
+ function detectInstallMethod() {
45
+ const configured = process.env.GENRUPT_CLI_INSTALL_METHOD?.trim();
46
+ if (configured)
47
+ return configured.slice(0, 80);
48
+ const userAgent = process.env.npm_config_user_agent?.toLowerCase() ?? "";
49
+ if (userAgent.includes("pnpm"))
50
+ return "pnpm";
51
+ if (userAgent.includes("yarn"))
52
+ return "yarn";
53
+ if (userAgent.includes("npm"))
54
+ return "npm";
55
+ return undefined;
56
+ }
57
+ function sanitizeMetadataString(value) {
58
+ const trimmed = value.trim().replace(/\s+/g, " ").slice(0, 240);
59
+ if (!trimmed)
60
+ return undefined;
61
+ if (/https?:\/\//i.test(trimmed) || /[\\/]/.test(trimmed))
62
+ return undefined;
63
+ return trimmed;
64
+ }
65
+ function sanitizeMetadataValue(value) {
66
+ if (value === null || typeof value === "boolean")
67
+ return value;
68
+ if (typeof value === "number")
69
+ return Number.isFinite(value) ? value : undefined;
70
+ if (typeof value === "string")
71
+ return sanitizeMetadataString(value);
72
+ if (Array.isArray(value)) {
73
+ const sanitized = value
74
+ .slice(0, 16)
75
+ .map((entry) => sanitizeMetadataValue(entry))
76
+ .filter((entry) => entry !== undefined);
77
+ return sanitized.length > 0 ? sanitized : undefined;
78
+ }
79
+ return undefined;
80
+ }
81
+ function sanitizeMetadata(metadata) {
82
+ if (!metadata)
83
+ return undefined;
84
+ const sanitized = {};
85
+ for (const [key, value] of Object.entries(metadata).slice(0, 24)) {
86
+ const safeKey = key.trim().replace(/[^a-zA-Z0-9_.-]/g, "_").slice(0, 64);
87
+ if (!safeKey)
88
+ continue;
89
+ const safeValue = sanitizeMetadataValue(value);
90
+ if (safeValue !== undefined) {
91
+ sanitized[safeKey] = safeValue;
92
+ }
93
+ }
94
+ return Object.keys(sanitized).length > 0 ? sanitized : undefined;
95
+ }
96
+ export function classifyCliError(error, fallback = "COMMAND_FAILED") {
97
+ const message = error instanceof Error ? error.message : String(error);
98
+ if (/not authenticated|authorization|auth|login/i.test(message))
99
+ return "AUTH_REQUIRED";
100
+ if (/subscription/i.test(message))
101
+ return "SUBSCRIPTION_REQUIRED";
102
+ if (/unsupported|file type|extension/i.test(message))
103
+ return "UNSUPPORTED_FILE_TYPE";
104
+ if (/npm install -g|globally|global genrupt cli/i.test(message)) {
105
+ return "GLOBAL_CLI_INSTALL_FAILED";
106
+ }
107
+ if (/network|fetch|enotfound|econn|etimedout|timeout/i.test(message))
108
+ return "NETWORK_FAILED";
109
+ if (/missing|required|invalid/i.test(message))
110
+ return "INVALID_INPUT";
111
+ return fallback;
112
+ }
113
+ export async function emitRuntimeTelemetry(payload, options = {}) {
114
+ if (telemetryDisabled())
115
+ return;
116
+ const config = await readTelemetryConfig(options.origin);
117
+ const controller = new AbortController();
118
+ const timeout = setTimeout(() => controller.abort(), TELEMETRY_TIMEOUT_MS);
119
+ try {
120
+ const headers = {
121
+ "content-type": "application/json",
122
+ "user-agent": `genrupt-cli/${CLI_VERSION}`,
123
+ };
124
+ if (config.accessToken) {
125
+ headers.authorization = `Bearer ${config.accessToken}`;
126
+ }
127
+ await fetch(`${config.origin}/api/agent/runtime/telemetry`, {
128
+ method: "POST",
129
+ headers,
130
+ body: JSON.stringify({
131
+ eventType: payload.eventType,
132
+ cliVersion: CLI_VERSION,
133
+ latestCliVersion: payload.latestCliVersion,
134
+ runtimeVersion: payload.runtimeVersion,
135
+ requiredCliVersion: payload.requiredCliVersion,
136
+ agent: payload.agent,
137
+ command: payload.command,
138
+ os: process.platform,
139
+ arch: process.arch,
140
+ nodeVersion: process.version,
141
+ packageManager: detectPackageManager(),
142
+ installMethod: payload.installMethod ?? detectInstallMethod(),
143
+ errorCode: payload.errorCode,
144
+ durationMs: payload.durationMs,
145
+ metadata: sanitizeMetadata(payload.metadata),
146
+ }),
147
+ signal: controller.signal,
148
+ });
149
+ }
150
+ catch {
151
+ // Telemetry must never block or change CLI behavior.
152
+ }
153
+ finally {
154
+ clearTimeout(timeout);
155
+ }
156
+ }
@@ -0,0 +1,77 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { CLI_VERSION } from "./constants.js";
4
+ import { getConfigDir } from "./config.js";
5
+ import { emitRuntimeTelemetry } from "./telemetry.js";
6
+ import { CLI_PACKAGE_NAME, fetchLatestCliVersion, isVersionNewer } from "./version.js";
7
+ const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
8
+ function getUpdateCheckPath() {
9
+ return path.join(getConfigDir(), "update-check.json");
10
+ }
11
+ async function readUpdateCheckCache() {
12
+ try {
13
+ const parsed = JSON.parse(await readFile(getUpdateCheckPath(), "utf8"));
14
+ return {
15
+ checkedAt: typeof parsed.checkedAt === "string" ? parsed.checkedAt : "",
16
+ latestVersion: typeof parsed.latestVersion === "string" ? parsed.latestVersion : undefined,
17
+ };
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ async function writeUpdateCheckCache(cache) {
24
+ try {
25
+ await mkdir(getConfigDir(), { recursive: true });
26
+ await writeFile(getUpdateCheckPath(), `${JSON.stringify(cache, null, 2)}\n`, "utf8");
27
+ }
28
+ catch {
29
+ // Update checks must never block normal CLI usage.
30
+ }
31
+ }
32
+ function shouldUseCachedCheck(cache) {
33
+ if (!cache?.checkedAt)
34
+ return false;
35
+ const checkedAtMs = new Date(cache.checkedAt).getTime();
36
+ return Number.isFinite(checkedAtMs) && Date.now() - checkedAtMs < UPDATE_CHECK_INTERVAL_MS;
37
+ }
38
+ function buildUpdateMessage(latestVersion) {
39
+ return [
40
+ "",
41
+ `A newer Genrupt CLI is available: ${latestVersion} (installed: ${CLI_VERSION}).`,
42
+ `Update: npm install -g ${CLI_PACKAGE_NAME}@latest`,
43
+ `One-time latest run: npx -y ${CLI_PACKAGE_NAME}@latest agent install`,
44
+ ].join("\n");
45
+ }
46
+ export async function maybeWarnAboutCliUpdate() {
47
+ const cached = await readUpdateCheckCache();
48
+ if (shouldUseCachedCheck(cached)) {
49
+ if (cached?.latestVersion && isVersionNewer(cached.latestVersion, CLI_VERSION)) {
50
+ console.warn(buildUpdateMessage(cached.latestVersion));
51
+ }
52
+ return;
53
+ }
54
+ try {
55
+ const latestVersion = await fetchLatestCliVersion();
56
+ await writeUpdateCheckCache({
57
+ checkedAt: new Date().toISOString(),
58
+ latestVersion,
59
+ });
60
+ if (latestVersion && isVersionNewer(latestVersion, CLI_VERSION)) {
61
+ console.warn(buildUpdateMessage(latestVersion));
62
+ await emitRuntimeTelemetry({
63
+ eventType: "stale_cli_detected",
64
+ command: "update_check",
65
+ latestCliVersion: latestVersion,
66
+ metadata: {
67
+ source: "daily_update_check",
68
+ },
69
+ });
70
+ }
71
+ }
72
+ catch {
73
+ await writeUpdateCheckCache({
74
+ checkedAt: new Date().toISOString(),
75
+ });
76
+ }
77
+ }
package/dist/upload.js CHANGED
@@ -1,20 +1,23 @@
1
- import { readFile, stat } from "node:fs/promises";
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { assertSuccessfulToolPayload, executeRemoteCapability } from "./mcpClient.js";
4
4
  const MAX_PRODUCT_REFERENCE_BYTES = 50 * 1024 * 1024;
5
- function detectContentType(filePath) {
6
- const extension = path.extname(filePath).toLowerCase();
7
- if (extension === ".jpg" || extension === ".jpeg") {
8
- return "image/jpeg";
9
- }
10
- if (extension === ".png") {
11
- return "image/png";
12
- }
13
- if (extension === ".webp") {
14
- return "image/webp";
15
- }
16
- throw new Error(`Unsupported image type for ${filePath}. Use JPEG, PNG, or WebP.`);
17
- }
5
+ const MAX_REFERENCE_MEDIA_BYTES = 50 * 1024 * 1024;
6
+ const MEDIA_BY_EXTENSION = new Map([
7
+ [".jpg", { kind: "image", contentType: "image/jpeg", fileExtension: ".jpg" }],
8
+ [".jpeg", { kind: "image", contentType: "image/jpeg", fileExtension: ".jpg" }],
9
+ [".png", { kind: "image", contentType: "image/png", fileExtension: ".png" }],
10
+ [".webp", { kind: "image", contentType: "image/webp", fileExtension: ".webp" }],
11
+ [".mp4", { kind: "video", contentType: "video/mp4", fileExtension: ".mp4" }],
12
+ [".mov", { kind: "video", contentType: "video/quicktime", fileExtension: ".mov" }],
13
+ [".webm", { kind: "video", contentType: "video/webm", fileExtension: ".webm" }],
14
+ [".mp3", { kind: "audio", contentType: "audio/mpeg", fileExtension: ".mp3" }],
15
+ [".wav", { kind: "audio", contentType: "audio/wav", fileExtension: ".wav" }],
16
+ [".m4a", { kind: "audio", contentType: "audio/mp4", fileExtension: ".m4a" }],
17
+ [".ogg", { kind: "audio", contentType: "audio/ogg", fileExtension: ".ogg" }],
18
+ ]);
19
+ const SUPPORTED_IMAGE_LABEL = "JPEG, PNG, or WebP";
20
+ const SUPPORTED_MEDIA_LABEL = "JPEG, PNG, WebP, MP4, MOV, WebM, MP3, WAV, M4A, or OGG";
18
21
  function asRecord(value) {
19
22
  return value && typeof value === "object" && !Array.isArray(value)
20
23
  ? value
@@ -44,6 +47,70 @@ function requireCompleteUploadPayload(payload) {
44
47
  }
45
48
  return record;
46
49
  }
50
+ function allowedKindsForMode(mode = "media") {
51
+ if (mode === "media") {
52
+ return new Set(["image", "video", "audio"]);
53
+ }
54
+ return new Set([mode]);
55
+ }
56
+ function describeAllowedKinds(allowedKinds) {
57
+ if (allowedKinds.size === 1 && allowedKinds.has("image"))
58
+ return SUPPORTED_IMAGE_LABEL;
59
+ if (allowedKinds.size === 1 && allowedKinds.has("video"))
60
+ return "MP4, MOV, or WebM";
61
+ if (allowedKinds.size === 1 && allowedKinds.has("audio"))
62
+ return "MP3, WAV, M4A, or OGG";
63
+ return SUPPORTED_MEDIA_LABEL;
64
+ }
65
+ function detectReferenceMedia(filePath) {
66
+ const metadata = MEDIA_BY_EXTENSION.get(path.extname(filePath).toLowerCase());
67
+ if (!metadata) {
68
+ throw new Error(`Unsupported file type for ${filePath}. Use ${SUPPORTED_MEDIA_LABEL}.`);
69
+ }
70
+ return metadata;
71
+ }
72
+ function isSupportedMediaPath(filePath, allowedKinds) {
73
+ const metadata = MEDIA_BY_EXTENSION.get(path.extname(filePath).toLowerCase());
74
+ return Boolean(metadata && allowedKinds.has(metadata.kind));
75
+ }
76
+ async function expandMediaInputPath(inputPath, allowedKinds) {
77
+ const resolvedPath = path.resolve(inputPath);
78
+ const fileStat = await stat(resolvedPath);
79
+ if (fileStat.isFile()) {
80
+ const metadata = detectReferenceMedia(resolvedPath);
81
+ if (!allowedKinds.has(metadata.kind)) {
82
+ throw new Error(`${inputPath} is ${metadata.kind} media. This upload command accepts ${describeAllowedKinds(allowedKinds)}.`);
83
+ }
84
+ return [resolvedPath];
85
+ }
86
+ if (fileStat.isDirectory()) {
87
+ const entries = await readdir(resolvedPath, { withFileTypes: true });
88
+ const mediaPaths = entries
89
+ .filter((entry) => entry.isFile() && isSupportedMediaPath(entry.name, allowedKinds))
90
+ .map((entry) => path.join(resolvedPath, entry.name))
91
+ .sort((left, right) => left.localeCompare(right));
92
+ if (mediaPaths.length === 0) {
93
+ throw new Error(`${inputPath} does not contain supported ${describeAllowedKinds(allowedKinds)} files.`);
94
+ }
95
+ return mediaPaths;
96
+ }
97
+ throw new Error(`${inputPath} is not a file or folder.`);
98
+ }
99
+ async function expandMediaInputPaths(inputPaths, mode) {
100
+ const allowedKinds = allowedKindsForMode(mode);
101
+ const expandedPaths = [];
102
+ const seen = new Set();
103
+ for (const inputPath of inputPaths) {
104
+ const paths = await expandMediaInputPath(inputPath, allowedKinds);
105
+ for (const filePath of paths) {
106
+ if (seen.has(filePath))
107
+ continue;
108
+ seen.add(filePath);
109
+ expandedPaths.push(filePath);
110
+ }
111
+ }
112
+ return expandedPaths;
113
+ }
47
114
  async function uploadBytes(params) {
48
115
  const body = new Blob([new Uint8Array(params.bytes)]);
49
116
  const response = await fetch(params.uploadUrl, {
@@ -56,67 +123,137 @@ async function uploadBytes(params) {
56
123
  throw new Error(`Storage upload failed with HTTP ${response.status}${body ? `: ${body.slice(0, 500)}` : ""}`);
57
124
  }
58
125
  }
59
- export async function uploadProductReferenceImages(options) {
60
- if (options.paths.length === 0) {
61
- throw new Error("At least one image path is required.");
126
+ async function uploadProductReferenceImageFile(filePath, options) {
127
+ const fileStat = await stat(filePath);
128
+ if (!fileStat.isFile()) {
129
+ throw new Error(`${filePath} is not a file.`);
62
130
  }
63
- const uploads = [];
64
- for (const filePath of options.paths) {
65
- const fileStat = await stat(filePath);
66
- if (!fileStat.isFile()) {
67
- throw new Error(`${filePath} is not a file.`);
68
- }
69
- if (fileStat.size > MAX_PRODUCT_REFERENCE_BYTES) {
70
- throw new Error(`${filePath} is larger than 50 MB.`);
71
- }
72
- const fileName = path.basename(filePath);
73
- const contentType = detectContentType(filePath);
74
- const createResult = await executeRemoteCapability("create_product_reference_upload", {
75
- fileName,
76
- contentType,
77
- byteLength: fileStat.size,
78
- ...(options.asin ? { asin: options.asin } : {}),
79
- ...(options.projectId ? { projectId: options.projectId } : {}),
80
- ...(options.productProfileId ? { productProfileId: options.productProfileId } : {}),
81
- ...(options.domain ? { domain: options.domain } : {}),
82
- });
83
- const createPayload = requireCreateUploadPayload(assertSuccessfulToolPayload(createResult));
84
- const bytes = await readFile(filePath);
85
- await uploadBytes({
86
- uploadUrl: createPayload.uploadUrl,
87
- method: createPayload.method ?? "PUT",
88
- headers: createPayload.headers ?? { "Content-Type": contentType },
89
- bytes,
90
- });
91
- const completeResult = await executeRemoteCapability("complete_product_reference_upload", {
92
- objectKey: createPayload.objectKey,
93
- assetUrl: createPayload.assetUrl,
94
- contentType,
95
- byteLength: fileStat.size,
96
- fileName,
97
- ...(options.asin ? { asin: options.asin } : {}),
98
- ...(options.projectId ? { projectId: options.projectId } : {}),
99
- ...(options.productProfileId ? { productProfileId: options.productProfileId } : {}),
100
- ...(options.domain ? { domain: options.domain } : {}),
101
- });
102
- const completePayload = requireCompleteUploadPayload(assertSuccessfulToolPayload(completeResult));
103
- uploads.push({
104
- path: filePath,
105
- fileName,
106
- objectKey: createPayload.objectKey,
107
- assetUrl: createPayload.assetUrl,
108
- assetId: completePayload.asset?.id ?? null,
109
- imageUrl: completePayload.asset?.imageUrl ?? createPayload.assetUrl,
110
- contentType,
111
- byteLength: fileStat.size,
112
- alreadyCompleted: completePayload.alreadyCompleted ?? false,
113
- });
131
+ if (fileStat.size > MAX_PRODUCT_REFERENCE_BYTES) {
132
+ throw new Error(`${filePath} is larger than 50 MB.`);
133
+ }
134
+ const fileName = path.basename(filePath);
135
+ const metadata = detectReferenceMedia(filePath);
136
+ if (metadata.kind !== "image") {
137
+ throw new Error(`Product reference uploads require ${SUPPORTED_IMAGE_LABEL} images.`);
114
138
  }
139
+ const createResult = await executeRemoteCapability("create_product_reference_upload", {
140
+ fileName,
141
+ contentType: metadata.contentType,
142
+ byteLength: fileStat.size,
143
+ ...(options.asin ? { asin: options.asin } : {}),
144
+ ...(options.projectId ? { projectId: options.projectId } : {}),
145
+ ...(options.productProfileId ? { productProfileId: options.productProfileId } : {}),
146
+ ...(options.domain ? { domain: options.domain } : {}),
147
+ });
148
+ const createPayload = requireCreateUploadPayload(assertSuccessfulToolPayload(createResult));
149
+ const bytes = await readFile(filePath);
150
+ await uploadBytes({
151
+ uploadUrl: createPayload.uploadUrl,
152
+ method: createPayload.method ?? "PUT",
153
+ headers: createPayload.headers ?? { "Content-Type": metadata.contentType },
154
+ bytes,
155
+ });
156
+ const completeResult = await executeRemoteCapability("complete_product_reference_upload", {
157
+ objectKey: createPayload.objectKey,
158
+ assetUrl: createPayload.assetUrl,
159
+ contentType: metadata.contentType,
160
+ byteLength: fileStat.size,
161
+ fileName,
162
+ ...(options.asin ? { asin: options.asin } : {}),
163
+ ...(options.projectId ? { projectId: options.projectId } : {}),
164
+ ...(options.productProfileId ? { productProfileId: options.productProfileId } : {}),
165
+ ...(options.domain ? { domain: options.domain } : {}),
166
+ });
167
+ const completePayload = requireCompleteUploadPayload(assertSuccessfulToolPayload(completeResult));
168
+ const imageUrl = completePayload.asset?.imageUrl ?? createPayload.assetUrl;
169
+ return {
170
+ path: filePath,
171
+ fileName,
172
+ mediaKind: "image",
173
+ objectKey: createPayload.objectKey,
174
+ assetUrl: createPayload.assetUrl,
175
+ url: imageUrl,
176
+ assetId: completePayload.asset?.id ?? null,
177
+ imageUrl,
178
+ contentType: metadata.contentType,
179
+ byteLength: fileStat.size,
180
+ alreadyCompleted: completePayload.alreadyCompleted ?? false,
181
+ };
182
+ }
183
+ async function uploadReferenceMediaFile(filePath, options) {
184
+ const fileStat = await stat(filePath);
185
+ if (!fileStat.isFile()) {
186
+ throw new Error(`${filePath} is not a file.`);
187
+ }
188
+ if (fileStat.size > MAX_REFERENCE_MEDIA_BYTES) {
189
+ throw new Error(`${filePath} is larger than 50 MB.`);
190
+ }
191
+ const fileName = path.basename(filePath);
192
+ const metadata = detectReferenceMedia(filePath);
193
+ const createResult = await executeRemoteCapability("create_reference_asset_upload", {
194
+ fileName,
195
+ contentType: metadata.contentType,
196
+ byteLength: fileStat.size,
197
+ fileExtension: metadata.fileExtension,
198
+ ...(options.projectId ? { projectId: options.projectId } : {}),
199
+ });
200
+ const createPayload = requireCreateUploadPayload(assertSuccessfulToolPayload(createResult));
201
+ const bytes = await readFile(filePath);
202
+ await uploadBytes({
203
+ uploadUrl: createPayload.uploadUrl,
204
+ method: createPayload.method ?? "PUT",
205
+ headers: createPayload.headers ?? { "Content-Type": metadata.contentType },
206
+ bytes,
207
+ });
208
+ return {
209
+ path: filePath,
210
+ fileName,
211
+ mediaKind: metadata.kind,
212
+ objectKey: createPayload.objectKey,
213
+ assetUrl: createPayload.assetUrl,
214
+ url: createPayload.assetUrl,
215
+ contentType: metadata.contentType,
216
+ byteLength: fileStat.size,
217
+ };
218
+ }
219
+ function buildUploadResult(uploads, nextSuggestedTool) {
115
220
  return {
116
221
  uploads,
117
222
  sourceAssetIds: uploads
118
223
  .map((upload) => upload.assetId)
119
224
  .filter((assetId) => Boolean(assetId)),
120
- nextSuggestedTool: "create_product_reference_sheet",
225
+ referenceImageUrls: uploads
226
+ .filter((upload) => upload.mediaKind === "image")
227
+ .map((upload) => upload.url),
228
+ referenceVideoUrls: uploads
229
+ .filter((upload) => upload.mediaKind === "video")
230
+ .map((upload) => upload.url),
231
+ referenceAudioUrls: uploads
232
+ .filter((upload) => upload.mediaKind === "audio")
233
+ .map((upload) => upload.url),
234
+ nextSuggestedTool,
121
235
  };
122
236
  }
237
+ export async function uploadProductReferenceImages(options) {
238
+ if (options.paths.length === 0) {
239
+ throw new Error("At least one image path is required.");
240
+ }
241
+ const imagePaths = await expandMediaInputPaths(options.paths, "image");
242
+ const uploads = [];
243
+ for (const filePath of imagePaths) {
244
+ uploads.push(await uploadProductReferenceImageFile(filePath, options));
245
+ }
246
+ return buildUploadResult(uploads, "create_product_reference_sheet");
247
+ }
248
+ export async function uploadReferenceMedia(options) {
249
+ if (options.paths.length === 0) {
250
+ throw new Error("At least one media path is required.");
251
+ }
252
+ const mode = options.mode ?? "media";
253
+ const mediaPaths = await expandMediaInputPaths(options.paths, mode);
254
+ const uploads = [];
255
+ for (const filePath of mediaPaths) {
256
+ uploads.push(await uploadReferenceMediaFile(filePath, options));
257
+ }
258
+ return buildUploadResult(uploads, "generate_video");
259
+ }
@@ -0,0 +1,32 @@
1
+ export const CLI_PACKAGE_NAME = "@genrupt/cli";
2
+ export const VERSION_CHECK_TIMEOUT_MS = 2_000;
3
+ export function parseVersionParts(version) {
4
+ const [major = "0", minor = "0", patch = "0"] = version.split(/[.-]/, 3);
5
+ return [major, minor, patch].map((part) => Number.parseInt(part, 10) || 0);
6
+ }
7
+ export function compareVersions(left, right) {
8
+ const leftParts = parseVersionParts(left);
9
+ const rightParts = parseVersionParts(right);
10
+ for (let index = 0; index < leftParts.length; index += 1) {
11
+ if (leftParts[index] > rightParts[index])
12
+ return 1;
13
+ if (leftParts[index] < rightParts[index])
14
+ return -1;
15
+ }
16
+ return 0;
17
+ }
18
+ export function isVersionNewer(candidate, current) {
19
+ return compareVersions(candidate, current) > 0;
20
+ }
21
+ export function isVersionAtLeast(candidate, minimum) {
22
+ return compareVersions(candidate, minimum) >= 0;
23
+ }
24
+ export async function fetchLatestCliVersion() {
25
+ const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(CLI_PACKAGE_NAME)}/latest`, {
26
+ signal: AbortSignal.timeout(VERSION_CHECK_TIMEOUT_MS),
27
+ });
28
+ if (!response.ok)
29
+ return undefined;
30
+ const payload = (await response.json());
31
+ return typeof payload.version === "string" ? payload.version : undefined;
32
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genrupt/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "Genrupt CLI for OAuth login, local file uploads, and local MCP bridge setup.",
5
5
  "license": "MIT",
6
6
  "repository": {