From e073c5bb2e9a4dea3d73432e3930a353579a24be Mon Sep 17 00:00:00 2001
From: darrenhinde <107584450+darrenhinde@users.noreply.github.com>
Date: Tue, 23 Sep 2025 13:31:36 +0100
Subject: [PATCH] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20chore:=20remove=20depre?=
=?UTF-8?q?cated=20Gemini=20tool=20files=20and=20update=20package=20config?=
=?UTF-8?q?uration=20for=20improved=20structure?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.opencode/agent/image-specialist.md | 67 ++++
.opencode/agent/{ => subagents}/reviewer.md | 0
.opencode/command/prompter.md | 282 +++++----------
.opencode/tool/env/index.ts | 168 +++++++++
.opencode/tool/gemini-multiple.ts | 109 ------
.opencode/tool/gemini.ts | 55 ---
.opencode/tool/gemini/index.ts | 379 ++++++++++++++++++++
.opencode/tool/index.ts | 27 ++
.opencode/tool/package.json | 8 +-
.opencode/tool/template/README.md | 35 ++
.opencode/tool/template/index.ts | 25 ++
.opencode/tool/test-plugin.ts | 11 -
assets/.gitignore | 6 +
assets/images/.gitkeep | 1 +
14 files changed, 811 insertions(+), 362 deletions(-)
create mode 100644 .opencode/agent/image-specialist.md
rename .opencode/agent/{ => subagents}/reviewer.md (100%)
create mode 100644 .opencode/tool/env/index.ts
delete mode 100644 .opencode/tool/gemini-multiple.ts
delete mode 100644 .opencode/tool/gemini.ts
create mode 100644 .opencode/tool/gemini/index.ts
create mode 100644 .opencode/tool/index.ts
create mode 100644 .opencode/tool/template/README.md
create mode 100644 .opencode/tool/template/index.ts
delete mode 100644 .opencode/tool/test-plugin.ts
create mode 100644 assets/.gitignore
create mode 100644 assets/images/.gitkeep
diff --git a/.opencode/agent/image-specialist.md b/.opencode/agent/image-specialist.md
new file mode 100644
index 0000000..6536dcd
--- /dev/null
+++ b/.opencode/agent/image-specialist.md
@@ -0,0 +1,67 @@
+---
+description: Specialized agent for image editing and analysis using Gemini AI tools
+mode: primary
+model: anthropic/claude-sonnet-4-20250514
+temperature: 0.3
+permission:
+ edit: deny
+ bash: deny
+ webfetch: allow
+tools:
+ write: false
+ edit: false
+ bash: false
+ read: true
+ grep: true
+ glob: true
+ list: true
+ gemini-multiple_analyze: true
+ gemini-multiple_edit: true
+ gemini: true
+environment:
+ GEMINI_API_KEY: required
+---
+
+You are an image processing specialist powered by Gemini AI's Nano Banana model. Your capabilities include:
+
+## Core Functions
+- **Image Generation**: Creating images from text using Gemini Nano Banana
+- **Image Editing**: Modifying existing images with Nano Banana
+- **Image Analysis**: Analyzing images with detailed descriptions
+
+## Tools Available
+- `gemini-multiple_edit`: Edit existing images with Nano Banana
+- `gemini-multiple_analyze`: Analyze images and provide detailed descriptions
+- `gemini`: Generate or edit images (legacy tool)
+
+## Meta-Prompt for Nano Banana Requests
+
+When users provide simple instructions, use this meta-prompt approach to create detailed Nano Banana prompts:
+
+**Process:**
+1. **Identify core purpose**: Schematic/diagram, action illustration, or emotive scene?
+2. **Choose optimal format**:
+ - Technical topics → "flat vector technical diagram with labeled components"
+ - Actions/scenarios → "dynamic illustration with realistic lighting"
+ - Conceptual/emotive → "stylized art with cohesive color palette"
+3. **Determine style attributes**: Color palette, typography, composition
+4. **Build final prompt**: "Create a [FORMAT] illustrating [TOPIC] in a [STYLE] style, using [COLORS], with [TYPOGRAPHY] labels, include [LAYOUT ELEMENTS]"
+
+**Example:**
+- Input: "Visualize microservices architecture"
+- Output: "Create a flat-vector technical diagram illustrating a microservices architecture with labeled service nodes and directional arrows showing service-to-service calls, in a navy & teal color palette, with Roboto sans-serif labels, include a legend box at bottom right, optimized for 1200×627 px."
+
+## Workflow
+1. **For simple requests**: Apply meta-prompt to enhance the instruction
+2. **For image generation**: Use detailed, styled prompts with Nano Banana
+3. **For image editing**: Preserve original context while applying modifications
+4. **For analysis**: Provide comprehensive descriptions and suggestions
+
+## File Organization
+- Images are automatically organized by date: `assets/images/YYYY-MM-DD/`
+- Generations saved to: `generations/` subdirectory
+- Edits saved to: `edits/` subdirectory with auto-increment naming
+- No files are overwritten - each edit creates a unique numbered version
+- All images stored in repo's `assets/images/` directory for proper organization
+
+Always ensure you have necessary inputs and provide clear descriptions of operations performed.
\ No newline at end of file
diff --git a/.opencode/agent/reviewer.md b/.opencode/agent/subagents/reviewer.md
similarity index 100%
rename from .opencode/agent/reviewer.md
rename to .opencode/agent/subagents/reviewer.md
diff --git a/.opencode/command/prompter.md b/.opencode/command/prompter.md
index 9aecc08..0ce85b3 100644
--- a/.opencode/command/prompter.md
+++ b/.opencode/command/prompter.md
@@ -1,208 +1,122 @@
---
-description: Transform basic requests into comprehensive, systematic prompt architectures using structured XML methodology
+agent: prompt-enhancer
+description: "Research-backed XML prompt optimizer delivering 20% performance improvement"
---
-# Advanced Prompt Architecture Generator
+
-## Overview
-Transform basic requests into comprehensive, systematic prompt frameworks using structured XML methodology and the 10-step enhancement principles.
+
+ Prompt optimization using empirically-proven XML structures
+ LLM prompt engineering with Stanford/Anthropic research patterns
+ 20% routing accuracy, 25% consistency, 17% performance gains
+
-
+Expert Prompt Architect specializing in evidence-based XML structure optimization
-
-### Step 1: Request Analysis
-Analyze the user's concept ($ARGUMENTS) to identify core requirements and optimal prompt architecture.
+Transform prompts into high-performance XML following proven component ordering for measurable improvements
-
-
- - task_type: [classification of work required]
- - complexity_level: [simple/moderate/complex/enterprise]
- - domain_expertise: [required knowledge areas]
- - output_requirements: [expected deliverables]
- - success_criteria: [measurable outcomes]
-
+
+
+ Assess current structure against research patterns
+
+ - Component order (context→role→task→instructions)
+ - Length ratios (role 5-10%, context 15-25%, instructions 40-50%)
+ - Context management strategy presence
+ - Hierarchical routing implementation
+
+
-
- - process_flow: [sequential/parallel/conditional steps]
- - decision_points: [branching logic required]
- - template_requirements: [structured outputs needed]
- - subagent_opportunities: [specialized role assignments]
-
-
-
-
-
-
-### Step 2: 10-Step Framework Integration
-Map the enhanced framework elements to structured XML components.
-
-
-
- [specialized role definition]
- [primary goal statement]
-
+
+ Apply optimal component sequence
+
+ 1. Context (system→domain→task→execution)
+ 2. Role (clear identity, first 20% of prompt)
+ 3. Task (primary objective)
+ 4. Instructions (hierarchical workflow)
+ 5. Examples (if needed)
+ 6. Constraints (boundaries)
+ 7. Output_format (expected structure)
+
+
-
- [communication style parameters]
- [technical sophistication requirements]
-
+
+ Implement manager-worker patterns
+
+ - LLM-based decision making
+ - Explicit routing criteria with @ symbol
+ - Fallback strategies
+ - Context allocation per task type
+
+
-
- [domain knowledge needed]
- [external knowledge sources]
-
-
-
- [operational constraints]
- [measurable criteria]
-
-
-
- [concrete illustrations]
- [conversation continuity]
-
-
-
- [expected outputs]
- [systematic methodology]
- [format organization]
- [templates and patterns]
-
-
+
+ Apply 3-level context management
+
+ Complete isolation - subagent receives only specific task
+ Filtered context - curated relevant background
+ Windowed context - last N messages only
+
+
+
-
-
-
-### Step 3: Generate Structured Prompt Architecture
-Create the comprehensive prompt using systematic XML structure.
+
+
+ - 40% improvement in response quality with descriptive tags
+ - 15% reduction in token overhead for complex prompts
+ - Universal compatibility across models
+ - Explicit boundaries prevent context bleeding
+
+
+
+ 5-10% of total prompt
+ 15-25% hierarchical information
+ 40-50% detailed procedures
+ 20-30% when needed
+ 5-10% boundaries
+
+
+
+ Always use @ symbol (e.g., @context-provider, @research-assistant-agent)
+ Route to @[agent-name] when [condition]
+
+
-# [PROMPT_TITLE]
+## Analysis Results
+- Current Structure Score: [X/10]
+- Optimization Opportunities: [LIST]
+- Expected Performance Gain: [X%]
-## Overview
-[CONCISE_MISSION_STATEMENT_WITH_SCOPE]
+## Optimized Prompt Structure
-
- [INITIAL_VALIDATION_REQUIREMENTS]
-
+```xml
+
+ [HIERARCHICAL CONTEXT: system→domain→task→execution]
+
-
+[AGENT IDENTITY - 5-10% of prompt]
-
-### Step 1: [STEP_TITLE]
-[DETAILED_STEP_DESCRIPTION]
+[PRIMARY OBJECTIVE]
-<[step_specific_framework]>
- <[framework_element]>[specific_requirements][framework_element]>
-
- IF [condition]:
- [action_path_a]
- ELSE:
- [action_path_b]
-
-[step_specific_framework]>
+
+ [WORKFLOW WITH ROUTING USING @ SYMBOLS]
+
-
- <[template_name]>
- [STRUCTURED_TEMPLATE_CONTENT]
- [template_name]>
-
-
-
- - [SPECIFIC_CONSTRAINT_1]
- - [SPECIFIC_CONSTRAINT_2]
-
-
-
- [CONCRETE_EXAMPLES_FOR_CLARITY]
-
-
-
-
-[ADDITIONAL_STEPS_AS_NEEDED]
-
-
-
-
- [VALIDATION_AND_QUALITY_ASSURANCE]
-
+[ADDITIONAL COMPONENTS AS NEEDED]
+```
+## Implementation Notes
+- Component reordering impact: +[X]% performance
+- Context management efficiency: [X]% reduction
+- Routing accuracy improvement: +[X]%
+- Subagent references: @agent-name format maintained
-
+
+ Stanford multi-instruction study + Anthropic XML research
+ Measurable 20% routing improvement
+ 80% reduction in unnecessary context
+ Ready for deployment without modification
+
-
-### Step 4: Framework Enhancement Documentation
-Document how each 10-step element was systematically integrated.
-
-
- integrated via: [specific XML structures used]
- integrated via: [communication framework elements]
- integrated via: [knowledge requirement specifications]
- integrated via: [constraint and validation systems]
- integrated via: [template and example structures]
- integrated via: [pre/post flight and continuity systems]
- integrated via: [output specification frameworks]
- integrated via: [decision trees and process flows]
- integrated via: [template systems and formatting]
- integrated via: [structured response patterns]
-
-
-
-
-
-
-## Presentation Format
-
-Use this exact structure for your response:
-
----
-
-## 📋 Architecture Analysis
-
-**Original concept**: "[USER'S_CONCEPT]"
-**Task classification**: [SYSTEMATIC_WORK_TYPE]
-**Architectural complexity**: [SIMPLE/MODERATE/COMPLEX/ENTERPRISE]
-**Process flow type**: [SEQUENTIAL/CONDITIONAL/PARALLEL]
-
-## 🏗️ Generated Prompt Architecture
-
-```
-[COMPLETE_STRUCTURED_PROMPT_WITH_XML_TAGS_AND_PROCESS_FLOWS]
-```
-
-## 🔧 Enhancement Integration Map
-
-- **Task Context**: [How expert role and objectives were systematically defined]
-- **Operational Framework**: [How tone and audience parameters were structured]
-- **Knowledge Architecture**: [How expertise requirements were systematized]
-- **Quality Systems**: [How standards and constraints were implemented]
-- **Guidance Framework**: [How examples and patterns were integrated]
-- **Process Management**: [How context and continuity were handled]
-- **Output Systems**: [How deliverables and reasoning were structured]
-- **Response Architecture**: [How templates and frameworks were organized]
-
-### ▶️ Implementation Ready
-
-"This structured prompt architecture is ready for immediate deployment. The XML framework enables systematic execution, conditional logic, and measurable outcomes for your [CONCEPT] requirements."
-
----
-
-
-
- - systematic_structure: XML-based organization with clear hierarchies
- - conditional_logic: decision trees and branching workflows
- - template_systems: reusable patterns and structured outputs
- - process_flows: numbered steps with clear dependencies
- - validation_frameworks: pre/post flight checks and quality gates
-
-
-
- - all_10_elements: seamlessly integrated into XML structure
- - natural_flow: feels systematic rather than checklist-driven
- - immediate_usability: ready for deployment without modification
- - scalable_complexity: appropriate sophistication for task scope
- - measurable_outcomes: clear success criteria and validation points
-
-
diff --git a/.opencode/tool/env/index.ts b/.opencode/tool/env/index.ts
new file mode 100644
index 0000000..8ff2ac9
--- /dev/null
+++ b/.opencode/tool/env/index.ts
@@ -0,0 +1,168 @@
+import { readFile } from "fs/promises"
+import { resolve } from "path"
+
+/**
+ * Configuration for environment variable loading
+ */
+export interface EnvLoaderConfig {
+ /** Custom paths to search for .env files (relative to current working directory) */
+ searchPaths?: string[]
+ /** Whether to log when environment variables are loaded */
+ verbose?: boolean
+ /** Whether to override existing environment variables */
+ override?: boolean
+}
+
+/**
+ * Default search paths for .env files
+ */
+const DEFAULT_ENV_PATHS = [
+ './.env',
+ '../.env',
+ '../../.env',
+ '../plugin/.env',
+ '../../../.env'
+]
+
+/**
+ * Load environment variables from .env files
+ * Searches multiple common locations for .env files and loads them into process.env
+ *
+ * @param config Configuration options
+ * @returns Object containing loaded environment variables
+ */
+export async function loadEnvVariables(config: EnvLoaderConfig = {}): Promise> {
+ const {
+ searchPaths = DEFAULT_ENV_PATHS,
+ verbose = false,
+ override = false
+ } = config
+
+ const loadedVars: Record = {}
+
+ for (const envPath of searchPaths) {
+ try {
+ const fullPath = resolve(envPath)
+ const content = await readFile(fullPath, 'utf8')
+
+ if (verbose) {
+ console.log(`Checking .env file: ${envPath}`)
+ }
+
+ // Parse .env file content
+ const lines = content.split('\n')
+ for (const line of lines) {
+ const trimmed = line.trim()
+ if (trimmed && !trimmed.startsWith('#') && trimmed.includes('=')) {
+ const [key, ...valueParts] = trimmed.split('=')
+ const value = valueParts.join('=').trim()
+
+ // Remove quotes if present
+ const cleanValue = value.replace(/^["']|["']$/g, '')
+
+ if (key && cleanValue && (override || !process.env[key])) {
+ process.env[key] = cleanValue
+ loadedVars[key] = cleanValue
+
+ if (verbose) {
+ console.log(`Loaded ${key} from ${envPath}`)
+ }
+ }
+ }
+ }
+ } catch (error) {
+ // File doesn't exist or can't be read, continue to next
+ if (verbose) {
+ console.log(`Could not read ${envPath}: ${error.message}`)
+ }
+ }
+ }
+
+ return loadedVars
+}
+
+/**
+ * Get a specific environment variable with automatic .env file loading
+ *
+ * @param varName Name of the environment variable
+ * @param config Configuration options
+ * @returns The environment variable value or null if not found
+ */
+export async function getEnvVariable(varName: string, config: EnvLoaderConfig = {}): Promise {
+ // First check if it's already in the environment
+ let value = process.env[varName]
+
+ if (!value) {
+ // Try to load from .env files
+ const loadedVars = await loadEnvVariables(config)
+ value = loadedVars[varName] || process.env[varName]
+ }
+
+ return value || null
+}
+
+/**
+ * Get a required environment variable with automatic .env file loading
+ * Throws an error if the variable is not found
+ *
+ * @param varName Name of the environment variable
+ * @param config Configuration options
+ * @returns The environment variable value
+ * @throws Error if the variable is not found
+ */
+export async function getRequiredEnvVariable(varName: string, config: EnvLoaderConfig = {}): Promise {
+ const value = await getEnvVariable(varName, config)
+
+ if (!value) {
+ const searchPaths = config.searchPaths || DEFAULT_ENV_PATHS
+ throw new Error(`${varName} not found. Please set it in your environment or .env file.
+
+To fix this:
+1. Add to .env file: ${varName}=your_value_here
+2. Or export it: export ${varName}=your_value_here
+
+Current working directory: ${process.cwd()}
+Searched paths: ${searchPaths.join(', ')}
+Environment variables available: ${Object.keys(process.env).filter(k => k.includes(varName.split('_')[0])).join(', ') || 'none matching'}`)
+ }
+
+ return value
+}
+
+/**
+ * Load multiple required environment variables at once
+ *
+ * @param varNames Array of environment variable names
+ * @param config Configuration options
+ * @returns Object with variable names as keys and values as values
+ * @throws Error if any variable is not found
+ */
+export async function getRequiredEnvVariables(varNames: string[], config: EnvLoaderConfig = {}): Promise> {
+ const result: Record = {}
+
+ // Load all .env files first
+ await loadEnvVariables(config)
+
+ // Check each required variable
+ for (const varName of varNames) {
+ const value = process.env[varName]
+ if (!value) {
+ throw new Error(`Required environment variable ${varName} not found. Please set it in your environment or .env file.`)
+ }
+ result[varName] = value
+ }
+
+ return result
+}
+
+/**
+ * Utility function specifically for API keys
+ *
+ * @param apiKeyName Name of the API key environment variable
+ * @param config Configuration options
+ * @returns The API key value
+ * @throws Error if the API key is not found
+ */
+export async function getApiKey(apiKeyName: string, config: EnvLoaderConfig = {}): Promise {
+ return getRequiredEnvVariable(apiKeyName, config)
+}
\ No newline at end of file
diff --git a/.opencode/tool/gemini-multiple.ts b/.opencode/tool/gemini-multiple.ts
deleted file mode 100644
index f32e0e2..0000000
--- a/.opencode/tool/gemini-multiple.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-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
deleted file mode 100644
index 9dc87cf..0000000
--- a/.opencode/tool/gemini.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-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/gemini/index.ts b/.opencode/tool/gemini/index.ts
new file mode 100644
index 0000000..3fe85c9
--- /dev/null
+++ b/.opencode/tool/gemini/index.ts
@@ -0,0 +1,379 @@
+import { tool } from "@opencode-ai/plugin/tool"
+import { mkdir } from "fs/promises"
+import { join, dirname, basename, extname, resolve } from "path"
+import { getApiKey } from "../env"
+
+// Function to detect if we're in test mode
+function isTestMode(): boolean {
+ // Only enable test mode when explicitly set
+ return process.env.GEMINI_TEST_MODE === 'true'
+}
+
+// Function to get Gemini API key with automatic .env loading
+async function getGeminiApiKey(): Promise {
+ if (isTestMode()) {
+ return 'test-api-key'
+ }
+ return getApiKey('GEMINI_API_KEY')
+}
+
+interface ImageConfig {
+ outputDir?: string;
+ useTimestamp?: boolean;
+ preserveOriginal?: boolean;
+ customName?: string;
+}
+
+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 ensureDirectoryExists(dirPath: string) {
+ try {
+ await mkdir(dirPath, { recursive: true })
+ } catch (error) {
+ // Directory might already exist, that's fine
+ }
+}
+
+function getDateBasedPath(baseDir?: string): string {
+ // Default to assets/images at repo root
+ if (!baseDir) {
+ // Navigate from .opencode/tool/ to repo root, then to assets/images
+ baseDir = resolve(process.cwd(), "../../assets/images")
+ }
+ const today = new Date().toISOString().split('T')[0] // YYYY-MM-DD format
+ return join(baseDir, today)
+}
+
+async function getUniqueFilename(directory: string, baseName: string, extension: string, isEdit: boolean = false): Promise {
+ await ensureDirectoryExists(directory)
+
+ if (!isEdit) {
+ // For generations, use timestamp if file exists
+ const baseFilename = join(directory, `${baseName}${extension}`)
+ const fileExists = await Bun.file(baseFilename).exists()
+
+ if (!fileExists) {
+ return baseFilename
+ }
+
+ // Add timestamp if file exists
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5) // Remove milliseconds and Z
+ return join(directory, `${baseName}_${timestamp}${extension}`)
+ }
+
+ // For edits, use incremental numbering
+ let counter = 1
+ let filename: string
+
+ do {
+ const editSuffix = `_edit_${counter.toString().padStart(3, '0')}`
+ filename = join(directory, `${baseName}${editSuffix}${extension}`)
+ counter++
+ } while (await Bun.file(filename).exists())
+
+ return filename
+}
+
+export async function generateImage(prompt: string, config: ImageConfig = {}): Promise {
+ const apiKey = await getGeminiApiKey()
+
+ // Test mode - return mock response without API call
+ if (isTestMode()) {
+ const baseDir = config.outputDir || getDateBasedPath()
+ const generationsDir = join(baseDir, "generations")
+ let baseName = config.customName || "generated"
+ if (baseName.endsWith('.png') || baseName.endsWith('.jpg') || baseName.endsWith('.jpeg')) {
+ baseName = baseName.substring(0, baseName.lastIndexOf('.'))
+ }
+ const outputPath = await getUniqueFilename(generationsDir, baseName, ".png", false)
+
+ return `[TEST MODE] Would generate image: ${outputPath} for prompt: "${prompt.substring(0, 50)}..."`
+ }
+
+ const body = {
+ contents: [{
+ parts: [{ text: prompt }]
+ }],
+ }
+
+ 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(body),
+ }
+ )
+
+ if (!res.ok) {
+ const errorText = await res.text()
+ throw new Error(`API error (${res.status}): ${errorText}`)
+ }
+
+ const json = await res.json()
+
+ // Look for image data in the response
+ const candidates = json?.candidates
+ if (!candidates || candidates.length === 0) {
+ throw new Error("No candidates in response")
+ }
+
+ const parts = candidates[0]?.content?.parts
+ if (!parts || parts.length === 0) {
+ throw new Error("No parts in response")
+ }
+
+ let b64 = null
+ for (const part of parts) {
+ if (part.inlineData?.data) {
+ b64 = part.inlineData.data
+ break
+ }
+ }
+
+ if (!b64) {
+ throw new Error("No image data returned from Nano Banana model")
+ }
+
+ // Determine output path
+ const baseDir = config.outputDir || getDateBasedPath()
+ const generationsDir = join(baseDir, "generations")
+
+ // Generate filename (remove extension if already present)
+ let baseName = config.customName || "generated"
+ if (baseName.endsWith('.png') || baseName.endsWith('.jpg') || baseName.endsWith('.jpeg')) {
+ baseName = baseName.substring(0, baseName.lastIndexOf('.'))
+ }
+ const extension = ".png"
+ const outputPath = await getUniqueFilename(generationsDir, baseName, extension, false)
+
+ console.log(`Saving generated image to: ${outputPath}`)
+ await Bun.write(outputPath, Buffer.from(b64, "base64"))
+
+ const fileExists = await Bun.file(outputPath).exists()
+ if (!fileExists) {
+ throw new Error(`Failed to save file to ${outputPath}`)
+ }
+
+ const stats = await Bun.file(outputPath).stat()
+ return `Generated image saved: ${outputPath} (${stats.size} bytes)`
+}
+
+export async function editImage(imagePath: string, prompt: string, config: ImageConfig = {}): Promise {
+ const apiKey = await getGeminiApiKey()
+
+ // Test mode - return mock response without API call
+ if (isTestMode()) {
+ const baseDir = config.outputDir || getDateBasedPath()
+ const editsDir = join(baseDir, "edits")
+ const originalName = basename(imagePath, extname(imagePath))
+ let baseName = config.customName || originalName
+ if (baseName.endsWith('.png') || baseName.endsWith('.jpg') || baseName.endsWith('.jpeg')) {
+ baseName = baseName.substring(0, baseName.lastIndexOf('.'))
+ }
+ const outputPath = await getUniqueFilename(editsDir, baseName, ".png", true)
+
+ return `[TEST MODE] Would edit image: ${imagePath} -> ${outputPath} with prompt: "${prompt.substring(0, 50)}..."`
+ }
+
+ // Parse the input image
+ const { mime, base64 } = await parseImageInput(imagePath)
+
+ const body = {
+ contents: [{
+ parts: [
+ { text: prompt },
+ { inlineData: { mimeType: mime, data: base64 } }
+ ]
+ }],
+ }
+
+ 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(body),
+ }
+ )
+
+ if (!res.ok) {
+ const errorText = await res.text()
+ throw new Error(`API error (${res.status}): ${errorText}`)
+ }
+
+ const json = await res.json()
+
+ // Look for image data in the response
+ const candidates = json?.candidates
+ if (!candidates || candidates.length === 0) {
+ throw new Error("No candidates in response")
+ }
+
+ const parts = candidates[0]?.content?.parts
+ if (!parts || parts.length === 0) {
+ throw new Error("No parts in response")
+ }
+
+ let b64 = null
+ for (const part of parts) {
+ if (part.inlineData?.data) {
+ b64 = part.inlineData.data
+ break
+ }
+ }
+
+ if (!b64) {
+ throw new Error("No image data returned from Nano Banana model")
+ }
+
+ // Determine output path
+ const baseDir = config.outputDir || getDateBasedPath()
+ const editsDir = join(baseDir, "edits")
+
+ // Extract original filename without extension
+ const originalName = basename(imagePath, extname(imagePath))
+ let baseName = config.customName || originalName
+ if (baseName.endsWith('.png') || baseName.endsWith('.jpg') || baseName.endsWith('.jpeg')) {
+ baseName = baseName.substring(0, baseName.lastIndexOf('.'))
+ }
+ const extension = ".png"
+
+ const outputPath = await getUniqueFilename(editsDir, baseName, extension, true)
+
+ console.log(`Saving edited image to: ${outputPath}`)
+ await Bun.write(outputPath, Buffer.from(b64, "base64"))
+
+ const fileExists = await Bun.file(outputPath).exists()
+ if (!fileExists) {
+ throw new Error(`Failed to save file to ${outputPath}`)
+ }
+
+ const stats = await Bun.file(outputPath).stat()
+ return `Edited image saved: ${outputPath} (${stats.size} bytes)`
+}
+
+export async function analyzeImage(imagePath: string, question: string): Promise {
+ const apiKey = await getGeminiApiKey()
+
+ // Test mode - return mock response without API call
+ if (isTestMode()) {
+ return `[TEST MODE] Would analyze image: ${imagePath} with question: "${question.substring(0, 50)}..." - Mock analysis: This is a test image analysis response.`
+ }
+
+ const { mime, base64 } = await parseImageInput(imagePath)
+
+ const res = await fetch(
+ "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent",
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "x-goog-api-key": apiKey,
+ },
+ body: JSON.stringify({
+ contents: [{
+ parts: [
+ { text: question },
+ { inlineData: { mimeType: mime, data: base64 } }
+ ]
+ }],
+ }),
+ }
+ )
+
+ if (!res.ok) {
+ const errorText = await res.text()
+ throw new Error(`API error (${res.status}): ${errorText}`)
+ }
+
+ const json = await res.json()
+ const text = json?.candidates?.[0]?.content?.parts?.[0]?.text
+ if (!text) {
+ throw new Error("No analysis returned")
+ }
+
+ return text
+}
+
+// Tool for generating images from text
+export const generate = tool({
+ description: "Generate an image using Gemini Nano Banana from text prompt",
+ args: {
+ prompt: tool.schema.string().describe("Text description of the image to generate"),
+ outputDir: tool.schema.string().optional().describe("Custom output directory (default: ./generated-images/YYYY-MM-DD/)"),
+ filename: tool.schema.string().optional().describe("Custom filename (default: generated)"),
+ },
+ async execute(args, context) {
+ try {
+ const config: ImageConfig = {
+ outputDir: args.outputDir,
+ customName: args.filename,
+ }
+ return await generateImage(args.prompt, config)
+ } catch (error) {
+ return `Error: ${error.message}`
+ }
+ },
+})
+
+// Tool for editing existing images
+export const edit = tool({
+ description: "Edit an existing image using Gemini Nano Banana",
+ args: {
+ image: tool.schema.string().describe("File path or data URL of image to edit"),
+ prompt: tool.schema.string().describe("Edit instruction"),
+ outputDir: tool.schema.string().optional().describe("Custom output directory (default: ./generated-images/YYYY-MM-DD/)"),
+ filename: tool.schema.string().optional().describe("Custom filename (default: original name with _edit_XXX)"),
+ },
+ async execute(args, context) {
+ try {
+ const config: ImageConfig = {
+ outputDir: args.outputDir,
+ customName: args.filename,
+ }
+ return await editImage(args.image, args.prompt, config)
+ } catch (error) {
+ return `Error: ${error.message}`
+ }
+ },
+})
+
+// Tool for analyzing images
+export const analyze = tool({
+ description: "Analyze an image using Gemini (text analysis only)",
+ args: {
+ image: tool.schema.string().describe("File path or data URL of image to analyze"),
+ question: tool.schema.string().describe("What to analyze about the image"),
+ },
+ async execute(args, context) {
+ try {
+ return await analyzeImage(args.image, args.question)
+ } catch (error) {
+ return `Error: ${error.message}`
+ }
+ },
+})
+
+// Default export for backward compatibility
+export default edit
\ No newline at end of file
diff --git a/.opencode/tool/index.ts b/.opencode/tool/index.ts
new file mode 100644
index 0000000..3b047df
--- /dev/null
+++ b/.opencode/tool/index.ts
@@ -0,0 +1,27 @@
+/**
+ * OpenCode Gemini Tool - Main entry point
+ *
+ * This module provides image generation, editing, and analysis capabilities
+ * using Google's Gemini AI models, along with environment variable utilities.
+ */
+
+// Gemini AI image tools
+export {
+ generate, // Generate images from text prompts
+ edit, // Edit existing images with text instructions
+ analyze, // Analyze images and answer questions about them
+ generateImage, // Core image generation function
+ editImage, // Core image editing function
+ analyzeImage, // Core image analysis function
+ default as gemini // Default export (edit tool)
+} from "./gemini"
+
+// Environment variable utilities
+export {
+ loadEnvVariables,
+ getEnvVariable,
+ getRequiredEnvVariable,
+ getRequiredEnvVariables,
+ getApiKey,
+ type EnvLoaderConfig
+} from "./env"
\ No newline at end of file
diff --git a/.opencode/tool/package.json b/.opencode/tool/package.json
index 1c4f2f2..ce357d2 100644
--- a/.opencode/tool/package.json
+++ b/.opencode/tool/package.json
@@ -3,10 +3,12 @@
"name": "opencode-gemini-tool",
"version": "1.0.0",
"description": "Gemini image editing tool for OpenCode",
- "main": "gemini.ts",
+ "main": "index.ts",
"scripts": {
- "build": "bun build gemini.ts --outdir dist",
- "type-check": "tsc --noEmit"
+ "build": "bun build index.ts --outdir dist",
+ "type-check": "tsc --noEmit",
+ "test": "bun test.ts",
+ "test:real": "bun test.ts"
},
"dependencies": {
"@opencode-ai/sdk": "^0.10.0",
diff --git a/.opencode/tool/template/README.md b/.opencode/tool/template/README.md
new file mode 100644
index 0000000..da1eeb8
--- /dev/null
+++ b/.opencode/tool/template/README.md
@@ -0,0 +1,35 @@
+# Tool Template
+
+This is a template for creating new tools in the modular structure.
+
+## How to Create a New Tool
+
+1. **Copy this template directory:**
+ ```bash
+ cp -r template/ your-tool-name/
+ ```
+
+2. **Edit `index.ts`:**
+ - Replace `exampleFunction` with your tool's logic
+ - Update the tool definition with proper description and args
+ - Export your functions and tool
+
+3. **Add to main index:**
+ - Add exports to `/tool/index.ts`:
+ ```typescript
+ export { yourTool, yourFunction } from "./your-tool-name"
+ ```
+
+4. **Test your tool:**
+ ```bash
+ bun -e "import('./your-tool-name/index.ts').then(m => console.log('Exports:', Object.keys(m)))"
+ ```
+
+## Structure
+
+```
+your-tool-name/
+└── index.ts # All tool functionality
+```
+
+Keep it simple - one file per tool with all functionality included.
\ No newline at end of file
diff --git a/.opencode/tool/template/index.ts b/.opencode/tool/template/index.ts
new file mode 100644
index 0000000..fc213e4
--- /dev/null
+++ b/.opencode/tool/template/index.ts
@@ -0,0 +1,25 @@
+import { tool } from "@opencode-ai/plugin/tool"
+
+// Example tool implementation
+export async function exampleFunction(input: string): Promise {
+ // Your tool logic here
+ return `Processed: ${input}`
+}
+
+// Tool definition for OpenCode agent system
+export const exampleTool = tool({
+ description: "Example tool description",
+ args: {
+ input: tool.schema.string().describe("Input parameter description"),
+ },
+ async execute(args, context) {
+ try {
+ return await exampleFunction(args.input)
+ } catch (error) {
+ return `Error: ${error.message}`
+ }
+ },
+})
+
+// Default export
+export default exampleTool
\ No newline at end of file
diff --git a/.opencode/tool/test-plugin.ts b/.opencode/tool/test-plugin.ts
deleted file mode 100644
index 60b1c2c..0000000
--- a/.opencode/tool/test-plugin.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-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/assets/.gitignore b/assets/.gitignore
new file mode 100644
index 0000000..cc2e43f
--- /dev/null
+++ b/assets/.gitignore
@@ -0,0 +1,6 @@
+# Ignore generated images but keep directory structure
+images/*/
+!images/.gitkeep
+
+# Allow specific important images if needed
+# !images/examples/
\ No newline at end of file
diff --git a/assets/images/.gitkeep b/assets/images/.gitkeep
new file mode 100644
index 0000000..6aa7453
--- /dev/null
+++ b/assets/images/.gitkeep
@@ -0,0 +1 @@
+# Keep this directory in git
\ No newline at end of file