clawbr 0.0.42 ā 0.0.44
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/dist/commands/generate.command.js +21 -10
- package/dist/commands/generate.command.js.map +1 -1
- package/dist/commands/post.command.js +35 -33
- package/dist/commands/post.command.js.map +1 -1
- package/dist/utils/image.js +42 -7
- package/dist/utils/image.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +8 -1
|
@@ -86,34 +86,37 @@ export class GenerateCommand extends CommandRunner {
|
|
|
86
86
|
const spinner = json ? null : ora(sourceImageData ? "Generating image from source..." : "Generating image...").start();
|
|
87
87
|
try {
|
|
88
88
|
let imageBuffer;
|
|
89
|
+
let modelUsed;
|
|
89
90
|
// Determine models to try
|
|
90
91
|
const primaryModel = model || getPrimaryModel(aiProvider);
|
|
91
|
-
|
|
92
|
+
// If the user explicitly specified a model, disable fallbacks so we
|
|
93
|
+
// honour their choice strictly and fail fast if it doesn't work.
|
|
94
|
+
const fallbackModels = model ? [] : getFallbackModels(aiProvider);
|
|
92
95
|
// Pass aspect ratio and image size to generation
|
|
93
96
|
const imageConfig = {};
|
|
94
97
|
if (aspectRatio) imageConfig.aspectRatio = aspectRatio;
|
|
95
98
|
if (imageSize) imageConfig.imageSize = imageSize;
|
|
96
99
|
if (aiProvider === "openrouter") {
|
|
97
|
-
imageBuffer = await this.generateWithFallback(prompt, size, apiKey, "openrouter", {
|
|
100
|
+
({ buffer: imageBuffer, modelUsed } = await this.generateWithFallback(prompt, size, apiKey, "openrouter", {
|
|
98
101
|
primary: primaryModel,
|
|
99
102
|
fallbacks: fallbackModels
|
|
100
|
-
}, spinner, sourceImageData, imageConfig);
|
|
103
|
+
}, spinner, sourceImageData, imageConfig));
|
|
101
104
|
} else if (aiProvider === "openai") {
|
|
102
105
|
if (sourceImageData) {
|
|
103
106
|
throw new Error("OpenAI does not support image-to-image generation. Use OpenRouter with a model that supports reference images.");
|
|
104
107
|
}
|
|
105
|
-
imageBuffer = await this.generateWithFallback(prompt, size, apiKey, "openai", {
|
|
108
|
+
({ buffer: imageBuffer, modelUsed } = await this.generateWithFallback(prompt, size, apiKey, "openai", {
|
|
106
109
|
primary: primaryModel,
|
|
107
110
|
fallbacks: fallbackModels
|
|
108
|
-
}, spinner);
|
|
111
|
+
}, spinner));
|
|
109
112
|
} else if (aiProvider === "google") {
|
|
110
113
|
if (sourceImageData) {
|
|
111
114
|
throw new Error("Google Imagen does not support image-to-image generation. Use OpenRouter with a model that supports reference images.");
|
|
112
115
|
}
|
|
113
|
-
imageBuffer = await this.generateWithFallback(prompt, size, apiKey, "google", {
|
|
116
|
+
({ buffer: imageBuffer, modelUsed } = await this.generateWithFallback(prompt, size, apiKey, "google", {
|
|
114
117
|
primary: primaryModel,
|
|
115
118
|
fallbacks: fallbackModels
|
|
116
|
-
}, spinner);
|
|
119
|
+
}, spinner));
|
|
117
120
|
} else {
|
|
118
121
|
if (spinner) spinner.fail();
|
|
119
122
|
throw new Error(`Unsupported AI provider: ${aiProvider}`);
|
|
@@ -135,7 +138,8 @@ export class GenerateCommand extends CommandRunner {
|
|
|
135
138
|
prompt,
|
|
136
139
|
output: outputPath,
|
|
137
140
|
size,
|
|
138
|
-
provider: aiProvider
|
|
141
|
+
provider: aiProvider,
|
|
142
|
+
modelUsed
|
|
139
143
|
}, null, 2));
|
|
140
144
|
} else {
|
|
141
145
|
console.log("\nšØ Image Generation Complete!");
|
|
@@ -180,7 +184,10 @@ export class GenerateCommand extends CommandRunner {
|
|
|
180
184
|
// Only show fallback message if we had to fall back
|
|
181
185
|
spinner.info(`Successfully generated with fallback model: ${model}`);
|
|
182
186
|
}
|
|
183
|
-
return
|
|
187
|
+
return {
|
|
188
|
+
buffer: imageBuffer,
|
|
189
|
+
modelUsed: model
|
|
190
|
+
};
|
|
184
191
|
} catch (error) {
|
|
185
192
|
lastError = error;
|
|
186
193
|
// If this wasn't the last model, log the failure and try the next one
|
|
@@ -310,7 +317,11 @@ export class GenerateCommand extends CommandRunner {
|
|
|
310
317
|
const imageUrl = result.choices[0].message.images[0].image_url.url;
|
|
311
318
|
// If it's a URL, fetch it
|
|
312
319
|
if (imageUrl.startsWith("http")) {
|
|
313
|
-
const imgRes = await fetch(imageUrl
|
|
320
|
+
const imgRes = await fetch(imageUrl, {
|
|
321
|
+
headers: {
|
|
322
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
|
|
323
|
+
}
|
|
324
|
+
});
|
|
314
325
|
const arrayBuffer = await imgRes.arrayBuffer();
|
|
315
326
|
return Buffer.from(arrayBuffer);
|
|
316
327
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/commands/generate.command.ts"],"sourcesContent":["import { Command, CommandRunner, Option } from \"nest-commander\";\nimport { writeFileSync } from \"fs\";\nimport ora from \"ora\";\nimport fetch from \"node-fetch\";\nimport { resolve } from \"path\";\nimport { generateImage } from \"ai\";\nimport { createOpenAI } from \"@ai-sdk/openai\";\nimport { createGoogleGenerativeAI } from \"@ai-sdk/google\";\nimport { loadCredentials } from \"../utils/credentials.js\";\nimport { resolveImageToDataUri, validateImageInput } from \"../utils/image.js\";\nimport { requireOnboarding } from \"../utils/config.js\";\nimport {\n getProviderModels,\n getModelById,\n isValidModel,\n getPrimaryModel,\n getFallbackModels,\n supportsReferenceImage,\n formatModelList,\n} from \"../config/image-models.js\";\n\ninterface GenerateCommandOptions {\n prompt?: string;\n output?: string;\n size?: string;\n sourceImage?: string;\n model?: string;\n aspectRatio?: string;\n imageSize?: string;\n json?: boolean;\n}\n\n@Command({\n name: \"generate\",\n description: \"Generate an image using AI with smart model fallback\",\n arguments: \"\",\n options: { isDefault: false },\n})\nexport class GenerateCommand extends CommandRunner {\n async run(inputs: string[], options: GenerateCommandOptions): Promise<void> {\n await requireOnboarding();\n const {\n prompt,\n output,\n size = \"1024x1024\",\n sourceImage,\n model,\n aspectRatio,\n imageSize,\n json = false,\n } = options;\n\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n // Validation\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n if (!prompt) {\n throw new Error(\n '--prompt is required. Example: clawbr generate --prompt \"a robot building software\" --output \"./robot.png\"'\n );\n }\n\n if (!output) {\n throw new Error(\n '--output is required. Example: clawbr generate --prompt \"...\" --output \"./image.png\"'\n );\n }\n\n // Validate source image if provided\n if (sourceImage) {\n const validation = validateImageInput(sourceImage);\n if (!validation.valid) {\n throw new Error(validation.error);\n }\n }\n\n // Validate size\n const validSizes = [\"256x256\", \"512x512\", \"1024x1024\", \"1792x1024\", \"1024x1792\"];\n if (!validSizes.includes(size)) {\n throw new Error(`Invalid size. Must be one of: ${validSizes.join(\", \")}`);\n }\n\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n // Load Credentials\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n const credentials = loadCredentials();\n\n if (!credentials) {\n throw new Error(\"Credentials not found. Run 'clawbr onboard' first to set up your account.\");\n }\n\n const { aiProvider, apiKeys } = credentials;\n const apiKey = apiKeys[aiProvider as keyof typeof apiKeys];\n\n if (!apiKey) {\n throw new Error(\n `No API key found for provider '${aiProvider}'. Run 'clawbr onboard' to configure.`\n );\n }\n\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n // Validate model if provided\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n if (model && !isValidModel(aiProvider, model)) {\n const availableModels = formatModelList(aiProvider);\n throw new Error(\n `Invalid model '${model}' for provider '${aiProvider}'.\\n\\nAvailable models:\\n${availableModels}`\n );\n }\n\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n // Check reference image support\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n if (sourceImage && model && !supportsReferenceImage(aiProvider, model)) {\n const modelInfo = getModelById(aiProvider, model);\n throw new Error(\n `Model '${modelInfo?.name || model}' does not support reference images.\\n\\n` +\n `For reference image support with ${aiProvider}, use one of:\\n` +\n getProviderModels(aiProvider)\n .filter((m) => m.supportsReferenceImage)\n .map((m) => ` ⢠${m.id}`)\n .join(\"\\n\")\n );\n }\n\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n // Prepare source image if provided\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n const sourceImageData = sourceImage ? await resolveImageToDataUri(sourceImage) : undefined;\n\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n // Generate Image with Smart Fallback\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n const spinner = json\n ? null\n : ora(sourceImageData ? \"Generating image from source...\" : \"Generating image...\").start();\n\n try {\n let imageBuffer: Buffer;\n\n // Determine models to try\n const primaryModel = model || getPrimaryModel(aiProvider);\n const fallbackModels = getFallbackModels(aiProvider);\n\n // Pass aspect ratio and image size to generation\n const imageConfig: { aspectRatio?: string; imageSize?: string } = {};\n if (aspectRatio) imageConfig.aspectRatio = aspectRatio;\n if (imageSize) imageConfig.imageSize = imageSize;\n\n if (aiProvider === \"openrouter\") {\n imageBuffer = await this.generateWithFallback(\n prompt,\n size,\n apiKey,\n \"openrouter\",\n { primary: primaryModel, fallbacks: fallbackModels },\n spinner,\n sourceImageData,\n imageConfig\n );\n } else if (aiProvider === \"openai\") {\n if (sourceImageData) {\n throw new Error(\n \"OpenAI does not support image-to-image generation. Use OpenRouter with a model that supports reference images.\"\n );\n }\n imageBuffer = await this.generateWithFallback(\n prompt,\n size,\n apiKey,\n \"openai\",\n { primary: primaryModel, fallbacks: fallbackModels },\n spinner\n );\n } else if (aiProvider === \"google\") {\n if (sourceImageData) {\n throw new Error(\n \"Google Imagen does not support image-to-image generation. Use OpenRouter with a model that supports reference images.\"\n );\n }\n imageBuffer = await this.generateWithFallback(\n prompt,\n size,\n apiKey,\n \"google\",\n { primary: primaryModel, fallbacks: fallbackModels },\n spinner\n );\n } else {\n if (spinner) spinner.fail();\n throw new Error(`Unsupported AI provider: ${aiProvider}`);\n }\n\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n // Save Image\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n const outputPath = resolve(output);\n writeFileSync(outputPath, imageBuffer);\n\n if (spinner) {\n spinner.succeed(`Image generated and saved to: ${outputPath}`);\n }\n\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n // Output\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n if (json) {\n console.log(\n JSON.stringify(\n {\n success: true,\n prompt,\n output: outputPath,\n size,\n provider: aiProvider,\n },\n null,\n 2\n )\n );\n } else {\n console.log(\"\\nšØ Image Generation Complete!\");\n console.log(\"āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\");\n console.log(`Prompt: ${prompt}`);\n console.log(`Size: ${size}`);\n if (sourceImageData) {\n console.log(`Source Image: ${sourceImage}`);\n }\n console.log(`Output: ${outputPath}`);\n console.log(`Provider: ${aiProvider}`);\n if (model) {\n console.log(`Model: ${model}`);\n }\n console.log(\"āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\\n\");\n }\n } catch (error) {\n if (spinner && spinner.isSpinning) {\n spinner.fail(\"Image generation failed\");\n }\n throw error;\n }\n }\n\n /**\n * Generate image with smart fallback chain\n * Tries primary model first, then falls back to alternatives if it fails\n */\n private async generateWithFallback(\n prompt: string,\n size: string,\n apiKey: string,\n provider: \"openrouter\" | \"openai\" | \"google\",\n config: { primary: string | null; fallbacks: string[] },\n spinner: {\n text: string;\n info: (msg: string) => void;\n warn: (msg: string) => void;\n isSpinning?: boolean;\n } | null,\n sourceImageData?: string,\n imageConfig?: { aspectRatio?: string; imageSize?: string }\n ): Promise<Buffer> {\n const modelsToTry = [config.primary, ...config.fallbacks].filter(\n (model): model is string => model !== null\n );\n\n let lastError: Error | null = null;\n\n for (let i = 0; i < modelsToTry.length; i++) {\n const model = modelsToTry[i];\n\n try {\n if (spinner) {\n const modelName = model.split(\"/\").pop() || model;\n spinner.text = `Generating image with ${modelName}... (attempt ${i + 1}/${modelsToTry.length})`;\n }\n\n const imageBuffer = await this.generateWithModel(\n prompt,\n size,\n apiKey,\n provider,\n model,\n sourceImageData,\n imageConfig\n );\n\n if (spinner && i > 0) {\n // Only show fallback message if we had to fall back\n spinner.info(`Successfully generated with fallback model: ${model}`);\n }\n\n return imageBuffer;\n } catch (error) {\n lastError = error as Error;\n\n // If this wasn't the last model, log the failure and try the next one\n if (i < modelsToTry.length - 1) {\n if (spinner) {\n spinner.warn(`Model ${model} failed, trying fallback...`);\n } else {\n console.warn(`Model ${model} failed: ${lastError.message}`);\n }\n continue;\n }\n }\n }\n\n // If we get here, all models failed\n throw new Error(\n `All models failed to generate image. Last error: ${lastError?.message || \"Unknown error\"}`\n );\n }\n\n /**\n * get the model configuration for the AI SDK\n */\n private getImageModel(provider: string, apiKey: string, model: string) {\n if (provider === \"openai\") {\n const openai = createOpenAI({ apiKey });\n return openai.image(model);\n } else if (provider === \"google\") {\n const google = createGoogleGenerativeAI({ apiKey });\n return google.image(model);\n }\n throw new Error(`Provider ${provider} not supported via AI SDK`);\n }\n\n /**\n * Generate image using a specific model\n */\n private async generateWithModel(\n prompt: string,\n size: string,\n apiKey: string,\n provider: \"openrouter\" | \"openai\" | \"google\",\n model: string,\n sourceImageData?: string,\n imageConfig?: { aspectRatio?: string; imageSize?: string }\n ): Promise<Buffer> {\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n // OPENROUTER (Via Fetch / Chat Completions)\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n if (provider === \"openrouter\") {\n // Calculate aspect ratio from size if not provided\n let aspectRatio = imageConfig?.aspectRatio || \"1:1\";\n if (!imageConfig?.aspectRatio) {\n const [width, height] = size.split(\"x\").map(Number);\n if (width && height) {\n const gcd = (a: number, b: number): number => (b === 0 ? a : gcd(b, a % b));\n const divisor = gcd(width, height);\n const calculated = `${width / divisor}:${height / divisor}`;\n\n // Map calculated ratio to supported OpenRouter ratios\n const supportedRatios: Record<string, string> = {\n \"1:1\": \"1:1\",\n \"2:3\": \"2:3\",\n \"3:2\": \"3:2\",\n \"3:4\": \"3:4\",\n \"4:3\": \"4:3\",\n \"4:5\": \"4:5\",\n \"5:4\": \"5:4\",\n \"9:16\": \"9:16\",\n \"16:9\": \"16:9\",\n \"21:9\": \"21:9\",\n // Common unsupported ratios mapped to closest supported\n \"7:4\": \"16:9\", // 1792x1024\n \"4:7\": \"9:16\", // 1024x1792\n \"64:27\": \"21:9\", // ultrawide variants\n };\n\n aspectRatio = supportedRatios[calculated] || \"1:1\";\n }\n }\n\n // Build messages array\n let content: Array<{ type: string; text?: string; image_url?: { url: string } }> | string;\n if (sourceImageData) {\n // Image-to-image generation: include source image in content\n content = [\n {\n type: \"text\",\n text: prompt,\n },\n {\n type: \"image_url\",\n image_url: {\n url: sourceImageData,\n },\n },\n ];\n } else {\n // Text-to-image generation: just the prompt\n content = prompt;\n }\n\n const response = await fetch(\"https://openrouter.ai/api/v1/chat/completions\", {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n \"HTTP-Referer\": \"https://clawbr.bricks-studio.ai\",\n \"X-Title\": \"clawbr CLI\",\n },\n body: JSON.stringify({\n model: model,\n messages: [\n {\n role: \"user\",\n content: content,\n },\n ],\n // Specific to Gemini/OpenRouter multimodal\n modalities: [\"image\", \"text\"],\n ...(aspectRatio || imageConfig?.imageSize\n ? {\n image_config: {\n ...(aspectRatio ? { aspect_ratio: aspectRatio } : {}),\n ...(imageConfig?.imageSize ? { image_size: imageConfig.imageSize } : {}),\n },\n }\n : {}),\n }),\n });\n\n if (!response.ok) {\n const text = await response.text();\n throw new Error(`OpenRouter API error: ${text}`);\n }\n\n const result = (await response.json()) as any;\n\n if (result.choices?.[0]?.message?.images?.[0]?.image_url?.url) {\n const imageUrl = result.choices[0].message.images[0].image_url.url;\n\n // If it's a URL, fetch it\n if (imageUrl.startsWith(\"http\")) {\n const imgRes = await fetch(imageUrl);\n const arrayBuffer = await imgRes.arrayBuffer();\n return Buffer.from(arrayBuffer);\n }\n\n // If it's base64 data URI\n if (imageUrl.startsWith(\"data:image\")) {\n const base64Data = imageUrl.split(\",\")[1];\n return Buffer.from(base64Data, \"base64\");\n }\n\n throw new Error(\"Unknown image URL format\");\n }\n\n throw new Error(\"No image generated from OpenRouter response\");\n }\n\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n // OPENAI / GOOGLE (Via AI SDK)\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n const imageModel = this.getImageModel(provider, apiKey, model);\n\n // Pass size as string directly as per SDK requirements.\n // We cast to 'any' to avoid strict template literal validation errors\n // since we know validSizes allows specifically \"1024x1024\" etc.\n const { image } = await generateImage({\n model: imageModel,\n prompt,\n n: 1,\n size: size as any,\n });\n\n // The image object from 'ai' SDK contains the base64 string\n return Buffer.from(image.base64, \"base64\");\n }\n\n @Option({\n flags: \"-p, --prompt <text>\",\n description: \"Text description of the image to generate\",\n })\n parsePrompt(val: string): string {\n return val;\n }\n\n @Option({\n flags: \"-o, --output <path>\",\n description: \"Path where the generated image will be saved\",\n })\n parseOutput(val: string): string {\n return val;\n }\n\n @Option({\n flags: \"-s, --size <size>\",\n description: \"Image size (256x256, 512x512, 1024x1024, 1792x1024, 1024x1792)\",\n })\n parseSize(val: string): string {\n return val;\n }\n\n @Option({\n flags: \"--source-image <path>\",\n description: \"Path to source image or URL (for image-to-image generation, OpenRouter only)\",\n })\n parseSourceImage(val: string): string {\n return val;\n }\n\n @Option({\n flags: \"-m, --model <modelId>\",\n description:\n \"Specific model to use (provider-dependent). Use model ID from your provider's list. Note: Not all models support reference images (--source-image).\",\n })\n parseModel(val: string): string {\n return val;\n }\n\n @Option({\n flags: \"--aspect-ratio <ratio>\",\n description:\n \"Aspect ratio for generated image (OpenRouter only). Supported: 1:1, 2:3, 3:2, 3:4, 4:3, 4:5, 5:4, 9:16, 16:9, 21:9\",\n })\n parseAspectRatio(val: string): string {\n return val;\n }\n\n @Option({\n flags: \"--image-size <size>\",\n description:\n \"Image resolution size (OpenRouter only). Supported: 1K (standard), 2K (higher), 4K (highest)\",\n })\n parseImageSize(val: string): string {\n return val;\n }\n\n @Option({\n flags: \"--json\",\n description: \"Output result in JSON format\",\n })\n parseJson(): boolean {\n return true;\n }\n}\n"],"names":["Command","CommandRunner","Option","writeFileSync","ora","fetch","resolve","generateImage","createOpenAI","createGoogleGenerativeAI","loadCredentials","resolveImageToDataUri","validateImageInput","requireOnboarding","getProviderModels","getModelById","isValidModel","getPrimaryModel","getFallbackModels","supportsReferenceImage","formatModelList","GenerateCommand","run","inputs","options","prompt","output","size","sourceImage","model","aspectRatio","imageSize","json","Error","validation","valid","error","validSizes","includes","join","credentials","aiProvider","apiKeys","apiKey","availableModels","modelInfo","name","filter","m","map","id","sourceImageData","undefined","spinner","start","imageBuffer","primaryModel","fallbackModels","imageConfig","generateWithFallback","primary","fallbacks","fail","outputPath","succeed","console","log","JSON","stringify","success","provider","isSpinning","config","modelsToTry","lastError","i","length","modelName","split","pop","text","generateWithModel","info","warn","message","getImageModel","openai","image","google","width","height","Number","gcd","a","b","divisor","calculated","supportedRatios","content","type","image_url","url","response","method","headers","Authorization","body","messages","role","modalities","image_config","aspect_ratio","image_size","ok","result","choices","images","imageUrl","startsWith","imgRes","arrayBuffer","Buffer","from","base64Data","imageModel","n","base64","parsePrompt","val","parseOutput","parseSize","parseSourceImage","parseModel","parseAspectRatio","parseImageSize","parseJson","flags","description","arguments","isDefault"],"mappings":";;;;;;;;;AAAA,SAASA,OAAO,EAAEC,aAAa,EAAEC,MAAM,QAAQ,iBAAiB;AAChE,SAASC,aAAa,QAAQ,KAAK;AACnC,OAAOC,SAAS,MAAM;AACtB,OAAOC,WAAW,aAAa;AAC/B,SAASC,OAAO,QAAQ,OAAO;AAC/B,SAASC,aAAa,QAAQ,KAAK;AACnC,SAASC,YAAY,QAAQ,iBAAiB;AAC9C,SAASC,wBAAwB,QAAQ,iBAAiB;AAC1D,SAASC,eAAe,QAAQ,0BAA0B;AAC1D,SAASC,qBAAqB,EAAEC,kBAAkB,QAAQ,oBAAoB;AAC9E,SAASC,iBAAiB,QAAQ,qBAAqB;AACvD,SACEC,iBAAiB,EACjBC,YAAY,EACZC,YAAY,EACZC,eAAe,EACfC,iBAAiB,EACjBC,sBAAsB,EACtBC,eAAe,QACV,4BAA4B;AAmBnC,OAAO,MAAMC,wBAAwBpB;IACnC,MAAMqB,IAAIC,MAAgB,EAAEC,OAA+B,EAAiB;QAC1E,MAAMX;QACN,MAAM,EACJY,MAAM,EACNC,MAAM,EACNC,OAAO,WAAW,EAClBC,WAAW,EACXC,KAAK,EACLC,WAAW,EACXC,SAAS,EACTC,OAAO,KAAK,EACb,GAAGR;QAEJ,wEAAwE;QACxE,aAAa;QACb,wEAAwE;QACxE,IAAI,CAACC,QAAQ;YACX,MAAM,IAAIQ,MACR;QAEJ;QAEA,IAAI,CAACP,QAAQ;YACX,MAAM,IAAIO,MACR;QAEJ;QAEA,oCAAoC;QACpC,IAAIL,aAAa;YACf,MAAMM,aAAatB,mBAAmBgB;YACtC,IAAI,CAACM,WAAWC,KAAK,EAAE;gBACrB,MAAM,IAAIF,MAAMC,WAAWE,KAAK;YAClC;QACF;QAEA,gBAAgB;QAChB,MAAMC,aAAa;YAAC;YAAW;YAAW;YAAa;YAAa;SAAY;QAChF,IAAI,CAACA,WAAWC,QAAQ,CAACX,OAAO;YAC9B,MAAM,IAAIM,MAAM,CAAC,8BAA8B,EAAEI,WAAWE,IAAI,CAAC,OAAO;QAC1E;QAEA,wEAAwE;QACxE,mBAAmB;QACnB,wEAAwE;QACxE,MAAMC,cAAc9B;QAEpB,IAAI,CAAC8B,aAAa;YAChB,MAAM,IAAIP,MAAM;QAClB;QAEA,MAAM,EAAEQ,UAAU,EAAEC,OAAO,EAAE,GAAGF;QAChC,MAAMG,SAASD,OAAO,CAACD,WAAmC;QAE1D,IAAI,CAACE,QAAQ;YACX,MAAM,IAAIV,MACR,CAAC,+BAA+B,EAAEQ,WAAW,qCAAqC,CAAC;QAEvF;QAEA,wEAAwE;QACxE,6BAA6B;QAC7B,wEAAwE;QACxE,IAAIZ,SAAS,CAACb,aAAayB,YAAYZ,QAAQ;YAC7C,MAAMe,kBAAkBxB,gBAAgBqB;YACxC,MAAM,IAAIR,MACR,CAAC,eAAe,EAAEJ,MAAM,gBAAgB,EAAEY,WAAW,yBAAyB,EAAEG,iBAAiB;QAErG;QAEA,wEAAwE;QACxE,gCAAgC;QAChC,wEAAwE;QACxE,IAAIhB,eAAeC,SAAS,CAACV,uBAAuBsB,YAAYZ,QAAQ;YACtE,MAAMgB,YAAY9B,aAAa0B,YAAYZ;YAC3C,MAAM,IAAII,MACR,CAAC,OAAO,EAAEY,WAAWC,QAAQjB,MAAM,wCAAwC,CAAC,GAC1E,CAAC,iCAAiC,EAAEY,WAAW,eAAe,CAAC,GAC/D3B,kBAAkB2B,YACfM,MAAM,CAAC,CAACC,IAAMA,EAAE7B,sBAAsB,EACtC8B,GAAG,CAAC,CAACD,IAAM,CAAC,IAAI,EAAEA,EAAEE,EAAE,EAAE,EACxBX,IAAI,CAAC;QAEd;QAEA,wEAAwE;QACxE,mCAAmC;QACnC,wEAAwE;QACxE,MAAMY,kBAAkBvB,cAAc,MAAMjB,sBAAsBiB,eAAewB;QAEjF,wEAAwE;QACxE,qCAAqC;QACrC,wEAAwE;QACxE,MAAMC,UAAUrB,OACZ,OACA5B,IAAI+C,kBAAkB,oCAAoC,uBAAuBG,KAAK;QAE1F,IAAI;YACF,IAAIC;YAEJ,0BAA0B;YAC1B,MAAMC,eAAe3B,SAASZ,gBAAgBwB;YAC9C,MAAMgB,iBAAiBvC,kBAAkBuB;YAEzC,iDAAiD;YACjD,MAAMiB,cAA4D,CAAC;YACnE,IAAI5B,aAAa4B,YAAY5B,WAAW,GAAGA;YAC3C,IAAIC,WAAW2B,YAAY3B,SAAS,GAAGA;YAEvC,IAAIU,eAAe,cAAc;gBAC/Bc,cAAc,MAAM,IAAI,CAACI,oBAAoB,CAC3ClC,QACAE,MACAgB,QACA,cACA;oBAAEiB,SAASJ;oBAAcK,WAAWJ;gBAAe,GACnDJ,SACAF,iBACAO;YAEJ,OAAO,IAAIjB,eAAe,UAAU;gBAClC,IAAIU,iBAAiB;oBACnB,MAAM,IAAIlB,MACR;gBAEJ;gBACAsB,cAAc,MAAM,IAAI,CAACI,oBAAoB,CAC3ClC,QACAE,MACAgB,QACA,UACA;oBAAEiB,SAASJ;oBAAcK,WAAWJ;gBAAe,GACnDJ;YAEJ,OAAO,IAAIZ,eAAe,UAAU;gBAClC,IAAIU,iBAAiB;oBACnB,MAAM,IAAIlB,MACR;gBAEJ;gBACAsB,cAAc,MAAM,IAAI,CAACI,oBAAoB,CAC3ClC,QACAE,MACAgB,QACA,UACA;oBAAEiB,SAASJ;oBAAcK,WAAWJ;gBAAe,GACnDJ;YAEJ,OAAO;gBACL,IAAIA,SAASA,QAAQS,IAAI;gBACzB,MAAM,IAAI7B,MAAM,CAAC,yBAAyB,EAAEQ,YAAY;YAC1D;YAEA,wEAAwE;YACxE,aAAa;YACb,wEAAwE;YACxE,MAAMsB,aAAazD,QAAQoB;YAC3BvB,cAAc4D,YAAYR;YAE1B,IAAIF,SAAS;gBACXA,QAAQW,OAAO,CAAC,CAAC,8BAA8B,EAAED,YAAY;YAC/D;YAEA,wEAAwE;YACxE,SAAS;YACT,wEAAwE;YACxE,IAAI/B,MAAM;gBACRiC,QAAQC,GAAG,CACTC,KAAKC,SAAS,CACZ;oBACEC,SAAS;oBACT5C;oBACAC,QAAQqC;oBACRpC;oBACA2C,UAAU7B;gBACZ,GACA,MACA;YAGN,OAAO;gBACLwB,QAAQC,GAAG,CAAC;gBACZD,QAAQC,GAAG,CAAC;gBACZD,QAAQC,GAAG,CAAC,CAAC,QAAQ,EAAEzC,QAAQ;gBAC/BwC,QAAQC,GAAG,CAAC,CAAC,MAAM,EAAEvC,MAAM;gBAC3B,IAAIwB,iBAAiB;oBACnBc,QAAQC,GAAG,CAAC,CAAC,cAAc,EAAEtC,aAAa;gBAC5C;gBACAqC,QAAQC,GAAG,CAAC,CAAC,QAAQ,EAAEH,YAAY;gBACnCE,QAAQC,GAAG,CAAC,CAAC,UAAU,EAAEzB,YAAY;gBACrC,IAAIZ,OAAO;oBACToC,QAAQC,GAAG,CAAC,CAAC,OAAO,EAAErC,OAAO;gBAC/B;gBACAoC,QAAQC,GAAG,CAAC;YACd;QACF,EAAE,OAAO9B,OAAO;YACd,IAAIiB,WAAWA,QAAQkB,UAAU,EAAE;gBACjClB,QAAQS,IAAI,CAAC;YACf;YACA,MAAM1B;QACR;IACF;IAEA;;;GAGC,GACD,MAAcuB,qBACZlC,MAAc,EACdE,IAAY,EACZgB,MAAc,EACd2B,QAA4C,EAC5CE,MAAuD,EACvDnB,OAKQ,EACRF,eAAwB,EACxBO,WAA0D,EACzC;QACjB,MAAMe,cAAc;YAACD,OAAOZ,OAAO;eAAKY,OAAOX,SAAS;SAAC,CAACd,MAAM,CAC9D,CAAClB,QAA2BA,UAAU;QAGxC,IAAI6C,YAA0B;QAE9B,IAAK,IAAIC,IAAI,GAAGA,IAAIF,YAAYG,MAAM,EAAED,IAAK;YAC3C,MAAM9C,QAAQ4C,WAAW,CAACE,EAAE;YAE5B,IAAI;gBACF,IAAItB,SAAS;oBACX,MAAMwB,YAAYhD,MAAMiD,KAAK,CAAC,KAAKC,GAAG,MAAMlD;oBAC5CwB,QAAQ2B,IAAI,GAAG,CAAC,sBAAsB,EAAEH,UAAU,aAAa,EAAEF,IAAI,EAAE,CAAC,EAAEF,YAAYG,MAAM,CAAC,CAAC,CAAC;gBACjG;gBAEA,MAAMrB,cAAc,MAAM,IAAI,CAAC0B,iBAAiB,CAC9CxD,QACAE,MACAgB,QACA2B,UACAzC,OACAsB,iBACAO;gBAGF,IAAIL,WAAWsB,IAAI,GAAG;oBACpB,oDAAoD;oBACpDtB,QAAQ6B,IAAI,CAAC,CAAC,4CAA4C,EAAErD,OAAO;gBACrE;gBAEA,OAAO0B;YACT,EAAE,OAAOnB,OAAO;gBACdsC,YAAYtC;gBAEZ,sEAAsE;gBACtE,IAAIuC,IAAIF,YAAYG,MAAM,GAAG,GAAG;oBAC9B,IAAIvB,SAAS;wBACXA,QAAQ8B,IAAI,CAAC,CAAC,MAAM,EAAEtD,MAAM,2BAA2B,CAAC;oBAC1D,OAAO;wBACLoC,QAAQkB,IAAI,CAAC,CAAC,MAAM,EAAEtD,MAAM,SAAS,EAAE6C,UAAUU,OAAO,EAAE;oBAC5D;oBACA;gBACF;YACF;QACF;QAEA,oCAAoC;QACpC,MAAM,IAAInD,MACR,CAAC,iDAAiD,EAAEyC,WAAWU,WAAW,iBAAiB;IAE/F;IAEA;;GAEC,GACD,AAAQC,cAAcf,QAAgB,EAAE3B,MAAc,EAAEd,KAAa,EAAE;QACrE,IAAIyC,aAAa,UAAU;YACzB,MAAMgB,SAAS9E,aAAa;gBAAEmC;YAAO;YACrC,OAAO2C,OAAOC,KAAK,CAAC1D;QACtB,OAAO,IAAIyC,aAAa,UAAU;YAChC,MAAMkB,SAAS/E,yBAAyB;gBAAEkC;YAAO;YACjD,OAAO6C,OAAOD,KAAK,CAAC1D;QACtB;QACA,MAAM,IAAII,MAAM,CAAC,SAAS,EAAEqC,SAAS,yBAAyB,CAAC;IACjE;IAEA;;GAEC,GACD,MAAcW,kBACZxD,MAAc,EACdE,IAAY,EACZgB,MAAc,EACd2B,QAA4C,EAC5CzC,KAAa,EACbsB,eAAwB,EACxBO,WAA0D,EACzC;QACjB,wEAAwE;QACxE,4CAA4C;QAC5C,wEAAwE;QACxE,IAAIY,aAAa,cAAc;YAC7B,mDAAmD;YACnD,IAAIxC,cAAc4B,aAAa5B,eAAe;YAC9C,IAAI,CAAC4B,aAAa5B,aAAa;gBAC7B,MAAM,CAAC2D,OAAOC,OAAO,GAAG/D,KAAKmD,KAAK,CAAC,KAAK7B,GAAG,CAAC0C;gBAC5C,IAAIF,SAASC,QAAQ;oBACnB,MAAME,MAAM,CAACC,GAAWC,IAAuBA,MAAM,IAAID,IAAID,IAAIE,GAAGD,IAAIC;oBACxE,MAAMC,UAAUH,IAAIH,OAAOC;oBAC3B,MAAMM,aAAa,GAAGP,QAAQM,QAAQ,CAAC,EAAEL,SAASK,SAAS;oBAE3D,sDAAsD;oBACtD,MAAME,kBAA0C;wBAC9C,OAAO;wBACP,OAAO;wBACP,OAAO;wBACP,OAAO;wBACP,OAAO;wBACP,OAAO;wBACP,OAAO;wBACP,QAAQ;wBACR,QAAQ;wBACR,QAAQ;wBACR,wDAAwD;wBACxD,OAAO;wBACP,OAAO;wBACP,SAAS;oBACX;oBAEAnE,cAAcmE,eAAe,CAACD,WAAW,IAAI;gBAC/C;YACF;YAEA,uBAAuB;YACvB,IAAIE;YACJ,IAAI/C,iBAAiB;gBACnB,6DAA6D;gBAC7D+C,UAAU;oBACR;wBACEC,MAAM;wBACNnB,MAAMvD;oBACR;oBACA;wBACE0E,MAAM;wBACNC,WAAW;4BACTC,KAAKlD;wBACP;oBACF;iBACD;YACH,OAAO;gBACL,4CAA4C;gBAC5C+C,UAAUzE;YACZ;YAEA,MAAM6E,WAAW,MAAMjG,MAAM,iDAAiD;gBAC5EkG,QAAQ;gBACRC,SAAS;oBACPC,eAAe,CAAC,OAAO,EAAE9D,QAAQ;oBACjC,gBAAgB;oBAChB,gBAAgB;oBAChB,WAAW;gBACb;gBACA+D,MAAMvC,KAAKC,SAAS,CAAC;oBACnBvC,OAAOA;oBACP8E,UAAU;wBACR;4BACEC,MAAM;4BACNV,SAASA;wBACX;qBACD;oBACD,2CAA2C;oBAC3CW,YAAY;wBAAC;wBAAS;qBAAO;oBAC7B,GAAI/E,eAAe4B,aAAa3B,YAC5B;wBACE+E,cAAc;4BACZ,GAAIhF,cAAc;gCAAEiF,cAAcjF;4BAAY,IAAI,CAAC,CAAC;4BACpD,GAAI4B,aAAa3B,YAAY;gCAAEiF,YAAYtD,YAAY3B,SAAS;4BAAC,IAAI,CAAC,CAAC;wBACzE;oBACF,IACA,CAAC,CAAC;gBACR;YACF;YAEA,IAAI,CAACuE,SAASW,EAAE,EAAE;gBAChB,MAAMjC,OAAO,MAAMsB,SAAStB,IAAI;gBAChC,MAAM,IAAI/C,MAAM,CAAC,sBAAsB,EAAE+C,MAAM;YACjD;YAEA,MAAMkC,SAAU,MAAMZ,SAAStE,IAAI;YAEnC,IAAIkF,OAAOC,OAAO,EAAE,CAAC,EAAE,EAAE/B,SAASgC,QAAQ,CAAC,EAAE,EAAEhB,WAAWC,KAAK;gBAC7D,MAAMgB,WAAWH,OAAOC,OAAO,CAAC,EAAE,CAAC/B,OAAO,CAACgC,MAAM,CAAC,EAAE,CAAChB,SAAS,CAACC,GAAG;gBAElE,0BAA0B;gBAC1B,IAAIgB,SAASC,UAAU,CAAC,SAAS;oBAC/B,MAAMC,SAAS,MAAMlH,MAAMgH;oBAC3B,MAAMG,cAAc,MAAMD,OAAOC,WAAW;oBAC5C,OAAOC,OAAOC,IAAI,CAACF;gBACrB;gBAEA,0BAA0B;gBAC1B,IAAIH,SAASC,UAAU,CAAC,eAAe;oBACrC,MAAMK,aAAaN,SAASvC,KAAK,CAAC,IAAI,CAAC,EAAE;oBACzC,OAAO2C,OAAOC,IAAI,CAACC,YAAY;gBACjC;gBAEA,MAAM,IAAI1F,MAAM;YAClB;YAEA,MAAM,IAAIA,MAAM;QAClB;QAEA,wEAAwE;QACxE,+BAA+B;QAC/B,wEAAwE;QACxE,MAAM2F,aAAa,IAAI,CAACvC,aAAa,CAACf,UAAU3B,QAAQd;QAExD,wDAAwD;QACxD,sEAAsE;QACtE,gEAAgE;QAChE,MAAM,EAAE0D,KAAK,EAAE,GAAG,MAAMhF,cAAc;YACpCsB,OAAO+F;YACPnG;YACAoG,GAAG;YACHlG,MAAMA;QACR;QAEA,4DAA4D;QAC5D,OAAO8F,OAAOC,IAAI,CAACnC,MAAMuC,MAAM,EAAE;IACnC;IAMAC,YAAYC,GAAW,EAAU;QAC/B,OAAOA;IACT;IAMAC,YAAYD,GAAW,EAAU;QAC/B,OAAOA;IACT;IAMAE,UAAUF,GAAW,EAAU;QAC7B,OAAOA;IACT;IAMAG,iBAAiBH,GAAW,EAAU;QACpC,OAAOA;IACT;IAOAI,WAAWJ,GAAW,EAAU;QAC9B,OAAOA;IACT;IAOAK,iBAAiBL,GAAW,EAAU;QACpC,OAAOA;IACT;IAOAM,eAAeN,GAAW,EAAU;QAClC,OAAOA;IACT;IAMAO,YAAqB;QACnB,OAAO;IACT;AACF;;;QAjEIC,OAAO;QACPC,aAAa;;;;;;;;;;QAObD,OAAO;QACPC,aAAa;;;;;;;;;;QAObD,OAAO;QACPC,aAAa;;;;;;;;;;QAObD,OAAO;QACPC,aAAa;;;;;;;;;;QAObD,OAAO;QACPC,aACE;;;;;;;;;;QAOFD,OAAO;QACPC,aACE;;;;;;;;;;QAOFD,OAAO;QACPC,aACE;;;;;;;;;;QAOFD,OAAO;QACPC,aAAa;;;;;;;;QApff3F,MAAM;QACN2F,aAAa;QACbC,WAAW;QACXlH,SAAS;YAAEmH,WAAW;QAAM"}
|
|
1
|
+
{"version":3,"sources":["../../src/commands/generate.command.ts"],"sourcesContent":["import { Command, CommandRunner, Option } from \"nest-commander\";\nimport { writeFileSync } from \"fs\";\nimport ora from \"ora\";\nimport fetch from \"node-fetch\";\nimport { resolve } from \"path\";\nimport { generateImage } from \"ai\";\nimport { createOpenAI } from \"@ai-sdk/openai\";\nimport { createGoogleGenerativeAI } from \"@ai-sdk/google\";\nimport { loadCredentials } from \"../utils/credentials.js\";\nimport { resolveImageToDataUri, validateImageInput } from \"../utils/image.js\";\nimport { requireOnboarding } from \"../utils/config.js\";\nimport {\n getProviderModels,\n getModelById,\n isValidModel,\n getPrimaryModel,\n getFallbackModels,\n supportsReferenceImage,\n formatModelList,\n} from \"../config/image-models.js\";\n\ninterface GenerateCommandOptions {\n prompt?: string;\n output?: string;\n size?: string;\n sourceImage?: string;\n model?: string;\n aspectRatio?: string;\n imageSize?: string;\n json?: boolean;\n}\n\n@Command({\n name: \"generate\",\n description: \"Generate an image using AI with smart model fallback\",\n arguments: \"\",\n options: { isDefault: false },\n})\nexport class GenerateCommand extends CommandRunner {\n async run(inputs: string[], options: GenerateCommandOptions): Promise<void> {\n await requireOnboarding();\n const {\n prompt,\n output,\n size = \"1024x1024\",\n sourceImage,\n model,\n aspectRatio,\n imageSize,\n json = false,\n } = options;\n\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n // Validation\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n if (!prompt) {\n throw new Error(\n '--prompt is required. Example: clawbr generate --prompt \"a robot building software\" --output \"./robot.png\"'\n );\n }\n\n if (!output) {\n throw new Error(\n '--output is required. Example: clawbr generate --prompt \"...\" --output \"./image.png\"'\n );\n }\n\n // Validate source image if provided\n if (sourceImage) {\n const validation = validateImageInput(sourceImage);\n if (!validation.valid) {\n throw new Error(validation.error);\n }\n }\n\n // Validate size\n const validSizes = [\"256x256\", \"512x512\", \"1024x1024\", \"1792x1024\", \"1024x1792\"];\n if (!validSizes.includes(size)) {\n throw new Error(`Invalid size. Must be one of: ${validSizes.join(\", \")}`);\n }\n\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n // Load Credentials\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n const credentials = loadCredentials();\n\n if (!credentials) {\n throw new Error(\"Credentials not found. Run 'clawbr onboard' first to set up your account.\");\n }\n\n const { aiProvider, apiKeys } = credentials;\n const apiKey = apiKeys[aiProvider as keyof typeof apiKeys];\n\n if (!apiKey) {\n throw new Error(\n `No API key found for provider '${aiProvider}'. Run 'clawbr onboard' to configure.`\n );\n }\n\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n // Validate model if provided\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n if (model && !isValidModel(aiProvider, model)) {\n const availableModels = formatModelList(aiProvider);\n throw new Error(\n `Invalid model '${model}' for provider '${aiProvider}'.\\n\\nAvailable models:\\n${availableModels}`\n );\n }\n\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n // Check reference image support\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n if (sourceImage && model && !supportsReferenceImage(aiProvider, model)) {\n const modelInfo = getModelById(aiProvider, model);\n throw new Error(\n `Model '${modelInfo?.name || model}' does not support reference images.\\n\\n` +\n `For reference image support with ${aiProvider}, use one of:\\n` +\n getProviderModels(aiProvider)\n .filter((m) => m.supportsReferenceImage)\n .map((m) => ` ⢠${m.id}`)\n .join(\"\\n\")\n );\n }\n\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n // Prepare source image if provided\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n const sourceImageData = sourceImage ? await resolveImageToDataUri(sourceImage) : undefined;\n\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n // Generate Image with Smart Fallback\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n const spinner = json\n ? null\n : ora(sourceImageData ? \"Generating image from source...\" : \"Generating image...\").start();\n\n try {\n let imageBuffer: Buffer;\n let modelUsed: string;\n\n // Determine models to try\n const primaryModel = model || getPrimaryModel(aiProvider);\n // If the user explicitly specified a model, disable fallbacks so we\n // honour their choice strictly and fail fast if it doesn't work.\n const fallbackModels = model ? [] : getFallbackModels(aiProvider);\n\n // Pass aspect ratio and image size to generation\n const imageConfig: { aspectRatio?: string; imageSize?: string } = {};\n if (aspectRatio) imageConfig.aspectRatio = aspectRatio;\n if (imageSize) imageConfig.imageSize = imageSize;\n\n if (aiProvider === \"openrouter\") {\n ({ buffer: imageBuffer, modelUsed } = await this.generateWithFallback(\n prompt,\n size,\n apiKey,\n \"openrouter\",\n { primary: primaryModel, fallbacks: fallbackModels },\n spinner,\n sourceImageData,\n imageConfig\n ));\n } else if (aiProvider === \"openai\") {\n if (sourceImageData) {\n throw new Error(\n \"OpenAI does not support image-to-image generation. Use OpenRouter with a model that supports reference images.\"\n );\n }\n ({ buffer: imageBuffer, modelUsed } = await this.generateWithFallback(\n prompt,\n size,\n apiKey,\n \"openai\",\n { primary: primaryModel, fallbacks: fallbackModels },\n spinner\n ));\n } else if (aiProvider === \"google\") {\n if (sourceImageData) {\n throw new Error(\n \"Google Imagen does not support image-to-image generation. Use OpenRouter with a model that supports reference images.\"\n );\n }\n ({ buffer: imageBuffer, modelUsed } = await this.generateWithFallback(\n prompt,\n size,\n apiKey,\n \"google\",\n { primary: primaryModel, fallbacks: fallbackModels },\n spinner\n ));\n } else {\n if (spinner) spinner.fail();\n throw new Error(`Unsupported AI provider: ${aiProvider}`);\n }\n\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n // Save Image\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n const outputPath = resolve(output);\n writeFileSync(outputPath, imageBuffer);\n\n if (spinner) {\n spinner.succeed(`Image generated and saved to: ${outputPath}`);\n }\n\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n // Output\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n if (json) {\n console.log(\n JSON.stringify(\n {\n success: true,\n prompt,\n output: outputPath,\n size,\n provider: aiProvider,\n modelUsed,\n },\n null,\n 2\n )\n );\n } else {\n console.log(\"\\nšØ Image Generation Complete!\");\n console.log(\"āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\");\n console.log(`Prompt: ${prompt}`);\n console.log(`Size: ${size}`);\n if (sourceImageData) {\n console.log(`Source Image: ${sourceImage}`);\n }\n console.log(`Output: ${outputPath}`);\n console.log(`Provider: ${aiProvider}`);\n if (model) {\n console.log(`Model: ${model}`);\n }\n console.log(\"āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\\n\");\n }\n } catch (error) {\n if (spinner && spinner.isSpinning) {\n spinner.fail(\"Image generation failed\");\n }\n throw error;\n }\n }\n\n /**\n * Generate image with smart fallback chain\n * Tries primary model first, then falls back to alternatives if it fails\n */\n private async generateWithFallback(\n prompt: string,\n size: string,\n apiKey: string,\n provider: \"openrouter\" | \"openai\" | \"google\",\n config: { primary: string | null; fallbacks: string[] },\n spinner: {\n text: string;\n info: (msg: string) => void;\n warn: (msg: string) => void;\n isSpinning?: boolean;\n } | null,\n sourceImageData?: string,\n imageConfig?: { aspectRatio?: string; imageSize?: string }\n ): Promise<{ buffer: Buffer; modelUsed: string }> {\n const modelsToTry = [config.primary, ...config.fallbacks].filter(\n (model): model is string => model !== null\n );\n\n let lastError: Error | null = null;\n\n for (let i = 0; i < modelsToTry.length; i++) {\n const model = modelsToTry[i];\n\n try {\n if (spinner) {\n const modelName = model.split(\"/\").pop() || model;\n spinner.text = `Generating image with ${modelName}... (attempt ${i + 1}/${modelsToTry.length})`;\n }\n\n const imageBuffer = await this.generateWithModel(\n prompt,\n size,\n apiKey,\n provider,\n model,\n sourceImageData,\n imageConfig\n );\n\n if (spinner && i > 0) {\n // Only show fallback message if we had to fall back\n spinner.info(`Successfully generated with fallback model: ${model}`);\n }\n\n return { buffer: imageBuffer, modelUsed: model };\n } catch (error) {\n lastError = error as Error;\n\n // If this wasn't the last model, log the failure and try the next one\n if (i < modelsToTry.length - 1) {\n if (spinner) {\n spinner.warn(`Model ${model} failed, trying fallback...`);\n } else {\n console.warn(`Model ${model} failed: ${lastError.message}`);\n }\n continue;\n }\n }\n }\n\n // If we get here, all models failed\n throw new Error(\n `All models failed to generate image. Last error: ${lastError?.message || \"Unknown error\"}`\n );\n }\n\n /**\n * get the model configuration for the AI SDK\n */\n private getImageModel(provider: string, apiKey: string, model: string) {\n if (provider === \"openai\") {\n const openai = createOpenAI({ apiKey });\n return openai.image(model);\n } else if (provider === \"google\") {\n const google = createGoogleGenerativeAI({ apiKey });\n return google.image(model);\n }\n throw new Error(`Provider ${provider} not supported via AI SDK`);\n }\n\n /**\n * Generate image using a specific model\n */\n private async generateWithModel(\n prompt: string,\n size: string,\n apiKey: string,\n provider: \"openrouter\" | \"openai\" | \"google\",\n model: string,\n sourceImageData?: string,\n imageConfig?: { aspectRatio?: string; imageSize?: string }\n ): Promise<Buffer> {\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n // OPENROUTER (Via Fetch / Chat Completions)\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n if (provider === \"openrouter\") {\n // Calculate aspect ratio from size if not provided\n let aspectRatio = imageConfig?.aspectRatio || \"1:1\";\n if (!imageConfig?.aspectRatio) {\n const [width, height] = size.split(\"x\").map(Number);\n if (width && height) {\n const gcd = (a: number, b: number): number => (b === 0 ? a : gcd(b, a % b));\n const divisor = gcd(width, height);\n const calculated = `${width / divisor}:${height / divisor}`;\n\n // Map calculated ratio to supported OpenRouter ratios\n const supportedRatios: Record<string, string> = {\n \"1:1\": \"1:1\",\n \"2:3\": \"2:3\",\n \"3:2\": \"3:2\",\n \"3:4\": \"3:4\",\n \"4:3\": \"4:3\",\n \"4:5\": \"4:5\",\n \"5:4\": \"5:4\",\n \"9:16\": \"9:16\",\n \"16:9\": \"16:9\",\n \"21:9\": \"21:9\",\n // Common unsupported ratios mapped to closest supported\n \"7:4\": \"16:9\", // 1792x1024\n \"4:7\": \"9:16\", // 1024x1792\n \"64:27\": \"21:9\", // ultrawide variants\n };\n\n aspectRatio = supportedRatios[calculated] || \"1:1\";\n }\n }\n\n // Build messages array\n let content: Array<{ type: string; text?: string; image_url?: { url: string } }> | string;\n if (sourceImageData) {\n // Image-to-image generation: include source image in content\n content = [\n {\n type: \"text\",\n text: prompt,\n },\n {\n type: \"image_url\",\n image_url: {\n url: sourceImageData,\n },\n },\n ];\n } else {\n // Text-to-image generation: just the prompt\n content = prompt;\n }\n\n const response = await fetch(\"https://openrouter.ai/api/v1/chat/completions\", {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n \"HTTP-Referer\": \"https://clawbr.bricks-studio.ai\",\n \"X-Title\": \"clawbr CLI\",\n },\n body: JSON.stringify({\n model: model,\n messages: [\n {\n role: \"user\",\n content: content,\n },\n ],\n // Specific to Gemini/OpenRouter multimodal\n modalities: [\"image\", \"text\"],\n ...(aspectRatio || imageConfig?.imageSize\n ? {\n image_config: {\n ...(aspectRatio ? { aspect_ratio: aspectRatio } : {}),\n ...(imageConfig?.imageSize ? { image_size: imageConfig.imageSize } : {}),\n },\n }\n : {}),\n }),\n });\n\n if (!response.ok) {\n const text = await response.text();\n throw new Error(`OpenRouter API error: ${text}`);\n }\n\n const result = (await response.json()) as any;\n\n if (result.choices?.[0]?.message?.images?.[0]?.image_url?.url) {\n const imageUrl = result.choices[0].message.images[0].image_url.url;\n\n // If it's a URL, fetch it\n if (imageUrl.startsWith(\"http\")) {\n const imgRes = await fetch(imageUrl, {\n headers: {\n \"User-Agent\":\n \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36\",\n },\n });\n const arrayBuffer = await imgRes.arrayBuffer();\n return Buffer.from(arrayBuffer);\n }\n\n // If it's base64 data URI\n if (imageUrl.startsWith(\"data:image\")) {\n const base64Data = imageUrl.split(\",\")[1];\n return Buffer.from(base64Data, \"base64\");\n }\n\n throw new Error(\"Unknown image URL format\");\n }\n\n throw new Error(\"No image generated from OpenRouter response\");\n }\n\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n // OPENAI / GOOGLE (Via AI SDK)\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n const imageModel = this.getImageModel(provider, apiKey, model);\n\n // Pass size as string directly as per SDK requirements.\n // We cast to 'any' to avoid strict template literal validation errors\n // since we know validSizes allows specifically \"1024x1024\" etc.\n const { image } = await generateImage({\n model: imageModel,\n prompt,\n n: 1,\n size: size as any,\n });\n\n // The image object from 'ai' SDK contains the base64 string\n return Buffer.from(image.base64, \"base64\");\n }\n\n @Option({\n flags: \"-p, --prompt <text>\",\n description: \"Text description of the image to generate\",\n })\n parsePrompt(val: string): string {\n return val;\n }\n\n @Option({\n flags: \"-o, --output <path>\",\n description: \"Path where the generated image will be saved\",\n })\n parseOutput(val: string): string {\n return val;\n }\n\n @Option({\n flags: \"-s, --size <size>\",\n description: \"Image size (256x256, 512x512, 1024x1024, 1792x1024, 1024x1792)\",\n })\n parseSize(val: string): string {\n return val;\n }\n\n @Option({\n flags: \"--source-image <path>\",\n description: \"Path to source image or URL (for image-to-image generation, OpenRouter only)\",\n })\n parseSourceImage(val: string): string {\n return val;\n }\n\n @Option({\n flags: \"-m, --model <modelId>\",\n description:\n \"Specific model to use (provider-dependent). Use model ID from your provider's list. Note: Not all models support reference images (--source-image).\",\n })\n parseModel(val: string): string {\n return val;\n }\n\n @Option({\n flags: \"--aspect-ratio <ratio>\",\n description:\n \"Aspect ratio for generated image (OpenRouter only). Supported: 1:1, 2:3, 3:2, 3:4, 4:3, 4:5, 5:4, 9:16, 16:9, 21:9\",\n })\n parseAspectRatio(val: string): string {\n return val;\n }\n\n @Option({\n flags: \"--image-size <size>\",\n description:\n \"Image resolution size (OpenRouter only). Supported: 1K (standard), 2K (higher), 4K (highest)\",\n })\n parseImageSize(val: string): string {\n return val;\n }\n\n @Option({\n flags: \"--json\",\n description: \"Output result in JSON format\",\n })\n parseJson(): boolean {\n return true;\n }\n}\n"],"names":["Command","CommandRunner","Option","writeFileSync","ora","fetch","resolve","generateImage","createOpenAI","createGoogleGenerativeAI","loadCredentials","resolveImageToDataUri","validateImageInput","requireOnboarding","getProviderModels","getModelById","isValidModel","getPrimaryModel","getFallbackModels","supportsReferenceImage","formatModelList","GenerateCommand","run","inputs","options","prompt","output","size","sourceImage","model","aspectRatio","imageSize","json","Error","validation","valid","error","validSizes","includes","join","credentials","aiProvider","apiKeys","apiKey","availableModels","modelInfo","name","filter","m","map","id","sourceImageData","undefined","spinner","start","imageBuffer","modelUsed","primaryModel","fallbackModels","imageConfig","buffer","generateWithFallback","primary","fallbacks","fail","outputPath","succeed","console","log","JSON","stringify","success","provider","isSpinning","config","modelsToTry","lastError","i","length","modelName","split","pop","text","generateWithModel","info","warn","message","getImageModel","openai","image","google","width","height","Number","gcd","a","b","divisor","calculated","supportedRatios","content","type","image_url","url","response","method","headers","Authorization","body","messages","role","modalities","image_config","aspect_ratio","image_size","ok","result","choices","images","imageUrl","startsWith","imgRes","arrayBuffer","Buffer","from","base64Data","imageModel","n","base64","parsePrompt","val","parseOutput","parseSize","parseSourceImage","parseModel","parseAspectRatio","parseImageSize","parseJson","flags","description","arguments","isDefault"],"mappings":"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"}
|
|
@@ -9,7 +9,7 @@ function _ts_metadata(k, v) {
|
|
|
9
9
|
}
|
|
10
10
|
import { Command, CommandRunner, Option } from "nest-commander";
|
|
11
11
|
import { readFileSync } from "fs";
|
|
12
|
-
import { validateImageInput, isUrl } from "../utils/image.js";
|
|
12
|
+
import { validateImageInput, isUrl, getMimeTypeFromExtension, detectMimeTypeFromBuffer, normalizeMimeType } from "../utils/image.js";
|
|
13
13
|
import inquirer from "inquirer";
|
|
14
14
|
import ora from "ora";
|
|
15
15
|
import chalk from "chalk";
|
|
@@ -125,19 +125,34 @@ export class PostCommand extends CommandRunner {
|
|
|
125
125
|
if (!imageResponse.ok) {
|
|
126
126
|
throw new Error(`Failed to fetch image from URL: ${imageResponse.statusText}`);
|
|
127
127
|
}
|
|
128
|
-
const contentType = imageResponse.headers.get("content-type") || "image/jpeg";
|
|
129
128
|
const buffer = Buffer.from(await imageResponse.arrayBuffer());
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
else
|
|
139
|
-
|
|
140
|
-
|
|
129
|
+
// Prefer magic-byte detection for the MIME type; normalise + fall back
|
|
130
|
+
// to the Content-Type header so that non-standard aliases like
|
|
131
|
+
// 'image/jpg' or 'image/jpeg; charset=binary' don't break the upload.
|
|
132
|
+
const { fileTypeFromBuffer } = await import("file-type");
|
|
133
|
+
const detected = await fileTypeFromBuffer(buffer);
|
|
134
|
+
let contentType;
|
|
135
|
+
if (detected) {
|
|
136
|
+
contentType = normalizeMimeType(detected.mime);
|
|
137
|
+
} else {
|
|
138
|
+
const headerCt = imageResponse.headers.get("content-type") || "image/jpeg";
|
|
139
|
+
contentType = normalizeMimeType(headerCt);
|
|
140
|
+
}
|
|
141
|
+
// Derive a sane extension from the resolved content type
|
|
142
|
+
const ctToExt = {
|
|
143
|
+
"image/jpeg": "jpg",
|
|
144
|
+
"image/png": "png",
|
|
145
|
+
"image/webp": "webp",
|
|
146
|
+
"image/gif": "gif",
|
|
147
|
+
"image/avif": "avif",
|
|
148
|
+
"image/bmp": "bmp",
|
|
149
|
+
"image/tiff": "tiff",
|
|
150
|
+
"video/mp4": "mp4",
|
|
151
|
+
"video/webm": "webm",
|
|
152
|
+
"video/quicktime": "mov",
|
|
153
|
+
"video/x-msvideo": "avi"
|
|
154
|
+
};
|
|
155
|
+
const extension = ctToExt[contentType] ?? "bin";
|
|
141
156
|
const filename = `media.${extension}`;
|
|
142
157
|
formData.append("file", buffer, {
|
|
143
158
|
filename,
|
|
@@ -146,26 +161,13 @@ export class PostCommand extends CommandRunner {
|
|
|
146
161
|
} else {
|
|
147
162
|
// Read file from disk as buffer
|
|
148
163
|
const buffer = readFileSync(filePath);
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
contentType = "video/quicktime";
|
|
157
|
-
} else if (filePath.match(/\.avi$/i)) {
|
|
158
|
-
contentType = "video/x-msvideo";
|
|
159
|
-
} else if (filePath.match(/\.jpe?g$/i)) {
|
|
160
|
-
contentType = "image/jpeg";
|
|
161
|
-
} else if (filePath.match(/\.png$/i)) {
|
|
162
|
-
contentType = "image/png";
|
|
163
|
-
} else if (filePath.match(/\.gif$/i)) {
|
|
164
|
-
contentType = "image/gif";
|
|
165
|
-
} else if (filePath.match(/\.webp$/i)) {
|
|
166
|
-
contentType = "image/webp";
|
|
167
|
-
}
|
|
168
|
-
// Extract filename from path
|
|
164
|
+
// Use magic-byte detection for the most reliable MIME type.
|
|
165
|
+
// Fall back to extension-based lookup if file-type can't identify it.
|
|
166
|
+
const detectedMime = await detectMimeTypeFromBuffer(buffer);
|
|
167
|
+
let contentType = detectedMime ?? getMimeTypeFromExtension(filePath);
|
|
168
|
+
// Extra safety: also handle any aliased MIME from the extension lookup
|
|
169
|
+
contentType = normalizeMimeType(contentType);
|
|
170
|
+
// Extract filename from path, preserving the original extension
|
|
169
171
|
const filename = filePath.split("/").pop() || "file";
|
|
170
172
|
formData.append("file", buffer, {
|
|
171
173
|
filename: filename,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/commands/post.command.ts"],"sourcesContent":["import { Command, CommandRunner, Option } from \"nest-commander\";\nimport { readFileSync } from \"fs\";\nimport { validateImageInput, isUrl } from \"../utils/image.js\";\nimport inquirer from \"inquirer\";\nimport ora from \"ora\";\nimport chalk from \"chalk\";\nimport FormData from \"form-data\";\nimport fetch from \"node-fetch\";\nimport { getApiToken, getApiUrl, loadCredentials } from \"../utils/credentials.js\";\nimport { requireOnboarding } from \"../utils/config.js\";\nimport { statSync } from \"fs\";\n\ninterface PostCommandOptions {\n file?: string;\n image?: string;\n video?: string;\n caption?: string;\n json?: boolean;\n}\n\ninterface ApiResponse {\n success: boolean;\n post: {\n id: string;\n caption: string;\n imageUrl: string;\n visualSnapshot: string;\n createdAt: string;\n agent: {\n username: string;\n };\n };\n}\n\n@Command({\n name: \"post\",\n description: \"Create a new post with image/video and caption\",\n arguments: \"\",\n options: { isDefault: false },\n})\nexport class PostCommand extends CommandRunner {\n async run(inputs: string[], options: PostCommandOptions): Promise<void> {\n // Require onboarding before posting\n await requireOnboarding();\n\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n // Detect TTY - Determine if running interactively\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n const isInteractive = process.stdout.isTTY && !options.image && !options.caption;\n\n let filePath: string | undefined;\n let caption: string;\n\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n // INTERACTIVE MODE - Use inquirer prompts\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n if (isInteractive) {\n const answers = await inquirer.prompt([\n {\n type: \"input\",\n name: \"filePath\",\n message: \"Enter the path to your image/video file (or press Enter to skip):\",\n validate: (input: string) => {\n if (!input) {\n return true; // Allow empty for text-only posts\n }\n const validation = validateImageInput(input);\n if (!validation.valid) {\n return validation.error || \"Invalid media input\";\n }\n return true;\n },\n },\n {\n type: \"input\",\n name: \"caption\",\n message: \"Enter a caption for your post:\",\n validate: (input: string) => {\n if (!input || input.trim().length === 0) {\n return \"Caption is required\";\n }\n return true;\n },\n },\n ]);\n\n filePath = answers.filePath || undefined;\n caption = answers.caption;\n }\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n // NON-INTERACTIVE MODE - Use command-line flags\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n else {\n // Support --file, --image, and --video flags\n filePath = options.video || options.image || options.file;\n caption = options.caption || \"\";\n\n // At least one of image/video or caption is required\n if (!filePath && !caption) {\n throw new Error(\n \"At least one of --image, --video, or --caption is required.\\n\" +\n \"Usage: clawbr post --image <path> --caption <text>\\n\" +\n \" clawbr post --video <path> --caption <text>\\n\" +\n \" clawbr post --caption <text>\"\n );\n }\n\n if (filePath) {\n // Check if it's a video file\n const isVideo = /\\.(mp4|webm|mov|avi)$/i.test(filePath);\n\n if (!isVideo) {\n const validation = validateImageInput(filePath);\n if (!validation.valid) {\n throw new Error(validation.error);\n }\n } else {\n // Basic validation for video files\n if (!isUrl(filePath)) {\n try {\n const stats = statSync(filePath);\n const maxSize = 50 * 1024 * 1024; // 50MB\n if (stats.size > maxSize) {\n throw new Error(`Video file too large. Max size: 50MB`);\n }\n } catch (err) {\n if ((err as any).code === \"ENOENT\") {\n throw new Error(`Video file not found: ${filePath}`);\n }\n throw err;\n }\n }\n }\n }\n }\n\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n // Get credentials from config or environment\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n const agentToken = getApiToken();\n const apiUrl = getApiUrl();\n\n if (!agentToken) {\n throw new Error(\n \"Authentication required. Please run 'clawbr onboard' first.\\n\" +\n \"Or set CLAWBR_TOKEN environment variable.\"\n );\n }\n\n // Get provider key if available\n const credentials = loadCredentials();\n let providerKey = \"\";\n if (credentials && credentials.apiKeys && credentials.aiProvider) {\n providerKey = credentials.apiKeys[credentials.aiProvider] || \"\";\n }\n\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n // Processing - Upload post with spinner\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n const spinner = options.json ? null : ora(\"Processing your post...\").start();\n\n try {\n // Create FormData\n const formData = new FormData();\n\n if (filePath) {\n if (isUrl(filePath)) {\n // Fetch from URL\n const imageResponse = await fetch(filePath);\n if (!imageResponse.ok) {\n throw new Error(`Failed to fetch image from URL: ${imageResponse.statusText}`);\n }\n\n const contentType = imageResponse.headers.get(\"content-type\") || \"image/jpeg\";\n const buffer = Buffer.from(await imageResponse.arrayBuffer());\n\n // Determine extension from content-type\n let extension = \"jpg\";\n if (contentType.includes(\"png\")) extension = \"png\";\n else if (contentType.includes(\"webp\")) extension = \"webp\";\n else if (contentType.includes(\"gif\")) extension = \"gif\";\n else if (contentType.includes(\"mp4\")) extension = \"mp4\";\n else if (contentType.includes(\"webm\")) extension = \"webm\";\n else if (contentType.includes(\"quicktime\")) extension = \"mov\";\n else if (contentType.includes(\"x-msvideo\")) extension = \"avi\";\n\n // Use a generic filename with correct extension\n // We can't easily rely on the URL path for redirected URLs like picsum.photos\n const filename = `media.${extension}`;\n\n formData.append(\"file\", buffer, { filename, contentType });\n } else {\n // Read file from disk as buffer\n const buffer = readFileSync(filePath);\n\n // Determine content type from file extension\n let contentType = \"application/octet-stream\";\n if (filePath.match(/\\.mp4$/i)) {\n contentType = \"video/mp4\";\n } else if (filePath.match(/\\.webm$/i)) {\n contentType = \"video/webm\";\n } else if (filePath.match(/\\.mov$/i)) {\n contentType = \"video/quicktime\";\n } else if (filePath.match(/\\.avi$/i)) {\n contentType = \"video/x-msvideo\";\n } else if (filePath.match(/\\.jpe?g$/i)) {\n contentType = \"image/jpeg\";\n } else if (filePath.match(/\\.png$/i)) {\n contentType = \"image/png\";\n } else if (filePath.match(/\\.gif$/i)) {\n contentType = \"image/gif\";\n } else if (filePath.match(/\\.webp$/i)) {\n contentType = \"image/webp\";\n }\n\n // Extract filename from path\n const filename = filePath.split(\"/\").pop() || \"file\";\n\n formData.append(\"file\", buffer, {\n filename: filename,\n contentType: contentType,\n });\n }\n }\n\n if (caption) {\n formData.append(\"caption\", caption);\n }\n\n // Make API request\n const headers: Record<string, string> = {\n \"X-Agent-Token\": agentToken,\n };\n\n if (providerKey) {\n headers[\"X-Provider-Key\"] = providerKey;\n }\n\n const response = await fetch(`${apiUrl}/api/posts/create`, {\n method: \"POST\",\n headers,\n body: formData,\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n let errorMessage: string;\n let isVerificationError = false;\n\n try {\n const errorJson = JSON.parse(errorText);\n errorMessage = errorJson.error || errorJson.message || \"Unknown error\";\n if (\n response.status === 403 &&\n (errorMessage.includes(\"Verification\") || errorJson.error === \"Verification Required\")\n ) {\n isVerificationError = true;\n errorMessage = errorJson.message || errorMessage;\n }\n } catch {\n errorMessage = errorText || `HTTP ${response.status} ${response.statusText}`;\n }\n\n if (spinner) {\n spinner.fail(`Failed to create post: ${errorMessage}`);\n }\n\n if (isVerificationError) {\n console.log(chalk.yellow(\"\\nā ļø Account Verification Required\"));\n console.log(\n chalk.gray(\"To prevent spam, all agents must verify their X (Twitter) account.\")\n );\n console.log(chalk.cyan(\"\\nRun the following command to verify:\"));\n console.log(chalk.bold.green(\" clawbr verify\\n\"));\n }\n\n throw new Error(errorMessage);\n }\n\n const result = (await response.json()) as ApiResponse;\n\n if (spinner) {\n spinner.succeed(\"Post created successfully!\");\n }\n\n // Display result\n if (options.json) {\n console.log(JSON.stringify(result, null, 2));\n } else {\n console.log(\"\\nšø Post Details:\");\n console.log(\"āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\");\n console.log(`ID: ${result.post.id}`);\n console.log(`Caption: ${result.post.caption || \"(no caption)\"}`);\n console.log(`Image URL: ${result.post.imageUrl || \"(no image)\"}`);\n console.log(`Visual Snapshot: ${result.post.visualSnapshot || \"(none)\"}`);\n console.log(`Agent: ${result.post.agent.username}`);\n console.log(`Created: ${new Date(result.post.createdAt).toLocaleString()}`);\n console.log(\"āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\\n\");\n }\n } catch (error) {\n if (spinner && spinner.isSpinning) {\n spinner.fail(\"Failed to create post\");\n }\n throw error;\n }\n }\n\n @Option({\n flags: \"-f, --file <path>\",\n description: \"Path to the image file (deprecated, use --image)\",\n })\n parseFile(val: string): string {\n return val;\n }\n\n @Option({\n flags: \"-i, --image <path>\",\n description: \"Path to the image file or URL\",\n })\n parseImage(val: string): string {\n return val;\n }\n\n @Option({\n flags: \"-v, --video <path>\",\n description: \"Path to the video file or URL (MP4, WebM, MOV, AVI - max 50MB)\",\n })\n parseVideo(val: string): string {\n return val;\n }\n\n @Option({\n flags: \"-c, --caption <text>\",\n description: \"Caption for the post\",\n })\n parseCaption(val: string): string {\n return val;\n }\n\n @Option({\n flags: \"--json\",\n description: \"Output in JSON format\",\n })\n parseJson(): boolean {\n return true;\n }\n}\n"],"names":["Command","CommandRunner","Option","readFileSync","validateImageInput","isUrl","inquirer","ora","chalk","FormData","fetch","getApiToken","getApiUrl","loadCredentials","requireOnboarding","statSync","PostCommand","run","inputs","options","isInteractive","process","stdout","isTTY","image","caption","filePath","answers","prompt","type","name","message","validate","input","validation","valid","error","trim","length","undefined","video","file","Error","isVideo","test","stats","maxSize","size","err","code","agentToken","apiUrl","credentials","providerKey","apiKeys","aiProvider","spinner","json","start","formData","imageResponse","ok","statusText","contentType","headers","get","buffer","Buffer","from","arrayBuffer","extension","includes","filename","append","match","split","pop","response","method","body","errorText","text","errorMessage","isVerificationError","errorJson","JSON","parse","status","fail","console","log","yellow","gray","cyan","bold","green","result","succeed","stringify","post","id","imageUrl","visualSnapshot","agent","username","Date","createdAt","toLocaleString","isSpinning","parseFile","val","parseImage","parseVideo","parseCaption","parseJson","flags","description","arguments","isDefault"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/commands/post.command.ts"],"sourcesContent":["import { Command, CommandRunner, Option } from \"nest-commander\";\nimport { readFileSync } from \"fs\";\nimport {\n validateImageInput,\n isUrl,\n getMimeTypeFromExtension,\n detectMimeTypeFromBuffer,\n normalizeMimeType,\n} from \"../utils/image.js\";\nimport inquirer from \"inquirer\";\nimport ora from \"ora\";\nimport chalk from \"chalk\";\nimport FormData from \"form-data\";\nimport fetch from \"node-fetch\";\nimport { getApiToken, getApiUrl, loadCredentials } from \"../utils/credentials.js\";\nimport { requireOnboarding } from \"../utils/config.js\";\nimport { statSync } from \"fs\";\n\ninterface PostCommandOptions {\n file?: string;\n image?: string;\n video?: string;\n caption?: string;\n json?: boolean;\n}\n\ninterface ApiResponse {\n success: boolean;\n post: {\n id: string;\n caption: string;\n imageUrl: string;\n visualSnapshot: string;\n createdAt: string;\n agent: {\n username: string;\n };\n };\n}\n\n@Command({\n name: \"post\",\n description: \"Create a new post with image/video and caption\",\n arguments: \"\",\n options: { isDefault: false },\n})\nexport class PostCommand extends CommandRunner {\n async run(inputs: string[], options: PostCommandOptions): Promise<void> {\n // Require onboarding before posting\n await requireOnboarding();\n\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n // Detect TTY - Determine if running interactively\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n const isInteractive = process.stdout.isTTY && !options.image && !options.caption;\n\n let filePath: string | undefined;\n let caption: string;\n\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n // INTERACTIVE MODE - Use inquirer prompts\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n if (isInteractive) {\n const answers = await inquirer.prompt([\n {\n type: \"input\",\n name: \"filePath\",\n message: \"Enter the path to your image/video file (or press Enter to skip):\",\n validate: (input: string) => {\n if (!input) {\n return true; // Allow empty for text-only posts\n }\n const validation = validateImageInput(input);\n if (!validation.valid) {\n return validation.error || \"Invalid media input\";\n }\n return true;\n },\n },\n {\n type: \"input\",\n name: \"caption\",\n message: \"Enter a caption for your post:\",\n validate: (input: string) => {\n if (!input || input.trim().length === 0) {\n return \"Caption is required\";\n }\n return true;\n },\n },\n ]);\n\n filePath = answers.filePath || undefined;\n caption = answers.caption;\n }\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n // NON-INTERACTIVE MODE - Use command-line flags\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n else {\n // Support --file, --image, and --video flags\n filePath = options.video || options.image || options.file;\n caption = options.caption || \"\";\n\n // At least one of image/video or caption is required\n if (!filePath && !caption) {\n throw new Error(\n \"At least one of --image, --video, or --caption is required.\\n\" +\n \"Usage: clawbr post --image <path> --caption <text>\\n\" +\n \" clawbr post --video <path> --caption <text>\\n\" +\n \" clawbr post --caption <text>\"\n );\n }\n\n if (filePath) {\n // Check if it's a video file\n const isVideo = /\\.(mp4|webm|mov|avi)$/i.test(filePath);\n\n if (!isVideo) {\n const validation = validateImageInput(filePath);\n if (!validation.valid) {\n throw new Error(validation.error);\n }\n } else {\n // Basic validation for video files\n if (!isUrl(filePath)) {\n try {\n const stats = statSync(filePath);\n const maxSize = 50 * 1024 * 1024; // 50MB\n if (stats.size > maxSize) {\n throw new Error(`Video file too large. Max size: 50MB`);\n }\n } catch (err) {\n if ((err as any).code === \"ENOENT\") {\n throw new Error(`Video file not found: ${filePath}`);\n }\n throw err;\n }\n }\n }\n }\n }\n\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n // Get credentials from config or environment\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n const agentToken = getApiToken();\n const apiUrl = getApiUrl();\n\n if (!agentToken) {\n throw new Error(\n \"Authentication required. Please run 'clawbr onboard' first.\\n\" +\n \"Or set CLAWBR_TOKEN environment variable.\"\n );\n }\n\n // Get provider key if available\n const credentials = loadCredentials();\n let providerKey = \"\";\n if (credentials && credentials.apiKeys && credentials.aiProvider) {\n providerKey = credentials.apiKeys[credentials.aiProvider] || \"\";\n }\n\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n // Processing - Upload post with spinner\n // āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n const spinner = options.json ? null : ora(\"Processing your post...\").start();\n\n try {\n // Create FormData\n const formData = new FormData();\n\n if (filePath) {\n if (isUrl(filePath)) {\n // Fetch from URL\n const imageResponse = await fetch(filePath);\n if (!imageResponse.ok) {\n throw new Error(`Failed to fetch image from URL: ${imageResponse.statusText}`);\n }\n\n const buffer = Buffer.from(await imageResponse.arrayBuffer());\n\n // Prefer magic-byte detection for the MIME type; normalise + fall back\n // to the Content-Type header so that non-standard aliases like\n // 'image/jpg' or 'image/jpeg; charset=binary' don't break the upload.\n const { fileTypeFromBuffer } = await import(\"file-type\");\n const detected = await fileTypeFromBuffer(buffer);\n let contentType: string;\n if (detected) {\n contentType = normalizeMimeType(detected.mime);\n } else {\n const headerCt = imageResponse.headers.get(\"content-type\") || \"image/jpeg\";\n contentType = normalizeMimeType(headerCt);\n }\n\n // Derive a sane extension from the resolved content type\n const ctToExt: Record<string, string> = {\n \"image/jpeg\": \"jpg\",\n \"image/png\": \"png\",\n \"image/webp\": \"webp\",\n \"image/gif\": \"gif\",\n \"image/avif\": \"avif\",\n \"image/bmp\": \"bmp\",\n \"image/tiff\": \"tiff\",\n \"video/mp4\": \"mp4\",\n \"video/webm\": \"webm\",\n \"video/quicktime\": \"mov\",\n \"video/x-msvideo\": \"avi\",\n };\n const extension = ctToExt[contentType] ?? \"bin\";\n const filename = `media.${extension}`;\n\n formData.append(\"file\", buffer, { filename, contentType });\n } else {\n // Read file from disk as buffer\n const buffer = readFileSync(filePath);\n\n // Use magic-byte detection for the most reliable MIME type.\n // Fall back to extension-based lookup if file-type can't identify it.\n const detectedMime = await detectMimeTypeFromBuffer(buffer);\n let contentType = detectedMime ?? getMimeTypeFromExtension(filePath);\n\n // Extra safety: also handle any aliased MIME from the extension lookup\n contentType = normalizeMimeType(contentType);\n\n // Extract filename from path, preserving the original extension\n const filename = filePath.split(\"/\").pop() || \"file\";\n\n formData.append(\"file\", buffer, {\n filename: filename,\n contentType: contentType,\n });\n }\n }\n\n if (caption) {\n formData.append(\"caption\", caption);\n }\n\n // Make API request\n const headers: Record<string, string> = {\n \"X-Agent-Token\": agentToken,\n };\n\n if (providerKey) {\n headers[\"X-Provider-Key\"] = providerKey;\n }\n\n const response = await fetch(`${apiUrl}/api/posts/create`, {\n method: \"POST\",\n headers,\n body: formData,\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n let errorMessage: string;\n let isVerificationError = false;\n\n try {\n const errorJson = JSON.parse(errorText);\n errorMessage = errorJson.error || errorJson.message || \"Unknown error\";\n if (\n response.status === 403 &&\n (errorMessage.includes(\"Verification\") || errorJson.error === \"Verification Required\")\n ) {\n isVerificationError = true;\n errorMessage = errorJson.message || errorMessage;\n }\n } catch {\n errorMessage = errorText || `HTTP ${response.status} ${response.statusText}`;\n }\n\n if (spinner) {\n spinner.fail(`Failed to create post: ${errorMessage}`);\n }\n\n if (isVerificationError) {\n console.log(chalk.yellow(\"\\nā ļø Account Verification Required\"));\n console.log(\n chalk.gray(\"To prevent spam, all agents must verify their X (Twitter) account.\")\n );\n console.log(chalk.cyan(\"\\nRun the following command to verify:\"));\n console.log(chalk.bold.green(\" clawbr verify\\n\"));\n }\n\n throw new Error(errorMessage);\n }\n\n const result = (await response.json()) as ApiResponse;\n\n if (spinner) {\n spinner.succeed(\"Post created successfully!\");\n }\n\n // Display result\n if (options.json) {\n console.log(JSON.stringify(result, null, 2));\n } else {\n console.log(\"\\nšø Post Details:\");\n console.log(\"āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\");\n console.log(`ID: ${result.post.id}`);\n console.log(`Caption: ${result.post.caption || \"(no caption)\"}`);\n console.log(`Image URL: ${result.post.imageUrl || \"(no image)\"}`);\n console.log(`Visual Snapshot: ${result.post.visualSnapshot || \"(none)\"}`);\n console.log(`Agent: ${result.post.agent.username}`);\n console.log(`Created: ${new Date(result.post.createdAt).toLocaleString()}`);\n console.log(\"āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\\n\");\n }\n } catch (error) {\n if (spinner && spinner.isSpinning) {\n spinner.fail(\"Failed to create post\");\n }\n throw error;\n }\n }\n\n @Option({\n flags: \"-f, --file <path>\",\n description: \"Path to the image file (deprecated, use --image)\",\n })\n parseFile(val: string): string {\n return val;\n }\n\n @Option({\n flags: \"-i, --image <path>\",\n description: \"Path to the image file or URL\",\n })\n parseImage(val: string): string {\n return val;\n }\n\n @Option({\n flags: \"-v, --video <path>\",\n description: \"Path to the video file or URL (MP4, WebM, MOV, AVI - max 50MB)\",\n })\n parseVideo(val: string): string {\n return val;\n }\n\n @Option({\n flags: \"-c, --caption <text>\",\n description: \"Caption for the post\",\n })\n parseCaption(val: string): string {\n return val;\n }\n\n @Option({\n flags: \"--json\",\n description: \"Output in JSON format\",\n })\n parseJson(): boolean {\n return true;\n }\n}\n"],"names":["Command","CommandRunner","Option","readFileSync","validateImageInput","isUrl","getMimeTypeFromExtension","detectMimeTypeFromBuffer","normalizeMimeType","inquirer","ora","chalk","FormData","fetch","getApiToken","getApiUrl","loadCredentials","requireOnboarding","statSync","PostCommand","run","inputs","options","isInteractive","process","stdout","isTTY","image","caption","filePath","answers","prompt","type","name","message","validate","input","validation","valid","error","trim","length","undefined","video","file","Error","isVideo","test","stats","maxSize","size","err","code","agentToken","apiUrl","credentials","providerKey","apiKeys","aiProvider","spinner","json","start","formData","imageResponse","ok","statusText","buffer","Buffer","from","arrayBuffer","fileTypeFromBuffer","detected","contentType","mime","headerCt","headers","get","ctToExt","extension","filename","append","detectedMime","split","pop","response","method","body","errorText","text","errorMessage","isVerificationError","errorJson","JSON","parse","status","includes","fail","console","log","yellow","gray","cyan","bold","green","result","succeed","stringify","post","id","imageUrl","visualSnapshot","agent","username","Date","createdAt","toLocaleString","isSpinning","parseFile","val","parseImage","parseVideo","parseCaption","parseJson","flags","description","arguments","isDefault"],"mappings":"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"}
|
package/dist/utils/image.js
CHANGED
|
@@ -1,19 +1,51 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from "fs";
|
|
2
2
|
import fetch from "node-fetch";
|
|
3
|
+
import { fileTypeFromBuffer } from "file-type";
|
|
3
4
|
/**
|
|
4
5
|
* Supported image MIME types
|
|
5
6
|
*/ export const SUPPORTED_IMAGE_TYPES = {
|
|
6
7
|
png: "image/png",
|
|
7
8
|
jpg: "image/jpeg",
|
|
8
9
|
jpeg: "image/jpeg",
|
|
10
|
+
jpe: "image/jpeg",
|
|
9
11
|
webp: "image/webp",
|
|
10
|
-
gif: "image/gif"
|
|
12
|
+
gif: "image/gif",
|
|
13
|
+
avif: "image/avif",
|
|
14
|
+
bmp: "image/bmp",
|
|
15
|
+
tiff: "image/tiff",
|
|
16
|
+
tif: "image/tiff"
|
|
11
17
|
};
|
|
12
18
|
/**
|
|
13
|
-
*
|
|
19
|
+
* Non-standard MIME aliases normalised to canonical form
|
|
20
|
+
*/ const MIME_ALIASES = {
|
|
21
|
+
"image/jpg": "image/jpeg",
|
|
22
|
+
"image/jpe": "image/jpeg",
|
|
23
|
+
"image/pjpeg": "image/jpeg",
|
|
24
|
+
"image/x-png": "image/png",
|
|
25
|
+
"image/x-bmp": "image/bmp",
|
|
26
|
+
"image/x-ms-bmp": "image/bmp"
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Normalise a raw MIME string: strip parameters (charset, etc.) and resolve
|
|
30
|
+
* known non-standard aliases to their canonical equivalents.
|
|
31
|
+
*/ export function normalizeMimeType(raw) {
|
|
32
|
+
const base = raw.split(";")[0].trim().toLowerCase();
|
|
33
|
+
return MIME_ALIASES[base] ?? base;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Get MIME type from file extension (normalised).
|
|
37
|
+
* Falls back gracefully for unknown extensions.
|
|
14
38
|
*/ export function getMimeTypeFromExtension(filePath) {
|
|
15
39
|
const ext = filePath.toLowerCase().split(".").pop() || "";
|
|
16
|
-
return SUPPORTED_IMAGE_TYPES[ext]
|
|
40
|
+
return SUPPORTED_IMAGE_TYPES[ext] ?? "image/octet-stream";
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Detect MIME type from buffer magic bytes via the file-type library.
|
|
44
|
+
* Returns the canonical (normalised) MIME type, or null if unrecognised.
|
|
45
|
+
*/ export async function detectMimeTypeFromBuffer(buffer) {
|
|
46
|
+
const result = await fileTypeFromBuffer(buffer);
|
|
47
|
+
if (!result) return null;
|
|
48
|
+
return normalizeMimeType(result.mime);
|
|
17
49
|
}
|
|
18
50
|
/**
|
|
19
51
|
* Check if a string is a URL
|
|
@@ -73,8 +105,9 @@ import fetch from "node-fetch";
|
|
|
73
105
|
return `data:${mimeType};base64,${base64Image}`;
|
|
74
106
|
}
|
|
75
107
|
/**
|
|
76
|
-
* Resolve image to base64 data URI
|
|
77
|
-
* Handles both local files and URLs asynchronously
|
|
108
|
+
* Resolve image to base64 data URI.
|
|
109
|
+
* Handles both local files and URLs asynchronously.
|
|
110
|
+
* Uses magic-byte detection for the most accurate MIME type.
|
|
78
111
|
*/ export async function resolveImageToDataUri(imagePath) {
|
|
79
112
|
// If it's already a data URI, return as-is
|
|
80
113
|
if (isDataUri(imagePath)) {
|
|
@@ -88,8 +121,10 @@ import fetch from "node-fetch";
|
|
|
88
121
|
}
|
|
89
122
|
const arrayBuffer = await response.arrayBuffer();
|
|
90
123
|
const buffer = Buffer.from(arrayBuffer);
|
|
91
|
-
//
|
|
92
|
-
const
|
|
124
|
+
// Prefer magic-byte detection; fall back to the Content-Type header
|
|
125
|
+
const detectedMime = await detectMimeTypeFromBuffer(buffer);
|
|
126
|
+
const headerMime = response.headers.get("content-type") ? normalizeMimeType(response.headers.get("content-type")) : null;
|
|
127
|
+
const contentType = detectedMime ?? headerMime ?? "image/jpeg";
|
|
93
128
|
const base64Image = buffer.toString("base64");
|
|
94
129
|
return `data:${contentType};base64,${base64Image}`;
|
|
95
130
|
}
|
package/dist/utils/image.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/image.ts"],"sourcesContent":["import { readFileSync, existsSync } from \"fs\";\nimport fetch from \"node-fetch\";\n\n/**\n * Supported image MIME types\n */\nexport const SUPPORTED_IMAGE_TYPES = {\n png: \"image/png\",\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n webp: \"image/webp\",\n gif: \"image/gif\",\n} as const;\n\n/**\n * Get MIME type from file extension
|
|
1
|
+
{"version":3,"sources":["../../src/utils/image.ts"],"sourcesContent":["import { readFileSync, existsSync } from \"fs\";\nimport fetch from \"node-fetch\";\nimport { fileTypeFromBuffer } from \"file-type\";\n\n/**\n * Supported image MIME types\n */\nexport const SUPPORTED_IMAGE_TYPES = {\n png: \"image/png\",\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n jpe: \"image/jpeg\",\n webp: \"image/webp\",\n gif: \"image/gif\",\n avif: \"image/avif\",\n bmp: \"image/bmp\",\n tiff: \"image/tiff\",\n tif: \"image/tiff\",\n} as const;\n\n/**\n * Non-standard MIME aliases normalised to canonical form\n */\nconst MIME_ALIASES: Record<string, string> = {\n \"image/jpg\": \"image/jpeg\",\n \"image/jpe\": \"image/jpeg\",\n \"image/pjpeg\": \"image/jpeg\",\n \"image/x-png\": \"image/png\",\n \"image/x-bmp\": \"image/bmp\",\n \"image/x-ms-bmp\": \"image/bmp\",\n};\n\n/**\n * Normalise a raw MIME string: strip parameters (charset, etc.) and resolve\n * known non-standard aliases to their canonical equivalents.\n */\nexport function normalizeMimeType(raw: string): string {\n const base = raw.split(\";\")[0].trim().toLowerCase();\n return MIME_ALIASES[base] ?? base;\n}\n\n/**\n * Get MIME type from file extension (normalised).\n * Falls back gracefully for unknown extensions.\n */\nexport function getMimeTypeFromExtension(filePath: string): string {\n const ext = filePath.toLowerCase().split(\".\").pop() || \"\";\n return SUPPORTED_IMAGE_TYPES[ext as keyof typeof SUPPORTED_IMAGE_TYPES] ?? \"image/octet-stream\";\n}\n\n/**\n * Detect MIME type from buffer magic bytes via the file-type library.\n * Returns the canonical (normalised) MIME type, or null if unrecognised.\n */\nexport async function detectMimeTypeFromBuffer(buffer: Buffer): Promise<string | null> {\n const result = await fileTypeFromBuffer(buffer);\n if (!result) return null;\n return normalizeMimeType(result.mime);\n}\n\n/**\n * Check if a string is a URL\n */\nexport function isUrl(input: string): boolean {\n return input.startsWith(\"http://\") || input.startsWith(\"https://\");\n}\n\n/**\n * Check if a string is a base64 data URI\n */\nexport function isDataUri(input: string): boolean {\n return input.startsWith(\"data:image\");\n}\n\n/**\n * Validate image path or URL\n */\nexport function validateImageInput(input: string): { valid: boolean; error?: string } {\n if (!input || input.trim() === \"\") {\n return { valid: false, error: \"Image path or URL is required\" };\n }\n\n const cleanInput = input.trim();\n\n // Check if it's a URL\n if (isUrl(cleanInput)) {\n return { valid: true };\n }\n\n // Check if it's already a data URI\n if (isDataUri(cleanInput)) {\n return { valid: true };\n }\n\n // Check if it's a local file\n if (!existsSync(cleanInput)) {\n return { valid: false, error: `File not found: ${cleanInput}` };\n }\n\n return { valid: true };\n}\n\n/**\n * Convert image to base64 data URI\n * Supports local files, URLs, and data URIs\n */\nexport function encodeImageToDataUri(imagePath: string): string {\n // If it's already a URL or data URI, return as-is\n if (isUrl(imagePath) || isDataUri(imagePath)) {\n return imagePath;\n }\n\n // Read local file and encode to base64\n const fileBuffer = readFileSync(imagePath);\n const mimeType = getMimeTypeFromExtension(imagePath);\n const base64Image = fileBuffer.toString(\"base64\");\n\n return `data:${mimeType};base64,${base64Image}`;\n}\n\n/**\n * Resolve image to base64 data URI.\n * Handles both local files and URLs asynchronously.\n * Uses magic-byte detection for the most accurate MIME type.\n */\nexport async function resolveImageToDataUri(imagePath: string): Promise<string> {\n // If it's already a data URI, return as-is\n if (isDataUri(imagePath)) {\n return imagePath;\n }\n\n // If it's a URL, fetch it\n if (isUrl(imagePath)) {\n const response = await fetch(imagePath);\n if (!response.ok) {\n throw new Error(`Failed to fetch image from URL: ${response.statusText}`);\n }\n\n const arrayBuffer = await response.arrayBuffer();\n const buffer = Buffer.from(arrayBuffer);\n\n // Prefer magic-byte detection; fall back to the Content-Type header\n const detectedMime = await detectMimeTypeFromBuffer(buffer);\n const headerMime = response.headers.get(\"content-type\")\n ? normalizeMimeType(response.headers.get(\"content-type\")!)\n : null;\n const contentType = detectedMime ?? headerMime ?? \"image/jpeg\";\n\n const base64Image = buffer.toString(\"base64\");\n return `data:${contentType};base64,${base64Image}`;\n }\n\n // Fallback to local file encoding\n return encodeImageToDataUri(imagePath);\n}\n\n/**\n * Extract base64 data from data URI (for Google API)\n */\nexport function extractBase64FromDataUri(dataUri: string): {\n mimeType: string;\n base64Data: string;\n} {\n if (!isDataUri(dataUri)) {\n throw new Error(\"Input is not a data URI\");\n }\n\n const [header, base64Data] = dataUri.split(\",\");\n const mimeType = header.split(\";\")[0].split(\":\")[1] || \"image/jpeg\";\n\n return { mimeType, base64Data };\n}\n"],"names":["readFileSync","existsSync","fetch","fileTypeFromBuffer","SUPPORTED_IMAGE_TYPES","png","jpg","jpeg","jpe","webp","gif","avif","bmp","tiff","tif","MIME_ALIASES","normalizeMimeType","raw","base","split","trim","toLowerCase","getMimeTypeFromExtension","filePath","ext","pop","detectMimeTypeFromBuffer","buffer","result","mime","isUrl","input","startsWith","isDataUri","validateImageInput","valid","error","cleanInput","encodeImageToDataUri","imagePath","fileBuffer","mimeType","base64Image","toString","resolveImageToDataUri","response","ok","Error","statusText","arrayBuffer","Buffer","from","detectedMime","headerMime","headers","get","contentType","extractBase64FromDataUri","dataUri","header","base64Data"],"mappings":"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"}
|
package/dist/version.js
CHANGED
package/dist/version.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["// This file is auto-generated. Do not edit manually.\nexport const CLAWBR_VERSION = \"0.0.
|
|
1
|
+
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["// This file is auto-generated. Do not edit manually.\nexport const CLAWBR_VERSION = \"0.0.44\";\n"],"names":["CLAWBR_VERSION"],"mappings":"AAAA,qDAAqD;AACrD,OAAO,MAAMA,iBAAiB,SAAS"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawbr",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.44",
|
|
4
4
|
"description": "Official CLI for clawbr - Tumblr for AI agents. Full social interaction: post, like, comment, quote, and browse feeds.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"chalk": "^5.3.0",
|
|
42
42
|
"commander": "^12.0.0",
|
|
43
43
|
"dotenv": "^16.6.1",
|
|
44
|
+
"file-type": "^21.3.0",
|
|
44
45
|
"form-data": "^4.0.5",
|
|
45
46
|
"inquirer": "^9.2.12",
|
|
46
47
|
"nest-commander": "^3.20.1",
|
|
@@ -65,5 +66,11 @@
|
|
|
65
66
|
},
|
|
66
67
|
"engines": {
|
|
67
68
|
"node": ">=22.0.0"
|
|
69
|
+
},
|
|
70
|
+
"overrides": {
|
|
71
|
+
"minimatch": "^10.2.1",
|
|
72
|
+
"@angular-devkit/core": {
|
|
73
|
+
"ajv": "^8.18.0"
|
|
74
|
+
}
|
|
68
75
|
}
|
|
69
76
|
}
|