From 25e6e919be3b0d4e020a6b6be9feb8db26c6eef5 Mon Sep 17 00:00:00 2001 From: darrenhinde <107584450+darrenhinde@users.noreply.github.com> Date: Sun, 21 Sep 2025 17:34:31 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20Gemini=20API=20key=20?= =?UTF-8?q?configuration=20to=20env.example=20for=20integration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .opencode/tool/README.md | 79 +++++++++++++++ .opencode/tool/bun.lock | 160 ++++++++++++++++++++++++++++++ .opencode/tool/gemini-multiple.ts | 109 ++++++++++++++++++++ .opencode/tool/gemini.ts | 55 ++++++++++ .opencode/tool/package.json | 20 ++++ .opencode/tool/test-plugin.ts | 11 ++ .opencode/tool/tsconfig.json | 13 +++ env.example | 4 + 8 files changed, 451 insertions(+) create mode 100644 .opencode/tool/README.md create mode 100644 .opencode/tool/bun.lock create mode 100644 .opencode/tool/gemini-multiple.ts create mode 100644 .opencode/tool/gemini.ts create mode 100644 .opencode/tool/package.json create mode 100644 .opencode/tool/test-plugin.ts create mode 100644 .opencode/tool/tsconfig.json diff --git a/.opencode/tool/README.md b/.opencode/tool/README.md new file mode 100644 index 0000000..5197794 --- /dev/null +++ b/.opencode/tool/README.md @@ -0,0 +1,79 @@ +# Gemini Image Tools + +This collection of tools allows you to edit and analyze images using Google's Gemini AI directly from OpenCode. + +## Setup + +1. Get your Gemini API key from [Google AI Studio](https://makersuite.google.com/app/apikey) +2. Add it to your `.env` file: + ```bash + GEMINI_API_KEY=your_api_key_here + ``` + +## Available Tools + +### `/gemini` - Simple Image Editor +Edit an image using file path or data URL: + +```bash +/gemini "path/to/image.png" "Add a red border around the image" "output.png" +/gemini "data:image/png;base64,AAA..." "Convert to black and white" +``` + +### `/gemini_multiple_edit` - Advanced Image Editor +Same functionality as `/gemini` but from the multiple tools file: + +```bash +/gemini_multiple_edit "image.jpg" "Make it look like a watercolor painting" "watercolor.jpg" +``` + +### `/gemini_multiple_analyze` - Image Analysis +Analyze an image without editing it: + +```bash +/gemini_multiple_analyze "screenshot.png" "What programming language is shown in this code?" +/gemini_multiple_analyze "photo.jpg" "Describe the objects and colors in this image" +``` + +### `/gemini_edit` - Auto-Detection Plugin +1. Paste an image directly into your OpenCode chat +2. Use the command with just the prompt: + +```bash +/gemini_edit "Add the text 'Hello World' in cursive at top" +/gemini_edit "Make this image look like a painting" +``` + +## Features + +- **File Path Support**: Pass local image file paths +- **Data URL Support**: Use base64 data URLs from pasted images +- **Auto-Detection**: Plugin automatically captures the latest pasted image +- **Image Analysis**: Ask questions about images without editing +- **Flexible Output**: Specify custom output filenames or use defaults +- **Error Handling**: Clear error messages for missing API keys or failed requests + +## Files + +- `gemini.ts` - Simple tool that accepts image arguments +- `gemini-multiple.ts` - Multiple tools (edit + analyze) in one file +- `../plugin/gemini-edit.ts` - Plugin with auto-detection of pasted images + +## API Endpoints + +- **Image Editing**: Uses Gemini 2.5 Flash with image preview capabilities +- **Image Analysis**: Uses Gemini 2.5 Flash for text-based analysis + +## Examples + +```bash +# Edit an image +/gemini "logo.png" "Add a subtle drop shadow" "logo-shadow.png" + +# Analyze code in a screenshot +/gemini_multiple_analyze "code-screenshot.png" "What bugs can you spot in this code?" + +# Auto-edit pasted image +# (paste image first, then run:) +/gemini_edit "Remove the background and make it transparent" +``` \ No newline at end of file diff --git a/.opencode/tool/bun.lock b/.opencode/tool/bun.lock new file mode 100644 index 0000000..ba13347 --- /dev/null +++ b/.opencode/tool/bun.lock @@ -0,0 +1,160 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "opencode-gemini-tool", + "dependencies": { + "@opencode-ai/sdk": "^0.10.0", + "zod": "^4.1.9", + }, + "devDependencies": { + "@opencode-ai/plugin": "^0.10.0", + "@types/node": "^24.2.1", + "bun-types": "latest", + }, + }, + }, + "packages": { + "@hey-api/json-schema-ref-parser": ["@hey-api/json-schema-ref-parser@1.0.6", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0", "lodash": "^4.17.21" } }, "sha512-yktiFZoWPtEW8QKS65eqKwA5MTKp88CyiL8q72WynrBs/73SAaxlSWlA2zW/DZlywZ5hX1OYzrCC0wFdvO9c2w=="], + + "@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.81.0", "", { "dependencies": { "@hey-api/json-schema-ref-parser": "1.0.6", "ansi-colors": "4.1.3", "c12": "2.0.1", "color-support": "1.1.3", "commander": "13.0.0", "handlebars": "4.7.8", "js-yaml": "4.1.0", "open": "10.1.2", "semver": "7.7.2" }, "peerDependencies": { "typescript": "^5.5.3" }, "bin": { "openapi-ts": "bin/index.cjs" } }, "sha512-PoJukNBkUfHOoMDpN33bBETX49TUhy7Hu8Sa0jslOvFndvZ5VjQr4Nl/Dzjb9LG1Lp5HjybyTJMA6a1zYk/q6A=="], + + "@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="], + + "@opencode-ai/plugin": ["@opencode-ai/plugin@0.10.0", "", { "dependencies": { "@opencode-ai/sdk": "0.10.0", "zod": "4.1.8" } }, "sha512-qviOlxiWZchKGfJ1WWWM9q8Jz/6PokcdEUvGW0xeuC11QAkgC/RRC0DDn62PCcJUuWTOfIBkIcIQdP4qzbYNKw=="], + + "@opencode-ai/sdk": ["@opencode-ai/sdk@0.10.0", "", { "dependencies": { "@hey-api/openapi-ts": "0.81.0" } }, "sha512-2ojC3KEdf5r9rmhKJJx4cq8gxhw5LATHokIUl+VXsbE5qWaLe7yzt5Xpb2505wyieMfUlujo6XDGUtnPQXWkAw=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="], + + "@types/react": ["@types/react@19.1.13", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ=="], + + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="], + + "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], + + "c12": ["c12@2.0.1", "", { "dependencies": { "chokidar": "^4.0.1", "confbox": "^0.1.7", "defu": "^6.1.4", "dotenv": "^16.4.5", "giget": "^1.2.3", "jiti": "^2.3.0", "mlly": "^1.7.1", "ohash": "^1.1.4", "pathe": "^1.1.2", "perfect-debounce": "^1.0.0", "pkg-types": "^1.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-Z4JgsKXHG37C6PYUtIxCfLJZvo6FyhHJoClwwb9ftUkLpPSkuYqn6Tr+vnaN8hymm0kIbcg6Ey3kv/Q71k5w/A=="], + + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], + + "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], + + "color-support": ["color-support@1.1.3", "", { "bin": { "color-support": "bin.js" } }, "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="], + + "commander": ["commander@13.0.0", "", {}, "sha512-oPYleIY8wmTVzkvQq10AEok6YcTC4sRUBl8F9gVuwchGVUCTbl/vhLTaQqutuuySYOsu8YTgV+OxKc/8Yvx+mQ=="], + + "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + + "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "default-browser": ["default-browser@5.2.1", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg=="], + + "default-browser-id": ["default-browser-id@5.0.0", "", {}, "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA=="], + + "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], + + "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + + "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], + + "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + + "fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="], + + "giget": ["giget@1.2.5", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.5.4", "pathe": "^2.0.3", "tar": "^6.2.1" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-r1ekGw/Bgpi3HLV3h1MRBIlSAdHoIMklpaQ3OQLFcRw9PwAj2rqigvIbg+dBUI51OxVI2jsEtDywDBjSiuf7Ug=="], + + "handlebars": ["handlebars@4.7.8", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="], + + "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], + + "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], + + "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], + + "jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="], + + "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], + + "minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], + + "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + + "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], + + "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], + + "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], + + "nypm": ["nypm@0.5.4", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "tinyexec": "^0.3.2", "ufo": "^1.5.4" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-X0SNNrZiGU8/e/zAB7sCTtdxWTMSIO73q+xuKgglm2Yvzwlo8UoC5FNySQFCvl84uPaeADkqHUZUkWy4aH4xOA=="], + + "ohash": ["ohash@1.1.6", "", {}, "sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg=="], + + "open": ["open@10.1.2", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "is-wsl": "^3.1.0" } }, "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw=="], + + "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], + + "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], + + "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + + "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], + + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], + + "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], + + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + + "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], + + "ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="], + + "uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="], + + "undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="], + + "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], + + "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "zod": ["zod@4.1.9", "", {}, "sha512-HI32jTq0AUAC125z30E8bQNz0RQ+9Uc+4J7V97gLYjZVKRjeydPgGt6dvQzFrav7MYOUGFqqOGiHpA/fdbd0cQ=="], + + "@opencode-ai/plugin/zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="], + + "fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "giget/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "mlly/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "nypm/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + } +} diff --git a/.opencode/tool/gemini-multiple.ts b/.opencode/tool/gemini-multiple.ts new file mode 100644 index 0000000..f32e0e2 --- /dev/null +++ b/.opencode/tool/gemini-multiple.ts @@ -0,0 +1,109 @@ +import { tool } from "@opencode-ai/plugin" + +async function parseImageInput(input: string) { + // Accepts file path ("./img.png") or data URL ("data:image/png;base64,...") + if (input.startsWith("data:")) { + const base64 = input.split(",")[1] + const mime = input.substring(5, input.indexOf(";")) + return { mime, base64 } + } + // Treat as file path + const file = Bun.file(input) + const arr = await file.arrayBuffer() + const base64 = Buffer.from(arr).toString("base64") + // Best-effort mime + const mime = file.type || "image/png" + return { mime, base64 } +} + +async function callGeminiAPI(mime: string, base64: string, prompt: string) { + const apiKey = process.env.GEMINI_API_KEY + if (!apiKey) throw new Error("Set GEMINI_API_KEY in your environment") + + const res = await fetch( + "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image-preview:generateContent", + { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-goog-api-key": apiKey, + }, + body: JSON.stringify({ + inputs: [{ mimeType: mime, data: base64 }], + contents: [{ parts: [{ text: prompt }]}], + }), + } + ) + + if (!res.ok) throw new Error(`API error: ${await res.text()}`) + const json = await res.json() + const b64 = json?.candidates?.[0]?.content?.parts?.[0]?.inlineData?.data + if (!b64) throw new Error("No image data returned") + + return b64 +} + +export const edit = tool({ + description: "Edit an image using Gemini with file path or data URL", + args: { + image: tool.schema.string().describe("File path or data URL"), + prompt: tool.schema.string().describe("Edit instruction"), + output: tool.schema.string().optional().describe("Output filename (default edited.png)"), + }, + async execute(args, context) { + try { + const { mime, base64 } = await parseImageInput(args.image) + const resultBase64 = await callGeminiAPI(mime, base64, args.prompt) + + const out = args.output || "edited.png" + await Bun.write(out, Buffer.from(resultBase64, "base64")) + return `Saved edited image to ${out}` + } catch (error) { + return `Error: ${error.message}` + } + }, +}) + +export const analyze = tool({ + description: "Analyze an image using Gemini without editing", + args: { + image: tool.schema.string().describe("File path or data URL"), + question: tool.schema.string().describe("What to analyze about the image"), + }, + async execute(args, context) { + try { + const { mime, base64 } = await parseImageInput(args.image) + + const apiKey = process.env.GEMINI_API_KEY + if (!apiKey) return "Set GEMINI_API_KEY in your environment" + + const res = await fetch( + "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent", + { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-goog-api-key": apiKey, + }, + body: JSON.stringify({ + contents: [{ + parts: [ + { text: args.question }, + { inlineData: { mimeType: mime, data: base64 } } + ] + }], + }), + } + ) + + if (!res.ok) return `API error: ${await res.text()}` + const json = await res.json() + const text = json?.candidates?.[0]?.content?.parts?.[0]?.text + if (!text) return "No analysis returned" + + return text + } catch (error) { + return `Error: ${error.message}` + } + }, +}) \ No newline at end of file diff --git a/.opencode/tool/gemini.ts b/.opencode/tool/gemini.ts new file mode 100644 index 0000000..9dc87cf --- /dev/null +++ b/.opencode/tool/gemini.ts @@ -0,0 +1,55 @@ +import { tool } from "@opencode-ai/plugin/tool" + +async function parseImageInput(input: string) { + // Accepts file path ("./img.png") or data URL ("data:image/png;base64,...") + if (input.startsWith("data:")) { + const base64 = input.split(",")[1] + const mime = input.substring(5, input.indexOf(";")) + return { mime, base64 } + } + // Treat as file path + const file = Bun.file(input) + const arr = await file.arrayBuffer() + const base64 = Buffer.from(arr).toString("base64") + // Best-effort mime + const mime = file.type || "image/png" + return { mime, base64 } +} + +export default tool({ + description: "Edit an image using Gemini. Pass file path or data URL.", + args: { + image: tool.schema.string().describe("File path or data URL"), + prompt: tool.schema.string().describe("Edit instruction"), + output: tool.schema.string().optional().describe("Output filename (default edited.png)"), + }, + async execute(args, context) { + const apiKey = process.env.GEMINI_API_KEY + if (!apiKey) return "Set GEMINI_API_KEY in your environment" + + const { mime, base64 } = await parseImageInput(args.image) + const res = await fetch( + "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image-preview:generateContent", + { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-goog-api-key": apiKey, + }, + body: JSON.stringify({ + inputs: [{ mimeType: mime, data: base64 }], + contents: [{ parts: [{ text: args.prompt }]}], + }), + } + ) + + if (!res.ok) return `API error: ${await res.text()}` + const json = await res.json() + const b64 = json?.candidates?.[0]?.content?.parts?.[0]?.inlineData?.data + if (!b64) return "No image data returned" + + const out = args.output || "edited.png" + await Bun.write(out, Buffer.from(b64, "base64")) + return `Saved ${out}` + }, +}) \ No newline at end of file diff --git a/.opencode/tool/package.json b/.opencode/tool/package.json new file mode 100644 index 0000000..1c4f2f2 --- /dev/null +++ b/.opencode/tool/package.json @@ -0,0 +1,20 @@ +{ + "type": "module", + "name": "opencode-gemini-tool", + "version": "1.0.0", + "description": "Gemini image editing tool for OpenCode", + "main": "gemini.ts", + "scripts": { + "build": "bun build gemini.ts --outdir dist", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "@opencode-ai/sdk": "^0.10.0", + "zod": "^4.1.9" + }, + "devDependencies": { + "@opencode-ai/plugin": "^0.10.0", + "@types/node": "^24.2.1", + "bun-types": "latest" + } +} \ No newline at end of file diff --git a/.opencode/tool/test-plugin.ts b/.opencode/tool/test-plugin.ts new file mode 100644 index 0000000..60b1c2c --- /dev/null +++ b/.opencode/tool/test-plugin.ts @@ -0,0 +1,11 @@ +import * as plugin from "@opencode-ai/plugin" + +console.log("Available exports:", Object.keys(plugin)) + +// Try to import tool +try { + const { tool } = await import("@opencode-ai/plugin") + console.log("Tool function:", typeof tool) +} catch (e) { + console.log("Tool import error:", e.message) +} \ No newline at end of file diff --git a/.opencode/tool/tsconfig.json b/.opencode/tool/tsconfig.json new file mode 100644 index 0000000..dc2a13b --- /dev/null +++ b/.opencode/tool/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", + "resolvePackageJsonExports": true, + "resolvePackageJsonImports": true, + "target": "ES2022", + "skipLibCheck": true, + "types": ["bun-types"] + }, + "include": ["*.ts"], + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/env.example b/env.example index 16039ff..4d6ba21 100644 --- a/env.example +++ b/env.example @@ -18,3 +18,7 @@ TELEGRAM_CHECK_INTERVAL=30000 # Enable/disable the plugin (true/false) TELEGRAM_ENABLED=true + +# Gemini API Configuration +# Get your API key from https://makersuite.google.com/app/apikey +GEMINI_API_KEY=your_gemini_api_key_here