diff --git a/packages/kbot/cat.png b/packages/kbot/cat.png new file mode 100644 index 00000000..3e987f49 Binary files /dev/null and b/packages/kbot/cat.png differ diff --git a/packages/kbot/dist-in/commands/images.d.ts b/packages/kbot/dist-in/commands/images.d.ts new file mode 100644 index 00000000..f33e5e82 --- /dev/null +++ b/packages/kbot/dist-in/commands/images.d.ts @@ -0,0 +1,2 @@ +export declare const ImageOptionsSchema: () => any; +export declare const imageCommand: (argv: any) => Promise; diff --git a/packages/kbot/dist-in/commands/images.js b/packages/kbot/dist-in/commands/images.js new file mode 100644 index 00000000..33e794ea --- /dev/null +++ b/packages/kbot/dist-in/commands/images.js @@ -0,0 +1,69 @@ +import { z } from 'zod'; +import { sync as write } from '@polymech/fs/write'; +import { sync as exists } from '@polymech/fs/exists'; +import { isArray } from '@polymech/core/primitives'; +import { OptionsSchema } from '../zod_schema.js'; +import { createImage, editImage } from '../lib/images-google.js'; +import { getLogger } from '../index.js'; +export const ImageOptionsSchema = () => { + const baseSchema = OptionsSchema().pick({ + prompt: true, + include: true, + dst: true, + model: true, + logLevel: true, + config: true, + api_key: true, + }); + return baseSchema.extend({ + model: z.string().default('gemini-2.5-flash-image-preview').describe('AI model to use for image generation/editing.'), + dst: z.string().describe('Destination path for the output image. Required.'), + prompt: z.string().optional().describe('The prompt for image creation or editing.'), + }); +}; +export const imageCommand = async (argv) => { + const logger = getLogger(argv); + try { + const parsedOptions = ImageOptionsSchema().parse(argv); + const { prompt, include, dst, ...rest } = parsedOptions; + if (!prompt && !include) { + logger.error('Either --prompt (for image creation) or --include (for image editing) must be provided.'); + return; + } + if (!dst) { + logger.error('--dst is required to specify the output file path.'); + return; + } + let imageBuffer = null; + if (include && isArray(include) && include.length > 0) { + // Image editing + const imagePath = include[0]; + if (!exists(imagePath)) { + logger.error(`Input image not found at: ${imagePath}`); + return; + } + if (!prompt) { + logger.error('--prompt is required for image editing.'); + return; + } + logger.info(`Editing image "${imagePath}" with prompt: "${prompt}"`); + imageBuffer = await editImage(prompt, imagePath, parsedOptions); + } + else if (prompt) { + // Image creation + logger.info(`Creating image with prompt: "${prompt}"`); + imageBuffer = await createImage(prompt, parsedOptions); + } + if (imageBuffer) { + write(dst, imageBuffer); + logger.info(`Image saved to: ${dst}`); + } + else { + logger.error('Failed to generate image.'); + } + } + catch (error) { + logger.error('Failed to parse options or generate image:', error.message, error.issues, error.stack); + } +}; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW1hZ2VzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2NvbW1hbmRzL2ltYWdlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsQ0FBQyxFQUFFLE1BQU0sS0FBSyxDQUFDO0FBRXhCLE9BQU8sRUFBRSxJQUFJLElBQUksS0FBSyxFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFDbkQsT0FBTyxFQUFFLElBQUksSUFBSSxNQUFNLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQUVyRCxPQUFPLEVBQUUsT0FBTyxFQUFZLE1BQU0sMkJBQTJCLENBQUM7QUFFOUQsT0FBTyxFQUFFLGFBQWEsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBQ2pELE9BQU8sRUFBRSxXQUFXLEVBQUUsU0FBUyxFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFDakUsT0FBTyxFQUFFLFNBQVMsRUFBRSxNQUFNLGFBQWEsQ0FBQztBQUV4QyxNQUFNLENBQUMsTUFBTSxrQkFBa0IsR0FBRyxHQUFHLEVBQUU7SUFDbkMsTUFBTSxVQUFVLEdBQUcsYUFBYSxFQUFFLENBQUMsSUFBSSxDQUFDO1FBQ3BDLE1BQU0sRUFBRSxJQUFJO1FBQ1osT0FBTyxFQUFFLElBQUk7UUFDYixHQUFHLEVBQUUsSUFBSTtRQUNULEtBQUssRUFBRSxJQUFJO1FBQ1gsUUFBUSxFQUFFLElBQUk7UUFDZCxNQUFNLEVBQUUsSUFBSTtRQUNaLE9BQU8sRUFBRSxJQUFJO0tBQ2hCLENBQUMsQ0FBQztJQUVILE9BQU8sVUFBVSxDQUFDLE1BQU0sQ0FBQztRQUNyQixLQUFLLEVBQUUsQ0FBQyxDQUFDLE1BQU0sRUFBRSxDQUFDLE9BQU8sQ0FBQyxnQ0FBZ0MsQ0FBQyxDQUFDLFFBQVEsQ0FBQywrQ0FBK0MsQ0FBQztRQUNySCxHQUFHLEVBQUUsQ0FBQyxDQUFDLE1BQU0sRUFBRSxDQUFDLFFBQVEsQ0FBQyxrREFBa0QsQ0FBQztRQUM1RSxNQUFNLEVBQUUsQ0FBQyxDQUFDLE1BQU0s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 \ No newline at end of file diff --git a/packages/kbot/dist-in/lib/images-google.d.ts b/packages/kbot/dist-in/lib/images-google.d.ts new file mode 100644 index 00000000..54a8b999 --- /dev/null +++ b/packages/kbot/dist-in/lib/images-google.d.ts @@ -0,0 +1,3 @@ +import { IKBotOptions } from "../zod_types.js"; +export declare const createImage: (prompt: string, options: IKBotOptions) => Promise; +export declare const editImage: (prompt: string, imagePath: string, options: IKBotOptions) => Promise; diff --git a/packages/kbot/dist-in/lib/images-google.js b/packages/kbot/dist-in/lib/images-google.js new file mode 100644 index 00000000..e22ede79 --- /dev/null +++ b/packages/kbot/dist-in/lib/images-google.js @@ -0,0 +1,70 @@ +import { GoogleGenerativeAI } from "@google/generative-ai"; +import * as fs from "node:fs"; +import { loadConfig } from "../config.js"; +import { logger } from "../index.js"; +import { lookup } from 'mime-types'; +const createGoogleGenAIClient = (options) => { + const config = loadConfig(options); + if (!config) { + logger.error("Config not found in $HOME/.osr/config.json. " + + "Optionally, export OSR_CONFIG with the path to the configuration file."); + return undefined; + } + let apiKey = options.api_key || config?.google?.key; + logger.debug(`Google API Key: ${apiKey}`); + if (!apiKey) { + logger.error(`No gemini key found. Please provide an "api_key", set it in the config, or pass it via JSON config.`); + return undefined; + } + return new GoogleGenerativeAI(apiKey); +}; +export const createImage = async (prompt, options) => { + const ai = createGoogleGenAIClient(options); + if (!ai) { + return null; + } + const model = ai.getGenerativeModel({ model: options.model || 'gemini-1.5-flash-image-preview' }); + const result = await model.generateContent(prompt); + const response = result.response; + const parts = response.candidates[0].content.parts; + for (const part of parts) { + if ('inlineData' in part) { + const inlineData = part.inlineData; + if (inlineData) { + return Buffer.from(inlineData.data, "base64"); + } + } + } + return null; +}; +export const editImage = async (prompt, imagePath, options) => { + const ai = createGoogleGenAIClient(options); + if (!ai) { + return null; + } + const model = ai.getGenerativeModel({ model: options.model || 'gemini-2.5-flash-image-preview' }); + const imageData = fs.readFileSync(imagePath); + const base64Image = imageData.toString("base64"); + const mimeType = lookup(imagePath) || 'image/png'; + const textPart = { text: prompt }; + const imagePart = { + inlineData: { + mimeType, + data: base64Image, + }, + }; + const promptParts = [textPart, imagePart]; + const result = await model.generateContent(promptParts); + const response = result.response; + const parts = response.candidates[0].content.parts; + for (const part of parts) { + if ('inlineData' in part) { + const inlineData = part.inlineData; + if (inlineData) { + return Buffer.from(inlineData.data, "base64"); + } + } + } + return null; +}; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW1hZ2VzLWdvb2dsZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9saWIvaW1hZ2VzLWdvb2dsZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsa0JBQWtCLEVBQVEsTUFBTSx1QkFBdUIsQ0FBQztBQUNqRSxPQUFPLEtBQUssRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUU5QixPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sY0FBYyxDQUFDO0FBQzFDLE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxhQUFhLENBQUM7QUFDckMsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLFlBQVksQ0FBQTtBQUVuQyxNQUFNLHVCQUF1QixHQUFHLENBQUMsT0FBcUIsRUFBRSxFQUFFO0lBQ3RELE1BQU0sTUFBTSxHQUFHLFVBQVUsQ0FBQyxPQUFPLENBQUMsQ0FBQztJQUNuQyxJQUFJLENBQUMsTUFBTSxFQUFFLENBQUM7UUFDVixNQUFNLENBQUMsS0FBSyxDQUNSLDhDQUE4QztZQUM5Qyx3RUFBd0UsQ0FDM0UsQ0FBQztRQUNGLE9BQU8sU0FBUyxDQUFDO0lBQ3JCLENBQUM7SUFFRCxJQUFJLE1BQU0sR0FBRyxPQUFPLENBQUMsT0FBTyxJQUFJLE1BQU0sRUFBRSxNQUFNLEVBQUUsR0FBRyxDQUFDO0lBQ3BELE1BQU0sQ0FBQyxLQUFLLENBQUMsbUJBQW1CLE1BQU0sRUFBRSxDQUFDLENBQUM7SUFFMUMsSUFBSSxDQUFDLE1BQU0sRUFBRSxDQUFDO1FBQ1YsTUFBTSxDQUFDLEtBQUssQ0FBQyxxR0FBcUcsQ0FBQyxDQUFDO1FBQ3BILE9BQU8sU0FBUyxDQUFDO0lBQ3JCLENBQUM7SUFFRCxPQUFPLElBQUksa0JBQWtCLENBQUMsTUFBTSxDQUFDLENBQUM7QUFDMUMsQ0FBQyxDQUFBO0FBRUQsTUFBTSxDQUFDLE1BQU0sV0FBVyxHQUFHLEtBQUssRUFBRSxNQUFjLEVBQUUsT0FBcUIsRUFBMEIsRUFBRTtJQUMvRixNQUFNLEVB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 \ No newline at end of file diff --git a/packages/kbot/dist-in/main.js b/packages/kbot/dist-in/main.js index d29489ac..f38a9e26 100644 --- a/packages/kbot/dist-in/main.js +++ b/packages/kbot/dist-in/main.js @@ -11,6 +11,7 @@ import { build } from './commands/build.js'; import { fetch } from './commands/fetch.js'; import { run } from './commands/run.js'; import { transcribeCommand, TranscribeOptionsSchema } from './commands/transcribe.js'; +import { imageCommand, ImageOptionsSchema } from './commands/images.js'; export const logger = createLogger('llm-tools'); const modify = async (argv) => await run(argv); const yargOptions = { @@ -30,6 +31,7 @@ const yargOptions = { yargs(hideBin(process.argv)) .command('init', 'Initialize KBot configuration', (yargs) => toYargs(yargs, OptionsSchema(), yargOptions), init) .command('modify [prompt]', 'Modify an existing project', (yargs) => toYargs(yargs, OptionsSchema(), yargOptions), modify) + .command('image [prompt]', 'Create or edit an image', (yargs) => toYargs(yargs, ImageOptionsSchema(), yargOptions), imageCommand) .command('transcribe', 'Transcribe audio files', (yargs) => toYargs(yargs, TranscribeOptionsSchema(), yargOptions), transcribeCommand) .command('types', 'Generate types', (yargs) => { }, (argv) => types()) .command('schemas', 'Generate schemas', (yargs) => { }, (argv) => schemas()) @@ -41,4 +43,4 @@ yargs(hideBin(process.argv)) .help() //.wrap(yargs.terminalWidth() - 20) .parse(); -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFpbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9tYWluLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFDQSxPQUFPLEtBQUssTUFBTSxPQUFPLENBQUE7QUFDekIsT0FBTyxFQUFFLE9BQU8sRUFBRSxNQUFNLGVBQWUsQ0FBQTtBQUN2QyxPQUFPLEVBQUUsT0FBTyxFQUFFLE1BQU0sbUJBQW1CLENBQUE7QUFDM0MsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLGVBQWUsQ0FBQTtBQUU1QyxPQUFPLEVBQUUsYUFBYSxFQUFFLE9BQU8sRUFBRSxLQUFLLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQTtBQUcvRCxPQUFPLFdBQVcsTUFBTSxvQkFBb0IsQ0FBQTtBQUM1QyxPQUFPLEVBQUUsUUFBUSxFQUFFLE1BQU0sd0JBQXdCLENBQUE7QUFDakQsT0FBTyxFQUFFLElBQUksRUFBRSxNQUFNLG9CQUFvQixDQUFBO0FBQ3pDLE9BQU8sRUFBRSxLQUFLLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQTtBQUMzQyxPQUFPLEVBQUUsS0FBSyxFQUFFLE1BQU0scUJBQXFCLENBQUE7QUFDM0MsT0FBTyxFQUFFLEdBQUcsRUFBRSxNQUFNLG1CQUFtQixDQUFBO0FBRXZDLE9BQU8sRUFBRSxpQkFBaUIsRUFBRSx1QkFBdUIsRUFBRSxNQUFNLDBCQUEwQixDQUFBO0FBRXJGLE1BQU0sQ0FBQyxNQUFNLE1BQU0sR0FBUSxZQUFZLENBQUMsV0FBVyxDQUFDLENBQUE7QUFFcEQsTUFBTSxNQUFNLEdBQUcsS0FBSyxFQUFFLElBQWUsRUFBRSxFQUFFLENBQUUsTUFBTSxHQUFHLENBQUMsSUFBaUIsQ0FBQyxDQUFBO0FBRXZFLE1BQU0sV0FBVyxHQUFRO0lBQ3ZCLEtBQUssRUFBRSxDQUFDLENBQUMsTUFBTSxFQUFFLEdBQUcsRUFBRSxPQUFPLEVBQUUsRUFBRTtRQUMvQixRQUFRLEdBQUcsRUFBRSxDQUFDO1lBQ1osS0FBSyxRQUFRO2dCQUNYLENBQUM7b0JBQ0Ms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 \ No newline at end of file +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFpbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9tYWluLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFDQSxPQUFPLEtBQUssTUFBTSxPQUFPLENBQUE7QUFDekIsT0FBTyxFQUFFLE9BQU8sRUFBRSxNQUFNLGVBQWUsQ0FBQTtBQUN2QyxPQUFPLEVBQUUsT0FBTyxFQUFFLE1BQU0sbUJBQW1CLENBQUE7QUFDM0MsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLGVBQWUsQ0FBQTtBQUU1QyxPQUFPLEVBQUUsYUFBYSxFQUFFLE9BQU8sRUFBRSxLQUFLLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQTtBQUcvRCxPQUFPLFdBQVcsTUFBTSxvQkFBb0IsQ0FBQTtBQUM1QyxPQUFPLEVBQUUsUUFBUSxFQUFFLE1BQU0sd0JBQXdCLENBQUE7QUFDakQsT0FBTyxFQUFFLElBQUksRUFBRSxNQUFNLG9CQUFvQixDQUFBO0FBQ3pDLE9BQU8sRUFBRSxLQUFLLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQTtBQUMzQyxPQUFPLEVBQUUsS0FBSyxFQUFFLE1BQU0scUJBQXFCLENBQUE7QUFDM0MsT0FBTyxFQUFFLEdBQUcsRUFBRSxNQUFNLG1CQUFtQixDQUFBO0FBRXZDLE9BQU8sRUFBRSxpQkFBaUIsRUFBRSx1QkFBdUIsRUFBRSxNQUFNLDBCQUEwQixDQUFBO0FBQ3JGLE9BQU8sRUFBRSxZQUFZLEVBQUUsa0JBQWtCLEVBQUUsTUFBTSxzQkFBc0IsQ0FBQTtBQUV2RSxNQUFNLENBQUMsTUFBTSxNQUFNLEdBQVEsWUFBWSxDQUFDLFdBQVcsQ0FBQyxDQUFBO0FBRXBELE1BQU0sTUFBTSxHQUFHLEtBQUssRUFBRSxJQUFlLEVBQUUsRUFBRSxDQUFFLE1BQU0sR0FBRyxDQUFDLElBQWlCLENBQUMsQ0FBQTtBQUV2RSxNQUFNLFdBQVcsR0FBUTtJQUN2QixLQUFLLEVBQUUsQ0FBQyxDQUFDLE1BQU0sRUFBRSxHQUFHLEVBQUUsT0FBTyxFQUFFLEVBQUU7UUFD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 \ No newline at end of file diff --git a/packages/kbot/docs/images.md b/packages/kbot/docs/images.md new file mode 100644 index 00000000..50003d34 --- /dev/null +++ b/packages/kbot/docs/images.md @@ -0,0 +1,38 @@ +# Image Command + +The `image` command allows you to create and edit images using Google's Gemini models. + +## Description + +This tool can be used in two modes: + +1. **Image Creation (Text-to-Image)**: Generate an image from a text description. +2. **Image Editing (Image-and-Text-to-Image)**: Modify an existing image based on a text description. + +## Usage + +### Image Creation + +To create an image, provide a text prompt using the `prompt` argument or option. You must also specify an output path with `--dst`. + +```bash +kbot image "A futuristic cityscape at sunset" --dst ./cityscape.png +``` + +### Image Editing + +To edit an image, you need to provide the path to the input image using the `--include` (or `-i`) option and a text prompt describing the desired changes. + +```bash +kbot image "Make the sky purple" --include ./cityscape.png --dst ./cityscape_purple.png +``` + +## Options + +- `[prompt]`: (Optional) The text prompt for creating or editing an image. Can be provided as a positional argument. +- `--dst `: (Required) The path to save the output image. +- `--include `, `-i `: (Optional) The path to the input image for editing. +- `--model `: (Optional) The model to use for image generation. Defaults to `gemini-1.5-flash-image-preview`. +- `--api_key `: (Optional) Your Google GenAI API key. It can also be configured in the kbot config file. +- `--logLevel `: (Optional) Set the logging level. +- `--config `: (Optional) Path to a custom configuration file. diff --git a/packages/kbot/package-lock.json b/packages/kbot/package-lock.json index cb3e837d..c49a5160 100644 --- a/packages/kbot/package-lock.json +++ b/packages/kbot/package-lock.json @@ -10,6 +10,8 @@ "license": "MIT", "dependencies": { "@dmitryrechkin/json-schema-to-zod": "1.0.1", + "@google/genai": "1.19.0", + "@google/generative-ai": "0.24.1", "@polymech/ai-tools": "file:../ai-tools", "@polymech/cache": "file:../cache", "@polymech/commons": "file:../commons", @@ -74,7 +76,6 @@ "dependencies": { "@datalust/winston-seq": "^2.0.0", "@inquirer/prompts": "^7.3.2", - "@keyv/sqlite": "^4.0.5", "@polymech/commons": "file:../commons", "@polymech/core": "file:../core", "@polymech/fs": "file:../fs", @@ -912,6 +913,36 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@google/genai": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.19.0.tgz", + "integrity": "sha512-mIMV3M/KfzzFA//0fziK472wKBJ1TdJLhozIUJKTPLyTDN1NotU+hyoHW/N0cfrcEWUK20YA0GxCeHC4z0SbMA==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^9.14.2", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.11.4" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@google/generative-ai": { + "version": "0.24.1", + "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.1.tgz", + "integrity": "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -2653,6 +2684,15 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -3017,7 +3057,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -3034,6 +3073,15 @@ ], "license": "MIT" }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -3156,6 +3204,12 @@ "node": "*" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-fill": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", @@ -4592,6 +4646,15 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.102", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.102.tgz", @@ -5418,6 +5481,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -5620,6 +5725,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -5710,6 +5841,19 @@ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -5833,6 +5977,19 @@ "node": ">=10.19.0" } }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -6339,6 +6496,15 @@ "node": ">= 10.16.0" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -6407,6 +6573,27 @@ "node": ">=18.0.0" } }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -7786,6 +7973,26 @@ "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", "license": "MIT" }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-gyp-build": { "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", @@ -9048,7 +9255,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -10063,6 +10269,12 @@ "node": ">=6" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/trim-repeated": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", @@ -11054,6 +11266,19 @@ "dev": true, "license": "MIT" }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -11262,6 +11487,12 @@ "defaults": "^1.0.3" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, "node_modules/webpack": { "version": "5.97.1", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", @@ -11477,6 +11708,16 @@ "node": ">=4.0" } }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -11598,8 +11839,6 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "license": "MIT", - "optional": true, - "peer": true, "engines": { "node": ">=10.0.0" }, diff --git a/packages/kbot/package.json b/packages/kbot/package.json index 77fbb9bc..4ec21216 100644 --- a/packages/kbot/package.json +++ b/packages/kbot/package.json @@ -27,6 +27,7 @@ "test": "vitest run", "test:basic": "vitest run tests/unit/basic.test.ts", "test:transcribe": "vitest run tests/unit/transcribe/transcribe.test.ts", + "test:images": "vitest run tests/unit/images/images.test.ts", "test:math": "vitest run tests/unit/math.test.ts", "test:format": "vitest run tests/unit/format.test.ts", "test:options-glob": "vitest run tests/unit/options-glob.test.ts", @@ -56,6 +57,8 @@ }, "dependencies": { "@dmitryrechkin/json-schema-to-zod": "1.0.1", + "@google/genai": "1.19.0", + "@google/generative-ai": "0.24.1", "@polymech/ai-tools": "file:../ai-tools", "@polymech/cache": "file:../cache", "@polymech/commons": "file:../commons", diff --git a/packages/kbot/src/commands/images.ts b/packages/kbot/src/commands/images.ts new file mode 100644 index 00000000..a6c515da --- /dev/null +++ b/packages/kbot/src/commands/images.ts @@ -0,0 +1,78 @@ +import { z } from 'zod'; +import * as path from 'node:path'; +import { sync as write } from '@polymech/fs/write'; +import { sync as exists } from '@polymech/fs/exists'; +import { ILogObj, Logger } from 'tslog'; +import { isArray, isString } from '@polymech/core/primitives'; + +import { OptionsSchema } from '../zod_schema.js'; +import { createImage, editImage } from '../lib/images-google.js'; +import { getLogger } from '../index.js'; + +export const ImageOptionsSchema = () => { + const baseSchema = OptionsSchema().pick({ + prompt: true, + include: true, + dst: true, + model: true, + logLevel: true, + config: true, + api_key: true, + }); + + return baseSchema.extend({ + model: z.string().default('gemini-2.5-flash-image-preview').describe('AI model to use for image generation/editing.'), + dst: z.string().describe('Destination path for the output image. Required.'), + prompt: z.string().optional().describe('The prompt for image creation or editing.'), + }); +} + +export const imageCommand = async (argv: any) => { + const logger = getLogger(argv); + + try { + const parsedOptions = ImageOptionsSchema().parse(argv); + const { prompt, include, dst, ...rest } = parsedOptions; + + if (!prompt && !include) { + logger.error('Either --prompt (for image creation) or --include (for image editing) must be provided.'); + return; + } + + if (!dst) { + logger.error('--dst is required to specify the output file path.'); + return; + } + + let imageBuffer: Buffer | null = null; + + if (include && isArray(include) && include.length > 0) { + // Image editing + const imagePath = include[0]; + if (!exists(imagePath)) { + logger.error(`Input image not found at: ${imagePath}`); + return; + } + if (!prompt) { + logger.error('--prompt is required for image editing.'); + return; + } + logger.info(`Editing image "${imagePath}" with prompt: "${prompt}"`); + imageBuffer = await editImage(prompt, imagePath, parsedOptions); + } else if (prompt) { + // Image creation + logger.info(`Creating image with prompt: "${prompt}"`); + imageBuffer = await createImage(prompt, parsedOptions); + } + + if (imageBuffer) { + write(dst, imageBuffer); + logger.info(`Image saved to: ${dst}`); + } else { + logger.error('Failed to generate image.'); + } + + } catch (error) { + logger.error('Failed to parse options or generate image:', error.message, error.issues, error.stack); + } +}; diff --git a/packages/kbot/src/lib/images-google.ts b/packages/kbot/src/lib/images-google.ts new file mode 100644 index 00000000..471d3342 --- /dev/null +++ b/packages/kbot/src/lib/images-google.ts @@ -0,0 +1,89 @@ +import { GoogleGenerativeAI, Part } from "@google/generative-ai"; +import * as fs from "node:fs"; +import { IKBotOptions } from "../zod_types.js"; +import { loadConfig } from "../config.js"; +import { logger } from "../index.js"; +import { lookup } from 'mime-types' + +const createGoogleGenAIClient = (options: IKBotOptions) => { + const config = loadConfig(options); + if (!config) { + logger.error( + "Config not found in $HOME/.osr/config.json. " + + "Optionally, export OSR_CONFIG with the path to the configuration file." + ); + return undefined; + } + + let apiKey = options.api_key || config?.google?.key; + logger.debug(`Google API Key: ${apiKey}`); + + if (!apiKey) { + logger.error(`No gemini key found. Please provide an "api_key", set it in the config, or pass it via JSON config.`); + return undefined; + } + + return new GoogleGenerativeAI(apiKey); +} + +export const createImage = async (prompt: string, options: IKBotOptions): Promise => { + const ai = createGoogleGenAIClient(options); + if (!ai) { + return null; + } + + const model = ai.getGenerativeModel({ model: options.model || 'gemini-1.5-flash-image-preview' }); + + const result = await model.generateContent(prompt); + + const response = result.response; + const parts = response.candidates[0].content.parts; + for (const part of parts) { + if ('inlineData' in part) { + const inlineData = part.inlineData; + if (inlineData) { + return Buffer.from(inlineData.data, "base64"); + } + } + } + + return null; +} + +export const editImage = async (prompt: string, imagePath: string, options: IKBotOptions): Promise => { + const ai = createGoogleGenAIClient(options); + if (!ai) { + return null; + } + + const model = ai.getGenerativeModel({ model: options.model || 'gemini-2.5-flash-image-preview' }); + + const imageData = fs.readFileSync(imagePath); + const base64Image = imageData.toString("base64"); + const mimeType = lookup(imagePath) || 'image/png'; + + const textPart: Part = { text: prompt }; + const imagePart: Part = { + inlineData: { + mimeType, + data: base64Image, + }, + }; + + const promptParts = [textPart, imagePart]; + + const result = await model.generateContent(promptParts); + + const response = result.response; + const parts = response.candidates[0].content.parts; + for (const part of parts) { + if ('inlineData' in part) { + const inlineData = part.inlineData; + if (inlineData) { + return Buffer.from(inlineData.data, "base64"); + } + } + } + + return null; +} diff --git a/packages/kbot/src/main.ts b/packages/kbot/src/main.ts index d4be38ff..84f2858e 100644 --- a/packages/kbot/src/main.ts +++ b/packages/kbot/src/main.ts @@ -15,6 +15,7 @@ import { fetch } from './commands/fetch.js' import { run } from './commands/run.js' import { transcribeCommand, TranscribeOptionsSchema } from './commands/transcribe.js' +import { imageCommand, ImageOptionsSchema } from './commands/images.js' export const logger: any = createLogger('llm-tools') @@ -48,6 +49,12 @@ yargs(hideBin(process.argv)) (yargs) => toYargs(yargs, OptionsSchema(), yargOptions), modify ) + .command( + 'image [prompt]', + 'Create or edit an image', + (yargs) => toYargs(yargs, ImageOptionsSchema(), yargOptions), + imageCommand + ) .command( 'transcribe', 'Transcribe audio files', diff --git a/packages/kbot/tests/unit/images/cat-bird.png b/packages/kbot/tests/unit/images/cat-bird.png new file mode 100644 index 00000000..b9af36f1 Binary files /dev/null and b/packages/kbot/tests/unit/images/cat-bird.png differ diff --git a/packages/kbot/tests/unit/images/cat-fish.png b/packages/kbot/tests/unit/images/cat-fish.png new file mode 100644 index 00000000..c32ca10f Binary files /dev/null and b/packages/kbot/tests/unit/images/cat-fish.png differ diff --git a/packages/kbot/tests/unit/images/cat.png b/packages/kbot/tests/unit/images/cat.png new file mode 100644 index 00000000..17b2ca79 Binary files /dev/null and b/packages/kbot/tests/unit/images/cat.png differ diff --git a/packages/kbot/tests/unit/images/images.test.ts b/packages/kbot/tests/unit/images/images.test.ts new file mode 100644 index 00000000..2e31bd84 --- /dev/null +++ b/packages/kbot/tests/unit/images/images.test.ts @@ -0,0 +1,73 @@ +import { describe, it, expect, afterAll, beforeAll } from 'vitest' +import * as path from 'node:path' +import * as fs from 'node:fs' +import { sync as exists } from "@polymech/fs/exists" + +import { imageCommand } from '../../../src/commands/images.js' +import { IKBotTask } from '@polymech/ai-tools' + +const TEST_DATA_DIR = './tests/unit/images' +const TEST_TIMEOUT = 60000 // Increased timeout for API call + +describe('Image Command', () => { + + const createOutputFile = path.resolve(path.join(TEST_DATA_DIR, 'cat.png')) + const editInputFile = path.resolve(path.join(TEST_DATA_DIR, 'cat-bird.png')) + const editOutputFile = path.resolve(path.join(TEST_DATA_DIR, 'cat-fish.png')) + + const cleanupFiles = () => { + if (fs.existsSync(createOutputFile)) { + // fs.unlinkSync(createOutputFile) + } + if (fs.existsSync(editOutputFile)) { + fs.unlinkSync(editOutputFile) + } + } + + beforeAll(() => { + if (!fs.existsSync(TEST_DATA_DIR)) { + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); + } + cleanupFiles() + }) + // afterAll(cleanupFiles) + + it('should create an image from a prompt and save it to a file', async () => { + const options: IKBotTask = { + prompt: 'create an image of a little fat orange cat eating little birds', + dst: createOutputFile, + logLevel: 2, + } + + await imageCommand(options) + + expect(exists(createOutputFile)).toBe('file') + + }, TEST_TIMEOUT) + + it('should edit an image from a prompt and an input file', async () => { + // First, ensure the input file exists for the edit operation + if (!exists(editInputFile)) { + // As a fallback, copy the created file to the expected input path + if (exists(createOutputFile)) { + fs.copyFileSync(createOutputFile, editInputFile) + } else { + // If neither exists, we must skip this test + console.warn(`Skipping edit test: Input file not found at ${editInputFile}`) + return + } + } + + const options: IKBotTask = { + prompt: 'replace bird with fish', + include: [editInputFile], + dst: editOutputFile, + logLevel: 2, + } + + await imageCommand(options) + + expect(exists(editOutputFile)).toBe('file') + + }, TEST_TIMEOUT) +}) \ No newline at end of file