Browse Source

🗑️ chore: remove deprecated Gemini tool files and update package configuration for improved structure

darrenhinde 6 months ago
parent
commit
e073c5bb2e

+ 67 - 0
.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.

.opencode/agent/reviewer.md → .opencode/agent/subagents/reviewer.md


+ 97 - 183
.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
+<!-- RESEARCH-BACKED OPTIMAL SEQUENCE -->
 
-## Overview
-Transform basic requests into comprehensive, systematic prompt frameworks using structured XML methodology and the 10-step enhancement principles.
+<context>
+  <system_context>Prompt optimization using empirically-proven XML structures</system_context>
+  <domain_context>LLM prompt engineering with Stanford/Anthropic research patterns</domain_context>
+  <optimization_metrics>20% routing accuracy, 25% consistency, 17% performance gains</optimization_metrics>
+</context>
 
-<process_flow>
+<role>Expert Prompt Architect specializing in evidence-based XML structure optimization</role>
 
-<step number="1" name="request_analysis">
-### Step 1: Request Analysis
-Analyze the user's concept ($ARGUMENTS) to identify core requirements and optimal prompt architecture.
+<task>Transform prompts into high-performance XML following proven component ordering for measurable improvements</task>
 
-<analysis_framework>
-  <request_decomposition>
-    - 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]
-  </request_decomposition>
+<instructions>
+  <step id="1" name="analyze">
+    <action>Assess current structure against research patterns</action>
+    <checklist>
+      - Component order (context→role→task→instructions)
+      - Length ratios (role 5-10%, context 15-25%, instructions 40-50%)
+      - Context management strategy presence
+      - Hierarchical routing implementation
+    </checklist>
+  </step>
   
-  <architectural_needs>
-    - process_flow: [sequential/parallel/conditional steps]
-    - decision_points: [branching logic required]
-    - template_requirements: [structured outputs needed]
-    - subagent_opportunities: [specialized role assignments]
-  </architectural_needs>
-</analysis_framework>
-
-</step>
-
-<step number="2" name="framework_application">
-### Step 2: 10-Step Framework Integration
-Map the enhanced framework elements to structured XML components.
-
-<framework_mapping>
-  <task_context>
-    <expert_role>[specialized role definition]</expert_role>
-    <mission_objective>[primary goal statement]</mission_objective>
-  </task_context>
+  <step id="2" name="restructure">
+    <action>Apply optimal component sequence</action>
+    <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)
+    </sequence>
+  </step>
   
-  <operational_context>
-    <tone_framework>[communication style parameters]</tone_framework>
-    <audience_level>[technical sophistication requirements]</audience_level>
-  </operational_context>
+  <step id="3" name="enhance_routing">
+    <action>Implement manager-worker patterns</action>
+    <routing_logic>
+      - LLM-based decision making
+      - Explicit routing criteria with @ symbol
+      - Fallback strategies
+      - Context allocation per task type
+    </routing_logic>
+  </step>
   
-  <knowledge_requirements>
-    <background_expertise>[domain knowledge needed]</background_expertise>
-    <resource_dependencies>[external knowledge sources]</resource_dependencies>
-  </knowledge_requirements>
+  <step id="4" name="optimize_context">
+    <action>Apply 3-level context management</action>
+    <levels>
+      <level_1 usage="80%">Complete isolation - subagent receives only specific task</level_1>
+      <level_2 usage="20%">Filtered context - curated relevant background</level_2>
+      <level_3 usage="rare">Windowed context - last N messages only</level_3>
+    </levels>
+  </step>
+</instructions>
+
+<proven_patterns>
+  <xml_advantages>
+    - 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
+  </xml_advantages>
   
-  <quality_standards>
-    <task_rules>[operational constraints]</task_rules>
-    <success_metrics>[measurable criteria]</success_metrics>
-  </quality_standards>
+  <component_ratios>
+    <role>5-10% of total prompt</role>
+    <context>15-25% hierarchical information</context>
+    <instructions>40-50% detailed procedures</instructions>
+    <examples>20-30% when needed</examples>
+    <constraints>5-10% boundaries</constraints>
+  </component_ratios>
   
-  <guidance_systems>
-    <examples_patterns>[concrete illustrations]</examples_patterns>
-    <context_management>[conversation continuity]</context_management>
-  </guidance_systems>
-  
-  <output_specifications>
-    <immediate_deliverables>[expected outputs]</immediate_deliverables>
-    <reasoning_approach>[systematic methodology]</reasoning_approach>
-    <structure_requirements>[format organization]</structure_requirements>
-    <response_framework>[templates and patterns]</response_framework>
-  </output_specifications>
-</framework_mapping>
-
-</step>
-
-<step number="3" name="architecture_generation">
-### Step 3: Generate Structured Prompt Architecture
-Create the comprehensive prompt using systematic XML structure.
+  <routing_patterns>
+    <subagent_references>Always use @ symbol (e.g., @context-provider, @research-assistant-agent)</subagent_references>
+    <delegation_syntax>Route to @[agent-name] when [condition]</delegation_syntax>
+  </routing_patterns>
+</proven_patterns>
 
 <output_template>
