Spaces:
Running
Running
feat(agent): add Claude Code-style agent, skills, slash-commands, hooks, todos, sandboxed workspace, and full-stack scaffolding
Browse filesAdds Claude Code-inspired agent capabilities:
- Agent loop with tools (read/write/edit/glob/grep/bash/todos)
- Skills system (markdown skill files, runtime loading)
- Slash commands (/commit /review /feature /design /explain /test /refactor /skill /help)
- Hooks system (block-dangerous-rm, warn-debug-code, warn-secrets-in-code, warn-eval-exec)
- Sandboxed workspace with path-escape protection
- Todo lists (todo_write / todo_read / todo_update)
- Full-stack scaffolding skill
- No external SDK dependencies; no Next.js conversion
- CLAUDE.md +147 -0
- README.md +81 -15
- code/agent/__init__.py +572 -0
- code/commands/__init__.py +148 -0
- code/commands/builtins/commit.md +25 -0
- code/commands/builtins/design.md +49 -0
- code/commands/builtins/explain.md +32 -0
- code/commands/builtins/feature.md +65 -0
- code/commands/builtins/help.md +25 -0
- code/commands/builtins/refactor.md +39 -0
- code/commands/builtins/review.md +50 -0
- code/commands/builtins/skill.md +12 -0
- code/commands/builtins/test.md +39 -0
- code/config/constants.py +40 -28
- code/hooks/__init__.py +242 -0
- code/hooks/builtins/block-dangerous-rm.local.md +16 -0
- code/hooks/builtins/warn-debug-code.local.md +11 -0
- code/hooks/builtins/warn-eval-exec.local.md +15 -0
- code/hooks/builtins/warn-secrets-in-code.local.md +22 -0
- code/server/routes.py +286 -1
- code/skills/__init__.py +192 -0
- code/skills/builtins/code-review/SKILL.md +65 -0
- code/skills/builtins/commit-workflow/SKILL.md +79 -0
- code/skills/builtins/debugging/SKILL.md +76 -0
- code/skills/builtins/feature-dev/SKILL.md +97 -0
- code/skills/builtins/frontend-design/SKILL.md +38 -0
- code/skills/builtins/fullstack-scaffold/SKILL.md +124 -0
- code/tools/__init__.py +38 -0
- code/tools/bash.py +127 -0
- code/tools/fs.py +378 -0
- code/tools/todos.py +86 -0
- index.html +523 -4
- requirements.txt +2 -0
CLAUDE.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SoniCoder β Project Memory
|
| 2 |
+
|
| 3 |
+
> This file is the project's persistent memory. The agent reads it on every session.
|
| 4 |
+
> Edit it freely β it overrides defaults.
|
| 5 |
+
|
| 6 |
+
## What is SoniCoder?
|
| 7 |
+
|
| 8 |
+
SoniCoder is a local-first AI coding agent that can:
|
| 9 |
+
- Generate complete fullstack applications in any language/framework
|
| 10 |
+
- Read, write, and edit files in a sandboxed workspace
|
| 11 |
+
- Run shell commands (git, npm, pip, tests)
|
| 12 |
+
- Apply specialized skills (frontend-design, feature-dev, code-review, debugging, fullstack-scaffold, commit-workflow)
|
| 13 |
+
- Respond to slash commands (/commit, /review, /feature, /design, /explain, /test, /refactor, /skill, /help)
|
| 14 |
+
- Deploy to HuggingFace Spaces with one click
|
| 15 |
+
|
| 16 |
+
## Architecture
|
| 17 |
+
|
| 18 |
+
```
|
| 19 |
+
app.py β Entry point: launches Gradio Server
|
| 20 |
+
code/
|
| 21 |
+
βββ config/constants.py β App config, system prompt, language options
|
| 22 |
+
βββ model/
|
| 23 |
+
β βββ loader.py β Dual model loading (text + VLM)
|
| 24 |
+
β βββ inference.py β Streaming inference (text + VLM)
|
| 25 |
+
βββ agent/__init__.py β Agent loop (model β tools)
|
| 26 |
+
βββ tools/
|
| 27 |
+
β βββ fs.py β read_file, write_file, edit_file, glob, grep, list_dir
|
| 28 |
+
β βββ bash.py β Sandboxed shell execution
|
| 29 |
+
β βββ todos.py β Todo list management
|
| 30 |
+
βββ skills/
|
| 31 |
+
β βββ __init__.py β Skill discovery + loading
|
| 32 |
+
β βββ builtins/ β Built-in skills (markdown)
|
| 33 |
+
βββ commands/
|
| 34 |
+
β βββ __init__.py β Slash command parser + expander
|
| 35 |
+
β βββ builtins/ β Built-in commands (markdown)
|
| 36 |
+
βββ hooks/
|
| 37 |
+
β βββ __init__.py β Hook rule engine
|
| 38 |
+
β βββ builtins/ β Built-in hook rules (markdown)
|
| 39 |
+
βββ execution/
|
| 40 |
+
β βββ code_extractor.py β Code extraction from model output
|
| 41 |
+
β βββ python_runner.py β Sandboxed Python execution
|
| 42 |
+
β βββ gradio_runner.py β Gradio app subprocess runner
|
| 43 |
+
βββ huggingface/
|
| 44 |
+
β βββ dockerfile_gen.py β Auto Dockerfile/package.json for JS
|
| 45 |
+
β βββ push.py β HF Hub push + ZIP packaging
|
| 46 |
+
βββ websearch/google_scraper.py β DuckDuckGo + Google scraping (no API)
|
| 47 |
+
βββ server/
|
| 48 |
+
βββ chat_helpers.py β Chat history + prompt building
|
| 49 |
+
βββ routes.py β All HTTP + API endpoints
|
| 50 |
+
index.html β Frontend (single-file SPA)
|
| 51 |
+
workspace/ β Sandboxed agent workspace (auto-created)
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
## Conventions
|
| 55 |
+
|
| 56 |
+
- **Python**: 3.11+, type hints everywhere, `from __future__ import annotations`
|
| 57 |
+
- **Style**: Black formatting, 4-space indent, 100 char line limit
|
| 58 |
+
- **Docstrings**: Google style for modules, functions, classes
|
| 59 |
+
- **Error handling**: catch specific exceptions, never bare `except:`
|
| 60 |
+
- **Logging**: use `logging.getLogger(__name__)`, never `print()`
|
| 61 |
+
- **Tests**: pytest, in `tests/` directory, `test_*.py` naming
|
| 62 |
+
- **Frontend**: single-file HTML with inline CSS/JS, no build step
|
| 63 |
+
|
| 64 |
+
## Server rules
|
| 65 |
+
|
| 66 |
+
- All servers bind to `0.0.0.0` (never `localhost`)
|
| 67 |
+
- Default port: `7860` (HF Spaces convention)
|
| 68 |
+
- Sub-servers use `7861`, `7862`, etc.
|
| 69 |
+
|
| 70 |
+
## Model
|
| 71 |
+
|
| 72 |
+
- Default: `openbmb/MiniCPM5-1B` (text-only, 2.17 GB)
|
| 73 |
+
- Optional: `openbmb/MiniCPM-V-4.6` (vision + text, 2.8 GB)
|
| 74 |
+
- Loaded in background thread on startup
|
| 75 |
+
- Streaming inference via `TextIteratorStreamer`
|
| 76 |
+
|
| 77 |
+
## Tool call format
|
| 78 |
+
|
| 79 |
+
The model calls tools by emitting fenced code blocks with `tool` as the language:
|
| 80 |
+
|
| 81 |
+
```tool
|
| 82 |
+
read_file
|
| 83 |
+
path: src/app.py
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
Multi-line values use YAML block scalars:
|
| 87 |
+
|
| 88 |
+
```tool
|
| 89 |
+
write_file
|
| 90 |
+
path: src/new.py
|
| 91 |
+
content: |
|
| 92 |
+
import os
|
| 93 |
+
def main():
|
| 94 |
+
pass
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
## Slash commands
|
| 98 |
+
|
| 99 |
+
| Command | Description |
|
| 100 |
+
|---------|-------------|
|
| 101 |
+
| `/commit` | Create a git commit with a generated message |
|
| 102 |
+
| `/review` | Review current changes for bugs and quality |
|
| 103 |
+
| `/feature <desc>` | Guided feature development workflow |
|
| 104 |
+
| `/design <brief>` | Generate a distinctive frontend design |
|
| 105 |
+
| `/explain <target>` | Explain how code works |
|
| 106 |
+
| `/test <target>` | Generate tests |
|
| 107 |
+
| `/refactor <target>` | Refactor code for clarity |
|
| 108 |
+
| `/skill <name>` | Load and apply a skill |
|
| 109 |
+
| `/help` | Show available commands and skills |
|
| 110 |
+
|
| 111 |
+
## Skills
|
| 112 |
+
|
| 113 |
+
| Skill | Description |
|
| 114 |
+
|-------|-------------|
|
| 115 |
+
| `frontend-design` | Distinctive visual design guidance |
|
| 116 |
+
| `feature-dev` | Guided feature implementation workflow |
|
| 117 |
+
| `code-review` | High-signal code review |
|
| 118 |
+
| `debugging` | Systematic debugging workflow |
|
| 119 |
+
| `fullstack-scaffold` | Project structure scaffolding rules |
|
| 120 |
+
| `commit-workflow` | Git commit best practices |
|
| 121 |
+
|
| 122 |
+
## Hooks
|
| 123 |
+
|
| 124 |
+
Hooks are markdown rules that fire on events (`bash`, `file`, `prompt`, `stop`).
|
| 125 |
+
They can `warn` (show a message) or `block` (prevent the action).
|
| 126 |
+
|
| 127 |
+
Built-in hooks:
|
| 128 |
+
- `block-dangerous-rm` β blocks `rm -rf /`, `~`, `$HOME`, `..`
|
| 129 |
+
- `warn-debug-code` οΏ½οΏ½οΏ½ warns on `console.log`, `debugger`, `print`, `alert`
|
| 130 |
+
- `warn-secrets-in-code` β warns on hardcoded API_KEY/SECRET/TOKEN/PASSWORD
|
| 131 |
+
- `warn-eval-exec` β warns on `eval()` and `exec()`
|
| 132 |
+
|
| 133 |
+
Users can add custom hooks in `workspace/.sonicoder/hooks/*.local.md`.
|
| 134 |
+
|
| 135 |
+
## Workspace
|
| 136 |
+
|
| 137 |
+
The agent's sandboxed filesystem lives at `./workspace/` (configurable via
|
| 138 |
+
`SONICODER_WORKSPACE` env var). All file tools refuse paths that escape this root.
|
| 139 |
+
|
| 140 |
+
## Deploy
|
| 141 |
+
|
| 142 |
+
Generated projects can be pushed to HuggingFace Spaces via the Deploy tab.
|
| 143 |
+
Supported SDKs:
|
| 144 |
+
- `static` β HTML/CSS/JS
|
| 145 |
+
- `gradio` β Python Gradio apps
|
| 146 |
+
- `streamlit` β Python Streamlit apps
|
| 147 |
+
- `docker` β JS/TS frameworks (auto-generates Dockerfile + package.json)
|
README.md
CHANGED
|
@@ -17,23 +17,33 @@ hf_oauth_scopes:
|
|
| 17 |
|
| 18 |
## SoniCoder
|
| 19 |
|
| 20 |
-
An AI-powered
|
| 21 |
|
| 22 |
-
|
| 23 |
|
| 24 |
-
- **
|
| 25 |
-
- **
|
| 26 |
-
- **
|
| 27 |
-
- **
|
| 28 |
-
- **
|
| 29 |
-
- **
|
| 30 |
-
- **HuggingFace Deploy**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
### Supported Languages & Frameworks
|
| 33 |
|
| 34 |
| Language | Frameworks |
|
| 35 |
|----------|-----------|
|
| 36 |
-
| Python | Flask, Django, FastAPI, Streamlit, Plain Python |
|
| 37 |
| JavaScript | React, Vue.js, Next.js, Express.js, Node.js, Vanilla JS |
|
| 38 |
| TypeScript | React, Next.js, Express.js, NestJS |
|
| 39 |
| HTML/CSS/JS | Tailwind CSS, Bootstrap, Vanilla |
|
|
@@ -46,6 +56,58 @@ An AI-powered fullstack application generator running **entirely locally** with
|
|
| 46 |
| Swift | Vapor, SwiftUI |
|
| 47 |
| Kotlin | Ktor, Spring Boot |
|
| 48 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
### Local Run
|
| 50 |
|
| 51 |
```bash
|
|
@@ -53,16 +115,20 @@ pip install -r requirements.txt
|
|
| 53 |
python app.py
|
| 54 |
```
|
| 55 |
|
| 56 |
-
The model (MiniCPM5-1B, ~2.17 GB)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
### HuggingFace Deploy
|
| 59 |
|
| 60 |
1. Generate your application
|
| 61 |
-
2. Go to the "Deploy" tab
|
| 62 |
-
3.
|
| 63 |
-
4. Select the Space SDK (
|
| 64 |
5. Click "Push to HuggingFace"
|
| 65 |
|
| 66 |
### No External APIs
|
| 67 |
|
| 68 |
-
This application does not use any external API calls. All model inference runs locally using
|
|
|
|
| 17 |
|
| 18 |
## SoniCoder
|
| 19 |
|
| 20 |
+
An AI-powered **autonomous coding agent** running entirely locally with no external API dependencies. Powered by [MiniCPM5-1B](https://huggingface.co/openbmb/MiniCPM5-1B) (2.17 GB).
|
| 21 |
|
| 22 |
+
Inspired by [Claude Code](https://github.com/anthropics/claude-code), SoniCoder adds:
|
| 23 |
|
| 24 |
+
- π€ **Agent Loop** β model calls tools (read/write/edit/glob/grep/bash/todos) in a feedback loop
|
| 25 |
+
- π― **Skills System** β load markdown skill files at runtime (frontend-design, feature-dev, code-review, debugging, fullstack-scaffold, commit-workflow)
|
| 26 |
+
- β‘ **Slash Commands** β `/commit`, `/review`, `/feature`, `/design`, `/explain`, `/test`, `/refactor`, `/skill`, `/help`
|
| 27 |
+
- πͺ **Hooks System** β pre/post tool execution rules (block dangerous commands, warn on debug code/secrets)
|
| 28 |
+
- π **Sandboxed Workspace** β agent manipulates files in `./workspace/` (path-escape protected)
|
| 29 |
+
- β
**Todo Lists** β track multi-step tasks Claude Code-style
|
| 30 |
+
- π **HuggingFace Deploy** β push generated projects directly to HuggingFace Spaces
|
| 31 |
+
|
| 32 |
+
### Features (original)
|
| 33 |
+
|
| 34 |
+
- **Local Inference**: MiniCPM5-1B via `transformers` β no API keys
|
| 35 |
+
- **Multi-Language**: Python, JavaScript, TypeScript, Java, Go, Rust, PHP, Ruby, C#, Swift, Kotlin
|
| 36 |
+
- **Frameworks**: React, Vue, Next.js, Express, Flask, Django, FastAPI, Spring Boot, and more
|
| 37 |
+
- **Live Preview**: sandboxed iframe preview of generated web apps
|
| 38 |
+
- **Code Execution**: run generated Python and see output
|
| 39 |
+
- **Project Download**: ZIP the generated project
|
| 40 |
+
- **HuggingFace Deploy**: one-click push to HF Spaces (Static/Gradio/Streamlit/Docker)
|
| 41 |
|
| 42 |
### Supported Languages & Frameworks
|
| 43 |
|
| 44 |
| Language | Frameworks |
|
| 45 |
|----------|-----------|
|
| 46 |
+
| Python | Flask, Django, FastAPI, Streamlit, Gradio, Plain Python |
|
| 47 |
| JavaScript | React, Vue.js, Next.js, Express.js, Node.js, Vanilla JS |
|
| 48 |
| TypeScript | React, Next.js, Express.js, NestJS |
|
| 49 |
| HTML/CSS/JS | Tailwind CSS, Bootstrap, Vanilla |
|
|
|
|
| 56 |
| Swift | Vapor, SwiftUI |
|
| 57 |
| Kotlin | Ktor, Spring Boot |
|
| 58 |
|
| 59 |
+
### Agent Tools
|
| 60 |
+
|
| 61 |
+
The agent can call these tools (Claude Code-style):
|
| 62 |
+
|
| 63 |
+
| Tool | Description |
|
| 64 |
+
|------|-------------|
|
| 65 |
+
| `read_file` | Read a file from the workspace |
|
| 66 |
+
| `write_file` | Write content to a file |
|
| 67 |
+
| `edit_file` | Replace text in a file (with uniqueness check) |
|
| 68 |
+
| `multi_edit` | Apply multiple edits atomically |
|
| 69 |
+
| `list_dir` | List directory contents |
|
| 70 |
+
| `glob` | Find files matching a pattern |
|
| 71 |
+
| `grep` | Search file contents with regex |
|
| 72 |
+
| `bash` | Run a shell command (sandboxed) |
|
| 73 |
+
| `todo_write` | Replace the todo list |
|
| 74 |
+
| `todo_read` | Read the current todo list |
|
| 75 |
+
| `todo_update` | Update a single todo |
|
| 76 |
+
|
| 77 |
+
### Slash Commands
|
| 78 |
+
|
| 79 |
+
| Command | Description |
|
| 80 |
+
|---------|-------------|
|
| 81 |
+
| `/commit [msg]` | Create a git commit with a generated message |
|
| 82 |
+
| `/review [file]` | Review changes for bugs and quality |
|
| 83 |
+
| `/feature <desc>` | Guided feature development |
|
| 84 |
+
| `/design <brief>` | Generate a distinctive frontend design |
|
| 85 |
+
| `/explain <target>` | Explain how code works |
|
| 86 |
+
| `/test [target]` | Generate tests |
|
| 87 |
+
| `/refactor <target>` | Refactor code for clarity |
|
| 88 |
+
| `/skill <name>` | Load and apply a skill |
|
| 89 |
+
| `/help` | Show available commands and skills |
|
| 90 |
+
|
| 91 |
+
### Built-in Skills
|
| 92 |
+
|
| 93 |
+
- **frontend-design** β distinctive visual design guidance (palette, typography, signature)
|
| 94 |
+
- **feature-dev** β 7-phase guided feature implementation
|
| 95 |
+
- **code-review** β high-signal review focusing on bugs and security
|
| 96 |
+
- **debugging** β systematic 6-phase debugging workflow
|
| 97 |
+
- **fullstack-scaffold** β project structure rules for any framework
|
| 98 |
+
- **commit-workflow** β conventional commits best practices
|
| 99 |
+
|
| 100 |
+
Add custom skills in `workspace/.sonicoder/skills/<name>/SKILL.md`.
|
| 101 |
+
|
| 102 |
+
### Built-in Hooks
|
| 103 |
+
|
| 104 |
+
- **block-dangerous-rm** β blocks `rm -rf /`, `~`, `$HOME`, `..`
|
| 105 |
+
- **warn-debug-code** β warns on `console.log`, `debugger`, `print`, `alert`
|
| 106 |
+
- **warn-secrets-in-code** β warns on hardcoded API_KEY/SECRET/TOKEN/PASSWORD
|
| 107 |
+
- **warn-eval-exec** β warns on `eval()` and `exec()`
|
| 108 |
+
|
| 109 |
+
Add custom hooks in `workspace/.sonicoder/hooks/<name>.local.md`.
|
| 110 |
+
|
| 111 |
### Local Run
|
| 112 |
|
| 113 |
```bash
|
|
|
|
| 115 |
python app.py
|
| 116 |
```
|
| 117 |
|
| 118 |
+
The model (MiniCPM5-1B, ~2.17 GB) downloads automatically on first run.
|
| 119 |
+
|
| 120 |
+
### Project Memory
|
| 121 |
+
|
| 122 |
+
The `CLAUDE.md` file at the project root is the agent's persistent memory. Edit it freely to override defaults and document project-specific conventions.
|
| 123 |
|
| 124 |
### HuggingFace Deploy
|
| 125 |
|
| 126 |
1. Generate your application
|
| 127 |
+
2. Go to the "Deploy" tab
|
| 128 |
+
3. Sign in with HuggingFace OAuth (or paste a token)
|
| 129 |
+
4. Select the Space SDK (Auto, Docker, Static, Gradio, Streamlit)
|
| 130 |
5. Click "Push to HuggingFace"
|
| 131 |
|
| 132 |
### No External APIs
|
| 133 |
|
| 134 |
+
This application does not use any external API calls. All model inference runs locally using `transformers` with MiniCPM5-1B. Web search uses DuckDuckGo/Google HTML scraping (no API key).
|
code/agent/__init__.py
ADDED
|
@@ -0,0 +1,572 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Agent orchestration β Claude Code-style agent loop.
|
| 2 |
+
|
| 3 |
+
The agent:
|
| 4 |
+
1. Receives a user prompt
|
| 5 |
+
2. Calls the model with available tools in system prompt
|
| 6 |
+
3. Parses the model's response for tool calls
|
| 7 |
+
4. Executes tools (with hooks checking)
|
| 8 |
+
5. Feeds results back to the model
|
| 9 |
+
6. Repeats until model stops calling tools or max iterations reached
|
| 10 |
+
|
| 11 |
+
Tool call format (model outputs):
|
| 12 |
+
```tool
|
| 13 |
+
read_file
|
| 14 |
+
path: src/app.py
|
| 15 |
+
```
|
| 16 |
+
|
| 17 |
+
Or multi-line:
|
| 18 |
+
```tool
|
| 19 |
+
write_file
|
| 20 |
+
path: src/new.py
|
| 21 |
+
content: |
|
| 22 |
+
import os
|
| 23 |
+
def main():
|
| 24 |
+
pass
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
The agent executes the tool, captures output, and feeds back as a
|
| 28 |
+
user-style message in the next iteration.
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
from __future__ import annotations
|
| 32 |
+
|
| 33 |
+
import json
|
| 34 |
+
import logging
|
| 35 |
+
import re
|
| 36 |
+
from typing import Any, Iterator
|
| 37 |
+
|
| 38 |
+
from code.commands import expand_command, parse_command_input
|
| 39 |
+
from code.config.constants import SYSTEM_PROMPT
|
| 40 |
+
from code.hooks import check_hook
|
| 41 |
+
from code.skills import build_skills_context
|
| 42 |
+
from code.tools import (
|
| 43 |
+
edit_file,
|
| 44 |
+
glob_paths,
|
| 45 |
+
grep_search,
|
| 46 |
+
list_dir,
|
| 47 |
+
multi_edit,
|
| 48 |
+
read_file,
|
| 49 |
+
run_bash,
|
| 50 |
+
todo_read,
|
| 51 |
+
todo_write,
|
| 52 |
+
todo_update,
|
| 53 |
+
write_file,
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
logger = logging.getLogger(__name__)
|
| 57 |
+
|
| 58 |
+
# βββ Tool registry ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 59 |
+
|
| 60 |
+
TOOL_REGISTRY: dict[str, Any] = {
|
| 61 |
+
"read_file": read_file,
|
| 62 |
+
"write_file": write_file,
|
| 63 |
+
"edit_file": edit_file,
|
| 64 |
+
"multi_edit": multi_edit,
|
| 65 |
+
"list_dir": list_dir,
|
| 66 |
+
"glob": glob_paths,
|
| 67 |
+
"grep": grep_search,
|
| 68 |
+
"bash": run_bash,
|
| 69 |
+
"todo_read": todo_read,
|
| 70 |
+
"todo_write": todo_write,
|
| 71 |
+
"todo_update": todo_update,
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _tool_schemas() -> str:
|
| 76 |
+
"""Return a description of all available tools for the system prompt."""
|
| 77 |
+
return """## Available Tools
|
| 78 |
+
|
| 79 |
+
You have access to these tools. To call a tool, output a fenced block with `tool` as the language, the tool name on the first line, and parameters as `key: value` pairs (one per line). For multi-line values, use YAML `|` block syntax.
|
| 80 |
+
|
| 81 |
+
### read_file
|
| 82 |
+
Read a text file from the workspace.
|
| 83 |
+
```
|
| 84 |
+
read_file
|
| 85 |
+
path: src/app.py
|
| 86 |
+
```
|
| 87 |
+
Optional: `offset` (1-indexed line to start from), `limit` (max lines).
|
| 88 |
+
|
| 89 |
+
### write_file
|
| 90 |
+
Write content to a file (creates parent dirs).
|
| 91 |
+
```
|
| 92 |
+
write_file
|
| 93 |
+
path: src/new.py
|
| 94 |
+
content: |
|
| 95 |
+
import os
|
| 96 |
+
def main():
|
| 97 |
+
print("hello")
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
### edit_file
|
| 101 |
+
Replace text in a file.
|
| 102 |
+
```
|
| 103 |
+
edit_file
|
| 104 |
+
path: src/app.py
|
| 105 |
+
old_str: print("hello")
|
| 106 |
+
new_str: print("goodbye")
|
| 107 |
+
```
|
| 108 |
+
Optional: `replace_all: true` to replace all occurrences.
|
| 109 |
+
|
| 110 |
+
### multi_edit
|
| 111 |
+
Apply multiple edits atomically.
|
| 112 |
+
```
|
| 113 |
+
multi_edit
|
| 114 |
+
path: src/app.py
|
| 115 |
+
edits: |
|
| 116 |
+
- old_str: "foo"
|
| 117 |
+
new_str: "bar"
|
| 118 |
+
- old_str: "baz"
|
| 119 |
+
new_str: "qux"
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
### list_dir
|
| 123 |
+
List directory contents.
|
| 124 |
+
```
|
| 125 |
+
list_dir
|
| 126 |
+
path: src
|
| 127 |
+
```
|
| 128 |
+
|
| 129 |
+
### glob
|
| 130 |
+
Find files matching a pattern.
|
| 131 |
+
```
|
| 132 |
+
glob
|
| 133 |
+
pattern: **/*.py
|
| 134 |
+
path: .
|
| 135 |
+
```
|
| 136 |
+
|
| 137 |
+
### grep
|
| 138 |
+
Search file contents with regex.
|
| 139 |
+
```
|
| 140 |
+
grep
|
| 141 |
+
pattern: def main
|
| 142 |
+
path: .
|
| 143 |
+
include: *.py
|
| 144 |
+
```
|
| 145 |
+
Optional: `ignore_case: true`, `max_results: 50`.
|
| 146 |
+
|
| 147 |
+
### bash
|
| 148 |
+
Run a shell command (sandboxed to workspace).
|
| 149 |
+
```
|
| 150 |
+
bash
|
| 151 |
+
command: npm test
|
| 152 |
+
timeout: 30
|
| 153 |
+
```
|
| 154 |
+
Optional: `cwd`, `timeout` (default 30s).
|
| 155 |
+
|
| 156 |
+
### todo_write
|
| 157 |
+
Replace the entire todo list.
|
| 158 |
+
```
|
| 159 |
+
todo_write
|
| 160 |
+
todos: |
|
| 161 |
+
- id: "1"
|
| 162 |
+
content: "Set up project structure"
|
| 163 |
+
status: "in_progress"
|
| 164 |
+
priority: "high"
|
| 165 |
+
- id: "2"
|
| 166 |
+
content: "Implement API endpoints"
|
| 167 |
+
status: "pending"
|
| 168 |
+
priority: "high"
|
| 169 |
+
```
|
| 170 |
+
|
| 171 |
+
### todo_read
|
| 172 |
+
Read the current todo list. No parameters.
|
| 173 |
+
|
| 174 |
+
### todo_update
|
| 175 |
+
Update a single todo by id.
|
| 176 |
+
```
|
| 177 |
+
todo_update
|
| 178 |
+
todo_id: "1"
|
| 179 |
+
status: "completed"
|
| 180 |
+
```
|
| 181 |
+
|
| 182 |
+
## Rules
|
| 183 |
+
|
| 184 |
+
- Call ONE tool per turn. Wait for the result before calling the next.
|
| 185 |
+
- After tool results come back, summarize what you learned and decide the next step.
|
| 186 |
+
- If you don't need a tool, just respond normally.
|
| 187 |
+
- Use `todo_write` to track multi-step tasks.
|
| 188 |
+
- Always use `read_file` before `edit_file` so you know the exact content.
|
| 189 |
+
- Use `bash` for git, test running, and other shell tasks.
|
| 190 |
+
"""
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
# βββ Tool call parsing ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 194 |
+
|
| 195 |
+
_TOOL_BLOCK_RE = re.compile(
|
| 196 |
+
r"```tool\s*\n(.*?)```",
|
| 197 |
+
re.DOTALL,
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def _parse_yaml_block(text: str) -> dict[str, Any]:
|
| 202 |
+
"""Parse a simple YAML-like block into a dict.
|
| 203 |
+
|
| 204 |
+
Supports:
|
| 205 |
+
- key: value (single line)
|
| 206 |
+
- key: | (multi-line block scalar)
|
| 207 |
+
- key: > (folded scalar)
|
| 208 |
+
- Nested lists with - item
|
| 209 |
+
"""
|
| 210 |
+
result: dict[str, Any] = {}
|
| 211 |
+
lines = text.split("\n")
|
| 212 |
+
i = 0
|
| 213 |
+
while i < len(lines):
|
| 214 |
+
line = lines[i]
|
| 215 |
+
stripped = line.rstrip()
|
| 216 |
+
if not stripped or stripped.startswith("#"):
|
| 217 |
+
i += 1
|
| 218 |
+
continue
|
| 219 |
+
|
| 220 |
+
# Match key: value or key: | or key: >
|
| 221 |
+
m = re.match(r"^(\w+)\s*:\s*(.*)$", stripped)
|
| 222 |
+
if not m:
|
| 223 |
+
i += 1
|
| 224 |
+
continue
|
| 225 |
+
|
| 226 |
+
key = m.group(1)
|
| 227 |
+
value = m.group(2).strip()
|
| 228 |
+
|
| 229 |
+
if value in ("|", "|-", ">", ">-"):
|
| 230 |
+
# Multi-line block scalar β collect indented lines
|
| 231 |
+
collect: list[str] = []
|
| 232 |
+
i += 1
|
| 233 |
+
while i < len(lines):
|
| 234 |
+
next_line = lines[i]
|
| 235 |
+
if next_line.strip() == "" and i + 1 < len(lines) and not lines[i + 1].startswith(" "):
|
| 236 |
+
break
|
| 237 |
+
if next_line.startswith(" ") or next_line.startswith("\t") or next_line.strip() == "":
|
| 238 |
+
collect.append(next_line)
|
| 239 |
+
i += 1
|
| 240 |
+
else:
|
| 241 |
+
break
|
| 242 |
+
# Dedent
|
| 243 |
+
block = "\n".join(collect)
|
| 244 |
+
# Remove common leading whitespace
|
| 245 |
+
block = re.sub(r"^( {2}|\t)", "", block, flags=re.MULTILINE)
|
| 246 |
+
result[key] = block.rstrip()
|
| 247 |
+
else:
|
| 248 |
+
# Try parsing as JSON for complex values
|
| 249 |
+
if value.startswith("[") or value.startswith("{"):
|
| 250 |
+
try:
|
| 251 |
+
result[key] = json.loads(value)
|
| 252 |
+
except json.JSONDecodeError:
|
| 253 |
+
result[key] = value
|
| 254 |
+
else:
|
| 255 |
+
result[key] = value
|
| 256 |
+
i += 1
|
| 257 |
+
|
| 258 |
+
return result
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def _parse_tool_call(text: str) -> dict[str, Any] | None:
|
| 262 |
+
"""Parse a single tool call block content into {tool, args}."""
|
| 263 |
+
lines = text.strip().split("\n", 1)
|
| 264 |
+
if not lines:
|
| 265 |
+
return None
|
| 266 |
+
|
| 267 |
+
tool_name = lines[0].strip()
|
| 268 |
+
if tool_name not in TOOL_REGISTRY:
|
| 269 |
+
return {"tool": tool_name, "error": f"Unknown tool: {tool_name}"}
|
| 270 |
+
|
| 271 |
+
args_block = lines[1] if len(lines) > 1 else ""
|
| 272 |
+
args = _parse_yaml_block(args_block)
|
| 273 |
+
|
| 274 |
+
# Type coercion for known int/bool fields
|
| 275 |
+
if "timeout" in args:
|
| 276 |
+
try:
|
| 277 |
+
args["timeout"] = int(args["timeout"])
|
| 278 |
+
except (ValueError, TypeError):
|
| 279 |
+
pass
|
| 280 |
+
if "offset" in args:
|
| 281 |
+
try:
|
| 282 |
+
args["offset"] = int(args["offset"])
|
| 283 |
+
except (ValueError, TypeError):
|
| 284 |
+
pass
|
| 285 |
+
if "limit" in args:
|
| 286 |
+
try:
|
| 287 |
+
args["limit"] = int(args["limit"])
|
| 288 |
+
except (ValueError, TypeError):
|
| 289 |
+
pass
|
| 290 |
+
if "replace_all" in args:
|
| 291 |
+
args["replace_all"] = str(args["replace_all"]).lower() in ("true", "1", "yes")
|
| 292 |
+
if "ignore_case" in args:
|
| 293 |
+
args["ignore_case"] = str(args["ignore_case"]).lower() in ("true", "1", "yes")
|
| 294 |
+
if "todos" in args and isinstance(args["todos"], str):
|
| 295 |
+
# Parse YAML list of todos
|
| 296 |
+
todos: list[dict[str, Any]] = []
|
| 297 |
+
for block in re.split(r"\n\s*-\s+", "\n" + args["todos"]):
|
| 298 |
+
if not block.strip():
|
| 299 |
+
continue
|
| 300 |
+
todo: dict[str, Any] = {}
|
| 301 |
+
for line in block.splitlines():
|
| 302 |
+
m = re.match(r"(\w+):\s*(.*)$", line.strip())
|
| 303 |
+
if m:
|
| 304 |
+
val = m.group(2).strip()
|
| 305 |
+
if m.group(1) in {"status", "priority"}:
|
| 306 |
+
todo[m.group(1)] = val
|
| 307 |
+
else:
|
| 308 |
+
todo[m.group(1)] = val
|
| 309 |
+
if todo:
|
| 310 |
+
todos.append(todo)
|
| 311 |
+
args["todos"] = todos
|
| 312 |
+
if "edits" in args and isinstance(args["edits"], str):
|
| 313 |
+
# Parse YAML list of edits
|
| 314 |
+
edits: list[dict[str, str]] = []
|
| 315 |
+
for block in re.split(r"\n\s*-\s+", "\n" + args["edits"]):
|
| 316 |
+
if not block.strip():
|
| 317 |
+
continue
|
| 318 |
+
edit: dict[str, str] = {}
|
| 319 |
+
for line in block.splitlines():
|
| 320 |
+
m = re.match(r"(\w+):\s*(.*)$", line.strip())
|
| 321 |
+
if m:
|
| 322 |
+
edit[m.group(1)] = m.group(2).strip()
|
| 323 |
+
if edit:
|
| 324 |
+
edits.append(edit)
|
| 325 |
+
args["edits"] = edits
|
| 326 |
+
|
| 327 |
+
return {"tool": tool_name, "args": args}
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
def find_tool_calls(text: str) -> list[dict[str, Any]]:
|
| 331 |
+
"""Find all tool call blocks in the model's output."""
|
| 332 |
+
calls: list[dict[str, Any]] = []
|
| 333 |
+
for match in _TOOL_BLOCK_RE.finditer(text):
|
| 334 |
+
parsed = _parse_tool_call(match.group(1))
|
| 335 |
+
if parsed:
|
| 336 |
+
calls.append(parsed)
|
| 337 |
+
return calls
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
# βββ Tool execution with hooks ββββββββββββββββββββββββββββββββββββββββββ
|
| 341 |
+
|
| 342 |
+
def execute_tool(tool_name: str, args: dict[str, Any]) -> dict[str, Any]:
|
| 343 |
+
"""Execute a single tool with hook checks."""
|
| 344 |
+
if tool_name not in TOOL_REGISTRY:
|
| 345 |
+
return {"success": False, "error": f"Unknown tool: {tool_name}"}
|
| 346 |
+
|
| 347 |
+
# Hook check
|
| 348 |
+
if tool_name == "bash":
|
| 349 |
+
hook_context = {"command": str(args.get("command", ""))}
|
| 350 |
+
hook_result = check_hook("bash", hook_context)
|
| 351 |
+
elif tool_name in {"write_file", "edit_file", "multi_edit"}:
|
| 352 |
+
hook_context = {
|
| 353 |
+
"file_path": str(args.get("path", "")),
|
| 354 |
+
"new_text": str(args.get("content", args.get("new_str", ""))),
|
| 355 |
+
}
|
| 356 |
+
hook_result = check_hook("file", hook_context)
|
| 357 |
+
else:
|
| 358 |
+
hook_result = {"blocked": False, "warnings": [], "matched_hooks": []}
|
| 359 |
+
|
| 360 |
+
if hook_result["blocked"]:
|
| 361 |
+
return {
|
| 362 |
+
"success": False,
|
| 363 |
+
"error": "Blocked by hook rule",
|
| 364 |
+
"hook_warnings": hook_result["warnings"],
|
| 365 |
+
"blocked": True,
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
try:
|
| 369 |
+
fn = TOOL_REGISTRY[tool_name]
|
| 370 |
+
result = fn(**args) if args else fn()
|
| 371 |
+
# Attach any warnings
|
| 372 |
+
if hook_result["warnings"]:
|
| 373 |
+
result["hook_warnings"] = hook_result["warnings"]
|
| 374 |
+
return result
|
| 375 |
+
except TypeError as exc:
|
| 376 |
+
return {"success": False, "error": f"Invalid arguments: {exc}"}
|
| 377 |
+
except Exception as exc:
|
| 378 |
+
logger.exception("Tool execution failed: %s", tool_name)
|
| 379 |
+
return {"success": False, "error": str(exc)}
|
| 380 |
+
|
| 381 |
+
|
| 382 |
+
# βββ Agent loop βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 383 |
+
|
| 384 |
+
MAX_ITERATIONS = 8
|
| 385 |
+
|
| 386 |
+
|
| 387 |
+
def build_agent_system_prompt(
|
| 388 |
+
target_language: str = "",
|
| 389 |
+
target_framework: str = "",
|
| 390 |
+
skills: list[str] | None = None,
|
| 391 |
+
) -> str:
|
| 392 |
+
"""Build the system prompt with tool descriptions and skill context."""
|
| 393 |
+
parts = [
|
| 394 |
+
SYSTEM_PROMPT,
|
| 395 |
+
"",
|
| 396 |
+
_tool_schemas(),
|
| 397 |
+
"",
|
| 398 |
+
"## Agent Behavior",
|
| 399 |
+
"",
|
| 400 |
+
"- You are an autonomous coding agent. Use tools to inspect and modify the workspace.",
|
| 401 |
+
"- Always plan first with `todo_write` when given a multi-step task.",
|
| 402 |
+
"- Use `read_file` before `edit_file` to know exact content.",
|
| 403 |
+
"- After each tool result, briefly note what you learned before the next step.",
|
| 404 |
+
"- When done, give a concise summary of what you did and what files changed.",
|
| 405 |
+
"- If a hook warns you, acknowledge it and adjust your approach.",
|
| 406 |
+
]
|
| 407 |
+
|
| 408 |
+
if target_language or target_framework:
|
| 409 |
+
parts.append("")
|
| 410 |
+
parts.append(f"Target: {target_language}" + (f" / {target_framework}" if target_framework else ""))
|
| 411 |
+
|
| 412 |
+
skills_ctx = build_skills_context(skills)
|
| 413 |
+
if skills_ctx:
|
| 414 |
+
parts.append("")
|
| 415 |
+
parts.append("## Skills Loaded")
|
| 416 |
+
parts.append("")
|
| 417 |
+
parts.append(skills_ctx)
|
| 418 |
+
|
| 419 |
+
return "\n".join(parts)
|
| 420 |
+
|
| 421 |
+
|
| 422 |
+
def run_agent(
|
| 423 |
+
user_input: str,
|
| 424 |
+
history: list[dict[str, str]] | None = None,
|
| 425 |
+
target_language: str = "",
|
| 426 |
+
target_framework: str = "",
|
| 427 |
+
skills: list[str] | None = None,
|
| 428 |
+
search_context: str = "",
|
| 429 |
+
image_url: str | None = None,
|
| 430 |
+
) -> Iterator[dict[str, Any]]:
|
| 431 |
+
"""Run the agent loop. Yields events as dict.
|
| 432 |
+
|
| 433 |
+
Events:
|
| 434 |
+
- {type: "status", status_text, status_state, ...}
|
| 435 |
+
- {type: "tool_call", tool, args, result}
|
| 436 |
+
- {type: "streaming", content, ...}
|
| 437 |
+
- {type: "complete", content, ...}
|
| 438 |
+
- {type: "error", message, ...}
|
| 439 |
+
"""
|
| 440 |
+
from code.model.inference import call_model
|
| 441 |
+
from code.model.loader import get_model_status, is_model_loaded
|
| 442 |
+
|
| 443 |
+
history = history or []
|
| 444 |
+
|
| 445 |
+
# Check for slash command
|
| 446 |
+
cmd_name, cmd_args = parse_command_input(user_input)
|
| 447 |
+
if cmd_name:
|
| 448 |
+
expansion = expand_command(cmd_name, cmd_args)
|
| 449 |
+
if expansion.get("success"):
|
| 450 |
+
# Replace user input with expanded command
|
| 451 |
+
user_input = expansion["prompt"]
|
| 452 |
+
yield {
|
| 453 |
+
"type": "status",
|
| 454 |
+
"status_text": f"Running /{cmd_name} command...",
|
| 455 |
+
"status_state": "working",
|
| 456 |
+
}
|
| 457 |
+
else:
|
| 458 |
+
yield {
|
| 459 |
+
"type": "error",
|
| 460 |
+
"message": expansion.get("error", "Unknown command"),
|
| 461 |
+
"available": expansion.get("available", []),
|
| 462 |
+
}
|
| 463 |
+
return
|
| 464 |
+
|
| 465 |
+
# Hook check on user prompt
|
| 466 |
+
prompt_hook = check_hook("prompt", {"user_prompt": user_input})
|
| 467 |
+
if prompt_hook["blocked"]:
|
| 468 |
+
yield {
|
| 469 |
+
"type": "error",
|
| 470 |
+
"message": "Prompt blocked by hook rule",
|
| 471 |
+
"warnings": prompt_hook["warnings"],
|
| 472 |
+
}
|
| 473 |
+
return
|
| 474 |
+
|
| 475 |
+
# Model status
|
| 476 |
+
if not is_model_loaded():
|
| 477 |
+
status = get_model_status()
|
| 478 |
+
yield {
|
| 479 |
+
"type": "error",
|
| 480 |
+
"message": status["message"],
|
| 481 |
+
}
|
| 482 |
+
return
|
| 483 |
+
|
| 484 |
+
# Build system prompt
|
| 485 |
+
system_prompt = build_agent_system_prompt(target_language, target_framework, skills)
|
| 486 |
+
|
| 487 |
+
# Add search context if present
|
| 488 |
+
if search_context:
|
| 489 |
+
user_input = f"{user_input}\n\n--- Web Search Results ---\n{search_context}"
|
| 490 |
+
|
| 491 |
+
# Build messages
|
| 492 |
+
messages: list[dict[str, Any]] = [{"role": "system", "content": system_prompt}]
|
| 493 |
+
for h in history:
|
| 494 |
+
role = h.get("role", "user")
|
| 495 |
+
content = str(h.get("content", "")).strip()
|
| 496 |
+
if role in {"user", "assistant"} and content:
|
| 497 |
+
messages.append({"role": role, "content": content})
|
| 498 |
+
messages.append({"role": "user", "content": user_input})
|
| 499 |
+
|
| 500 |
+
# Agent loop
|
| 501 |
+
for iteration in range(MAX_ITERATIONS):
|
| 502 |
+
yield {
|
| 503 |
+
"type": "status",
|
| 504 |
+
"status_text": f"Thinking... (step {iteration + 1})",
|
| 505 |
+
"status_state": "working",
|
| 506 |
+
"iteration": iteration + 1,
|
| 507 |
+
}
|
| 508 |
+
|
| 509 |
+
# Call model
|
| 510 |
+
full_response = ""
|
| 511 |
+
for partial in call_model(messages, image_url=image_url):
|
| 512 |
+
full_response = partial
|
| 513 |
+
yield {
|
| 514 |
+
"type": "streaming",
|
| 515 |
+
"content": partial,
|
| 516 |
+
"iteration": iteration + 1,
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
if not full_response:
|
| 520 |
+
yield {"type": "error", "message": "Empty model response"}
|
| 521 |
+
return
|
| 522 |
+
|
| 523 |
+
# Check for tool calls
|
| 524 |
+
tool_calls = find_tool_calls(full_response)
|
| 525 |
+
|
| 526 |
+
if not tool_calls:
|
| 527 |
+
# No tools called β final response
|
| 528 |
+
yield {
|
| 529 |
+
"type": "complete",
|
| 530 |
+
"content": full_response,
|
| 531 |
+
"iterations": iteration + 1,
|
| 532 |
+
}
|
| 533 |
+
return
|
| 534 |
+
|
| 535 |
+
# Execute each tool call in order
|
| 536 |
+
for tc in tool_calls:
|
| 537 |
+
tool_name = tc.get("tool")
|
| 538 |
+
args = tc.get("args", {})
|
| 539 |
+
|
| 540 |
+
if "error" in tc:
|
| 541 |
+
# Unknown tool
|
| 542 |
+
tool_result = {"success": False, "error": tc["error"]}
|
| 543 |
+
else:
|
| 544 |
+
yield {
|
| 545 |
+
"type": "tool_call",
|
| 546 |
+
"tool": tool_name,
|
| 547 |
+
"args": args,
|
| 548 |
+
"iteration": iteration + 1,
|
| 549 |
+
}
|
| 550 |
+
tool_result = execute_tool(tool_name, args)
|
| 551 |
+
|
| 552 |
+
yield {
|
| 553 |
+
"type": "tool_result",
|
| 554 |
+
"tool": tool_name,
|
| 555 |
+
"result": tool_result,
|
| 556 |
+
"iteration": iteration + 1,
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
+
# Feed result back to model
|
| 560 |
+
result_str = json.dumps(tool_result, indent=2, default=str)
|
| 561 |
+
messages.append({"role": "assistant", "content": full_response})
|
| 562 |
+
messages.append({
|
| 563 |
+
"role": "user",
|
| 564 |
+
"content": f"Tool `{tool_name}` result:\n```json\n{result_str}\n```\n\nContinue with the next step or finish if done.",
|
| 565 |
+
})
|
| 566 |
+
|
| 567 |
+
# Max iterations reached
|
| 568 |
+
yield {
|
| 569 |
+
"type": "complete",
|
| 570 |
+
"content": full_response + "\n\n_(Max iterations reached)_",
|
| 571 |
+
"iterations": MAX_ITERATIONS,
|
| 572 |
+
}
|
code/commands/__init__.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Slash commands system β Claude Code-style.
|
| 2 |
+
|
| 3 |
+
Commands are markdown files with YAML frontmatter that define
|
| 4 |
+
prompt templates triggered by `/command` syntax.
|
| 5 |
+
|
| 6 |
+
Built-in commands live in code/commands/builtins/.
|
| 7 |
+
User commands live in workspace's .sonicoder/commands/.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import logging
|
| 13 |
+
import os
|
| 14 |
+
import re
|
| 15 |
+
from typing import Any
|
| 16 |
+
|
| 17 |
+
from code.skills import _parse_frontmatter
|
| 18 |
+
|
| 19 |
+
logger = logging.getLogger(__name__)
|
| 20 |
+
|
| 21 |
+
_BUILTIN_COMMANDS_DIR = os.path.join(os.path.dirname(__file__), "builtins")
|
| 22 |
+
_USER_COMMANDS_DIRNAME = ".sonicoder/commands"
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _command_dirs() -> list[str]:
|
| 26 |
+
dirs = [_BUILTIN_COMMANDS_DIR]
|
| 27 |
+
try:
|
| 28 |
+
from code.tools.fs import get_workspace_root
|
| 29 |
+
user_dir = os.path.join(get_workspace_root(), _USER_COMMANDS_DIRNAME)
|
| 30 |
+
if os.path.isdir(user_dir):
|
| 31 |
+
dirs.append(user_dir)
|
| 32 |
+
except Exception:
|
| 33 |
+
pass
|
| 34 |
+
return dirs
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _load_command(filepath: str) -> dict[str, Any] | None:
|
| 38 |
+
try:
|
| 39 |
+
with open(filepath, "r", encoding="utf-8") as f:
|
| 40 |
+
content = f.read()
|
| 41 |
+
except Exception as exc:
|
| 42 |
+
logger.warning("Failed to read %s: %s", filepath, exc)
|
| 43 |
+
return None
|
| 44 |
+
|
| 45 |
+
meta, body = _parse_frontmatter(content)
|
| 46 |
+
name = meta.get("name") or os.path.splitext(os.path.basename(filepath))[0]
|
| 47 |
+
return {
|
| 48 |
+
"name": name,
|
| 49 |
+
"description": meta.get("description", ""),
|
| 50 |
+
"argument_hint": meta.get("argument-hint", ""),
|
| 51 |
+
"allowed_tools": [t.strip() for t in meta.get("allowed-tools", "").split(",") if t.strip()],
|
| 52 |
+
"body": body.strip(),
|
| 53 |
+
"path": filepath,
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def list_commands() -> list[dict[str, Any]]:
|
| 58 |
+
"""List all available slash commands."""
|
| 59 |
+
commands: list[dict[str, Any]] = []
|
| 60 |
+
seen: set[str] = set()
|
| 61 |
+
|
| 62 |
+
for cmds_dir in _command_dirs():
|
| 63 |
+
if not os.path.isdir(cmds_dir):
|
| 64 |
+
continue
|
| 65 |
+
for entry in sorted(os.listdir(cmds_dir)):
|
| 66 |
+
if not entry.endswith(".md"):
|
| 67 |
+
continue
|
| 68 |
+
filepath = os.path.join(cmds_dir, entry)
|
| 69 |
+
cmd = _load_command(filepath)
|
| 70 |
+
if cmd and cmd["name"] not in seen:
|
| 71 |
+
seen.add(cmd["name"])
|
| 72 |
+
commands.append({
|
| 73 |
+
"name": cmd["name"],
|
| 74 |
+
"description": cmd["description"],
|
| 75 |
+
"argument_hint": cmd["argument_hint"],
|
| 76 |
+
})
|
| 77 |
+
return commands
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def get_command(name: str) -> dict[str, Any] | None:
|
| 81 |
+
"""Get full command content by name."""
|
| 82 |
+
for cmds_dir in _command_dirs():
|
| 83 |
+
if not os.path.isdir(cmds_dir):
|
| 84 |
+
continue
|
| 85 |
+
# Try name.md and name/something.md
|
| 86 |
+
direct = os.path.join(cmds_dir, f"{name}.md")
|
| 87 |
+
if os.path.isfile(direct):
|
| 88 |
+
return _load_command(direct)
|
| 89 |
+
# Try subdirectory: name/command.md
|
| 90 |
+
if os.path.isdir(os.path.join(cmds_dir, name)):
|
| 91 |
+
for entry in os.listdir(os.path.join(cmds_dir, name)):
|
| 92 |
+
if entry.endswith(".md"):
|
| 93 |
+
return _load_command(os.path.join(cmds_dir, name, entry))
|
| 94 |
+
return None
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def parse_command_input(user_input: str) -> tuple[str | None, str]:
|
| 98 |
+
"""Parse a user input string for a slash command.
|
| 99 |
+
|
| 100 |
+
Returns (command_name, arguments) or (None, user_input) if not a command.
|
| 101 |
+
"""
|
| 102 |
+
stripped = user_input.strip()
|
| 103 |
+
if not stripped.startswith("/"):
|
| 104 |
+
return None, user_input
|
| 105 |
+
|
| 106 |
+
# Match /command-name or /namespace:command
|
| 107 |
+
match = re.match(r"^/([a-zA-Z][\w:-]*)\s*(.*)$", stripped, re.DOTALL)
|
| 108 |
+
if not match:
|
| 109 |
+
return None, user_input
|
| 110 |
+
|
| 111 |
+
return match.group(1), match.group(2).strip()
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def expand_command(name: str, arguments: str = "") -> dict[str, Any]:
|
| 115 |
+
"""Expand a slash command into a full prompt for the model.
|
| 116 |
+
|
| 117 |
+
Replaces $ARGUMENTS placeholder with the user-provided arguments.
|
| 118 |
+
"""
|
| 119 |
+
cmd = get_command(name)
|
| 120 |
+
if not cmd:
|
| 121 |
+
return {
|
| 122 |
+
"success": False,
|
| 123 |
+
"error": f"Unknown command: /{name}",
|
| 124 |
+
"available": [c["name"] for c in list_commands()],
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
body = cmd["body"]
|
| 128 |
+
# Replace $ARGUMENTS
|
| 129 |
+
expanded = body.replace("$ARGUMENTS", arguments)
|
| 130 |
+
|
| 131 |
+
# Also support bash-style $(cmd) execution for context blocks (like Claude Code)
|
| 132 |
+
# e.g. !`git status` becomes the output of `git status`
|
| 133 |
+
from code.tools.bash import run_bash
|
| 134 |
+
|
| 135 |
+
def _exec_bash(match: re.Match) -> str:
|
| 136 |
+
cmd_str = match.group(1)
|
| 137 |
+
result = run_bash(cmd_str, timeout=10)
|
| 138 |
+
return result.get("stdout", "") + result.get("stderr", "")
|
| 139 |
+
|
| 140 |
+
expanded = re.sub(r"!`([^`]+)`", _exec_bash, expanded)
|
| 141 |
+
|
| 142 |
+
return {
|
| 143 |
+
"success": True,
|
| 144 |
+
"name": cmd["name"],
|
| 145 |
+
"description": cmd["description"],
|
| 146 |
+
"prompt": expanded,
|
| 147 |
+
"allowed_tools": cmd["allowed_tools"],
|
| 148 |
+
}
|
code/commands/builtins/commit.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: commit
|
| 3 |
+
description: Create a git commit with a generated message
|
| 4 |
+
argument-hint: Optional commit message override
|
| 5 |
+
---
|
| 6 |
+
## Context
|
| 7 |
+
|
| 8 |
+
- Current git status: !`git status`
|
| 9 |
+
- Current git diff (staged and unstaged changes): !`git diff HEAD`
|
| 10 |
+
- Current branch: !`git branch --show-current`
|
| 11 |
+
- Recent commits: !`git log --oneline -10`
|
| 12 |
+
|
| 13 |
+
## Your task
|
| 14 |
+
|
| 15 |
+
Based on the above changes, create a single git commit.
|
| 16 |
+
|
| 17 |
+
Write a clean commit message in conventional-commits format:
|
| 18 |
+
- Type: feat, fix, docs, style, refactor, perf, test, chore
|
| 19 |
+
- Subject: imperative mood, lowercase, under 72 chars, no period
|
| 20 |
+
- Optional body explaining WHY (not what)
|
| 21 |
+
|
| 22 |
+
If $ARGUMENTS is provided, use it as the commit message.
|
| 23 |
+
|
| 24 |
+
Stage the relevant files and commit using a heredoc to preserve formatting.
|
| 25 |
+
Do not use any other tools or do anything else. Do not send any other text besides these tool calls.
|
code/commands/builtins/design.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: design
|
| 3 |
+
description: Generate a distinctive frontend design β palette, typography, layout, signature
|
| 4 |
+
argument-hint: Design brief (subject, audience, mood)
|
| 5 |
+
---
|
| 6 |
+
# Frontend Design
|
| 7 |
+
|
| 8 |
+
Brief: $ARGUMENTS
|
| 9 |
+
|
| 10 |
+
You are the design lead at a small studio known for giving every client a visual identity that could not be mistaken for anyone else's.
|
| 11 |
+
|
| 12 |
+
## Step 1: Pin the subject
|
| 13 |
+
|
| 14 |
+
If the brief doesn't pin down what the product is, do it yourself:
|
| 15 |
+
- Name one concrete subject
|
| 16 |
+
- Its audience
|
| 17 |
+
- The page's single job
|
| 18 |
+
|
| 19 |
+
## Step 2: Design tokens
|
| 20 |
+
|
| 21 |
+
Output a compact token system:
|
| 22 |
+
|
| 23 |
+
### Color (4-6 named hex values)
|
| 24 |
+
- Background, foreground, accent, muted, border, etc.
|
| 25 |
+
|
| 26 |
+
### Typography (2+ faces)
|
| 27 |
+
- Display face (used with restraint)
|
| 28 |
+
- Body face
|
| 29 |
+
- Optional utility face for captions/data
|
| 30 |
+
|
| 31 |
+
### Layout
|
| 32 |
+
- One-sentence concept
|
| 33 |
+
- ASCII wireframe
|
| 34 |
+
|
| 35 |
+
### Signature
|
| 36 |
+
- The single unique element this page will be remembered by
|
| 37 |
+
|
| 38 |
+
## Step 3: Build
|
| 39 |
+
|
| 40 |
+
After the design plan, generate the complete HTML/CSS/JS in a single self-contained file (or multi-file @@FILE: project if appropriate).
|
| 41 |
+
|
| 42 |
+
Rules:
|
| 43 |
+
- Mobile responsive (html/body margin:0, 100% width)
|
| 44 |
+
- Visible keyboard focus states
|
| 45 |
+
- Respect `prefers-reduced-motion`
|
| 46 |
+
- Take ONE real aesthetic risk you can justify
|
| 47 |
+
- Cut any decoration that doesn't serve the brief
|
| 48 |
+
|
| 49 |
+
Output the design plan first, then the code.
|
code/commands/builtins/explain.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: explain
|
| 3 |
+
description: Explain how the current codebase or a specific file works
|
| 4 |
+
argument-hint: File path or "codebase"
|
| 5 |
+
---
|
| 6 |
+
# Explain
|
| 7 |
+
|
| 8 |
+
Target: $ARGUMENTS
|
| 9 |
+
|
| 10 |
+
Explain the code clearly and concisely.
|
| 11 |
+
|
| 12 |
+
If target is "codebase" or empty:
|
| 13 |
+
1. Use `list_dir` to map the project structure
|
| 14 |
+
2. Identify the entry point (app.py, main.py, index.js, etc.)
|
| 15 |
+
3. Read the entry point and key modules
|
| 16 |
+
4. Produce a high-level architecture summary
|
| 17 |
+
|
| 18 |
+
If target is a file path:
|
| 19 |
+
1. `read_file` the target
|
| 20 |
+
2. Read any files it imports/requires (one level deep)
|
| 21 |
+
3. Explain:
|
| 22 |
+
- What the file does
|
| 23 |
+
- Its main functions/classes
|
| 24 |
+
- How it fits into the larger project
|
| 25 |
+
- Any non-obvious patterns or gotchas
|
| 26 |
+
|
| 27 |
+
Format your explanation for a developer who is new to this code. Use:
|
| 28 |
+
- Short paragraphs for prose
|
| 29 |
+
- Code snippets for examples
|
| 30 |
+
- A "Key takeaways" list at the end
|
| 31 |
+
|
| 32 |
+
Don't editorialize β describe what's there, not what should be.
|
code/commands/builtins/feature.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: feature
|
| 3 |
+
description: Guided feature development β understand, design, then implement
|
| 4 |
+
argument-hint: Feature description
|
| 5 |
+
---
|
| 6 |
+
# Feature Development
|
| 7 |
+
|
| 8 |
+
Initial request: $ARGUMENTS
|
| 9 |
+
|
| 10 |
+
You are helping a developer implement a new feature. Follow a systematic approach.
|
| 11 |
+
|
| 12 |
+
## Phase 1: Discovery
|
| 13 |
+
|
| 14 |
+
If the feature is unclear, ask the user:
|
| 15 |
+
- What problem are they solving?
|
| 16 |
+
- What should the feature do?
|
| 17 |
+
- Any constraints or requirements?
|
| 18 |
+
|
| 19 |
+
## Phase 2: Codebase Exploration
|
| 20 |
+
|
| 21 |
+
Use `list_dir` and `glob` to map the project, then `grep` and `read_file` to find:
|
| 22 |
+
- Similar existing features
|
| 23 |
+
- Architecture patterns
|
| 24 |
+
- Naming conventions
|
| 25 |
+
- Test patterns
|
| 26 |
+
|
| 27 |
+
## Phase 3: Clarifying Questions
|
| 28 |
+
|
| 29 |
+
Present ALL clarifying questions to the user before designing. Cover:
|
| 30 |
+
- Edge cases
|
| 31 |
+
- Error handling
|
| 32 |
+
- Integration points
|
| 33 |
+
- Scope boundaries
|
| 34 |
+
- Backward compatibility
|
| 35 |
+
|
| 36 |
+
Wait for answers before proceeding.
|
| 37 |
+
|
| 38 |
+
## Phase 4: Architecture Design
|
| 39 |
+
|
| 40 |
+
Design the implementation:
|
| 41 |
+
- Files to create/modify
|
| 42 |
+
- Component responsibilities
|
| 43 |
+
- Data flow
|
| 44 |
+
- Build sequence
|
| 45 |
+
|
| 46 |
+
Present to user with trade-offs. **Ask for approval.**
|
| 47 |
+
|
| 48 |
+
## Phase 5: Implementation
|
| 49 |
+
|
| 50 |
+
After approval:
|
| 51 |
+
1. Create a todo list with `todo_write`
|
| 52 |
+
2. Read all relevant files
|
| 53 |
+
3. Implement following codebase conventions
|
| 54 |
+
4. Update todos as you progress
|
| 55 |
+
|
| 56 |
+
## Phase 6: Quality Review
|
| 57 |
+
|
| 58 |
+
Self-review for:
|
| 59 |
+
- Bugs / functional correctness
|
| 60 |
+
- Simplicity / DRY
|
| 61 |
+
- Project conventions
|
| 62 |
+
|
| 63 |
+
## Phase 7: Summary
|
| 64 |
+
|
| 65 |
+
Summarize what was built, decisions made, files modified, and next steps.
|
code/commands/builtins/help.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: help
|
| 3 |
+
description: Show available commands and skills
|
| 4 |
+
argument-hint: Optional command name to get help for
|
| 5 |
+
---
|
| 6 |
+
# Help
|
| 7 |
+
|
| 8 |
+
If $ARGUMENTS is empty, list all available slash commands and skills with brief descriptions.
|
| 9 |
+
|
| 10 |
+
If $ARGUMENTS is a command or skill name, show its full description and usage.
|
| 11 |
+
|
| 12 |
+
Format:
|
| 13 |
+
|
| 14 |
+
```
|
| 15 |
+
## Commands
|
| 16 |
+
/command-name β Description
|
| 17 |
+
...
|
| 18 |
+
|
| 19 |
+
## Skills
|
| 20 |
+
skill-name β Description
|
| 21 |
+
...
|
| 22 |
+
|
| 23 |
+
Type /command for any command above.
|
| 24 |
+
Type /skill <skill-name> to load skill instructions.
|
| 25 |
+
```
|
code/commands/builtins/refactor.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: refactor
|
| 3 |
+
description: Refactor code for clarity, simplicity, and maintainability
|
| 4 |
+
argument-hint: File path or "current changes"
|
| 5 |
+
---
|
| 6 |
+
# Refactor
|
| 7 |
+
|
| 8 |
+
Target: $ARGUMENTS
|
| 9 |
+
|
| 10 |
+
Refactor the specified code without changing its behavior.
|
| 11 |
+
|
| 12 |
+
## Process
|
| 13 |
+
|
| 14 |
+
1. `read_file` the target.
|
| 15 |
+
2. Identify smells:
|
| 16 |
+
- Duplicated code (extract function/component)
|
| 17 |
+
- Long functions (split into smaller ones)
|
| 18 |
+
- Deep nesting (early returns, guard clauses)
|
| 19 |
+
- Unclear names (rename to reveal intent)
|
| 20 |
+
- Magic numbers (extract constants)
|
| 21 |
+
- Mixed concerns (separate responsibilities)
|
| 22 |
+
- Dead code (remove)
|
| 23 |
+
3. Plan changes β show the user before applying if the refactor is significant.
|
| 24 |
+
4. Apply changes with `edit_file` or `write_file`.
|
| 25 |
+
5. Verify nothing broke: run tests with `bash` if available.
|
| 26 |
+
|
| 27 |
+
## Rules
|
| 28 |
+
|
| 29 |
+
- **Behavior-preserving**: the refactored code must produce the same output for the same input.
|
| 30 |
+
- **One refactor at a time**: don't mix 5 changes into one edit.
|
| 31 |
+
- **Match conventions**: follow the surrounding code style.
|
| 32 |
+
- **Update names everywhere**: don't leave stale references.
|
| 33 |
+
- **Test after each significant change**.
|
| 34 |
+
|
| 35 |
+
Don't refactor for refactoring's sake. Only refactor when:
|
| 36 |
+
- The code is hard to read
|
| 37 |
+
- The code is hard to change
|
| 38 |
+
- The code has bugs you're about to fix anyway
|
| 39 |
+
- The user explicitly asked
|
code/commands/builtins/review.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: review
|
| 3 |
+
description: Review the current changes for bugs and quality issues
|
| 4 |
+
argument-hint: Optional file or PR to review
|
| 5 |
+
---
|
| 6 |
+
## Review Task
|
| 7 |
+
|
| 8 |
+
Review the current code changes for quality and correctness.
|
| 9 |
+
|
| 10 |
+
If $ARGUMENTS is provided, review that specific file or changeset.
|
| 11 |
+
Otherwise, review the current `git diff HEAD`.
|
| 12 |
+
|
| 13 |
+
## Process
|
| 14 |
+
|
| 15 |
+
1. Use `bash` to get the diff:
|
| 16 |
+
- If arguments given and it's a file: `git diff HEAD -- $ARGUMENTS`
|
| 17 |
+
- Otherwise: `git diff HEAD`
|
| 18 |
+
|
| 19 |
+
2. Read each changed file fully with `read_file` for context.
|
| 20 |
+
|
| 21 |
+
3. Apply the code-review skill β focus on HIGH SIGNAL issues only:
|
| 22 |
+
- Bugs that will cause incorrect behavior
|
| 23 |
+
- Security issues (hardcoded secrets, injection, path traversal)
|
| 24 |
+
- Syntax/type errors
|
| 25 |
+
- Clear convention violations
|
| 26 |
+
|
| 27 |
+
4. Do NOT flag:
|
| 28 |
+
- Subjective style preferences
|
| 29 |
+
- Potential issues that depend on specific inputs
|
| 30 |
+
- Pedantic nitpicks
|
| 31 |
+
|
| 32 |
+
5. Output your review in this format:
|
| 33 |
+
|
| 34 |
+
```
|
| 35 |
+
## Code Review
|
| 36 |
+
|
| 37 |
+
### Critical
|
| 38 |
+
- `file:line` β Description and fix
|
| 39 |
+
|
| 40 |
+
### High
|
| 41 |
+
- `file:line` β Description and fix
|
| 42 |
+
|
| 43 |
+
### Medium
|
| 44 |
+
- `file:line` β Description and fix
|
| 45 |
+
|
| 46 |
+
### Summary
|
| 47 |
+
Brief overall assessment.
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
If no issues: state "No issues found. Checked for bugs, security, and conventions."
|
code/commands/builtins/skill.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: skill
|
| 3 |
+
description: Load and apply a specific skill
|
| 4 |
+
argument-hint: Skill name
|
| 5 |
+
---
|
| 6 |
+
# Skill Invocation
|
| 7 |
+
|
| 8 |
+
Loading skill: $ARGUMENTS
|
| 9 |
+
|
| 10 |
+
The skill instructions have been loaded into context. Apply them to the current task.
|
| 11 |
+
|
| 12 |
+
If the user provided additional context after the skill name, treat it as the task brief.
|
code/commands/builtins/test.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: test
|
| 3 |
+
description: Generate tests for the current code or a specific file
|
| 4 |
+
argument-hint: File path or test framework
|
| 5 |
+
---
|
| 6 |
+
# Test Generation
|
| 7 |
+
|
| 8 |
+
Target: $ARGUMENTS
|
| 9 |
+
|
| 10 |
+
Generate tests for the specified file or the current changes.
|
| 11 |
+
|
| 12 |
+
## Process
|
| 13 |
+
|
| 14 |
+
1. If a file path is given, `read_file` it. Otherwise use `bash` to get `git diff HEAD` and identify changed files.
|
| 15 |
+
|
| 16 |
+
2. Identify the test framework in use:
|
| 17 |
+
- Python: pytest (preferred), unittest
|
| 18 |
+
- JavaScript: vitest, jest, mocha
|
| 19 |
+
- Check `package.json` or `requirements.txt` for hints
|
| 20 |
+
|
| 21 |
+
3. Read existing tests in the `tests/` or `__tests__/` directory to match style.
|
| 22 |
+
|
| 23 |
+
4. Generate tests that cover:
|
| 24 |
+
- Happy path (typical usage)
|
| 25 |
+
- Edge cases (empty input, boundary values)
|
| 26 |
+
- Error cases (invalid input, network failures)
|
| 27 |
+
- Integration points (if applicable)
|
| 28 |
+
|
| 29 |
+
5. Write the test file to the appropriate location using `write_file`.
|
| 30 |
+
|
| 31 |
+
6. Run the tests with `bash` to verify they pass.
|
| 32 |
+
|
| 33 |
+
## Rules
|
| 34 |
+
|
| 35 |
+
- One test file per source file (e.g. `src/auth.py` β `tests/test_auth.py`)
|
| 36 |
+
- Use descriptive test names: `test_user_can_login_with_valid_credentials`
|
| 37 |
+
- Arrange-Act-Assert pattern
|
| 38 |
+
- Don't test the framework β test your code's behavior
|
| 39 |
+
- Mock external dependencies (network, filesystem, time) when flaky
|
code/config/constants.py
CHANGED
|
@@ -80,22 +80,27 @@ LANGUAGE_MAP: dict[str, list[str]] = {lang: frameworks for lang, frameworks in L
|
|
| 80 |
|
| 81 |
# βββ System Prompt βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 82 |
|
| 83 |
-
SYSTEM_PROMPT = """You are
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
CRITICAL RULES:
|
| 86 |
-
- Do NOT use <think> or <thinking> tags. Do NOT reason aloud.
|
| 87 |
-
-
|
| 88 |
-
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
FILE OUTPUT FORMAT - IMPORTANT:
|
| 98 |
-
When generating multi-file projects, wrap each file in this format:
|
| 99 |
@@FILE: path/to/file.ext@@
|
| 100 |
(file content here)
|
| 101 |
@@FILE: path/to/another/file.ext@@
|
|
@@ -109,30 +114,37 @@ For single-file code, use standard markdown fenced blocks:
|
|
| 109 |
```typescript for TypeScript
|
| 110 |
etc.
|
| 111 |
|
| 112 |
-
|
| 113 |
-
For React, Next.js, Vue.js, Express, NestJS, or any JS/TS framework:
|
| 114 |
-
-
|
| 115 |
-
- Include a package.json with name, version, scripts, and dependencies
|
| 116 |
-
- Include all source files (src/App.jsx, src/index.js, etc.)
|
| 117 |
- For React+Vite: include vite.config.js and index.html
|
| 118 |
- For Next.js: include next.config.js with output: 'standalone'
|
| 119 |
-
- For Express: main entry is index.js with app.listen(7860)
|
| 120 |
- Server ports MUST be 7860 and bind to 0.0.0.0
|
| 121 |
- Do NOT include node_modules or lock files
|
|
|
|
| 122 |
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
|
| 125 |
-
|
| 126 |
- import gradio as gr
|
| 127 |
-
-
|
| 128 |
- Call iface.launch(server_name="0.0.0.0", server_port=7860) at the end
|
| 129 |
-
- Include all
|
| 130 |
-
|
| 131 |
-
For Python, prefer standard library or common packages. Do not use network calls, subprocesses, shell commands, or long-running loops in demo code (except Gradio apps which are server-based).
|
| 132 |
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
-
If
|
|
|
|
|
|
|
| 136 |
"""
|
| 137 |
|
| 138 |
# βββ Example Prompts ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 80 |
|
| 81 |
# βββ System Prompt βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 82 |
|
| 83 |
+
SYSTEM_PROMPT = """You are SoniCoder β an autonomous coding agent that can write full-stack applications, manipulate files, run shell commands, and apply skills.
|
| 84 |
+
|
| 85 |
+
CAPABILITIES:
|
| 86 |
+
- Generate complete, runnable applications in any language/framework
|
| 87 |
+
- Read, write, and edit files in a sandboxed workspace
|
| 88 |
+
- Run shell commands (git, npm, pip, tests) via the bash tool
|
| 89 |
+
- Track multi-step tasks with the todo system
|
| 90 |
+
- Apply specialized skills (frontend-design, feature-dev, code-review, debugging, fullstack-scaffold, commit-workflow)
|
| 91 |
+
- Respond to slash commands: /commit, /review, /feature, /design, /explain, /test, /refactor, /skill, /help
|
| 92 |
|
| 93 |
CRITICAL RULES:
|
| 94 |
+
- Do NOT use <think> or <thinking> tags. Do NOT reason aloud.
|
| 95 |
+
- For multi-step tasks, ALWAYS create a todo list first with `todo_write`.
|
| 96 |
+
- Read files before editing them β `read_file` then `edit_file`.
|
| 97 |
+
- Match the codebase's existing style and conventions.
|
| 98 |
+
- After tool calls, briefly note what you learned before the next step.
|
| 99 |
+
- When done, give a concise summary of what changed.
|
| 100 |
+
|
| 101 |
+
WHEN GENERATING CODE (without tools):
|
| 102 |
+
|
| 103 |
+
FILE OUTPUT FORMAT for multi-file projects:
|
|
|
|
|
|
|
|
|
|
| 104 |
@@FILE: path/to/file.ext@@
|
| 105 |
(file content here)
|
| 106 |
@@FILE: path/to/another/file.ext@@
|
|
|
|
| 114 |
```typescript for TypeScript
|
| 115 |
etc.
|
| 116 |
|
| 117 |
+
FULLSTACK PROJECT RULES:
|
| 118 |
+
- For React, Next.js, Vue.js, Express, NestJS, or any JS/TS framework: ALWAYS use @@FILE: multi-file format
|
| 119 |
+
- Include package.json with name, version, scripts, and dependencies
|
|
|
|
|
|
|
| 120 |
- For React+Vite: include vite.config.js and index.html
|
| 121 |
- For Next.js: include next.config.js with output: 'standalone'
|
| 122 |
+
- For Express: main entry is index.js with app.listen(7860, '0.0.0.0')
|
| 123 |
- Server ports MUST be 7860 and bind to 0.0.0.0
|
| 124 |
- Do NOT include node_modules or lock files
|
| 125 |
+
- For Python web apps: use gradio/flask/fastapi/streamlit as appropriate
|
| 126 |
|
| 127 |
+
WEB APP RULES (HTML/CSS/JS):
|
| 128 |
+
- Return a single self-contained HTML document with all CSS and JavaScript inline
|
| 129 |
+
- Make the page fully responsive: html/body at margin:0 and 100% width/height
|
| 130 |
+
- Use flexbox/grid layouts; size any canvas to its container
|
| 131 |
+
- Respect prefers-reduced-motion
|
| 132 |
+
- Include visible keyboard focus states
|
| 133 |
|
| 134 |
+
GRADIO APP RULES:
|
| 135 |
- import gradio as gr
|
| 136 |
+
- Use gr.Interface() or gr.Blocks()
|
| 137 |
- Call iface.launch(server_name="0.0.0.0", server_port=7860) at the end
|
| 138 |
+
- Include all processing logic inline
|
|
|
|
|
|
|
| 139 |
|
| 140 |
+
PYTHON RULES:
|
| 141 |
+
- Prefer standard library or common packages
|
| 142 |
+
- Do not use network calls, subprocesses, or long-running loops in demo code
|
| 143 |
+
- Add proper error handling and comments
|
| 144 |
|
| 145 |
+
If web search results are provided, use them to inform your code generation.
|
| 146 |
+
If the user provides an image, analyze it and generate code based on what you see.
|
| 147 |
+
If a hook warns you, acknowledge it and adjust your approach.
|
| 148 |
"""
|
| 149 |
|
| 150 |
# βββ Example Prompts ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
code/hooks/__init__.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Hooks system β pre/post tool execution rules.
|
| 2 |
+
|
| 3 |
+
Inspired by Claude Code's hookify plugin. Rules are markdown files
|
| 4 |
+
with YAML frontmatter that define:
|
| 5 |
+
- event: bash | file | prompt | stop | all
|
| 6 |
+
- pattern: regex to match
|
| 7 |
+
- action: warn | block
|
| 8 |
+
- message: shown to the user/agent when triggered
|
| 9 |
+
|
| 10 |
+
Rules are discovered from:
|
| 11 |
+
- code/hooks/builtins/ (built-in rules)
|
| 12 |
+
- workspace/.sonicoder/hooks/ (user rules)
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import logging
|
| 18 |
+
import os
|
| 19 |
+
import re
|
| 20 |
+
from typing import Any
|
| 21 |
+
|
| 22 |
+
from code.skills import _parse_frontmatter
|
| 23 |
+
|
| 24 |
+
logger = logging.getLogger(__name__)
|
| 25 |
+
|
| 26 |
+
_BUILTIN_HOOKS_DIR = os.path.join(os.path.dirname(__file__), "builtins")
|
| 27 |
+
_USER_HOOKS_DIRNAME = ".sonicoder/hooks"
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _hook_dirs() -> list[str]:
|
| 31 |
+
dirs = [_BUILTIN_HOOKS_DIR]
|
| 32 |
+
try:
|
| 33 |
+
from code.tools.fs import get_workspace_root
|
| 34 |
+
user_dir = os.path.join(get_workspace_root(), _USER_HOOKS_DIRNAME)
|
| 35 |
+
if os.path.isdir(user_dir):
|
| 36 |
+
dirs.append(user_dir)
|
| 37 |
+
except Exception:
|
| 38 |
+
pass
|
| 39 |
+
return dirs
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def _load_hook(filepath: str) -> dict[str, Any] | None:
|
| 43 |
+
try:
|
| 44 |
+
with open(filepath, "r", encoding="utf-8") as f:
|
| 45 |
+
content = f.read()
|
| 46 |
+
except Exception:
|
| 47 |
+
return None
|
| 48 |
+
|
| 49 |
+
meta, body = _parse_frontmatter(content)
|
| 50 |
+
|
| 51 |
+
name = meta.get("name", os.path.splitext(os.path.basename(filepath))[0])
|
| 52 |
+
enabled = meta.get("enabled", "true").lower() == "true"
|
| 53 |
+
event = meta.get("event", "all").lower()
|
| 54 |
+
action = meta.get("action", "warn").lower()
|
| 55 |
+
pattern = meta.get("pattern", "")
|
| 56 |
+
conditions_raw = meta.get("conditions", "")
|
| 57 |
+
|
| 58 |
+
# Parse conditions (simplified β actual hookify uses YAML lists)
|
| 59 |
+
conditions: list[dict[str, str]] = []
|
| 60 |
+
if conditions_raw:
|
| 61 |
+
# Very rough parse: each "- field: x\n operator: y\n pattern: z"
|
| 62 |
+
for block in re.split(r"(?=\n-\s+field:)", "\n" + conditions_raw):
|
| 63 |
+
field_m = re.search(r"field:\s*(\S+)", block)
|
| 64 |
+
op_m = re.search(r"operator:\s*(\S+)", block)
|
| 65 |
+
pat_m = re.search(r"pattern:\s*(.+?)(?=\n\s*$|\n\s*-|\Z)", block, re.DOTALL)
|
| 66 |
+
if field_m and op_m and pat_m:
|
| 67 |
+
conditions.append({
|
| 68 |
+
"field": field_m.group(1),
|
| 69 |
+
"operator": op_m.group(1),
|
| 70 |
+
"pattern": pat_m.group(1).strip(),
|
| 71 |
+
})
|
| 72 |
+
|
| 73 |
+
return {
|
| 74 |
+
"name": name,
|
| 75 |
+
"enabled": enabled,
|
| 76 |
+
"event": event,
|
| 77 |
+
"action": action,
|
| 78 |
+
"pattern": pattern,
|
| 79 |
+
"conditions": conditions,
|
| 80 |
+
"message": body.strip(),
|
| 81 |
+
"path": filepath,
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def list_hooks() -> list[dict[str, Any]]:
|
| 86 |
+
"""List all hooks (metadata only)."""
|
| 87 |
+
hooks: list[dict[str, Any]] = []
|
| 88 |
+
seen: set[str] = set()
|
| 89 |
+
|
| 90 |
+
for hooks_dir in _hook_dirs():
|
| 91 |
+
if not os.path.isdir(hooks_dir):
|
| 92 |
+
continue
|
| 93 |
+
for entry in sorted(os.listdir(hooks_dir)):
|
| 94 |
+
if not entry.endswith(".md"):
|
| 95 |
+
continue
|
| 96 |
+
filepath = os.path.join(hooks_dir, entry)
|
| 97 |
+
hook = _load_hook(filepath)
|
| 98 |
+
if hook and hook["name"] not in seen:
|
| 99 |
+
seen.add(hook["name"])
|
| 100 |
+
hooks.append({
|
| 101 |
+
"name": hook["name"],
|
| 102 |
+
"enabled": hook["enabled"],
|
| 103 |
+
"event": hook["event"],
|
| 104 |
+
"action": hook["action"],
|
| 105 |
+
"pattern": hook["pattern"],
|
| 106 |
+
})
|
| 107 |
+
return hooks
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def _match_condition(condition: dict[str, str], context: dict[str, Any]) -> bool:
|
| 111 |
+
"""Check if a single condition matches."""
|
| 112 |
+
field = condition.get("field", "")
|
| 113 |
+
operator = condition.get("operator", "regex_match")
|
| 114 |
+
pattern = condition.get("pattern", "")
|
| 115 |
+
|
| 116 |
+
value = str(context.get(field, ""))
|
| 117 |
+
|
| 118 |
+
if operator == "regex_match":
|
| 119 |
+
return bool(re.search(pattern, value))
|
| 120 |
+
elif operator == "contains":
|
| 121 |
+
return pattern in value
|
| 122 |
+
elif operator == "equals":
|
| 123 |
+
return value == pattern
|
| 124 |
+
elif operator == "not_contains":
|
| 125 |
+
return pattern not in value
|
| 126 |
+
elif operator == "starts_with":
|
| 127 |
+
return value.startswith(pattern)
|
| 128 |
+
elif operator == "ends_with":
|
| 129 |
+
return value.endswith(pattern)
|
| 130 |
+
return False
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def _match_hook(hook: dict[str, Any], event: str, context: dict[str, Any]) -> bool:
|
| 134 |
+
"""Check if a hook matches the given event and context."""
|
| 135 |
+
if not hook["enabled"]:
|
| 136 |
+
return False
|
| 137 |
+
if hook["event"] != "all" and hook["event"] != event:
|
| 138 |
+
return False
|
| 139 |
+
|
| 140 |
+
# Simple pattern match (single pattern)
|
| 141 |
+
if hook["pattern"]:
|
| 142 |
+
# For bash event, match against command
|
| 143 |
+
target = str(context.get("command", context.get("file_path", context.get("user_prompt", ""))))
|
| 144 |
+
if not re.search(hook["pattern"], target):
|
| 145 |
+
return False
|
| 146 |
+
|
| 147 |
+
# Multi-condition match (all conditions must match)
|
| 148 |
+
if hook["conditions"]:
|
| 149 |
+
for cond in hook["conditions"]:
|
| 150 |
+
if not _match_condition(cond, context):
|
| 151 |
+
return False
|
| 152 |
+
|
| 153 |
+
return True
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def check_hook(event: str, context: dict[str, Any]) -> dict[str, Any]:
|
| 157 |
+
"""Check all hooks for an event.
|
| 158 |
+
|
| 159 |
+
Args:
|
| 160 |
+
event: One of 'bash', 'file', 'prompt', 'stop', 'all'
|
| 161 |
+
context: Dict with relevant fields (command, file_path, new_text, user_prompt, etc.)
|
| 162 |
+
|
| 163 |
+
Returns:
|
| 164 |
+
dict with:
|
| 165 |
+
- blocked: bool β whether the action should be blocked
|
| 166 |
+
- warnings: list of warning messages to show
|
| 167 |
+
- matched_hooks: list of hook names that matched
|
| 168 |
+
"""
|
| 169 |
+
warnings: list[str] = []
|
| 170 |
+
matched: list[str] = []
|
| 171 |
+
blocked = False
|
| 172 |
+
|
| 173 |
+
for hooks_dir in _hook_dirs():
|
| 174 |
+
if not os.path.isdir(hooks_dir):
|
| 175 |
+
continue
|
| 176 |
+
for entry in sorted(os.listdir(hooks_dir)):
|
| 177 |
+
if not entry.endswith(".md"):
|
| 178 |
+
continue
|
| 179 |
+
filepath = os.path.join(hooks_dir, entry)
|
| 180 |
+
hook = _load_hook(filepath)
|
| 181 |
+
if not hook:
|
| 182 |
+
continue
|
| 183 |
+
if _match_hook(hook, event, context):
|
| 184 |
+
matched.append(hook["name"])
|
| 185 |
+
if hook["action"] == "block":
|
| 186 |
+
blocked = True
|
| 187 |
+
warnings.append(f"π BLOCKED by rule '{hook['name']}':\n\n{hook['message']}")
|
| 188 |
+
else:
|
| 189 |
+
warnings.append(f"β οΈ Warning from rule '{hook['name']}':\n\n{hook['message']}")
|
| 190 |
+
|
| 191 |
+
return {
|
| 192 |
+
"blocked": blocked,
|
| 193 |
+
"warnings": warnings,
|
| 194 |
+
"matched_hooks": matched,
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def create_hook(
|
| 199 |
+
name: str,
|
| 200 |
+
event: str,
|
| 201 |
+
pattern: str,
|
| 202 |
+
action: str = "warn",
|
| 203 |
+
message: str = "",
|
| 204 |
+
enabled: bool = True,
|
| 205 |
+
) -> dict[str, Any]:
|
| 206 |
+
"""Create a new user hook (saved to workspace/.sonicoder/hooks/)."""
|
| 207 |
+
try:
|
| 208 |
+
from code.tools.fs import get_workspace_root
|
| 209 |
+
hooks_dir = os.path.join(get_workspace_root(), _USER_HOOKS_DIRNAME)
|
| 210 |
+
os.makedirs(hooks_dir, exist_ok=True)
|
| 211 |
+
|
| 212 |
+
filepath = os.path.join(hooks_dir, f"{name}.local.md")
|
| 213 |
+
content = f"""---
|
| 214 |
+
name: {name}
|
| 215 |
+
enabled: {str(enabled).lower()}
|
| 216 |
+
event: {event}
|
| 217 |
+
pattern: {pattern}
|
| 218 |
+
action: {action}
|
| 219 |
+
---
|
| 220 |
+
|
| 221 |
+
{message}
|
| 222 |
+
"""
|
| 223 |
+
with open(filepath, "w", encoding="utf-8") as f:
|
| 224 |
+
f.write(content)
|
| 225 |
+
|
| 226 |
+
return {"success": True, "name": name, "path": filepath}
|
| 227 |
+
except Exception as exc:
|
| 228 |
+
return {"success": False, "error": str(exc)}
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
def delete_hook(name: str) -> dict[str, Any]:
|
| 232 |
+
"""Delete a user hook by name."""
|
| 233 |
+
try:
|
| 234 |
+
from code.tools.fs import get_workspace_root
|
| 235 |
+
hooks_dir = os.path.join(get_workspace_root(), _USER_HOOKS_DIRNAME)
|
| 236 |
+
filepath = os.path.join(hooks_dir, f"{name}.local.md")
|
| 237 |
+
if os.path.exists(filepath):
|
| 238 |
+
os.remove(filepath)
|
| 239 |
+
return {"success": True, "name": name}
|
| 240 |
+
return {"success": False, "error": f"Hook not found: {name}"}
|
| 241 |
+
except Exception as exc:
|
| 242 |
+
return {"success": False, "error": str(exc)}
|
code/hooks/builtins/block-dangerous-rm.local.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: block-dangerous-rm
|
| 3 |
+
enabled: true
|
| 4 |
+
event: bash
|
| 5 |
+
pattern: rm\s+-rf\s+(/|~|\$HOME|\.\.)
|
| 6 |
+
action: block
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
π **Dangerous rm command detected!**
|
| 10 |
+
|
| 11 |
+
This command could delete important files. The operation has been blocked.
|
| 12 |
+
|
| 13 |
+
If this is intentional, please:
|
| 14 |
+
- Verify the path is correct
|
| 15 |
+
- Use a more specific path
|
| 16 |
+
- Make sure you have backups
|
code/hooks/builtins/warn-debug-code.local.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: warn-debug-code
|
| 3 |
+
enabled: true
|
| 4 |
+
event: file
|
| 5 |
+
pattern: (console\.log\(|debugger;|print\(|alert\()
|
| 6 |
+
action: warn
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
π **Debug code detected**
|
| 10 |
+
|
| 11 |
+
Remember to remove debugging statements (console.log, debugger, print, alert) before committing.
|
code/hooks/builtins/warn-eval-exec.local.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: warn-eval-exec
|
| 3 |
+
enabled: true
|
| 4 |
+
event: bash
|
| 5 |
+
pattern: (^|\s)(eval|exec)\s*\(
|
| 6 |
+
action: warn
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
β οΈ **eval()/exec() detected**
|
| 10 |
+
|
| 11 |
+
Using eval() or exec() on untrusted input is a code injection risk.
|
| 12 |
+
Consider safer alternatives:
|
| 13 |
+
- ast.literal_eval() for Python literals
|
| 14 |
+
- json.loads() for JSON
|
| 15 |
+
- Function constructors with explicit scope for JS
|
code/hooks/builtins/warn-secrets-in-code.local.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: warn-secrets-in-code
|
| 3 |
+
enabled: true
|
| 4 |
+
event: file
|
| 5 |
+
pattern: (API_KEY|SECRET|TOKEN|PASSWORD)\s*=\s*["'][^"']+["']
|
| 6 |
+
action: warn
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
π **Possible hardcoded secret detected**
|
| 10 |
+
|
| 11 |
+
Hardcoded credentials are a security risk. Use environment variables instead:
|
| 12 |
+
|
| 13 |
+
```python
|
| 14 |
+
import os
|
| 15 |
+
api_key = os.environ.get("API_KEY")
|
| 16 |
+
```
|
| 17 |
+
|
| 18 |
+
```javascript
|
| 19 |
+
const apiKey = process.env.API_KEY;
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
Make sure to add the real secret to `.env` (and `.env` to `.gitignore`).
|
code/server/routes.py
CHANGED
|
@@ -11,6 +11,16 @@ Defines all HTTP and API endpoints:
|
|
| 11 |
- API switch_model β switch between loaded models
|
| 12 |
- API upload_image β upload image for VLM inference
|
| 13 |
- API hf_auth β get HF OAuth profile & organizations
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
"""
|
| 15 |
|
| 16 |
from __future__ import annotations
|
|
@@ -24,7 +34,28 @@ from pathlib import Path
|
|
| 24 |
from typing import Any
|
| 25 |
|
| 26 |
from fastapi.responses import HTMLResponse, FileResponse
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
from code.config.constants import (
|
| 30 |
APP_TITLE,
|
|
@@ -82,6 +113,25 @@ async def homepage():
|
|
| 82 |
with open(html_path, "r", encoding="utf-8") as f:
|
| 83 |
content = f.read()
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
config = json.dumps({
|
| 86 |
"app_title": APP_TITLE,
|
| 87 |
"model_id": MODEL_CONFIGS[DEFAULT_MODEL_KEY]["id"],
|
|
@@ -93,6 +143,9 @@ async def homepage():
|
|
| 93 |
for label, prompt, lang, fw in EXAMPLE_PROMPTS
|
| 94 |
],
|
| 95 |
"default_model": "minicpm5-1b",
|
|
|
|
|
|
|
|
|
|
| 96 |
})
|
| 97 |
content = content.replace("__RUNTIME_CONFIG__", config)
|
| 98 |
return content
|
|
@@ -630,3 +683,235 @@ def handle_push_hf(
|
|
| 630 |
def get_app() -> Server:
|
| 631 |
"""Return the configured Gradio Server app instance."""
|
| 632 |
return app
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
- API switch_model β switch between loaded models
|
| 12 |
- API upload_image β upload image for VLM inference
|
| 13 |
- API hf_auth β get HF OAuth profile & organizations
|
| 14 |
+
- API agent_run β Claude Code-style agent loop with tools
|
| 15 |
+
- API list_skills β list available skills
|
| 16 |
+
- API list_commandsβ list available slash commands
|
| 17 |
+
- API list_hooks β list configured hooks
|
| 18 |
+
- API workspace_treeβ list workspace files
|
| 19 |
+
- API workspace_readβ read a workspace file
|
| 20 |
+
- API workspace_writeβ write a workspace file
|
| 21 |
+
- API workspace_bashβ run a bash command in workspace
|
| 22 |
+
- API todo_read β read current todo list
|
| 23 |
+
- API todo_write β update todo list
|
| 24 |
"""
|
| 25 |
|
| 26 |
from __future__ import annotations
|
|
|
|
| 34 |
from typing import Any
|
| 35 |
|
| 36 |
from fastapi.responses import HTMLResponse, FileResponse
|
| 37 |
+
try:
|
| 38 |
+
from gradio import Server
|
| 39 |
+
except ImportError:
|
| 40 |
+
# Fallback for older/newer Gradio versions where Server may not be exposed
|
| 41 |
+
# at the top level. We provide a minimal shim so the module can still be
|
| 42 |
+
# imported for testing purposes.
|
| 43 |
+
class Server: # type: ignore
|
| 44 |
+
"""Minimal shim for Gradio Server when not available."""
|
| 45 |
+
def __init__(self, *args, **kwargs):
|
| 46 |
+
from fastapi import FastAPI
|
| 47 |
+
self._fastapi = FastAPI()
|
| 48 |
+
|
| 49 |
+
def get(self, path: str, **kwargs):
|
| 50 |
+
return self._fastapi.get(path, **kwargs)
|
| 51 |
+
|
| 52 |
+
def api(self, name: str = None, concurrency_limit: int = 1):
|
| 53 |
+
def decorator(fn):
|
| 54 |
+
# Store as attribute so it can be inspected
|
| 55 |
+
fn._api_name = name
|
| 56 |
+
fn._concurrency_limit = concurrency_limit
|
| 57 |
+
return fn
|
| 58 |
+
return decorator
|
| 59 |
|
| 60 |
from code.config.constants import (
|
| 61 |
APP_TITLE,
|
|
|
|
| 113 |
with open(html_path, "r", encoding="utf-8") as f:
|
| 114 |
content = f.read()
|
| 115 |
|
| 116 |
+
# Load skills, commands, hooks for the frontend
|
| 117 |
+
try:
|
| 118 |
+
from code.skills import list_skills
|
| 119 |
+
skills_list = list_skills()
|
| 120 |
+
except Exception:
|
| 121 |
+
skills_list = []
|
| 122 |
+
|
| 123 |
+
try:
|
| 124 |
+
from code.commands import list_commands
|
| 125 |
+
commands_list = list_commands()
|
| 126 |
+
except Exception:
|
| 127 |
+
commands_list = []
|
| 128 |
+
|
| 129 |
+
try:
|
| 130 |
+
from code.hooks import list_hooks
|
| 131 |
+
hooks_list = list_hooks()
|
| 132 |
+
except Exception:
|
| 133 |
+
hooks_list = []
|
| 134 |
+
|
| 135 |
config = json.dumps({
|
| 136 |
"app_title": APP_TITLE,
|
| 137 |
"model_id": MODEL_CONFIGS[DEFAULT_MODEL_KEY]["id"],
|
|
|
|
| 143 |
for label, prompt, lang, fw in EXAMPLE_PROMPTS
|
| 144 |
],
|
| 145 |
"default_model": "minicpm5-1b",
|
| 146 |
+
"skills": skills_list,
|
| 147 |
+
"commands": commands_list,
|
| 148 |
+
"hooks": hooks_list,
|
| 149 |
})
|
| 150 |
content = content.replace("__RUNTIME_CONFIG__", config)
|
| 151 |
return content
|
|
|
|
| 683 |
def get_app() -> Server:
|
| 684 |
"""Return the configured Gradio Server app instance."""
|
| 685 |
return app
|
| 686 |
+
|
| 687 |
+
|
| 688 |
+
# βββ Agent / Skills / Commands / Hooks / Workspace Endpoints ββββββββββ
|
| 689 |
+
|
| 690 |
+
|
| 691 |
+
@app.api(name="agent_run", concurrency_limit=2)
|
| 692 |
+
def handle_agent_run(
|
| 693 |
+
prompt: str,
|
| 694 |
+
target_language: str = "",
|
| 695 |
+
target_framework: str = "",
|
| 696 |
+
history_json: str = "[]",
|
| 697 |
+
skills_json: str = "[]",
|
| 698 |
+
search_enabled: str = "false",
|
| 699 |
+
image_url: str = "",
|
| 700 |
+
) -> str:
|
| 701 |
+
"""Run the Claude Code-style agent loop with tools.
|
| 702 |
+
|
| 703 |
+
Yields JSON events: status, tool_call, tool_result, streaming, complete, error.
|
| 704 |
+
"""
|
| 705 |
+
from code.agent import run_agent
|
| 706 |
+
|
| 707 |
+
history = json.loads(history_json) if history_json else []
|
| 708 |
+
skills = json.loads(skills_json) if skills_json else []
|
| 709 |
+
|
| 710 |
+
prompt = (prompt or "").strip()
|
| 711 |
+
if not prompt:
|
| 712 |
+
yield json.dumps({
|
| 713 |
+
"type": "error",
|
| 714 |
+
"message": "Empty prompt",
|
| 715 |
+
})
|
| 716 |
+
return
|
| 717 |
+
|
| 718 |
+
# Optional web search
|
| 719 |
+
search_context = ""
|
| 720 |
+
if search_enabled.lower() == "true":
|
| 721 |
+
try:
|
| 722 |
+
search_results = web_search_google(prompt, num_results=6)
|
| 723 |
+
if search_results:
|
| 724 |
+
search_context = format_search_results(search_results)
|
| 725 |
+
yield json.dumps({
|
| 726 |
+
"type": "search_results",
|
| 727 |
+
"results": search_results,
|
| 728 |
+
"status_text": f"Found {len(search_results)} results, running agent...",
|
| 729 |
+
})
|
| 730 |
+
except Exception as exc:
|
| 731 |
+
logger.warning("Web search failed: %s", exc)
|
| 732 |
+
|
| 733 |
+
try:
|
| 734 |
+
for event in run_agent(
|
| 735 |
+
user_input=prompt,
|
| 736 |
+
history=history,
|
| 737 |
+
target_language=target_language,
|
| 738 |
+
target_framework=target_framework,
|
| 739 |
+
skills=skills,
|
| 740 |
+
search_context=search_context,
|
| 741 |
+
image_url=image_url.strip() or None,
|
| 742 |
+
):
|
| 743 |
+
yield json.dumps(event, default=str)
|
| 744 |
+
except Exception as exc:
|
| 745 |
+
logger.exception("Agent run failed")
|
| 746 |
+
yield json.dumps({
|
| 747 |
+
"type": "error",
|
| 748 |
+
"message": str(exc),
|
| 749 |
+
})
|
| 750 |
+
|
| 751 |
+
|
| 752 |
+
@app.api(name="list_skills", concurrency_limit=4)
|
| 753 |
+
def handle_list_skills() -> str:
|
| 754 |
+
"""List all available skills."""
|
| 755 |
+
from code.skills import list_skills
|
| 756 |
+
skills = list_skills()
|
| 757 |
+
yield json.dumps({"success": True, "skills": skills})
|
| 758 |
+
|
| 759 |
+
|
| 760 |
+
@app.api(name="list_commands", concurrency_limit=4)
|
| 761 |
+
def handle_list_commands() -> str:
|
| 762 |
+
"""List all available slash commands."""
|
| 763 |
+
from code.commands import list_commands
|
| 764 |
+
commands = list_commands()
|
| 765 |
+
yield json.dumps({"success": True, "commands": commands})
|
| 766 |
+
|
| 767 |
+
|
| 768 |
+
@app.api(name="list_hooks", concurrency_limit=4)
|
| 769 |
+
def handle_list_hooks() -> str:
|
| 770 |
+
"""List all configured hooks."""
|
| 771 |
+
from code.hooks import list_hooks
|
| 772 |
+
hooks = list_hooks()
|
| 773 |
+
yield json.dumps({"success": True, "hooks": hooks})
|
| 774 |
+
|
| 775 |
+
|
| 776 |
+
@app.api(name="workspace_tree", concurrency_limit=4)
|
| 777 |
+
def handle_workspace_tree() -> str:
|
| 778 |
+
"""Return the workspace file tree."""
|
| 779 |
+
from code.tools.fs import list_workspace_tree
|
| 780 |
+
result = list_workspace_tree()
|
| 781 |
+
yield json.dumps(result, default=str)
|
| 782 |
+
|
| 783 |
+
|
| 784 |
+
@app.api(name="workspace_read", concurrency_limit=4)
|
| 785 |
+
def handle_workspace_read(path: str, offset: int = 0, limit: int = 0) -> str:
|
| 786 |
+
"""Read a file from the workspace."""
|
| 787 |
+
from code.tools.fs import read_file
|
| 788 |
+
args = {"path": path}
|
| 789 |
+
if offset:
|
| 790 |
+
args["offset"] = offset
|
| 791 |
+
if limit:
|
| 792 |
+
args["limit"] = limit
|
| 793 |
+
result = read_file(**args)
|
| 794 |
+
yield json.dumps(result, default=str)
|
| 795 |
+
|
| 796 |
+
|
| 797 |
+
@app.api(name="workspace_write", concurrency_limit=1)
|
| 798 |
+
def handle_workspace_write(path: str, content: str) -> str:
|
| 799 |
+
"""Write a file to the workspace."""
|
| 800 |
+
from code.tools.fs import write_file
|
| 801 |
+
result = write_file(path=path, content=content)
|
| 802 |
+
yield json.dumps(result, default=str)
|
| 803 |
+
|
| 804 |
+
|
| 805 |
+
@app.api(name="workspace_bash", concurrency_limit=1)
|
| 806 |
+
def handle_workspace_bash(command: str, timeout: int = 30) -> str:
|
| 807 |
+
"""Run a bash command in the workspace."""
|
| 808 |
+
from code.tools.bash import run_bash
|
| 809 |
+
result = run_bash(command=command, timeout=timeout)
|
| 810 |
+
yield json.dumps(result, default=str)
|
| 811 |
+
|
| 812 |
+
|
| 813 |
+
@app.api(name="workspace_edit", concurrency_limit=1)
|
| 814 |
+
def handle_workspace_edit(
|
| 815 |
+
path: str,
|
| 816 |
+
old_str: str,
|
| 817 |
+
new_str: str,
|
| 818 |
+
replace_all: str = "false",
|
| 819 |
+
) -> str:
|
| 820 |
+
"""Edit a file in the workspace."""
|
| 821 |
+
from code.tools.fs import edit_file
|
| 822 |
+
result = edit_file(
|
| 823 |
+
path=path,
|
| 824 |
+
old_str=old_str,
|
| 825 |
+
new_str=new_str,
|
| 826 |
+
replace_all=replace_all.lower() == "true",
|
| 827 |
+
)
|
| 828 |
+
yield json.dumps(result, default=str)
|
| 829 |
+
|
| 830 |
+
|
| 831 |
+
@app.api(name="workspace_glob", concurrency_limit=4)
|
| 832 |
+
def handle_workspace_glob(pattern: str, path: str = ".") -> str:
|
| 833 |
+
"""Glob files in the workspace."""
|
| 834 |
+
from code.tools.fs import glob_paths
|
| 835 |
+
result = glob_paths(pattern=pattern, path=path)
|
| 836 |
+
yield json.dumps(result, default=str)
|
| 837 |
+
|
| 838 |
+
|
| 839 |
+
@app.api(name="workspace_grep", concurrency_limit=4)
|
| 840 |
+
def handle_workspace_grep(
|
| 841 |
+
pattern: str,
|
| 842 |
+
path: str = ".",
|
| 843 |
+
include: str = "",
|
| 844 |
+
ignore_case: str = "false",
|
| 845 |
+
) -> str:
|
| 846 |
+
"""Grep file contents in the workspace."""
|
| 847 |
+
from code.tools.fs import grep_search
|
| 848 |
+
result = grep_search(
|
| 849 |
+
pattern=pattern,
|
| 850 |
+
path=path,
|
| 851 |
+
include=include or None,
|
| 852 |
+
ignore_case=ignore_case.lower() == "true",
|
| 853 |
+
)
|
| 854 |
+
yield json.dumps(result, default=str)
|
| 855 |
+
|
| 856 |
+
|
| 857 |
+
@app.api(name="todo_read", concurrency_limit=4)
|
| 858 |
+
def handle_todo_read(session_id: str = "default") -> str:
|
| 859 |
+
"""Read the current todo list."""
|
| 860 |
+
from code.tools.todos import todo_read
|
| 861 |
+
result = todo_read(session_id=session_id)
|
| 862 |
+
yield json.dumps(result, default=str)
|
| 863 |
+
|
| 864 |
+
|
| 865 |
+
@app.api(name="todo_write", concurrency_limit=1)
|
| 866 |
+
def handle_todo_write(todos_json: str, session_id: str = "default") -> str:
|
| 867 |
+
"""Replace the todo list."""
|
| 868 |
+
from code.tools.todos import todo_write
|
| 869 |
+
todos = json.loads(todos_json) if todos_json else []
|
| 870 |
+
result = todo_write(todos=todos, session_id=session_id)
|
| 871 |
+
yield json.dumps(result, default=str)
|
| 872 |
+
|
| 873 |
+
|
| 874 |
+
@app.api(name="workspace_snapshot", concurrency_limit=2)
|
| 875 |
+
def handle_workspace_snapshot() -> str:
|
| 876 |
+
"""Return all workspace files for ZIP/deploy."""
|
| 877 |
+
from code.tools.fs import snapshot_workspace
|
| 878 |
+
files = snapshot_workspace()
|
| 879 |
+
yield json.dumps({"success": True, "files": files, "count": len(files)})
|
| 880 |
+
|
| 881 |
+
|
| 882 |
+
@app.api(name="workspace_reset", concurrency_limit=1)
|
| 883 |
+
def handle_workspace_reset() -> str:
|
| 884 |
+
"""Clear the workspace."""
|
| 885 |
+
from code.tools.fs import reset_workspace
|
| 886 |
+
result = reset_workspace()
|
| 887 |
+
yield json.dumps(result, default=str)
|
| 888 |
+
|
| 889 |
+
|
| 890 |
+
@app.api(name="create_hook", concurrency_limit=1)
|
| 891 |
+
def handle_create_hook(
|
| 892 |
+
name: str,
|
| 893 |
+
event: str,
|
| 894 |
+
pattern: str,
|
| 895 |
+
action: str = "warn",
|
| 896 |
+
message: str = "",
|
| 897 |
+
enabled: str = "true",
|
| 898 |
+
) -> str:
|
| 899 |
+
"""Create a new user hook."""
|
| 900 |
+
from code.hooks import create_hook
|
| 901 |
+
result = create_hook(
|
| 902 |
+
name=name,
|
| 903 |
+
event=event,
|
| 904 |
+
pattern=pattern,
|
| 905 |
+
action=action,
|
| 906 |
+
message=message,
|
| 907 |
+
enabled=enabled.lower() == "true",
|
| 908 |
+
)
|
| 909 |
+
yield json.dumps(result, default=str)
|
| 910 |
+
|
| 911 |
+
|
| 912 |
+
@app.api(name="delete_hook", concurrency_limit=1)
|
| 913 |
+
def handle_delete_hook(name: str) -> str:
|
| 914 |
+
"""Delete a user hook by name."""
|
| 915 |
+
from code.hooks import delete_hook
|
| 916 |
+
result = delete_hook(name)
|
| 917 |
+
yield json.dumps(result, default=str)
|
code/skills/__init__.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Skills system β load markdown skill files at runtime.
|
| 2 |
+
|
| 3 |
+
Inspired by Claude Code's Skill system. Each skill is a directory with:
|
| 4 |
+
- SKILL.md: the skill instructions (markdown with YAML frontmatter)
|
| 5 |
+
- references/ (optional): supporting docs
|
| 6 |
+
- scripts/ (optional): helper scripts
|
| 7 |
+
|
| 8 |
+
Skills are discovered under code/skills/builtins/ and can also be loaded
|
| 9 |
+
from the workspace's .sonicoder/skills/ directory.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import logging
|
| 15 |
+
import os
|
| 16 |
+
import re
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
from typing import Any
|
| 19 |
+
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
# βββ Skill discovery roots ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 23 |
+
|
| 24 |
+
_BUILTIN_SKILLS_DIR = os.path.join(os.path.dirname(__file__), "builtins")
|
| 25 |
+
_USER_SKILLS_DIRNAME = ".sonicoder/skills" # relative to workspace root
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _skill_dirs() -> list[str]:
|
| 29 |
+
"""Return all directories to search for skills."""
|
| 30 |
+
dirs = [_BUILTIN_SKILLS_DIR]
|
| 31 |
+
# Add user skills dir from workspace
|
| 32 |
+
try:
|
| 33 |
+
from code.tools.fs import get_workspace_root
|
| 34 |
+
user_dir = os.path.join(get_workspace_root(), _USER_SKILLS_DIRNAME)
|
| 35 |
+
if os.path.isdir(user_dir):
|
| 36 |
+
dirs.append(user_dir)
|
| 37 |
+
except Exception:
|
| 38 |
+
pass
|
| 39 |
+
return dirs
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
# βββ YAML frontmatter parsing βββββββββββββββββββββββββββββββββββββββββββ
|
| 43 |
+
|
| 44 |
+
_FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n(.*)$", re.DOTALL)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def _parse_frontmatter(content: str) -> tuple[dict[str, str], str]:
|
| 48 |
+
"""Parse YAML frontmatter from markdown. Returns (metadata, body)."""
|
| 49 |
+
match = _FRONTMATTER_RE.match(content)
|
| 50 |
+
if not match:
|
| 51 |
+
return {}, content
|
| 52 |
+
|
| 53 |
+
raw_yaml = match.group(1)
|
| 54 |
+
body = match.group(2)
|
| 55 |
+
|
| 56 |
+
# Very simple YAML parser (key: value pairs only)
|
| 57 |
+
meta: dict[str, str] = {}
|
| 58 |
+
for line in raw_yaml.splitlines():
|
| 59 |
+
line = line.strip()
|
| 60 |
+
if not line or line.startswith("#"):
|
| 61 |
+
continue
|
| 62 |
+
if ":" in line:
|
| 63 |
+
key, _, value = line.partition(":")
|
| 64 |
+
meta[key.strip()] = value.strip().strip("\"'")
|
| 65 |
+
return meta, body
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
# βββ Skill loading ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 69 |
+
|
| 70 |
+
def _load_skill(skill_dir: str) -> dict[str, Any] | None:
|
| 71 |
+
"""Load a single skill from a directory."""
|
| 72 |
+
skill_md = os.path.join(skill_dir, "SKILL.md")
|
| 73 |
+
if not os.path.isfile(skill_md):
|
| 74 |
+
return None
|
| 75 |
+
|
| 76 |
+
try:
|
| 77 |
+
with open(skill_md, "r", encoding="utf-8") as f:
|
| 78 |
+
content = f.read()
|
| 79 |
+
except Exception as exc:
|
| 80 |
+
logger.warning("Failed to read %s: %s", skill_md, exc)
|
| 81 |
+
return None
|
| 82 |
+
|
| 83 |
+
meta, body = _parse_frontmatter(content)
|
| 84 |
+
|
| 85 |
+
# Load any reference files
|
| 86 |
+
references: dict[str, str] = {}
|
| 87 |
+
refs_dir = os.path.join(skill_dir, "references")
|
| 88 |
+
if os.path.isdir(refs_dir):
|
| 89 |
+
for fname in os.listdir(refs_dir):
|
| 90 |
+
if fname.endswith((".md", ".txt")):
|
| 91 |
+
try:
|
| 92 |
+
with open(os.path.join(refs_dir, fname), "r", encoding="utf-8") as f:
|
| 93 |
+
references[fname] = f.read()
|
| 94 |
+
except Exception:
|
| 95 |
+
pass
|
| 96 |
+
|
| 97 |
+
return {
|
| 98 |
+
"name": meta.get("name", os.path.basename(skill_dir)),
|
| 99 |
+
"description": meta.get("description", ""),
|
| 100 |
+
"language": meta.get("language", ""),
|
| 101 |
+
"tags": [t.strip() for t in meta.get("tags", "").split(",") if t.strip()],
|
| 102 |
+
"body": body.strip(),
|
| 103 |
+
"references": references,
|
| 104 |
+
"path": skill_dir,
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def list_skills() -> list[dict[str, Any]]:
|
| 109 |
+
"""List all available skills (metadata only, no body)."""
|
| 110 |
+
skills: list[dict[str, Any]] = []
|
| 111 |
+
seen_names: set[str] = set()
|
| 112 |
+
|
| 113 |
+
for skills_dir in _skill_dirs():
|
| 114 |
+
if not os.path.isdir(skills_dir):
|
| 115 |
+
continue
|
| 116 |
+
for entry in sorted(os.listdir(skills_dir)):
|
| 117 |
+
entry_path = os.path.join(skills_dir, entry)
|
| 118 |
+
if not os.path.isdir(entry_path):
|
| 119 |
+
continue
|
| 120 |
+
skill = _load_skill(entry_path)
|
| 121 |
+
if skill and skill["name"] not in seen_names:
|
| 122 |
+
seen_names.add(skill["name"])
|
| 123 |
+
skills.append({
|
| 124 |
+
"name": skill["name"],
|
| 125 |
+
"description": skill["description"],
|
| 126 |
+
"language": skill["language"],
|
| 127 |
+
"tags": skill["tags"],
|
| 128 |
+
})
|
| 129 |
+
return skills
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def get_skill(name: str) -> dict[str, Any] | None:
|
| 133 |
+
"""Get full skill content by name."""
|
| 134 |
+
for skills_dir in _skill_dirs():
|
| 135 |
+
if not os.path.isdir(skills_dir):
|
| 136 |
+
continue
|
| 137 |
+
# Try directory match
|
| 138 |
+
for entry in os.listdir(skills_dir):
|
| 139 |
+
entry_path = os.path.join(skills_dir, entry)
|
| 140 |
+
if not os.path.isdir(entry_path):
|
| 141 |
+
continue
|
| 142 |
+
skill = _load_skill(entry_path)
|
| 143 |
+
if skill and skill["name"] == name:
|
| 144 |
+
return skill
|
| 145 |
+
return None
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def invoke_skill(name: str) -> dict[str, Any]:
|
| 149 |
+
"""Invoke a skill by name β returns its full body and references."""
|
| 150 |
+
skill = get_skill(name)
|
| 151 |
+
if not skill:
|
| 152 |
+
return {
|
| 153 |
+
"success": False,
|
| 154 |
+
"error": f"Skill not found: {name}",
|
| 155 |
+
"available": [s["name"] for s in list_skills()],
|
| 156 |
+
}
|
| 157 |
+
return {
|
| 158 |
+
"success": True,
|
| 159 |
+
"name": skill["name"],
|
| 160 |
+
"description": skill["description"],
|
| 161 |
+
"body": skill["body"],
|
| 162 |
+
"references": skill["references"],
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def build_skills_context(skill_names: list[str] | None = None) -> str:
|
| 167 |
+
"""Build a context string with skill bodies to inject into the prompt.
|
| 168 |
+
|
| 169 |
+
If skill_names is None, includes all skills (brief listing only).
|
| 170 |
+
"""
|
| 171 |
+
if not skill_names:
|
| 172 |
+
# List all skills briefly
|
| 173 |
+
skills = list_skills()
|
| 174 |
+
if not skills:
|
| 175 |
+
return ""
|
| 176 |
+
lines = ["Available skills (use /skill <name> to load full instructions):"]
|
| 177 |
+
for s in skills:
|
| 178 |
+
desc = s["description"][:120]
|
| 179 |
+
lines.append(f"- {s['name']}: {desc}")
|
| 180 |
+
return "\n".join(lines)
|
| 181 |
+
|
| 182 |
+
# Load full bodies for requested skills
|
| 183 |
+
parts: list[str] = []
|
| 184 |
+
for name in skill_names:
|
| 185 |
+
skill = get_skill(name)
|
| 186 |
+
if skill:
|
| 187 |
+
parts.append(f"# Skill: {skill['name']}\n\n{skill['body']}")
|
| 188 |
+
for ref_name, ref_body in skill["references"].items():
|
| 189 |
+
parts.append(f"\n## Reference: {ref_name}\n\n{ref_body}")
|
| 190 |
+
else:
|
| 191 |
+
parts.append(f"# Skill: {name}\n\n(Skill not found)")
|
| 192 |
+
return "\n\n---\n\n".join(parts)
|
code/skills/builtins/code-review/SKILL.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: code-review
|
| 3 |
+
description: Review code for bugs, simplicity, DRY violations, and project conventions. Use when the user asks for a review or before committing changes.
|
| 4 |
+
language: any
|
| 5 |
+
tags: review, quality, bugs, refactoring
|
| 6 |
+
---
|
| 7 |
+
# Code Review
|
| 8 |
+
|
| 9 |
+
Review code with high signal and low noise. We only want HIGH SIGNAL issues.
|
| 10 |
+
|
| 11 |
+
## What to flag
|
| 12 |
+
|
| 13 |
+
Flag issues where:
|
| 14 |
+
- The code will fail to compile or parse (syntax errors, type errors, missing imports, unresolved references)
|
| 15 |
+
- The code will definitely produce wrong results regardless of inputs (clear logic errors)
|
| 16 |
+
- Clear, unambiguous project convention violations where you can quote the exact rule being broken
|
| 17 |
+
- Security issues (hardcoded secrets, SQL injection, XSS, path traversal, command injection)
|
| 18 |
+
- Resource leaks (unclosed files, connections, etc.)
|
| 19 |
+
|
| 20 |
+
## What NOT to flag
|
| 21 |
+
|
| 22 |
+
Do NOT flag:
|
| 23 |
+
- Code style or quality concerns (subjective)
|
| 24 |
+
- Potential issues that depend on specific inputs or state
|
| 25 |
+
- Subjective suggestions or improvements
|
| 26 |
+
- Issues a linter will catch
|
| 27 |
+
- Pre-existing issues outside the diff
|
| 28 |
+
- Pedantic nitpicks that a senior engineer would not flag
|
| 29 |
+
|
| 30 |
+
## Process
|
| 31 |
+
|
| 32 |
+
1. **Read all changed files** using `read_file`. If the user mentions a diff, focus on the changed lines plus surrounding context.
|
| 33 |
+
2. **Run parallel reviews** mentally:
|
| 34 |
+
- Review 1: Bugs and functional correctness β does it do what it claims?
|
| 35 |
+
- Review 2: Simplicity/DRY/elegance β can it be simpler?
|
| 36 |
+
- Review 3: Project conventions β does it match the codebase style?
|
| 37 |
+
3. **Validate each issue**: For each potential issue, check whether it's actually a problem by reading surrounding code.
|
| 38 |
+
4. **Filter false positives**: Drop anything you're not certain about.
|
| 39 |
+
5. **Present findings** sorted by severity (critical > high > medium > low). For each issue:
|
| 40 |
+
- File and line number
|
| 41 |
+
- Description of the issue
|
| 42 |
+
- Suggested fix (with code snippet if small)
|
| 43 |
+
|
| 44 |
+
## Output format
|
| 45 |
+
|
| 46 |
+
If issues found:
|
| 47 |
+
```
|
| 48 |
+
## Code Review
|
| 49 |
+
|
| 50 |
+
### Critical
|
| 51 |
+
- `path/to/file.py:42` β Description and fix
|
| 52 |
+
|
| 53 |
+
### High
|
| 54 |
+
- `path/to/file.py:50` β Description and fix
|
| 55 |
+
|
| 56 |
+
### Medium
|
| 57 |
+
- ...
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
If no issues:
|
| 61 |
+
```
|
| 62 |
+
## Code Review
|
| 63 |
+
|
| 64 |
+
No issues found. Checked for bugs, simplicity, DRY, and project conventions.
|
| 65 |
+
```
|
code/skills/builtins/commit-workflow/SKILL.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: commit-workflow
|
| 3 |
+
description: Guided git commit workflow. Use before committing changes β generates a clean commit message from the diff.
|
| 4 |
+
language: any
|
| 5 |
+
tags: git, commit, version-control
|
| 6 |
+
---
|
| 7 |
+
# Commit Workflow
|
| 8 |
+
|
| 9 |
+
When the user asks to commit changes, follow this workflow.
|
| 10 |
+
|
| 11 |
+
## Step 1: Inspect the changes
|
| 12 |
+
|
| 13 |
+
Run via `bash`:
|
| 14 |
+
- `git status` β see what's modified/staged/untracked
|
| 15 |
+
- `git diff HEAD` β full diff of staged and unstaged changes
|
| 16 |
+
- `git branch --show-current` β current branch
|
| 17 |
+
- `git log --oneline -10` β recent commits (to match style)
|
| 18 |
+
|
| 19 |
+
## Step 2: Analyze
|
| 20 |
+
|
| 21 |
+
Determine:
|
| 22 |
+
- What's the **single logical change** being committed?
|
| 23 |
+
- Are there multiple unrelated changes? If so, suggest splitting into multiple commits.
|
| 24 |
+
- What's the conventional commit type?
|
| 25 |
+
- `feat:` new feature
|
| 26 |
+
- `fix:` bug fix
|
| 27 |
+
- `docs:` documentation only
|
| 28 |
+
- `style:` formatting, no code change
|
| 29 |
+
- `refactor:` code change that neither fixes a bug nor adds a feature
|
| 30 |
+
- `perf:` code change that improves performance
|
| 31 |
+
- `test:` adding tests
|
| 32 |
+
- `chore:` build process, tooling, deps
|
| 33 |
+
|
| 34 |
+
## Step 3: Write the message
|
| 35 |
+
|
| 36 |
+
Format:
|
| 37 |
+
```
|
| 38 |
+
<type>(<optional scope>): <imperative subject under 72 chars>
|
| 39 |
+
|
| 40 |
+
<optional body explaining why, wrapped at 72 chars>
|
| 41 |
+
|
| 42 |
+
<optional footer like "Fixes #123">
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
Rules:
|
| 46 |
+
- Subject in imperative mood: "Add" not "Added" or "Adds"
|
| 47 |
+
- Subject lowercase, no period
|
| 48 |
+
- Body explains **why**, not what (the diff shows what)
|
| 49 |
+
- Reference issues in footer
|
| 50 |
+
|
| 51 |
+
## Step 4: Stage and commit
|
| 52 |
+
|
| 53 |
+
If specific files should be staged, run `git add <files>` explicitly. Otherwise `git add -A` is fine.
|
| 54 |
+
|
| 55 |
+
Commit with a heredoc to preserve formatting:
|
| 56 |
+
```bash
|
| 57 |
+
git commit -m "$(cat <<'EOF'
|
| 58 |
+
feat(auth): add OAuth2 login flow
|
| 59 |
+
|
| 60 |
+
Implements the login button, callback handler, and session
|
| 61 |
+
persistence using JWT in httpOnly cookies.
|
| 62 |
+
|
| 63 |
+
Fixes #142
|
| 64 |
+
EOF
|
| 65 |
+
)"
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
## Step 5: Verify
|
| 69 |
+
|
| 70 |
+
- `git log -1 --stat` to confirm the commit looks right
|
| 71 |
+
- `git status` to confirm clean working tree (or remaining unrelated changes)
|
| 72 |
+
|
| 73 |
+
## Anti-patterns
|
| 74 |
+
|
| 75 |
+
- β `git commit -m "fix"` β too vague
|
| 76 |
+
- β `git commit -m "Update file.py"` β describes what, not why
|
| 77 |
+
- β Mixing unrelated changes in one commit
|
| 78 |
+
- β Committing `node_modules/`, `.env`, build artifacts
|
| 79 |
+
- β Using `--no-verify` to skip hooks without explanation
|
code/skills/builtins/debugging/SKILL.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: debugging
|
| 3 |
+
description: Systematic debugging workflow for diagnosing and fixing errors. Use when the user reports a bug, crash, or unexpected behavior.
|
| 4 |
+
language: any
|
| 5 |
+
tags: debug, errors, troubleshooting, fix
|
| 6 |
+
---
|
| 7 |
+
# Debugging
|
| 8 |
+
|
| 9 |
+
Approach debugging systematically. Don't guess β investigate.
|
| 10 |
+
|
| 11 |
+
## Phase 1: Reproduce
|
| 12 |
+
|
| 13 |
+
**Goal**: Make the bug happen reliably.
|
| 14 |
+
|
| 15 |
+
- Ask: what exact steps trigger it?
|
| 16 |
+
- Ask: what was the expected vs actual behavior?
|
| 17 |
+
- Ask: is it consistent or intermittent?
|
| 18 |
+
- If you can reproduce it locally, do so with `bash` and capture the full error output.
|
| 19 |
+
|
| 20 |
+
## Phase 2: Isolate
|
| 21 |
+
|
| 22 |
+
**Goal**: Find the smallest scope where the bug reproduces.
|
| 23 |
+
|
| 24 |
+
- Read the relevant code with `read_file`
|
| 25 |
+
- Trace the data flow from input to failure point
|
| 26 |
+
- Add temporary print/logging statements if needed
|
| 27 |
+
- Identify the exact line where things go wrong
|
| 28 |
+
|
| 29 |
+
## Phase 3: Diagnose
|
| 30 |
+
|
| 31 |
+
**Goal**: Understand WHY the bug happens.
|
| 32 |
+
|
| 33 |
+
Common root causes:
|
| 34 |
+
- **Null/None/undefined**: missing null checks
|
| 35 |
+
- **Type confusion**: passing wrong type, silent coercion
|
| 36 |
+
- **Off-by-one**: loop bounds, slicing
|
| 37 |
+
- **Race conditions**: shared state, async ordering
|
| 38 |
+
- **Stale state**: cached data not invalidated
|
| 39 |
+
- **Wrong assumption**: code expects something the caller doesn't guarantee
|
| 40 |
+
- **Environment**: missing env vars, wrong paths, permissions
|
| 41 |
+
|
| 42 |
+
State the root cause in one sentence before fixing.
|
| 43 |
+
|
| 44 |
+
## Phase 4: Fix
|
| 45 |
+
|
| 46 |
+
**Goal**: Apply the minimal correct fix.
|
| 47 |
+
|
| 48 |
+
- Fix the root cause, not the symptom
|
| 49 |
+
- Don't introduce new patterns β match the surrounding code style
|
| 50 |
+
- Add a comment if the fix is non-obvious
|
| 51 |
+
- Consider: are there other places with the same bug?
|
| 52 |
+
|
| 53 |
+
## Phase 5: Verify
|
| 54 |
+
|
| 55 |
+
**Goal**: Confirm the fix works and doesn't break anything.
|
| 56 |
+
|
| 57 |
+
- Re-run the reproduction steps
|
| 58 |
+
- Run any existing tests with `bash`
|
| 59 |
+
- Test edge cases related to the bug
|
| 60 |
+
- Check that you haven't introduced regressions
|
| 61 |
+
|
| 62 |
+
## Phase 6: Document
|
| 63 |
+
|
| 64 |
+
**Goal**: Prevent recurrence.
|
| 65 |
+
|
| 66 |
+
- If appropriate, add a regression test
|
| 67 |
+
- Update relevant docs/comments
|
| 68 |
+
- Note the fix in the commit message
|
| 69 |
+
|
| 70 |
+
## Anti-patterns to avoid
|
| 71 |
+
|
| 72 |
+
- β Shotgun debugging: changing random things hoping it works
|
| 73 |
+
- β Fixing symptoms: papering over the real issue
|
| 74 |
+
- β Adding null checks everywhere: hiding the real bug
|
| 75 |
+
- β "Works on my machine": dismissing environmental factors
|
| 76 |
+
- β Skipping verification: assuming the fix works
|
code/skills/builtins/feature-dev/SKILL.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: feature-dev
|
| 3 |
+
description: Guided feature development with codebase understanding and architecture focus. Use when the user asks to implement a feature or build something non-trivial.
|
| 4 |
+
language: any
|
| 5 |
+
tags: workflow, architecture, planning, implementation
|
| 6 |
+
---
|
| 7 |
+
# Feature Development
|
| 8 |
+
|
| 9 |
+
You are helping a developer implement a new feature. Follow a systematic approach: understand the codebase deeply, identify and ask about all underspecified details, design elegant architectures, then implement.
|
| 10 |
+
|
| 11 |
+
## Core Principles
|
| 12 |
+
|
| 13 |
+
- **Ask clarifying questions**: Identify all ambiguities, edge cases, and underspecified behaviors. Ask specific, concrete questions rather than making assumptions.
|
| 14 |
+
- **Understand before acting**: Read and comprehend existing code patterns first
|
| 15 |
+
- **Simple and elegant**: Prioritize readable, maintainable, architecturally sound code
|
| 16 |
+
- **Use TodoWrite**: Track all progress throughout
|
| 17 |
+
|
| 18 |
+
## Phase 1: Discovery
|
| 19 |
+
|
| 20 |
+
**Goal**: Understand what needs to be built
|
| 21 |
+
|
| 22 |
+
**Actions**:
|
| 23 |
+
1. Create a todo list with all phases
|
| 24 |
+
2. If feature unclear, ask user for:
|
| 25 |
+
- What problem are they solving?
|
| 26 |
+
- What should the feature do?
|
| 27 |
+
- Any constraints or requirements?
|
| 28 |
+
3. Summarize understanding and confirm with user
|
| 29 |
+
|
| 30 |
+
## Phase 2: Codebase Exploration
|
| 31 |
+
|
| 32 |
+
**Goal**: Understand relevant existing code and patterns
|
| 33 |
+
|
| 34 |
+
**Actions**:
|
| 35 |
+
1. Use `list_dir` and `glob` to map the project structure
|
| 36 |
+
2. Use `grep` to find similar features and patterns
|
| 37 |
+
3. Use `read_file` on 5-10 key files identified
|
| 38 |
+
4. Present comprehensive summary of findings and patterns discovered
|
| 39 |
+
|
| 40 |
+
## Phase 3: Clarifying Questions
|
| 41 |
+
|
| 42 |
+
**Goal**: Fill in gaps and resolve all ambiguities before designing
|
| 43 |
+
|
| 44 |
+
**CRITICAL**: This is one of the most important phases. DO NOT SKIP.
|
| 45 |
+
|
| 46 |
+
**Actions**:
|
| 47 |
+
1. Review the codebase findings and original feature request
|
| 48 |
+
2. Identify underspecified aspects: edge cases, error handling, integration points, scope boundaries, design preferences, backward compatibility, performance needs
|
| 49 |
+
3. **Present all questions to the user in a clear, organized list**
|
| 50 |
+
4. **Wait for answers before proceeding to architecture design**
|
| 51 |
+
|
| 52 |
+
If the user says "whatever you think is best", provide your recommendation and get explicit confirmation.
|
| 53 |
+
|
| 54 |
+
## Phase 4: Architecture Design
|
| 55 |
+
|
| 56 |
+
**Goal**: Design an implementation approach with concrete trade-offs
|
| 57 |
+
|
| 58 |
+
**Actions**:
|
| 59 |
+
1. Design the implementation: minimal changes (smallest change, maximum reuse), clean architecture (maintainability, elegant abstractions), or pragmatic balance (speed + quality)
|
| 60 |
+
2. Present to user: brief summary, trade-offs, **your recommendation with reasoning**, concrete implementation differences
|
| 61 |
+
3. **Ask user to approve the approach**
|
| 62 |
+
|
| 63 |
+
## Phase 5: Implementation
|
| 64 |
+
|
| 65 |
+
**Goal**: Build the feature
|
| 66 |
+
|
| 67 |
+
**DO NOT START WITHOUT USER APPROVAL**
|
| 68 |
+
|
| 69 |
+
**Actions**:
|
| 70 |
+
1. Wait for explicit user approval
|
| 71 |
+
2. Read all relevant files identified in previous phases
|
| 72 |
+
3. Implement following chosen architecture
|
| 73 |
+
4. Follow codebase conventions strictly
|
| 74 |
+
5. Write clean, well-documented code
|
| 75 |
+
6. Update todos as you progress
|
| 76 |
+
|
| 77 |
+
## Phase 6: Quality Review
|
| 78 |
+
|
| 79 |
+
**Goal**: Ensure code is simple, DRY, elegant, easy to read, and functionally correct
|
| 80 |
+
|
| 81 |
+
**Actions**:
|
| 82 |
+
1. Self-review for: simplicity/DRY/elegance, bugs/functional correctness, project conventions/abstractions
|
| 83 |
+
2. Consolidate findings and identify highest severity issues that you recommend fixing
|
| 84 |
+
3. **Present findings to user and ask what they want to do** (fix now, fix later, or proceed as-is)
|
| 85 |
+
4. Address issues based on user decision
|
| 86 |
+
|
| 87 |
+
## Phase 7: Summary
|
| 88 |
+
|
| 89 |
+
**Goal**: Document what was accomplished
|
| 90 |
+
|
| 91 |
+
**Actions**:
|
| 92 |
+
1. Mark all todos complete
|
| 93 |
+
2. Summarize:
|
| 94 |
+
- What was built
|
| 95 |
+
- Key decisions made
|
| 96 |
+
- Files modified
|
| 97 |
+
- Suggested next steps
|
code/skills/builtins/frontend-design/SKILL.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: frontend-design
|
| 3 |
+
description: Guidance for distinctive, intentional visual design when building UI. Helps with palette, typography, and avoiding templated defaults.
|
| 4 |
+
language: any
|
| 5 |
+
tags: frontend, design, ui, css, html
|
| 6 |
+
---
|
| 7 |
+
# Frontend Design
|
| 8 |
+
|
| 9 |
+
Approach this as the design lead at a small studio known for giving every client a visual identity that could not be mistaken for anyone else's. This client has already rejected proposals that felt templated, and is paying for a distinctive point of view: make deliberate, opinionated choices about palette, typography, and layout that are specific to this brief, and take one real aesthetic risk you can justify.
|
| 10 |
+
|
| 11 |
+
## Ground it in the subject
|
| 12 |
+
|
| 13 |
+
If the brief does not pin down what the product or subject is, pin it yourself before designing: name one concrete subject, its audience, and the page's single job, and state your choice. The subject's own world, its materials, instruments, artifacts, and vernacular, is where distinctive choices come from.
|
| 14 |
+
|
| 15 |
+
## Design principles
|
| 16 |
+
|
| 17 |
+
For web designs, the hero is a thesis. Open with the most characteristic thing in the subject's world. Be deliberate with your choice: a big number with a small label, supporting stats, and a gradient accent is the template answer, only use if that's truly the best option.
|
| 18 |
+
|
| 19 |
+
Typography carries the personality of the page. Pair the display and body faces deliberately, not the same families you would reach for on any other project, and set a clear type scale with intentional weights, widths, and spacing.
|
| 20 |
+
|
| 21 |
+
Structure is information. Structural devices, numbering, eyebrows, dividers, labels, should encode something true about the content, not decorate it.
|
| 22 |
+
|
| 23 |
+
Leverage motion deliberately. Think about where and if animation can serve the subject: a page-load sequence, a scroll-triggered reveal, hover micro-interactions, ambient atmosphere.
|
| 24 |
+
|
| 25 |
+
## Process: brainstorm, explore, plan, critique, build, critique again
|
| 26 |
+
|
| 27 |
+
Work in two passes. First, brainstorm a short design plan based on the human's design brief: create a compact token system with color, type, layout, and signature.
|
| 28 |
+
|
| 29 |
+
- **Color**: describe the palette as 4-6 named hex values
|
| 30 |
+
- **Type**: the typefaces for 2+ roles (display + body + utility)
|
| 31 |
+
- **Layout**: a layout concept with one-sentence prose descriptions and ASCII wireframes
|
| 32 |
+
- **Signature**: the single unique element this page will be remembered by
|
| 33 |
+
|
| 34 |
+
Then review that plan against the brief before building: if any part of it reads like the generic default you would produce for any similar page rather than a choice made for this specific brief β revise that part, say what you changed and why. Only after you've confirmed the relative uniqueness of your design plan should you start to write the code.
|
| 35 |
+
|
| 36 |
+
## Restraint and self-critique
|
| 37 |
+
|
| 38 |
+
Spend your boldness in one place. Let the signature element be the one memorable thing, keep everything around it quiet and disciplined, and cut any decoration that does not serve the brief. Build to a quality floor without announcing it: responsive down to mobile, visible keyboard focus, reduced motion respected.
|
code/skills/builtins/fullstack-scaffold/SKILL.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: fullstack-scaffold
|
| 3 |
+
description: Scaffold complete fullstack projects with proper structure. Generates package.json, requirements.txt, Dockerfiles, and config files for any framework.
|
| 4 |
+
language: any
|
| 5 |
+
tags: scaffold, fullstack, project, structure, framework
|
| 6 |
+
---
|
| 7 |
+
# Fullstack Project Scaffolding
|
| 8 |
+
|
| 9 |
+
When generating a fullstack application, always produce a complete, runnable project β not just snippets.
|
| 10 |
+
|
| 11 |
+
## Project structure rules
|
| 12 |
+
|
| 13 |
+
### Python (Flask/FastAPI/Django/Streamlit/Gradio)
|
| 14 |
+
```
|
| 15 |
+
project-name/
|
| 16 |
+
βββ app.py # Entry point (HF Spaces expects app.py)
|
| 17 |
+
βββ requirements.txt # Pinned dependencies
|
| 18 |
+
βββ README.md # With HF Space frontmatter if deploying
|
| 19 |
+
βββ .env.example # Document required env vars
|
| 20 |
+
βββ src/
|
| 21 |
+
β βββ __init__.py
|
| 22 |
+
β βββ routes/ # API routes
|
| 23 |
+
β βββ models/ # Data models
|
| 24 |
+
β βββ services/ # Business logic
|
| 25 |
+
β βββ utils/ # Helpers
|
| 26 |
+
βββ tests/
|
| 27 |
+
β βββ test_app.py
|
| 28 |
+
βββ static/ # Static assets (if web framework)
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
### Next.js / React (Vite)
|
| 32 |
+
```
|
| 33 |
+
project-name/
|
| 34 |
+
βββ package.json # name, version, scripts, deps
|
| 35 |
+
βββ next.config.js # (Next.js only) output: 'standalone'
|
| 36 |
+
βββ vite.config.js # (Vite only)
|
| 37 |
+
βββ tsconfig.json # (TypeScript)
|
| 38 |
+
βββ Dockerfile # Multi-stage build for HF Spaces
|
| 39 |
+
βββ .dockerignore
|
| 40 |
+
βββ README.md
|
| 41 |
+
βββ public/
|
| 42 |
+
β βββ (static assets)
|
| 43 |
+
βββ src/
|
| 44 |
+
βββ app/ # Next.js App Router
|
| 45 |
+
βββ pages/ # Next.js Pages Router (legacy)
|
| 46 |
+
βββ components/ # Reusable components
|
| 47 |
+
βββ lib/ # Utilities, helpers
|
| 48 |
+
βββ hooks/ # Custom hooks
|
| 49 |
+
βββ styles/ # Global CSS
|
| 50 |
+
βββ types/ # TypeScript types
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
### Express / NestJS
|
| 54 |
+
```
|
| 55 |
+
project-name/
|
| 56 |
+
βββ package.json
|
| 57 |
+
βββ Dockerfile
|
| 58 |
+
βββ .dockerignore
|
| 59 |
+
βββ README.md
|
| 60 |
+
βββ src/
|
| 61 |
+
β βββ index.js # Entry β app.listen(7860, '0.0.0.0')
|
| 62 |
+
β βββ routes/
|
| 63 |
+
β βββ middleware/
|
| 64 |
+
β βββ controllers/ # NestJS only
|
| 65 |
+
β βββ services/
|
| 66 |
+
β βββ models/
|
| 67 |
+
βββ tests/
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
## Port and host rules (critical for HF Spaces)
|
| 71 |
+
|
| 72 |
+
- All servers MUST listen on `0.0.0.0` (not `localhost` or `127.0.0.1`)
|
| 73 |
+
- All servers MUST use port `7860` (HF Spaces default)
|
| 74 |
+
- For sub-servers (Gradio subprocess), use 7861, 7862, etc.
|
| 75 |
+
|
| 76 |
+
## Dockerfile rules
|
| 77 |
+
|
| 78 |
+
For JS/TS projects, generate a Dockerfile:
|
| 79 |
+
- Use `node:20-slim` as base
|
| 80 |
+
- Multi-stage build: deps β builder β runner
|
| 81 |
+
- For SPAs (React/Vue), serve with nginx on port 7860
|
| 82 |
+
- For Next.js, use `output: 'standalone'` and copy `.next/standalone`
|
| 83 |
+
- Run as non-root user
|
| 84 |
+
|
| 85 |
+
For Python projects, the HF Space SDK auto-generates the runtime β no Dockerfile needed unless using Docker SDK.
|
| 86 |
+
|
| 87 |
+
## package.json rules
|
| 88 |
+
|
| 89 |
+
- Always include `name`, `version`, `private: true`
|
| 90 |
+
- Scripts: `dev`, `build`, `start` (start must bind 0.0.0.0:7860)
|
| 91 |
+
- Pin major versions: `"react": "^18.3.0"` not `"react": "latest"`
|
| 92 |
+
- Dev dependencies: TypeScript, types, build tools, linters
|
| 93 |
+
- Production dependencies: runtime libraries only
|
| 94 |
+
|
| 95 |
+
## requirements.txt rules
|
| 96 |
+
|
| 97 |
+
- Pin with `>=` to allow patch updates: `flask>=3.0.0`
|
| 98 |
+
- Group by category (web, db, ml, dev) with comments
|
| 99 |
+
- Always include the framework (flask, fastapi, gradio, etc.)
|
| 100 |
+
- Never include stdlib modules
|
| 101 |
+
|
| 102 |
+
## README.md rules
|
| 103 |
+
|
| 104 |
+
For HF Spaces, include frontmatter:
|
| 105 |
+
```yaml
|
| 106 |
+
---
|
| 107 |
+
title: App Name
|
| 108 |
+
emoji: π
|
| 109 |
+
colorFrom: blue
|
| 110 |
+
colorTo: purple
|
| 111 |
+
sdk: gradio # or docker, static, streamlit
|
| 112 |
+
app_file: app.py # or index.html, Dockerfile
|
| 113 |
+
pinned: false
|
| 114 |
+
---
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
## Don't include
|
| 118 |
+
|
| 119 |
+
- `node_modules/`
|
| 120 |
+
- `.next/` (build output)
|
| 121 |
+
- `__pycache__/`
|
| 122 |
+
- `.venv/`, `venv/`
|
| 123 |
+
- Lock files (let HF install fresh)
|
| 124 |
+
- `.env` (only `.env.example`)
|
code/tools/__init__.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""File system and shell tools for the agent.
|
| 2 |
+
|
| 3 |
+
Inspired by Claude Code's tool set:
|
| 4 |
+
- read_file / write_file / edit_file
|
| 5 |
+
- glob / grep
|
| 6 |
+
- bash (subprocess)
|
| 7 |
+
- list_dir
|
| 8 |
+
- todo_write / todo_read
|
| 9 |
+
"""
|
| 10 |
+
from code.tools.fs import (
|
| 11 |
+
read_file,
|
| 12 |
+
write_file,
|
| 13 |
+
edit_file,
|
| 14 |
+
multi_edit,
|
| 15 |
+
list_dir,
|
| 16 |
+
glob_paths,
|
| 17 |
+
grep_search,
|
| 18 |
+
)
|
| 19 |
+
from code.tools.bash import run_bash
|
| 20 |
+
from code.tools.todos import (
|
| 21 |
+
todo_read,
|
| 22 |
+
todo_write,
|
| 23 |
+
todo_update,
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
__all__ = [
|
| 27 |
+
"read_file",
|
| 28 |
+
"write_file",
|
| 29 |
+
"edit_file",
|
| 30 |
+
"multi_edit",
|
| 31 |
+
"list_dir",
|
| 32 |
+
"glob_paths",
|
| 33 |
+
"grep_search",
|
| 34 |
+
"run_bash",
|
| 35 |
+
"todo_read",
|
| 36 |
+
"todo_write",
|
| 37 |
+
"todo_update",
|
| 38 |
+
]
|
code/tools/bash.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Bash subprocess tool with timeout and output capture."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
import shlex
|
| 7 |
+
import subprocess
|
| 8 |
+
from typing import Any
|
| 9 |
+
|
| 10 |
+
from code.tools.fs import _resolve_safe, get_workspace_root
|
| 11 |
+
|
| 12 |
+
# βββ Safety: commands that are forbidden by default ββββββββββββββββββββ
|
| 13 |
+
|
| 14 |
+
_BLOCKED_PATTERNS = [
|
| 15 |
+
"rm -rf /",
|
| 16 |
+
"rm -rf ~",
|
| 17 |
+
"rm -rf $HOME",
|
| 18 |
+
":(){:|:&};:",
|
| 19 |
+
"mkfs",
|
| 20 |
+
"dd if=/dev/zero of=/dev/",
|
| 21 |
+
"> /dev/sda",
|
| 22 |
+
"shutdown",
|
| 23 |
+
"reboot",
|
| 24 |
+
"halt",
|
| 25 |
+
]
|
| 26 |
+
|
| 27 |
+
# Default env vars to scrub for safety
|
| 28 |
+
_ENV_SCRUB = {"HF_TOKEN", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "AWS_SECRET_ACCESS_KEY"}
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _is_safe_command(cmd: str) -> tuple[bool, str]:
|
| 32 |
+
"""Check if a command is safe to run."""
|
| 33 |
+
stripped = cmd.strip()
|
| 34 |
+
if not stripped:
|
| 35 |
+
return False, "Empty command"
|
| 36 |
+
for pat in _BLOCKED_PATTERNS:
|
| 37 |
+
if pat in stripped:
|
| 38 |
+
return False, f"Blocked pattern detected: {pat}"
|
| 39 |
+
return True, ""
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def run_bash(
|
| 43 |
+
command: str,
|
| 44 |
+
cwd: str | None = None,
|
| 45 |
+
timeout: int = 30,
|
| 46 |
+
env_extra: dict[str, str] | None = None,
|
| 47 |
+
) -> dict[str, Any]:
|
| 48 |
+
"""Run a shell command in the workspace.
|
| 49 |
+
|
| 50 |
+
Args:
|
| 51 |
+
command: Shell command to execute.
|
| 52 |
+
cwd: Working directory (relative to workspace root, defaults to workspace).
|
| 53 |
+
timeout: Max seconds before killing the process.
|
| 54 |
+
env_extra: Extra environment variables.
|
| 55 |
+
|
| 56 |
+
Returns:
|
| 57 |
+
dict with: stdout, stderr, returncode, timed_out
|
| 58 |
+
"""
|
| 59 |
+
try:
|
| 60 |
+
safe, reason = _is_safe_command(command)
|
| 61 |
+
if not safe:
|
| 62 |
+
return {
|
| 63 |
+
"success": False,
|
| 64 |
+
"stdout": "",
|
| 65 |
+
"stderr": reason,
|
| 66 |
+
"returncode": -1,
|
| 67 |
+
"timed_out": False,
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
if cwd:
|
| 71 |
+
work_dir = _resolve_safe(cwd)
|
| 72 |
+
else:
|
| 73 |
+
work_dir = get_workspace_root()
|
| 74 |
+
|
| 75 |
+
# Build env: scrub secrets, add extras
|
| 76 |
+
env = {k: v for k, v in os.environ.items() if k not in _ENV_SCRUB}
|
| 77 |
+
if env_extra:
|
| 78 |
+
env.update(env_extra)
|
| 79 |
+
|
| 80 |
+
# Run with bash -c for full shell semantics
|
| 81 |
+
completed = subprocess.run(
|
| 82 |
+
["bash", "-c", command],
|
| 83 |
+
cwd=work_dir,
|
| 84 |
+
env=env,
|
| 85 |
+
capture_output=True,
|
| 86 |
+
text=True,
|
| 87 |
+
timeout=timeout,
|
| 88 |
+
check=False,
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
# Truncate huge outputs
|
| 92 |
+
stdout = completed.stdout
|
| 93 |
+
stderr = completed.stderr
|
| 94 |
+
if len(stdout) > 50_000:
|
| 95 |
+
stdout = stdout[:50_000] + f"\n... truncated ({len(stdout) - 50_000} chars) ..."
|
| 96 |
+
if len(stderr) > 50_000:
|
| 97 |
+
stderr = stderr[:50_000] + f"\n... truncated ({len(stderr) - 50_000} chars) ..."
|
| 98 |
+
|
| 99 |
+
return {
|
| 100 |
+
"success": completed.returncode == 0,
|
| 101 |
+
"stdout": stdout,
|
| 102 |
+
"stderr": stderr,
|
| 103 |
+
"returncode": completed.returncode,
|
| 104 |
+
"timed_out": False,
|
| 105 |
+
"command": command,
|
| 106 |
+
"cwd": cwd or ".",
|
| 107 |
+
}
|
| 108 |
+
except subprocess.TimeoutExpired as exc:
|
| 109 |
+
stdout = exc.stdout or "" if isinstance(exc.stdout, str) else (exc.stdout or b"").decode("utf-8", errors="replace")
|
| 110 |
+
stderr = exc.stderr or "" if isinstance(exc.stderr, str) else (exc.stderr or b"").decode("utf-8", errors="replace")
|
| 111 |
+
return {
|
| 112 |
+
"success": False,
|
| 113 |
+
"stdout": stdout,
|
| 114 |
+
"stderr": f"Timeout after {timeout}s\n{stderr}",
|
| 115 |
+
"returncode": -1,
|
| 116 |
+
"timed_out": True,
|
| 117 |
+
"command": command,
|
| 118 |
+
}
|
| 119 |
+
except Exception as exc:
|
| 120 |
+
return {
|
| 121 |
+
"success": False,
|
| 122 |
+
"stdout": "",
|
| 123 |
+
"stderr": str(exc),
|
| 124 |
+
"returncode": -1,
|
| 125 |
+
"timed_out": False,
|
| 126 |
+
"command": command,
|
| 127 |
+
}
|
code/tools/fs.py
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""File system tools: read, write, edit, list, glob, grep.
|
| 2 |
+
|
| 3 |
+
All tools are sandboxed to a configurable workspace root (default: ./workspace).
|
| 4 |
+
They return JSON-serializable dicts so they can be exposed via the API.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import fnmatch
|
| 10 |
+
import os
|
| 11 |
+
import re
|
| 12 |
+
import shutil
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
from typing import Any
|
| 15 |
+
|
| 16 |
+
# βββ Workspace sandbox ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 17 |
+
|
| 18 |
+
# Default workspace: ./workspace under the app root
|
| 19 |
+
_DEFAULT_WORKSPACE = os.environ.get(
|
| 20 |
+
"SONICODER_WORKSPACE",
|
| 21 |
+
os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "workspace")),
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def get_workspace_root() -> str:
|
| 26 |
+
"""Return the absolute path of the agent's workspace root."""
|
| 27 |
+
root = _DEFAULT_WORKSPACE
|
| 28 |
+
os.makedirs(root, exist_ok=True)
|
| 29 |
+
return root
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _resolve_safe(path: str) -> str:
|
| 33 |
+
"""Resolve a path safely within the workspace root.
|
| 34 |
+
|
| 35 |
+
Raises ValueError if the resolved path escapes the workspace.
|
| 36 |
+
"""
|
| 37 |
+
root = get_workspace_root()
|
| 38 |
+
if os.path.isabs(path):
|
| 39 |
+
full = os.path.abspath(path)
|
| 40 |
+
else:
|
| 41 |
+
full = os.path.abspath(os.path.join(root, path))
|
| 42 |
+
|
| 43 |
+
# Ensure path is within the workspace
|
| 44 |
+
if not (full == root or full.startswith(root + os.sep)):
|
| 45 |
+
raise ValueError(
|
| 46 |
+
f"Path '{path}' resolves outside the workspace root ({root}). "
|
| 47 |
+
"Agent tools are sandboxed."
|
| 48 |
+
)
|
| 49 |
+
return full
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
# βββ read_file ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 53 |
+
|
| 54 |
+
def read_file(path: str, offset: int = 0, limit: int | None = None) -> dict[str, Any]:
|
| 55 |
+
"""Read a text file from the workspace.
|
| 56 |
+
|
| 57 |
+
Args:
|
| 58 |
+
path: Relative path inside the workspace, or absolute within it.
|
| 59 |
+
offset: 1-indexed line to start reading from.
|
| 60 |
+
limit: Maximum number of lines to read.
|
| 61 |
+
|
| 62 |
+
Returns:
|
| 63 |
+
dict with: path, content, line_count, truncated
|
| 64 |
+
"""
|
| 65 |
+
try:
|
| 66 |
+
full = _resolve_safe(path)
|
| 67 |
+
if not os.path.exists(full):
|
| 68 |
+
return {"success": False, "error": f"File not found: {path}"}
|
| 69 |
+
if os.path.isdir(full):
|
| 70 |
+
return {"success": False, "error": f"Path is a directory: {path}"}
|
| 71 |
+
|
| 72 |
+
with open(full, "r", encoding="utf-8", errors="replace") as f:
|
| 73 |
+
lines = f.readlines()
|
| 74 |
+
|
| 75 |
+
total = len(lines)
|
| 76 |
+
start = max(0, (offset - 1) if offset > 0 else 0)
|
| 77 |
+
end = (start + limit) if limit else total
|
| 78 |
+
selected = lines[start:end]
|
| 79 |
+
|
| 80 |
+
# Re-number for display
|
| 81 |
+
numbered = "".join(
|
| 82 |
+
f"{start + i + 1:6}\t{line}" for i, line in enumerate(selected)
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
return {
|
| 86 |
+
"success": True,
|
| 87 |
+
"path": path,
|
| 88 |
+
"content": numbered,
|
| 89 |
+
"line_count": total,
|
| 90 |
+
"returned_lines": len(selected),
|
| 91 |
+
"truncated": end < total,
|
| 92 |
+
}
|
| 93 |
+
except Exception as exc:
|
| 94 |
+
return {"success": False, "error": str(exc)}
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
# βββ write_file βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 98 |
+
|
| 99 |
+
def write_file(path: str, content: str) -> dict[str, Any]:
|
| 100 |
+
"""Write content to a file, creating parent directories as needed."""
|
| 101 |
+
try:
|
| 102 |
+
full = _resolve_safe(path)
|
| 103 |
+
os.makedirs(os.path.dirname(full), exist_ok=True)
|
| 104 |
+
with open(full, "w", encoding="utf-8") as f:
|
| 105 |
+
f.write(content)
|
| 106 |
+
return {
|
| 107 |
+
"success": True,
|
| 108 |
+
"path": path,
|
| 109 |
+
"bytes_written": len(content.encode("utf-8")),
|
| 110 |
+
}
|
| 111 |
+
except Exception as exc:
|
| 112 |
+
return {"success": False, "error": str(exc)}
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
# βββ edit_file ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 116 |
+
|
| 117 |
+
def edit_file(
|
| 118 |
+
path: str,
|
| 119 |
+
old_str: str,
|
| 120 |
+
new_str: str,
|
| 121 |
+
replace_all: bool = False,
|
| 122 |
+
) -> dict[str, Any]:
|
| 123 |
+
"""Replace occurrences of old_str with new_str in a file."""
|
| 124 |
+
try:
|
| 125 |
+
full = _resolve_safe(path)
|
| 126 |
+
if not os.path.exists(full):
|
| 127 |
+
return {"success": False, "error": f"File not found: {path}"}
|
| 128 |
+
|
| 129 |
+
with open(full, "r", encoding="utf-8") as f:
|
| 130 |
+
content = f.read()
|
| 131 |
+
|
| 132 |
+
if old_str not in content:
|
| 133 |
+
return {
|
| 134 |
+
"success": False,
|
| 135 |
+
"error": f"old_str not found in {path}. Edit aborted.",
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
if old_str == new_str:
|
| 139 |
+
return {"success": False, "error": "old_str and new_str are identical."}
|
| 140 |
+
|
| 141 |
+
count = content.count(old_str) if replace_all else 1
|
| 142 |
+
if not replace_all and count > 1:
|
| 143 |
+
return {
|
| 144 |
+
"success": False,
|
| 145 |
+
"error": (
|
| 146 |
+
f"old_str is not unique ({count} matches) in {path}. "
|
| 147 |
+
"Provide more context or use replace_all=true."
|
| 148 |
+
),
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
new_content = content.replace(old_str, new_str) if replace_all else content.replace(
|
| 152 |
+
old_str, new_str, 1
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
with open(full, "w", encoding="utf-8") as f:
|
| 156 |
+
f.write(new_content)
|
| 157 |
+
|
| 158 |
+
return {
|
| 159 |
+
"success": True,
|
| 160 |
+
"path": path,
|
| 161 |
+
"replacements": count,
|
| 162 |
+
}
|
| 163 |
+
except Exception as exc:
|
| 164 |
+
return {"success": False, "error": str(exc)}
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def multi_edit(path: str, edits: list[dict[str, Any]]) -> dict[str, Any]:
|
| 168 |
+
"""Apply multiple edits to a file atomically (all-or-nothing)."""
|
| 169 |
+
try:
|
| 170 |
+
full = _resolve_safe(path)
|
| 171 |
+
if not os.path.exists(full):
|
| 172 |
+
return {"success": False, "error": f"File not found: {path}"}
|
| 173 |
+
|
| 174 |
+
with open(full, "r", encoding="utf-8") as f:
|
| 175 |
+
content = f.read()
|
| 176 |
+
|
| 177 |
+
applied = 0
|
| 178 |
+
for edit in edits:
|
| 179 |
+
old_str = edit.get("old_str", "")
|
| 180 |
+
new_str = edit.get("new_str", "")
|
| 181 |
+
replace_all = edit.get("replace_all", False)
|
| 182 |
+
if old_str not in content:
|
| 183 |
+
return {
|
| 184 |
+
"success": False,
|
| 185 |
+
"error": f"old_str not found in {path} for edit #{applied + 1}.",
|
| 186 |
+
"applied": applied,
|
| 187 |
+
}
|
| 188 |
+
if old_str == new_str:
|
| 189 |
+
return {
|
| 190 |
+
"success": False,
|
| 191 |
+
"error": f"old_str and new_str identical in edit #{applied + 1}.",
|
| 192 |
+
"applied": applied,
|
| 193 |
+
}
|
| 194 |
+
content = content.replace(old_str, new_str) if replace_all else content.replace(
|
| 195 |
+
old_str, new_str, 1
|
| 196 |
+
)
|
| 197 |
+
applied += 1
|
| 198 |
+
|
| 199 |
+
with open(full, "w", encoding="utf-8") as f:
|
| 200 |
+
f.write(content)
|
| 201 |
+
|
| 202 |
+
return {"success": True, "path": path, "applied": applied}
|
| 203 |
+
except Exception as exc:
|
| 204 |
+
return {"success": False, "error": str(exc)}
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
# βββ list_dir βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 208 |
+
|
| 209 |
+
def list_dir(path: str = ".") -> dict[str, Any]:
|
| 210 |
+
"""List directory contents."""
|
| 211 |
+
try:
|
| 212 |
+
full = _resolve_safe(path)
|
| 213 |
+
if not os.path.exists(full):
|
| 214 |
+
return {"success": False, "error": f"Path not found: {path}"}
|
| 215 |
+
if not os.path.isdir(full):
|
| 216 |
+
return {"success": False, "error": f"Not a directory: {path}"}
|
| 217 |
+
|
| 218 |
+
entries = []
|
| 219 |
+
for name in sorted(os.listdir(full)):
|
| 220 |
+
entry_path = os.path.join(full, name)
|
| 221 |
+
stat = os.stat(entry_path)
|
| 222 |
+
entries.append({
|
| 223 |
+
"name": name,
|
| 224 |
+
"type": "dir" if os.path.isdir(entry_path) else "file",
|
| 225 |
+
"size": stat.st_size,
|
| 226 |
+
"path": os.path.relpath(entry_path, get_workspace_root()),
|
| 227 |
+
})
|
| 228 |
+
|
| 229 |
+
return {
|
| 230 |
+
"success": True,
|
| 231 |
+
"path": path,
|
| 232 |
+
"entries": entries,
|
| 233 |
+
}
|
| 234 |
+
except Exception as exc:
|
| 235 |
+
return {"success": False, "error": str(exc)}
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
# βββ glob βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 239 |
+
|
| 240 |
+
def glob_paths(pattern: str, path: str = ".") -> dict[str, Any]:
|
| 241 |
+
"""Glob file paths matching a pattern, recursively."""
|
| 242 |
+
try:
|
| 243 |
+
full = _resolve_safe(path)
|
| 244 |
+
matches: list[str] = []
|
| 245 |
+
for root_dir, _dirs, files in os.walk(full):
|
| 246 |
+
for fname in files:
|
| 247 |
+
if fnmatch.fnmatch(fname, pattern) or fnmatch.fnmatch(
|
| 248 |
+
os.path.relpath(os.path.join(root_dir, fname), full), pattern
|
| 249 |
+
):
|
| 250 |
+
matches.append(os.path.relpath(os.path.join(root_dir, fname), get_workspace_root()))
|
| 251 |
+
matches.sort()
|
| 252 |
+
return {"success": True, "pattern": pattern, "matches": matches}
|
| 253 |
+
except Exception as exc:
|
| 254 |
+
return {"success": False, "error": str(exc)}
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
# βββ grep βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 258 |
+
|
| 259 |
+
def grep_search(
|
| 260 |
+
pattern: str,
|
| 261 |
+
path: str = ".",
|
| 262 |
+
include: str | None = None,
|
| 263 |
+
ignore_case: bool = False,
|
| 264 |
+
max_results: int = 100,
|
| 265 |
+
) -> dict[str, Any]:
|
| 266 |
+
"""Search file contents with a regex pattern."""
|
| 267 |
+
try:
|
| 268 |
+
full = _resolve_safe(path)
|
| 269 |
+
flags = re.IGNORECASE if ignore_case else 0
|
| 270 |
+
regex = re.compile(pattern, flags)
|
| 271 |
+
|
| 272 |
+
matches: list[dict[str, Any]] = []
|
| 273 |
+
for root_dir, _dirs, files in os.walk(full):
|
| 274 |
+
for fname in files:
|
| 275 |
+
if include and not fnmatch.fnmatch(fname, include):
|
| 276 |
+
continue
|
| 277 |
+
fpath = os.path.join(root_dir, fname)
|
| 278 |
+
try:
|
| 279 |
+
with open(fpath, "r", encoding="utf-8", errors="replace") as f:
|
| 280 |
+
for lineno, line in enumerate(f, 1):
|
| 281 |
+
if regex.search(line):
|
| 282 |
+
matches.append({
|
| 283 |
+
"file": os.path.relpath(fpath, get_workspace_root()),
|
| 284 |
+
"line": lineno,
|
| 285 |
+
"text": line.rstrip()[:500],
|
| 286 |
+
})
|
| 287 |
+
if len(matches) >= max_results:
|
| 288 |
+
return {
|
| 289 |
+
"success": True,
|
| 290 |
+
"pattern": pattern,
|
| 291 |
+
"matches": matches,
|
| 292 |
+
"truncated": True,
|
| 293 |
+
}
|
| 294 |
+
except (UnicodeDecodeError, PermissionError):
|
| 295 |
+
continue
|
| 296 |
+
|
| 297 |
+
return {
|
| 298 |
+
"success": True,
|
| 299 |
+
"pattern": pattern,
|
| 300 |
+
"matches": matches,
|
| 301 |
+
"truncated": False,
|
| 302 |
+
}
|
| 303 |
+
except re.error as exc:
|
| 304 |
+
return {"success": False, "error": f"Invalid regex: {exc}"}
|
| 305 |
+
except Exception as exc:
|
| 306 |
+
return {"success": False, "error": str(exc)}
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
# βββ Workspace management βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 310 |
+
|
| 311 |
+
def list_workspace_tree(max_depth: int = 3) -> dict[str, Any]:
|
| 312 |
+
"""Return a tree view of the workspace."""
|
| 313 |
+
try:
|
| 314 |
+
root = get_workspace_root()
|
| 315 |
+
|
| 316 |
+
def _walk(path: str, depth: int) -> dict[str, Any]:
|
| 317 |
+
if depth > max_depth:
|
| 318 |
+
return {"name": os.path.basename(path), "type": "dir", "truncated": True}
|
| 319 |
+
entries = []
|
| 320 |
+
try:
|
| 321 |
+
for name in sorted(os.listdir(path)):
|
| 322 |
+
full = os.path.join(path, name)
|
| 323 |
+
if os.path.isdir(full):
|
| 324 |
+
entries.append(_walk(full, depth + 1))
|
| 325 |
+
else:
|
| 326 |
+
entries.append({
|
| 327 |
+
"name": name,
|
| 328 |
+
"type": "file",
|
| 329 |
+
"size": os.path.getsize(full),
|
| 330 |
+
})
|
| 331 |
+
except PermissionError:
|
| 332 |
+
pass
|
| 333 |
+
return {"name": os.path.basename(path), "type": "dir", "children": entries}
|
| 334 |
+
|
| 335 |
+
tree = _walk(root, 0)
|
| 336 |
+
return {"success": True, "tree": tree}
|
| 337 |
+
except Exception as exc:
|
| 338 |
+
return {"success": False, "error": str(exc)}
|
| 339 |
+
|
| 340 |
+
|
| 341 |
+
def reset_workspace() -> dict[str, Any]:
|
| 342 |
+
"""Clear all files in the workspace (used by /new command)."""
|
| 343 |
+
try:
|
| 344 |
+
root = get_workspace_root()
|
| 345 |
+
if os.path.exists(root):
|
| 346 |
+
for entry in os.listdir(root):
|
| 347 |
+
full = os.path.join(root, entry)
|
| 348 |
+
if os.path.isdir(full):
|
| 349 |
+
shutil.rmtree(full)
|
| 350 |
+
else:
|
| 351 |
+
os.remove(full)
|
| 352 |
+
return {"success": True, "message": "Workspace cleared"}
|
| 353 |
+
except Exception as exc:
|
| 354 |
+
return {"success": False, "error": str(exc)}
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
def snapshot_workspace() -> dict[str, str]:
|
| 358 |
+
"""Return a dict of {relative_path: content} for all text files in the workspace.
|
| 359 |
+
|
| 360 |
+
Used to package workspace files for ZIP/HF deploy.
|
| 361 |
+
"""
|
| 362 |
+
root = get_workspace_root()
|
| 363 |
+
files: dict[str, str] = {}
|
| 364 |
+
for dirpath, _dirs, fnames in os.walk(root):
|
| 365 |
+
# Skip hidden dirs and node_modules / __pycache__
|
| 366 |
+
parts = os.path.relpath(dirpath, root).split(os.sep)
|
| 367 |
+
if any(p.startswith(".") or p in {"node_modules", "__pycache__", ".venv", "venv"} for p in parts):
|
| 368 |
+
continue
|
| 369 |
+
for fname in fnames:
|
| 370 |
+
if fname.startswith("."):
|
| 371 |
+
continue
|
| 372 |
+
full = os.path.join(dirpath, fname)
|
| 373 |
+
try:
|
| 374 |
+
with open(full, "r", encoding="utf-8") as f:
|
| 375 |
+
files[os.path.relpath(full, root)] = f.read()
|
| 376 |
+
except (UnicodeDecodeError, PermissionError):
|
| 377 |
+
continue
|
| 378 |
+
return files
|
code/tools/todos.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Todo list tool β Claude Code-style task tracking.
|
| 2 |
+
|
| 3 |
+
Todos are persisted per-session in memory. Each todo has:
|
| 4 |
+
- id (string)
|
| 5 |
+
- content (string)
|
| 6 |
+
- status: pending | in_progress | completed
|
| 7 |
+
- priority: high | medium | low
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import threading
|
| 13 |
+
from typing import Any
|
| 14 |
+
|
| 15 |
+
# βββ Per-session state ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 16 |
+
|
| 17 |
+
_sessions: dict[str, list[dict[str, Any]]] = {}
|
| 18 |
+
_lock = threading.Lock()
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def _get_session_id(session_id: str | None) -> str:
|
| 22 |
+
return session_id or "default"
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def todo_read(session_id: str | None = None) -> dict[str, Any]:
|
| 26 |
+
"""Read the current todo list for a session."""
|
| 27 |
+
sid = _get_session_id(session_id)
|
| 28 |
+
with _lock:
|
| 29 |
+
todos = list(_sessions.get(sid, []))
|
| 30 |
+
return {"success": True, "todos": todos}
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def todo_write(
|
| 34 |
+
todos: list[dict[str, Any]],
|
| 35 |
+
session_id: str | None = None,
|
| 36 |
+
) -> dict[str, Any]:
|
| 37 |
+
"""Replace the entire todo list for a session.
|
| 38 |
+
|
| 39 |
+
Each todo: {id, content, status, priority}
|
| 40 |
+
"""
|
| 41 |
+
sid = _get_session_id(session_id)
|
| 42 |
+
|
| 43 |
+
# Validate and normalize
|
| 44 |
+
normalized: list[dict[str, Any]] = []
|
| 45 |
+
for t in todos:
|
| 46 |
+
if not isinstance(t, dict):
|
| 47 |
+
continue
|
| 48 |
+
normalized.append({
|
| 49 |
+
"id": str(t.get("id", "")),
|
| 50 |
+
"content": str(t.get("content", "")),
|
| 51 |
+
"status": t.get("status", "pending") if t.get("status") in {"pending", "in_progress", "completed"} else "pending",
|
| 52 |
+
"priority": t.get("priority", "medium") if t.get("priority") in {"high", "medium", "low"} else "medium",
|
| 53 |
+
})
|
| 54 |
+
|
| 55 |
+
with _lock:
|
| 56 |
+
_sessions[sid] = normalized
|
| 57 |
+
|
| 58 |
+
return {"success": True, "todos": normalized, "count": len(normalized)}
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def todo_update(
|
| 62 |
+
todo_id: str,
|
| 63 |
+
status: str | None = None,
|
| 64 |
+
content: str | None = None,
|
| 65 |
+
session_id: str | None = None,
|
| 66 |
+
) -> dict[str, Any]:
|
| 67 |
+
"""Update a single todo by id."""
|
| 68 |
+
sid = _get_session_id(session_id)
|
| 69 |
+
with _lock:
|
| 70 |
+
todos = _sessions.get(sid, [])
|
| 71 |
+
for t in todos:
|
| 72 |
+
if t["id"] == todo_id:
|
| 73 |
+
if status in {"pending", "in_progress", "completed"}:
|
| 74 |
+
t["status"] = status
|
| 75 |
+
if content is not None:
|
| 76 |
+
t["content"] = content
|
| 77 |
+
return {"success": True, "todo": t}
|
| 78 |
+
return {"success": False, "error": f"Todo not found: {todo_id}"}
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def todo_clear(session_id: str | None = None) -> dict[str, Any]:
|
| 82 |
+
"""Clear all todos for a session."""
|
| 83 |
+
sid = _get_session_id(session_id)
|
| 84 |
+
with _lock:
|
| 85 |
+
_sessions.pop(sid, None)
|
| 86 |
+
return {"success": True}
|
index.html
CHANGED
|
@@ -5,9 +5,9 @@
|
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
<title>SoniCoder</title>
|
| 7 |
<meta name="description" content="AI-powered fullstack app generator with local model inference">
|
| 8 |
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 9 |
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 10 |
-
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
| 11 |
<style>
|
| 12 |
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 13 |
RESET & BASE
|
|
@@ -1159,6 +1159,7 @@ body.hide-thinking .think-block { display: none; }
|
|
| 1159 |
<button class="output-tab" data-tab="console" onclick="switchTab('console')">Console</button>
|
| 1160 |
<button class="output-tab" data-tab="code" onclick="switchTab('code')">Code</button>
|
| 1161 |
<button class="output-tab" data-tab="search" onclick="switchTab('search')">Search</button>
|
|
|
|
| 1162 |
<button class="output-tab" data-tab="deploy" onclick="switchTab('deploy')">Deploy</button>
|
| 1163 |
</div>
|
| 1164 |
<div id="output-content">
|
|
@@ -1215,6 +1216,75 @@ body.hide-thinking .think-block { display: none; }
|
|
| 1215 |
</div>
|
| 1216 |
</div>
|
| 1217 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1218 |
<!-- Deploy Pane -->
|
| 1219 |
<div class="tab-pane" id="pane-deploy">
|
| 1220 |
<div class="deploy-section">
|
|
@@ -1328,6 +1398,10 @@ const state = {
|
|
| 1328 |
currentModelType: 'text',
|
| 1329 |
uploadedImageFileUrl: '',
|
| 1330 |
uploadedImageName: '',
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1331 |
};
|
| 1332 |
|
| 1333 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -1352,7 +1426,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 1352 |
renderExamples();
|
| 1353 |
|
| 1354 |
// Welcome message
|
| 1355 |
-
addSystemMessage('Welcome to SoniCoder. The model is loading
|
| 1356 |
|
| 1357 |
// Input auto-resize & keybinding
|
| 1358 |
const input = document.getElementById('chat-input');
|
|
@@ -2195,6 +2269,451 @@ function stopGeneration() {
|
|
| 2195 |
onGenerationEnd();
|
| 2196 |
}
|
| 2197 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2198 |
function resetConversation(announcement) {
|
| 2199 |
state.history = [];
|
| 2200 |
state.executionContext = {};
|
|
|
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
<title>SoniCoder</title>
|
| 7 |
<meta name="description" content="AI-powered fullstack app generator with local model inference">
|
| 8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com/">
|
| 9 |
+
<link rel="preconnect" href="https://fonts.gstatic.com/" crossorigin>
|
| 10 |
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
| 11 |
<style>
|
| 12 |
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 13 |
RESET & BASE
|
|
|
|
| 1159 |
<button class="output-tab" data-tab="console" onclick="switchTab('console')">Console</button>
|
| 1160 |
<button class="output-tab" data-tab="code" onclick="switchTab('code')">Code</button>
|
| 1161 |
<button class="output-tab" data-tab="search" onclick="switchTab('search')">Search</button>
|
| 1162 |
+
<button class="output-tab" data-tab="agent" onclick="switchTab('agent')">Agent</button>
|
| 1163 |
<button class="output-tab" data-tab="deploy" onclick="switchTab('deploy')">Deploy</button>
|
| 1164 |
</div>
|
| 1165 |
<div id="output-content">
|
|
|
|
| 1216 |
</div>
|
| 1217 |
</div>
|
| 1218 |
|
| 1219 |
+
<!-- Agent Pane (Claude Code-style) -->
|
| 1220 |
+
<div class="tab-pane" id="pane-agent">
|
| 1221 |
+
<div class="deploy-section">
|
| 1222 |
+
<div class="deploy-title">🤖 Agent Mode (Claude Code-style)</div>
|
| 1223 |
+
|
| 1224 |
+
<!-- Agent mode toggle -->
|
| 1225 |
+
<div class="deploy-field">
|
| 1226 |
+
<label>Agent Mode</label>
|
| 1227 |
+
<div style="display:flex;align-items:center;gap:12px;">
|
| 1228 |
+
<label style="display:flex;align-items:center;gap:6px;cursor:pointer;font-weight:400;">
|
| 1229 |
+
<input type="checkbox" id="agent-mode-toggle" onchange="toggleAgentMode()" style="cursor:pointer;">
|
| 1230 |
+
<span>Enable agent loop (model calls tools: read/write/edit/glob/grep/bash/todos)</span>
|
| 1231 |
+
</label>
|
| 1232 |
+
</div>
|
| 1233 |
+
<div class="deploy-hint">When ON, the model can manipulate files in the sandboxed workspace and run shell commands.</div>
|
| 1234 |
+
</div>
|
| 1235 |
+
|
| 1236 |
+
<!-- Slash Commands -->
|
| 1237 |
+
<div class="deploy-field">
|
| 1238 |
+
<label>Slash Commands</label>
|
| 1239 |
+
<div id="commands-list" style="display:grid;grid-template-columns:1fr 1fr;gap:6px;font-size:11px;"></div>
|
| 1240 |
+
<div class="deploy-hint">Type the command in chat (e.g. <code>/commit</code>, <code>/review</code>)</div>
|
| 1241 |
+
</div>
|
| 1242 |
+
|
| 1243 |
+
<!-- Skills -->
|
| 1244 |
+
<div class="deploy-field">
|
| 1245 |
+
<label>Skills</label>
|
| 1246 |
+
<div id="skills-list" style="display:grid;grid-template-columns:1fr 1fr;gap:6px;font-size:11px;"></div>
|
| 1247 |
+
<div class="deploy-hint">Click a skill to activate it for the next prompt</div>
|
| 1248 |
+
</div>
|
| 1249 |
+
|
| 1250 |
+
<!-- Active skills display -->
|
| 1251 |
+
<div class="deploy-field" id="active-skills-section" style="display:none;">
|
| 1252 |
+
<label>Active Skills</label>
|
| 1253 |
+
<div id="active-skills-display" style="display:flex;flex-wrap:wrap;gap:4px;"></div>
|
| 1254 |
+
<button onclick="clearActiveSkills()" style="margin-top:6px;font-size:10px;color:var(--red);background:none;border:1px solid var(--red);padding:2px 8px;cursor:pointer;border-radius:3px;">Clear all</button>
|
| 1255 |
+
</div>
|
| 1256 |
+
|
| 1257 |
+
<!-- Hooks -->
|
| 1258 |
+
<div class="deploy-field">
|
| 1259 |
+
<label>Hooks (active rules)</label>
|
| 1260 |
+
<div id="hooks-list" style="display:grid;grid-template-columns:1fr;gap:4px;font-size:11px;"></div>
|
| 1261 |
+
<div class="deploy-hint">Rules fire on bash/file/prompt events. Add custom rules in <code>workspace/.sonicoder/hooks/</code></div>
|
| 1262 |
+
</div>
|
| 1263 |
+
|
| 1264 |
+
<!-- Todo List -->
|
| 1265 |
+
<div class="deploy-field">
|
| 1266 |
+
<label>Todo List</label>
|
| 1267 |
+
<div id="todos-display" style="font-size:11px;max-height:200px;overflow-y:auto;">
|
| 1268 |
+
<div style="color:var(--gray-dim);">No todos yet. Use the agent to create some.</div>
|
| 1269 |
+
</div>
|
| 1270 |
+
<button onclick="refreshTodos()" style="margin-top:6px;font-size:10px;background:none;border:1px solid var(--border);padding:2px 8px;cursor:pointer;border-radius:3px;color:var(--gray-light);">Refresh</button>
|
| 1271 |
+
</div>
|
| 1272 |
+
|
| 1273 |
+
<!-- Workspace -->
|
| 1274 |
+
<div class="deploy-field">
|
| 1275 |
+
<label>Workspace Files</label>
|
| 1276 |
+
<div id="workspace-tree" style="font-size:11px;max-height:300px;overflow-y:auto;font-family:var(--font-mono);background:var(--bg-code);padding:8px;border-radius:4px;border:1px solid var(--border);">
|
| 1277 |
+
<div style="color:var(--gray-dim);">Empty. The agent will create files here.</div>
|
| 1278 |
+
</div>
|
| 1279 |
+
<div style="display:flex;gap:6px;margin-top:6px;">
|
| 1280 |
+
<button onclick="refreshWorkspace()" style="font-size:10px;background:none;border:1px solid var(--border);padding:2px 8px;cursor:pointer;border-radius:3px;color:var(--gray-light);">Refresh</button>
|
| 1281 |
+
<button onclick="resetWorkspace()" style="font-size:10px;background:none;border:1px solid var(--red);padding:2px 8px;cursor:pointer;border-radius:3px;color:var(--red);">Reset workspace</button>
|
| 1282 |
+
</div>
|
| 1283 |
+
<div class="deploy-hint">Files live in <code>./workspace/</code> β sandboxed, path-escape protected</div>
|
| 1284 |
+
</div>
|
| 1285 |
+
</div>
|
| 1286 |
+
</div>
|
| 1287 |
+
|
| 1288 |
<!-- Deploy Pane -->
|
| 1289 |
<div class="tab-pane" id="pane-deploy">
|
| 1290 |
<div class="deploy-section">
|
|
|
|
| 1398 |
currentModelType: 'text',
|
| 1399 |
uploadedImageFileUrl: '',
|
| 1400 |
uploadedImageName: '',
|
| 1401 |
+
// Agent mode (Claude Code-style)
|
| 1402 |
+
agentMode: false,
|
| 1403 |
+
activeSkills: [],
|
| 1404 |
+
todos: [],
|
| 1405 |
};
|
| 1406 |
|
| 1407 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 1426 |
renderExamples();
|
| 1427 |
|
| 1428 |
// Welcome message
|
| 1429 |
+
addSystemMessage('Welcome to SoniCoder β a Claude Code-style agent running locally. The model is loading (no API keys needed). Open the "Agent" tab to enable tool use (read/write/edit/glob/grep/bash), browse skills and slash commands, or just describe the app you want to build.');
|
| 1430 |
|
| 1431 |
// Input auto-resize & keybinding
|
| 1432 |
const input = document.getElementById('chat-input');
|
|
|
|
| 2269 |
onGenerationEnd();
|
| 2270 |
}
|
| 2271 |
|
| 2272 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2273 |
+
// AGENT MODE (Claude Code-style)
|
| 2274 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2275 |
+
|
| 2276 |
+
async function callAgentApi(name, data) {
|
| 2277 |
+
// Call a Gradio API endpoint and return an event source
|
| 2278 |
+
const resp = await fetch(`/gradio_api/call/${name}`, {
|
| 2279 |
+
method: 'POST',
|
| 2280 |
+
headers: { 'Content-Type': 'application/json' },
|
| 2281 |
+
body: JSON.stringify({ data: data }),
|
| 2282 |
+
});
|
| 2283 |
+
if (!resp.ok) throw new Error(`API ${name} failed: ${resp.status}`);
|
| 2284 |
+
const { event_id } = await resp.json();
|
| 2285 |
+
return new EventSource(`/gradio_api/call/${name}/${event_id}`);
|
| 2286 |
+
}
|
| 2287 |
+
|
| 2288 |
+
function toggleAgentMode() {
|
| 2289 |
+
state.agentMode = document.getElementById('agent-mode-toggle').checked;
|
| 2290 |
+
if (state.agentMode) {
|
| 2291 |
+
addSystemMessage('π€ Agent mode enabled. The model can now call tools (read_file, write_file, edit_file, bash, etc.) and manipulate the workspace. Slash commands like /commit, /review, /feature are also active.');
|
| 2292 |
+
// Refresh workspace + todos + skills display
|
| 2293 |
+
refreshWorkspace();
|
| 2294 |
+
refreshTodos();
|
| 2295 |
+
} else {
|
| 2296 |
+
addSystemMessage('Agent mode disabled. Back to standard chat mode.');
|
| 2297 |
+
}
|
| 2298 |
+
}
|
| 2299 |
+
|
| 2300 |
+
function renderSkillsList() {
|
| 2301 |
+
const container = document.getElementById('skills-list');
|
| 2302 |
+
if (!container) return;
|
| 2303 |
+
const skills = (CONFIG.skills || []);
|
| 2304 |
+
if (skills.length === 0) {
|
| 2305 |
+
container.innerHTML = '<div style="color:var(--gray-dim);grid-column:1/-1;">No skills available.</div>';
|
| 2306 |
+
return;
|
| 2307 |
+
}
|
| 2308 |
+
container.innerHTML = skills.map(s => {
|
| 2309 |
+
const isActive = state.activeSkills.includes(s.name);
|
| 2310 |
+
const bg = isActive ? 'var(--green-dim)' : 'var(--bg-code)';
|
| 2311 |
+
const color = isActive ? 'var(--green)' : 'var(--gray-light)';
|
| 2312 |
+
const border = isActive ? 'var(--green)' : 'var(--border)';
|
| 2313 |
+
return `<div style="padding:6px 8px;background:${bg};color:${color};border:1px solid ${border};border-radius:3px;cursor:pointer;" onclick="toggleSkill('${s.name}')">
|
| 2314 |
+
<div style="font-weight:600;">${s.name}</div>
|
| 2315 |
+
<div style="font-size:10px;color:var(--gray-mid);margin-top:2px;">${(s.description || '').slice(0, 80)}</div>
|
| 2316 |
+
</div>`;
|
| 2317 |
+
}).join('');
|
| 2318 |
+
}
|
| 2319 |
+
|
| 2320 |
+
function toggleSkill(name) {
|
| 2321 |
+
const idx = state.activeSkills.indexOf(name);
|
| 2322 |
+
if (idx >= 0) {
|
| 2323 |
+
state.activeSkills.splice(idx, 1);
|
| 2324 |
+
} else {
|
| 2325 |
+
state.activeSkills.push(name);
|
| 2326 |
+
}
|
| 2327 |
+
renderSkillsList();
|
| 2328 |
+
renderActiveSkills();
|
| 2329 |
+
}
|
| 2330 |
+
|
| 2331 |
+
function renderActiveSkills() {
|
| 2332 |
+
const section = document.getElementById('active-skills-section');
|
| 2333 |
+
const display = document.getElementById('active-skills-display');
|
| 2334 |
+
if (!section || !display) return;
|
| 2335 |
+
if (state.activeSkills.length === 0) {
|
| 2336 |
+
section.style.display = 'none';
|
| 2337 |
+
return;
|
| 2338 |
+
}
|
| 2339 |
+
section.style.display = 'block';
|
| 2340 |
+
display.innerHTML = state.activeSkills.map(name =>
|
| 2341 |
+
`<span style="background:var(--green-dim);color:var(--green);padding:2px 8px;border:1px solid var(--green);border-radius:3px;font-size:10px;">${name} <span style="cursor:pointer;margin-left:4px;" onclick="toggleSkill('${name}')">x</span></span>`
|
| 2342 |
+
).join('');
|
| 2343 |
+
}
|
| 2344 |
+
|
| 2345 |
+
function clearActiveSkills() {
|
| 2346 |
+
state.activeSkills = [];
|
| 2347 |
+
renderSkillsList();
|
| 2348 |
+
renderActiveSkills();
|
| 2349 |
+
}
|
| 2350 |
+
|
| 2351 |
+
function renderCommandsList() {
|
| 2352 |
+
const container = document.getElementById('commands-list');
|
| 2353 |
+
if (!container) return;
|
| 2354 |
+
const commands = (CONFIG.commands || []);
|
| 2355 |
+
if (commands.length === 0) {
|
| 2356 |
+
container.innerHTML = '<div style="color:var(--gray-dim);grid-column:1/-1;">No commands available.</div>';
|
| 2357 |
+
return;
|
| 2358 |
+
}
|
| 2359 |
+
container.innerHTML = commands.map(c =>
|
| 2360 |
+
`<div style="padding:6px 8px;background:var(--bg-code);border:1px solid var(--border);border-radius:3px;">
|
| 2361 |
+
<div style="font-weight:600;color:var(--cyan);">/${c.name}</div>
|
| 2362 |
+
<div style="font-size:10px;color:var(--gray-mid);margin-top:2px;">${(c.description || '').slice(0, 80)}</div>
|
| 2363 |
+
</div>`
|
| 2364 |
+
).join('');
|
| 2365 |
+
}
|
| 2366 |
+
|
| 2367 |
+
function renderHooksList() {
|
| 2368 |
+
const container = document.getElementById('hooks-list');
|
| 2369 |
+
if (!container) return;
|
| 2370 |
+
const hooks = (CONFIG.hooks || []);
|
| 2371 |
+
if (hooks.length === 0) {
|
| 2372 |
+
container.innerHTML = '<div style="color:var(--gray-dim);">No hooks configured.</div>';
|
| 2373 |
+
return;
|
| 2374 |
+
}
|
| 2375 |
+
container.innerHTML = hooks.map(h => {
|
| 2376 |
+
const enabledColor = h.enabled ? 'var(--green)' : 'var(--gray-dim)';
|
| 2377 |
+
const actionColor = h.action === 'block' ? 'var(--red)' : 'var(--amber)';
|
| 2378 |
+
return `<div style="padding:6px 8px;background:var(--bg-code);border:1px solid var(--border);border-radius:3px;">
|
| 2379 |
+
<div style="display:flex;justify-content:space-between;align-items:center;">
|
| 2380 |
+
<span style="font-weight:600;color:${enabledColor};">${h.enabled ? 'ON' : 'OFF'} ${h.name}</span>
|
| 2381 |
+
<span style="font-size:10px;color:${actionColor};">[${h.action}] ${h.event}</span>
|
| 2382 |
+
</div>
|
| 2383 |
+
<div style="font-size:10px;color:var(--gray-mid);margin-top:2px;font-family:var(--font-mono);">${(h.pattern || '').slice(0, 80)}</div>
|
| 2384 |
+
</div>`;
|
| 2385 |
+
}).join('');
|
| 2386 |
+
}
|
| 2387 |
+
|
| 2388 |
+
async function refreshTodos() {
|
| 2389 |
+
try {
|
| 2390 |
+
const es = await callAgentApi('todo_read', ['default']);
|
| 2391 |
+
es.addEventListener('complete', (e) => {
|
| 2392 |
+
try {
|
| 2393 |
+
const data = JSON.parse(e.data);
|
| 2394 |
+
const result = JSON.parse(data[0]);
|
| 2395 |
+
state.todos = result.todos || [];
|
| 2396 |
+
renderTodos();
|
| 2397 |
+
} catch (err) { console.error('Todo refresh error:', err); }
|
| 2398 |
+
es.close();
|
| 2399 |
+
});
|
| 2400 |
+
es.addEventListener('error', () => es.close());
|
| 2401 |
+
} catch (err) { console.error('refreshTodos failed:', err); }
|
| 2402 |
+
}
|
| 2403 |
+
|
| 2404 |
+
function renderTodos() {
|
| 2405 |
+
const container = document.getElementById('todos-display');
|
| 2406 |
+
if (!container) return;
|
| 2407 |
+
if (state.todos.length === 0) {
|
| 2408 |
+
container.innerHTML = '<div style="color:var(--gray-dim);">No todos yet. Use the agent to create some.</div>';
|
| 2409 |
+
return;
|
| 2410 |
+
}
|
| 2411 |
+
const statusColor = { pending: 'var(--gray-mid)', in_progress: 'var(--amber)', completed: 'var(--green)' };
|
| 2412 |
+
const statusIcon = { pending: 'β', in_progress: 'β', completed: 'β' };
|
| 2413 |
+
container.innerHTML = state.todos.map(t =>
|
| 2414 |
+
`<div style="padding:4px 0;border-bottom:1px solid var(--border);">
|
| 2415 |
+
<span style="color:${statusColor[t.status] || 'var(--gray-mid)'};">${statusIcon[t.status] || 'β'}</span>
|
| 2416 |
+
<span style="margin-left:6px;color:var(--gray-light);">[${t.priority || 'med'}] ${t.content || t.id}</span>
|
| 2417 |
+
</div>`
|
| 2418 |
+
).join('');
|
| 2419 |
+
}
|
| 2420 |
+
|
| 2421 |
+
async function refreshWorkspace() {
|
| 2422 |
+
try {
|
| 2423 |
+
const es = await callAgentApi('workspace_tree', []);
|
| 2424 |
+
es.addEventListener('complete', (e) => {
|
| 2425 |
+
try {
|
| 2426 |
+
const data = JSON.parse(e.data);
|
| 2427 |
+
const result = JSON.parse(data[0]);
|
| 2428 |
+
if (result.success) {
|
| 2429 |
+
renderWorkspaceTree(result.tree);
|
| 2430 |
+
}
|
| 2431 |
+
} catch (err) { console.error('Workspace refresh error:', err); }
|
| 2432 |
+
es.close();
|
| 2433 |
+
});
|
| 2434 |
+
es.addEventListener('error', () => es.close());
|
| 2435 |
+
} catch (err) { console.error('refreshWorkspace failed:', err); }
|
| 2436 |
+
}
|
| 2437 |
+
|
| 2438 |
+
function renderWorkspaceTree(node, depth = 0) {
|
| 2439 |
+
const container = document.getElementById('workspace-tree');
|
| 2440 |
+
if (!container) return;
|
| 2441 |
+
if (depth === 0) {
|
| 2442 |
+
if (!node || (node.children || []).length === 0) {
|
| 2443 |
+
container.innerHTML = '<div style="color:var(--gray-dim);">Empty. The agent will create files here.</div>';
|
| 2444 |
+
return;
|
| 2445 |
+
}
|
| 2446 |
+
container.innerHTML = renderWorkspaceNode(node, 0);
|
| 2447 |
+
}
|
| 2448 |
+
}
|
| 2449 |
+
|
| 2450 |
+
function renderWorkspaceNode(node, depth) {
|
| 2451 |
+
const indent = ' '.repeat(depth * 2);
|
| 2452 |
+
if (node.type === 'dir') {
|
| 2453 |
+
const children = (node.children || []).map(c => renderWorkspaceNode(c, depth + 1)).join('');
|
| 2454 |
+
return `<div>${indent}<span style="color:var(--cyan);">π ${node.name}</span></div>${children}`;
|
| 2455 |
+
} else {
|
| 2456 |
+
const size = node.size ? `${(node.size / 1024).toFixed(1)}KB` : '';
|
| 2457 |
+
return `<div>${indent}<span style="color:var(--gray-light);">π ${node.name}</span> <span style="color:var(--gray-dim);font-size:10px;">${size}</span></div>`;
|
| 2458 |
+
}
|
| 2459 |
+
}
|
| 2460 |
+
|
| 2461 |
+
async function resetWorkspace() {
|
| 2462 |
+
if (!confirm('Reset the workspace? All files will be deleted.')) return;
|
| 2463 |
+
try {
|
| 2464 |
+
const es = await callAgentApi('workspace_reset', []);
|
| 2465 |
+
es.addEventListener('complete', (e) => {
|
| 2466 |
+
try {
|
| 2467 |
+
const data = JSON.parse(e.data);
|
| 2468 |
+
const result = JSON.parse(data[0]);
|
| 2469 |
+
addSystemMessage('Workspace cleared.');
|
| 2470 |
+
refreshWorkspace();
|
| 2471 |
+
} catch (err) { console.error(err); }
|
| 2472 |
+
es.close();
|
| 2473 |
+
});
|
| 2474 |
+
es.addEventListener('error', () => es.close());
|
| 2475 |
+
} catch (err) { console.error('resetWorkspace failed:', err); }
|
| 2476 |
+
}
|
| 2477 |
+
|
| 2478 |
+
// Override sendMessage to support agent mode
|
| 2479 |
+
const originalSendMessage = sendMessage;
|
| 2480 |
+
|
| 2481 |
+
async function sendMessageAgent(prompt) {
|
| 2482 |
+
if (state.isGenerating) return;
|
| 2483 |
+
if (!state.modelReady) {
|
| 2484 |
+
addSystemMessage('The model is still loading. Please wait...');
|
| 2485 |
+
return;
|
| 2486 |
+
}
|
| 2487 |
+
|
| 2488 |
+
state.isGenerating = true;
|
| 2489 |
+
toggleInputState(true);
|
| 2490 |
+
addUserMessage(prompt);
|
| 2491 |
+
addAssistantMessage();
|
| 2492 |
+
renderStatus('Running agent...', 'working');
|
| 2493 |
+
|
| 2494 |
+
const historyJSON = JSON.stringify(state.history.slice(0, -2)); // exclude just-added user+assistant placeholders
|
| 2495 |
+
const framework = document.getElementById('framework-select')?.value || state.targetFramework;
|
| 2496 |
+
const skillsJSON = JSON.stringify(state.activeSkills || []);
|
| 2497 |
+
|
| 2498 |
+
try {
|
| 2499 |
+
const resp = await fetch('/gradio_api/call/agent_run', {
|
| 2500 |
+
method: 'POST',
|
| 2501 |
+
headers: { 'Content-Type': 'application/json' },
|
| 2502 |
+
body: JSON.stringify({
|
| 2503 |
+
data: [prompt, state.targetLanguage, framework, historyJSON, skillsJSON, state.searchEnabled ? 'true' : 'false', state.uploadedImageFileUrl || '']
|
| 2504 |
+
})
|
| 2505 |
+
});
|
| 2506 |
+
|
| 2507 |
+
if (!resp.ok) throw new Error(`API error: ${resp.status} ${resp.statusText}`);
|
| 2508 |
+
|
| 2509 |
+
const { event_id } = await resp.json();
|
| 2510 |
+
const eventSource = new EventSource(`/gradio_api/call/agent_run/${event_id}`);
|
| 2511 |
+
state.currentEventSource = eventSource;
|
| 2512 |
+
|
| 2513 |
+
let lastContent = '';
|
| 2514 |
+
|
| 2515 |
+
eventSource.addEventListener('generating', (e) => {
|
| 2516 |
+
try {
|
| 2517 |
+
const dataArray = JSON.parse(e.data);
|
| 2518 |
+
const event = JSON.parse(dataArray[0]);
|
| 2519 |
+
handleAgentEvent(event);
|
| 2520 |
+
} catch (err) { console.error('Parse error (generating):', err); }
|
| 2521 |
+
});
|
| 2522 |
+
|
| 2523 |
+
eventSource.addEventListener('complete', (e) => {
|
| 2524 |
+
try {
|
| 2525 |
+
const dataArray = JSON.parse(e.data);
|
| 2526 |
+
const event = JSON.parse(dataArray[0]);
|
| 2527 |
+
handleAgentEvent(event);
|
| 2528 |
+
} catch (err) { console.error('Parse error (complete):', err); }
|
| 2529 |
+
eventSource.close();
|
| 2530 |
+
onGenerationEnd();
|
| 2531 |
+
// Refresh workspace + todos after agent run
|
| 2532 |
+
refreshWorkspace();
|
| 2533 |
+
refreshTodos();
|
| 2534 |
+
});
|
| 2535 |
+
|
| 2536 |
+
eventSource.addEventListener('error', (e) => {
|
| 2537 |
+
let errorMsg = 'Agent error.';
|
| 2538 |
+
if (e.data) errorMsg = e.data;
|
| 2539 |
+
console.error('SSE error:', errorMsg);
|
| 2540 |
+
finalizeAssistantMessage();
|
| 2541 |
+
addSystemMessage(`Error: ${errorMsg}`);
|
| 2542 |
+
renderStatus('Error', 'error');
|
| 2543 |
+
eventSource.close();
|
| 2544 |
+
onGenerationEnd();
|
| 2545 |
+
});
|
| 2546 |
+
|
| 2547 |
+
} catch (err) {
|
| 2548 |
+
console.error('Agent send error:', err);
|
| 2549 |
+
finalizeAssistantMessage();
|
| 2550 |
+
addSystemMessage(`Error: ${err.message}`);
|
| 2551 |
+
renderStatus('Error', 'error');
|
| 2552 |
+
onGenerationEnd();
|
| 2553 |
+
}
|
| 2554 |
+
}
|
| 2555 |
+
|
| 2556 |
+
function handleAgentEvent(event) {
|
| 2557 |
+
if (!event || typeof event !== 'object') return;
|
| 2558 |
+
|
| 2559 |
+
switch (event.type) {
|
| 2560 |
+
case 'status':
|
| 2561 |
+
renderStatus(event.status_text || 'Working...', event.status_state || 'working');
|
| 2562 |
+
break;
|
| 2563 |
+
|
| 2564 |
+
case 'streaming':
|
| 2565 |
+
// Update the current assistant message with streaming content
|
| 2566 |
+
const msgs = document.querySelectorAll('.chat-message.assistant .message-content');
|
| 2567 |
+
const last = msgs[msgs.length - 1];
|
| 2568 |
+
if (last) {
|
| 2569 |
+
last.innerHTML = window.renderMarkdown ? renderMarkdown(event.content || '') : (event.content || '').replace(/\n/g, '<br>');
|
| 2570 |
+
}
|
| 2571 |
+
// Update history
|
| 2572 |
+
if (state.history.length > 0 && state.history[state.history.length - 1].role === 'assistant') {
|
| 2573 |
+
state.history[state.history.length - 1].content = event.content || '';
|
| 2574 |
+
}
|
| 2575 |
+
renderStatus(`Thinking (step ${event.iteration || 1})...`, 'working');
|
| 2576 |
+
break;
|
| 2577 |
+
|
| 2578 |
+
case 'tool_call':
|
| 2579 |
+
addSystemMessage(`π§ Calling tool: ${event.tool}` + (event.args && Object.keys(event.args).length ? `(${JSON.stringify(event.args).slice(0, 200)})` : ''));
|
| 2580 |
+
renderStatus(`Running tool: ${event.tool}...`, 'working');
|
| 2581 |
+
break;
|
| 2582 |
+
|
| 2583 |
+
case 'tool_result':
|
| 2584 |
+
const result = event.result || {};
|
| 2585 |
+
const success = result.success !== false;
|
| 2586 |
+
const summary = success ? 'β' : 'β';
|
| 2587 |
+
let resultPreview = '';
|
| 2588 |
+
if (result.stdout) resultPreview = result.stdout.slice(0, 200);
|
| 2589 |
+
else if (result.content) resultPreview = result.content.slice(0, 200);
|
| 2590 |
+
else if (result.error) resultPreview = result.error;
|
| 2591 |
+
else if (result.entries) resultPreview = `${result.entries.length} entries`;
|
| 2592 |
+
else if (result.matches) resultPreview = `${result.matches.length} matches`;
|
| 2593 |
+
else if (result.count !== undefined) resultPreview = `${result.count} items`;
|
| 2594 |
+
else resultPreview = JSON.stringify(result).slice(0, 200);
|
| 2595 |
+
|
| 2596 |
+
// Show hook warnings if any
|
| 2597 |
+
if (result.hook_warnings && result.hook_warnings.length > 0) {
|
| 2598 |
+
for (const w of result.hook_warnings) {
|
| 2599 |
+
addSystemMessage(`β οΈ Hook: ${w.slice(0, 300)}`);
|
| 2600 |
+
}
|
| 2601 |
+
}
|
| 2602 |
+
|
| 2603 |
+
addSystemMessage(`${summary} ${event.tool}: ${resultPreview}${resultPreview.length >= 200 ? '...' : ''}`);
|
| 2604 |
+
break;
|
| 2605 |
+
|
| 2606 |
+
case 'search_results':
|
| 2607 |
+
state.currentSearchResults = event.results || [];
|
| 2608 |
+
renderStatus(`Found ${event.results?.length || 0} results, running agent...`, 'working');
|
| 2609 |
+
break;
|
| 2610 |
+
|
| 2611 |
+
case 'complete':
|
| 2612 |
+
// Finalize the assistant message
|
| 2613 |
+
const content = event.content || '';
|
| 2614 |
+
if (state.history.length > 0 && state.history[state.history.length - 1].role === 'assistant') {
|
| 2615 |
+
state.history[state.history.length - 1].content = content;
|
| 2616 |
+
}
|
| 2617 |
+
finalizeAssistantMessage();
|
| 2618 |
+
|
| 2619 |
+
// Try to extract code from the response
|
| 2620 |
+
tryExtractCodeFromResponse(content);
|
| 2621 |
+
|
| 2622 |
+
renderStatus('Done', 'success');
|
| 2623 |
+
break;
|
| 2624 |
+
|
| 2625 |
+
case 'error':
|
| 2626 |
+
finalizeAssistantMessage();
|
| 2627 |
+
addSystemMessage(`Error: ${event.message || 'Unknown error'}`);
|
| 2628 |
+
if (event.available && event.available.length) {
|
| 2629 |
+
addSystemMessage('Available: ' + event.available.join(', '));
|
| 2630 |
+
}
|
| 2631 |
+
renderStatus('Error', 'error');
|
| 2632 |
+
break;
|
| 2633 |
+
}
|
| 2634 |
+
}
|
| 2635 |
+
|
| 2636 |
+
function tryExtractCodeFromResponse(content) {
|
| 2637 |
+
// Reuse the existing code extraction logic by simulating a chat payload
|
| 2638 |
+
if (!content) return;
|
| 2639 |
+
// Strip tool call blocks for display
|
| 2640 |
+
const cleanContent = content.replace(/```tool\s*\n.*?```/gs, '').trim();
|
| 2641 |
+
if (!cleanContent) return;
|
| 2642 |
+
|
| 2643 |
+
// Try to extract code blocks
|
| 2644 |
+
const codeMatch = cleanContent.match(/```([a-zA-Z0-9_+.#-]*)\s*\n(.*?)```/s);
|
| 2645 |
+
if (codeMatch) {
|
| 2646 |
+
const code = codeMatch[2].trim();
|
| 2647 |
+
const lang = codeMatch[1].toLowerCase();
|
| 2648 |
+
state.lastCode = code;
|
| 2649 |
+
state.lastCodeLang = lang;
|
| 2650 |
+
// Update code tab
|
| 2651 |
+
const codeDisplay = document.getElementById('code-display');
|
| 2652 |
+
if (codeDisplay) {
|
| 2653 |
+
codeDisplay.innerHTML = `<pre><code class="language-${lang}">${escapeHtml(code)}</code></pre>`;
|
| 2654 |
+
}
|
| 2655 |
+
document.getElementById('code-tab-lang').textContent = lang || 'text';
|
| 2656 |
+
document.getElementById('btn-download').style.display = 'inline-block';
|
| 2657 |
+
|
| 2658 |
+
// If HTML, show in preview
|
| 2659 |
+
if (lang === 'html' || /^<!doctype|<html/i.test(code)) {
|
| 2660 |
+
const iframe = document.getElementById('preview-iframe');
|
| 2661 |
+
if (iframe) {
|
| 2662 |
+
iframe.srcdoc = code;
|
| 2663 |
+
document.getElementById('preview-placeholder').style.display = 'none';
|
| 2664 |
+
document.getElementById('preview-image').style.display = 'none';
|
| 2665 |
+
iframe.style.display = 'block';
|
| 2666 |
+
}
|
| 2667 |
+
}
|
| 2668 |
+
}
|
| 2669 |
+
|
| 2670 |
+
// Try multi-file extraction
|
| 2671 |
+
const fileMatch = cleanContent.match(/@@FILE:\s*(.+?)@@\s*\n(.*?)(?=@@FILE:|@@END@@)/s);
|
| 2672 |
+
if (fileMatch) {
|
| 2673 |
+
// Build project files
|
| 2674 |
+
const files = {};
|
| 2675 |
+
const fileRegex = /@@FILE:\s*(.+?)@@\s*\n(.*?)(?=@@FILE:|@@END@@)/gs;
|
| 2676 |
+
let m;
|
| 2677 |
+
while ((m = fileRegex.exec(cleanContent)) !== null) {
|
| 2678 |
+
files[m[1].trim()] = m[2].trim();
|
| 2679 |
+
}
|
| 2680 |
+
if (Object.keys(files).length > 0) {
|
| 2681 |
+
state.executionContext = state.executionContext || {};
|
| 2682 |
+
state.executionContext.project_files = files;
|
| 2683 |
+
// Update project files display in deploy tab
|
| 2684 |
+
const pf = document.getElementById('project-files');
|
| 2685 |
+
if (pf) {
|
| 2686 |
+
pf.innerHTML = '<div style="font-size:11px;color:var(--gray-mid);margin-top:8px;">' +
|
| 2687 |
+
Object.keys(files).map(f => `<div>π ${f}</div>`).join('') +
|
| 2688 |
+
'</div>';
|
| 2689 |
+
}
|
| 2690 |
+
}
|
| 2691 |
+
}
|
| 2692 |
+
}
|
| 2693 |
+
|
| 2694 |
+
function escapeHtml(s) {
|
| 2695 |
+
return String(s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
| 2696 |
+
}
|
| 2697 |
+
|
| 2698 |
+
// Override sendMessage to route to agent when agentMode is on
|
| 2699 |
+
sendMessage = async function(prompt) {
|
| 2700 |
+
if (state.agentMode) {
|
| 2701 |
+
return sendMessageAgent(prompt);
|
| 2702 |
+
}
|
| 2703 |
+
return originalSendMessage(prompt);
|
| 2704 |
+
};
|
| 2705 |
+
|
| 2706 |
+
// Initialize agent UI on page load
|
| 2707 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 2708 |
+
// Render skills/commands/hooks after CONFIG is loaded
|
| 2709 |
+
setTimeout(() => {
|
| 2710 |
+
renderSkillsList();
|
| 2711 |
+
renderCommandsList();
|
| 2712 |
+
renderHooksList();
|
| 2713 |
+
renderActiveSkills();
|
| 2714 |
+
}, 100);
|
| 2715 |
+
});
|
| 2716 |
+
|
| 2717 |
function resetConversation(announcement) {
|
| 2718 |
state.history = [];
|
| 2719 |
state.executionContext = {};
|
requirements.txt
CHANGED
|
@@ -8,3 +8,5 @@ requests>=2.31.0
|
|
| 8 |
beautifulsoup4>=4.12.0
|
| 9 |
Pillow>=10.0
|
| 10 |
torchvision>=0.16.0
|
|
|
|
|
|
|
|
|
| 8 |
beautifulsoup4>=4.12.0
|
| 9 |
Pillow>=10.0
|
| 10 |
torchvision>=0.16.0
|
| 11 |
+
# New deps for agent features (most are stdlib in 3.11+)
|
| 12 |
+
# (No new external deps required β agent/skills/hooks/commands use stdlib only)
|