diff --git a/.opencode/AGENT-SYSTEM-BLUEPRINT.md b/.opencode/AGENT-SYSTEM-BLUEPRINT.md new file mode 100644 index 0000000..0d7770b --- /dev/null +++ b/.opencode/AGENT-SYSTEM-BLUEPRINT.md @@ -0,0 +1,760 @@ +# The OpenCode Agent System Blueprint + +_Build Intelligent Workflow Systems with Context-Aware AI_ + +## The Golden Rule + +**Context flows in one direction: Commands load context immediately, Agents can look up additional context deterministically.** + +Like a well-organized library: the librarian (command) brings you the right books immediately, but you (agent) can look up specific references when needed. + +## How @ Symbol Context Loading Works (FUNDAMENTAL) + +### The Magic of Automatic Context Injection + +When you create a slash command with @ references, OpenCode automatically loads that context into the agent's memory BEFORE the agent starts thinking: + +```markdown +# .opencode/command/create-component.md + +--- + +name: create-component +agent: simple-coder + +--- + +You are creating React components following our patterns. + +**Request:** $ARGUMENTS + +@.opencode/context/frontend/react-patterns.md +@.opencode/context/styling/design-system.md +@.opencode/context/core/validation-patterns.md + +Create the component now. +``` + +**What happens when user types `/create-component "user profile card"`:** + +1. ✅ OpenCode reads the command file +2. ✅ **Automatically loads** react-patterns.md, design-system.md, validation-patterns.md +3. ✅ **Injects all context** into the agent's working memory +4. ✅ Agent receives: user request + all loaded context + instructions +5. ✅ Agent can immediately use patterns without looking them up + +### Why This Mechanism Is Powerful + +**❌ Without @ loading:** + +``` +Agent: "I need to create a component but don't know your patterns. Let me search..." +→ Agent uses generic patterns or has to look up files +→ Inconsistent results, slower execution +``` + +**✅ With @ loading:** + +``` +Agent: "I have your React patterns, design system, and validation rules loaded. Creating component..." +→ Agent immediately follows your exact patterns +→ Consistent, fast, high-quality results +``` + +### Context Loading Strategy + +**Base Context (Always Load):** + +```markdown +@.opencode/context/core/essential-patterns.md +@.opencode/context/architecture/project-structure.md +``` + +**Domain-Specific Context (Load Based on Command Purpose):** + +```markdown +# Frontend commands + +@.opencode/context/frontend/react-patterns.md +@.opencode/context/styling/design-system.md + +# Backend commands + +@.opencode/context/backend/server-patterns.md +@.opencode/context/database/query-patterns.md + +# Testing commands + +@.opencode/context/testing/test-patterns.md +``` + +### Dynamic Context Loading + +```markdown +# Advanced: Conditional context based on request analysis + +@.opencode/context/core/essential-patterns.md +!`if echo "$ARGUMENTS" | grep -i "component\|ui" > /dev/null; then echo "@.opencode/context/frontend/react-patterns.md"; fi` +!`if echo "$ARGUMENTS" | grep -i "database\|query" > /dev/null; then echo "@.opencode/context/database/query-patterns.md"; fi` +``` + +### Best Practices for @ Context Loading + +1. **Load 2-4 context files maximum** (prevent cognitive overload) +2. **Always include core patterns** (essential-patterns.md) +3. **Load domain-specific patterns** based on command purpose +4. **Keep context files focused** (50-150 lines each) +5. **Use conditional loading** for dynamic context selection + +### Why This Architecture Matters + +Understanding @ symbol auto-loading is crucial because: + +- **It's the foundation** of how agents get consistent context +- **It determines command design** - you must anticipate what context agents need +- **It explains the architecture** - why commands are "smart loaders" and agents are "focused executors" +- **It guides best practices** - load the right context, not too much, not too little + +**This mechanism is what makes the system "intelligent" - agents always start with the right knowledge instead of having to discover it.** + +## Core Principles + +### 1. Single-Level Context Loading + +**OpenCode processes `@` references only in command templates, NOT recursively in file contents.** + +```markdown +# ✅ This works (in command template) + +@.opencode/context/frontend/patterns.md +@.opencode/context/styling/design-system.md + +# ❌ This doesn't work (inside patterns.md file) + +# If patterns.md contains @other-file.md, it's treated as plain text +``` + +**Critical Implication**: Commands must load ALL necessary context upfront using @ references. Agents can look up additional files using tools (read, grep, glob), but cannot use @ loading themselves. + +### 2. Deterministic vs Non-Deterministic Behavior + +- **Commands**: Non-deterministic - analyze requests and load appropriate context +- **Agents**: Deterministic - predictable behavior, can look up additional context + +### 3. Context Optimization + +- **Maximum 4 context files per command** (250-450 lines total) +- **50-150 lines per context file** (optimal range) +- **Always load core patterns** + request-specific context + +## The 5-Part Intelligent System + +### 1. Commands (.opencode/command/) + +**What:** Entry points that load immediate context based on request analysis +**Rule:** Commands are non-deterministic - they adapt context to the request + +``` +/workflow → Intelligent routing with dynamic context +/plan-task → Task planning with architecture context +/execute-task → Step execution with progress tracking +/create-frontend-component → UI development with styling + frontend context +/create-backend-operation → Server logic with backend + database context +/create-database-operation → Database queries with focused database context +/review-completion → Quality assurance with minimal context +/validate-with-tests → Automated testing with testing context +``` + +**Command Structure Pattern:** + +```markdown +--- +name: command-name +agent: target-agent +description: "What this command does" +--- + +You are [doing specific task]. [Direct instructions to agent]. + +**Request:** $ARGUMENTS + +**Your Instructions:** + +1. **ANALYZE** the request... +2. **IMPLEMENT** following patterns... +3. **VALIDATE** the results... + +**Context Loaded:** +@.opencode/context/core/essential-patterns.md +@.opencode/context/[domain]/[specific-patterns].md +@.opencode/context/architecture/architecture-lite.md + +**Success Criteria:** + +- [Specific measurable outcomes] + +Execute this [task type] now. +``` + +### 2. Agents (.opencode/agent/) + +**What:** Deterministic AI workers with specific capabilities +**Rule:** Agents can look up additional context but are predictable in behavior + +``` +Primary Agents: +├── workflow-orchestrator.md → Routes requests and analyzes complexity +├── task-planner.md → Creates detailed task plans with file tracking +├── task-executor.md → Executes planned steps systematically +├── post-flight-reviewer.md → Reviews completed work for compliance +└── simple-coder.md → Implements focused development tasks + +Subagents (Specialized): +├── subagents/code-reviewer.md → Security and quality reviews (read-only) +├── subagents/test-writer.md → Test creation and validation (reports issues) +├── subagents/pattern-finder.md → Discovers existing implementations +└── subagents/doc-writer.md → Documentation creation and updates +``` + +**Agent Structure Pattern:** + +```markdown +--- +description: "What this agent does" +mode: primary|subagent +model: claude-4-sonnet +temperature: 0.1-0.3 +tools: + read: true|false + edit: true|false + write: true|false + bash: true|false + task: true|false +permissions: + edit: + "**/*.env*": "deny" + "**/*.secret": "deny" +--- + +You are [specific role]. [Direct instructions for behavior]. + +**EXECUTE** this [process type] for every [task type]: + +**1. [ACTION]** the [subject]: + +- [Specific instruction 1] +- [Specific instruction 2] + +**2. [ACTION]** the [subject]: + +- [Specific instruction 1] +- [Specific instruction 2] + +**RULES**: + +- **ALWAYS** [critical requirement] +- **NEVER** [forbidden action] +- **ONLY** [scope limitation] + +Execute [task type] now. +``` + +### 3. Context (.opencode/context/) + +**What:** Layered knowledge system with focused domains +**Rule:** Single-level loading - no recursive context references + +``` +context/ +├── core/ +│ └── essential-patterns.md (76 lines - always loaded) +├── architecture/ +│ └── architecture-lite.md (74 lines - project structure) +├── frontend/ +│ ├── frontend-patterns.md (230 lines - React patterns) +│ └── styling/design-system.md (122 lines - UI standards) +├── backend/ +│ ├── backend-patterns.md (326 lines → split into focused files) +│ └── server-actions.md (100 lines - focused server action patterns) +├── database/ +│ └── database-queries.md (207 lines - query patterns) +├── security/ +│ └── security-patterns.md (205 lines - auth patterns) +├── testing/ +│ ├── testing-patterns.md (322 lines → split) +│ └── unit-testing.md (100 lines - focused test patterns) +└── debugging/ + └── common-errors.md (304 lines → split by error type) +``` + +**Context File Structure Pattern:** + +````markdown +**[ACTION]** [subject] using these exact patterns: + +**[PATTERN NAME]** - [When to use]: + +```[language] +// Example code with clear comments +// Showing exactly what to do +``` +```` + +**[ANOTHER PATTERN]** - [When to use]: + +```[language] +// Another example +``` + +**RULES**: + +- **ALWAYS** [critical requirement] +- **NEVER** [forbidden action] +- **USE** [specific tools/methods] + +``` + +### 4. Task Management (tasks/) +**What:** File-based progress tracking with checkbox systems +**Rule:** Every complex task gets a plan file with step-by-step tracking + +``` + +tasks/ +├── features/ # Feature development +│ └── [feature-name]/ +│ ├── task-plan.md # Main plan with checkboxes +│ ├── execution-log.md # Detailed progress log +│ └── review-report.md # Post-flight review +├── fixes/ # Bug fixes +│ └── [issue-name]/ +│ ├── task-plan.md +│ └── fix-report.md +├── improvements/ # Code improvements +│ └── [improvement-name]/ +└── single/ # Simple tasks +└── [task-name].md + +```` + +**Task Plan File Structure:** +```markdown +# Task: [Task Name] + +## Overview +**Request**: [Original user request] +**Complexity**: [Simple/Medium/Complex] +**Estimated Duration**: [Time estimate] +**Dependencies**: [List any blocking requirements] + +## Task Breakdown + +### Phase 1: [Phase Name] +- [ ] **Step 1.1**: [Specific action] + - **Agent**: @[agent-name] + - **Context**: [Required context files] + - **Validation**: [How to verify completion] + - **Duration**: [Time estimate] + +- [ ] **Step 1.2**: [Specific action] + - **Agent**: @[agent-name] + - **Context**: [Required context files] + - **Validation**: [How to verify completion] + - **Duration**: [Time estimate] + +## Quality Gates +- [ ] **Build Validation**: TypeScript compilation passes +- [ ] **Code Review**: Security and quality review completed +- [ ] **Testing**: All tests pass and coverage adequate +- [ ] **Integration**: Feature works end-to-end + +## Acceptance Criteria +- [ ] [Specific requirement 1] +- [ ] [Specific requirement 2] + +## Progress Tracking +**Started**: [Date/Time] +**Last Updated**: [Date/Time] +**Status**: [Planning/In Progress/Review/Complete] +**Completed Steps**: 0/[Total Steps] +```` + +### 5. Workflow Orchestration + +**What:** Multi-agent coordination with quality gates +**Rule:** Complex workflows use multiple agents with validation checkpoints + +``` +Workflow Types: +├── Simple (< 30 min) → Direct execution with focused context +├── Medium (30min-2hrs) → Task planning with step tracking +└── Complex (> 2hrs) → Multi-phase with quality gates + +Quality Gates: +├── Build Validation → TypeScript, linting, build checks +├── Code Review → Security and quality assessment +├── Testing → Automated test execution +└── Post-Flight Review → Instruction compliance verification +``` + +## System Flow (Intelligent Workflow Management) + +```mermaid +flowchart TD + A[User Request] --> B[Workflow Orchestrator] + B --> C{Analyze Request} + C --> D{Complexity?} + D -->|Simple| E[Direct Execution] + D -->|Complex| F[Task Planning] + + E --> G[Load Focused Context] + G --> H[Execute with Agent] + H --> I[Validate Results] + I --> J[Complete] + + F --> K[Create Task Plan File] + K --> L[Step-by-Step Execution] + L --> M[Update Progress Tracking] + M --> N{More Steps?} + N -->|Yes| O[Next Step] + N -->|No| P[Quality Gates] + O --> L + P --> Q[Post-Flight Review] + Q --> R[Mark Complete] + + style A fill:#E6F3FF + style J fill:#E6FFE6 + style R fill:#E6FFE6 +``` + +## Context Loading Strategy + +### Dynamic Context Loading Pattern + +```markdown +# In workflow orchestrator + +**ANALYZE** request: "$ARGUMENTS" + +**BASE CONTEXT** (always loaded): +@.opencode/context/core/essential-patterns.md +@.opencode/context/architecture/architecture-lite.md + +**CONDITIONAL CONTEXT** (based on request analysis): +!`if echo "$ARGUMENTS" | grep -i -E "(component|ui|frontend)" > /dev/null; then echo "@.opencode/context/styling/design-system.md"; fi` +!`if echo "$ARGUMENTS" | grep -i -E "(server|backend|action)" > /dev/null; then echo "@.opencode/context/backend/server-actions.md"; fi` +!`if echo "$ARGUMENTS" | grep -i -E "(database|query|data)" > /dev/null; then echo "@.opencode/context/database/database-queries.md"; fi` +``` + +### Context Size Guidelines + +- **✅ Optimal**: 50-150 lines (focused, actionable patterns) +- **⚠️ Acceptable**: 150-250 lines (comprehensive but manageable) +- **❌ Too Large**: 250+ lines (should be split into focused files) + +### Context Loading Rules + +1. **Always load core patterns** (essential-patterns.md + architecture-lite.md) +2. **Maximum 4 context files** per command (prevent overload) +3. **Load based on request analysis** (dynamic, not static) +4. **Use bash commands** for conditional loading + +## Implementation Guide + +### Step 1: Create Your Workflow Commands + +```markdown +# Essential Commands to Create: + +/workflow # Main entry with intelligent routing +/plan-task # Complex task breakdown +/execute-task # Step-by-step execution +/create-frontend-component # UI development +/create-backend-operation # Server logic +/review-completion # Quality assurance +``` + +### Step 2: Build Your Agent System + +```markdown +# Core Agents Needed: + +workflow-orchestrator.md # Request analysis and routing +task-planner.md # Detailed planning with file tracking +task-executor.md # Step execution with progress updates +post-flight-reviewer.md # Compliance and quality review +simple-coder.md # Focused implementation work + +# Specialized Subagents: + +subagents/code-reviewer.md # Security and quality (read-only) +subagents/test-writer.md # Test creation (reports issues) +``` + +### Step 3: Structure Your Context + +```markdown +# Context Organization: + +core/essential-patterns.md # Always loaded (76 lines) +architecture/architecture-lite.md # Project structure (74 lines) +frontend/frontend-patterns.md # React patterns (230 lines) +styling/design-system.md # UI standards (122 lines) +backend/server-actions.md # Server patterns (100 lines) +database/database-queries.md # Query patterns (207 lines) +``` + +### Step 4: Implement Task Management + +```markdown +# Task File Structure: + +tasks/features/[name]/task-plan.md # Main planning file +tasks/features/[name]/review-report.md # Post-completion review +tasks/fixes/[name]/task-plan.md # Bug fix planning +tasks/single/[name].md # Simple tasks +``` + +### Step 5: Add Quality Gates + +```markdown +# Automated Validation: + +!`pnpm tsc --noEmit` # TypeScript check +!`pnpm lint` # Code quality +!`pnpm build` # Build validation +!`pnpm test` # Test execution +``` + +## Best Practices + +### Context Management + +1. **Keep context files focused** (50-150 lines) +2. **Use single-level loading** (no recursive `@` references) +3. **Load dynamically** based on request analysis +4. **Always include core patterns** + architecture + +### Agent Design + +1. **Make agents deterministic** (predictable behavior) +2. **Give clear, direct instructions** (not documentation) +3. **Separate concerns** (one agent, one responsibility) +4. **Use structured response formats** + +### Task Management + +1. **Break complex work into steps** (15-30 minutes each) +2. **Use checkbox tracking** for progress visibility +3. **Include validation criteria** for each step +4. **Implement quality gates** at key milestones + +### Workflow Orchestration + +1. **Analyze before routing** (complexity and domain) +2. **Use appropriate workflows** (simple vs complex) +3. **Coordinate multiple agents** for complex tasks +4. **Validate at every step** (build, test, review) + +## System Architecture + +```mermaid +graph TB + subgraph "User Interface" + CMD["/workflow
/plan-task
/execute-task"] + end + + subgraph ".opencode/command/" + C1[workflow.md] + C2[plan-task.md] + C3[execute-task.md] + C4[create-frontend-component.md] + C5[create-backend-operation.md] + end + + subgraph ".opencode/agent/" + A1[workflow-orchestrator.md] + A2[task-planner.md] + A3[task-executor.md] + A4[simple-coder.md] + A5[post-flight-reviewer.md] + end + + subgraph ".opencode/context/" + CTX1[core/essential-patterns.md] + CTX2[architecture/architecture-lite.md] + CTX3[frontend/frontend-patterns.md] + CTX4[backend/server-actions.md] + CTX5[database/database-queries.md] + end + + subgraph "tasks/" + T1[features/[name]/task-plan.md] + T2[fixes/[name]/task-plan.md] + T3[single/[name].md] + end + + CMD --> C1 + CMD --> C2 + CMD --> C3 + + C1 --> A1 + C2 --> A2 + C3 --> A3 + C4 --> A4 + C5 --> A4 + + A1 --> CTX1 + A1 --> CTX2 + A2 --> CTX2 + A3 --> CTX1 + A4 --> CTX3 + A4 --> CTX4 + + A2 --> T1 + A3 --> T1 + A3 --> T2 +``` + +## Using the System (User Guide) + +### How Slash Commands Work + +**Slash commands automatically inject context into agents using @ references:** + +```bash +# User types this: +/create-component "user profile card with avatar upload" + +# OpenCode automatically: +# 1. Loads the command file: .opencode/command/create-component.md +# 2. Reads all @ referenced context files +# 3. Injects context + user request into the agent +# 4. Agent starts with full knowledge of your patterns +``` + +### Basic Commands + +- `/workflow "your request"` - Main entry point for any development task +- `/plan-task "complex feature"` - For multi-step planning +- `/execute-task` - Continue working on planned tasks + +### Command Examples + +- `/workflow "Create a user profile component with form validation"` +- `/create-frontend-component "Dashboard header with user menu"` +- `/review-completion` - Check completed work for quality + +### When to Use Which Command + +- Simple tasks (< 30 min) → `/workflow` +- Complex features → `/plan-task` then `/execute-task` +- Specialized work → `/create-frontend-component`, `/create-backend-operation` + +### Why This Works Better Than Chat + +- **Consistent patterns**: Agents always use YOUR coding standards +- **Faster execution**: No time spent discovering or asking about patterns +- **Quality results**: Context-aware from the first response +- **Scalable knowledge**: Add new patterns once, use everywhere + +## Quick Start Implementation + +### 1. Start with Core System + +```bash +# Create essential structure +mkdir -p .opencode/{command,agent,context/{core,architecture}} +mkdir -p tasks/{features,fixes,single} +``` + +### 2. Create Main Entry Point + +```markdown +# .opencode/command/workflow.md + +--- + +name: workflow +agent: workflow-orchestrator + +--- + +You are analyzing requests and routing to optimal workflows. + +**Request:** $ARGUMENTS + +**Context Loaded:** +@.opencode/context/core/essential-patterns.md +@.opencode/context/architecture/architecture-lite.md + +Route to appropriate specialized workflow now. +``` + +### 3. Build Workflow Orchestrator + +```markdown +# .opencode/agent/workflow-orchestrator.md + +--- + +description: "Routes requests to specialized workflows" +mode: primary +tools: [read, grep, glob, task] + +--- + +You are analyzing requests and routing to specialized workflows. + +**ANALYZE** the request and **ROUTE** to appropriate command: + +- Feature development → /plan-task or /create-frontend-component +- Bug fixes → /fix-issue +- Code review → /review-code + +Execute routing now. +``` + +### 4. Add Essential Context + +````markdown +# .opencode/context/core/essential-patterns.md + +**CORE PATTERNS** - Essential patterns for all development: + +**AUTHENTICATION PATTERN**: + +```typescript +const user = await getCurrentUser(); +if (!user) return { error: "Unauthorized" }; +``` +```` + +**VALIDATION PATTERN**: + +```typescript +const validated = schema.safeParse(data); +if (!validated.success) return { error: "Validation failed" }; +``` + +```` + +### 5. Test Your System +```bash +# Test the workflow +/workflow "Create a user profile component" +# Should route to appropriate workflow with right context +```` + +## Remember + +1. **Context flows one direction** - Commands load immediately, Agents look up deterministically +2. **Keep context focused** - 50-150 lines per file, maximum 4 files per command +3. **Make agents predictable** - Deterministic behavior with clear instructions +4. **Track everything** - File-based task management with checkbox progress +5. **Validate continuously** - Quality gates and post-flight reviews +6. **Start simple** - Build core system first, add complexity gradually + +--- + +_Think of this system like a professional development team: each member has a specific role, they communicate clearly, they track their work systematically, and they validate quality at every step._ diff --git a/.opencode/AGENTS.md b/.opencode/AGENTS.md index 1446907..2270ffd 100644 --- a/.opencode/AGENTS.md +++ b/.opencode/AGENTS.md @@ -1,24 +1,28 @@ -# OpenCode Agents for Mastra RAG Template + This repository defines task-focused agents to streamline planning, implementation, review, documentation, and testing. Agents: -- `plan-project`: Roadmaps, milestones, ADRs, risk register -- `plan-analyse`: Repo survey, external research, dependency and risk mapping -- `mastra`: Implementation for ingestion, embeddings, LanceDB, retrieval, assembly, and agents -- `review`: Code review with targeted feedback and suggested diffs -- `documentation`: Doc updates for README, specs, examples -- `write-test`: Unit/integration tests, mocks, deterministic practices +- `general`: General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks +- `reviewer`: Code review, security, and quality assurance agent +- `subagents/codebase-pattern-analyst`: TypeScript implementation agent for modular and functional development +- `subagents/coder-agent`: Executes coding subtasks in sequence, ensuring completion as specified +- `subagents/build-agent`: Type check and build validation agent +- `subagents/tester`: Test authoring and TDD agent +- `subagents/documentation`: Documentation authoring agent +- `@task-manager`: Task planning and management agent +- `@tester`: Test execution and validation agent +- `@documentation`: Documentation generation and updates agent Usage: ```bash -# Start a session with an agent -opencode --agent plan-project +# Launch a task with a specific agent +task --description "Short task description" --prompt "Detailed task instructions" --subagent_type general -# One-off task -opencode run --agent mastra "Implement LanceDB schema and retrieval module" +# Example for code review +task --description "Review code" --prompt "Review the changes in pull request #123" --subagent_type reviewer ``` Safety: diff --git a/.opencode/agent/codebase-agent.md b/.opencode/agent/codebase-agent.md index 60abd23..40219d8 100644 --- a/.opencode/agent/codebase-agent.md +++ b/.opencode/agent/codebase-agent.md @@ -33,9 +33,9 @@ Always start with phrase "DIGGING IN..." You have access to the following subagents: - `@task-manager` -- `@subagents/coder-agent` -- `@subagents/tester` -- `@subagents/documentation` +- `@image-specialist` +- `@subagents/tester` @tester +- `@subagents/documentation` @documentation Focus: You are a TypeScript coding specialist focused on writing clean, maintainable, and scalable code. Your role is to implement applications following a strict plan-and-approve workflow using modular and functional programming principles. @@ -78,6 +78,7 @@ Do NOT proceed without explicit approval Phase 2: Implementation (After Approval Only) Implement incrementally - complete one step at a time, never implement the entire plan at once +If need images for a task, so pass it to the `@image-specialist` to make images for the task and tell it where to save the images. So you can use the images in the task. After each increment: - Use appropriate runtime (node/bun) to execute the code and check for errors before moving on to the next step - Run type checks using TypeScript compiler diff --git a/.opencode/agent/image-specialist.md b/.opencode/agent/image-specialist.md new file mode 100644 index 0000000..1d73a73 --- /dev/null +++ b/.opencode/agent/image-specialist.md @@ -0,0 +1,65 @@ +--- +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 +--- + +You are an image processing specialist powered by Gemini AI's Nano Banana model. Your capabilities include: + +## Core Functions +- **Image Generation**: Creating images from text using Gemini Nano Banana +- **Image Editing**: Modifying existing images with Nano Banana +- **Image Analysis**: Analyzing images with detailed descriptions + +## Tools Available +- `gemini-multiple_edit`: Edit existing images with Nano Banana +- `gemini-multiple_analyze`: Analyze images and provide detailed descriptions +- `gemini`: Generate or edit images (legacy tool) + +## Meta-Prompt for Nano Banana Requests + +When users provide simple instructions, use this meta-prompt approach to create detailed Nano Banana prompts: + +**Process:** +1. **Identify core purpose**: Schematic/diagram, action illustration, or emotive scene? +2. **Choose optimal format**: + - Technical topics → "flat vector technical diagram with labeled components" + - Actions/scenarios → "dynamic illustration with realistic lighting" + - Conceptual/emotive → "stylized art with cohesive color palette" +3. **Determine style attributes**: Color palette, typography, composition +4. **Build final prompt**: "Create a [FORMAT] illustrating [TOPIC] in a [STYLE] style, using [COLORS], with [TYPOGRAPHY] labels, include [LAYOUT ELEMENTS]" + +**Example:** +- Input: "Visualize microservices architecture" +- Output: "Create a flat-vector technical diagram illustrating a microservices architecture with labeled service nodes and directional arrows showing service-to-service calls, in a navy & teal color palette, with Roboto sans-serif labels, include a legend box at bottom right, optimized for 1200×627 px." + +## Workflow +1. **For simple requests**: Apply meta-prompt to enhance the instruction +2. **For image generation**: Use detailed, styled prompts with Nano Banana +3. **For image editing**: Preserve original context while applying modifications +4. **For analysis**: Provide comprehensive descriptions and suggestions + +## File Organization +- Images are automatically organized by date: `assets/images/YYYY-MM-DD/` +- Generations saved to: `generations/` subdirectory +- Edits saved to: `edits/` subdirectory with auto-increment naming +- No files are overwritten - each edit creates a unique numbered version +- All images stored in repo's `assets/images/` directory for proper organization + +Always ensure you have necessary inputs and provide clear descriptions of operations performed. \ No newline at end of file diff --git a/.opencode/agent/subagents/build-agent.md b/.opencode/agent/subagents/build-agent.md new file mode 100644 index 0000000..39a66ac --- /dev/null +++ b/.opencode/agent/subagents/build-agent.md @@ -0,0 +1,42 @@ +--- + +description: "Type check and build validation agent" +mode: subagent +model: claude-4-sonnet +temperature: 0.1 +tools: + bash: true + read: true + grep: true +permissions: + bash: + "tsc": "allow" + "npm run build": "allow" + "yarn build": "allow" + "pnpm build": "allow" + "*": "deny" + edit: + "**/*": "deny" +--- + +# Build Agent + +You are a build validation agent. For every request, perform the following steps: + +1. **Type Check** + - Run the TypeScript compiler (`tsc`). + - If there are any type errors, return the error output and stop. + +2. **Build Check** + - If type checking passes, run the build command (`npm run build`, `yarn build`, or `pnpm build` as appropriate). + - If there are any build errors, return the error output. + +3. **Success** + - If both steps complete without errors, return a success message. + +**Rules:** +- Only run type check and build check. +- Only report errors if they occur; otherwise, report success. +- Do not modify any code. + +Execute type check and build validation now. \ No newline at end of file diff --git a/.opencode/agent/reviewer.md b/.opencode/agent/subagents/reviewer.md similarity index 71% rename from .opencode/agent/reviewer.md rename to .opencode/agent/subagents/reviewer.md index 7559dfb..b65a314 100644 --- a/.opencode/agent/reviewer.md +++ b/.opencode/agent/subagents/reviewer.md @@ -1,4 +1,5 @@ --- + description: "Code review, security, and quality assurance agent" mode: subagent model: claude-4-sonnet @@ -25,15 +26,22 @@ Responsibilities: - Check alignment with naming conventions and modular patterns - Identify and flag potential security vulnerabilities (e.g., XSS, injection, insecure dependencies) - Flag potential performance and maintainability issues +- Load project-specific context for accurate pattern validation - First sentence should be Start with "Reviewing..., what would you devs do if I didn't check up on you?" Workflow: -1. Share a short review plan (files/concerns to inspect, including security aspects) and ask to proceed. -2. Provide concise review notes with suggested diffs (do not apply changes), including any security concerns. +1. **ANALYZE** request and load relevant project context +2. Share a short review plan (files/concerns to inspect, including security aspects) and ask to proceed. +3. Provide concise review notes with suggested diffs (do not apply changes), including any security concerns. Output: Start with "Reviewing..., what would you devs do if I didn't check up on you?" Then give a short summary of the review. - Risk level (including security risk) and recommended follow-ups + +**Context Loading:** +- Load project patterns and security guidelines +- Analyze code against established conventions +- Flag deviations from team standards \ No newline at end of file diff --git a/.opencode/agent/tester.md b/.opencode/agent/subagents/tester.md similarity index 98% rename from .opencode/agent/tester.md rename to .opencode/agent/subagents/tester.md index debf13d..dedd81d 100644 --- a/.opencode/agent/tester.md +++ b/.opencode/agent/subagents/tester.md @@ -1,7 +1,6 @@ --- description: "Test authoring and TDD agent" mode: subagent -model: google/gemini-2.5-flash temperature: 0.1 tools: read: true diff --git a/.opencode/agent/workflow-orchestrator.md b/.opencode/agent/workflow-orchestrator.md new file mode 100644 index 0000000..da80827 --- /dev/null +++ b/.opencode/agent/workflow-orchestrator.md @@ -0,0 +1,56 @@ +--- + +description: "Routes requests to specialized workflows with selective context loading" +mode: primary +model: claude-4-sonnet +temperature: 0.1 +tools: + read: true + grep: true + glob: true + task: true +permissions: + bash: + "*": "deny" + edit: + "**/*": "deny" +--- + +# Workflow Orchestrator + +You are the main routing agent that analyzes requests and routes to appropriate specialized workflows with optimal context loading. + +**ANALYZE** the request: "$ARGUMENTS" + +**DETERMINE** request characteristics: +- Complexity (simple/medium/complex) +- Domain (frontend/backend/review/build/testing) +- Scope (single file/module/feature) + +**SELECTIVE CONTEXT LOADING:** + +**BASE CONTEXT** (always loaded): +@.opencode/context/core/essential-patterns.md +@.opencode/context/project/project-context.md + +**CONDITIONAL CONTEXT** (based on analysis): +!`if echo "$ARGUMENTS" | grep -i -E "(review|security|quality)" > /dev/null; then echo "@.opencode/context/project/project-context.md"; fi` +!`if echo "$ARGUMENTS" | grep -i -E "(build|type|lint|compile)" > /dev/null; then echo "@.opencode/context/project/project-context.md"; fi` +!`if echo "$ARGUMENTS" | grep -i -E "(test|spec|unit|integration)" > /dev/null; then echo "@.opencode/context/project/project-context.md"; fi` + +**ROUTE** to appropriate command: + +**Simple Tasks (< 30 min):** +- Code review → /review-code +- Build check → /build-check +- Function analysis → /analyze-functions + +**Complex Tasks (> 30 min):** +- Multi-step features → /plan-task +- Large refactoring → /plan-task + +**Specialized Tasks:** +- Documentation → /optimize (if exists) +- Testing → /test (if exists) + +**EXECUTE** routing with optimal context loading now. \ No newline at end of file diff --git a/.opencode/command/prompt-enchancer.md b/.opencode/command/prompt-enchancer.md new file mode 100644 index 0000000..e20c121 --- /dev/null +++ b/.opencode/command/prompt-enchancer.md @@ -0,0 +1,121 @@ +--- +description: "Research-backed XML prompt optimizer delivering 20% performance improvement" +--- + + + + + Prompt optimization using empirically-proven XML structures + LLM prompt engineering with Stanford/Anthropic research patterns + 20% routing accuracy, 25% consistency, 17% performance gains + + +Expert Prompt Architect specializing in evidence-based XML structure optimization + +Transform prompts into high-performance XML following proven component ordering for measurable improvements + + + + Assess current structure against research patterns + + - Component order (context→role→task→instructions) + - Length ratios (role 5-10%, context 15-25%, instructions 40-50%) + - Context management strategy presence + - Hierarchical routing implementation + + + + + Apply optimal component sequence + + 1. Context (system→domain→task→execution) + 2. Role (clear identity, first 20% of prompt) + 3. Task (primary objective) + 4. Instructions (hierarchical workflow) + 5. Examples (if needed) + 6. Constraints (boundaries) + 7. Output_format (expected structure) + + + + + Implement manager-worker patterns + + - LLM-based decision making + - Explicit routing criteria with @ symbol + - Fallback strategies + - Context allocation per task type + + + + + Apply 3-level context management + + Complete isolation - subagent receives only specific task + Filtered context - curated relevant background + Windowed context - last N messages only + + + + + + + - 40% improvement in response quality with descriptive tags + - 15% reduction in token overhead for complex prompts + - Universal compatibility across models + - Explicit boundaries prevent context bleeding + + + + 5-10% of total prompt + 15-25% hierarchical information + 40-50% detailed procedures + 20-30% when needed + 5-10% boundaries + + + + Always use @ symbol (e.g., @context-provider, @research-assistant-agent) + Route to @[agent-name] when [condition] + + + + +## Analysis Results +- Current Structure Score: [X/10] +- Optimization Opportunities: [LIST] +- Expected Performance Gain: [X%] + +## Optimized Prompt Structure + +```xml + + [HIERARCHICAL CONTEXT: system→domain→task→execution] + + +[AGENT IDENTITY - 5-10% of prompt] + +[PRIMARY OBJECTIVE] + + + [WORKFLOW WITH ROUTING USING @ SYMBOLS] + + +[ADDITIONAL COMPONENTS AS NEEDED] +``` + +## Implementation Notes +- Component reordering impact: +[X]% performance +- Context management efficiency: [X]% reduction +- Routing accuracy improvement: +[X]% +- Subagent references: @agent-name format maintained + + + + Stanford multi-instruction study + Anthropic XML research + Measurable 20% routing improvement + 80% reduction in unnecessary context + Ready for deployment without modification + + + diff --git a/.opencode/command/prompter.md b/.opencode/command/prompter.md deleted file mode 100644 index a8bf959..0000000 --- a/.opencode/command/prompter.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -description: Transform basic requests into comprehensive, well-structured prompts using the 10-step framework ---- - -You are a prompt enhancement specialist. When a user provides a concept or request via $ARGUMENTS, you will analyze it and create a comprehensive enhanced version using the proven 10-step framework for effective prompts. - -## Your Enhancement Process: - -**Step 1: Analyze the Original Request** -Examine the provided concept and identify: -- Task type and scope -- Required expertise domain -- End goal and success criteria -- Potential challenges or complexity - -**Step 2: Apply the 10-Step Enhancement Framework** -Transform the request by incorporating these essential elements: - -1. **Task Context** - Expert role and main objective -2. **Tone Context** - Communication style and audience level -3. **Background Knowledge** - Required expertise and resources -4. **Task Rules & Standards** - Quality criteria and constraints -5. **Examples & Patterns** - Concrete illustrations of approaches -6. **Context Management** - Conversation continuity handling -7. **Immediate Deliverables** - Expected outputs -8. **Reasoning Approach** - Systematic thinking methodology -9. **Output Structure** - Format and organization requirements -10. **Response Framework** - Templates and structured approaches - -**Step 3: Present Your Analysis** - -Use this format: - ---- -## 📋 Request Analysis -**Original concept**: "[USER'S CONCEPT]" -**Task type**: [Brief description of work involved] -**Required expertise**: [Domain knowledge needed] -**Complexity level**: [Simple/Moderate/Complex] - -## 🎯 10-Step Enhanced Framework -Instead of a basic request, here's a structured approach using all 10 framework elements: - -### ✨ Enhanced Request: -``` -[Create a comprehensive request that naturally incorporates all 10 elements, written as a single cohesive prompt that the user could copy and use. Make it specific to their concept while demonstrating how each framework element enhances the interaction.] -``` - -## 🔧 Framework Elements Applied: -- **Task Context**: [How expert role was defined] -- **Tone Context**: [Communication style set] -- **Background Knowledge**: [Expertise requirements specified] -- **Task Rules & Standards**: [Quality criteria added] -- **Examples & Patterns**: [Illustrations provided] -- **Context Management**: [Continuity handled] -- **Immediate Deliverables**: [Expected outputs clarified] -- **Reasoning Approach**: [Thinking methodology specified] -- **Output Structure**: [Format requirements defined] -- **Response Framework**: [Templates/structure provided] - -### ▶️ Suggested Next Step -"Would you like me to proceed with this enhanced 10-step approach? Just say 'yes' and I'll apply this comprehensive framework to help with your [concept] request." - ---- - -## Important Guidelines: -- Make the enhanced request feel natural, not like a checklist -- Ensure all 10 framework elements are meaningfully integrated -- Tailor the complexity and tone to match the original concept -- The enhanced request should be immediately usable by the user -- Focus on practical improvements that will genuinely enhance their interaction - diff --git a/.opencode/context/core/essential-patterns.md b/.opencode/context/core/essential-patterns.md new file mode 100644 index 0000000..689df3f --- /dev/null +++ b/.opencode/context/core/essential-patterns.md @@ -0,0 +1,250 @@ +# Essential Patterns - Core Knowledge Base + +## Error Handling Pattern + +**ALWAYS** handle errors gracefully: + +```typescript +try { + const result = await riskyOperation(); + return { success: true, data: result }; +} catch (error) { + console.error('Operation failed:', error); + return { success: false, error: error.message }; +} +``` + +## Validation Pattern + +**ALWAYS** validate input data: + +```typescript +function validateInput(input: any): { valid: boolean; errors?: string[] } { + const errors: string[] = []; + + if (!input) errors.push('Input is required'); + if (typeof input !== 'string') errors.push('Input must be a string'); + if (input.length < 3) errors.push('Input must be at least 3 characters'); + + return { + valid: errors.length === 0, + errors: errors.length > 0 ? errors : undefined + }; +} +``` + +## Logging Pattern + +**USE** consistent logging levels: + +```typescript +// Debug information (development only) +console.debug('Processing request:', requestId); + +// Info for important events +console.info('User authenticated:', userId); + +// Warning for potential issues +console.warn('Rate limit approaching for user:', userId); + +// Error for failures +console.error('Database connection failed:', error); +``` + +## Security Pattern + +**NEVER** expose sensitive information: + +```typescript +// ❌ BAD: Exposing internal errors +return { error: 'Internal server error: ' + error.message }; + +// ✅ GOOD: Generic error message +return { error: 'An unexpected error occurred. Please try again.' }; +``` + +## File System Safety Pattern + +**ALWAYS** validate file paths: + +```typescript +import path from 'path'; + +function safeReadFile(userPath: string): string | null { + const resolvedPath = path.resolve(userPath); + const allowedDir = path.resolve('./allowed-directory'); + + // Ensure path is within allowed directory + if (!resolvedPath.startsWith(allowedDir)) { + throw new Error('Access denied: Invalid path'); + } + + return fs.readFileSync(resolvedPath, 'utf8'); +} +``` + +## Type Safety Pattern + +**ALWAYS** use strict TypeScript types: + +```typescript +interface User { + id: string; + name: string; + email: string; + createdAt: Date; +} + +interface ApiResponse { + success: boolean; + data?: T; + error?: string; +} + +// Use generics for type-safe responses +function createUser(userData: Omit): ApiResponse { + // Implementation +} +``` + +## Async/Await Pattern + +**ALWAYS** handle promises properly: + +```typescript +// ❌ BAD: Nested promises +fetchUser().then(user => { + return fetchPosts(user.id).then(posts => { + return { user, posts }; + }); +}); + +// ✅ GOOD: Async/await with error handling +async function getUserWithPosts(userId: string) { + try { + const user = await fetchUser(userId); + const posts = await fetchPosts(user.id); + return { user, posts }; + } catch (error) { + console.error('Failed to fetch user data:', error); + throw error; + } +} +``` + +## Configuration Pattern + +**ALWAYS** use environment variables for configuration: + +```typescript +// config.ts +export const config = { + port: parseInt(process.env.PORT || '3000'), + databaseUrl: process.env.DATABASE_URL, + jwtSecret: process.env.JWT_SECRET, + nodeEnv: process.env.NODE_ENV || 'development', +}; + +// Validate required config +if (!config.databaseUrl) { + throw new Error('DATABASE_URL environment variable is required'); +} +``` + +## Testing Pattern + +**ALWAYS** write testable code: + +```typescript +// ❌ BAD: Hard to test +export function processPayment(amount: number) { + const apiKey = process.env.STRIPE_KEY; + // Direct API call +} + +// ✅ GOOD: Dependency injection +export interface PaymentService { + processPayment(amount: number): Promise; +} + +export function createPaymentProcessor(service: PaymentService) { + return { + async process(amount: number) { + return service.processPayment(amount); + } + }; +} +``` + +## Documentation Pattern + +**ALWAYS** document complex logic: + +```typescript +/** + * Calculates the total price including tax and discounts + * @param basePrice - The original price before modifications + * @param taxRate - Tax rate as decimal (e.g., 0.08 for 8%) + * @param discountPercent - Discount percentage (0-100) + * @returns The final price after tax and discount + */ +function calculateTotalPrice( + basePrice: number, + taxRate: number = 0.08, + discountPercent: number = 0 +): number { + const discountAmount = basePrice * (discountPercent / 100); + const discountedPrice = basePrice - discountAmount; + const taxAmount = discountedPrice * taxRate; + return discountedPrice + taxAmount; +} +``` + +## Performance Pattern + +**AVOID** unnecessary operations in loops: + +```typescript +// ❌ BAD: Repeated calculations +const results = []; +for (let i = 0; i < items.length; i++) { + results.push(items[i] * calculateTax(items[i])); // calculateTax called repeatedly +} + +// ✅ GOOD: Pre-calculate or cache +const results = []; +const taxRate = getCurrentTaxRate(); // Calculate once +for (let i = 0; i < items.length; i++) { + results.push(items[i] * taxRate); +} +``` + +## Code Organization Pattern + +**KEEP** functions focused and small: + +```typescript +// ❌ BAD: One function doing too much +function processOrder(orderData) { + // Validate input + // Calculate pricing + // Save to database + // Send email + // Log analytics +} + +// ✅ GOOD: Separated concerns +function validateOrder(orderData) { /* validation logic */ } +function calculatePricing(orderData) { /* pricing logic */ } +function saveOrder(orderData) { /* database logic */ } +function sendConfirmation(orderData) { /* email logic */ } +function logAnalytics(orderData) { /* analytics logic */ } + +async function processOrder(orderData) { + validateOrder(orderData); + const pricing = calculatePricing(orderData); + await saveOrder({ ...orderData, pricing }); + await sendConfirmation(orderData); + logAnalytics(orderData); +} +``` \ No newline at end of file diff --git a/.opencode/context/project/project-context.md b/.opencode/context/project/project-context.md new file mode 100644 index 0000000..c90da68 --- /dev/null +++ b/.opencode/context/project/project-context.md @@ -0,0 +1,97 @@ +# OpenCode Agent System Project Context + +## Technology Stack + +**Primary Language:** TypeScript +**Runtime:** Node.js/Bun +**Package Manager:** npm/pnpm/yarn +**Build Tools:** TypeScript Compiler (tsc) +**Testing:** Jest/Vitest (if configured) +**Linting:** ESLint (if configured) + +## Project Structure + +``` +.opencode/ +├── agent/ # AI agents for specific tasks +│ ├── subagents/ # Specialized subagents +│ └── *.md # Primary agents +├── command/ # Slash commands +├── context/ # Knowledge base for agents +└── plugin/ # Extensions and integrations + +tasks/ # Task management files +``` + +## Core Patterns + +### Agent Structure Pattern +```markdown +--- +description: "What this agent does" +mode: primary|subagent +tools: [read, edit, bash, etc.] +permissions: [security restrictions] +--- + +# Agent Name + +[Direct instructions for behavior] + +**EXECUTE** this [process type] for every [task type]: + +**1. [ACTION]** the [subject]: +- [Specific instruction 1] +- [Specific instruction 2] + +**RULES:** +- **ALWAYS** [critical requirement] +- **NEVER** [forbidden action] +``` + +### Command Structure Pattern +```markdown +--- +name: command-name +agent: target-agent +--- + +You are [doing specific task]. + +**Request:** $ARGUMENTS + +**Context Loaded:** +@.opencode/context/core/essential-patterns.md +@[additional context files] + +Execute [task] now. +``` + +### Context Loading Rules +- Commands load context immediately using @ references +- Agents can look up additional context deterministically +- Maximum 4 context files per command (250-450 lines total) +- Keep context files focused (50-150 lines each) + +## Security Guidelines + +- Agents have restricted permissions by default +- Sensitive operations require explicit approval +- No direct file system modifications without validation +- Build commands limited to safe operations + +## Development Workflow + +1. **Planning:** Create detailed task plans for complex work +2. **Implementation:** Execute one step at a time with validation +3. **Review:** Code review and security checks +4. **Testing:** Automated testing and build validation +5. **Documentation:** Update docs and context files + +## Quality Gates + +- TypeScript compilation passes +- Code review completed +- Build process succeeds +- Tests pass (if available) +- Documentation updated \ No newline at end of file diff --git a/.opencode/tool/README.md b/.opencode/tool/README.md new file mode 100644 index 0000000..5197794 --- /dev/null +++ b/.opencode/tool/README.md @@ -0,0 +1,79 @@ +# Gemini Image Tools + +This collection of tools allows you to edit and analyze images using Google's Gemini AI directly from OpenCode. + +## Setup + +1. Get your Gemini API key from [Google AI Studio](https://makersuite.google.com/app/apikey) +2. Add it to your `.env` file: + ```bash + GEMINI_API_KEY=your_api_key_here + ``` + +## Available Tools + +### `/gemini` - Simple Image Editor +Edit an image using file path or data URL: + +```bash +/gemini "path/to/image.png" "Add a red border around the image" "output.png" +/gemini "data:image/png;base64,AAA..." "Convert to black and white" +``` + +### `/gemini_multiple_edit` - Advanced Image Editor +Same functionality as `/gemini` but from the multiple tools file: + +```bash +/gemini_multiple_edit "image.jpg" "Make it look like a watercolor painting" "watercolor.jpg" +``` + +### `/gemini_multiple_analyze` - Image Analysis +Analyze an image without editing it: + +```bash +/gemini_multiple_analyze "screenshot.png" "What programming language is shown in this code?" +/gemini_multiple_analyze "photo.jpg" "Describe the objects and colors in this image" +``` + +### `/gemini_edit` - Auto-Detection Plugin +1. Paste an image directly into your OpenCode chat +2. Use the command with just the prompt: + +```bash +/gemini_edit "Add the text 'Hello World' in cursive at top" +/gemini_edit "Make this image look like a painting" +``` + +## Features + +- **File Path Support**: Pass local image file paths +- **Data URL Support**: Use base64 data URLs from pasted images +- **Auto-Detection**: Plugin automatically captures the latest pasted image +- **Image Analysis**: Ask questions about images without editing +- **Flexible Output**: Specify custom output filenames or use defaults +- **Error Handling**: Clear error messages for missing API keys or failed requests + +## Files + +- `gemini.ts` - Simple tool that accepts image arguments +- `gemini-multiple.ts` - Multiple tools (edit + analyze) in one file +- `../plugin/gemini-edit.ts` - Plugin with auto-detection of pasted images + +## API Endpoints + +- **Image Editing**: Uses Gemini 2.5 Flash with image preview capabilities +- **Image Analysis**: Uses Gemini 2.5 Flash for text-based analysis + +## Examples + +```bash +# Edit an image +/gemini "logo.png" "Add a subtle drop shadow" "logo-shadow.png" + +# Analyze code in a screenshot +/gemini_multiple_analyze "code-screenshot.png" "What bugs can you spot in this code?" + +# Auto-edit pasted image +# (paste image first, then run:) +/gemini_edit "Remove the background and make it transparent" +``` \ No newline at end of file diff --git a/.opencode/tool/bun.lock b/.opencode/tool/bun.lock new file mode 100644 index 0000000..ba13347 --- /dev/null +++ b/.opencode/tool/bun.lock @@ -0,0 +1,160 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "opencode-gemini-tool", + "dependencies": { + "@opencode-ai/sdk": "^0.10.0", + "zod": "^4.1.9", + }, + "devDependencies": { + "@opencode-ai/plugin": "^0.10.0", + "@types/node": "^24.2.1", + "bun-types": "latest", + }, + }, + }, + "packages": { + "@hey-api/json-schema-ref-parser": ["@hey-api/json-schema-ref-parser@1.0.6", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0", "lodash": "^4.17.21" } }, "sha512-yktiFZoWPtEW8QKS65eqKwA5MTKp88CyiL8q72WynrBs/73SAaxlSWlA2zW/DZlywZ5hX1OYzrCC0wFdvO9c2w=="], + + "@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.81.0", "", { "dependencies": { "@hey-api/json-schema-ref-parser": "1.0.6", "ansi-colors": "4.1.3", "c12": "2.0.1", "color-support": "1.1.3", "commander": "13.0.0", "handlebars": "4.7.8", "js-yaml": "4.1.0", "open": "10.1.2", "semver": "7.7.2" }, "peerDependencies": { "typescript": "^5.5.3" }, "bin": { "openapi-ts": "bin/index.cjs" } }, "sha512-PoJukNBkUfHOoMDpN33bBETX49TUhy7Hu8Sa0jslOvFndvZ5VjQr4Nl/Dzjb9LG1Lp5HjybyTJMA6a1zYk/q6A=="], + + "@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="], + + "@opencode-ai/plugin": ["@opencode-ai/plugin@0.10.0", "", { "dependencies": { "@opencode-ai/sdk": "0.10.0", "zod": "4.1.8" } }, "sha512-qviOlxiWZchKGfJ1WWWM9q8Jz/6PokcdEUvGW0xeuC11QAkgC/RRC0DDn62PCcJUuWTOfIBkIcIQdP4qzbYNKw=="], + + "@opencode-ai/sdk": ["@opencode-ai/sdk@0.10.0", "", { "dependencies": { "@hey-api/openapi-ts": "0.81.0" } }, "sha512-2ojC3KEdf5r9rmhKJJx4cq8gxhw5LATHokIUl+VXsbE5qWaLe7yzt5Xpb2505wyieMfUlujo6XDGUtnPQXWkAw=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="], + + "@types/react": ["@types/react@19.1.13", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ=="], + + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="], + + "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], + + "c12": ["c12@2.0.1", "", { "dependencies": { "chokidar": "^4.0.1", "confbox": "^0.1.7", "defu": "^6.1.4", "dotenv": "^16.4.5", "giget": "^1.2.3", "jiti": "^2.3.0", "mlly": "^1.7.1", "ohash": "^1.1.4", "pathe": "^1.1.2", "perfect-debounce": "^1.0.0", "pkg-types": "^1.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-Z4JgsKXHG37C6PYUtIxCfLJZvo6FyhHJoClwwb9ftUkLpPSkuYqn6Tr+vnaN8hymm0kIbcg6Ey3kv/Q71k5w/A=="], + + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], + + "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], + + "color-support": ["color-support@1.1.3", "", { "bin": { "color-support": "bin.js" } }, "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="], + + "commander": ["commander@13.0.0", "", {}, "sha512-oPYleIY8wmTVzkvQq10AEok6YcTC4sRUBl8F9gVuwchGVUCTbl/vhLTaQqutuuySYOsu8YTgV+OxKc/8Yvx+mQ=="], + + "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + + "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "default-browser": ["default-browser@5.2.1", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg=="], + + "default-browser-id": ["default-browser-id@5.0.0", "", {}, "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA=="], + + "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], + + "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + + "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], + + "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + + "fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="], + + "giget": ["giget@1.2.5", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.5.4", "pathe": "^2.0.3", "tar": "^6.2.1" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-r1ekGw/Bgpi3HLV3h1MRBIlSAdHoIMklpaQ3OQLFcRw9PwAj2rqigvIbg+dBUI51OxVI2jsEtDywDBjSiuf7Ug=="], + + "handlebars": ["handlebars@4.7.8", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="], + + "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], + + "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], + + "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], + + "jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="], + + "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], + + "minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], + + "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + + "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], + + "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], + + "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], + + "nypm": ["nypm@0.5.4", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "tinyexec": "^0.3.2", "ufo": "^1.5.4" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-X0SNNrZiGU8/e/zAB7sCTtdxWTMSIO73q+xuKgglm2Yvzwlo8UoC5FNySQFCvl84uPaeADkqHUZUkWy4aH4xOA=="], + + "ohash": ["ohash@1.1.6", "", {}, "sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg=="], + + "open": ["open@10.1.2", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "is-wsl": "^3.1.0" } }, "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw=="], + + "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], + + "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], + + "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + + "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], + + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], + + "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], + + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + + "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], + + "ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="], + + "uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="], + + "undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="], + + "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], + + "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "zod": ["zod@4.1.9", "", {}, "sha512-HI32jTq0AUAC125z30E8bQNz0RQ+9Uc+4J7V97gLYjZVKRjeydPgGt6dvQzFrav7MYOUGFqqOGiHpA/fdbd0cQ=="], + + "@opencode-ai/plugin/zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="], + + "fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "giget/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "mlly/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "nypm/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + } +} diff --git a/.opencode/tool/env/index.ts b/.opencode/tool/env/index.ts new file mode 100644 index 0000000..8ff2ac9 --- /dev/null +++ b/.opencode/tool/env/index.ts @@ -0,0 +1,168 @@ +import { readFile } from "fs/promises" +import { resolve } from "path" + +/** + * Configuration for environment variable loading + */ +export interface EnvLoaderConfig { + /** Custom paths to search for .env files (relative to current working directory) */ + searchPaths?: string[] + /** Whether to log when environment variables are loaded */ + verbose?: boolean + /** Whether to override existing environment variables */ + override?: boolean +} + +/** + * Default search paths for .env files + */ +const DEFAULT_ENV_PATHS = [ + './.env', + '../.env', + '../../.env', + '../plugin/.env', + '../../../.env' +] + +/** + * Load environment variables from .env files + * Searches multiple common locations for .env files and loads them into process.env + * + * @param config Configuration options + * @returns Object containing loaded environment variables + */ +export async function loadEnvVariables(config: EnvLoaderConfig = {}): Promise> { + const { + searchPaths = DEFAULT_ENV_PATHS, + verbose = false, + override = false + } = config + + const loadedVars: Record = {} + + for (const envPath of searchPaths) { + try { + const fullPath = resolve(envPath) + const content = await readFile(fullPath, 'utf8') + + if (verbose) { + console.log(`Checking .env file: ${envPath}`) + } + + // Parse .env file content + const lines = content.split('\n') + for (const line of lines) { + const trimmed = line.trim() + if (trimmed && !trimmed.startsWith('#') && trimmed.includes('=')) { + const [key, ...valueParts] = trimmed.split('=') + const value = valueParts.join('=').trim() + + // Remove quotes if present + const cleanValue = value.replace(/^["']|["']$/g, '') + + if (key && cleanValue && (override || !process.env[key])) { + process.env[key] = cleanValue + loadedVars[key] = cleanValue + + if (verbose) { + console.log(`Loaded ${key} from ${envPath}`) + } + } + } + } + } catch (error) { + // File doesn't exist or can't be read, continue to next + if (verbose) { + console.log(`Could not read ${envPath}: ${error.message}`) + } + } + } + + return loadedVars +} + +/** + * Get a specific environment variable with automatic .env file loading + * + * @param varName Name of the environment variable + * @param config Configuration options + * @returns The environment variable value or null if not found + */ +export async function getEnvVariable(varName: string, config: EnvLoaderConfig = {}): Promise { + // First check if it's already in the environment + let value = process.env[varName] + + if (!value) { + // Try to load from .env files + const loadedVars = await loadEnvVariables(config) + value = loadedVars[varName] || process.env[varName] + } + + return value || null +} + +/** + * Get a required environment variable with automatic .env file loading + * Throws an error if the variable is not found + * + * @param varName Name of the environment variable + * @param config Configuration options + * @returns The environment variable value + * @throws Error if the variable is not found + */ +export async function getRequiredEnvVariable(varName: string, config: EnvLoaderConfig = {}): Promise { + const value = await getEnvVariable(varName, config) + + if (!value) { + const searchPaths = config.searchPaths || DEFAULT_ENV_PATHS + throw new Error(`${varName} not found. Please set it in your environment or .env file. + +To fix this: +1. Add to .env file: ${varName}=your_value_here +2. Or export it: export ${varName}=your_value_here + +Current working directory: ${process.cwd()} +Searched paths: ${searchPaths.join(', ')} +Environment variables available: ${Object.keys(process.env).filter(k => k.includes(varName.split('_')[0])).join(', ') || 'none matching'}`) + } + + return value +} + +/** + * Load multiple required environment variables at once + * + * @param varNames Array of environment variable names + * @param config Configuration options + * @returns Object with variable names as keys and values as values + * @throws Error if any variable is not found + */ +export async function getRequiredEnvVariables(varNames: string[], config: EnvLoaderConfig = {}): Promise> { + const result: Record = {} + + // Load all .env files first + await loadEnvVariables(config) + + // Check each required variable + for (const varName of varNames) { + const value = process.env[varName] + if (!value) { + throw new Error(`Required environment variable ${varName} not found. Please set it in your environment or .env file.`) + } + result[varName] = value + } + + return result +} + +/** + * Utility function specifically for API keys + * + * @param apiKeyName Name of the API key environment variable + * @param config Configuration options + * @returns The API key value + * @throws Error if the API key is not found + */ +export async function getApiKey(apiKeyName: string, config: EnvLoaderConfig = {}): Promise { + return getRequiredEnvVariable(apiKeyName, config) +} \ No newline at end of file diff --git a/.opencode/tool/gemini/index.ts b/.opencode/tool/gemini/index.ts new file mode 100644 index 0000000..3fe85c9 --- /dev/null +++ b/.opencode/tool/gemini/index.ts @@ -0,0 +1,379 @@ +import { tool } from "@opencode-ai/plugin/tool" +import { mkdir } from "fs/promises" +import { join, dirname, basename, extname, resolve } from "path" +import { getApiKey } from "../env" + +// Function to detect if we're in test mode +function isTestMode(): boolean { + // Only enable test mode when explicitly set + return process.env.GEMINI_TEST_MODE === 'true' +} + +// Function to get Gemini API key with automatic .env loading +async function getGeminiApiKey(): Promise { + if (isTestMode()) { + return 'test-api-key' + } + return getApiKey('GEMINI_API_KEY') +} + +interface ImageConfig { + outputDir?: string; + useTimestamp?: boolean; + preserveOriginal?: boolean; + customName?: string; +} + +async function parseImageInput(input: string) { + // Accepts file path ("./img.png") or data URL ("data:image/png;base64,...") + if (input.startsWith("data:")) { + const base64 = input.split(",")[1] + const mime = input.substring(5, input.indexOf(";")) + return { mime, base64 } + } + // Treat as file path + const file = Bun.file(input) + const arr = await file.arrayBuffer() + const base64 = Buffer.from(arr).toString("base64") + // Best-effort mime + const mime = file.type || "image/png" + return { mime, base64 } +} + +async function ensureDirectoryExists(dirPath: string) { + try { + await mkdir(dirPath, { recursive: true }) + } catch (error) { + // Directory might already exist, that's fine + } +} + +function getDateBasedPath(baseDir?: string): string { + // Default to assets/images at repo root + if (!baseDir) { + // Navigate from .opencode/tool/ to repo root, then to assets/images + baseDir = resolve(process.cwd(), "../../assets/images") + } + const today = new Date().toISOString().split('T')[0] // YYYY-MM-DD format + return join(baseDir, today) +} + +async function getUniqueFilename(directory: string, baseName: string, extension: string, isEdit: boolean = false): Promise { + await ensureDirectoryExists(directory) + + if (!isEdit) { + // For generations, use timestamp if file exists + const baseFilename = join(directory, `${baseName}${extension}`) + const fileExists = await Bun.file(baseFilename).exists() + + if (!fileExists) { + return baseFilename + } + + // Add timestamp if file exists + const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5) // Remove milliseconds and Z + return join(directory, `${baseName}_${timestamp}${extension}`) + } + + // For edits, use incremental numbering + let counter = 1 + let filename: string + + do { + const editSuffix = `_edit_${counter.toString().padStart(3, '0')}` + filename = join(directory, `${baseName}${editSuffix}${extension}`) + counter++ + } while (await Bun.file(filename).exists()) + + return filename +} + +export async function generateImage(prompt: string, config: ImageConfig = {}): Promise { + const apiKey = await getGeminiApiKey() + + // Test mode - return mock response without API call + if (isTestMode()) { + const baseDir = config.outputDir || getDateBasedPath() + const generationsDir = join(baseDir, "generations") + let baseName = config.customName || "generated" + if (baseName.endsWith('.png') || baseName.endsWith('.jpg') || baseName.endsWith('.jpeg')) { + baseName = baseName.substring(0, baseName.lastIndexOf('.')) + } + const outputPath = await getUniqueFilename(generationsDir, baseName, ".png", false) + + return `[TEST MODE] Would generate image: ${outputPath} for prompt: "${prompt.substring(0, 50)}..."` + } + + const body = { + contents: [{ + parts: [{ text: prompt }] + }], + } + + const res = await fetch( + "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image-preview:generateContent", + { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-goog-api-key": apiKey, + }, + body: JSON.stringify(body), + } + ) + + if (!res.ok) { + const errorText = await res.text() + throw new Error(`API error (${res.status}): ${errorText}`) + } + + const json = await res.json() + + // Look for image data in the response + const candidates = json?.candidates + if (!candidates || candidates.length === 0) { + throw new Error("No candidates in response") + } + + const parts = candidates[0]?.content?.parts + if (!parts || parts.length === 0) { + throw new Error("No parts in response") + } + + let b64 = null + for (const part of parts) { + if (part.inlineData?.data) { + b64 = part.inlineData.data + break + } + } + + if (!b64) { + throw new Error("No image data returned from Nano Banana model") + } + + // Determine output path + const baseDir = config.outputDir || getDateBasedPath() + const generationsDir = join(baseDir, "generations") + + // Generate filename (remove extension if already present) + let baseName = config.customName || "generated" + if (baseName.endsWith('.png') || baseName.endsWith('.jpg') || baseName.endsWith('.jpeg')) { + baseName = baseName.substring(0, baseName.lastIndexOf('.')) + } + const extension = ".png" + const outputPath = await getUniqueFilename(generationsDir, baseName, extension, false) + + console.log(`Saving generated image to: ${outputPath}`) + await Bun.write(outputPath, Buffer.from(b64, "base64")) + + const fileExists = await Bun.file(outputPath).exists() + if (!fileExists) { + throw new Error(`Failed to save file to ${outputPath}`) + } + + const stats = await Bun.file(outputPath).stat() + return `Generated image saved: ${outputPath} (${stats.size} bytes)` +} + +export async function editImage(imagePath: string, prompt: string, config: ImageConfig = {}): Promise { + const apiKey = await getGeminiApiKey() + + // Test mode - return mock response without API call + if (isTestMode()) { + const baseDir = config.outputDir || getDateBasedPath() + const editsDir = join(baseDir, "edits") + const originalName = basename(imagePath, extname(imagePath)) + let baseName = config.customName || originalName + if (baseName.endsWith('.png') || baseName.endsWith('.jpg') || baseName.endsWith('.jpeg')) { + baseName = baseName.substring(0, baseName.lastIndexOf('.')) + } + const outputPath = await getUniqueFilename(editsDir, baseName, ".png", true) + + return `[TEST MODE] Would edit image: ${imagePath} -> ${outputPath} with prompt: "${prompt.substring(0, 50)}..."` + } + + // Parse the input image + const { mime, base64 } = await parseImageInput(imagePath) + + const body = { + contents: [{ + parts: [ + { text: prompt }, + { inlineData: { mimeType: mime, data: base64 } } + ] + }], + } + + const res = await fetch( + "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image-preview:generateContent", + { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-goog-api-key": apiKey, + }, + body: JSON.stringify(body), + } + ) + + if (!res.ok) { + const errorText = await res.text() + throw new Error(`API error (${res.status}): ${errorText}`) + } + + const json = await res.json() + + // Look for image data in the response + const candidates = json?.candidates + if (!candidates || candidates.length === 0) { + throw new Error("No candidates in response") + } + + const parts = candidates[0]?.content?.parts + if (!parts || parts.length === 0) { + throw new Error("No parts in response") + } + + let b64 = null + for (const part of parts) { + if (part.inlineData?.data) { + b64 = part.inlineData.data + break + } + } + + if (!b64) { + throw new Error("No image data returned from Nano Banana model") + } + + // Determine output path + const baseDir = config.outputDir || getDateBasedPath() + const editsDir = join(baseDir, "edits") + + // Extract original filename without extension + const originalName = basename(imagePath, extname(imagePath)) + let baseName = config.customName || originalName + if (baseName.endsWith('.png') || baseName.endsWith('.jpg') || baseName.endsWith('.jpeg')) { + baseName = baseName.substring(0, baseName.lastIndexOf('.')) + } + const extension = ".png" + + const outputPath = await getUniqueFilename(editsDir, baseName, extension, true) + + console.log(`Saving edited image to: ${outputPath}`) + await Bun.write(outputPath, Buffer.from(b64, "base64")) + + const fileExists = await Bun.file(outputPath).exists() + if (!fileExists) { + throw new Error(`Failed to save file to ${outputPath}`) + } + + const stats = await Bun.file(outputPath).stat() + return `Edited image saved: ${outputPath} (${stats.size} bytes)` +} + +export async function analyzeImage(imagePath: string, question: string): Promise { + const apiKey = await getGeminiApiKey() + + // Test mode - return mock response without API call + if (isTestMode()) { + return `[TEST MODE] Would analyze image: ${imagePath} with question: "${question.substring(0, 50)}..." - Mock analysis: This is a test image analysis response.` + } + + const { mime, base64 } = await parseImageInput(imagePath) + + const res = await fetch( + "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent", + { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-goog-api-key": apiKey, + }, + body: JSON.stringify({ + contents: [{ + parts: [ + { text: question }, + { inlineData: { mimeType: mime, data: base64 } } + ] + }], + }), + } + ) + + if (!res.ok) { + const errorText = await res.text() + throw new Error(`API error (${res.status}): ${errorText}`) + } + + const json = await res.json() + const text = json?.candidates?.[0]?.content?.parts?.[0]?.text + if (!text) { + throw new Error("No analysis returned") + } + + return text +} + +// Tool for generating images from text +export const generate = tool({ + description: "Generate an image using Gemini Nano Banana from text prompt", + args: { + prompt: tool.schema.string().describe("Text description of the image to generate"), + outputDir: tool.schema.string().optional().describe("Custom output directory (default: ./generated-images/YYYY-MM-DD/)"), + filename: tool.schema.string().optional().describe("Custom filename (default: generated)"), + }, + async execute(args, context) { + try { + const config: ImageConfig = { + outputDir: args.outputDir, + customName: args.filename, + } + return await generateImage(args.prompt, config) + } catch (error) { + return `Error: ${error.message}` + } + }, +}) + +// Tool for editing existing images +export const edit = tool({ + description: "Edit an existing image using Gemini Nano Banana", + args: { + image: tool.schema.string().describe("File path or data URL of image to edit"), + prompt: tool.schema.string().describe("Edit instruction"), + outputDir: tool.schema.string().optional().describe("Custom output directory (default: ./generated-images/YYYY-MM-DD/)"), + filename: tool.schema.string().optional().describe("Custom filename (default: original name with _edit_XXX)"), + }, + async execute(args, context) { + try { + const config: ImageConfig = { + outputDir: args.outputDir, + customName: args.filename, + } + return await editImage(args.image, args.prompt, config) + } catch (error) { + return `Error: ${error.message}` + } + }, +}) + +// Tool for analyzing images +export const analyze = tool({ + description: "Analyze an image using Gemini (text analysis only)", + args: { + image: tool.schema.string().describe("File path or data URL of image to analyze"), + question: tool.schema.string().describe("What to analyze about the image"), + }, + async execute(args, context) { + try { + return await analyzeImage(args.image, args.question) + } catch (error) { + return `Error: ${error.message}` + } + }, +}) + +// Default export for backward compatibility +export default edit \ No newline at end of file diff --git a/.opencode/tool/index.ts b/.opencode/tool/index.ts new file mode 100644 index 0000000..3b047df --- /dev/null +++ b/.opencode/tool/index.ts @@ -0,0 +1,27 @@ +/** + * OpenCode Gemini Tool - Main entry point + * + * This module provides image generation, editing, and analysis capabilities + * using Google's Gemini AI models, along with environment variable utilities. + */ + +// Gemini AI image tools +export { + generate, // Generate images from text prompts + edit, // Edit existing images with text instructions + analyze, // Analyze images and answer questions about them + generateImage, // Core image generation function + editImage, // Core image editing function + analyzeImage, // Core image analysis function + default as gemini // Default export (edit tool) +} from "./gemini" + +// Environment variable utilities +export { + loadEnvVariables, + getEnvVariable, + getRequiredEnvVariable, + getRequiredEnvVariables, + getApiKey, + type EnvLoaderConfig +} from "./env" \ No newline at end of file diff --git a/.opencode/tool/package.json b/.opencode/tool/package.json new file mode 100644 index 0000000..ce357d2 --- /dev/null +++ b/.opencode/tool/package.json @@ -0,0 +1,22 @@ +{ + "type": "module", + "name": "opencode-gemini-tool", + "version": "1.0.0", + "description": "Gemini image editing tool for OpenCode", + "main": "index.ts", + "scripts": { + "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", + "zod": "^4.1.9" + }, + "devDependencies": { + "@opencode-ai/plugin": "^0.10.0", + "@types/node": "^24.2.1", + "bun-types": "latest" + } +} \ No newline at end of file diff --git a/.opencode/tool/template/README.md b/.opencode/tool/template/README.md new file mode 100644 index 0000000..da1eeb8 --- /dev/null +++ b/.opencode/tool/template/README.md @@ -0,0 +1,35 @@ +# Tool Template + +This is a template for creating new tools in the modular structure. + +## How to Create a New Tool + +1. **Copy this template directory:** + ```bash + cp -r template/ your-tool-name/ + ``` + +2. **Edit `index.ts`:** + - Replace `exampleFunction` with your tool's logic + - Update the tool definition with proper description and args + - Export your functions and tool + +3. **Add to main index:** + - Add exports to `/tool/index.ts`: + ```typescript + export { yourTool, yourFunction } from "./your-tool-name" + ``` + +4. **Test your tool:** + ```bash + bun -e "import('./your-tool-name/index.ts').then(m => console.log('Exports:', Object.keys(m)))" + ``` + +## Structure + +``` +your-tool-name/ +└── index.ts # All tool functionality +``` + +Keep it simple - one file per tool with all functionality included. \ No newline at end of file diff --git a/.opencode/tool/template/index.ts b/.opencode/tool/template/index.ts new file mode 100644 index 0000000..fc213e4 --- /dev/null +++ b/.opencode/tool/template/index.ts @@ -0,0 +1,25 @@ +import { tool } from "@opencode-ai/plugin/tool" + +// Example tool implementation +export async function exampleFunction(input: string): Promise { + // Your tool logic here + return `Processed: ${input}` +} + +// Tool definition for OpenCode agent system +export const exampleTool = tool({ + description: "Example tool description", + args: { + input: tool.schema.string().describe("Input parameter description"), + }, + async execute(args, context) { + try { + return await exampleFunction(args.input) + } catch (error) { + return `Error: ${error.message}` + } + }, +}) + +// Default export +export default exampleTool \ No newline at end of file diff --git a/.opencode/tool/tsconfig.json b/.opencode/tool/tsconfig.json new file mode 100644 index 0000000..dc2a13b --- /dev/null +++ b/.opencode/tool/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", + "resolvePackageJsonExports": true, + "resolvePackageJsonImports": true, + "target": "ES2022", + "skipLibCheck": true, + "types": ["bun-types"] + }, + "include": ["*.ts"], + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/assets/.gitignore b/assets/.gitignore new file mode 100644 index 0000000..cc2e43f --- /dev/null +++ b/assets/.gitignore @@ -0,0 +1,6 @@ +# Ignore generated images but keep directory structure +images/*/ +!images/.gitkeep + +# Allow specific important images if needed +# !images/examples/ \ No newline at end of file diff --git a/assets/images/.gitkeep b/assets/images/.gitkeep new file mode 100644 index 0000000..6aa7453 --- /dev/null +++ b/assets/images/.gitkeep @@ -0,0 +1 @@ +# Keep this directory in git \ No newline at end of file diff --git a/env.example b/env.example index 16039ff..4d6ba21 100644 --- a/env.example +++ b/env.example @@ -18,3 +18,7 @@ TELEGRAM_CHECK_INTERVAL=30000 # Enable/disable the plugin (true/false) TELEGRAM_ENABLED=true + +# Gemini API Configuration +# Get your API key from https://makersuite.google.com/app/apikey +GEMINI_API_KEY=your_gemini_api_key_here