diff --git a/.claude/commands/create-spec.md b/.claude/commands/create-spec.md index f8cae28e..f7da2f27 100644 --- a/.claude/commands/create-spec.md +++ b/.claude/commands/create-spec.md @@ -457,7 +457,7 @@ Create a new file using this XML structure: **Output path:** `$ARGUMENTS/prompts/initializer_prompt.md` If the output directory has an existing `initializer_prompt.md`, read it and update the feature count. -If not, copy from `.claude/templates/initializer_prompt.template.md` first, then update. +If not, copy from `.opencode/templates/initializer_prompt.template.md` first, then update. **CRITICAL: You MUST update the feature count placeholder:** diff --git a/.env.example b/.env.example index 157af452..02b6400b 100644 --- a/.env.example +++ b/.env.example @@ -3,10 +3,14 @@ # CLI Command Selection # Choose which CLI command to use for the agent. -# - claude: Uses Anthropic's official Claude Code CLI (default) +# - opencode: Uses Opencode's CLI or SDK-backed workflow (default) # - glm: Uses GLM CLI (or any other compatible CLI) -# Defaults to 'claude' if not specified -# CLI_COMMAND=claude +# Defaults to 'opencode' if not specified +# CLI_COMMAND=opencode + +# Opencode API Key (preferred): +# Set OPENCODE_API_KEY to use the Opencode SDK directly instead of a CLI. +# OPENCODE_API_KEY=your_key_here # Playwright Browser Mode # Controls whether Playwright runs Chrome in headless mode (no visible browser window). diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..58092ddc --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,53 @@ +# Copilot Instructions for Contributors (Autocoder) + +Purpose: give AI coding agents (and humans) the immediate context and concrete commands needed to be productive in this repo. + +Quick-start workflow +- Python setup: `python -m venv venv && source venv/bin/activate && pip install -r requirements.txt`. +- Run the CLI agent: `python autonomous_agent_demo.py --project-dir `. +- YOLO (fast prototyping): add `--yolo` (skips Playwright-based browser tests and marks features as passing after lint/type-check). +- Web UI: in `ui/` run `npm ci` then `npm run dev` (dev) or `npm run build` (production). `start_ui.sh`/`start_ui.bat` serve `ui/dist/`. + +Key verification & CI commands +- Lint: `ruff check .` +- Security checks: `python test_security.py` (covers bash command allowlist and scripts like `init.sh`). +- Full local check (as used in project templates): `ruff check . && python test_security.py && cd ui && npm run lint && npm run build`. +- GitHub CI uses Python 3.11 and Node 20 (.github/workflows/ci.yml). + +Big-picture architecture & data flow (short) +- Agent runtime: `autonomous_agent_demo.py` (entry) -> `agent.py` (session loop) -> `client.py` (Opencode client adapter config, MCP servers, allowed tool lists). +- MCP servers: `mcp_server/feature_mcp.py` exposes feature management tools; Playwright MCP available for browser automation in non-YOLO mode. +- Features & tests: stored in project `features.db` (SQLAlchemy models in `api/database.py`). Agents operate via MCP tools like `mcp__features__feature_get_next` and `mcp__features__feature_mark_passing`. +- UI: FastAPI backend `start_ui.py` + `server/routers/*`; `ui/src/*` consumes REST + WebSocket (`/ws/projects/{project_name}`) for logs and progress. + +Project-specific conventions & important patterns +- Prompt fallback chain: project-specific `prompts/{name}.md` -> `.opencode/templates/{name}.template.md` (See `prompts.py`). +- YOLO mode: `--yolo` flag in `autonomous_agent_demo.py` and toggle in UI. In YOLO: + - Playwright MCP server is skipped + - Regression testing is skipped + - Features are marked passing on lint/type-check success +- Security: all bash commands are validated in `security.py` and tested in `test_security.py`. Example rules: + - `chmod +x init.sh` is allowed; numeric modes (`chmod 777`) and recursive `-R` are blocked. + - `./init.sh` is allowed; `bash init.sh` or other script names may be blocked. + - When changing the allowlist, update `test_security.py` accordingly. +- Tool allowlists and Playwright tooling are configured in `client.py`. Note: `mcp__playwright__browser_run_code` was removed due to crashes—avoid reintroducing without tests. + +Where to look first (most impact) +- `OPENCODE.md` — project overview & workflows (already maintained and authoritative). +- `client.py` — how agents are configured, allowed tools, and Playwright headless flag (`PLAYWRIGHT_HEADLESS`). +- `security.py` & `test_security.py` — bash security rules and tests. +- `mcp_server/feature_mcp.py` — available MCP tools and their behavior. +- `autonomous_agent_demo.py` / `agent.py` — agent lifecycle and prompts used. +- `ui/` — run and build steps; `ui/src/hooks/useWebSocket.ts` and `ui/src/hooks/useProjects.ts` for real-time behavior. + +Examples agents should follow +- To run the same checks CI uses: `ruff check . && python test_security.py && cd ui && npm run lint && npm run build`. +- To run agent locally with Playwright (standard mode): `python autonomous_agent_demo.py --project-dir ` (ensure Playwright is available or use default environment from `start.sh`). +- When adding an MCP tool, add a corresponding test and document it in `mcp_server/feature_mcp.py` and `client.py`. + +Editing guidelines +- Keep prompt changes additive and preserve existing prompt templates in `.opencode/templates`. (rename `.claude/templates` → `.opencode/templates`) +- If you change security rules or allowed commands, add/adjust tests in `test_security.py`. +- When updating the UI, remember the `start_ui.*` scripts serve the pre-built `ui/dist/`; a `npm run build` is required before using those scripts. + +If anything here is unclear or you'd like a different level of detail (examples or links to specific functions), tell me which sections to expand and I'll iterate. ✅ diff --git a/.claude/agents/code-review.md b/.opencode/agents/code-review.md similarity index 99% rename from .claude/agents/code-review.md rename to .opencode/agents/code-review.md index ddccdb7f..fb7b1bac 100644 --- a/.claude/agents/code-review.md +++ b/.opencode/agents/code-review.md @@ -123,14 +123,14 @@ Structure your review as follows: - Be thorough but constructive - explain why something is an issue - Provide specific, actionable feedback with examples - Acknowledge good code when you see it -- Consider the project's existing patterns and conventions (from CLAUDE.md) +- Consider the project's existing patterns and conventions (from OPENCODE.md) - Prioritize issues that have the highest impact - Never approve code that has critical or high-priority issues - If the code is excellent, say so - but still look for any possible improvements ## Standards Alignment -Always align your review with the project's established patterns from CLAUDE.md, including: +Always align your review with the project's established patterns from OPENCODE.md, including: - The project's architecture and design patterns - Existing coding conventions - Technology-specific best practices diff --git a/.claude/agents/coder.md b/.opencode/agents/coder.md similarity index 99% rename from .claude/agents/coder.md rename to .opencode/agents/coder.md index d09f9074..19e21596 100644 --- a/.claude/agents/coder.md +++ b/.opencode/agents/coder.md @@ -22,7 +22,7 @@ Before writing ANY code, you MUST: - Existing similar implementations to use as reference - Configuration files (package.json, pyproject.toml, tsconfig.json, etc.) - README files and documentation - - CLAUDE.md or similar project instruction files + - OPENCODE.md or similar project instruction files 2. **Identify Patterns and Standards**: Search for and document: - Naming conventions (files, functions, classes, variables) diff --git a/.claude/agents/deep-dive.md b/.opencode/agents/deep-dive.md similarity index 99% rename from .claude/agents/deep-dive.md rename to .opencode/agents/deep-dive.md index 9dba4c59..41f12b43 100644 --- a/.claude/agents/deep-dive.md +++ b/.opencode/agents/deep-dive.md @@ -83,7 +83,7 @@ You have access to powerful tools - USE THEM EXTENSIVELY: 4. **Risk Awareness**: Always consider what could go wrong. Security, performance, maintainability, edge cases. -5. **Context Sensitivity**: Align recommendations with the project's existing patterns, constraints, and standards (including any CLAUDE.md guidance). +5. **Context Sensitivity**: Align recommendations with the project's existing patterns, constraints, and standards (including any OPENCODE.md guidance). ## Output Structure diff --git a/.claude/commands/check-code.md b/.opencode/commands/check-code.md similarity index 100% rename from .claude/commands/check-code.md rename to .opencode/commands/check-code.md diff --git a/.claude/commands/checkpoint.md b/.opencode/commands/checkpoint.md similarity index 96% rename from .claude/commands/checkpoint.md rename to .opencode/commands/checkpoint.md index 4787866b..dfcbd14b 100644 --- a/.claude/commands/checkpoint.md +++ b/.opencode/commands/checkpoint.md @@ -37,4 +37,4 @@ IMPORTANT: - Do NOT skip any files - include everything - Make the commit message descriptive enough that someone reviewing the git log can understand what was accomplished - Follow the project's existing commit message conventions (check git log first) -- Include the Claude Code co-author attribution in the commit message +- Include the Opencode co-author attribution in the commit message diff --git a/.opencode/commands/create-spec.md b/.opencode/commands/create-spec.md new file mode 100644 index 00000000..f7da2f27 --- /dev/null +++ b/.opencode/commands/create-spec.md @@ -0,0 +1,578 @@ +--- +description: Create an app spec for autonomous coding (project) +--- + +# PROJECT DIRECTORY + +This command **requires** the project directory as an argument via `$ARGUMENTS`. + +**Example:** `/create-spec generations/my-app` + +**Output location:** `$ARGUMENTS/prompts/app_spec.txt` and `$ARGUMENTS/prompts/initializer_prompt.md` + +If `$ARGUMENTS` is empty, inform the user they must provide a project path and exit. + +--- + +# GOAL + +Help the user create a comprehensive project specification for a long-running autonomous coding process. This specification will be used by AI coding agents to build their application across multiple sessions. + +This tool works for projects of any size - from simple utilities to large-scale applications. + +--- + +# YOUR ROLE + +You are the **Spec Creation Assistant** - an expert at translating project ideas into detailed technical specifications. Your job is to: + +1. Understand what the user wants to build (in their own words) +2. Ask about features and functionality (things anyone can describe) +3. **Derive** the technical details (database, API, architecture) from their requirements +4. Generate the specification files that autonomous coding agents will use + +**IMPORTANT: Cater to all skill levels.** Many users are product owners or have functional knowledge but aren't technical. They know WHAT they want to build, not HOW to build it. You should: + +- Ask questions anyone can answer (features, user flows, what screens exist) +- **Derive** technical details (database schema, API endpoints, architecture) yourself +- Only ask technical questions if the user wants to be involved in those decisions + +**Use conversational questions** to gather information. For questions with clear options, present them as numbered choices that the user can select from. For open-ended exploration, use natural conversation. + +--- + +# CONVERSATION FLOW + +There are two paths through this process: + +**Quick Path** (recommended for most users): You describe what you want, agent derives the technical details +**Detailed Path**: You want input on technology choices, database design, API structure, etc. + +**CRITICAL: This is a CONVERSATION, not a form.** + +- Ask questions for ONE phase at a time +- WAIT for the user to respond before moving to the next phase +- Acknowledge their answers before continuing +- Do NOT bundle multiple phases into one message + +--- + +## Phase 1: Project Overview + +Start with simple questions anyone can answer: + +1. **Project Name**: What should this project be called? +2. **Description**: In your own words, what are you building and what problem does it solve? +3. **Target Audience**: Who will use this? + +**IMPORTANT: Ask these questions and WAIT for the user to respond before continuing.** +Do NOT immediately jump to Phase 2. Let the user answer, acknowledge their responses, then proceed. + +--- + +## Phase 2: Involvement Level + +Ask the user about their involvement preference: + +> "How involved do you want to be in technical decisions? +> +> 1. **Quick Mode (Recommended)** - You describe what you want, I'll handle database, API, and architecture +> 2. **Detailed Mode** - You want input on technology choices and architecture decisions +> +> Which would you prefer?" + +**If Quick Mode**: Skip to Phase 3, then go to Phase 4 (Features). You will derive technical details yourself. +**If Detailed Mode**: Go through all phases, asking technical questions. + +## Phase 3: Technology Preferences + +**For Quick Mode users**, also ask about tech preferences: + +> "Any technology preferences, or should I choose sensible defaults? +> +> 1. **Use defaults (Recommended)** - React, Node.js, SQLite - solid choices for most apps +> 2. **I have preferences** - I'll specify my preferred languages/frameworks" + +**For Detailed Mode users**, ask specific tech questions about frontend, backend, database, etc. + +## Phase 4: Features (THE MAIN PHASE) + +This is where you spend most of your time. Ask questions in plain language that anyone can answer. + +**Start broad with open conversation:** + +> "Walk me through your app. What does a user see when they first open it? What can they do?" + +**Then ask about key feature areas:** + +> "Let me ask about a few common feature areas: +> +> 1. **User Accounts** - Do users need to log in / have accounts? (Yes with profiles, No anonymous use, or Maybe optional) +> 2. **Mobile Support** - Should this work well on mobile phones? (Yes fully responsive, Desktop only, or Basic mobile) +> 3. **Search** - Do users need to search or filter content? (Yes, No, or Basic only) +> 4. **Sharing** - Any sharing or collaboration features? (Yes, No, or Maybe later)" + +**Then drill into the "Yes" answers with open conversation:** + +**4a. The Main Experience** + +- What's the main thing users do in your app? +- Walk me through a typical user session + +**4b. User Accounts** (if they said Yes) + +- What can they do with their account? +- Any roles or permissions? + +**4c. What Users Create/Manage** + +- What "things" do users create, save, or manage? +- Can they edit or delete these things? +- Can they organize them (folders, tags, categories)? + +**4d. Settings & Customization** + +- What should users be able to customize? +- Light/dark mode? Other display preferences? + +**4e. Search & Finding Things** (if they said Yes) + +- What do they search for? +- What filters would be helpful? + +**4f. Sharing & Collaboration** (if they said Yes) + +- What can be shared? +- View-only or collaborative editing? + +**4g. Any Dashboards or Analytics?** + +- Does the user see any stats, reports, or metrics? + +**4h. Domain-Specific Features** + +- What else is unique to your app? +- Any features we haven't covered? + +**4i. Security & Access Control (if app has authentication)** + +Ask about user roles: + +> "Who are the different types of users? +> +> 1. **Just regular users** - Everyone has the same permissions +> 2. **Users + Admins** - Regular users and administrators with extra powers +> 3. **Multiple roles** - Several distinct user types (e.g., viewer, editor, manager, admin)" + +**If multiple roles, explore in conversation:** + +- What can each role see? +- What can each role do? +- Are there pages only certain roles can access? +- What happens if someone tries to access something they shouldn't? + +**Also ask about authentication:** + +- How do users log in? (email/password, social login, SSO) +- Password requirements? (for security testing) +- Session timeout? Auto-logout after inactivity? +- Any sensitive operations requiring extra confirmation? + +**4j. Data Flow & Integration** + +- What data do users create vs what's system-generated? +- Are there workflows that span multiple steps or pages? +- What happens to related data when something is deleted? +- Are there any external systems or APIs to integrate with? +- Any import/export functionality? + +**4k. Error & Edge Cases** + +- What should happen if the network fails mid-action? +- What about duplicate entries (e.g., same email twice)? +- Very long text inputs? +- Empty states (what shows when there's no data)? + +**Keep asking follow-up questions until you have a complete picture.** For each feature area, understand: + +- What the user sees +- What actions they can take +- What happens as a result +- Who is allowed to do it (permissions) +- What errors could occur + +## Phase 4L: Derive Feature Count (DO NOT ASK THE USER) + +After gathering all features, **you** (the agent) should tally up the testable features. Do NOT ask the user how many features they want - derive it from what was discussed. + +**Typical ranges for reference:** + +- **Simple apps** (todo list, calculator, notes): ~20-50 features +- **Medium apps** (blog, task manager with auth): ~100 features +- **Advanced apps** (e-commerce, CRM, full SaaS): ~150-200 features + +These are just reference points - your actual count should come from the requirements discussed. + +**How to count features:** +For each feature area discussed, estimate the number of discrete, testable behaviors: + +- Each CRUD operation = 1 feature (create, read, update, delete) +- Each UI interaction = 1 feature (click, drag, hover effect) +- Each validation/error case = 1 feature +- Each visual requirement = 1 feature (styling, animation, responsive behavior) + +**Present your estimate to the user:** + +> "Based on what we discussed, here's my feature breakdown: +> +> - [Category 1]: ~X features +> - [Category 2]: ~Y features +> - [Category 3]: ~Z features +> - ... +> +> **Total: ~N features** +> +> Does this seem right, or should I adjust?" + +Let the user confirm or adjust. This becomes your `feature_count` for the spec. + +## Phase 5: Technical Details (DERIVED OR DISCUSSED) + +**For Quick Mode users:** +Tell them: "Based on what you've described, I'll design the database, API, and architecture. Here's a quick summary of what I'm planning..." + +Then briefly outline: + +- Main data entities you'll create (in plain language: "I'll create tables for users, projects, documents, etc.") +- Overall app structure ("sidebar navigation with main content area") +- Any key technical decisions + +Ask: "Does this sound right? Any concerns?" + +**For Detailed Mode users:** +Walk through each technical area: + +**5a. Database Design** + +- What entities/tables are needed? +- Key fields for each? +- Relationships? + +**5b. API Design** + +- What endpoints are needed? +- How should they be organized? + +**5c. UI Layout** + +- Overall structure (columns, navigation) +- Key screens/pages +- Design preferences (colors, themes) + +**5d. Implementation Phases** + +- What order to build things? +- Dependencies? + +## Phase 6: Success Criteria + +Ask in simple terms: + +> "What does 'done' look like for you? When would you consider this app complete and successful?" + +Prompt for: + +- Must-have functionality +- Quality expectations (polished vs functional) +- Any specific requirements + +## Phase 7: Review & Approval + +Present everything gathered: + +1. **Summary of the app** (in plain language) +2. **Feature count** +3. **Technology choices** (whether specified or derived) +4. **Brief technical plan** (for their awareness) + +First ask in conversation if they want to make changes. + +**Then ask for final confirmation:** + +> "Ready to generate the specification files? +> +> 1. **Yes, generate files** - Create app_spec.txt and update prompt files +> 2. **I have changes** - Let me add or modify something first" + +--- + +# FILE GENERATION + +**Note: This section is for YOU (the agent) to execute. Do not burden the user with these technical details.** + +## Output Directory + +The output directory is: `$ARGUMENTS/prompts/` + +Once the user approves, generate these files: + +## 1. Generate `app_spec.txt` + +**Output path:** `$ARGUMENTS/prompts/app_spec.txt` + +Create a new file using this XML structure: + +```xml + + [Project Name] + + + [2-3 sentence description from Phase 1] + + + + + [Framework] + [Styling solution] + [Additional frontend config] + + + [Runtime] + [Database] + [Additional backend config] + + + [API style] + [Additional communication config] + + + + + + [Setup requirements] + + + + [derived count from Phase 4L] + + + + + + - [Can do X] + - [Can see Y] + - [Cannot access Z] + + + - /admin/* (admin only) + - /settings (authenticated users) + + + [Repeat for each role] + + + [email/password | social | SSO] + [duration or "none"] + [if applicable] + + + - [Delete account requires password confirmation] + - [Financial actions require 2FA] + + + + + <[category_name]> + - [Feature 1] + - [Feature 2] + - [Feature 3] + + [Repeat for all feature categories] + + + + + <[table_name]> + - [field1], [field2], [field3] + - [additional fields] + + [Repeat for all tables] + + + + + <[category]> + - [VERB] /api/[path] + - [VERB] /api/[path] + + [Repeat for all categories] + + + + + [Layout description] + + [Additional UI sections as needed] + + + + + [Colors] + + + [Font preferences] + + + + + + [Phase Title] + + - [Task 1] + - [Task 2] + + + [Repeat for all phases] + + + + + [Functionality criteria] + + + [UX criteria] + + + [Technical criteria] + + + [Design criteria] + + + +``` + +## 2. Update `initializer_prompt.md` + +**Output path:** `$ARGUMENTS/prompts/initializer_prompt.md` + +If the output directory has an existing `initializer_prompt.md`, read it and update the feature count. +If not, copy from `.opencode/templates/initializer_prompt.template.md` first, then update. + +**CRITICAL: You MUST update the feature count placeholder:** + +1. Find the line containing `**[FEATURE_COUNT]**` in the "REQUIRED FEATURE COUNT" section +2. Replace `[FEATURE_COUNT]` with the exact number agreed upon in Phase 4L (e.g., `25`) +3. The result should read like: `You must create exactly **25** features using the...` + +**Example edit:** +``` +Before: **CRITICAL:** You must create exactly **[FEATURE_COUNT]** features using the `feature_create_bulk` tool. +After: **CRITICAL:** You must create exactly **25** features using the `feature_create_bulk` tool. +``` + +**Verify the update:** After editing, read the file again to confirm the feature count appears correctly. If `[FEATURE_COUNT]` still appears in the file, the update failed and you must try again. + +**Note:** You may also update `coding_prompt.md` if the user requests changes to how the coding agent should work. Include it in the status file if modified. + +## 3. Write Status File (REQUIRED - Do This Last) + +**Output path:** `$ARGUMENTS/prompts/.spec_status.json` + +**CRITICAL:** After you have completed ALL requested file changes, write this status file to signal completion to the UI. This is required for the "Continue to Project" button to appear. + +Write this JSON file: + +```json +{ + "status": "complete", + "version": 1, + "timestamp": "[current ISO 8601 timestamp, e.g., 2025-01-15T14:30:00.000Z]", + "files_written": [ + "prompts/app_spec.txt", + "prompts/initializer_prompt.md" + ], + "feature_count": [the feature count from Phase 4L] +} +``` + +**Include ALL files you modified** in the `files_written` array. If the user asked you to also modify `coding_prompt.md`, include it: + +```json +{ + "status": "complete", + "version": 1, + "timestamp": "2025-01-15T14:30:00.000Z", + "files_written": [ + "prompts/app_spec.txt", + "prompts/initializer_prompt.md", + "prompts/coding_prompt.md" + ], + "feature_count": 35 +} +``` + +**IMPORTANT:** +- Write this file LAST, after all other files are successfully written +- Only write it when you consider ALL requested work complete +- The UI polls this file to detect completion and show the Continue button +- If the user asks for additional changes after you've written this, you may update it again when the new changes are complete + +--- + +# AFTER FILE GENERATION: NEXT STEPS + +Once files are generated, tell the user what to do next: + +> "Your specification files have been created in `$ARGUMENTS/prompts/`! +> +> **Files created:** +> - `$ARGUMENTS/prompts/app_spec.txt` +> - `$ARGUMENTS/prompts/initializer_prompt.md` +> +> The **Continue to Project** button should now appear. Click it to start the autonomous coding agent! +> +> **If you don't see the button:** Type `/exit` or click **Exit to Project** in the header. +> +> **Important timing expectations:** +> +> - **First session:** The agent generates features in the database. This takes several minutes. +> - **Subsequent sessions:** Each coding iteration takes 5-15 minutes depending on complexity. +> - **Full app:** Building all [X] features will take many hours across multiple sessions. +> +> **Controls:** +> +> - Press `Ctrl+C` to pause the agent at any time +> - Run `start.bat` (Windows) or `./start.sh` (Mac/Linux) to resume where you left off" + +Replace `[X]` with their feature count. + +--- + +# IMPORTANT REMINDERS + +- **Meet users where they are**: Not everyone is technical. Ask about what they want, not how to build it. +- **Quick Mode is the default**: Most users should be able to describe their app and let you handle the technical details. +- **Derive, don't interrogate**: For non-technical users, derive database schema, API endpoints, and architecture from their feature descriptions. Don't ask them to specify these. +- **Use plain language**: Instead of "What entities need CRUD operations?", ask "What things can users create, edit, or delete?" +- **Be thorough on features**: This is where to spend time. Keep asking follow-up questions until you have a complete picture. +- **Derive feature count, don't guess**: After gathering requirements, tally up testable features yourself and present the estimate. Don't use fixed tiers or ask users to guess. +- **Validate before generating**: Present a summary including your derived feature count and get explicit approval before creating files. + +--- + +# BEGIN + +Start by greeting the user warmly. Ask ONLY the Phase 1 questions: + +> "Hi! I'm here to help you create a detailed specification for your app. +> +> Let's start with the basics: +> +> 1. What do you want to call this project? +> 2. In your own words, what are you building? +> 3. Who will use it - just you, or others too?" + +**STOP HERE and wait for their response.** Do not ask any other questions yet. Do not use AskUserQuestion yet. Just have a conversation about their project basics first. + +After they respond, acknowledge what they said, then move to Phase 2. diff --git a/.claude/commands/expand-project.md b/.opencode/commands/expand-project.md similarity index 100% rename from .claude/commands/expand-project.md rename to .opencode/commands/expand-project.md diff --git a/.claude/skills/frontend-design/LICENSE.txt b/.opencode/skills/frontend-design/LICENSE.txt similarity index 100% rename from .claude/skills/frontend-design/LICENSE.txt rename to .opencode/skills/frontend-design/LICENSE.txt diff --git a/.claude/skills/frontend-design/SKILL.md b/.opencode/skills/frontend-design/SKILL.md similarity index 95% rename from .claude/skills/frontend-design/SKILL.md rename to .opencode/skills/frontend-design/SKILL.md index 5be498e2..9b5e5490 100644 --- a/.claude/skills/frontend-design/SKILL.md +++ b/.opencode/skills/frontend-design/SKILL.md @@ -39,4 +39,4 @@ Interpret creatively and make unexpected choices that feel genuinely designed fo **IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well. -Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision. +Remember: Opencode is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision. diff --git a/.claude/templates/app_spec.template.txt b/.opencode/templates/app_spec.template.txt similarity index 99% rename from .claude/templates/app_spec.template.txt rename to .opencode/templates/app_spec.template.txt index ebdfb856..b5b082b1 100644 --- a/.claude/templates/app_spec.template.txt +++ b/.opencode/templates/app_spec.template.txt @@ -5,7 +5,7 @@ This is a placeholder template. Replace with your actual project specification. You can either: - 1. Use the /create-spec command to generate this interactively with Claude + 1. Use the /create-spec command to generate this interactively with Opencode 2. Manually edit this file following the structure below See existing projects in generations/ for examples of complete specifications. diff --git a/.claude/templates/coding_prompt.template.md b/.opencode/templates/coding_prompt.template.md similarity index 98% rename from .claude/templates/coding_prompt.template.md rename to .opencode/templates/coding_prompt.template.md index 823d2972..8c63f17b 100644 --- a/.claude/templates/coding_prompt.template.md +++ b/.opencode/templates/coding_prompt.template.md @@ -18,7 +18,7 @@ ls -la cat app_spec.txt # 4. Read progress notes from previous sessions (last 500 lines to avoid context overflow) -tail -500 claude-progress.txt +tail -500 opencode-progress.txt # 5. Check recent git history git log --oneline -20 @@ -138,7 +138,7 @@ If you must skip (truly external blocker only): Use the feature_skip tool with feature_id={id} ``` -Document the SPECIFIC external blocker in `claude-progress.txt`. "Functionality not built" is NEVER a valid reason. +Document the SPECIFIC external blocker in `opencode-progress.txt`. "Functionality not built" is NEVER a valid reason. ### STEP 5: IMPLEMENT THE FEATURE @@ -290,7 +290,7 @@ git commit -m "Implement [feature name] - verified end-to-end ### STEP 9: UPDATE PROGRESS NOTES -Update `claude-progress.txt` with: +Update `opencode-progress.txt` with: - What you accomplished this session - Which test(s) you completed @@ -303,7 +303,7 @@ Update `claude-progress.txt` with: Before context fills up: 1. Commit all working code -2. Update claude-progress.txt +2. Update opencode-progress.txt 3. Mark features as passing if tests verified 4. Ensure no uncommitted changes 5. Leave app in working state (no broken features) diff --git a/.claude/templates/coding_prompt_yolo.template.md b/.opencode/templates/coding_prompt_yolo.template.md similarity index 97% rename from .claude/templates/coding_prompt_yolo.template.md rename to .opencode/templates/coding_prompt_yolo.template.md index 1ab2179a..8e9c0b47 100644 --- a/.claude/templates/coding_prompt_yolo.template.md +++ b/.opencode/templates/coding_prompt_yolo.template.md @@ -29,7 +29,7 @@ ls -la cat app_spec.txt # 4. Read progress notes from previous sessions (last 500 lines to avoid context overflow) -tail -500 claude-progress.txt +tail -500 opencode-progress.txt # 5. Check recent git history git log --oneline -20 @@ -105,7 +105,7 @@ If you must skip (truly external blocker only): Use the feature_skip tool with feature_id={id} ``` -Document the SPECIFIC external blocker in `claude-progress.txt`. "Functionality not built" is NEVER a valid reason. +Document the SPECIFIC external blocker in `opencode-progress.txt`. "Functionality not built" is NEVER a valid reason. ### STEP 4: IMPLEMENT THE FEATURE @@ -172,7 +172,7 @@ git commit -m "Implement [feature name] - YOLO mode ### STEP 8: UPDATE PROGRESS NOTES -Update `claude-progress.txt` with: +Update `opencode-progress.txt` with: - What you accomplished this session - Which feature(s) you completed @@ -185,7 +185,7 @@ Update `claude-progress.txt` with: Before context fills up: 1. Commit all working code -2. Update claude-progress.txt +2. Update opencode-progress.txt 3. Mark features as passing if lint/type-check verified 4. Ensure no uncommitted changes 5. Leave app in working state diff --git a/.claude/templates/initializer_prompt.template.md b/.opencode/templates/initializer_prompt.template.md similarity index 99% rename from .claude/templates/initializer_prompt.template.md rename to .opencode/templates/initializer_prompt.template.md index 312cd179..c29934ae 100644 --- a/.claude/templates/initializer_prompt.template.md +++ b/.opencode/templates/initializer_prompt.template.md @@ -511,7 +511,7 @@ Remember: Before your context fills up: 1. Commit all work with descriptive messages -2. Create `claude-progress.txt` with a summary of what you accomplished +2. Create `opencode-progress.txt` with a summary of what you accomplished 3. Verify features were created using the feature_get_stats tool 4. Leave the environment in a clean, working state diff --git a/CLAUDE.md b/CLAUDE.md index 51c09493..1ed09f25 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,10 +1,10 @@ -# CLAUDE.md +# OPENCODE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +This file provides guidance to Opencode (opencode.ai) when working with code in this repository. ## Project Overview -This is an autonomous coding agent system with a React-based UI. It uses the Claude Agent SDK to build complete applications over multiple sessions using a two-agent pattern: +This is an autonomous coding agent system with a React-based UI. It uses the Opencode SDK to build complete applications over multiple sessions using a two-agent pattern: 1. **Initializer Agent** - First session reads an app spec and creates features in a SQLite database 2. **Coding Agent** - Subsequent sessions implement features one by one, marking them as passing @@ -43,6 +43,9 @@ python start.py python autonomous_agent_demo.py --project-dir C:/Projects/my-app python autonomous_agent_demo.py --project-dir my-app # if registered +# To use the Opencode SDK, install it into your environment: +# pip install --pre opencode-ai + # YOLO mode: rapid prototyping without browser testing python autonomous_agent_demo.py --project-dir my-app --yolo ``` @@ -160,21 +163,21 @@ Defense-in-depth approach configured in `client.py`: ## Claude Code Integration -- `.claude/commands/create-spec.md` - `/create-spec` slash command for interactive spec creation -- `.claude/skills/frontend-design/SKILL.md` - Skill for distinctive UI design -- `.claude/templates/` - Prompt templates copied to new projects +- `.opencode/commands/create-spec.md` - `/create-spec` slash command for interactive spec creation +- `.opencode/skills/frontend-design/SKILL.md` - Skill for distinctive UI design +- `.opencode/templates/` - Prompt templates copied to new projects ## Key Patterns ### Prompt Loading Fallback Chain 1. Project-specific: `{project_dir}/prompts/{name}.md` -2. Base template: `.claude/templates/{name}.template.md` +2. Base template: `.opencode/templates/{name}.template.md` ### Agent Session Flow 1. Check if `features.db` has features (determines initializer vs coding agent) -2. Create ClaudeSDKClient with security settings +2. Create an Opencode client with security settings 3. Send prompt and stream response 4. Auto-continue with 3-second delay between sessions diff --git a/OPENCODE.md b/OPENCODE.md new file mode 100644 index 00000000..4bb160cc --- /dev/null +++ b/OPENCODE.md @@ -0,0 +1,197 @@ +# OPENCODE.md + +This file provides guidance to Opencode (opencode.ai) when working with code in this repository. + +## Project Overview + +This is an autonomous coding agent system with a React-based UI. It uses the Opencode SDK to build complete applications over multiple sessions using a two-agent pattern: + +1. **Initializer Agent** - First session reads an app spec and creates features in a SQLite database +2. **Coding Agent** - Subsequent sessions implement features one by one, marking them as passing + +## Commands + +### Quick Start (Recommended) + +```bash +# Windows - launches CLI menu +start.bat + +# macOS/Linux +./start.sh + +# Launch Web UI (serves pre-built React app) +start_ui.bat # Windows +./start_ui.sh # macOS/Linux +``` + +### Python Backend (Manual) + +```bash +# Create and activate virtual environment +python -m venv venv +venv\Scripts\activate # Windows +source venv/bin/activate # macOS/Linux + +# Install dependencies +pip install -r requirements.txt + +# Run the main CLI launcher +python start.py + +# Run agent directly for a project (use absolute path or registered name) +python autonomous_agent_demo.py --project-dir C:/Projects/my-app +python autonomous_agent_demo.py --project-dir my-app # if registered + +# To use the Opencode SDK, install it into your environment: +# pip install --pre opencode-ai + +# YOLO mode: rapid prototyping without browser testing +python autonomous_agent_demo.py --project-dir my-app --yolo +``` + +### YOLO Mode (Rapid Prototyping) + +YOLO mode skips all testing for faster feature iteration: + +```bash +# CLI +python autonomous_agent_demo.py --project-dir my-app --yolo + +# UI: Toggle the lightning bolt button before starting the agent +``` + +**What's different in YOLO mode:** +- No regression testing (skips `feature_get_for_regression`) +- No Playwright MCP server (browser automation disabled) +- Features marked passing after lint/type-check succeeds +- Faster iteration for prototyping + +**What's the same:** +- Lint and type-check still run to verify code compiles +- Feature MCP server for tracking progress +- All other development tools available + +**When to use:** Early prototyping when you want to quickly scaffold features without verification overhead. Switch back to standard mode for production-quality development. + +### React UI (in ui/ directory) + +```bash +cd ui +npm install +npm run dev # Development server (hot reload) +npm run build # Production build (required for start_ui.bat) +npm run lint # Run ESLint +``` + +**Note:** The `start_ui.bat` script serves the pre-built UI from `ui/dist/`. After making UI changes, run `npm run build` in the `ui/` directory. + +## Architecture + +### Core Python Modules + +- `start.py` - CLI launcher with project creation/selection menu +- `autonomous_agent_demo.py` - Entry point for running the agent +- `agent.py` - Agent session loop +- `client.py` - Opencode client configuration with security hooks and MCP servers +- `security.py` - Bash command allowlist validation (ALLOWED_COMMANDS whitelist) +- `prompts.py` - Prompt template loading with project-specific fallback +- `progress.py` - Progress tracking, database queries, webhook notifications +- `registry.py` - Project registry for mapping names to paths (cross-platform) + +### Project Registry + +Projects can be stored in any directory. The registry maps project names to paths using SQLite: +- **All platforms**: `~/.autocoder/registry.db` + +The registry uses: +- SQLite database with SQLAlchemy ORM +- POSIX path format (forward slashes) for cross-platform compatibility +- SQLite's built-in transaction handling for concurrency safety + +### Server API (server/) + +The FastAPI server provides REST endpoints for the UI: + +- `server/routers/projects.py` - Project CRUD with registry integration +- `server/routers/features.py` - Feature management +- `server/routers/agent.py` - Agent control (start/stop/pause/resume) +- `server/routers/filesystem.py` - Filesystem browser API with security controls +- `server/routers/spec_creation.py` - WebSocket for interactive spec creation + +### Feature Management + +Features are stored in SQLite (`features.db`) via SQLAlchemy. The agent interacts with features through an MCP server: + +- `mcp_server/feature_mcp.py` - MCP server exposing feature management tools +- `api/database.py` - SQLAlchemy models (Feature table with priority, category, name, description, steps, passes) + +MCP tools available to the agent: +- `feature_get_stats` - Progress statistics +- `feature_get_next` - Get highest-priority pending feature +- `feature_get_for_regression` - Random passing features for regression testing +- `feature_mark_passing` - Mark feature complete +- `feature_skip` - Move feature to end of queue +- `feature_create_bulk` - Initialize all features (used by initializer) + +### React UI (ui/) + +- Tech stack: React 18, TypeScript, TanStack Query, Tailwind CSS v4, Radix UI +- `src/App.tsx` - Main app with project selection, kanban board, agent controls +- `src/hooks/useWebSocket.ts` - Real-time updates via WebSocket +- `src/hooks/useProjects.ts` - React Query hooks for API calls +- `src/lib/api.ts` - REST API client +- `src/lib/types.ts` - TypeScript type definitions +- `src/components/FolderBrowser.tsx` - Server-side filesystem browser for project folder selection +- `src/components/NewProjectModal.tsx` - Multi-step project creation wizard + +### Project Structure for Generated Apps + +Projects can be stored in any directory (registered in `~/.autocoder/registry.db`). Each project contains: +- `prompts/app_spec.txt` - Application specification (XML format) +- `prompts/initializer_prompt.md` - First session prompt +- `prompts/coding_prompt.md` - Continuation session prompt +- `features.db` - SQLite database with feature test cases +- `.agent.lock` - Lock file to prevent multiple agent instances + +### Security Model + +Defense-in-depth approach configured in `client.py`: +1. OS-level sandbox for bash commands +2. Filesystem restricted to project directory only +3. Bash commands validated against `ALLOWED_COMMANDS` in `security.py` + +## Opencode Integration + +- `.opencode/commands/create-spec.md` - `/create-spec` slash command for interactive spec creation +- `.opencode/skills/frontend-design/SKILL.md` - Skill for distinctive UI design +- `.opencode/templates/` - Prompt templates copied to new projects + +## Key Patterns + +### Prompt Loading Fallback Chain + +1. Project-specific: `{project_dir}/prompts/{name}.md` +2. Base template: `.opencode/templates/{name}.template.md` + +### Agent Session Flow + +1. Check if `features.db` has features (determines initializer vs coding agent) +2. Create an Opencode client with security settings +3. Send prompt and stream response +4. Auto-continue with 3-second delay between sessions + +### Real-time UI Updates + +The UI receives updates via WebSocket (`/ws/projects/{project_name}`): +- `progress` - Test pass counts +- `agent_status` - Running/paused/stopped/crashed +- `log` - Agent output lines (streamed from subprocess stdout) +- `feature_update` - Feature status changes + +### Design System + +The UI uses a **neobrutalism** design with Tailwind CSS v4: +- CSS variables defined in `ui/src/styles/globals.css` via `@theme` directive +- Custom animations: `animate-slide-in`, `animate-pulse-neo`, `animate-shimmer` +- Color tokens: `--color-neo-pending` (yellow), `--color-neo-progress` (cyan), `--color-neo-done` (green) diff --git a/README.md b/README.md index a5f62316..e55942fc 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-FFDD00?style=flat&logo=buy-me-a-coffee&logoColor=black)](https://www.buymeacoffee.com/leonvanzyl) -A long-running autonomous coding agent powered by the Claude Agent SDK. This tool can build complete applications over multiple sessions using a two-agent pattern (initializer + coding agent). Includes a React-based UI for monitoring progress in real-time. +A long-running autonomous coding agent powered by the Opencode SDK. This tool can build complete applications over multiple sessions using a two-agent pattern (initializer + coding agent). Includes a React-based UI for monitoring progress in real-time. ## Video Tutorial @@ -14,26 +14,20 @@ A long-running autonomous coding agent powered by the Claude Agent SDK. This too ## Prerequisites -### Claude Code CLI (Required) +### Opencode SDK (Required) -This project requires the Claude Code CLI to be installed. Install it using one of these methods: +This project requires the Opencode Python SDK to be available in the environment. Install it via: -**macOS / Linux:** +**macOS / Linux / Windows:** ```bash -curl -fsSL https://claude.ai/install.sh | bash -``` - -**Windows (PowerShell):** -```powershell -irm https://claude.ai/install.ps1 | iex +pip install --pre opencode-ai ``` ### Authentication You need one of the following: -- **Claude Pro/Max Subscription** - Use `claude login` to authenticate (recommended) -- **Anthropic API Key** - Pay-per-use from https://console.anthropic.com/ +- **Opencode API Key** - Set `OPENCODE_API_KEY` in your environment per the docs at https://opencode.ai/docs --- @@ -70,8 +64,8 @@ start.bat ``` The start script will: -1. Check if Claude CLI is installed -2. Check if you're authenticated (prompt to run `claude login` if not) +1. Check if the Opencode SDK is installed +2. Check if you're authenticated (ensure `OPENCODE_API_KEY` is set in the environment) 3. Create a Python virtual environment 4. Install dependencies 5. Launch the main menu @@ -82,7 +76,7 @@ You'll see options to: - **Create new project** - Start a fresh project with AI-assisted spec generation - **Continue existing project** - Resume work on a previous project -For new projects, you can use the built-in `/create-spec` command to interactively create your app specification with Claude's help. +For new projects, you can use the built-in `/create-spec` command to interactively create your app specification with Opencode's help. --- @@ -139,7 +133,7 @@ autonomous-coding/ ├── start_ui.py # Web UI backend (FastAPI server launcher) ├── autonomous_agent_demo.py # Agent entry point ├── agent.py # Agent session logic -├── client.py # Claude SDK client configuration +├── client.py # Opencode client configuration ├── security.py # Bash command allowlist and validation ├── progress.py # Progress tracking utilities ├── prompts.py # Prompt loading utilities @@ -160,10 +154,10 @@ autonomous-coding/ │ │ └── lib/ # API client and types │ ├── package.json │ └── vite.config.ts -├── .claude/ +├── .opencode/ │ ├── commands/ │ │ └── create-spec.md # /create-spec slash command -│ ├── skills/ # Claude Code skills +│ ├── skills/ # Opencode skills │ └── templates/ # Prompt templates ├── generations/ # Generated projects go here ├── requirements.txt # Python dependencies @@ -184,7 +178,7 @@ generations/my_project/ │ ├── initializer_prompt.md # First session prompt │ └── coding_prompt.md # Continuation session prompt ├── init.sh # Environment setup script -├── claude-progress.txt # Session progress notes +├── opencode-progress.txt # Session progress notes └── [application files] # Generated application code ``` @@ -306,11 +300,11 @@ Edit `security.py` to add or remove commands from `ALLOWED_COMMANDS`. ## Troubleshooting -**"Claude CLI not found"** -Install the Claude Code CLI using the instructions in the Prerequisites section. +**"Opencode SDK/CLI not found"** +Install the Opencode SDK and ensure `OPENCODE_API_KEY` is configured (see Prerequisites). -**"Not authenticated with Claude"** -Run `claude login` to authenticate. The start script will prompt you to do this automatically. +**"Not authenticated with Opencode"** +Ensure `OPENCODE_API_KEY` is set in your environment. The start script will prompt if it detects missing credentials. **"Appears to hang on first run"** This is normal. The initializer agent is generating detailed test cases, which takes significant time. Watch for `[Tool: ...]` output to confirm the agent is working. diff --git a/agent.py b/agent.py index 50edc46d..516982a3 100644 --- a/agent.py +++ b/agent.py @@ -14,10 +14,10 @@ from typing import Optional from zoneinfo import ZoneInfo -from claude_agent_sdk import ClaudeSDKClient +from opencode_adapter import OpencodeClient # Fix Windows console encoding for Unicode characters (emoji, etc.) -# Without this, print() crashes when Claude outputs emoji like ✅ +# Without this, print() crashes when Opencode outputs emoji like ✅ if sys.platform == "win32": sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace") @@ -36,15 +36,15 @@ async def run_agent_session( - client: ClaudeSDKClient, + client: OpencodeClient, message: str, project_dir: Path, ) -> tuple[str, str]: """ - Run a single agent session using Claude Agent SDK. + Run a single agent session using the Opencode SDK. Args: - client: Claude SDK client + client: Opencode SDK client message: The prompt to send project_dir: Project directory path @@ -53,7 +53,7 @@ async def run_agent_session( - "continue" if agent should continue working - "error" if an error occurred """ - print("Sending prompt to Claude Agent SDK...\n") + print("Sending prompt to Opencode SDK...\n") try: # Send the query @@ -120,7 +120,7 @@ async def run_autonomous_agent( Args: project_dir: Directory for the project - model: Claude model to use + model: Opencode model to use max_iterations: Maximum number of iterations (None for unlimited) yolo_mode: If True, skip browser testing and use YOLO prompt """ @@ -202,7 +202,7 @@ async def run_autonomous_agent( target_time_str = None if "limit reached" in response.lower(): - print("Claude Agent SDK indicated limit reached.") + print("Opencode indicated limit reached.") # Try to parse reset time from response match = re.search( @@ -243,7 +243,7 @@ async def run_autonomous_agent( if target_time_str: print( - f"\nClaude Code Limit Reached. Agent will auto-continue in {delay_seconds:.0f}s ({target_time_str})...", + f"\nOpencode Limit Reached. Agent will auto-continue in {delay_seconds:.0f}s ({target_time_str})...", flush=True, ) else: diff --git a/auth.py b/auth.py index a75d6cce..5e14da36 100644 --- a/auth.py +++ b/auth.py @@ -2,20 +2,20 @@ Authentication Error Detection ============================== -Shared utilities for detecting Claude CLI authentication errors. +Shared utilities for detecting Opencode authentication errors. Used by both CLI (start.py) and server (process_manager.py) to provide consistent error detection and messaging. """ import re -# Patterns that indicate authentication errors from Claude CLI +# Patterns that indicate authentication errors from Opencode or CLI wrappers AUTH_ERROR_PATTERNS = [ r"not\s+logged\s+in", r"not\s+authenticated", r"authentication\s+(failed|required|error)", r"login\s+required", - r"please\s+(run\s+)?['\"]?claude\s+login", + r"please\s+(run\s+)?['\"]?opencode\s+login", r"unauthorized", r"invalid\s+(token|credential|api.?key)", r"expired\s+(token|session|credential)", @@ -26,7 +26,7 @@ def is_auth_error(text: str) -> bool: """ - Check if text contains Claude CLI authentication error messages. + Check if text contains Opencode authentication error messages. Uses case-insensitive pattern matching against known error messages. @@ -51,13 +51,12 @@ def is_auth_error(text: str) -> bool: Authentication Error Detected ================================================== -Claude CLI requires authentication to work. +Opencode requires an API key (set `OPENCODE_API_KEY` in your environment). -To fix this, run: - claude login +To fix this, set the environment variable: + export OPENCODE_API_KEY=your_api_key_here -This will open a browser window to sign in. -After logging in, try running this command again. +Then re-run the command. ================================================== """ @@ -67,13 +66,12 @@ def is_auth_error(text: str) -> bool: AUTHENTICATION ERROR DETECTED ================================================================================ -Claude CLI requires authentication to work. +Opencode requires an API key (set `OPENCODE_API_KEY` in your environment). -To fix this, run: - claude login +To fix this, set the environment variable: + export OPENCODE_API_KEY=your_api_key_here -This will open a browser window to sign in. -After logging in, try starting the agent again. +Then start the agent again if it was stopped. ================================================================================ """ diff --git a/autonomous_agent_demo.py b/autonomous_agent_demo.py index 4e2b6563..6dc6e91b 100644 --- a/autonomous_agent_demo.py +++ b/autonomous_agent_demo.py @@ -3,7 +3,7 @@ Autonomous Coding Agent Demo ============================ -A minimal harness demonstrating long-running autonomous coding with Claude. +A minimal harness demonstrating long-running autonomous coding with Opencode. This script implements the two-agent pattern (initializer + coding agent) and incorporates all the strategies from the long-running agents guide. @@ -48,8 +48,8 @@ def parse_args() -> argparse.Namespace: # Use registered project name (looked up from registry) python autonomous_agent_demo.py --project-dir my-app - # Use a specific model - python autonomous_agent_demo.py --project-dir my-app --model claude-sonnet-4-5-20250929 + # Use a specific model (provider-dependent) + python autonomous_agent_demo.py --project-dir my-app --model default # Limit iterations for testing python autonomous_agent_demo.py --project-dir my-app --max-iterations 5 @@ -58,7 +58,7 @@ def parse_args() -> argparse.Namespace: python autonomous_agent_demo.py --project-dir my-app --yolo Authentication: - Uses Claude CLI authentication (run 'claude login' if not logged in) + Uses Opencode API key authentication. Set `OPENCODE_API_KEY` in your environment or follow the instructions at https://opencode.ai/docs Authentication is handled by start.bat/start.sh before this runs """, ) @@ -81,7 +81,7 @@ def parse_args() -> argparse.Namespace: "--model", type=str, default=DEFAULT_MODEL, - help=f"Claude model to use (default: {DEFAULT_MODEL})", + help=f"Model to use (provider-agnostic; default: {DEFAULT_MODEL})", ) parser.add_argument( @@ -99,7 +99,7 @@ def main() -> None: args = parse_args() # Note: Authentication is handled by start.bat/start.sh before this script runs. - # The Claude SDK auto-detects credentials from ~/.claude/.credentials.json + # Opencode uses OPENCODE_API_KEY in the environment (see docs at https://opencode.ai/docs). # Resolve project directory: # 1. If absolute path, use as-is diff --git a/client.py b/client.py index c0582767..15950836 100644 --- a/client.py +++ b/client.py @@ -1,54 +1,29 @@ """ -Claude SDK Client Configuration -=============================== +Opencode Client Configuration +============================= -Functions for creating and configuring the Claude Agent SDK client. +Functions for creating and configuring the Opencode client adapter. """ import json import os -import shutil import sys from pathlib import Path -from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient -from claude_agent_sdk.types import HookMatcher from dotenv import load_dotenv -from security import bash_security_hook +from opencode_adapter import OpencodeClient # Load environment variables from .env file if present load_dotenv() -# Default CLI command - can be overridden via CLI_COMMAND environment variable -# Common values: "claude" (default), "glm" -DEFAULT_CLI_COMMAND = "claude" - # Default Playwright headless mode - can be overridden via PLAYWRIGHT_HEADLESS env var -# When True, browser runs invisibly in background -# When False, browser window is visible (default - useful for monitoring agent progress) DEFAULT_PLAYWRIGHT_HEADLESS = False -def get_cli_command() -> str: - """ - Get the CLI command to use for the agent. - - Reads from CLI_COMMAND environment variable, defaults to 'claude'. - This allows users to use alternative CLIs like 'glm'. - """ - return os.getenv("CLI_COMMAND", DEFAULT_CLI_COMMAND) - - def get_playwright_headless() -> bool: - """ - Get the Playwright headless mode setting. - - Reads from PLAYWRIGHT_HEADLESS environment variable, defaults to False. - Returns True for headless mode (invisible browser), False for visible browser. - """ + """Get the Playwright headless mode setting.""" value = os.getenv("PLAYWRIGHT_HEADLESS", "false").lower() - # Accept various truthy/falsy values return value in ("true", "1", "yes", "on") @@ -65,114 +40,57 @@ def get_playwright_headless() -> bool: # Playwright MCP tools for browser automation PLAYWRIGHT_TOOLS = [ - # Core navigation & screenshots "mcp__playwright__browser_navigate", - "mcp__playwright__browser_navigate_back", "mcp__playwright__browser_take_screenshot", "mcp__playwright__browser_snapshot", - - # Element interaction "mcp__playwright__browser_click", "mcp__playwright__browser_type", "mcp__playwright__browser_fill_form", "mcp__playwright__browser_select_option", "mcp__playwright__browser_hover", - "mcp__playwright__browser_drag", - "mcp__playwright__browser_press_key", - - # JavaScript & debugging "mcp__playwright__browser_evaluate", - # "mcp__playwright__browser_run_code", # REMOVED - causes Playwright MCP server crash "mcp__playwright__browser_console_messages", "mcp__playwright__browser_network_requests", - - # Browser management - "mcp__playwright__browser_close", - "mcp__playwright__browser_resize", - "mcp__playwright__browser_tabs", - "mcp__playwright__browser_wait_for", - "mcp__playwright__browser_handle_dialog", - "mcp__playwright__browser_file_upload", - "mcp__playwright__browser_install", ] # Built-in tools -BUILTIN_TOOLS = [ - "Read", - "Write", - "Edit", - "Glob", - "Grep", - "Bash", - "WebFetch", - "WebSearch", -] +BUILTIN_TOOLS = ["Read", "Write", "Edit", "Glob", "Grep", "Bash", "WebFetch", "WebSearch"] def create_client(project_dir: Path, model: str, yolo_mode: bool = False): - """ - Create a Claude Agent SDK client with multi-layered security. - - Args: - project_dir: Directory for the project - model: Claude model to use - yolo_mode: If True, skip Playwright MCP server for rapid prototyping - - Returns: - Configured ClaudeSDKClient (from claude_agent_sdk) - - Security layers (defense in depth): - 1. Sandbox - OS-level bash command isolation prevents filesystem escape - 2. Permissions - File operations restricted to project_dir only - 3. Security hooks - Bash commands validated against an allowlist - (see security.py for ALLOWED_COMMANDS) + """Create an Opencode client adapter and write project settings file. - Note: Authentication is handled by start.bat/start.sh before this runs. - The Claude SDK auto-detects credentials from the Claude CLI configuration + The Opencode SDK is a REST client; we use an adapter to provide a + minimal interface compatible with the rest of the codebase. """ # Build allowed tools list based on mode - # In YOLO mode, exclude Playwright tools for faster prototyping allowed_tools = [*BUILTIN_TOOLS, *FEATURE_MCP_TOOLS] if not yolo_mode: allowed_tools.extend(PLAYWRIGHT_TOOLS) # Build permissions list permissions_list = [ - # Allow all file operations within the project directory "Read(./**)", "Write(./**)", "Edit(./**)", "Glob(./**)", "Grep(./**)", - # Bash permission granted here, but actual commands are validated - # by the bash_security_hook (see security.py for allowed commands) "Bash(*)", - # Allow web tools for documentation lookup "WebFetch", "WebSearch", - # Allow Feature MCP tools for feature management *FEATURE_MCP_TOOLS, ] if not yolo_mode: - # Allow Playwright MCP tools for browser automation (standard mode only) permissions_list.extend(PLAYWRIGHT_TOOLS) - # Create comprehensive security settings - # Note: Using relative paths ("./**") restricts access to project directory - # since cwd is set to project_dir security_settings = { "sandbox": {"enabled": True, "autoAllowBashIfSandboxed": True}, - "permissions": { - "defaultMode": "acceptEdits", # Auto-approve edits within allowed directories - "allow": permissions_list, - }, + "permissions": {"defaultMode": "acceptEdits", "allow": permissions_list}, } - # Ensure project directory exists before creating settings file project_dir.mkdir(parents=True, exist_ok=True) - # Write settings to a file in the project directory - settings_file = project_dir / ".claude_settings.json" + settings_file = project_dir / ".opencode_settings.json" with open(settings_file, "w") as f: json.dump(security_settings, f, indent=2) @@ -184,59 +102,23 @@ def create_client(project_dir: Path, model: str, yolo_mode: bool = False): print(" - MCP servers: features (database) - YOLO MODE (no Playwright)") else: print(" - MCP servers: playwright (browser), features (database)") - print(" - Project settings enabled (skills, commands, CLAUDE.md)") + print(" - Project settings enabled (skills, commands, OPENCODE.md)") print() - # Use system CLI instead of bundled one (avoids Bun runtime crash on Windows) - # CLI command is configurable via CLI_COMMAND environment variable - cli_command = get_cli_command() - system_cli = shutil.which(cli_command) - if system_cli: - print(f" - Using system CLI: {system_cli}") - else: - print(f" - Warning: System CLI '{cli_command}' not found, using bundled CLI") - - # Build MCP servers config - features is always included, playwright only in standard mode + # Build MCP servers config mcp_servers = { "features": { - "command": sys.executable, # Use the same Python that's running this script + "command": sys.executable, "args": ["-m", "mcp_server.feature_mcp"], - "env": { - # Inherit parent environment (PATH, ANTHROPIC_API_KEY, etc.) - **os.environ, - # Add custom variables - "PROJECT_DIR": str(project_dir.resolve()), - "PYTHONPATH": str(Path(__file__).parent.resolve()), - }, - }, + "env": {**os.environ, "PROJECT_DIR": str(project_dir.resolve()), "PYTHONPATH": str(Path(__file__).parent.resolve())}, + } } + if not yolo_mode: - # Include Playwright MCP server for browser automation (standard mode only) - # Headless mode is configurable via PLAYWRIGHT_HEADLESS environment variable playwright_args = ["@playwright/mcp@latest", "--viewport-size", "1280x720"] if get_playwright_headless(): playwright_args.append("--headless") - mcp_servers["playwright"] = { - "command": "npx", - "args": playwright_args, - } + mcp_servers["playwright"] = {"command": "npx", "args": playwright_args} - return ClaudeSDKClient( - options=ClaudeAgentOptions( - model=model, - cli_path=system_cli, # Use system CLI to avoid bundled Bun crash (exit code 3) - system_prompt="You are an expert full-stack developer building a production-quality web application.", - setting_sources=["project"], # Enable skills, commands, and CLAUDE.md from project dir - max_buffer_size=10 * 1024 * 1024, # 10MB for large Playwright screenshots - allowed_tools=allowed_tools, - mcp_servers=mcp_servers, - hooks={ - "PreToolUse": [ - HookMatcher(matcher="Bash", hooks=[bash_security_hook]), - ], - }, - max_turns=1000, - cwd=str(project_dir.resolve()), - settings=str(settings_file.resolve()), # Use absolute path - ) - ) + # Return an Opencode adapter instance + return OpencodeClient(project_dir, model, yolo_mode=yolo_mode) diff --git a/opencode_adapter.py b/opencode_adapter.py new file mode 100644 index 00000000..545ffad0 --- /dev/null +++ b/opencode_adapter.py @@ -0,0 +1,169 @@ +"""Opencode SDK adapter + +Provides a minimal async client wrapper that exposes the methods used by +our agent loop: async context manager, `query(message)` to send a prompt and +`receive_response()` async generator to yield message-like objects. + +This keeps the rest of the codebase mostly untouched while using the +`opencode_ai` AsyncOpencode client under the hood. +""" + +from __future__ import annotations + +import asyncio +from typing import AsyncIterator + +try: + from opencode_ai import AsyncOpencode +except Exception: # pragma: no cover - best-effort import + AsyncOpencode = None # type: ignore + + +class TextBlock: + def __init__(self, text: str) -> None: + self.text = text + + +class ToolUseBlock: + def __init__(self, name: str, input: object | None = None) -> None: + self.name = name + self.input = input + + +class ToolResultBlock: + def __init__(self, content: object | str, is_error: bool = False) -> None: + self.content = content + self.is_error = is_error + + +class AssistantMessage: + def __init__(self, content: list) -> None: + self.content = content + + +class UserMessage: + def __init__(self, content: list) -> None: + self.content = content + + +class OpencodeClient: + """Minimal adapter around AsyncOpencode. + + Behavior: + - On __aenter__, creates a session via `client.session.create()` + - query(message) posts a chat message to the session + - receive_response() polls messages for the session and yields + message-like objects compatible with existing agent handling + """ + + def __init__(self, project_dir, model: str, yolo_mode: bool = False): + if AsyncOpencode is None: + raise RuntimeError( + "opencode_ai is not installed. Install with `pip install --pre opencode-ai`" + ) + self.project_dir = project_dir + self.model = model + self.yolo_mode = yolo_mode + self._client: AsyncOpencode | None = None + self._session = None + + async def __aenter__(self): + self._client = AsyncOpencode() + # Create a fresh session + self._session = await self._client.session.create() + + # Choose a provider (fallback to first available) + providers_resp = await self._client.app.providers() + if getattr(providers_resp, "providers", None): + self._provider_id = providers_resp.providers[0].id + else: + self._provider_id = "" + + return self + + async def __aexit__(self, exc_type, exc, tb): + if self._client: + # Best-effort close + aclose = getattr(self._client, "aclose", None) + if aclose: + await aclose() + + async def query(self, message: str) -> None: + """Send a message to the session.""" + assert self._client is not None and self._session is not None + # Build a simple text part + parts = [{"type": "text", "text": message}] + + # Send chat message + try: + await self._client.session.chat( + self._session.id, + model_id=self.model, + parts=parts, + provider_id=self._provider_id, + ) + except Exception: + # Send failures should be surfaced to caller via receive_response + raise + + async def receive_response(self) -> AsyncIterator[object]: + """Poll and yield session messages as simplified objects. + + Yields objects with type name 'AssistantMessage' or 'UserMessage' and + .content as list of TextBlock/ToolUseBlock/ToolResultBlock objects. + """ + assert self._client is not None and self._session is not None + + # Poll messages from the session. We fetch messages repeatedly until no + # new messages are returned for a short period. This keeps things simple + # and avoids the complexity of wiring SSE streaming here. + last_seen = 0 + idle_cycles = 0 + + while True: + items = await self._client.session.messages(self._session.id) + # items is a list of {info: Message, parts: [Part, ...]} + if not items: + idle_cycles += 1 + else: + idle_cycles = 0 + + new_items = items[last_seen:] + for item in new_items: + role = getattr(item.info, "role", "assistant") + parts_out = [] + for part in item.parts: + ptype = getattr(part, "type", "text") + if ptype == "text": + text = getattr(part, "text", "") + parts_out.append(TextBlock(text)) + elif ptype == "tool": + tool_name = getattr(part, "tool", "") + state = getattr(part, "state", None) + # Completed -> ToolResultBlock with output + if state and getattr(state, "status", None) == "completed": + output = getattr(state, "output", "") + parts_out.append(ToolResultBlock(output, is_error=False)) + elif state and getattr(state, "status", None) == "error": + # Some errors are nested; serialize roughly + parts_out.append(ToolResultBlock(str(state), is_error=True)) + else: + # For running/pending, expose as ToolUseBlock + parts_out.append(ToolUseBlock(tool_name, None)) + else: + # Other parts (step start/finish, snapshots) - stringify + parts_out.append(TextBlock(str(part))) + + if role == "assistant": + yield AssistantMessage(parts_out) + else: + yield UserMessage(parts_out) + + last_seen = len(items) + + # If no new items for a few cycles, assume conversation done + if idle_cycles > 3: + return + + # Small sleep to avoid tight polling loop + await asyncio.sleep(0.4) diff --git a/prompts.py b/prompts.py index 0fc403b5..8ac954c1 100644 --- a/prompts.py +++ b/prompts.py @@ -6,14 +6,14 @@ Fallback chain: 1. Project-specific: {project_dir}/prompts/{name}.md -2. Base template: .claude/templates/{name}.template.md +2. Base template: .opencode/templates/{name}.template.md """ import shutil from pathlib import Path # Base templates location (generic templates) -TEMPLATES_DIR = Path(__file__).parent / ".claude" / "templates" +TEMPLATES_DIR = Path(__file__).parent / ".opencode" / "templates" def get_project_prompts_dir(project_dir: Path) -> Path: @@ -27,7 +27,7 @@ def load_prompt(name: str, project_dir: Path | None = None) -> str: Fallback order: 1. Project-specific: {project_dir}/prompts/{name}.md - 2. Base template: .claude/templates/{name}.template.md + 2. Base template: .opencode/templates/{name}.template.md Args: name: The prompt name (without extension), e.g., "initializer_prompt" diff --git a/registry.py b/registry.py index 20d31dfc..d021caf1 100644 --- a/registry.py +++ b/registry.py @@ -31,15 +31,14 @@ # Available models with display names # To add a new model: add an entry here with {"id": "model-id", "name": "Display Name"} AVAILABLE_MODELS = [ - {"id": "claude-opus-4-5-20251101", "name": "Claude Opus 4.5"}, - {"id": "claude-sonnet-4-5-20250929", "name": "Claude Sonnet 4.5"}, + {"id": "default", "name": "Default Provider Model"}, ] # List of valid model IDs (derived from AVAILABLE_MODELS) VALID_MODELS = [m["id"] for m in AVAILABLE_MODELS] # Default model and settings -DEFAULT_MODEL = "claude-opus-4-5-20251101" +DEFAULT_MODEL = "default" DEFAULT_YOLO_MODE = False # SQLite connection settings diff --git a/requirements.txt b/requirements.txt index 1ff89a79..9102fe6c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -claude-agent-sdk>=0.1.0,<0.2.0 +opencode-ai>=0.1.0 # install with `pip install --pre opencode-ai` python-dotenv>=1.0.0 sqlalchemy>=2.0.0 fastapi>=0.115.0 @@ -12,3 +12,4 @@ aiofiles>=24.0.0 ruff>=0.8.0 mypy>=1.13.0 pytest>=8.0.0 +pytest-asyncio>=0.23.0 diff --git a/server/main.py b/server/main.py index 91b9875a..9442d40d 100644 --- a/server/main.py +++ b/server/main.py @@ -18,13 +18,8 @@ def get_cli_command() -> str: - """ - Get the CLI command to use for the agent. - - Reads from CLI_COMMAND environment variable, defaults to 'claude'. - This allows users to use alternative CLIs like 'glm'. - """ - return os.getenv("CLI_COMMAND", "claude") + """Deprecated helper retained for compatibility.""" + return os.getenv("CLI_COMMAND", "opencode") # defaults to opencode if not specified from fastapi import FastAPI, HTTPException, Request, WebSocket from fastapi.middleware.cors import CORSMiddleware @@ -140,23 +135,22 @@ async def health_check(): @app.get("/api/setup/status", response_model=SetupStatus) async def setup_status(): """Check system setup status.""" - # Check for CLI (configurable via CLI_COMMAND environment variable) - cli_command = get_cli_command() - claude_cli = shutil.which(cli_command) is not None + # Check whether the opencode SDK is importable + try: + opencode_sdk = True + except Exception: + opencode_sdk = False - # Check for CLI configuration directory - # Note: CLI no longer stores credentials in ~/.claude/.credentials.json - # The existence of ~/.claude indicates the CLI has been configured - claude_dir = Path.home() / ".claude" - credentials = claude_dir.exists() and claude_dir.is_dir() + # Check for OPENCODE_API_KEY in environment + opencode_api_key = bool(os.getenv("OPENCODE_API_KEY")) # Check for Node.js and npm node = shutil.which("node") is not None npm = shutil.which("npm") is not None return SetupStatus( - claude_cli=claude_cli, - credentials=credentials, + opencode_sdk=opencode_sdk, + opencode_api_key=opencode_api_key, node=node, npm=npm, ) diff --git a/server/routers/assistant_chat.py b/server/routers/assistant_chat.py index dae53b4a..f727f935 100644 --- a/server/routers/assistant_chat.py +++ b/server/routers/assistant_chat.py @@ -230,7 +230,7 @@ async def assistant_chat_websocket(websocket: WebSocket, project_name: str): Server -> Client: - {"type": "conversation_created", "conversation_id": int} - New conversation created - - {"type": "text", "content": "..."} - Text chunk from Claude + - {"type": "text", "content": "..."} - Text chunk from Opencode - {"type": "tool_call", "tool": "...", "input": {...}} - Tool being called - {"type": "response_done"} - Response complete - {"type": "error", "content": "..."} - Error message @@ -306,7 +306,7 @@ async def assistant_chat_websocket(websocket: WebSocket, project_name: str): }) continue - # Stream Claude's response + # Stream Opencode's response async for chunk in session.send_message(user_content): await websocket.send_json(chunk) diff --git a/server/routers/expand_project.py b/server/routers/expand_project.py index 50bf1962..fdce7465 100644 --- a/server/routers/expand_project.py +++ b/server/routers/expand_project.py @@ -2,7 +2,7 @@ Expand Project Router ===================== -WebSocket and REST endpoints for interactive project expansion with Claude. +WebSocket and REST endpoints for interactive project expansion with Opencode. Allows adding multiple features to existing projects via natural language. """ @@ -112,7 +112,7 @@ async def expand_project_websocket(websocket: WebSocket, project_name: str): - {"type": "ping"} - Keep-alive ping Server -> Client: - - {"type": "text", "content": "..."} - Text chunk from Claude + - {"type": "text", "content": "..."} - Text chunk from Opencode - {"type": "features_created", "count": N, "features": [...]} - Features added - {"type": "expansion_complete", "total_added": N} - Session complete - {"type": "response_done"} - Response complete @@ -211,7 +211,7 @@ async def expand_project_websocket(websocket: WebSocket, project_name: str): }) continue - # Stream Claude's response + # Stream Opencode's response async for chunk in session.send_message(user_content, attachments if attachments else None): await websocket.send_json(chunk) diff --git a/server/routers/spec_creation.py b/server/routers/spec_creation.py index 87f79a68..414f2ff7 100644 --- a/server/routers/spec_creation.py +++ b/server/routers/spec_creation.py @@ -2,7 +2,7 @@ Spec Creation Router ==================== -WebSocket and REST endpoints for interactive spec creation with Claude. +WebSocket and REST endpoints for interactive spec creation with Opencode. """ import json @@ -111,8 +111,8 @@ async def get_spec_file_status(project_name: str): """ Get spec creation status by reading .spec_status.json from the project. - This is used for polling to detect when Claude has finished writing spec files. - Claude writes this status file as the final step after completing all spec work. + This is used for polling to detect when Opencode has finished writing spec files. + Opencode writes this status file as the final step after completing all spec work. """ if not validate_project_name(project_name): raise HTTPException(status_code=400, detail="Invalid project name") @@ -176,7 +176,7 @@ async def spec_chat_websocket(websocket: WebSocket, project_name: str): - {"type": "ping"} - Keep-alive ping Server -> Client: - - {"type": "text", "content": "..."} - Text chunk from Claude + - {"type": "text", "content": "..."} - Text chunk from Opencode - {"type": "question", "questions": [...], "tool_id": "..."} - Structured question - {"type": "spec_complete", "path": "..."} - Spec file created - {"type": "file_written", "path": "..."} - Other file written @@ -280,7 +280,7 @@ async def spec_chat_websocket(websocket: WebSocket, project_name: str): spec_complete_received = False spec_path = None - # Stream Claude's response (with attachments if present) + # Stream Opencode's response (with attachments if present) async for chunk in session.send_message(user_content, attachments if attachments else None): # Track spec_complete but don't send complete yet if chunk.get("type") == "spec_complete": @@ -327,7 +327,7 @@ async def spec_chat_websocket(websocket: WebSocket, project_name: str): spec_complete_received = False spec_path = None - # Stream Claude's response + # Stream Opencode's response async for chunk in session.send_message(user_response): # Track spec_complete but don't send complete yet if chunk.get("type") == "spec_complete": diff --git a/server/schemas.py b/server/schemas.py index cb0a4ecc..62d8c6fa 100644 --- a/server/schemas.py +++ b/server/schemas.py @@ -28,7 +28,7 @@ class ProjectCreate(BaseModel): """Request schema for creating a new project.""" name: str = Field(..., min_length=1, max_length=50, pattern=r'^[a-zA-Z0-9_-]+$') path: str = Field(..., min_length=1, description="Absolute path to project directory") - spec_method: Literal["claude", "manual"] = "claude" + spec_method: Literal["opencode", "manual"] = "opencode" class ProjectStats(BaseModel): @@ -157,8 +157,8 @@ class AgentActionResponse(BaseModel): class SetupStatus(BaseModel): """System setup status.""" - claude_cli: bool - credentials: bool + opencode_sdk: bool + opencode_api_key: bool node: bool npm: bool diff --git a/server/services/assistant_chat_session.py b/server/services/assistant_chat_session.py index bebed941..32bbdeab 100755 --- a/server/services/assistant_chat_session.py +++ b/server/services/assistant_chat_session.py @@ -9,17 +9,15 @@ import json import logging -import os -import shutil -import sys import threading from datetime import datetime from pathlib import Path from typing import AsyncGenerator, Optional -from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient from dotenv import load_dotenv +from opencode_adapter import OpencodeClient + from .assistant_database import ( add_message, create_conversation, @@ -29,16 +27,6 @@ load_dotenv() -def get_cli_command() -> str: - """ - Get the CLI command to use for the agent. - - Reads from CLI_COMMAND environment variable, defaults to 'claude'. - This allows users to use alternative CLIs like 'glm'. - """ - return os.getenv("CLI_COMMAND", "claude") - - logger = logging.getLogger(__name__) # Root directory of the project @@ -160,7 +148,7 @@ class AssistantChatSession: """ Manages a read-only assistant conversation for a project. - Uses Claude Opus 4.5 with only read-only tools enabled. + Uses Opencode with only read-only tools enabled. Persists conversation history to SQLite. """ @@ -176,24 +164,23 @@ def __init__(self, project_name: str, project_dir: Path, conversation_id: Option self.project_name = project_name self.project_dir = project_dir self.conversation_id = conversation_id - self.client: Optional[ClaudeSDKClient] = None + self.client: Optional[OpencodeClient] = None self._client_entered: bool = False self.created_at = datetime.now() async def close(self) -> None: - """Clean up resources and close the Claude client.""" + """Clean up resources and close the Opencode client.""" if self.client and self._client_entered: try: await self.client.__aexit__(None, None, None) except Exception as e: - logger.warning(f"Error closing Claude client: {e}") + logger.warning(f"Error closing Opencode client: {e}") finally: self._client_entered = False self.client = None async def start(self) -> AsyncGenerator[dict, None]: - """ - Initialize session with the Claude client. + """Initialize session with the Opencode client. Creates a new conversation if none exists, then sends an initial greeting. Yields message chunks as they stream in. @@ -222,48 +209,19 @@ async def start(self) -> AsyncGenerator[dict, None]: "allow": permissions_list, }, } - settings_file = self.project_dir / ".claude_assistant_settings.json" + settings_file = self.project_dir / ".opencode_assistant_settings.json" with open(settings_file, "w") as f: json.dump(security_settings, f, indent=2) - # Build MCP servers config - only features MCP for read-only access - mcp_servers = { - "features": { - "command": sys.executable, - "args": ["-m", "mcp_server.feature_mcp"], - "env": { - **os.environ, - "PROJECT_DIR": str(self.project_dir.resolve()), - "PYTHONPATH": str(ROOT_DIR.resolve()), - }, - }, - } - - # Get system prompt with project context - system_prompt = get_system_prompt(self.project_name, self.project_dir) - - # Use system CLI (configurable via CLI_COMMAND environment variable) - cli_command = get_cli_command() - system_cli = shutil.which(cli_command) + # (local MCP servers and CLI detection are not needed for assistant sessions) try: - self.client = ClaudeSDKClient( - options=ClaudeAgentOptions( - model="claude-opus-4-5-20251101", - cli_path=system_cli, - system_prompt=system_prompt, - allowed_tools=[*READONLY_BUILTIN_TOOLS, *ASSISTANT_FEATURE_TOOLS], - mcp_servers=mcp_servers, - permission_mode="bypassPermissions", - max_turns=100, - cwd=str(self.project_dir.resolve()), - settings=str(settings_file.resolve()), - ) - ) + # Use Opencode adapter + self.client = OpencodeClient(self.project_dir, model="default", yolo_mode=False) await self.client.__aenter__() self._client_entered = True except Exception as e: - logger.exception("Failed to create Claude client") + logger.exception("Failed to create Opencode client") yield {"type": "error", "content": f"Failed to initialize assistant: {str(e)}"} return @@ -282,7 +240,7 @@ async def start(self) -> AsyncGenerator[dict, None]: async def send_message(self, user_message: str) -> AsyncGenerator[dict, None]: """ - Send user message and stream Claude's response. + Send user message and stream Opencode's response. Args: user_message: The user's message @@ -306,23 +264,23 @@ async def send_message(self, user_message: str) -> AsyncGenerator[dict, None]: add_message(self.project_dir, self.conversation_id, "user", user_message) try: - async for chunk in self._query_claude(user_message): + async for chunk in self._query_opencode(user_message): yield chunk yield {"type": "response_done"} except Exception as e: - logger.exception("Error during Claude query") + logger.exception("Error during Opencode query") yield {"type": "error", "content": f"Error: {str(e)}"} - async def _query_claude(self, message: str) -> AsyncGenerator[dict, None]: + async def _query_opencode(self, message: str) -> AsyncGenerator[dict, None]: """ - Internal method to query Claude and stream responses. + Internal method to query Opencode and stream responses. Handles tool calls and text responses. """ if not self.client: return - # Send message to Claude + # Send message to Opencode await self.client.query(message) full_response = "" @@ -350,6 +308,12 @@ async def _query_claude(self, message: str) -> AsyncGenerator[dict, None]: "input": tool_input, } + elif block_type == "ToolResultBlock" and hasattr(block, "content"): + # Tool results are returned by Opencode tool executions + result = getattr(block, "content", "") + is_error = getattr(block, "is_error", False) + yield {"type": "tool_result", "result": result, "is_error": is_error} + # Store the complete response in the database if full_response and self.conversation_id: add_message(self.project_dir, self.conversation_id, "assistant", full_response) diff --git a/server/services/expand_chat_session.py b/server/services/expand_chat_session.py index 659c7766..6416db89 100644 --- a/server/services/expand_chat_session.py +++ b/server/services/expand_chat_session.py @@ -2,7 +2,7 @@ Expand Chat Session =================== -Manages interactive project expansion conversation with Claude. +Manages interactive project expansion conversation with Opencode. Uses the expand-project.md skill to help users add features to existing projects. """ @@ -11,16 +11,16 @@ import logging import os import re -import shutil import threading import uuid from datetime import datetime from pathlib import Path from typing import AsyncGenerator, Optional -from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient from dotenv import load_dotenv +from opencode_adapter import OpencodeClient + from ..schemas import ImageAttachment # Load environment variables from .env file if present @@ -31,10 +31,10 @@ def get_cli_command() -> str: """ Get the CLI command to use for the agent. - Reads from CLI_COMMAND environment variable, defaults to 'claude'. + Reads from CLI_COMMAND environment variable, defaults to 'opencode'. This allows users to use alternative CLIs like 'glm'. """ - return os.getenv("CLI_COMMAND", "claude") + return os.getenv("CLI_COMMAND", "opencode") logger = logging.getLogger(__name__) @@ -61,7 +61,7 @@ class ExpandChatSession: Unlike SpecChatSession which writes spec files, this session: 1. Reads existing app_spec.txt for context - 2. Parses feature definitions from Claude's output + 2. Parses feature definitions from Opencode's output 3. Creates features via REST API 4. Tracks which features were created during the session """ @@ -76,7 +76,7 @@ def __init__(self, project_name: str, project_dir: Path): """ self.project_name = project_name self.project_dir = project_dir - self.client: Optional[ClaudeSDKClient] = None + self.client: Optional[OpencodeClient] = None self.messages: list[dict] = [] self.complete: bool = False self.created_at = datetime.now() @@ -88,12 +88,12 @@ def __init__(self, project_name: str, project_dir: Path): self._query_lock = asyncio.Lock() async def close(self) -> None: - """Clean up resources and close the Claude client.""" + """Clean up resources and close the Opencode client.""" if self.client and self._client_entered: try: await self.client.__aexit__(None, None, None) except Exception as e: - logger.warning(f"Error closing Claude client: {e}") + logger.warning(f"Error closing Opencode client: {e}") finally: self._client_entered = False self.client = None @@ -107,12 +107,12 @@ async def close(self) -> None: async def start(self) -> AsyncGenerator[dict, None]: """ - Initialize session and get initial greeting from Claude. + Initialize session and get initial greeting from Opencode. Yields message chunks as they stream in. """ # Load the expand-project skill - skill_path = ROOT_DIR / ".claude" / "commands" / "expand-project.md" + skill_path = ROOT_DIR / ".opencode" / "commands" / "expand-project.md" if not skill_path.exists(): yield { @@ -135,16 +135,8 @@ async def start(self) -> AsyncGenerator[dict, None]: except UnicodeDecodeError: skill_content = skill_path.read_text(encoding="utf-8", errors="replace") - # Find and validate CLI before creating temp files - # CLI command is configurable via CLI_COMMAND environment variable - cli_command = get_cli_command() - system_cli = shutil.which(cli_command) - if not system_cli: - yield { - "type": "error", - "content": f"CLI '{cli_command}' not found. Please install it or check your CLI_COMMAND setting." - } - return + # Opencode SDK (REST) is used; no local CLI required for this operation + # Ensure opencode_ai is importable in the running environment. # Create temporary security settings file (unique per session to avoid conflicts) security_settings = { @@ -157,46 +149,33 @@ async def start(self) -> AsyncGenerator[dict, None]: ], }, } - settings_file = self.project_dir / f".claude_settings.expand.{uuid.uuid4().hex}.json" + settings_file = self.project_dir / f".opencode_settings.expand.{uuid.uuid4().hex}.json" self._settings_file = settings_file with open(settings_file, "w", encoding="utf-8") as f: json.dump(security_settings, f, indent=2) # Replace $ARGUMENTS with absolute project path project_path = str(self.project_dir.resolve()) - system_prompt = skill_content.replace("$ARGUMENTS", project_path) + skill_content = skill_content.replace("$ARGUMENTS", project_path) - # Create Claude SDK client + # Create Opencode client try: - self.client = ClaudeSDKClient( - options=ClaudeAgentOptions( - model="claude-opus-4-5-20251101", - cli_path=system_cli, - system_prompt=system_prompt, - allowed_tools=[ - "Read", - "Glob", - ], - permission_mode="acceptEdits", - max_turns=100, - cwd=str(self.project_dir.resolve()), - settings=str(settings_file.resolve()), - ) - ) + # Use Opencode adapter + self.client = OpencodeClient(self.project_dir, model="default", yolo_mode=False) await self.client.__aenter__() self._client_entered = True except Exception: - logger.exception("Failed to create Claude client") + logger.exception("Failed to create Opencode client") yield { "type": "error", - "content": "Failed to initialize Claude" + "content": "Failed to initialize Opencode" } return # Start the conversation try: async with self._query_lock: - async for chunk in self._query_claude("Begin the project expansion process."): + async for chunk in self._query_opencode("Begin the project expansion process."): yield chunk yield {"type": "response_done"} except Exception: @@ -212,7 +191,7 @@ async def send_message( attachments: list[ImageAttachment] | None = None ) -> AsyncGenerator[dict, None]: """ - Send user message and stream Claude's response. + Send user message and stream Opencode's response. Args: user_message: The user's response @@ -243,23 +222,23 @@ async def send_message( try: # Use lock to prevent concurrent queries from corrupting the response stream async with self._query_lock: - async for chunk in self._query_claude(user_message, attachments): + async for chunk in self._query_opencode(user_message, attachments): yield chunk yield {"type": "response_done"} except Exception: - logger.exception("Error during Claude query") + logger.exception("Error during Opencode query") yield { "type": "error", "content": "Error while processing message" } - async def _query_claude( + async def _query_opencode( self, message: str, attachments: list[ImageAttachment] | None = None ) -> AsyncGenerator[dict, None]: """ - Internal method to query Claude and stream responses. + Internal method to query Opencode and stream responses. Handles text responses and detects feature creation blocks. """ diff --git a/server/services/process_manager.py b/server/services/process_manager.py index fd80665d..a994ea5d 100644 --- a/server/services/process_manager.py +++ b/server/services/process_manager.py @@ -247,7 +247,7 @@ async def start(self, yolo_mode: bool = False, model: str | None = None) -> tupl Args: yolo_mode: If True, run in YOLO mode (no browser testing) - model: Model to use (e.g., claude-opus-4-5-20251101) + model: Model to use (e.g., 'default' or provider-specific model id) Returns: Tuple of (success, message) @@ -280,7 +280,7 @@ async def start(self, yolo_mode: bool = False, model: str | None = None) -> tupl try: # Start subprocess with piped stdout/stderr - # Use project_dir as cwd so Claude SDK sandbox allows access to project files + # Use project_dir as cwd so Opencode client sandbox allows access to project files self.process = subprocess.Popen( cmd, stdout=subprocess.PIPE, diff --git a/server/services/spec_chat_session.py b/server/services/spec_chat_session.py index 7cb2beb7..a18f9d98 100644 --- a/server/services/spec_chat_session.py +++ b/server/services/spec_chat_session.py @@ -2,22 +2,22 @@ Spec Creation Chat Session ========================== -Manages interactive spec creation conversation with Claude. +Manages interactive spec creation conversation using Opencode. Uses the create-spec.md skill to guide users through app spec creation. """ import json import logging import os -import shutil import threading from datetime import datetime from pathlib import Path from typing import AsyncGenerator, Optional -from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient from dotenv import load_dotenv +from opencode_adapter import OpencodeClient + from ..schemas import ImageAttachment # Load environment variables from .env file if present @@ -28,10 +28,8 @@ def get_cli_command() -> str: """ Get the CLI command to use for the agent. - Reads from CLI_COMMAND environment variable, defaults to 'claude'. - This allows users to use alternative CLIs like 'glm'. - """ - return os.getenv("CLI_COMMAND", "claude") + Reads from CLI_COMMAND environment variable (legacy helper).""" + return os.getenv("CLI_COMMAND", "opencode") logger = logging.getLogger(__name__) @@ -40,7 +38,7 @@ async def _make_multimodal_message(content_blocks: list[dict]) -> AsyncGenerator """ Create an async generator that yields a properly formatted multimodal message. - The Claude Agent SDK's query() method accepts either: + The Opencode client's message format supports: - A string (simple text) - An AsyncIterable[dict] (for custom message formats) @@ -80,7 +78,7 @@ def __init__(self, project_name: str, project_dir: Path): """ self.project_name = project_name self.project_dir = project_dir - self.client: Optional[ClaudeSDKClient] = None + self.client: Optional[OpencodeClient] = None self.messages: list[dict] = [] self.complete: bool = False self.created_at = datetime.now() @@ -88,24 +86,24 @@ def __init__(self, project_name: str, project_dir: Path): self._client_entered: bool = False # Track if context manager is active async def close(self) -> None: - """Clean up resources and close the Claude client.""" + """Clean up resources and close the Opencode client.""" if self.client and self._client_entered: try: await self.client.__aexit__(None, None, None) except Exception as e: - logger.warning(f"Error closing Claude client: {e}") + logger.warning(f"Error closing Opencode client: {e}") finally: self._client_entered = False self.client = None async def start(self) -> AsyncGenerator[dict, None]: """ - Initialize session and get initial greeting from Claude. + Initialize session and get initial greeting from Opencode. Yields message chunks as they stream in. """ # Load the create-spec skill - skill_path = ROOT_DIR / ".claude" / "commands" / "create-spec.md" + skill_path = ROOT_DIR / ".opencode" / "commands" / "create-spec.md" if not skill_path.exists(): yield { @@ -122,9 +120,9 @@ async def start(self) -> AsyncGenerator[dict, None]: # Ensure project directory exists (like CLI does in start.py) self.project_dir.mkdir(parents=True, exist_ok=True) - # Delete app_spec.txt so Claude can create it fresh + # Delete app_spec.txt so Opencode can create it fresh # The SDK requires reading existing files before writing, but app_spec.txt is created new - # Note: We keep initializer_prompt.md so Claude can read and update the template + # Note: We keep initializer_prompt.md so Opencode can read and update the template prompts_dir = self.project_dir / "prompts" app_spec_path = prompts_dir / "app_spec.txt" if app_spec_path.exists(): @@ -145,53 +143,34 @@ async def start(self) -> AsyncGenerator[dict, None]: ], }, } - settings_file = self.project_dir / ".claude_settings.json" + settings_file = self.project_dir / ".opencode_settings.json" with open(settings_file, "w") as f: json.dump(security_settings, f, indent=2) # Replace $ARGUMENTS with absolute project path (like CLI does in start.py:184) # Using absolute path avoids confusion when project folder name differs from app name project_path = str(self.project_dir.resolve()) - system_prompt = skill_content.replace("$ARGUMENTS", project_path) + skill_content = skill_content.replace("$ARGUMENTS", project_path) - # Create Claude SDK client with limited tools for spec creation + # Create an Opencode client with limited tools for spec creation # Use Opus for best quality spec generation # Use system CLI to avoid bundled Bun runtime crash (exit code 3) on Windows - # CLI command is configurable via CLI_COMMAND environment variable - cli_command = get_cli_command() - system_cli = shutil.which(cli_command) try: - self.client = ClaudeSDKClient( - options=ClaudeAgentOptions( - model="claude-opus-4-5-20251101", - cli_path=system_cli, - system_prompt=system_prompt, - allowed_tools=[ - "Read", - "Write", - "Edit", - "Glob", - ], - permission_mode="acceptEdits", # Auto-approve file writes for spec creation - max_turns=100, - cwd=str(self.project_dir.resolve()), - settings=str(settings_file.resolve()), - ) - ) - # Enter the async context and track it + # Use Opencode adapter + self.client = OpencodeClient(self.project_dir, model="default", yolo_mode=False) await self.client.__aenter__() self._client_entered = True except Exception as e: - logger.exception("Failed to create Claude client") + logger.exception("Failed to create Opencode client") yield { "type": "error", - "content": f"Failed to initialize Claude: {str(e)}" + "content": f"Failed to initialize Opencode: {str(e)}" } return - # Start the conversation - Claude will send the Phase 1 greeting + # Start the conversation - Opencode will send the Phase 1 greeting try: - async for chunk in self._query_claude("Begin the spec creation process."): + async for chunk in self._query_opencode("Begin the spec creation process."): yield chunk # Signal that the response is complete (for UI to hide loading indicator) yield {"type": "response_done"} @@ -208,7 +187,7 @@ async def send_message( attachments: list[ImageAttachment] | None = None ) -> AsyncGenerator[dict, None]: """ - Send user message and stream Claude's response. + Send user message and stream Opencode's response. Args: user_message: The user's response @@ -237,24 +216,24 @@ async def send_message( }) try: - async for chunk in self._query_claude(user_message, attachments): + async for chunk in self._query_opencode(user_message, attachments): yield chunk # Signal that the response is complete (for UI to hide loading indicator) yield {"type": "response_done"} except Exception as e: - logger.exception("Error during Claude query") + logger.exception("Error during Opencode query") yield { "type": "error", "content": f"Error: {str(e)}" } - async def _query_claude( + async def _query_opencode( self, message: str, attachments: list[ImageAttachment] | None = None ) -> AsyncGenerator[dict, None]: """ - Internal method to query Claude and stream responses. + Internal method to query Opencode and stream responses. Handles tool calls (Write) and text responses. Supports multimodal content with image attachments. @@ -288,8 +267,8 @@ async def _query_claude( } }) - # Send multimodal content to Claude using async generator format - # The SDK's query() accepts AsyncIterable[dict] for custom message formats + # Send multimodal content to Opencode using async generator format + # The client's query() accepts AsyncIterable[dict] for custom message formats await self.client.query(_make_multimodal_message(content_blocks)) logger.info(f"Sent multimodal message with {len(attachments)} image(s)") else: diff --git a/start.bat b/start.bat index d09ca379..53c263ba 100644 --- a/start.bat +++ b/start.bat @@ -7,13 +7,13 @@ echo Autonomous Coding Agent echo ======================================== echo. -REM Check if Claude CLI is installed -where claude >nul 2>nul +REM Check if Opencode Python SDK is available +python -c "import opencode_ai" >nul 2>nul if %errorlevel% neq 0 ( - echo [ERROR] Claude CLI not found + echo [ERROR] Opencode Python SDK not found echo. - echo Please install Claude CLI first: - echo https://claude.ai/download + echo Please install the SDK first: + echo pip install --pre opencode-ai echo. echo Then run this script again. echo. @@ -21,19 +21,15 @@ if %errorlevel% neq 0 ( exit /b 1 ) -echo [OK] Claude CLI found +echo [OK] Opencode SDK available -REM Note: Claude CLI no longer stores credentials in ~/.claude/.credentials.json -REM We can't reliably check auth status without making an API call, so we just -REM verify the CLI is installed and remind the user to login if needed -set "CLAUDE_DIR=%USERPROFILE%\.claude" -if exist "%CLAUDE_DIR%\" ( - echo [OK] Claude CLI directory found - echo ^(If you're not logged in, run: claude login^) +REM Note: Opencode uses API keys or other auth mechanisms. Ensure OPENCODE_API_KEY is set. +if defined OPENCODE_API_KEY ( + echo [OK] OPENCODE_API_KEY found in environment ) else ( - echo [!] Claude CLI not configured + echo [!] Opencode API key not configured echo. - echo Please run 'claude login' to authenticate before continuing. + echo Please set OPENCODE_API_KEY per https://opencode.ai/docs echo. pause ) diff --git a/start.py b/start.py index df979096..9df29dd4 100644 --- a/start.py +++ b/start.py @@ -4,7 +4,7 @@ Provides an interactive menu to create new projects or continue existing ones. Supports two paths for new projects: -1. Claude path: Use /create-spec to generate spec interactively +1. Opencode path: Use /create-spec to generate spec interactively 2. Manual path: Edit template files directly, then continue """ @@ -25,10 +25,10 @@ def get_cli_command() -> str: """ Get the CLI command to use for the agent. - Reads from CLI_COMMAND environment variable, defaults to 'claude'. + Reads from CLI_COMMAND environment variable, defaults to 'opencode'. This allows users to use alternative CLIs like 'glm'. """ - return os.getenv("CLI_COMMAND", "claude") + return os.getenv("CLI_COMMAND", "opencode") from prompts import ( @@ -218,7 +218,7 @@ def ensure_project_scaffolded(project_name: str, project_dir: Path) -> Path: def run_spec_creation(project_dir: Path) -> bool: """ - Run Claude Code with /create-spec command to create project specification. + Run Opencode CLI with /create-spec command to create project specification. The project path is passed as an argument so create-spec knows where to write files. Captures stderr to detect authentication errors and provide helpful guidance. @@ -228,10 +228,10 @@ def run_spec_creation(project_dir: Path) -> bool: print("=" * 50) print(f"\nProject directory: {project_dir}") print(f"Prompts will be saved to: {get_project_prompts_dir(project_dir)}") - print("\nLaunching Claude Code for interactive spec creation...") + print("\nLaunching Opencode CLI for interactive spec creation...") print("Answer the questions to define your project.") - print("When done, Claude will generate the spec files.") - print("Exit Claude Code (Ctrl+C or /exit) when finished.\n") + print("When done, Opencode will generate the spec files.") + print("Exit Opencode (Ctrl+C or /exit) when finished.\n") try: # Launch CLI with /create-spec command @@ -254,7 +254,7 @@ def run_spec_creation(project_dir: Path) -> bool: # If there was stderr output but not an auth error, show it if stderr_output.strip() and result.returncode != 0: - print(f"\nClaude CLI error: {stderr_output.strip()}") + print(f"\nOpencode CLI error: {stderr_output.strip()}") # Check if spec was created in project prompts directory if check_spec_exists(project_dir): @@ -267,15 +267,16 @@ def run_spec_creation(project_dir: Path) -> bool: print(f"Please ensure app_spec.txt exists in: {get_project_prompts_dir(project_dir)}") # If failed with non-zero exit and no spec, might be auth issue if result.returncode != 0: - print(f"\nIf you're having authentication issues, try running: {cli_command} login") + print(f"\nIf you're having authentication issues, ensure {cli_command} is installed or set OPENCODE_API_KEY in your environment") return False except FileNotFoundError: cli_command = get_cli_command() print(f"\nError: '{cli_command}' command not found.") - if cli_command == "claude": - print("Make sure Claude Code CLI is installed:") - print(" npm install -g @anthropic-ai/claude-code") + if cli_command == "opencode": + print("Make sure the Opencode SDK is installed and configure OPENCODE_API_KEY:") + print(" pip install --pre opencode-ai") + print(" export OPENCODE_API_KEY=your_key_here") else: print(f"Make sure the '{cli_command}' CLI is installed and in your PATH.") return False @@ -324,12 +325,12 @@ def run_manual_spec_flow(project_dir: Path) -> bool: def ask_spec_creation_choice() -> str | None: - """Ask user whether to create spec with Claude or manually.""" + """Ask user whether to create spec with Opencode or manually.""" print("\n" + "-" * 40) print(" Specification Setup") print("-" * 40) print("\nHow would you like to define your project?") - print("\n[1] Create spec with Claude (recommended)") + print("\n[1] Create spec with Opencode (recommended)") print(" Interactive conversation to define your project") print("\n[2] Edit templates manually") print(" Edit the template files directly in your editor") @@ -349,8 +350,8 @@ def create_new_project_flow() -> tuple[str, Path] | None: 1. Get project name and path 2. Create project directory and scaffold prompts - 3. Ask: Claude or Manual? - 4. If Claude: Run /create-spec with project path + 3. Ask: Opencode or Manual? + 4. If Opencode: Run /create-spec with project path 5. If Manual: Show paths, wait for Enter 6. Return (name, path) tuple if successful """ @@ -369,7 +370,7 @@ def create_new_project_flow() -> tuple[str, Path] | None: if choice == 'b': return None elif choice == '1': - # Create spec with Claude + # Create spec with Opencode success = run_spec_creation(project_dir) if not success: print("\nYou can try again later or edit the templates manually.") diff --git a/start.sh b/start.sh index 88eaa8e4..af32ce3b 100644 --- a/start.sh +++ b/start.sh @@ -7,29 +7,26 @@ echo " Autonomous Coding Agent" echo "========================================" echo "" -# Check if Claude CLI is installed -if ! command -v claude &> /dev/null; then - echo "[ERROR] Claude CLI not found" +# Check if Opencode Python SDK is available +if ! python -c "import opencode_ai" &> /dev/null; then + echo "[ERROR] Opencode Python SDK not found" echo "" - echo "Please install Claude CLI first:" - echo " curl -fsSL https://claude.ai/install.sh | bash" + echo "Please install the SDK first:" + echo " pip install --pre opencode-ai" echo "" echo "Then run this script again." exit 1 fi -echo "[OK] Claude CLI found" +echo "[OK] Opencode SDK available" -# Note: Claude CLI no longer stores credentials in ~/.claude/.credentials.json -# We can't reliably check auth status without making an API call, so we just -# verify the CLI is installed and remind the user to login if needed -if [ -d "$HOME/.claude" ]; then - echo "[OK] Claude CLI directory found" - echo " (If you're not logged in, run: claude login)" +# Note: Opencode uses API keys or other auth mechanisms. Ensure OPENCODE_API_KEY is set. +if [ -n "${OPENCODE_API_KEY:-}" ]; then + echo "[OK] OPENCODE_API_KEY found in environment" else - echo "[!] Claude CLI not configured" + echo "[!] Opencode API key not configured" echo "" - echo "Please run 'claude login' to authenticate before continuing." + echo "Please set OPENCODE_API_KEY per https://opencode.ai/docs" echo "" read -p "Press Enter to continue anyway, or Ctrl+C to exit..." fi diff --git a/start_ui.sh b/start_ui.sh index a95cd8a0..ef62d84d 100644 --- a/start_ui.sh +++ b/start_ui.sh @@ -9,24 +9,22 @@ echo " AutoCoder UI" echo "====================================" echo "" -# Check if Claude CLI is installed -if ! command -v claude &> /dev/null; then - echo "[!] Claude CLI not found" +# Check if Opencode Python SDK is available +if ! python -c "import opencode_ai" &> /dev/null; then + echo "[!] Opencode Python SDK not found" echo "" - echo " The agent requires Claude CLI to work." - echo " Install it from: https://claude.ai/download" - echo "" - echo " After installing, run: claude login" + echo " The agent requires the Opencode SDK to be installed." + echo " Install it with: pip install --pre opencode-ai" echo "" else - echo "[OK] Claude CLI found" - # Note: Claude CLI no longer stores credentials in ~/.claude/.credentials.json - # We can't reliably check auth status without making an API call - if [ -d "$HOME/.claude" ]; then - echo " (If you're not logged in, run: claude login)" - else - echo "[!] Claude CLI not configured - run 'claude login' first" - fi + echo "[OK] Opencode SDK available" +fi + +# Note: Opencode uses API keys for authentication +if [ -n "${OPENCODE_API_KEY:-}" ]; then + echo "[OK] OPENCODE_API_KEY found in environment" +else + echo "[!] Opencode API key not configured - set OPENCODE_API_KEY" fi echo "" diff --git a/test_security.py b/test_security.py index 6788a6d4..a84c6d39 100644 --- a/test_security.py +++ b/test_security.py @@ -41,8 +41,8 @@ def check_hook(command: str, should_block: bool) -> bool: return True -def test_extract_commands(): - """Test the command extraction logic.""" +def run_extract_commands(): + """Run the command extraction tests and return (passed, failed).""" print("\nTesting command extraction:\n") passed = 0 failed = 0 @@ -69,8 +69,14 @@ def test_extract_commands(): return passed, failed -def test_validate_chmod(): - """Test chmod command validation.""" +def test_extract_commands(): + """Pytest wrapper for command extraction.""" + passed, failed = run_extract_commands() + assert failed == 0 + + +def run_validate_chmod(): + """Run chmod validation tests and return (passed, failed).""" print("\nTesting chmod validation:\n") passed = 0 failed = 0 @@ -112,8 +118,14 @@ def test_validate_chmod(): return passed, failed -def test_validate_init_script(): - """Test init.sh script execution validation.""" +def test_validate_chmod(): + """Pytest wrapper for chmod validation.""" + passed, failed = run_validate_chmod() + assert failed == 0 + + +def run_validate_init_script(): + """Run init.sh validation tests and return (passed, failed).""" print("\nTesting init.sh validation:\n") passed = 0 failed = 0 @@ -151,6 +163,12 @@ def test_validate_init_script(): return passed, failed +def test_validate_init_script(): + """Pytest wrapper for init.sh validation.""" + passed, failed = run_validate_init_script() + assert failed == 0 + + def main(): print("=" * 70) print(" SECURITY HOOK TESTS") @@ -160,17 +178,17 @@ def main(): failed = 0 # Test command extraction - ext_passed, ext_failed = test_extract_commands() + ext_passed, ext_failed = run_extract_commands() passed += ext_passed failed += ext_failed # Test chmod validation - chmod_passed, chmod_failed = test_validate_chmod() + chmod_passed, chmod_failed = run_validate_chmod() passed += chmod_passed failed += chmod_failed # Test init.sh validation - init_passed, init_failed = test_validate_init_script() + init_passed, init_failed = run_validate_init_script() passed += init_passed failed += init_failed diff --git a/tests/test_opencode_adapter.py b/tests/test_opencode_adapter.py new file mode 100644 index 00000000..bf8843a4 --- /dev/null +++ b/tests/test_opencode_adapter.py @@ -0,0 +1,86 @@ +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from opencode_adapter import AssistantMessage, OpencodeClient, TextBlock, ToolResultBlock + + +@pytest.mark.asyncio +async def test_context_and_query_uses_session_and_provider(): + mock_client = MagicMock() + mock_client.session.create = AsyncMock(return_value=SimpleNamespace(id="sess1")) + mock_client.app.providers = AsyncMock(return_value=SimpleNamespace(providers=[SimpleNamespace(id="prov1")])) + mock_client.session.chat = AsyncMock() + mock_client.aclose = AsyncMock() + + with patch("opencode_adapter.AsyncOpencode", return_value=mock_client): + client = OpencodeClient(Path("/tmp"), model="default", yolo_mode=False) + await client.__aenter__() + assert client._session.id == "sess1" + assert client._provider_id == "prov1" + + await client.query("Hello world") + mock_client.session.chat.assert_awaited() + called_args = mock_client.session.chat.call_args[0] + # First positional arg is session id + assert called_args[0] == "sess1" + + # kwargs include model_id and parts + kwargs = mock_client.session.chat.call_args.kwargs + assert kwargs["model_id"] == "default" + assert kwargs["parts"][0]["text"] == "Hello world" + + await client.__aexit__(None, None, None) + + +@pytest.mark.asyncio +async def test_receive_response_yields_text_and_tool_result(): + # Prepare messages sequences: one message with text part, then one with tool completed + text_part = SimpleNamespace(type="text", text="Hello from Opencode") + tool_state = SimpleNamespace(status="completed", output="Wrote file") + tool_part = SimpleNamespace(type="tool", tool="Write", state=tool_state) + + msg1 = SimpleNamespace(info=SimpleNamespace(role="assistant"), parts=[text_part]) + msg2 = SimpleNamespace(info=SimpleNamespace(role="assistant"), parts=[tool_part]) + + + mock_client = MagicMock() + mock_client.session.create = AsyncMock(return_value=SimpleNamespace(id="sess1")) + mock_client.app.providers = AsyncMock(return_value=SimpleNamespace(providers=[SimpleNamespace(id="prov1")])) + # Ensure aclose is awaitable + mock_client.aclose = AsyncMock() + + # messages returns cumulative lists (adapter expects cumulative history) + calls = [ [msg1], [msg1, msg2], [msg1, msg2], [], [], [] ] + async def fake_messages(session_id): + if calls: + return calls.pop(0) + return [] + + mock_client.session.messages = AsyncMock(side_effect=fake_messages) + + with patch("opencode_adapter.AsyncOpencode", return_value=mock_client): + client = OpencodeClient(Path("/tmp"), model="default", yolo_mode=False) + await client.__aenter__() + + results = [] + async for msg in client.receive_response(): + results.append(msg) + + # Two messages expected + assert len(results) == 2 + first = results[0] + second = results[1] + + assert isinstance(first, AssistantMessage) + assert isinstance(first.content[0], TextBlock) + assert first.content[0].text == "Hello from Opencode" + + # Tool result should be ToolResultBlock with content 'Wrote file' + assert any(isinstance(b, ToolResultBlock) for b in second.content) + tool_block = next(b for b in second.content if isinstance(b, ToolResultBlock)) + assert tool_block.content == "Wrote file" + + await client.__aexit__(None, None, None) diff --git a/ui/src/components/ExpandProjectChat.tsx b/ui/src/components/ExpandProjectChat.tsx index 1077a6da..0478b530 100644 --- a/ui/src/components/ExpandProjectChat.tsx +++ b/ui/src/components/ExpandProjectChat.tsx @@ -1,7 +1,7 @@ /** * Expand Project Chat Component * - * Full chat interface for interactive project expansion with Claude. + * Full chat interface for interactive project expansion using Opencode. * Allows users to describe new features in natural language. */ @@ -239,7 +239,7 @@ export function ExpandProjectChat({ Starting Project Expansion

- Connecting to Claude to help you add new features to your project... + Connecting to the agent (Opencode) to help you add new features to your project...

{connectionStatus === 'error' && (