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

This commit is contained in:
darrenhinde 2025-09-23 13:31:36 +01:00
parent 25e6e919be
commit e073c5bb2e
14 changed files with 811 additions and 362 deletions

View file

@ -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.

View file

@ -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 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>
</step>
<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>
<step number="2" name="framework_application">
### Step 2: 10-Step Framework Integration
Map the enhanced framework elements to structured XML components.
<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>
<framework_mapping>
<task_context>
<expert_role>[specialized role definition]</expert_role>
<mission_objective>[primary goal statement]</mission_objective>
</task_context>
<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>
<operational_context>
<tone_framework>[communication style parameters]</tone_framework>
<audience_level>[technical sophistication requirements]</audience_level>
</operational_context>
<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>
<knowledge_requirements>
<background_expertise>[domain knowledge needed]</background_expertise>
<resource_dependencies>[external knowledge sources]</resource_dependencies>
</knowledge_requirements>
<quality_standards>
<task_rules>[operational constraints]</task_rules>
<success_metrics>[measurable criteria]</success_metrics>
</quality_standards>
<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]
## Analysis Results
- Current Structure Score: [X/10]
- Optimization Opportunities: [LIST]
- Expected Performance Gain: [X%]
## Overview
[CONCISE_MISSION_STATEMENT_WITH_SCOPE]
## Optimized Prompt Structure
<pre_flight_check>
[INITIAL_VALIDATION_REQUIREMENTS]
</pre_flight_check>
```xml
<context>
[HIERARCHICAL CONTEXT: system→domain→task→execution]
</context>
<process_flow>
<role>[AGENT IDENTITY - 5-10% of prompt]</role>
<step number="1" name="[STEP_NAME]">
### Step 1: [STEP_TITLE]
[DETAILED_STEP_DESCRIPTION]
<task>[PRIMARY OBJECTIVE]</task>
<[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]>
<instructions>
[WORKFLOW WITH ROUTING USING @ SYMBOLS]
</instructions>
<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>
[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
</output_template>
</step>
<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>
<step number="4" name="enhancement_analysis">
### Step 4: Framework Enhancement Documentation
Document how each 10-step element was systematically integrated.
<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>
</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
```
[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."
---
<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
.opencode/tool/env/index.ts vendored Normal file
View file

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

View file

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

View file

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

View file

@ -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
.opencode/tool/index.ts Normal file
View file

@ -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"

View file

@ -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",

View file

@ -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.

View file

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

View file

@ -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
assets/.gitignore vendored Normal file
View file

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

1
assets/images/.gitkeep Normal file
View file

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