-# [PROMPT_TITLE]
-
-## Overview
-[CONCISE_MISSION_STATEMENT_WITH_SCOPE]
-
-<pre_flight_check>
-  [INITIAL_VALIDATION_REQUIREMENTS]
-</pre_flight_check>
-
-<process_flow>
-
-<step number="1" name="[STEP_NAME]">
-### Step 1: [STEP_TITLE]
-[DETAILED_STEP_DESCRIPTION]
-
-<[step_specific_framework]>
-  <[framework_element]>[specific_requirements]</[framework_element]>
-  <decision_tree>
-    IF [condition]:
-      [action_path_a]
-    ELSE:
-      [action_path_b]
-  </decision_tree>
-</[step_specific_framework]>
+## Analysis Results
+- Current Structure Score: [X/10]
+- Optimization Opportunities: [LIST]
+- Expected Performance Gain: [X%]
 
-<templates>
-  <[template_name]>
-    [STRUCTURED_TEMPLATE_CONTENT]
-  </[template_name]>
-</templates>
-
-<constraints>
-  - [SPECIFIC_CONSTRAINT_1]
-  - [SPECIFIC_CONSTRAINT_2]
-</constraints>
-
-<examples>
-  [CONCRETE_EXAMPLES_FOR_CLARITY]
-</examples>
-
-</step>
-
-[ADDITIONAL_STEPS_AS_NEEDED]
-
-</process_flow>
-
-<post_flight_check>
-  [VALIDATION_AND_QUALITY_ASSURANCE]
-</post_flight_check>
-
-</output_template>
+## Optimized Prompt Structure
 
-</step>
+```xml
+<context>
+  [HIERARCHICAL CONTEXT: system→domain→task→execution]
+</context>
 
-<step number="4" name="enhancement_analysis">
-### Step 4: Framework Enhancement Documentation
-Document how each 10-step element was systematically integrated.
+<role>[AGENT IDENTITY - 5-10% of prompt]</role>
 
-<enhancement_mapping>
-  <task_context>integrated via: [specific XML structures used]</task_context>
-  <tone_context>integrated via: [communication framework elements]</tone_context>
-  <background_knowledge>integrated via: [knowledge requirement specifications]</background_knowledge>
-  <task_rules>integrated via: [constraint and validation systems]</task_rules>
-  <examples_patterns>integrated via: [template and example structures]</examples_patterns>
-  <context_management>integrated via: [pre/post flight and continuity systems]</context_management>
-  <immediate_deliverables>integrated via: [output specification frameworks]</immediate_deliverables>
-  <reasoning_approach>integrated via: [decision trees and process flows]</reasoning_approach>
-  <output_structure>integrated via: [template systems and formatting]</output_structure>
-  <response_framework>integrated via: [structured response patterns]</response_framework>
-</enhancement_mapping>
+<task>[PRIMARY OBJECTIVE]</task>
 
-</step>
-
-</process_flow>
-
-## 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
+<instructions>
+  [WORKFLOW WITH ROUTING USING @ SYMBOLS]
+</instructions>
 
+[ADDITIONAL COMPONENTS AS NEEDED]
 ```
-[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."
+## Implementation Notes
+- Component reordering impact: +[X]% performance
+- Context management efficiency: [X]% reduction
+- Routing accuracy improvement: +[X]%
+- Subagent references: @agent-name format maintained
+</output_template>
 
----
+<quality_principles>
+  <research_based>Stanford multi-instruction study + Anthropic XML research</research_based>
+  <performance_focused>Measurable 20% routing improvement</performance_focused>
+  <context_efficient>80% reduction in unnecessary context</context_efficient>
+  <immediate_usability>Ready for deployment without modification</immediate_usability>
+</quality_principles>
 
-<quality_standards>
-  <architectural_principles>
-    - 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
-  </architectural_principles>
-  
-  <enhancement_requirements>
-    - 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
-  </enhancement_requirements>
-</quality_standards>
 

+ 168 - 0
.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<Record<string, string>> {
+  const { 
+    searchPaths = DEFAULT_ENV_PATHS, 
+    verbose = false, 
+    override = false 
+  } = config
+  
+  const loadedVars: Record<string, string> = {}
+  
+  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<string | null> {
+  // 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<string> {
+  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<Record<string, string>> {
+  const result: Record<string, string> = {}
+  
+  // 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<string> {
+  return getRequiredEnvVariable(apiKeyName, config)
+}

+ 0 - 109
.opencode/tool/gemini-multiple.ts

@@ -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}`
-    }
-  },
-})

+ 0 - 55
.opencode/tool/gemini.ts

@@ -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}`
-  },
-})

+ 379 - 0
.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<string> {
+  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<string> {
+  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<string> {
+  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<string> {
+  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<string> {
+  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

+ 27 - 0
.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"

+ 5 - 3
.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",

+ 35 - 0
.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.

+ 25 - 0
.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<string> {
+  // 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

+ 0 - 11
.opencode/tool/test-plugin.ts

@@ -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)
-}

+ 6 - 0
assets/.gitignore

@@ -0,0 +1,6 @@
+# Ignore generated images but keep directory structure
+images/*/
+!images/.gitkeep
+
+# Allow specific important images if needed
+# !images/examples/

+ 1 - 0
assets/images/.gitkeep

@@ -0,0 +1 @@
+# Keep this directory in git