🗑️ chore: remove deprecated Gemini tool files and update package configuration for improved structure
This commit is contained in:
parent
25e6e919be
commit
e073c5bb2e
14 changed files with 811 additions and 362 deletions
67
.opencode/agent/image-specialist.md
Normal file
67
.opencode/agent/image-specialist.md
Normal 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.
|
||||||
|
|
@ -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
|
<context>
|
||||||
Transform basic requests into comprehensive, systematic prompt frameworks using structured XML methodology and the 10-step enhancement principles.
|
<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">
|
<task>Transform prompts into high-performance XML following proven component ordering for measurable improvements</task>
|
||||||
### Step 1: Request Analysis
|
|
||||||
Analyze the user's concept ($ARGUMENTS) to identify core requirements and optimal prompt architecture.
|
|
||||||
|
|
||||||
<analysis_framework>
|
<instructions>
|
||||||
<request_decomposition>
|
<step id="1" name="analyze">
|
||||||
- task_type: [classification of work required]
|
<action>Assess current structure against research patterns</action>
|
||||||
- complexity_level: [simple/moderate/complex/enterprise]
|
<checklist>
|
||||||
- domain_expertise: [required knowledge areas]
|
- Component order (context→role→task→instructions)
|
||||||
- output_requirements: [expected deliverables]
|
- Length ratios (role 5-10%, context 15-25%, instructions 40-50%)
|
||||||
- success_criteria: [measurable outcomes]
|
- Context management strategy presence
|
||||||
</request_decomposition>
|
- Hierarchical routing implementation
|
||||||
|
</checklist>
|
||||||
|
</step>
|
||||||
|
|
||||||
<architectural_needs>
|
<step id="2" name="restructure">
|
||||||
- process_flow: [sequential/parallel/conditional steps]
|
<action>Apply optimal component sequence</action>
|
||||||
- decision_points: [branching logic required]
|
<sequence>
|
||||||
- template_requirements: [structured outputs needed]
|
1. Context (system→domain→task→execution)
|
||||||
- subagent_opportunities: [specialized role assignments]
|
2. Role (clear identity, first 20% of prompt)
|
||||||
</architectural_needs>
|
3. Task (primary objective)
|
||||||
</analysis_framework>
|
4. Instructions (hierarchical workflow)
|
||||||
|
5. Examples (if needed)
|
||||||
</step>
|
6. Constraints (boundaries)
|
||||||
|
7. Output_format (expected structure)
|
||||||
<step number="2" name="framework_application">
|
</sequence>
|
||||||
### Step 2: 10-Step Framework Integration
|
</step>
|
||||||
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>
|
|
||||||
|
|
||||||
<operational_context>
|
<step id="3" name="enhance_routing">
|
||||||
<tone_framework>[communication style parameters]</tone_framework>
|
<action>Implement manager-worker patterns</action>
|
||||||
<audience_level>[technical sophistication requirements]</audience_level>
|
<routing_logic>
|
||||||
</operational_context>
|
- LLM-based decision making
|
||||||
|
- Explicit routing criteria with @ symbol
|
||||||
|
- Fallback strategies
|
||||||
|
- Context allocation per task type
|
||||||
|
</routing_logic>
|
||||||
|
</step>
|
||||||
|
|
||||||
<knowledge_requirements>
|
<step id="4" name="optimize_context">
|
||||||
<background_expertise>[domain knowledge needed]</background_expertise>
|
<action>Apply 3-level context management</action>
|
||||||
<resource_dependencies>[external knowledge sources]</resource_dependencies>
|
<levels>
|
||||||
</knowledge_requirements>
|
<level_1 usage="80%">Complete isolation - subagent receives only specific task</level_1>
|
||||||
|
<level_2 usage="20%">Filtered context - curated relevant background</level_2>
|
||||||
<quality_standards>
|
<level_3 usage="rare">Windowed context - last N messages only</level_3>
|
||||||
<task_rules>[operational constraints]</task_rules>
|
</levels>
|
||||||
<success_metrics>[measurable criteria]</success_metrics>
|
</step>
|
||||||
</quality_standards>
|
</instructions>
|
||||||
|
|
||||||
<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>
|
<proven_patterns>
|
||||||
|
<xml_advantages>
|
||||||
<step number="3" name="architecture_generation">
|
- 40% improvement in response quality with descriptive tags
|
||||||
### Step 3: Generate Structured Prompt Architecture
|
- 15% reduction in token overhead for complex prompts
|
||||||
Create the comprehensive prompt using systematic XML structure.
|
- Universal compatibility across models
|
||||||
|
- Explicit boundaries prevent context bleeding
|
||||||
|
</xml_advantages>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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>
|
<output_template>
|
||||||
# [PROMPT_TITLE]
|
## Analysis Results
|
||||||
|
- Current Structure Score: [X/10]
|
||||||
|
- Optimization Opportunities: [LIST]
|
||||||
|
- Expected Performance Gain: [X%]
|
||||||
|
|
||||||
## Overview
|
## Optimized Prompt Structure
|
||||||
[CONCISE_MISSION_STATEMENT_WITH_SCOPE]
|
|
||||||
|
|
||||||
<pre_flight_check>
|
```xml
|
||||||
[INITIAL_VALIDATION_REQUIREMENTS]
|
<context>
|
||||||
</pre_flight_check>
|
[HIERARCHICAL CONTEXT: system→domain→task→execution]
|
||||||
|
</context>
|
||||||
|
|
||||||
<process_flow>
|
<role>[AGENT IDENTITY - 5-10% of prompt]</role>
|
||||||
|
|
||||||
<step number="1" name="[STEP_NAME]">
|
<task>[PRIMARY OBJECTIVE]</task>
|
||||||
### Step 1: [STEP_TITLE]
|
|
||||||
[DETAILED_STEP_DESCRIPTION]
|
|
||||||
|
|
||||||
<[step_specific_framework]>
|
<instructions>
|
||||||
<[framework_element]>[specific_requirements]</[framework_element]>
|
[WORKFLOW WITH ROUTING USING @ SYMBOLS]
|
||||||
<decision_tree>
|
</instructions>
|
||||||
IF [condition]:
|
|
||||||
[action_path_a]
|
|
||||||
ELSE:
|
|
||||||
[action_path_b]
|
|
||||||
</decision_tree>
|
|
||||||
</[step_specific_framework]>
|
|
||||||
|
|
||||||
<templates>
|
[ADDITIONAL COMPONENTS AS NEEDED]
|
||||||
<[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>
|
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
- Component reordering impact: +[X]% performance
|
||||||
|
- Context management efficiency: [X]% reduction
|
||||||
|
- Routing accuracy improvement: +[X]%
|
||||||
|
- Subagent references: @agent-name format maintained
|
||||||
</output_template>
|
</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
168
.opencode/tool/env/index.ts
vendored
Normal 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)
|
||||||
|
}
|
||||||
|
|
@ -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}`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
@ -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
.opencode/tool/gemini/index.ts
Normal file
379
.opencode/tool/gemini/index.ts
Normal 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
27
.opencode/tool/index.ts
Normal 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"
|
||||||
|
|
@ -3,10 +3,12 @@
|
||||||
"name": "opencode-gemini-tool",
|
"name": "opencode-gemini-tool",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Gemini image editing tool for OpenCode",
|
"description": "Gemini image editing tool for OpenCode",
|
||||||
"main": "gemini.ts",
|
"main": "index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "bun build gemini.ts --outdir dist",
|
"build": "bun build index.ts --outdir dist",
|
||||||
"type-check": "tsc --noEmit"
|
"type-check": "tsc --noEmit",
|
||||||
|
"test": "bun test.ts",
|
||||||
|
"test:real": "bun test.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "^0.10.0",
|
"@opencode-ai/sdk": "^0.10.0",
|
||||||
|
|
|
||||||
35
.opencode/tool/template/README.md
Normal file
35
.opencode/tool/template/README.md
Normal 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.
|
||||||
25
.opencode/tool/template/index.ts
Normal file
25
.opencode/tool/template/index.ts
Normal 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
|
||||||
|
|
@ -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
6
assets/.gitignore
vendored
Normal 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
1
assets/images/.gitkeep
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# Keep this directory in git
|
||||||
Loading…
Reference in a new issue