mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-02-04 16:03:08 +00:00
Compare commits
11 Commits
1607fc8175
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
326f38b3c4 | ||
|
|
6d15d020ec | ||
|
|
196038fa26 | ||
|
|
4549840330 | ||
|
|
451a5a9d05 | ||
|
|
c2ad993e75 | ||
|
|
f6510b4dd8 | ||
|
|
2507bfd5f0 | ||
|
|
f32e7efda6 | ||
|
|
b2ab1ecc7a | ||
|
|
016eead8b4 |
@@ -97,7 +97,7 @@ Fix ALL issues before considering the implementation complete. Never leave linti
|
||||
|
||||
## Project-Specific Context
|
||||
|
||||
For this project (autocoder):
|
||||
For this project (autoforge):
|
||||
- **Python Backend**: Uses SQLAlchemy, FastAPI, follows patterns in `api/`, `mcp_server/`
|
||||
- **React UI**: Uses React 18, TypeScript, TanStack Query, Tailwind CSS v4, Radix UI
|
||||
- **Design System**: Neobrutalism style with specific color tokens and animations
|
||||
|
||||
@@ -8,7 +8,7 @@ This command **requires** the project directory as an argument via `$ARGUMENTS`.
|
||||
|
||||
**Example:** `/create-spec generations/my-app`
|
||||
|
||||
**Output location:** `$ARGUMENTS/.autocoder/prompts/app_spec.txt` and `$ARGUMENTS/.autocoder/prompts/initializer_prompt.md`
|
||||
**Output location:** `$ARGUMENTS/.autoforge/prompts/app_spec.txt` and `$ARGUMENTS/.autoforge/prompts/initializer_prompt.md`
|
||||
|
||||
If `$ARGUMENTS` is empty, inform the user they must provide a project path and exit.
|
||||
|
||||
@@ -347,13 +347,13 @@ First ask in conversation if they want to make changes.
|
||||
|
||||
## Output Directory
|
||||
|
||||
The output directory is: `$ARGUMENTS/.autocoder/prompts/`
|
||||
The output directory is: `$ARGUMENTS/.autoforge/prompts/`
|
||||
|
||||
Once the user approves, generate these files:
|
||||
|
||||
## 1. Generate `app_spec.txt`
|
||||
|
||||
**Output path:** `$ARGUMENTS/.autocoder/prompts/app_spec.txt`
|
||||
**Output path:** `$ARGUMENTS/.autoforge/prompts/app_spec.txt`
|
||||
|
||||
Create a new file using this XML structure:
|
||||
|
||||
@@ -489,7 +489,7 @@ Create a new file using this XML structure:
|
||||
|
||||
## 2. Update `initializer_prompt.md`
|
||||
|
||||
**Output path:** `$ARGUMENTS/.autocoder/prompts/initializer_prompt.md`
|
||||
**Output path:** `$ARGUMENTS/.autoforge/prompts/initializer_prompt.md`
|
||||
|
||||
If the output directory has an existing `initializer_prompt.md`, read it and update the feature count.
|
||||
If not, copy from `.claude/templates/initializer_prompt.template.md` first, then update.
|
||||
@@ -512,7 +512,7 @@ After: **CRITICAL:** You must create exactly **25** features using the `feature
|
||||
|
||||
## 3. Write Status File (REQUIRED - Do This Last)
|
||||
|
||||
**Output path:** `$ARGUMENTS/.autocoder/prompts/.spec_status.json`
|
||||
**Output path:** `$ARGUMENTS/.autoforge/prompts/.spec_status.json`
|
||||
|
||||
**CRITICAL:** After you have completed ALL requested file changes, write this status file to signal completion to the UI. This is required for the "Continue to Project" button to appear.
|
||||
|
||||
@@ -524,8 +524,8 @@ Write this JSON file:
|
||||
"version": 1,
|
||||
"timestamp": "[current ISO 8601 timestamp, e.g., 2025-01-15T14:30:00.000Z]",
|
||||
"files_written": [
|
||||
".autocoder/prompts/app_spec.txt",
|
||||
".autocoder/prompts/initializer_prompt.md"
|
||||
".autoforge/prompts/app_spec.txt",
|
||||
".autoforge/prompts/initializer_prompt.md"
|
||||
],
|
||||
"feature_count": [the feature count from Phase 4L]
|
||||
}
|
||||
@@ -539,9 +539,9 @@ Write this JSON file:
|
||||
"version": 1,
|
||||
"timestamp": "2025-01-15T14:30:00.000Z",
|
||||
"files_written": [
|
||||
".autocoder/prompts/app_spec.txt",
|
||||
".autocoder/prompts/initializer_prompt.md",
|
||||
".autocoder/prompts/coding_prompt.md"
|
||||
".autoforge/prompts/app_spec.txt",
|
||||
".autoforge/prompts/initializer_prompt.md",
|
||||
".autoforge/prompts/coding_prompt.md"
|
||||
],
|
||||
"feature_count": 35
|
||||
}
|
||||
@@ -559,11 +559,11 @@ Write this JSON file:
|
||||
|
||||
Once files are generated, tell the user what to do next:
|
||||
|
||||
> "Your specification files have been created in `$ARGUMENTS/.autocoder/prompts/`!
|
||||
> "Your specification files have been created in `$ARGUMENTS/.autoforge/prompts/`!
|
||||
>
|
||||
> **Files created:**
|
||||
> - `$ARGUMENTS/.autocoder/prompts/app_spec.txt`
|
||||
> - `$ARGUMENTS/.autocoder/prompts/initializer_prompt.md`
|
||||
> - `$ARGUMENTS/.autoforge/prompts/app_spec.txt`
|
||||
> - `$ARGUMENTS/.autoforge/prompts/initializer_prompt.md`
|
||||
>
|
||||
> The **Continue to Project** button should now appear. Click it to start the autonomous coding agent!
|
||||
>
|
||||
|
||||
@@ -42,7 +42,7 @@ You are the **Project Expansion Assistant** - an expert at understanding existin
|
||||
# FIRST: Read and Understand Existing Project
|
||||
|
||||
**Step 1:** Read the existing specification:
|
||||
- Read `$ARGUMENTS/.autocoder/prompts/app_spec.txt`
|
||||
- Read `$ARGUMENTS/.autoforge/prompts/app_spec.txt`
|
||||
|
||||
**Step 2:** Present a summary to the user:
|
||||
|
||||
@@ -231,4 +231,4 @@ If they want to add more, go back to Phase 1.
|
||||
|
||||
# BEGIN
|
||||
|
||||
Start by reading the app specification file at `$ARGUMENTS/.autocoder/prompts/app_spec.txt`, then greet the user with a summary of their existing project and ask what they want to add.
|
||||
Start by reading the app specification file at `$ARGUMENTS/.autoforge/prompts/app_spec.txt`, then greet the user with a summary of their existing project and ask what they want to add.
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
allowed-tools: Read, Write, Bash, Glob, Grep
|
||||
description: Convert GSD codebase mapping to Autocoder app_spec.txt
|
||||
---
|
||||
|
||||
# GSD to Autocoder Spec
|
||||
|
||||
Convert `.planning/codebase/*.md` (from `/gsd:map-codebase`) to Autocoder's `.autocoder/prompts/app_spec.txt`.
|
||||
|
||||
@.claude/skills/gsd-to-autocoder-spec/SKILL.md
|
||||
10
.claude/commands/gsd-to-autoforge-spec.md
Normal file
10
.claude/commands/gsd-to-autoforge-spec.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
allowed-tools: Read, Write, Bash, Glob, Grep
|
||||
description: Convert GSD codebase mapping to AutoForge app_spec.txt
|
||||
---
|
||||
|
||||
# GSD to AutoForge Spec
|
||||
|
||||
Convert `.planning/codebase/*.md` (from `/gsd:map-codebase`) to AutoForge's `.autoforge/prompts/app_spec.txt`.
|
||||
|
||||
@.claude/skills/gsd-to-autoforge-spec/SKILL.md
|
||||
@@ -1,21 +1,21 @@
|
||||
---
|
||||
name: gsd-to-autocoder-spec
|
||||
name: gsd-to-autoforge-spec
|
||||
description: |
|
||||
Convert GSD codebase mapping to Autocoder app_spec.txt. This skill should be used when
|
||||
the user has run /gsd:map-codebase and wants to use Autocoder on an existing project.
|
||||
Triggers: "convert to autocoder", "gsd to spec", "create app_spec from codebase",
|
||||
"use autocoder on existing project", after /gsd:map-codebase completion.
|
||||
Convert GSD codebase mapping to AutoForge app_spec.txt. This skill should be used when
|
||||
the user has run /gsd:map-codebase and wants to use AutoForge on an existing project.
|
||||
Triggers: "convert to autoforge", "gsd to spec", "create app_spec from codebase",
|
||||
"use autoforge on existing project", after /gsd:map-codebase completion.
|
||||
---
|
||||
|
||||
# GSD to Autocoder Spec Converter
|
||||
# GSD to AutoForge Spec Converter
|
||||
|
||||
Converts `.planning/codebase/*.md` (GSD mapping output) to `.autocoder/prompts/app_spec.txt` (Autocoder format).
|
||||
Converts `.planning/codebase/*.md` (GSD mapping output) to `.autoforge/prompts/app_spec.txt` (AutoForge format).
|
||||
|
||||
## When to Use
|
||||
|
||||
- After running `/gsd:map-codebase` on an existing project
|
||||
- When onboarding an existing codebase to Autocoder
|
||||
- User wants Autocoder to continue development on existing code
|
||||
- When onboarding an existing codebase to AutoForge
|
||||
- User wants AutoForge to continue development on existing code
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -84,12 +84,12 @@ Extract:
|
||||
|
||||
Create `prompts/` directory:
|
||||
```bash
|
||||
mkdir -p .autocoder/prompts
|
||||
mkdir -p .autoforge/prompts
|
||||
```
|
||||
|
||||
**Mapping GSD Documents to Autocoder Spec:**
|
||||
**Mapping GSD Documents to AutoForge Spec:**
|
||||
|
||||
| GSD Source | Autocoder Target |
|
||||
| GSD Source | AutoForge Target |
|
||||
|------------|------------------|
|
||||
| STACK.md Languages | `<technology_stack>` |
|
||||
| STACK.md Frameworks | `<frontend>`, `<backend>` |
|
||||
@@ -114,7 +114,7 @@ mkdir -p .autocoder/prompts
|
||||
**Write the spec file** using the XML format from [references/app-spec-format.md](references/app-spec-format.md):
|
||||
|
||||
```bash
|
||||
cat > .autocoder/prompts/app_spec.txt << 'EOF'
|
||||
cat > .autoforge/prompts/app_spec.txt << 'EOF'
|
||||
<project_specification>
|
||||
<project_name>{from package.json or directory}</project_name>
|
||||
|
||||
@@ -173,9 +173,9 @@ EOF
|
||||
### Step 5: Verify Generated Spec
|
||||
|
||||
```bash
|
||||
head -100 .autocoder/prompts/app_spec.txt
|
||||
head -100 .autoforge/prompts/app_spec.txt
|
||||
echo "---"
|
||||
grep -c "User can\|System\|API\|Feature" .autocoder/prompts/app_spec.txt || echo "0"
|
||||
grep -c "User can\|System\|API\|Feature" .autoforge/prompts/app_spec.txt || echo "0"
|
||||
```
|
||||
|
||||
**Validation checklist:**
|
||||
@@ -194,15 +194,15 @@ Output:
|
||||
app_spec.txt generated from GSD codebase mapping.
|
||||
|
||||
Source: .planning/codebase/*.md
|
||||
Output: .autocoder/prompts/app_spec.txt
|
||||
Output: .autoforge/prompts/app_spec.txt
|
||||
|
||||
Next: Start Autocoder
|
||||
Next: Start AutoForge
|
||||
|
||||
cd {project_dir}
|
||||
python ~/projects/autocoder/start.py
|
||||
python ~/projects/autoforge/start.py
|
||||
|
||||
Or via UI:
|
||||
~/projects/autocoder/start_ui.sh
|
||||
~/projects/autoforge/start_ui.sh
|
||||
|
||||
The Initializer will create features.db from this spec.
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
# Autocoder app_spec.txt XML Format
|
||||
# AutoForge app_spec.txt XML Format
|
||||
|
||||
Complete reference for the XML structure expected by Autocoder's Initializer agent.
|
||||
Complete reference for the XML structure expected by AutoForge's Initializer agent.
|
||||
|
||||
## Root Structure
|
||||
|
||||
@@ -275,7 +275,7 @@ The Initializer agent expects features distributed across categories:
|
||||
| Medium web app | 200-250 | 10-15 |
|
||||
| Complex full-stack | 300-400 | 15-20 |
|
||||
|
||||
## GSD to Autocoder Mapping
|
||||
## GSD to AutoForge Mapping
|
||||
|
||||
When converting from GSD codebase mapping:
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
# GLM/Alternative API Configuration (Optional)
|
||||
# To use Zhipu AI's GLM models instead of Claude, uncomment and set these variables.
|
||||
# This only affects AutoCoder - your global Claude Code settings remain unchanged.
|
||||
# This only affects AutoForge - your global Claude Code settings remain unchanged.
|
||||
# Get an API key at: https://z.ai/subscribe
|
||||
#
|
||||
# ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,6 +2,7 @@
|
||||
generations/
|
||||
automaker/
|
||||
temp/
|
||||
temp-docs/
|
||||
|
||||
nul
|
||||
issues/
|
||||
@@ -114,6 +115,7 @@ Desktop.ini
|
||||
ui/dist/
|
||||
ui/.vite/
|
||||
.vite/
|
||||
*.tgz
|
||||
|
||||
# ===================
|
||||
# Environment files
|
||||
|
||||
32
.npmignore
Normal file
32
.npmignore
Normal file
@@ -0,0 +1,32 @@
|
||||
venv/
|
||||
**/__pycache__/
|
||||
**/*.pyc
|
||||
.git/
|
||||
.github/
|
||||
node_modules/
|
||||
test_*.py
|
||||
tests/
|
||||
generations/
|
||||
*.db
|
||||
.env
|
||||
requirements.txt
|
||||
CLAUDE.md
|
||||
LICENSE.md
|
||||
README.md
|
||||
ui/src/
|
||||
ui/node_modules/
|
||||
ui/tsconfig*.json
|
||||
ui/vite.config.ts
|
||||
ui/eslint.config.js
|
||||
ui/index.html
|
||||
ui/public/
|
||||
ui/playwright.config.ts
|
||||
ui/tests/
|
||||
start.bat
|
||||
start_ui.bat
|
||||
start.sh
|
||||
start_ui.sh
|
||||
start_ui.py
|
||||
.claude/agents/
|
||||
.claude/skills/
|
||||
.claude/settings.json
|
||||
234
CLAUDE.md
234
CLAUDE.md
@@ -17,18 +17,28 @@ This is an autonomous coding agent system with a React-based UI. It uses the Cla
|
||||
|
||||
## Commands
|
||||
|
||||
### Quick Start (Recommended)
|
||||
### npm Global Install (Recommended)
|
||||
|
||||
```bash
|
||||
# Windows - launches CLI menu
|
||||
start.bat
|
||||
npm install -g autoforge-ai
|
||||
autoforge # Start server (first run sets up Python venv)
|
||||
autoforge config # Edit ~/.autoforge/.env in $EDITOR
|
||||
autoforge config --show # Print active configuration
|
||||
autoforge --port 9999 # Custom port
|
||||
autoforge --no-browser # Don't auto-open browser
|
||||
autoforge --repair # Delete and recreate ~/.autoforge/venv/
|
||||
```
|
||||
|
||||
# macOS/Linux
|
||||
./start.sh
|
||||
### From Source (Development)
|
||||
|
||||
```bash
|
||||
# Launch Web UI (serves pre-built React app)
|
||||
start_ui.bat # Windows
|
||||
./start_ui.sh # macOS/Linux
|
||||
|
||||
# CLI menu
|
||||
start.bat # Windows
|
||||
./start.sh # macOS/Linux
|
||||
```
|
||||
|
||||
### Python Backend (Manual)
|
||||
@@ -54,6 +64,12 @@ python autonomous_agent_demo.py --project-dir my-app --yolo
|
||||
|
||||
# Parallel mode: run multiple agents concurrently (1-5 agents)
|
||||
python autonomous_agent_demo.py --project-dir my-app --parallel --max-concurrency 3
|
||||
|
||||
# Batch mode: implement multiple features per agent session (1-3)
|
||||
python autonomous_agent_demo.py --project-dir my-app --batch-size 3
|
||||
|
||||
# Batch specific features by ID
|
||||
python autonomous_agent_demo.py --project-dir my-app --batch-features 1,2,3
|
||||
```
|
||||
|
||||
### YOLO Mode (Rapid Prototyping)
|
||||
@@ -68,7 +84,7 @@ python autonomous_agent_demo.py --project-dir my-app --yolo
|
||||
```
|
||||
|
||||
**What's different in YOLO mode:**
|
||||
- No regression testing (skips `feature_get_for_regression`)
|
||||
- No regression testing
|
||||
- No Playwright MCP server (browser automation disabled)
|
||||
- Features marked passing after lint/type-check succeeds
|
||||
- Faster iteration for prototyping
|
||||
@@ -97,10 +113,13 @@ npm run lint # Run ESLint
|
||||
### Python
|
||||
|
||||
```bash
|
||||
ruff check . # Lint
|
||||
mypy . # Type check
|
||||
python test_security.py # Security unit tests (163 tests)
|
||||
python test_security_integration.py # Integration tests (9 tests)
|
||||
ruff check . # Lint
|
||||
mypy . # Type check
|
||||
python test_security.py # Security unit tests (12 tests)
|
||||
python test_security_integration.py # Integration tests (9 tests)
|
||||
python -m pytest test_client.py # Client tests (20 tests)
|
||||
python -m pytest test_dependency_resolver.py # Dependency resolver tests (12 tests)
|
||||
python -m pytest test_rate_limit_utils.py # Rate limit tests (22 tests)
|
||||
```
|
||||
|
||||
### React UI
|
||||
@@ -108,11 +127,17 @@ python test_security_integration.py # Integration tests (9 tests)
|
||||
```bash
|
||||
cd ui
|
||||
npm run lint # ESLint
|
||||
npm run build # Type check + build
|
||||
npm run build # Type check + build (Vite 7)
|
||||
npm run test:e2e # Playwright end-to-end tests
|
||||
npm run test:e2e:ui # Playwright tests with UI
|
||||
```
|
||||
|
||||
### CI/CD
|
||||
|
||||
GitHub Actions (`.github/workflows/ci.yml`) runs on push/PR to master:
|
||||
- **Python job**: ruff lint + security tests
|
||||
- **UI job**: ESLint + TypeScript build
|
||||
|
||||
### Code Quality
|
||||
|
||||
Configuration in `pyproject.toml`:
|
||||
@@ -121,24 +146,40 @@ Configuration in `pyproject.toml`:
|
||||
|
||||
## Architecture
|
||||
|
||||
### npm CLI (bin/, lib/)
|
||||
|
||||
The `autoforge` command is a Node.js wrapper that manages the Python environment and server lifecycle:
|
||||
- `bin/autoforge.js` - Entry point (shebang script)
|
||||
- `lib/cli.js` - Main CLI logic: Python 3.11+ detection (cross-platform), venv management at `~/.autoforge/venv/` with composite marker (requirements hash + Python version), `.env` config loading from `~/.autoforge/.env`, uvicorn server startup with PID file, and signal handling
|
||||
- `package.json` - npm package config (`autoforge-ai` on npm), `files` whitelist with `__pycache__` exclusions, `prepublishOnly` builds the UI
|
||||
- `requirements-prod.txt` - Runtime-only Python deps (excludes ruff, mypy, pytest)
|
||||
- `.npmignore` - Excludes dev files, tests, UI source from the published tarball
|
||||
|
||||
Publishing: `npm publish` (triggers `prepublishOnly` which builds UI, then publishes ~600KB tarball with 84 files)
|
||||
|
||||
### Core Python Modules
|
||||
|
||||
- `start.py` - CLI launcher with project creation/selection menu
|
||||
- `autonomous_agent_demo.py` - Entry point for running the agent
|
||||
- `autocoder_paths.py` - Central path resolution with dual-path backward compatibility and migration
|
||||
- `autonomous_agent_demo.py` - Entry point for running the agent (supports `--yolo`, `--parallel`, `--batch-size`, `--batch-features`)
|
||||
- `autoforge_paths.py` - Central path resolution with dual-path backward compatibility and migration
|
||||
- `agent.py` - Agent session loop using Claude Agent SDK
|
||||
- `client.py` - ClaudeSDKClient configuration with security hooks and MCP servers
|
||||
- `client.py` - ClaudeSDKClient configuration with security hooks, MCP servers, and Vertex AI support
|
||||
- `security.py` - Bash command allowlist validation (ALLOWED_COMMANDS whitelist)
|
||||
- `prompts.py` - Prompt template loading with project-specific fallback
|
||||
- `prompts.py` - Prompt template loading with project-specific fallback and batch feature prompts
|
||||
- `progress.py` - Progress tracking, database queries, webhook notifications
|
||||
- `registry.py` - Project registry for mapping names to paths (cross-platform)
|
||||
- `registry.py` - Project registry for mapping names to paths (cross-platform), global settings model
|
||||
- `parallel_orchestrator.py` - Concurrent agent execution with dependency-aware scheduling
|
||||
- `auth.py` - Authentication error detection for Claude CLI
|
||||
- `env_constants.py` - Shared environment variable constants (API_ENV_VARS) used by client.py and chat sessions
|
||||
- `rate_limit_utils.py` - Rate limit detection, retry parsing, exponential backoff with jitter
|
||||
- `api/database.py` - SQLAlchemy models (Feature, Schedule, ScheduleOverride)
|
||||
- `api/dependency_resolver.py` - Cycle detection (Kahn's algorithm + DFS) and dependency validation
|
||||
- `api/migration.py` - JSON-to-SQLite migration utility
|
||||
|
||||
### Project Registry
|
||||
|
||||
Projects can be stored in any directory. The registry maps project names to paths using SQLite:
|
||||
- **All platforms**: `~/.autocoder/registry.db`
|
||||
- **All platforms**: `~/.autoforge/registry.db`
|
||||
|
||||
The registry uses:
|
||||
- SQLite database with SQLAlchemy ORM
|
||||
@@ -147,13 +188,36 @@ The registry uses:
|
||||
|
||||
### Server API (server/)
|
||||
|
||||
The FastAPI server provides REST endpoints for the UI:
|
||||
The FastAPI server provides REST and WebSocket endpoints for the UI:
|
||||
|
||||
- `server/routers/projects.py` - Project CRUD with registry integration
|
||||
- `server/routers/features.py` - Feature management
|
||||
- `server/routers/agent.py` - Agent control (start/stop/pause/resume)
|
||||
- `server/routers/filesystem.py` - Filesystem browser API with security controls
|
||||
- `server/routers/spec_creation.py` - WebSocket for interactive spec creation
|
||||
**Routers** (`server/routers/`):
|
||||
- `projects.py` - Project CRUD with registry integration
|
||||
- `features.py` - Feature management
|
||||
- `agent.py` - Agent control (start/stop/pause/resume)
|
||||
- `filesystem.py` - Filesystem browser API with security controls
|
||||
- `spec_creation.py` - WebSocket for interactive spec creation
|
||||
- `expand_project.py` - Interactive project expansion via natural language
|
||||
- `assistant_chat.py` - Read-only project assistant chat (WebSocket/REST)
|
||||
- `terminal.py` - Interactive terminal I/O with PTY support (WebSocket bidirectional)
|
||||
- `devserver.py` - Dev server control (start/stop) and config
|
||||
- `schedules.py` - CRUD for time-based agent scheduling
|
||||
- `settings.py` - Global settings management (model selection, YOLO, batch size, headless browser)
|
||||
|
||||
**Services** (`server/services/`):
|
||||
- `process_manager.py` - Agent process lifecycle management
|
||||
- `project_config.py` - Project type detection and dev command management
|
||||
- `terminal_manager.py` - Terminal session management with PTY (`pywinpty` on Windows)
|
||||
- `scheduler_service.py` - APScheduler-based automated agent scheduling
|
||||
- `dev_server_manager.py` - Dev server lifecycle management
|
||||
- `assistant_chat_session.py` / `assistant_database.py` - Assistant chat sessions with SQLite persistence
|
||||
- `spec_chat_session.py` - Spec creation chat sessions
|
||||
- `expand_chat_session.py` - Expand project chat sessions
|
||||
- `chat_constants.py` - Shared constants for chat services
|
||||
|
||||
**Utilities** (`server/utils/`):
|
||||
- `process_utils.py` - Process management utilities
|
||||
- `project_helpers.py` - Project path resolution helpers
|
||||
- `validation.py` - Project name validation
|
||||
|
||||
### Feature Management
|
||||
|
||||
@@ -164,18 +228,26 @@ Features are stored in SQLite (`features.db`) via SQLAlchemy. The agent interact
|
||||
|
||||
MCP tools available to the agent:
|
||||
- `feature_get_stats` - Progress statistics
|
||||
- `feature_get_next` - Get highest-priority pending feature (respects dependencies)
|
||||
- `feature_claim_next` - Atomically claim next available feature (for parallel mode)
|
||||
- `feature_get_for_regression` - Random passing features for regression testing
|
||||
- `feature_get_by_id` - Get a single feature by ID
|
||||
- `feature_get_summary` - Get summary of all features
|
||||
- `feature_get_ready` - Get features ready to work on (dependencies met)
|
||||
- `feature_get_blocked` - Get features blocked by unmet dependencies
|
||||
- `feature_get_graph` - Get full dependency graph
|
||||
- `feature_claim_and_get` - Atomically claim next available feature (for parallel mode)
|
||||
- `feature_mark_in_progress` - Mark feature as in progress
|
||||
- `feature_mark_passing` - Mark feature complete
|
||||
- `feature_mark_failing` - Mark feature as failing
|
||||
- `feature_skip` - Move feature to end of queue
|
||||
- `feature_clear_in_progress` - Clear in-progress status
|
||||
- `feature_create_bulk` - Initialize all features (used by initializer)
|
||||
- `feature_create` - Create a single feature
|
||||
- `feature_add_dependency` - Add dependency between features (with cycle detection)
|
||||
- `feature_remove_dependency` - Remove a dependency
|
||||
- `feature_set_dependencies` - Set all dependencies for a feature at once
|
||||
|
||||
### React UI (ui/)
|
||||
|
||||
- Tech stack: React 19, TypeScript, TanStack Query, Tailwind CSS v4, Radix UI, dagre (graph layout)
|
||||
- Tech stack: React 19, TypeScript, Vite 7, TanStack Query, Tailwind CSS v4, Radix UI, dagre (graph layout), xterm.js (terminal)
|
||||
- `src/App.tsx` - Main app with project selection, kanban board, agent controls
|
||||
- `src/hooks/useWebSocket.ts` - Real-time updates via WebSocket (progress, agent status, logs, agent updates)
|
||||
- `src/hooks/useProjects.ts` - React Query hooks for API calls
|
||||
@@ -187,6 +259,17 @@ Key components:
|
||||
- `DependencyGraph.tsx` - Interactive node graph visualization with dagre layout
|
||||
- `CelebrationOverlay.tsx` - Confetti animation on feature completion
|
||||
- `FolderBrowser.tsx` - Server-side filesystem browser for project folder selection
|
||||
- `Terminal.tsx` / `TerminalTabs.tsx` - xterm.js-based multi-tab terminal
|
||||
- `AssistantPanel.tsx` / `AssistantChat.tsx` - AI assistant for project Q&A
|
||||
- `ExpandProjectModal.tsx` / `ExpandProjectChat.tsx` - Add features via natural language
|
||||
- `DevServerControl.tsx` - Dev server start/stop control
|
||||
- `ScheduleModal.tsx` - Schedule management UI
|
||||
- `SettingsModal.tsx` - Global settings panel
|
||||
|
||||
In-app documentation (`/#/docs` route):
|
||||
- `src/components/docs/sections/` - Content for each doc section (GettingStarted.tsx, AgentSystem.tsx, etc.)
|
||||
- `src/components/docs/docsData.ts` - Sidebar structure, subsection IDs, search keywords
|
||||
- `src/components/docs/DocsPage.tsx` - Page layout; `DocsContent.tsx` - section renderer with scroll tracking
|
||||
|
||||
Keyboard shortcuts (press `?` for help):
|
||||
- `D` - Toggle debug panel
|
||||
@@ -197,18 +280,18 @@ Keyboard shortcuts (press `?` for help):
|
||||
|
||||
### Project Structure for Generated Apps
|
||||
|
||||
Projects can be stored in any directory (registered in `~/.autocoder/registry.db`). Each project contains:
|
||||
- `.autocoder/prompts/app_spec.txt` - Application specification (XML format)
|
||||
- `.autocoder/prompts/initializer_prompt.md` - First session prompt
|
||||
- `.autocoder/prompts/coding_prompt.md` - Continuation session prompt
|
||||
- `.autocoder/features.db` - SQLite database with feature test cases
|
||||
- `.autocoder/.agent.lock` - Lock file to prevent multiple agent instances
|
||||
- `.autocoder/allowed_commands.yaml` - Project-specific bash command allowlist (optional)
|
||||
- `.autocoder/.gitignore` - Ignores runtime files
|
||||
Projects can be stored in any directory (registered in `~/.autoforge/registry.db`). Each project contains:
|
||||
- `.autoforge/prompts/app_spec.txt` - Application specification (XML format)
|
||||
- `.autoforge/prompts/initializer_prompt.md` - First session prompt
|
||||
- `.autoforge/prompts/coding_prompt.md` - Continuation session prompt
|
||||
- `.autoforge/features.db` - SQLite database with feature test cases
|
||||
- `.autoforge/.agent.lock` - Lock file to prevent multiple agent instances
|
||||
- `.autoforge/allowed_commands.yaml` - Project-specific bash command allowlist (optional)
|
||||
- `.autoforge/.gitignore` - Ignores runtime files
|
||||
- `CLAUDE.md` - Stays at project root (SDK convention)
|
||||
- `app_spec.txt` - Root copy for agent template compatibility
|
||||
|
||||
Legacy projects with files at root level (e.g., `features.db`, `prompts/`) are auto-migrated to `.autocoder/` on next agent start. Dual-path resolution ensures old and new layouts work transparently.
|
||||
Legacy projects with files at root level (e.g., `features.db`, `prompts/`) are auto-migrated to `.autoforge/` on next agent start. Dual-path resolution ensures old and new layouts work transparently.
|
||||
|
||||
### Security Model
|
||||
|
||||
@@ -248,29 +331,20 @@ The following directories (relative to home) are always blocked:
|
||||
- `.docker`, `.config/gcloud` - Container/cloud configs
|
||||
- `.npmrc`, `.pypirc`, `.netrc` - Package manager credentials
|
||||
|
||||
**Example Output:**
|
||||
|
||||
```
|
||||
Created security settings at /path/to/project/.claude_settings.json
|
||||
- Sandbox enabled (OS-level bash isolation)
|
||||
- Filesystem restricted to: /path/to/project
|
||||
- Extra read paths (validated): /Users/me/docs, /opt/shared-libs
|
||||
```
|
||||
|
||||
#### Per-Project Allowed Commands
|
||||
|
||||
The agent's bash command access is controlled through a hierarchical configuration system:
|
||||
|
||||
**Command Hierarchy (highest to lowest priority):**
|
||||
1. **Hardcoded Blocklist** (`security.py`) - NEVER allowed (dd, sudo, shutdown, etc.)
|
||||
2. **Org Blocklist** (`~/.autocoder/config.yaml`) - Cannot be overridden by projects
|
||||
3. **Org Allowlist** (`~/.autocoder/config.yaml`) - Available to all projects
|
||||
2. **Org Blocklist** (`~/.autoforge/config.yaml`) - Cannot be overridden by projects
|
||||
3. **Org Allowlist** (`~/.autoforge/config.yaml`) - Available to all projects
|
||||
4. **Global Allowlist** (`security.py`) - Default commands (npm, git, curl, etc.)
|
||||
5. **Project Allowlist** (`.autocoder/allowed_commands.yaml`) - Project-specific commands
|
||||
5. **Project Allowlist** (`.autoforge/allowed_commands.yaml`) - Project-specific commands
|
||||
|
||||
**Project Configuration:**
|
||||
|
||||
Each project can define custom allowed commands in `.autocoder/allowed_commands.yaml`:
|
||||
Each project can define custom allowed commands in `.autoforge/allowed_commands.yaml`:
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
@@ -290,7 +364,7 @@ commands:
|
||||
|
||||
**Organization Configuration:**
|
||||
|
||||
System administrators can set org-wide policies in `~/.autocoder/config.yaml`:
|
||||
System administrators can set org-wide policies in `~/.autoforge/config.yaml`:
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
@@ -318,13 +392,29 @@ blocked_commands:
|
||||
|
||||
**Files:**
|
||||
- `security.py` - Command validation logic and hardcoded blocklist
|
||||
- `test_security.py` - Unit tests for security system (136 tests)
|
||||
- `test_security_integration.py` - Integration tests with real hooks (9 tests)
|
||||
- `TEST_SECURITY.md` - Quick testing reference guide
|
||||
- `test_security.py` - Unit tests for security system
|
||||
- `test_security_integration.py` - Integration tests with real hooks
|
||||
- `examples/project_allowed_commands.yaml` - Project config example (all commented by default)
|
||||
- `examples/org_config.yaml` - Org config example (all commented by default)
|
||||
- `examples/README.md` - Comprehensive guide with use cases, testing, and troubleshooting
|
||||
|
||||
### Vertex AI Configuration (Optional)
|
||||
|
||||
Run coding agents via Google Cloud Vertex AI:
|
||||
|
||||
1. Install and authenticate gcloud CLI: `gcloud auth application-default login`
|
||||
2. Configure `.env`:
|
||||
```
|
||||
CLAUDE_CODE_USE_VERTEX=1
|
||||
CLOUD_ML_REGION=us-east5
|
||||
ANTHROPIC_VERTEX_PROJECT_ID=your-gcp-project-id
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL=claude-opus-4-5@20251101
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL=claude-sonnet-4-5@20250929
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL=claude-3-5-haiku@20241022
|
||||
```
|
||||
|
||||
**Note:** Use `@` instead of `-` in model names for Vertex AI.
|
||||
|
||||
### Ollama Local Models (Optional)
|
||||
|
||||
Run coding agents using local models via Ollama v0.14.0+:
|
||||
@@ -341,7 +431,7 @@ Run coding agents using local models via Ollama v0.14.0+:
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL=qwen3-coder
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL=qwen3-coder
|
||||
```
|
||||
5. Run autocoder normally - it will use your local Ollama models
|
||||
5. Run AutoForge normally - it will use your local Ollama models
|
||||
|
||||
**Recommended coding models:**
|
||||
- `qwen3-coder` - Good balance of speed and capability
|
||||
@@ -360,8 +450,24 @@ Run coding agents using local models via Ollama v0.14.0+:
|
||||
|
||||
## Claude Code Integration
|
||||
|
||||
- `.claude/commands/create-spec.md` - `/create-spec` slash command for interactive spec creation
|
||||
- `.claude/skills/frontend-design/SKILL.md` - Skill for distinctive UI design
|
||||
**Slash commands** (`.claude/commands/`):
|
||||
- `/create-spec` - Interactive spec creation for new projects
|
||||
- `/expand-project` - Expand existing project with new features
|
||||
- `/gsd-to-autoforge-spec` - Convert GSD codebase mapping to app_spec.txt
|
||||
- `/check-code` - Run lint and type-check for code quality
|
||||
- `/checkpoint` - Create comprehensive checkpoint commit
|
||||
- `/review-pr` - Review pull requests
|
||||
|
||||
**Custom agents** (`.claude/agents/`):
|
||||
- `coder.md` - Elite software architect agent for code implementation (Opus)
|
||||
- `code-review.md` - Code review agent for quality/security/performance analysis (Opus)
|
||||
- `deep-dive.md` - Technical investigator for deep analysis and debugging (Opus)
|
||||
|
||||
**Skills** (`.claude/skills/`):
|
||||
- `frontend-design` - Distinctive, production-grade UI design
|
||||
- `gsd-to-autoforge-spec` - Convert GSD codebase mapping to AutoForge app_spec format
|
||||
|
||||
**Other:**
|
||||
- `.claude/templates/` - Prompt templates copied to new projects
|
||||
- `examples/` - Configuration examples and documentation for security settings
|
||||
|
||||
@@ -369,12 +475,12 @@ Run coding agents using local models via Ollama v0.14.0+:
|
||||
|
||||
### Prompt Loading Fallback Chain
|
||||
|
||||
1. Project-specific: `{project_dir}/.autocoder/prompts/{name}.md` (or legacy `{project_dir}/prompts/{name}.md`)
|
||||
1. Project-specific: `{project_dir}/.autoforge/prompts/{name}.md` (or legacy `{project_dir}/prompts/{name}.md`)
|
||||
2. Base template: `.claude/templates/{name}.template.md`
|
||||
|
||||
### Agent Session Flow
|
||||
|
||||
1. Check if `.autocoder/features.db` has features (determines initializer vs coding agent)
|
||||
1. Check if `.autoforge/features.db` has features (determines initializer vs coding agent)
|
||||
2. Create ClaudeSDKClient with security settings
|
||||
3. Send prompt and stream response
|
||||
4. Auto-continue with 3-second delay between sessions
|
||||
@@ -392,7 +498,7 @@ The UI receives updates via WebSocket (`/ws/projects/{project_name}`):
|
||||
|
||||
When running with `--parallel`, the orchestrator:
|
||||
1. Spawns multiple Claude agents as subprocesses (up to `--max-concurrency`)
|
||||
2. Each agent claims features atomically via `feature_claim_next`
|
||||
2. Each agent claims features atomically via `feature_claim_and_get`
|
||||
3. Features blocked by unmet dependencies are skipped
|
||||
4. Browser contexts are isolated per agent using `--isolated` flag
|
||||
5. AgentTracker parses output and emits `agent_update` messages for UI
|
||||
@@ -405,6 +511,16 @@ The orchestrator enforces strict bounds on concurrent processes:
|
||||
- Testing agents are capped at `max_concurrency` (same as coding agents)
|
||||
- Total process count never exceeds 11 Python processes (1 orchestrator + 5 coding + 5 testing)
|
||||
|
||||
### Multi-Feature Batching
|
||||
|
||||
Agents can implement multiple features per session using `--batch-size` (1-3, default: 3):
|
||||
- `--batch-size N` - Max features per coding agent batch
|
||||
- `--testing-batch-size N` - Features per testing batch (1-5, default: 3)
|
||||
- `--batch-features 1,2,3` - Specific feature IDs for batch implementation
|
||||
- `--testing-batch-features 1,2,3` - Specific feature IDs for batch regression testing
|
||||
- `prompts.py` provides `get_batch_feature_prompt()` for multi-feature prompt generation
|
||||
- Configurable in UI via settings panel
|
||||
|
||||
### Design System
|
||||
|
||||
The UI uses a **neobrutalism** design with Tailwind CSS v4:
|
||||
|
||||
190
README.md
190
README.md
@@ -1,4 +1,4 @@
|
||||
# AutoCoder
|
||||
# AutoForge
|
||||
|
||||
[](https://www.buymeacoffee.com/leonvanzyl)
|
||||
|
||||
@@ -14,9 +14,11 @@ A long-running autonomous coding agent powered by the Claude Agent SDK. This too
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Claude Code CLI (Required)
|
||||
- **Node.js 20+** - Required for the CLI
|
||||
- **Python 3.11+** - Auto-detected on first run ([download](https://www.python.org/downloads/))
|
||||
- **Claude Code CLI** - Install and authenticate (see below)
|
||||
|
||||
This project requires the Claude Code CLI to be installed. Install it using one of these methods:
|
||||
### Claude Code CLI (Required)
|
||||
|
||||
**macOS / Linux:**
|
||||
```bash
|
||||
@@ -39,35 +41,63 @@ You need one of the following:
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Option 1: Web UI (Recommended)
|
||||
### Option 1: npm Install (Recommended)
|
||||
|
||||
**Windows:**
|
||||
```cmd
|
||||
start_ui.bat
|
||||
```
|
||||
|
||||
**macOS / Linux:**
|
||||
```bash
|
||||
./start_ui.sh
|
||||
npm install -g autoforge-ai
|
||||
autoforge
|
||||
```
|
||||
|
||||
On first run, AutoForge automatically:
|
||||
1. Checks for Python 3.11+
|
||||
2. Creates a virtual environment at `~/.autoforge/venv/`
|
||||
3. Installs Python dependencies
|
||||
4. Copies a default config file to `~/.autoforge/.env`
|
||||
5. Starts the server and opens your browser
|
||||
|
||||
### CLI Commands
|
||||
|
||||
```
|
||||
autoforge Start the server (default)
|
||||
autoforge config Open ~/.autoforge/.env in $EDITOR
|
||||
autoforge config --path Print config file path
|
||||
autoforge config --show Show active configuration values
|
||||
autoforge --port PORT Custom port (default: auto from 8888)
|
||||
autoforge --host HOST Custom host (default: 127.0.0.1)
|
||||
autoforge --no-browser Don't auto-open browser
|
||||
autoforge --repair Delete and recreate virtual environment
|
||||
autoforge --version Print version
|
||||
autoforge --help Show help
|
||||
```
|
||||
|
||||
### Option 2: From Source (Development)
|
||||
|
||||
Clone the repository and use the start scripts directly. This is the recommended path if you want to contribute or modify AutoForge itself.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/leonvanzyl/autoforge.git
|
||||
cd autoforge
|
||||
```
|
||||
|
||||
**Web UI:**
|
||||
|
||||
| Platform | Command |
|
||||
|---|---|
|
||||
| Windows | `start_ui.bat` |
|
||||
| macOS / Linux | `./start_ui.sh` |
|
||||
|
||||
This launches the React-based web UI at `http://localhost:5173` with:
|
||||
- Project selection and creation
|
||||
- Kanban board view of features
|
||||
- Real-time agent output streaming
|
||||
- Start/pause/stop controls
|
||||
|
||||
### Option 2: CLI Mode
|
||||
**CLI Mode:**
|
||||
|
||||
**Windows:**
|
||||
```cmd
|
||||
start.bat
|
||||
```
|
||||
|
||||
**macOS / Linux:**
|
||||
```bash
|
||||
./start.sh
|
||||
```
|
||||
| Platform | Command |
|
||||
|---|---|
|
||||
| Windows | `start.bat` |
|
||||
| macOS / Linux | `./start.sh` |
|
||||
|
||||
The start script will:
|
||||
1. Check if Claude CLI is installed
|
||||
@@ -130,44 +160,43 @@ Features are stored in SQLite via SQLAlchemy and managed through an MCP server t
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
autonomous-coding/
|
||||
├── start.bat # Windows CLI start script
|
||||
├── start.sh # macOS/Linux CLI start script
|
||||
├── start_ui.bat # Windows Web UI start script
|
||||
├── start_ui.sh # macOS/Linux Web UI start script
|
||||
├── start.py # CLI menu and project management
|
||||
├── start_ui.py # Web UI backend (FastAPI server launcher)
|
||||
├── autonomous_agent_demo.py # Agent entry point
|
||||
├── agent.py # Agent session logic
|
||||
├── client.py # Claude SDK client configuration
|
||||
├── security.py # Bash command allowlist and validation
|
||||
├── progress.py # Progress tracking utilities
|
||||
├── prompts.py # Prompt loading utilities
|
||||
autoforge/
|
||||
├── bin/ # npm CLI entry point
|
||||
├── lib/ # CLI bootstrap and setup logic
|
||||
├── start.py # CLI menu and project management
|
||||
├── start_ui.py # Web UI backend (FastAPI server launcher)
|
||||
├── autonomous_agent_demo.py # Agent entry point
|
||||
├── agent.py # Agent session logic
|
||||
├── client.py # Claude SDK client configuration
|
||||
├── security.py # Bash command allowlist and validation
|
||||
├── progress.py # Progress tracking utilities
|
||||
├── prompts.py # Prompt loading utilities
|
||||
├── api/
|
||||
│ └── database.py # SQLAlchemy models (Feature table)
|
||||
│ └── database.py # SQLAlchemy models (Feature table)
|
||||
├── mcp_server/
|
||||
│ └── feature_mcp.py # MCP server for feature management tools
|
||||
│ └── feature_mcp.py # MCP server for feature management tools
|
||||
├── server/
|
||||
│ ├── main.py # FastAPI REST API server
|
||||
│ ├── websocket.py # WebSocket handler for real-time updates
|
||||
│ ├── schemas.py # Pydantic schemas
|
||||
│ ├── routers/ # API route handlers
|
||||
│ └── services/ # Business logic services
|
||||
├── ui/ # React frontend
|
||||
│ ├── main.py # FastAPI REST API server
|
||||
│ ├── websocket.py # WebSocket handler for real-time updates
|
||||
│ ├── schemas.py # Pydantic schemas
|
||||
│ ├── routers/ # API route handlers
|
||||
│ └── services/ # Business logic services
|
||||
├── ui/ # React frontend
|
||||
│ ├── src/
|
||||
│ │ ├── App.tsx # Main app component
|
||||
│ │ ├── hooks/ # React Query and WebSocket hooks
|
||||
│ │ └── lib/ # API client and types
|
||||
│ │ ├── App.tsx # Main app component
|
||||
│ │ ├── hooks/ # React Query and WebSocket hooks
|
||||
│ │ └── lib/ # API client and types
|
||||
│ ├── package.json
|
||||
│ └── vite.config.ts
|
||||
├── .claude/
|
||||
│ ├── commands/
|
||||
│ │ └── create-spec.md # /create-spec slash command
|
||||
│ ├── skills/ # Claude Code skills
|
||||
│ └── templates/ # Prompt templates
|
||||
├── generations/ # Generated projects go here
|
||||
├── requirements.txt # Python dependencies
|
||||
└── .env # Optional configuration (N8N webhook)
|
||||
│ │ └── create-spec.md # /create-spec slash command
|
||||
│ ├── skills/ # Claude Code skills
|
||||
│ └── templates/ # Prompt templates
|
||||
├── requirements.txt # Python dependencies (development)
|
||||
├── requirements-prod.txt # Python dependencies (npm install)
|
||||
├── package.json # npm package definition
|
||||
└── .env # Optional configuration
|
||||
```
|
||||
|
||||
---
|
||||
@@ -264,11 +293,20 @@ The UI receives live updates via WebSocket (`/ws/projects/{project_name}`):
|
||||
|
||||
---
|
||||
|
||||
## Configuration (Optional)
|
||||
## Configuration
|
||||
|
||||
AutoForge reads configuration from a `.env` file. The file location depends on how you installed AutoForge:
|
||||
|
||||
| Install method | Config file location | Edit command |
|
||||
|---|---|---|
|
||||
| npm (global) | `~/.autoforge/.env` | `autoforge config` |
|
||||
| From source | `.env` in the project root | Edit directly |
|
||||
|
||||
A default config file is created automatically on first run. Use `autoforge config` to open it in your editor, or `autoforge config --show` to print the active values.
|
||||
|
||||
### N8N Webhook Integration
|
||||
|
||||
The agent can send progress notifications to an N8N webhook. Create a `.env` file:
|
||||
Add to your `.env` to send progress notifications to an N8N webhook:
|
||||
|
||||
```bash
|
||||
# Optional: N8N webhook for progress notifications
|
||||
@@ -290,7 +328,7 @@ When test progress increases, the agent sends:
|
||||
|
||||
### Using GLM Models (Alternative to Claude)
|
||||
|
||||
To use Zhipu AI's GLM models instead of Claude, add these variables to your `.env` file in the AutoCoder directory:
|
||||
Add these variables to your `.env` file to use Zhipu AI's GLM models:
|
||||
|
||||
```bash
|
||||
ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic
|
||||
@@ -301,10 +339,40 @@ ANTHROPIC_DEFAULT_OPUS_MODEL=glm-4.7
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL=glm-4.5-air
|
||||
```
|
||||
|
||||
This routes AutoCoder's API requests through Zhipu's Claude-compatible API, allowing you to use GLM-4.7 and other models. **This only affects AutoCoder** - your global Claude Code settings remain unchanged.
|
||||
This routes AutoForge's API requests through Zhipu's Claude-compatible API, allowing you to use GLM-4.7 and other models. **This only affects AutoForge** - your global Claude Code settings remain unchanged.
|
||||
|
||||
Get an API key at: https://z.ai/subscribe
|
||||
|
||||
### Using Ollama Local Models
|
||||
|
||||
Add these variables to your `.env` file to run agents with local models via Ollama v0.14.0+:
|
||||
|
||||
```bash
|
||||
ANTHROPIC_BASE_URL=http://localhost:11434
|
||||
ANTHROPIC_AUTH_TOKEN=ollama
|
||||
API_TIMEOUT_MS=3000000
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL=qwen3-coder
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL=qwen3-coder
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL=qwen3-coder
|
||||
```
|
||||
|
||||
See the [CLAUDE.md](CLAUDE.md) for recommended models and known limitations.
|
||||
|
||||
### Using Vertex AI
|
||||
|
||||
Add these variables to your `.env` file to run agents via Google Cloud Vertex AI:
|
||||
|
||||
```bash
|
||||
CLAUDE_CODE_USE_VERTEX=1
|
||||
CLOUD_ML_REGION=us-east5
|
||||
ANTHROPIC_VERTEX_PROJECT_ID=your-gcp-project-id
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL=claude-opus-4-5@20251101
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL=claude-sonnet-4-5@20250929
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL=claude-3-5-haiku@20241022
|
||||
```
|
||||
|
||||
Requires `gcloud auth application-default login` first. Note the `@` separator (not `-`) in Vertex AI model names.
|
||||
|
||||
---
|
||||
|
||||
## Customization
|
||||
@@ -335,6 +403,18 @@ This is normal. The initializer agent is generating detailed test cases, which t
|
||||
**"Command blocked by security hook"**
|
||||
The agent tried to run a command not in the allowlist. This is the security system working as intended. If needed, add the command to `ALLOWED_COMMANDS` in `security.py`.
|
||||
|
||||
**"Python 3.11+ required but not found"**
|
||||
Install Python 3.11 or later from [python.org](https://www.python.org/downloads/). Make sure `python3` (or `python` on Windows) is on your PATH.
|
||||
|
||||
**"Python venv module not available"**
|
||||
On Debian/Ubuntu, the venv module is packaged separately. Install it with `sudo apt install python3.XX-venv` (replace `XX` with your Python minor version, e.g., `python3.12-venv`).
|
||||
|
||||
**"AutoForge is already running"**
|
||||
A server instance is already active. Use the browser URL shown in the terminal, or stop the existing instance with Ctrl+C first.
|
||||
|
||||
**Virtual environment issues after a Python upgrade**
|
||||
Run `autoforge --repair` to delete and recreate the virtual environment from scratch.
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
@@ -183,7 +183,7 @@ class ScheduleOverride(Base):
|
||||
|
||||
def get_database_path(project_dir: Path) -> Path:
|
||||
"""Return the path to the SQLite database for a project."""
|
||||
from autocoder_paths import get_features_db_path
|
||||
from autoforge_paths import get_features_db_path
|
||||
return get_features_db_path(project_dir)
|
||||
|
||||
|
||||
@@ -385,7 +385,7 @@ def create_database(project_dir: Path) -> tuple:
|
||||
|
||||
db_url = get_database_url(project_dir)
|
||||
|
||||
# Ensure parent directory exists (for .autocoder/ layout)
|
||||
# Ensure parent directory exists (for .autoforge/ layout)
|
||||
db_path = get_database_path(project_dir)
|
||||
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
"""
|
||||
Autocoder Path Resolution
|
||||
AutoForge Path Resolution
|
||||
=========================
|
||||
|
||||
Central module for resolving paths to autocoder-generated files within a project.
|
||||
Central module for resolving paths to autoforge-generated files within a project.
|
||||
|
||||
Implements a dual-path resolution strategy for backward compatibility:
|
||||
Implements a tri-path resolution strategy for backward compatibility:
|
||||
|
||||
1. Check ``project_dir / ".autocoder" / X`` (new layout)
|
||||
2. Check ``project_dir / X`` (legacy root-level layout)
|
||||
3. Default to the new location for fresh projects
|
||||
1. Check ``project_dir / ".autoforge" / X`` (current layout)
|
||||
2. Check ``project_dir / ".autocoder" / X`` (legacy layout)
|
||||
3. Check ``project_dir / X`` (legacy root-level layout)
|
||||
4. Default to the new location for fresh projects
|
||||
|
||||
This allows existing projects with root-level ``features.db``, ``.agent.lock``,
|
||||
etc. to keep working while new projects store everything under ``.autocoder/``.
|
||||
etc. to keep working while new projects store everything under ``.autoforge/``.
|
||||
Projects using the old ``.autocoder/`` directory are auto-migrated on next start.
|
||||
|
||||
The ``migrate_project_layout`` function can move an old-layout project to the
|
||||
new layout safely, with full integrity checks for SQLite databases.
|
||||
@@ -25,10 +27,10 @@ from pathlib import Path
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# .gitignore content written into every .autocoder/ directory
|
||||
# .gitignore content written into every .autoforge/ directory
|
||||
# ---------------------------------------------------------------------------
|
||||
_GITIGNORE_CONTENT = """\
|
||||
# Autocoder runtime files
|
||||
# AutoForge runtime files
|
||||
features.db
|
||||
features.db-wal
|
||||
features.db-shm
|
||||
@@ -49,15 +51,18 @@ assistant.db-shm
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _resolve_path(project_dir: Path, filename: str) -> Path:
|
||||
"""Resolve a file path using dual-path strategy.
|
||||
"""Resolve a file path using tri-path strategy.
|
||||
|
||||
Checks the new ``.autocoder/`` location first, then falls back to the
|
||||
legacy root-level location. If neither exists, returns the new location
|
||||
so that newly-created files land in ``.autocoder/``.
|
||||
Checks the new ``.autoforge/`` location first, then the legacy
|
||||
``.autocoder/`` location, then the root-level location. If none exist,
|
||||
returns the new location so that newly-created files land in ``.autoforge/``.
|
||||
"""
|
||||
new = project_dir / ".autocoder" / filename
|
||||
new = project_dir / ".autoforge" / filename
|
||||
if new.exists():
|
||||
return new
|
||||
legacy = project_dir / ".autocoder" / filename
|
||||
if legacy.exists():
|
||||
return legacy
|
||||
old = project_dir / filename
|
||||
if old.exists():
|
||||
return old
|
||||
@@ -65,14 +70,17 @@ def _resolve_path(project_dir: Path, filename: str) -> Path:
|
||||
|
||||
|
||||
def _resolve_dir(project_dir: Path, dirname: str) -> Path:
|
||||
"""Resolve a directory path using dual-path strategy.
|
||||
"""Resolve a directory path using tri-path strategy.
|
||||
|
||||
Same logic as ``_resolve_path`` but intended for directories such as
|
||||
``prompts/``.
|
||||
"""
|
||||
new = project_dir / ".autocoder" / dirname
|
||||
new = project_dir / ".autoforge" / dirname
|
||||
if new.exists():
|
||||
return new
|
||||
legacy = project_dir / ".autocoder" / dirname
|
||||
if legacy.exists():
|
||||
return legacy
|
||||
old = project_dir / dirname
|
||||
if old.exists():
|
||||
return old
|
||||
@@ -80,27 +88,27 @@ def _resolve_dir(project_dir: Path, dirname: str) -> Path:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# .autocoder directory management
|
||||
# .autoforge directory management
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def get_autocoder_dir(project_dir: Path) -> Path:
|
||||
"""Return the ``.autocoder`` directory path. Does NOT create it."""
|
||||
return project_dir / ".autocoder"
|
||||
def get_autoforge_dir(project_dir: Path) -> Path:
|
||||
"""Return the ``.autoforge`` directory path. Does NOT create it."""
|
||||
return project_dir / ".autoforge"
|
||||
|
||||
|
||||
def ensure_autocoder_dir(project_dir: Path) -> Path:
|
||||
"""Create the ``.autocoder/`` directory (if needed) and write its ``.gitignore``.
|
||||
def ensure_autoforge_dir(project_dir: Path) -> Path:
|
||||
"""Create the ``.autoforge/`` directory (if needed) and write its ``.gitignore``.
|
||||
|
||||
Returns:
|
||||
The path to the ``.autocoder`` directory.
|
||||
The path to the ``.autoforge`` directory.
|
||||
"""
|
||||
autocoder_dir = get_autocoder_dir(project_dir)
|
||||
autocoder_dir.mkdir(parents=True, exist_ok=True)
|
||||
autoforge_dir = get_autoforge_dir(project_dir)
|
||||
autoforge_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
gitignore_path = autocoder_dir / ".gitignore"
|
||||
gitignore_path = autoforge_dir / ".gitignore"
|
||||
gitignore_path.write_text(_GITIGNORE_CONTENT, encoding="utf-8")
|
||||
|
||||
return autocoder_dir
|
||||
return autoforge_dir
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -154,9 +162,9 @@ def get_prompts_dir(project_dir: Path) -> Path:
|
||||
def get_expand_settings_path(project_dir: Path, uuid_hex: str) -> Path:
|
||||
"""Return the path for an ephemeral expand-session settings file.
|
||||
|
||||
These files are short-lived and always stored in ``.autocoder/``.
|
||||
These files are short-lived and always stored in ``.autoforge/``.
|
||||
"""
|
||||
return project_dir / ".autocoder" / f".claude_settings.expand.{uuid_hex}.json"
|
||||
return project_dir / ".autoforge" / f".claude_settings.expand.{uuid_hex}.json"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -166,8 +174,9 @@ def get_expand_settings_path(project_dir: Path, uuid_hex: str) -> Path:
|
||||
def has_agent_running(project_dir: Path) -> bool:
|
||||
"""Check whether any agent or dev-server lock file exists at either location.
|
||||
|
||||
Inspects both the legacy root-level paths and the new ``.autocoder/``
|
||||
paths so that a running agent is detected regardless of project layout.
|
||||
Inspects the legacy root-level paths, the old ``.autocoder/`` paths, and
|
||||
the new ``.autoforge/`` paths so that a running agent is detected
|
||||
regardless of project layout.
|
||||
|
||||
Returns:
|
||||
``True`` if any ``.agent.lock`` or ``.devserver.lock`` exists.
|
||||
@@ -176,8 +185,11 @@ def has_agent_running(project_dir: Path) -> bool:
|
||||
for name in lock_names:
|
||||
if (project_dir / name).exists():
|
||||
return True
|
||||
# Check both old and new directory names for backward compatibility
|
||||
if (project_dir / ".autocoder" / name).exists():
|
||||
return True
|
||||
if (project_dir / ".autoforge" / name).exists():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@@ -186,7 +198,7 @@ def has_agent_running(project_dir: Path) -> bool:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def migrate_project_layout(project_dir: Path) -> list[str]:
|
||||
"""Migrate a project from the legacy root-level layout to ``.autocoder/``.
|
||||
"""Migrate a project from the legacy root-level layout to ``.autoforge/``.
|
||||
|
||||
The migration is incremental and safe:
|
||||
|
||||
@@ -199,7 +211,7 @@ def migrate_project_layout(project_dir: Path) -> list[str]:
|
||||
|
||||
Returns:
|
||||
A list of human-readable descriptions of what was migrated, e.g.
|
||||
``["prompts/ -> .autocoder/prompts/", "features.db -> .autocoder/features.db"]``.
|
||||
``["prompts/ -> .autoforge/prompts/", "features.db -> .autoforge/features.db"]``.
|
||||
An empty list means nothing was migrated (either everything is
|
||||
already migrated, or the agent is running).
|
||||
"""
|
||||
@@ -208,18 +220,31 @@ def migrate_project_layout(project_dir: Path) -> list[str]:
|
||||
logger.warning("Migration skipped: agent or dev-server is running for %s", project_dir)
|
||||
return []
|
||||
|
||||
autocoder_dir = ensure_autocoder_dir(project_dir)
|
||||
migrated: list[str] = []
|
||||
# --- 0. Migrate .autocoder/ → .autoforge/ directory -------------------
|
||||
old_autocoder_dir = project_dir / ".autocoder"
|
||||
new_autoforge_dir = project_dir / ".autoforge"
|
||||
if old_autocoder_dir.exists() and old_autocoder_dir.is_dir() and not new_autoforge_dir.exists():
|
||||
try:
|
||||
old_autocoder_dir.rename(new_autoforge_dir)
|
||||
logger.info("Migrated .autocoder/ -> .autoforge/")
|
||||
migrated: list[str] = [".autocoder/ -> .autoforge/"]
|
||||
except Exception:
|
||||
logger.warning("Failed to migrate .autocoder/ -> .autoforge/", exc_info=True)
|
||||
migrated = []
|
||||
else:
|
||||
migrated = []
|
||||
|
||||
autoforge_dir = ensure_autoforge_dir(project_dir)
|
||||
|
||||
# --- 1. Migrate prompts/ directory -----------------------------------
|
||||
try:
|
||||
old_prompts = project_dir / "prompts"
|
||||
new_prompts = autocoder_dir / "prompts"
|
||||
new_prompts = autoforge_dir / "prompts"
|
||||
if old_prompts.exists() and old_prompts.is_dir() and not new_prompts.exists():
|
||||
shutil.copytree(str(old_prompts), str(new_prompts))
|
||||
shutil.rmtree(str(old_prompts))
|
||||
migrated.append("prompts/ -> .autocoder/prompts/")
|
||||
logger.info("Migrated prompts/ -> .autocoder/prompts/")
|
||||
migrated.append("prompts/ -> .autoforge/prompts/")
|
||||
logger.info("Migrated prompts/ -> .autoforge/prompts/")
|
||||
except Exception:
|
||||
logger.warning("Failed to migrate prompts/ directory", exc_info=True)
|
||||
|
||||
@@ -228,7 +253,7 @@ def migrate_project_layout(project_dir: Path) -> list[str]:
|
||||
for db_name in db_names:
|
||||
try:
|
||||
old_db = project_dir / db_name
|
||||
new_db = autocoder_dir / db_name
|
||||
new_db = autoforge_dir / db_name
|
||||
if old_db.exists() and not new_db.exists():
|
||||
# Flush WAL to ensure all data is in the main database file
|
||||
conn = sqlite3.connect(str(old_db))
|
||||
@@ -263,8 +288,8 @@ def migrate_project_layout(project_dir: Path) -> list[str]:
|
||||
wal_file = project_dir / f"{db_name}{suffix}"
|
||||
wal_file.unlink(missing_ok=True)
|
||||
|
||||
migrated.append(f"{db_name} -> .autocoder/{db_name}")
|
||||
logger.info("Migrated %s -> .autocoder/%s", db_name, db_name)
|
||||
migrated.append(f"{db_name} -> .autoforge/{db_name}")
|
||||
logger.info("Migrated %s -> .autoforge/%s", db_name, db_name)
|
||||
except Exception:
|
||||
logger.warning("Failed to migrate %s", db_name, exc_info=True)
|
||||
|
||||
@@ -279,11 +304,11 @@ def migrate_project_layout(project_dir: Path) -> list[str]:
|
||||
for filename in simple_files:
|
||||
try:
|
||||
old_file = project_dir / filename
|
||||
new_file = autocoder_dir / filename
|
||||
new_file = autoforge_dir / filename
|
||||
if old_file.exists() and not new_file.exists():
|
||||
shutil.move(str(old_file), str(new_file))
|
||||
migrated.append(f"{filename} -> .autocoder/{filename}")
|
||||
logger.info("Migrated %s -> .autocoder/%s", filename, filename)
|
||||
migrated.append(f"{filename} -> .autoforge/{filename}")
|
||||
logger.info("Migrated %s -> .autoforge/%s", filename, filename)
|
||||
except Exception:
|
||||
logger.warning("Failed to migrate %s", filename, exc_info=True)
|
||||
|
||||
@@ -221,11 +221,11 @@ def main() -> None:
|
||||
print("Use an absolute path or register the project first.")
|
||||
return
|
||||
|
||||
# Migrate project layout to .autocoder/ if needed (idempotent, safe)
|
||||
from autocoder_paths import migrate_project_layout
|
||||
# Migrate project layout to .autoforge/ if needed (idempotent, safe)
|
||||
from autoforge_paths import migrate_project_layout
|
||||
migrated = migrate_project_layout(project_dir)
|
||||
if migrated:
|
||||
print(f"Migrated project files to .autocoder/: {', '.join(migrated)}", flush=True)
|
||||
print(f"Migrated project files to .autoforge/: {', '.join(migrated)}", flush=True)
|
||||
|
||||
# Parse batch testing feature IDs (comma-separated string -> list[int])
|
||||
testing_feature_ids: list[int] | None = None
|
||||
|
||||
3
bin/autoforge.js
Normal file
3
bin/autoforge.js
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
import { run } from '../lib/cli.js';
|
||||
run(process.argv.slice(2));
|
||||
@@ -382,7 +382,7 @@ def create_client(
|
||||
project_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Write settings to a file in the project directory
|
||||
from autocoder_paths import get_claude_settings_path
|
||||
from autoforge_paths import get_claude_settings_path
|
||||
settings_file = get_claude_settings_path(project_dir)
|
||||
settings_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(settings_file, "w") as f:
|
||||
@@ -450,7 +450,7 @@ def create_client(
|
||||
|
||||
# Build environment overrides for API endpoint configuration
|
||||
# These override system env vars for the Claude CLI subprocess,
|
||||
# allowing AutoCoder to use alternative APIs (e.g., GLM) without
|
||||
# allowing AutoForge to use alternative APIs (e.g., GLM) without
|
||||
# affecting the user's global Claude Code settings
|
||||
sdk_env = {}
|
||||
for var in API_ENV_VARS:
|
||||
|
||||
@@ -7,7 +7,7 @@ subprocesses. Imported by both ``client.py`` (agent sessions) and
|
||||
``server/services/chat_constants.py`` (chat sessions) to avoid maintaining
|
||||
duplicate lists.
|
||||
|
||||
These allow autocoder to use alternative API endpoints (Ollama, GLM,
|
||||
These allow autoforge to use alternative API endpoints (Ollama, GLM,
|
||||
Vertex AI) without affecting the user's global Claude Code settings.
|
||||
"""
|
||||
|
||||
|
||||
@@ -179,7 +179,7 @@ To see what you can reduce:
|
||||
|
||||
```bash
|
||||
# Count commands by prefix
|
||||
grep "^ - name:" .autocoder/allowed_commands.yaml | \
|
||||
grep "^ - name:" .autoforge/allowed_commands.yaml | \
|
||||
sed 's/^ - name: //' | \
|
||||
cut -d' ' -f1 | \
|
||||
sort | uniq -c | sort -rn
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# AutoCoder Security Configuration Examples
|
||||
# AutoForge Security Configuration Examples
|
||||
|
||||
This directory contains example configuration files for controlling which bash commands the autonomous coding agent can execute.
|
||||
|
||||
@@ -18,11 +18,11 @@ This directory contains example configuration files for controlling which bash c
|
||||
|
||||
### For a Single Project (Most Common)
|
||||
|
||||
When you create a new project with AutoCoder, it automatically creates:
|
||||
When you create a new project with AutoForge, it automatically creates:
|
||||
|
||||
```text
|
||||
my-project/
|
||||
.autocoder/
|
||||
.autoforge/
|
||||
allowed_commands.yaml ← Automatically created from template
|
||||
```
|
||||
|
||||
@@ -34,17 +34,17 @@ If you want commands available across **all projects**, manually create:
|
||||
|
||||
```bash
|
||||
# Copy the example to your home directory
|
||||
cp examples/org_config.yaml ~/.autocoder/config.yaml
|
||||
cp examples/org_config.yaml ~/.autoforge/config.yaml
|
||||
|
||||
# Edit it to add org-wide commands
|
||||
nano ~/.autocoder/config.yaml
|
||||
nano ~/.autoforge/config.yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project-Level Configuration
|
||||
|
||||
**File:** `{project_dir}/.autocoder/allowed_commands.yaml`
|
||||
**File:** `{project_dir}/.autoforge/allowed_commands.yaml`
|
||||
|
||||
**Purpose:** Define commands needed for THIS specific project.
|
||||
|
||||
@@ -82,7 +82,7 @@ commands:
|
||||
|
||||
## Organization-Level Configuration
|
||||
|
||||
**File:** `~/.autocoder/config.yaml`
|
||||
**File:** `~/.autoforge/config.yaml`
|
||||
|
||||
**Purpose:** Define commands and policies for ALL projects.
|
||||
|
||||
@@ -127,13 +127,13 @@ When the agent tries to run a command, the system checks in this order:
|
||||
└─────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 2. ORG BLOCKLIST (~/.autocoder/config.yaml) │
|
||||
│ 2. ORG BLOCKLIST (~/.autoforge/config.yaml) │
|
||||
│ Commands you block organization-wide │
|
||||
│ ❌ Projects CANNOT override these │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 3. ORG ALLOWLIST (~/.autocoder/config.yaml) │
|
||||
│ 3. ORG ALLOWLIST (~/.autoforge/config.yaml) │
|
||||
│ Commands available to all projects │
|
||||
│ ✅ Automatically available │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
@@ -145,7 +145,7 @@ When the agent tries to run a command, the system checks in this order:
|
||||
└─────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 5. PROJECT ALLOWLIST (.autocoder/allowed_commands) │
|
||||
│ 5. PROJECT ALLOWLIST (.autoforge/allowed_commands) │
|
||||
│ Project-specific commands │
|
||||
│ ✅ Available only to this project │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
@@ -195,7 +195,7 @@ Matches:
|
||||
|
||||
### iOS Development
|
||||
|
||||
**Project config** (`.autocoder/allowed_commands.yaml`):
|
||||
**Project config** (`.autoforge/allowed_commands.yaml`):
|
||||
```yaml
|
||||
version: 1
|
||||
commands:
|
||||
@@ -245,7 +245,7 @@ commands:
|
||||
|
||||
### Enterprise Organization (Restrictive)
|
||||
|
||||
**Org config** (`~/.autocoder/config.yaml`):
|
||||
**Org config** (`~/.autoforge/config.yaml`):
|
||||
```yaml
|
||||
version: 1
|
||||
|
||||
@@ -265,7 +265,7 @@ blocked_commands:
|
||||
|
||||
### Startup Team (Permissive)
|
||||
|
||||
**Org config** (`~/.autocoder/config.yaml`):
|
||||
**Org config** (`~/.autoforge/config.yaml`):
|
||||
```yaml
|
||||
version: 1
|
||||
|
||||
@@ -394,7 +394,7 @@ These commands are **NEVER allowed**, even with user approval:
|
||||
|
||||
**Solution:** Add the command to your project config:
|
||||
```yaml
|
||||
# In .autocoder/allowed_commands.yaml
|
||||
# In .autoforge/allowed_commands.yaml
|
||||
commands:
|
||||
- name: X
|
||||
description: What this command does
|
||||
@@ -405,7 +405,7 @@ commands:
|
||||
**Cause:** The command is in the org blocklist or hardcoded blocklist.
|
||||
|
||||
**Solution:**
|
||||
- If in org blocklist: Edit `~/.autocoder/config.yaml` to remove it
|
||||
- If in org blocklist: Edit `~/.autoforge/config.yaml` to remove it
|
||||
- If in hardcoded blocklist: Cannot be allowed (by design)
|
||||
|
||||
### Error: "Could not parse YAML config"
|
||||
@@ -422,8 +422,8 @@ commands:
|
||||
**Solution:**
|
||||
1. Restart the agent (changes are loaded on startup)
|
||||
2. Verify file location:
|
||||
- Project: `{project}/.autocoder/allowed_commands.yaml`
|
||||
- Org: `~/.autocoder/config.yaml` (must be manually created)
|
||||
- Project: `{project}/.autoforge/allowed_commands.yaml`
|
||||
- Org: `~/.autoforge/config.yaml` (must be manually created)
|
||||
3. Check YAML is valid (run through a YAML validator)
|
||||
|
||||
---
|
||||
@@ -432,7 +432,7 @@ commands:
|
||||
|
||||
### Running the Tests
|
||||
|
||||
AutoCoder has comprehensive tests for the security system:
|
||||
AutoForge has comprehensive tests for the security system:
|
||||
|
||||
**Unit Tests** (136 tests - fast):
|
||||
```bash
|
||||
@@ -481,7 +481,7 @@ python start.py
|
||||
cd path/to/security-test
|
||||
|
||||
# Edit the config
|
||||
nano .autocoder/allowed_commands.yaml
|
||||
nano .autoforge/allowed_commands.yaml
|
||||
```
|
||||
|
||||
**3. Add a test command (e.g., Swift):**
|
||||
@@ -509,7 +509,7 @@ Or:
|
||||
```text
|
||||
Command 'wget' is not allowed.
|
||||
To allow this command:
|
||||
1. Add to .autocoder/allowed_commands.yaml for this project, OR
|
||||
1. Add to .autoforge/allowed_commands.yaml for this project, OR
|
||||
2. Request mid-session approval (the agent can ask)
|
||||
```
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Organization-Level AutoCoder Configuration
|
||||
# Organization-Level AutoForge Configuration
|
||||
# ============================================
|
||||
# Location: ~/.autocoder/config.yaml
|
||||
# Location: ~/.autoforge/config.yaml
|
||||
#
|
||||
# IMPORTANT: This file is OPTIONAL and must be manually created by you.
|
||||
# It does NOT exist by default.
|
||||
@@ -22,7 +22,7 @@ version: 1
|
||||
# Organization-Wide Allowed Commands
|
||||
# ==========================================
|
||||
# These commands become available to ALL projects automatically.
|
||||
# Projects don't need to add them to their own .autocoder/allowed_commands.yaml
|
||||
# Projects don't need to add them to their own .autoforge/allowed_commands.yaml
|
||||
#
|
||||
# By default, this is empty. Uncomment and add commands as needed.
|
||||
|
||||
@@ -122,7 +122,7 @@ approval_timeout_minutes: 5
|
||||
# Default commands: npm, git, curl, ls, cat, etc.
|
||||
# Always available to all projects.
|
||||
#
|
||||
# 5. Project Allowed Commands (.autocoder/allowed_commands.yaml)
|
||||
# 5. Project Allowed Commands (.autoforge/allowed_commands.yaml)
|
||||
# Project-specific commands defined in each project.
|
||||
# LOWEST PRIORITY (can't override blocks above).
|
||||
#
|
||||
@@ -165,7 +165,7 @@ approval_timeout_minutes: 5
|
||||
# ==========================================
|
||||
# To Create This File
|
||||
# ==========================================
|
||||
# 1. Copy this example to: ~/.autocoder/config.yaml
|
||||
# 1. Copy this example to: ~/.autoforge/config.yaml
|
||||
# 2. Uncomment and customize the sections you need
|
||||
# 3. Leave empty lists if you don't need org-level controls
|
||||
#
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Project-Specific Allowed Commands
|
||||
# ==================================
|
||||
# Location: {project_dir}/.autocoder/allowed_commands.yaml
|
||||
# Location: {project_dir}/.autoforge/allowed_commands.yaml
|
||||
#
|
||||
# This file defines bash commands that the autonomous coding agent can use
|
||||
# for THIS SPECIFIC PROJECT, beyond the default allowed commands.
|
||||
#
|
||||
# When you create a new project, AutoCoder automatically creates this file
|
||||
# in your project's .autocoder/ directory. You can customize it for your
|
||||
# When you create a new project, AutoForge automatically creates this file
|
||||
# in your project's .autoforge/ directory. You can customize it for your
|
||||
# project's specific needs (iOS, Rust, Python, etc.).
|
||||
|
||||
version: 1
|
||||
@@ -115,7 +115,7 @@ commands: []
|
||||
# Limits:
|
||||
# - Maximum 100 commands per project
|
||||
# - Commands in the blocklist (sudo, dd, shutdown, etc.) can NEVER be allowed
|
||||
# - Org-level blocked commands (see ~/.autocoder/config.yaml) cannot be overridden
|
||||
# - Org-level blocked commands (see ~/.autoforge/config.yaml) cannot be overridden
|
||||
#
|
||||
# Default Allowed Commands (always available):
|
||||
# File operations: ls, cat, head, tail, wc, grep, cp, mkdir, mv, rm, touch
|
||||
|
||||
791
lib/cli.js
Normal file
791
lib/cli.js
Normal file
@@ -0,0 +1,791 @@
|
||||
/**
|
||||
* AutoForge CLI
|
||||
* =============
|
||||
*
|
||||
* Main CLI module for the AutoForge npm global package.
|
||||
* Handles Python detection, virtual environment management,
|
||||
* config loading, and uvicorn server lifecycle.
|
||||
*
|
||||
* Uses only Node.js built-in modules -- no external dependencies.
|
||||
*/
|
||||
|
||||
import { execFileSync, spawn, execSync } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, rmSync, copyFileSync } from 'node:fs';
|
||||
import { createRequire } from 'node:module';
|
||||
import { createServer } from 'node:net';
|
||||
import { homedir, platform } from 'node:os';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Path constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Root of the autoforge npm package (one level up from lib/) */
|
||||
const PKG_DIR = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
|
||||
/** User config home: ~/.autoforge/ */
|
||||
const CONFIG_HOME = join(homedir(), '.autoforge');
|
||||
|
||||
/** Virtual-environment directory managed by the CLI */
|
||||
const VENV_DIR = join(CONFIG_HOME, 'venv');
|
||||
|
||||
/** Composite marker written after a successful pip install */
|
||||
const DEPS_MARKER = join(VENV_DIR, '.deps-installed');
|
||||
|
||||
/** PID file for the running server */
|
||||
const PID_FILE = join(CONFIG_HOME, 'server.pid');
|
||||
|
||||
/** Path to the production requirements file inside the package */
|
||||
const REQUIREMENTS_FILE = join(PKG_DIR, 'requirements-prod.txt');
|
||||
|
||||
/** Path to the .env example shipped with the package */
|
||||
const ENV_EXAMPLE = join(PKG_DIR, '.env.example');
|
||||
|
||||
/** User .env config file */
|
||||
const ENV_FILE = join(CONFIG_HOME, '.env');
|
||||
|
||||
const IS_WIN = platform() === 'win32';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Package version (read lazily via createRequire)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { version: VERSION } = require(join(PKG_DIR, 'package.json'));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Indented console output matching the spec format. */
|
||||
function log(msg = '') {
|
||||
console.log(` ${msg}`);
|
||||
}
|
||||
|
||||
/** Print a fatal error and exit. */
|
||||
function die(msg) {
|
||||
console.error(`\n Error: ${msg}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a Python version string like "Python 3.13.6" and return
|
||||
* { major, minor, patch, raw } or null on failure.
|
||||
*/
|
||||
function parsePythonVersion(raw) {
|
||||
const m = raw.match(/Python\s+(\d+)\.(\d+)\.(\d+)/);
|
||||
if (!m) return null;
|
||||
return {
|
||||
major: Number(m[1]),
|
||||
minor: Number(m[2]),
|
||||
patch: Number(m[3]),
|
||||
raw: `${m[1]}.${m[2]}.${m[3]}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Try a single Python candidate. Returns { exe, version } or null.
|
||||
* `candidate` is either a bare name or an array of args (e.g. ['py', '-3']).
|
||||
*/
|
||||
function tryPythonCandidate(candidate) {
|
||||
const args = Array.isArray(candidate) ? candidate : [candidate];
|
||||
const exe = args[0];
|
||||
const extraArgs = args.slice(1);
|
||||
|
||||
try {
|
||||
const out = execFileSync(exe, [...extraArgs, '--version'], {
|
||||
encoding: 'utf8',
|
||||
timeout: 10_000,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
const ver = parsePythonVersion(out);
|
||||
if (!ver) return null;
|
||||
|
||||
// Require 3.11+
|
||||
if (ver.major < 3 || (ver.major === 3 && ver.minor < 11)) {
|
||||
return { exe: args.join(' '), version: ver, tooOld: true };
|
||||
}
|
||||
|
||||
return { exe: args.join(' '), version: ver, tooOld: false };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Python detection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Find a suitable Python >= 3.11 interpreter.
|
||||
*
|
||||
* Search order is platform-dependent:
|
||||
* Windows: python -> py -3 -> python3
|
||||
* macOS/Linux: python3 -> python
|
||||
*
|
||||
* The AUTOFORGE_PYTHON env var overrides automatic detection.
|
||||
*
|
||||
* After finding a candidate we also verify that the venv module is
|
||||
* available (Debian/Ubuntu strip it out of the base package).
|
||||
*/
|
||||
function findPython() {
|
||||
// Allow explicit override via environment variable
|
||||
const override = process.env.AUTOFORGE_PYTHON;
|
||||
if (override) {
|
||||
const result = tryPythonCandidate(override);
|
||||
if (!result) {
|
||||
die(`AUTOFORGE_PYTHON is set to "${override}" but it could not be executed.`);
|
||||
}
|
||||
if (result.tooOld) {
|
||||
die(
|
||||
`Python ${result.version.raw} found (via AUTOFORGE_PYTHON), but 3.11+ required.\n` +
|
||||
' Install Python 3.11+ from https://python.org'
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Platform-specific candidate order
|
||||
const candidates = IS_WIN
|
||||
? ['python', ['py', '-3'], 'python3']
|
||||
: ['python3', 'python'];
|
||||
|
||||
let bestTooOld = null;
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const result = tryPythonCandidate(candidate);
|
||||
if (!result) continue;
|
||||
|
||||
if (result.tooOld) {
|
||||
// Remember the first "too old" result for a better error message
|
||||
if (!bestTooOld) bestTooOld = result;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verify venv module is available (Debian/Ubuntu may need python3-venv)
|
||||
try {
|
||||
const exeParts = result.exe.split(' ');
|
||||
execFileSync(exeParts[0], [...exeParts.slice(1), '-c', 'import ensurepip'], {
|
||||
encoding: 'utf8',
|
||||
timeout: 10_000,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
} catch {
|
||||
die(
|
||||
`Python venv module not available.\n` +
|
||||
` Run: sudo apt install python3.${result.version.minor}-venv`
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Provide the most helpful error message we can
|
||||
if (bestTooOld) {
|
||||
die(
|
||||
`Python ${bestTooOld.version.raw} found, but 3.11+ required.\n` +
|
||||
' Install Python 3.11+ from https://python.org'
|
||||
);
|
||||
}
|
||||
die(
|
||||
'Python 3.11+ required but not found.\n' +
|
||||
' Install from https://python.org'
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Venv management
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Return the path to the Python executable inside the venv. */
|
||||
function venvPython() {
|
||||
return IS_WIN
|
||||
? join(VENV_DIR, 'Scripts', 'python.exe')
|
||||
: join(VENV_DIR, 'bin', 'python');
|
||||
}
|
||||
|
||||
/** SHA-256 hash of the requirements-prod.txt file contents. */
|
||||
function requirementsHash() {
|
||||
const content = readFileSync(REQUIREMENTS_FILE, 'utf8');
|
||||
return createHash('sha256').update(content).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the composite deps marker. Returns the parsed JSON object
|
||||
* or null if the file is missing / corrupt.
|
||||
*/
|
||||
function readMarker() {
|
||||
try {
|
||||
return JSON.parse(readFileSync(DEPS_MARKER, 'utf8'));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the virtual environment exists and dependencies are installed.
|
||||
* Returns true if all setup steps were already satisfied (fast path).
|
||||
*
|
||||
* @param {object} python - The result of findPython()
|
||||
* @param {boolean} forceRecreate - If true, delete and recreate the venv
|
||||
*/
|
||||
function ensureVenv(python, forceRecreate) {
|
||||
mkdirSync(CONFIG_HOME, { recursive: true });
|
||||
|
||||
const marker = readMarker();
|
||||
const reqHash = requirementsHash();
|
||||
const pyExe = venvPython();
|
||||
|
||||
// Determine if the venv itself needs to be (re)created
|
||||
let needsCreate = forceRecreate || !existsSync(pyExe);
|
||||
|
||||
if (!needsCreate && marker) {
|
||||
// Recreate if Python major.minor changed
|
||||
const markerMinor = marker.python_version;
|
||||
const currentMinor = `${python.version.major}.${python.version.minor}`;
|
||||
if (markerMinor && markerMinor !== currentMinor) {
|
||||
needsCreate = true;
|
||||
}
|
||||
|
||||
// Recreate if the recorded python path no longer exists
|
||||
if (marker.python_path && !existsSync(marker.python_path)) {
|
||||
needsCreate = true;
|
||||
}
|
||||
}
|
||||
|
||||
let depsUpToDate = false;
|
||||
if (!needsCreate && marker && marker.requirements_hash === reqHash) {
|
||||
depsUpToDate = true;
|
||||
}
|
||||
|
||||
// Fast path: nothing to do
|
||||
if (!needsCreate && depsUpToDate) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Slow path: show setup progress ---
|
||||
|
||||
log('[2/3] Setting up environment...');
|
||||
|
||||
if (needsCreate) {
|
||||
if (existsSync(VENV_DIR)) {
|
||||
log(' Removing old virtual environment...');
|
||||
rmSync(VENV_DIR, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
log(` Creating virtual environment at ~/.autoforge/venv/`);
|
||||
const exeParts = python.exe.split(' ');
|
||||
try {
|
||||
execFileSync(exeParts[0], [...exeParts.slice(1), '-m', 'venv', VENV_DIR], {
|
||||
encoding: 'utf8',
|
||||
timeout: 120_000,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
} catch (err) {
|
||||
die(`Failed to create virtual environment: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Install / update dependencies
|
||||
log(' Installing dependencies...');
|
||||
try {
|
||||
execFileSync(pyExe, ['-m', 'pip', 'install', '-q', '--upgrade', 'pip'], {
|
||||
encoding: 'utf8',
|
||||
timeout: 300_000,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
execFileSync(pyExe, ['-m', 'pip', 'install', '-q', '-r', REQUIREMENTS_FILE], {
|
||||
encoding: 'utf8',
|
||||
timeout: 600_000,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
} catch (err) {
|
||||
die(`Failed to install dependencies: ${err.message}`);
|
||||
}
|
||||
|
||||
// Write marker only after pip succeeds to prevent partial state
|
||||
const markerData = {
|
||||
requirements_hash: reqHash,
|
||||
python_version: `${python.version.major}.${python.version.minor}`,
|
||||
python_path: pyExe,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
writeFileSync(DEPS_MARKER, JSON.stringify(markerData, null, 2), 'utf8');
|
||||
|
||||
log(' Done');
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config (.env) management
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Parse a .env file into a plain object.
|
||||
* Handles comments, blank lines, and quoted values.
|
||||
*/
|
||||
function parseEnvFile(filePath) {
|
||||
const env = {};
|
||||
if (!existsSync(filePath)) return env;
|
||||
|
||||
const lines = readFileSync(filePath, 'utf8').split('\n');
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
|
||||
const eqIdx = trimmed.indexOf('=');
|
||||
if (eqIdx === -1) continue;
|
||||
|
||||
const key = trimmed.slice(0, eqIdx).trim();
|
||||
let value = trimmed.slice(eqIdx + 1).trim();
|
||||
|
||||
// Strip matching quotes (single or double)
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
|
||||
if (key) {
|
||||
env[key] = value;
|
||||
}
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure ~/.autoforge/.env exists. On first run, copy .env.example
|
||||
* from the package directory and print a notice.
|
||||
*
|
||||
* Returns true if the file was newly created.
|
||||
*/
|
||||
function ensureEnvFile() {
|
||||
if (existsSync(ENV_FILE)) return false;
|
||||
|
||||
mkdirSync(CONFIG_HOME, { recursive: true });
|
||||
|
||||
if (existsSync(ENV_EXAMPLE)) {
|
||||
copyFileSync(ENV_EXAMPLE, ENV_FILE);
|
||||
} else {
|
||||
// Fallback: create a minimal placeholder
|
||||
writeFileSync(ENV_FILE, '# AutoForge configuration\n# See documentation for available options.\n', 'utf8');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Port detection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Find an available TCP port starting from `start`.
|
||||
* Tries by actually binding a socket (most reliable cross-platform approach).
|
||||
*/
|
||||
function findAvailablePort(start = 8888, maxAttempts = 20) {
|
||||
for (let port = start; port < start + maxAttempts; port++) {
|
||||
try {
|
||||
const server = createServer();
|
||||
// Use a synchronous-like approach: try to listen, then close immediately
|
||||
const result = new Promise((resolve, reject) => {
|
||||
server.once('error', reject);
|
||||
server.listen(port, '127.0.0.1', () => {
|
||||
server.close(() => resolve(port));
|
||||
});
|
||||
});
|
||||
// We cannot await here (sync context), so use the blocking approach:
|
||||
// Try to bind synchronously using a different technique.
|
||||
server.close();
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
// Synchronous fallback: try to connect; if connection refused, port is free.
|
||||
for (let port = start; port < start + maxAttempts; port++) {
|
||||
try {
|
||||
execFileSync(process.execPath, [
|
||||
'-e',
|
||||
`const s=require("net").createServer();` +
|
||||
`s.listen(${port},"127.0.0.1",()=>{s.close();process.exit(0)});` +
|
||||
`s.on("error",()=>process.exit(1))`,
|
||||
], { timeout: 3000, stdio: 'pipe' });
|
||||
return port;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
die(`No available ports found in range ${start}-${start + maxAttempts - 1}`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PID file management
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Read PID from the PID file. Returns the PID number or null. */
|
||||
function readPid() {
|
||||
try {
|
||||
const content = readFileSync(PID_FILE, 'utf8').trim();
|
||||
const pid = Number(content);
|
||||
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Check whether a process with the given PID is still running. */
|
||||
function isProcessAlive(pid) {
|
||||
try {
|
||||
process.kill(pid, 0); // signal 0 = existence check
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Write the PID file. */
|
||||
function writePid(pid) {
|
||||
mkdirSync(CONFIG_HOME, { recursive: true });
|
||||
writeFileSync(PID_FILE, String(pid), 'utf8');
|
||||
}
|
||||
|
||||
/** Remove the PID file. */
|
||||
function removePid() {
|
||||
try {
|
||||
unlinkSync(PID_FILE);
|
||||
} catch {
|
||||
// Ignore -- file may already be gone
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Browser opening
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Open a URL in the user's default browser (best-effort). */
|
||||
function openBrowser(url) {
|
||||
try {
|
||||
if (IS_WIN) {
|
||||
// "start" is a cmd built-in; the empty title string avoids
|
||||
// issues when the URL contains special characters.
|
||||
execSync(`start "" "${url}"`, { stdio: 'ignore' });
|
||||
} else if (platform() === 'darwin') {
|
||||
execFileSync('open', [url], { stdio: 'ignore' });
|
||||
} else {
|
||||
// Linux: only attempt if a display server is available and
|
||||
// we are not in an SSH session.
|
||||
const hasDisplay = process.env.DISPLAY || process.env.WAYLAND_DISPLAY;
|
||||
const isSSH = !!process.env.SSH_TTY;
|
||||
if (hasDisplay && !isSSH) {
|
||||
execFileSync('xdg-open', [url], { stdio: 'ignore' });
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Non-fatal: user can open the URL manually
|
||||
}
|
||||
}
|
||||
|
||||
/** Detect headless / CI environments where opening a browser is pointless. */
|
||||
function isHeadless() {
|
||||
if (process.env.CI) return true;
|
||||
if (process.env.CODESPACES) return true;
|
||||
if (process.env.SSH_TTY) return true;
|
||||
// Linux without a display server
|
||||
if (!IS_WIN && platform() !== 'darwin' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Process cleanup
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Kill a process tree. On Windows uses taskkill; elsewhere sends SIGTERM. */
|
||||
function killProcess(pid) {
|
||||
try {
|
||||
if (IS_WIN) {
|
||||
execSync(`taskkill /pid ${pid} /t /f`, { stdio: 'ignore' });
|
||||
} else {
|
||||
process.kill(pid, 'SIGTERM');
|
||||
}
|
||||
} catch {
|
||||
// Process may already be gone
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CLI commands
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function printVersion() {
|
||||
console.log(`autoforge v${VERSION}`);
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
console.log(`
|
||||
AutoForge v${VERSION}
|
||||
Autonomous coding agent with web UI
|
||||
|
||||
Usage:
|
||||
autoforge Start the server (default)
|
||||
autoforge config Open ~/.autoforge/.env in $EDITOR
|
||||
autoforge config --path Print config file path
|
||||
autoforge config --show Show effective configuration
|
||||
|
||||
Options:
|
||||
--port PORT Custom port (default: auto from 8888)
|
||||
--host HOST Custom host (default: 127.0.0.1)
|
||||
--no-browser Don't auto-open browser
|
||||
--repair Delete and recreate virtual environment
|
||||
--dev Development mode (requires cloned repo)
|
||||
--version Print version
|
||||
--help Show this help
|
||||
`);
|
||||
}
|
||||
|
||||
function handleConfig(args) {
|
||||
ensureEnvFile();
|
||||
|
||||
if (args.includes('--path')) {
|
||||
console.log(ENV_FILE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.includes('--show')) {
|
||||
if (!existsSync(ENV_FILE)) {
|
||||
log('No configuration file found.');
|
||||
return;
|
||||
}
|
||||
const lines = readFileSync(ENV_FILE, 'utf8').split('\n');
|
||||
const active = lines.filter(l => {
|
||||
const t = l.trim();
|
||||
return t && !t.startsWith('#');
|
||||
});
|
||||
if (active.length === 0) {
|
||||
log('No active configuration. All lines are commented out.');
|
||||
log(`Edit: ${ENV_FILE}`);
|
||||
} else {
|
||||
for (const line of active) {
|
||||
console.log(line);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Open in editor
|
||||
const editor = process.env.EDITOR || process.env.VISUAL || (IS_WIN ? 'notepad' : 'vi');
|
||||
try {
|
||||
execFileSync(editor, [ENV_FILE], { stdio: 'inherit' });
|
||||
} catch {
|
||||
log(`Could not open editor "${editor}".`);
|
||||
log(`Edit the file manually: ${ENV_FILE}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main server start
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function startServer(opts) {
|
||||
const { port: requestedPort, host, noBrowser, repair } = opts;
|
||||
|
||||
// Step 1: Find Python
|
||||
const fastPath = !repair && existsSync(venvPython()) && readMarker()?.requirements_hash === requirementsHash();
|
||||
|
||||
let python;
|
||||
if (fastPath) {
|
||||
// Skip the Python search header on fast path -- we already have a working venv
|
||||
python = null;
|
||||
} else {
|
||||
log(`[1/3] Checking Python...`);
|
||||
python = findPython();
|
||||
log(` Found Python ${python.version.raw} at ${python.exe}`);
|
||||
}
|
||||
|
||||
// Step 2: Ensure venv and deps
|
||||
if (!python) {
|
||||
// Fast path still needs a python reference for potential repair
|
||||
python = findPython();
|
||||
}
|
||||
const wasAlreadyReady = ensureVenv(python, repair);
|
||||
|
||||
// Step 3: Config file
|
||||
const configCreated = ensureEnvFile();
|
||||
|
||||
// Load .env into process.env for the spawned server
|
||||
const dotenvVars = parseEnvFile(ENV_FILE);
|
||||
|
||||
// Determine port
|
||||
const port = requestedPort || findAvailablePort();
|
||||
|
||||
// Check for already-running instance
|
||||
const existingPid = readPid();
|
||||
if (existingPid && isProcessAlive(existingPid)) {
|
||||
log(`AutoForge is already running at http://${host}:${port}`);
|
||||
log('Opening browser...');
|
||||
if (!noBrowser && !isHeadless()) {
|
||||
openBrowser(`http://${host}:${port}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean up stale PID file
|
||||
if (existingPid) {
|
||||
removePid();
|
||||
}
|
||||
|
||||
// Show server startup step only on slow path
|
||||
if (!wasAlreadyReady) {
|
||||
log('[3/3] Starting server...');
|
||||
}
|
||||
|
||||
if (configCreated) {
|
||||
log(` Created config file: ~/.autoforge/.env`);
|
||||
log(' Edit this file to configure API providers (Ollama, Vertex AI, z.ai)');
|
||||
log('');
|
||||
}
|
||||
|
||||
// Security warning for non-localhost host
|
||||
if (host !== '127.0.0.1') {
|
||||
console.log('');
|
||||
console.log(' !! SECURITY WARNING !!');
|
||||
console.log(` Remote access enabled on host: ${host}`);
|
||||
console.log(' The AutoForge UI will be accessible from other machines.');
|
||||
console.log(' Ensure you understand the security implications.');
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Build environment for uvicorn
|
||||
const serverEnv = { ...process.env, ...dotenvVars, PYTHONPATH: PKG_DIR };
|
||||
|
||||
// Enable remote access flag for the FastAPI server
|
||||
if (host !== '127.0.0.1') {
|
||||
serverEnv.AUTOFORGE_ALLOW_REMOTE = '1';
|
||||
}
|
||||
|
||||
// Spawn uvicorn
|
||||
const pyExe = venvPython();
|
||||
const child = spawn(
|
||||
pyExe,
|
||||
[
|
||||
'-m', 'uvicorn',
|
||||
'server.main:app',
|
||||
'--host', host,
|
||||
'--port', String(port),
|
||||
],
|
||||
{
|
||||
cwd: PKG_DIR,
|
||||
env: serverEnv,
|
||||
stdio: 'inherit',
|
||||
}
|
||||
);
|
||||
|
||||
writePid(child.pid);
|
||||
|
||||
// Open browser after a short delay to let the server start
|
||||
if (!noBrowser && !isHeadless()) {
|
||||
setTimeout(() => openBrowser(`http://${host}:${port}`), 2000);
|
||||
}
|
||||
|
||||
const url = `http://${host}:${port}`;
|
||||
console.log('');
|
||||
log(`Server running at ${url}`);
|
||||
log('Press Ctrl+C to stop');
|
||||
|
||||
// Graceful shutdown handlers
|
||||
const cleanup = () => {
|
||||
killProcess(child.pid);
|
||||
removePid();
|
||||
};
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('');
|
||||
cleanup();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
cleanup();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// If the child exits on its own, clean up and propagate the exit code
|
||||
child.on('exit', (code) => {
|
||||
removePid();
|
||||
process.exit(code ?? 1);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Entry point
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Main CLI entry point.
|
||||
*
|
||||
* @param {string[]} args - Command-line arguments (process.argv.slice(2))
|
||||
*/
|
||||
export function run(args) {
|
||||
// --version / -v
|
||||
if (args.includes('--version') || args.includes('-v')) {
|
||||
printVersion();
|
||||
return;
|
||||
}
|
||||
|
||||
// --help / -h
|
||||
if (args.includes('--help') || args.includes('-h')) {
|
||||
printHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
// --dev guard: this only works from a cloned repository
|
||||
if (args.includes('--dev')) {
|
||||
die(
|
||||
'Dev mode requires a cloned repository.\n' +
|
||||
' Clone from https://github.com/paperlinguist/autocoder and run start_ui.sh'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// "config" subcommand
|
||||
if (args[0] === 'config') {
|
||||
handleConfig(args.slice(1));
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse flags for server start
|
||||
const host = getFlagValue(args, '--host') || '127.0.0.1';
|
||||
const portStr = getFlagValue(args, '--port');
|
||||
const port = portStr ? Number(portStr) : null;
|
||||
const noBrowser = args.includes('--no-browser');
|
||||
const repair = args.includes('--repair');
|
||||
|
||||
if (port !== null && (!Number.isFinite(port) || port < 1 || port > 65535)) {
|
||||
die('Invalid port number. Must be between 1 and 65535.');
|
||||
}
|
||||
|
||||
// Print banner
|
||||
console.log('');
|
||||
log(`AutoForge v${VERSION}`);
|
||||
console.log('');
|
||||
|
||||
startServer({ port, host, noBrowser, repair });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Argument parsing helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Extract the value following a flag from the args array.
|
||||
* E.g. getFlagValue(['--port', '9000', '--host', '0.0.0.0'], '--port') => '9000'
|
||||
*/
|
||||
function getFlagValue(args, flag) {
|
||||
const idx = args.indexOf(flag);
|
||||
if (idx === -1 || idx + 1 >= args.length) return null;
|
||||
return args[idx + 1];
|
||||
}
|
||||
53
package.json
Normal file
53
package.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "autoforge-ai",
|
||||
"version": "0.1.1",
|
||||
"description": "Autonomous coding agent with web UI - build complete apps with AI",
|
||||
"license": "AGPL-3.0",
|
||||
"bin": {
|
||||
"autoforge": "./bin/autoforge.js"
|
||||
},
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"files": [
|
||||
"bin/",
|
||||
"lib/",
|
||||
"api/",
|
||||
"server/",
|
||||
"mcp_server/",
|
||||
"ui/dist/",
|
||||
"ui/package.json",
|
||||
".claude/commands/",
|
||||
".claude/templates/",
|
||||
"examples/",
|
||||
"start.py",
|
||||
"agent.py",
|
||||
"auth.py",
|
||||
"autoforge_paths.py",
|
||||
"autonomous_agent_demo.py",
|
||||
"client.py",
|
||||
"env_constants.py",
|
||||
"parallel_orchestrator.py",
|
||||
"progress.py",
|
||||
"prompts.py",
|
||||
"registry.py",
|
||||
"rate_limit_utils.py",
|
||||
"security.py",
|
||||
"requirements-prod.txt",
|
||||
"pyproject.toml",
|
||||
".env.example",
|
||||
"!**/__pycache__/",
|
||||
"!**/*.pyc"
|
||||
],
|
||||
"keywords": [
|
||||
"ai",
|
||||
"coding-agent",
|
||||
"claude",
|
||||
"autonomous",
|
||||
"code-generation"
|
||||
],
|
||||
"scripts": {
|
||||
"prepublishOnly": "npm --prefix ui install && npm --prefix ui run build"
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import asyncio
|
||||
import atexit
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -39,11 +40,11 @@ from server.utils.process_utils import kill_process_tree
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Root directory of autocoder (where this script and autonomous_agent_demo.py live)
|
||||
AUTOCODER_ROOT = Path(__file__).parent.resolve()
|
||||
# Root directory of autoforge (where this script and autonomous_agent_demo.py live)
|
||||
AUTOFORGE_ROOT = Path(__file__).parent.resolve()
|
||||
|
||||
# Debug log file path
|
||||
DEBUG_LOG_FILE = AUTOCODER_ROOT / "orchestrator_debug.log"
|
||||
DEBUG_LOG_FILE = AUTOFORGE_ROOT / "orchestrator_debug.log"
|
||||
|
||||
|
||||
class DebugLogger:
|
||||
@@ -822,7 +823,7 @@ class ParallelOrchestrator:
|
||||
cmd = [
|
||||
sys.executable,
|
||||
"-u", # Force unbuffered stdout/stderr
|
||||
str(AUTOCODER_ROOT / "autonomous_agent_demo.py"),
|
||||
str(AUTOFORGE_ROOT / "autonomous_agent_demo.py"),
|
||||
"--project-dir", str(self.project_dir),
|
||||
"--max-iterations", "1",
|
||||
"--agent-type", "coding",
|
||||
@@ -844,7 +845,7 @@ class ParallelOrchestrator:
|
||||
"text": True,
|
||||
"encoding": "utf-8",
|
||||
"errors": "replace",
|
||||
"cwd": str(AUTOCODER_ROOT), # Run from autocoder root for proper imports
|
||||
"cwd": str(self.project_dir), # Run from project dir so CLI creates .claude/ in project
|
||||
"env": {**os.environ, "PYTHONUNBUFFERED": "1"},
|
||||
}
|
||||
if sys.platform == "win32":
|
||||
@@ -888,7 +889,7 @@ class ParallelOrchestrator:
|
||||
cmd = [
|
||||
sys.executable,
|
||||
"-u",
|
||||
str(AUTOCODER_ROOT / "autonomous_agent_demo.py"),
|
||||
str(AUTOFORGE_ROOT / "autonomous_agent_demo.py"),
|
||||
"--project-dir", str(self.project_dir),
|
||||
"--max-iterations", "1",
|
||||
"--agent-type", "coding",
|
||||
@@ -907,7 +908,7 @@ class ParallelOrchestrator:
|
||||
"text": True,
|
||||
"encoding": "utf-8",
|
||||
"errors": "replace",
|
||||
"cwd": str(AUTOCODER_ROOT),
|
||||
"cwd": str(self.project_dir), # Run from project dir so CLI creates .claude/ in project
|
||||
"env": {**os.environ, "PYTHONUNBUFFERED": "1"},
|
||||
}
|
||||
if sys.platform == "win32":
|
||||
@@ -991,7 +992,7 @@ class ParallelOrchestrator:
|
||||
cmd = [
|
||||
sys.executable,
|
||||
"-u",
|
||||
str(AUTOCODER_ROOT / "autonomous_agent_demo.py"),
|
||||
str(AUTOFORGE_ROOT / "autonomous_agent_demo.py"),
|
||||
"--project-dir", str(self.project_dir),
|
||||
"--max-iterations", "1",
|
||||
"--agent-type", "testing",
|
||||
@@ -1011,7 +1012,7 @@ class ParallelOrchestrator:
|
||||
"text": True,
|
||||
"encoding": "utf-8",
|
||||
"errors": "replace",
|
||||
"cwd": str(AUTOCODER_ROOT),
|
||||
"cwd": str(self.project_dir), # Run from project dir so CLI creates .claude/ in project
|
||||
"env": {**os.environ, "PYTHONUNBUFFERED": "1"},
|
||||
}
|
||||
if sys.platform == "win32":
|
||||
@@ -1052,7 +1053,7 @@ class ParallelOrchestrator:
|
||||
|
||||
cmd = [
|
||||
sys.executable, "-u",
|
||||
str(AUTOCODER_ROOT / "autonomous_agent_demo.py"),
|
||||
str(AUTOFORGE_ROOT / "autonomous_agent_demo.py"),
|
||||
"--project-dir", str(self.project_dir),
|
||||
"--agent-type", "initializer",
|
||||
"--max-iterations", "1",
|
||||
@@ -1072,7 +1073,7 @@ class ParallelOrchestrator:
|
||||
"text": True,
|
||||
"encoding": "utf-8",
|
||||
"errors": "replace",
|
||||
"cwd": str(AUTOCODER_ROOT),
|
||||
"cwd": str(AUTOFORGE_ROOT),
|
||||
"env": {**os.environ, "PYTHONUNBUFFERED": "1"},
|
||||
}
|
||||
if sys.platform == "win32":
|
||||
@@ -1116,6 +1117,11 @@ class ParallelOrchestrator:
|
||||
|
||||
return True
|
||||
|
||||
# Pattern to detect when a batch agent claims a new feature
|
||||
_CLAIM_FEATURE_PATTERN = re.compile(
|
||||
r"feature_claim_and_get\b.*?['\"]?feature_id['\"]?\s*[:=]\s*(\d+)"
|
||||
)
|
||||
|
||||
def _read_output(
|
||||
self,
|
||||
feature_id: int | None,
|
||||
@@ -1124,6 +1130,7 @@ class ParallelOrchestrator:
|
||||
agent_type: Literal["coding", "testing"] = "coding",
|
||||
):
|
||||
"""Read output from subprocess and emit events."""
|
||||
current_feature_id = feature_id
|
||||
try:
|
||||
if proc.stdout is None:
|
||||
proc.wait()
|
||||
@@ -1132,11 +1139,17 @@ class ParallelOrchestrator:
|
||||
if abort.is_set():
|
||||
break
|
||||
line = line.rstrip()
|
||||
# Detect when a batch agent claims a new feature
|
||||
claim_match = self._CLAIM_FEATURE_PATTERN.search(line)
|
||||
if claim_match:
|
||||
claimed_id = int(claim_match.group(1))
|
||||
if claimed_id != current_feature_id:
|
||||
current_feature_id = claimed_id
|
||||
if self.on_output is not None:
|
||||
self.on_output(feature_id or 0, line)
|
||||
self.on_output(current_feature_id or 0, line)
|
||||
else:
|
||||
# Both coding and testing agents now use [Feature #X] format
|
||||
print(f"[Feature #{feature_id}] {line}", flush=True)
|
||||
print(f"[Feature #{current_feature_id}] {line}", flush=True)
|
||||
proc.wait()
|
||||
finally:
|
||||
# CRITICAL: Kill the process tree to clean up any child processes (e.g., Claude CLI)
|
||||
|
||||
@@ -46,7 +46,7 @@ def has_features(project_dir: Path) -> bool:
|
||||
return True
|
||||
|
||||
# Check SQLite database
|
||||
from autocoder_paths import get_features_db_path
|
||||
from autoforge_paths import get_features_db_path
|
||||
db_file = get_features_db_path(project_dir)
|
||||
if not db_file.exists():
|
||||
return False
|
||||
@@ -72,7 +72,7 @@ def count_passing_tests(project_dir: Path) -> tuple[int, int, int]:
|
||||
Returns:
|
||||
(passing_count, in_progress_count, total_count)
|
||||
"""
|
||||
from autocoder_paths import get_features_db_path
|
||||
from autoforge_paths import get_features_db_path
|
||||
db_file = get_features_db_path(project_dir)
|
||||
if not db_file.exists():
|
||||
return 0, 0, 0
|
||||
@@ -122,7 +122,7 @@ def get_all_passing_features(project_dir: Path) -> list[dict]:
|
||||
Returns:
|
||||
List of dicts with id, category, name for each passing feature
|
||||
"""
|
||||
from autocoder_paths import get_features_db_path
|
||||
from autoforge_paths import get_features_db_path
|
||||
db_file = get_features_db_path(project_dir)
|
||||
if not db_file.exists():
|
||||
return []
|
||||
@@ -147,7 +147,7 @@ def send_progress_webhook(passing: int, total: int, project_dir: Path) -> None:
|
||||
if not WEBHOOK_URL:
|
||||
return # Webhook not configured
|
||||
|
||||
from autocoder_paths import get_progress_cache_path
|
||||
from autoforge_paths import get_progress_cache_path
|
||||
cache_file = get_progress_cache_path(project_dir)
|
||||
previous = 0
|
||||
previous_passing_ids = set()
|
||||
|
||||
14
prompts.py
14
prompts.py
@@ -19,7 +19,7 @@ TEMPLATES_DIR = Path(__file__).parent / ".claude" / "templates"
|
||||
|
||||
def get_project_prompts_dir(project_dir: Path) -> Path:
|
||||
"""Get the prompts directory for a specific project."""
|
||||
from autocoder_paths import get_prompts_dir
|
||||
from autoforge_paths import get_prompts_dir
|
||||
return get_prompts_dir(project_dir)
|
||||
|
||||
|
||||
@@ -315,9 +315,9 @@ def scaffold_project_prompts(project_dir: Path) -> Path:
|
||||
project_prompts = get_project_prompts_dir(project_dir)
|
||||
project_prompts.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create .autocoder directory with .gitignore for runtime files
|
||||
from autocoder_paths import ensure_autocoder_dir
|
||||
autocoder_dir = ensure_autocoder_dir(project_dir)
|
||||
# Create .autoforge directory with .gitignore for runtime files
|
||||
from autoforge_paths import ensure_autoforge_dir
|
||||
autoforge_dir = ensure_autoforge_dir(project_dir)
|
||||
|
||||
# Define template mappings: (source_template, destination_name)
|
||||
templates = [
|
||||
@@ -340,14 +340,14 @@ def scaffold_project_prompts(project_dir: Path) -> Path:
|
||||
except (OSError, PermissionError) as e:
|
||||
print(f" Warning: Could not copy {dest_name}: {e}")
|
||||
|
||||
# Copy allowed_commands.yaml template to .autocoder/
|
||||
# Copy allowed_commands.yaml template to .autoforge/
|
||||
examples_dir = Path(__file__).parent / "examples"
|
||||
allowed_commands_template = examples_dir / "project_allowed_commands.yaml"
|
||||
allowed_commands_dest = autocoder_dir / "allowed_commands.yaml"
|
||||
allowed_commands_dest = autoforge_dir / "allowed_commands.yaml"
|
||||
if allowed_commands_template.exists() and not allowed_commands_dest.exists():
|
||||
try:
|
||||
shutil.copy(allowed_commands_template, allowed_commands_dest)
|
||||
copied_files.append(".autocoder/allowed_commands.yaml")
|
||||
copied_files.append(".autoforge/allowed_commands.yaml")
|
||||
except (OSError, PermissionError) as e:
|
||||
print(f" Warning: Could not copy allowed_commands.yaml: {e}")
|
||||
|
||||
|
||||
27
registry.py
27
registry.py
@@ -3,7 +3,7 @@ Project Registry Module
|
||||
=======================
|
||||
|
||||
Cross-platform project registry for storing project name to path mappings.
|
||||
Uses SQLite database stored at ~/.autocoder/registry.db.
|
||||
Uses SQLite database stored at ~/.autoforge/registry.db.
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -23,6 +23,22 @@ from sqlalchemy.orm import DeclarativeBase, sessionmaker
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _migrate_registry_dir() -> None:
|
||||
"""Migrate ~/.autocoder/ to ~/.autoforge/ if needed.
|
||||
|
||||
Provides backward compatibility by automatically renaming the old
|
||||
config directory to the new location on first access.
|
||||
"""
|
||||
old_dir = Path.home() / ".autocoder"
|
||||
new_dir = Path.home() / ".autoforge"
|
||||
if old_dir.exists() and not new_dir.exists():
|
||||
try:
|
||||
old_dir.rename(new_dir)
|
||||
logger.info("Migrated registry directory: ~/.autocoder/ -> ~/.autoforge/")
|
||||
except Exception:
|
||||
logger.warning("Failed to migrate ~/.autocoder/ to ~/.autoforge/", exc_info=True)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Model Configuration (Single Source of Truth)
|
||||
# =============================================================================
|
||||
@@ -120,12 +136,15 @@ _engine_lock = threading.Lock()
|
||||
|
||||
def get_config_dir() -> Path:
|
||||
"""
|
||||
Get the config directory: ~/.autocoder/
|
||||
Get the config directory: ~/.autoforge/
|
||||
|
||||
Automatically migrates from ~/.autocoder/ if needed.
|
||||
|
||||
Returns:
|
||||
Path to ~/.autocoder/ (created if it doesn't exist)
|
||||
Path to ~/.autoforge/ (created if it doesn't exist)
|
||||
"""
|
||||
config_dir = Path.home() / ".autocoder"
|
||||
_migrate_registry_dir()
|
||||
config_dir = Path.home() / ".autoforge"
|
||||
config_dir.mkdir(parents=True, exist_ok=True)
|
||||
return config_dir
|
||||
|
||||
|
||||
14
requirements-prod.txt
Normal file
14
requirements-prod.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
# Production runtime dependencies only
|
||||
# For development, use requirements.txt (includes ruff, mypy, pytest)
|
||||
claude-agent-sdk>=0.1.0,<0.2.0
|
||||
python-dotenv>=1.0.0
|
||||
sqlalchemy>=2.0.0
|
||||
fastapi>=0.115.0
|
||||
uvicorn[standard]>=0.32.0
|
||||
websockets>=13.0
|
||||
python-multipart>=0.0.17
|
||||
psutil>=6.0.0
|
||||
aiofiles>=24.0.0
|
||||
apscheduler>=3.10.0,<4.0.0
|
||||
pywinpty>=2.0.0; sys_platform == "win32"
|
||||
pyyaml>=6.0.0
|
||||
22
security.py
22
security.py
@@ -553,14 +553,23 @@ def get_org_config_path() -> Path:
|
||||
Get the organization-level config file path.
|
||||
|
||||
Returns:
|
||||
Path to ~/.autocoder/config.yaml
|
||||
Path to ~/.autoforge/config.yaml (falls back to ~/.autocoder/config.yaml)
|
||||
"""
|
||||
return Path.home() / ".autocoder" / "config.yaml"
|
||||
new_path = Path.home() / ".autoforge" / "config.yaml"
|
||||
if new_path.exists():
|
||||
return new_path
|
||||
# Backward compatibility: check old location
|
||||
old_path = Path.home() / ".autocoder" / "config.yaml"
|
||||
if old_path.exists():
|
||||
return old_path
|
||||
return new_path
|
||||
|
||||
|
||||
def load_org_config() -> Optional[dict]:
|
||||
"""
|
||||
Load organization-level config from ~/.autocoder/config.yaml.
|
||||
Load organization-level config from ~/.autoforge/config.yaml.
|
||||
|
||||
Falls back to ~/.autocoder/config.yaml for backward compatibility.
|
||||
|
||||
Returns:
|
||||
Dict with parsed org config, or None if file doesn't exist or is invalid
|
||||
@@ -630,7 +639,10 @@ def load_project_commands(project_dir: Path) -> Optional[dict]:
|
||||
Returns:
|
||||
Dict with parsed YAML config, or None if file doesn't exist or is invalid
|
||||
"""
|
||||
config_path = project_dir.resolve() / ".autocoder" / "allowed_commands.yaml"
|
||||
# Check new location first, fall back to old for backward compatibility
|
||||
config_path = project_dir.resolve() / ".autoforge" / "allowed_commands.yaml"
|
||||
if not config_path.exists():
|
||||
config_path = project_dir.resolve() / ".autocoder" / "allowed_commands.yaml"
|
||||
|
||||
if not config_path.exists():
|
||||
return None
|
||||
@@ -909,7 +921,7 @@ async def bash_security_hook(input_data, tool_use_id=None, context=None):
|
||||
# Provide helpful error message with config hint
|
||||
error_msg = f"Command '{cmd}' is not allowed.\n"
|
||||
error_msg += "To allow this command:\n"
|
||||
error_msg += " 1. Add to .autocoder/allowed_commands.yaml for this project, OR\n"
|
||||
error_msg += " 1. Add to .autoforge/allowed_commands.yaml for this project, OR\n"
|
||||
error_msg += " 2. Request mid-session approval (the agent can ask)\n"
|
||||
error_msg += "Note: Some commands are blocked at org-level and cannot be overridden."
|
||||
return {
|
||||
|
||||
@@ -94,7 +94,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
# Check if remote access is enabled via environment variable
|
||||
# Set by start_ui.py when --host is not 127.0.0.1
|
||||
ALLOW_REMOTE = os.environ.get("AUTOCODER_ALLOW_REMOTE", "").lower() in ("1", "true", "yes")
|
||||
ALLOW_REMOTE = os.environ.get("AUTOFORGE_ALLOW_REMOTE", "").lower() in ("1", "true", "yes")
|
||||
|
||||
if ALLOW_REMOTE:
|
||||
logger.warning(
|
||||
@@ -133,7 +133,7 @@ else:
|
||||
if not ALLOW_REMOTE:
|
||||
@app.middleware("http")
|
||||
async def require_localhost(request: Request, call_next):
|
||||
"""Only allow requests from localhost (disabled when AUTOCODER_ALLOW_REMOTE=1)."""
|
||||
"""Only allow requests from localhost (disabled when AUTOFORGE_ALLOW_REMOTE=1)."""
|
||||
client_host = request.client.host if request.client else None
|
||||
|
||||
# Allow localhost connections
|
||||
|
||||
@@ -121,7 +121,7 @@ async def expand_project_websocket(websocket: WebSocket, project_name: str):
|
||||
return
|
||||
|
||||
# Verify project has app_spec.txt
|
||||
from autocoder_paths import get_prompts_dir
|
||||
from autoforge_paths import get_prompts_dir
|
||||
spec_path = get_prompts_dir(project_dir) / "app_spec.txt"
|
||||
if not spec_path.exists():
|
||||
await websocket.close(code=4004, reason="Project has no spec. Create spec first.")
|
||||
|
||||
@@ -126,7 +126,7 @@ async def list_features(project_name: str):
|
||||
if not project_dir.exists():
|
||||
raise HTTPException(status_code=404, detail="Project directory not found")
|
||||
|
||||
from autocoder_paths import get_features_db_path
|
||||
from autoforge_paths import get_features_db_path
|
||||
db_file = get_features_db_path(project_dir)
|
||||
if not db_file.exists():
|
||||
return FeatureListResponse(pending=[], in_progress=[], done=[])
|
||||
@@ -322,7 +322,7 @@ async def get_dependency_graph(project_name: str):
|
||||
if not project_dir.exists():
|
||||
raise HTTPException(status_code=404, detail="Project directory not found")
|
||||
|
||||
from autocoder_paths import get_features_db_path
|
||||
from autoforge_paths import get_features_db_path
|
||||
db_file = get_features_db_path(project_dir)
|
||||
if not db_file.exists():
|
||||
return DependencyGraphResponse(nodes=[], edges=[])
|
||||
@@ -388,7 +388,7 @@ async def get_feature(project_name: str, feature_id: int):
|
||||
if not project_dir.exists():
|
||||
raise HTTPException(status_code=404, detail="Project directory not found")
|
||||
|
||||
from autocoder_paths import get_features_db_path
|
||||
from autoforge_paths import get_features_db_path
|
||||
db_file = get_features_db_path(project_dir)
|
||||
if not db_file.exists():
|
||||
raise HTTPException(status_code=404, detail="No features database found")
|
||||
|
||||
@@ -276,7 +276,7 @@ async def delete_project(name: str, delete_files: bool = False):
|
||||
raise HTTPException(status_code=404, detail=f"Project '{name}' not found")
|
||||
|
||||
# Check if agent is running
|
||||
from autocoder_paths import has_agent_running
|
||||
from autoforge_paths import has_agent_running
|
||||
if has_agent_running(project_dir):
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
@@ -407,7 +407,7 @@ async def reset_project(name: str, full_reset: bool = False):
|
||||
raise HTTPException(status_code=404, detail="Project directory not found")
|
||||
|
||||
# Check if agent is running
|
||||
from autocoder_paths import has_agent_running
|
||||
from autoforge_paths import has_agent_running
|
||||
if has_agent_running(project_dir):
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
@@ -424,7 +424,7 @@ async def reset_project(name: str, full_reset: bool = False):
|
||||
|
||||
deleted_files: list[str] = []
|
||||
|
||||
from autocoder_paths import (
|
||||
from autoforge_paths import (
|
||||
get_assistant_db_path,
|
||||
get_claude_assistant_settings_path,
|
||||
get_claude_settings_path,
|
||||
@@ -466,7 +466,7 @@ async def reset_project(name: str, full_reset: bool = False):
|
||||
|
||||
# Full reset: also delete prompts directory
|
||||
if full_reset:
|
||||
from autocoder_paths import get_prompts_dir
|
||||
from autoforge_paths import get_prompts_dir
|
||||
# Delete prompts from both possible locations
|
||||
for prompts_dir in [get_prompts_dir(project_dir), project_dir / "prompts"]:
|
||||
if prompts_dir.exists():
|
||||
|
||||
@@ -105,7 +105,7 @@ async def get_spec_file_status(project_name: str):
|
||||
if not project_dir.exists():
|
||||
raise HTTPException(status_code=404, detail="Project directory not found")
|
||||
|
||||
from autocoder_paths import get_prompts_dir
|
||||
from autoforge_paths import get_prompts_dir
|
||||
status_file = get_prompts_dir(project_dir) / ".spec_status.json"
|
||||
|
||||
if not status_file.exists():
|
||||
|
||||
@@ -64,7 +64,7 @@ def get_system_prompt(project_name: str, project_dir: Path) -> str:
|
||||
"""Generate the system prompt for the assistant with project context."""
|
||||
# Try to load app_spec.txt for context
|
||||
app_spec_content = ""
|
||||
from autocoder_paths import get_prompts_dir
|
||||
from autoforge_paths import get_prompts_dir
|
||||
app_spec_path = get_prompts_dir(project_dir) / "app_spec.txt"
|
||||
if app_spec_path.exists():
|
||||
try:
|
||||
@@ -224,7 +224,7 @@ class AssistantChatSession:
|
||||
"allow": permissions_list,
|
||||
},
|
||||
}
|
||||
from autocoder_paths import get_claude_assistant_settings_path
|
||||
from autoforge_paths import get_claude_assistant_settings_path
|
||||
settings_file = get_claude_assistant_settings_path(self.project_dir)
|
||||
settings_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(settings_file, "w") as f:
|
||||
|
||||
@@ -64,7 +64,7 @@ class ConversationMessage(Base):
|
||||
|
||||
def get_db_path(project_dir: Path) -> Path:
|
||||
"""Get the path to the assistant database for a project."""
|
||||
from autocoder_paths import get_assistant_db_path
|
||||
from autoforge_paths import get_assistant_db_path
|
||||
return get_assistant_db_path(project_dir)
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from pathlib import Path
|
||||
from typing import AsyncGenerator
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Root directory of the autocoder project (repository root).
|
||||
# Root directory of the autoforge project (repository root).
|
||||
# Used throughout the server package whenever the repo root is needed.
|
||||
# -------------------------------------------------------------------
|
||||
ROOT_DIR = Path(__file__).parent.parent.parent
|
||||
|
||||
@@ -115,7 +115,7 @@ class DevServerProcessManager:
|
||||
self._callbacks_lock = threading.Lock()
|
||||
|
||||
# Lock file to prevent multiple instances (stored in project directory)
|
||||
from autocoder_paths import get_devserver_lock_path
|
||||
from autoforge_paths import get_devserver_lock_path
|
||||
self.lock_file = get_devserver_lock_path(self.project_dir)
|
||||
|
||||
@property
|
||||
@@ -504,10 +504,10 @@ def cleanup_orphaned_devserver_locks() -> int:
|
||||
continue
|
||||
|
||||
# Check both legacy and new locations for lock files
|
||||
from autocoder_paths import get_autocoder_dir
|
||||
from autoforge_paths import get_autoforge_dir
|
||||
lock_locations = [
|
||||
project_path / ".devserver.lock",
|
||||
get_autocoder_dir(project_path) / ".devserver.lock",
|
||||
get_autoforge_dir(project_path) / ".devserver.lock",
|
||||
]
|
||||
lock_file = None
|
||||
for candidate in lock_locations:
|
||||
|
||||
@@ -103,7 +103,7 @@ class ExpandChatSession:
|
||||
return
|
||||
|
||||
# Verify project has existing spec
|
||||
from autocoder_paths import get_prompts_dir
|
||||
from autoforge_paths import get_prompts_dir
|
||||
spec_path = get_prompts_dir(self.project_dir) / "app_spec.txt"
|
||||
if not spec_path.exists():
|
||||
yield {
|
||||
@@ -142,7 +142,7 @@ class ExpandChatSession:
|
||||
],
|
||||
},
|
||||
}
|
||||
from autocoder_paths import get_expand_settings_path
|
||||
from autoforge_paths import get_expand_settings_path
|
||||
settings_file = get_expand_settings_path(self.project_dir, uuid.uuid4().hex)
|
||||
settings_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
self._settings_file = settings_file
|
||||
|
||||
@@ -92,7 +92,7 @@ class AgentProcessManager:
|
||||
self._callbacks_lock = threading.Lock()
|
||||
|
||||
# Lock file to prevent multiple instances (stored in project directory)
|
||||
from autocoder_paths import get_agent_lock_path
|
||||
from autoforge_paths import get_agent_lock_path
|
||||
self.lock_file = get_agent_lock_path(self.project_dir)
|
||||
|
||||
@property
|
||||
@@ -587,10 +587,10 @@ def cleanup_orphaned_locks() -> int:
|
||||
continue
|
||||
|
||||
# Check both legacy and new locations for lock files
|
||||
from autocoder_paths import get_autocoder_dir
|
||||
from autoforge_paths import get_autoforge_dir
|
||||
lock_locations = [
|
||||
project_path / ".agent.lock",
|
||||
get_autocoder_dir(project_path) / ".agent.lock",
|
||||
get_autoforge_dir(project_path) / ".agent.lock",
|
||||
]
|
||||
lock_file = None
|
||||
for candidate in lock_locations:
|
||||
|
||||
@@ -6,7 +6,7 @@ Handles project type detection and dev command configuration.
|
||||
Detects project types by scanning for configuration files and provides
|
||||
default or custom dev commands for each project.
|
||||
|
||||
Configuration is stored in {project_dir}/.autocoder/config.json.
|
||||
Configuration is stored in {project_dir}/.autoforge/config.json.
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -88,13 +88,22 @@ def _get_config_path(project_dir: Path) -> Path:
|
||||
"""
|
||||
Get the path to the project config file.
|
||||
|
||||
Checks the new .autoforge/ location first, falls back to .autocoder/
|
||||
for backward compatibility.
|
||||
|
||||
Args:
|
||||
project_dir: Path to the project directory.
|
||||
|
||||
Returns:
|
||||
Path to the .autocoder/config.json file.
|
||||
Path to the config.json file in the appropriate directory.
|
||||
"""
|
||||
return project_dir / ".autocoder" / "config.json"
|
||||
new_path = project_dir / ".autoforge" / "config.json"
|
||||
if new_path.exists():
|
||||
return new_path
|
||||
old_path = project_dir / ".autocoder" / "config.json"
|
||||
if old_path.exists():
|
||||
return old_path
|
||||
return new_path
|
||||
|
||||
|
||||
def _load_config(project_dir: Path) -> dict:
|
||||
@@ -137,7 +146,7 @@ def _save_config(project_dir: Path, config: dict) -> None:
|
||||
"""
|
||||
Save the project configuration to disk.
|
||||
|
||||
Creates the .autocoder directory if it doesn't exist.
|
||||
Creates the .autoforge directory if it doesn't exist.
|
||||
|
||||
Args:
|
||||
project_dir: Path to the project directory.
|
||||
@@ -148,7 +157,7 @@ def _save_config(project_dir: Path, config: dict) -> None:
|
||||
"""
|
||||
config_path = _get_config_path(project_dir)
|
||||
|
||||
# Ensure the .autocoder directory exists
|
||||
# Ensure the .autoforge directory exists
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
@@ -408,11 +417,11 @@ def clear_dev_command(project_dir: Path) -> None:
|
||||
config_path.unlink(missing_ok=True)
|
||||
logger.info("Removed empty config file for %s", project_dir.name)
|
||||
|
||||
# Also remove .autocoder directory if empty
|
||||
autocoder_dir = config_path.parent
|
||||
if autocoder_dir.exists() and not any(autocoder_dir.iterdir()):
|
||||
autocoder_dir.rmdir()
|
||||
logger.debug("Removed empty .autocoder directory for %s", project_dir.name)
|
||||
# Also remove .autoforge directory if empty
|
||||
autoforge_dir = config_path.parent
|
||||
if autoforge_dir.exists() and not any(autoforge_dir.iterdir()):
|
||||
autoforge_dir.rmdir()
|
||||
logger.debug("Removed empty .autoforge directory for %s", project_dir.name)
|
||||
except OSError as e:
|
||||
logger.warning("Failed to clean up config for %s: %s", project_dir.name, e)
|
||||
else:
|
||||
|
||||
@@ -92,7 +92,7 @@ class SchedulerService:
|
||||
async def _load_project_schedules(self, project_name: str, project_dir: Path) -> int:
|
||||
"""Load schedules for a single project. Returns count of schedules loaded."""
|
||||
from api.database import Schedule, create_database
|
||||
from autocoder_paths import get_features_db_path
|
||||
from autoforge_paths import get_features_db_path
|
||||
|
||||
db_path = get_features_db_path(project_dir)
|
||||
if not db_path.exists():
|
||||
@@ -568,7 +568,7 @@ class SchedulerService:
|
||||
):
|
||||
"""Check if a project should be started on server startup."""
|
||||
from api.database import Schedule, ScheduleOverride, create_database
|
||||
from autocoder_paths import get_features_db_path
|
||||
from autoforge_paths import get_features_db_path
|
||||
|
||||
db_path = get_features_db_path(project_dir)
|
||||
if not db_path.exists():
|
||||
|
||||
@@ -95,7 +95,7 @@ class SpecChatSession:
|
||||
# Delete app_spec.txt so Claude can create it fresh
|
||||
# The SDK requires reading existing files before writing, but app_spec.txt is created new
|
||||
# Note: We keep initializer_prompt.md so Claude can read and update the template
|
||||
from autocoder_paths import get_prompts_dir
|
||||
from autoforge_paths import get_prompts_dir
|
||||
prompts_dir = get_prompts_dir(self.project_dir)
|
||||
app_spec_path = prompts_dir / "app_spec.txt"
|
||||
if app_spec_path.exists():
|
||||
@@ -116,7 +116,7 @@ class SpecChatSession:
|
||||
],
|
||||
},
|
||||
}
|
||||
from autocoder_paths import get_claude_settings_path
|
||||
from autoforge_paths import get_claude_settings_path
|
||||
settings_file = get_claude_settings_path(self.project_dir)
|
||||
settings_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(settings_file, "w") as f:
|
||||
|
||||
@@ -3,7 +3,7 @@ cd /d "%~dp0"
|
||||
|
||||
echo.
|
||||
echo ========================================
|
||||
echo Autonomous Coding Agent
|
||||
echo AutoForge - Autonomous Coding Agent
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
|
||||
2
start.py
2
start.py
@@ -82,7 +82,7 @@ def get_existing_projects() -> list[tuple[str, Path]]:
|
||||
def display_menu(projects: list[tuple[str, Path]]) -> None:
|
||||
"""Display the main menu."""
|
||||
print("\n" + "=" * 50)
|
||||
print(" Autonomous Coding Agent Launcher")
|
||||
print(" AutoForge - Autonomous Coding Agent")
|
||||
print("=" * 50)
|
||||
print("\n[1] Create new project")
|
||||
|
||||
|
||||
2
start.sh
2
start.sh
@@ -3,7 +3,7 @@ cd "$(dirname "$0")"
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " Autonomous Coding Agent"
|
||||
echo " AutoForge - Autonomous Coding Agent"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
@echo off
|
||||
cd /d "%~dp0"
|
||||
REM AutoCoder UI Launcher for Windows
|
||||
REM AutoForge UI Launcher for Windows
|
||||
REM This script launches the web UI for the autonomous coding agent.
|
||||
|
||||
echo.
|
||||
echo ====================================
|
||||
echo AutoCoder UI
|
||||
echo AutoForge UI
|
||||
echo ====================================
|
||||
echo.
|
||||
|
||||
|
||||
12
start_ui.py
12
start_ui.py
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
AutoCoder UI Launcher
|
||||
AutoForge UI Launcher
|
||||
=====================
|
||||
|
||||
Automated launcher that handles all setup:
|
||||
@@ -265,7 +265,7 @@ def start_dev_server(port: int, host: str = "127.0.0.1") -> tuple:
|
||||
# Set environment for remote access if needed
|
||||
env = os.environ.copy()
|
||||
if host != "127.0.0.1":
|
||||
env["AUTOCODER_ALLOW_REMOTE"] = "1"
|
||||
env["AUTOFORGE_ALLOW_REMOTE"] = "1"
|
||||
|
||||
# Start FastAPI
|
||||
backend = subprocess.Popen([
|
||||
@@ -297,7 +297,7 @@ def start_production_server(port: int, host: str = "127.0.0.1"):
|
||||
|
||||
# Enable remote access in server if not localhost
|
||||
if host != "127.0.0.1":
|
||||
env["AUTOCODER_ALLOW_REMOTE"] = "1"
|
||||
env["AUTOFORGE_ALLOW_REMOTE"] = "1"
|
||||
|
||||
# NOTE: --reload is NOT used because on Windows it breaks asyncio subprocess
|
||||
# support (uvicorn's reload worker doesn't inherit the ProactorEventLoop policy).
|
||||
@@ -313,7 +313,7 @@ def start_production_server(port: int, host: str = "127.0.0.1"):
|
||||
|
||||
def main() -> None:
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser(description="AutoCoder UI Launcher")
|
||||
parser = argparse.ArgumentParser(description="AutoForge UI Launcher")
|
||||
parser.add_argument("--dev", action="store_true", help="Run in development mode with Vite hot reload")
|
||||
parser.add_argument("--host", default="127.0.0.1", help="Host to bind to (default: 127.0.0.1)")
|
||||
parser.add_argument("--port", type=int, default=None, help="Port to bind to (default: auto-detect from 8888)")
|
||||
@@ -328,7 +328,7 @@ def main() -> None:
|
||||
print(" SECURITY WARNING")
|
||||
print("!" * 50)
|
||||
print(f" Remote access enabled on host: {host}")
|
||||
print(" The AutoCoder UI will be accessible from other machines.")
|
||||
print(" The AutoForge UI will be accessible from other machines.")
|
||||
print(" Ensure you understand the security implications:")
|
||||
print(" - The agent has file system access to project directories")
|
||||
print(" - The API can start/stop agents and modify files")
|
||||
@@ -336,7 +336,7 @@ def main() -> None:
|
||||
print("!" * 50 + "\n")
|
||||
|
||||
print("=" * 50)
|
||||
print(" AutoCoder UI Setup")
|
||||
print(" AutoForge UI Setup")
|
||||
print("=" * 50)
|
||||
|
||||
total_steps = 6 if not dev_mode else 5
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")"
|
||||
# AutoCoder UI Launcher for Unix/Linux/macOS
|
||||
# AutoForge UI Launcher for Unix/Linux/macOS
|
||||
# This script launches the web UI for the autonomous coding agent.
|
||||
|
||||
echo ""
|
||||
echo "===================================="
|
||||
echo " AutoCoder UI"
|
||||
echo " AutoForge UI"
|
||||
echo "===================================="
|
||||
echo ""
|
||||
|
||||
|
||||
@@ -273,11 +273,11 @@ def test_yaml_loading():
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
project_dir = Path(tmpdir)
|
||||
autocoder_dir = project_dir / ".autocoder"
|
||||
autocoder_dir.mkdir()
|
||||
autoforge_dir = project_dir / ".autoforge"
|
||||
autoforge_dir.mkdir()
|
||||
|
||||
# Test 1: Valid YAML
|
||||
config_path = autocoder_dir / "allowed_commands.yaml"
|
||||
config_path = autoforge_dir / "allowed_commands.yaml"
|
||||
config_path.write_text("""version: 1
|
||||
commands:
|
||||
- name: swift
|
||||
@@ -297,7 +297,7 @@ commands:
|
||||
failed += 1
|
||||
|
||||
# Test 2: Missing file returns None
|
||||
(project_dir / ".autocoder" / "allowed_commands.yaml").unlink()
|
||||
(project_dir / ".autoforge" / "allowed_commands.yaml").unlink()
|
||||
config = load_project_commands(project_dir)
|
||||
if config is None:
|
||||
print(" PASS: Missing file returns None")
|
||||
@@ -407,11 +407,11 @@ def test_project_commands():
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
project_dir = Path(tmpdir)
|
||||
autocoder_dir = project_dir / ".autocoder"
|
||||
autocoder_dir.mkdir()
|
||||
autoforge_dir = project_dir / ".autoforge"
|
||||
autoforge_dir.mkdir()
|
||||
|
||||
# Create a config with Swift commands
|
||||
config_path = autocoder_dir / "allowed_commands.yaml"
|
||||
config_path = autoforge_dir / "allowed_commands.yaml"
|
||||
config_path.write_text("""version: 1
|
||||
commands:
|
||||
- name: swift
|
||||
@@ -482,7 +482,7 @@ def test_org_config_loading():
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
# Use temporary_home for cross-platform compatibility
|
||||
with temporary_home(tmpdir):
|
||||
org_dir = Path(tmpdir) / ".autocoder"
|
||||
org_dir = Path(tmpdir) / ".autoforge"
|
||||
org_dir.mkdir()
|
||||
org_config_path = org_dir / "config.yaml"
|
||||
|
||||
@@ -576,7 +576,7 @@ def test_hierarchy_resolution():
|
||||
with tempfile.TemporaryDirectory() as tmpproject:
|
||||
# Use temporary_home for cross-platform compatibility
|
||||
with temporary_home(tmphome):
|
||||
org_dir = Path(tmphome) / ".autocoder"
|
||||
org_dir = Path(tmphome) / ".autoforge"
|
||||
org_dir.mkdir()
|
||||
org_config_path = org_dir / "config.yaml"
|
||||
|
||||
@@ -593,9 +593,9 @@ blocked_commands:
|
||||
""")
|
||||
|
||||
project_dir = Path(tmpproject)
|
||||
project_autocoder = project_dir / ".autocoder"
|
||||
project_autocoder.mkdir()
|
||||
project_config = project_autocoder / "allowed_commands.yaml"
|
||||
project_autoforge = project_dir / ".autoforge"
|
||||
project_autoforge.mkdir()
|
||||
project_config = project_autoforge / "allowed_commands.yaml"
|
||||
|
||||
# Create project config
|
||||
project_config.write_text("""version: 1
|
||||
@@ -660,7 +660,7 @@ def test_org_blocklist_enforcement():
|
||||
with tempfile.TemporaryDirectory() as tmpproject:
|
||||
# Use temporary_home for cross-platform compatibility
|
||||
with temporary_home(tmphome):
|
||||
org_dir = Path(tmphome) / ".autocoder"
|
||||
org_dir = Path(tmphome) / ".autoforge"
|
||||
org_dir.mkdir()
|
||||
org_config_path = org_dir / "config.yaml"
|
||||
|
||||
@@ -671,8 +671,8 @@ blocked_commands:
|
||||
""")
|
||||
|
||||
project_dir = Path(tmpproject)
|
||||
project_autocoder = project_dir / ".autocoder"
|
||||
project_autocoder.mkdir()
|
||||
project_autoforge = project_dir / ".autoforge"
|
||||
project_autoforge.mkdir()
|
||||
|
||||
# Try to use terraform (should be blocked)
|
||||
input_data = {"tool_name": "Bash", "tool_input": {"command": "terraform apply"}}
|
||||
@@ -735,7 +735,7 @@ def test_pkill_extensibility():
|
||||
with tempfile.TemporaryDirectory() as tmphome:
|
||||
with tempfile.TemporaryDirectory() as tmpproject:
|
||||
with temporary_home(tmphome):
|
||||
org_dir = Path(tmphome) / ".autocoder"
|
||||
org_dir = Path(tmphome) / ".autoforge"
|
||||
org_dir.mkdir()
|
||||
org_config_path = org_dir / "config.yaml"
|
||||
|
||||
@@ -762,9 +762,9 @@ pkill_processes:
|
||||
with tempfile.TemporaryDirectory() as tmpproject:
|
||||
with temporary_home(tmphome):
|
||||
project_dir = Path(tmpproject)
|
||||
project_autocoder = project_dir / ".autocoder"
|
||||
project_autocoder.mkdir()
|
||||
project_config = project_autocoder / "allowed_commands.yaml"
|
||||
project_autoforge = project_dir / ".autoforge"
|
||||
project_autoforge.mkdir()
|
||||
project_config = project_autoforge / "allowed_commands.yaml"
|
||||
|
||||
# Create project config with extra pkill processes
|
||||
project_config.write_text("""version: 1
|
||||
@@ -804,7 +804,7 @@ pkill_processes:
|
||||
with tempfile.TemporaryDirectory() as tmphome:
|
||||
with tempfile.TemporaryDirectory() as tmpproject:
|
||||
with temporary_home(tmphome):
|
||||
org_dir = Path(tmphome) / ".autocoder"
|
||||
org_dir = Path(tmphome) / ".autoforge"
|
||||
org_dir.mkdir()
|
||||
org_config_path = org_dir / "config.yaml"
|
||||
|
||||
@@ -829,7 +829,7 @@ pkill_processes:
|
||||
with tempfile.TemporaryDirectory() as tmphome:
|
||||
with tempfile.TemporaryDirectory() as tmpproject:
|
||||
with temporary_home(tmphome):
|
||||
org_dir = Path(tmphome) / ".autocoder"
|
||||
org_dir = Path(tmphome) / ".autoforge"
|
||||
org_dir.mkdir()
|
||||
org_config_path = org_dir / "config.yaml"
|
||||
|
||||
@@ -851,7 +851,7 @@ pkill_processes:
|
||||
with tempfile.TemporaryDirectory() as tmphome:
|
||||
with tempfile.TemporaryDirectory() as tmpproject:
|
||||
with temporary_home(tmphome):
|
||||
org_dir = Path(tmphome) / ".autocoder"
|
||||
org_dir = Path(tmphome) / ".autoforge"
|
||||
org_dir.mkdir()
|
||||
org_config_path = org_dir / "config.yaml"
|
||||
|
||||
@@ -875,7 +875,7 @@ pkill_processes:
|
||||
with tempfile.TemporaryDirectory() as tmphome:
|
||||
with tempfile.TemporaryDirectory() as tmpproject:
|
||||
with temporary_home(tmphome):
|
||||
org_dir = Path(tmphome) / ".autocoder"
|
||||
org_dir = Path(tmphome) / ".autoforge"
|
||||
org_dir.mkdir()
|
||||
org_config_path = org_dir / "config.yaml"
|
||||
|
||||
|
||||
@@ -79,9 +79,9 @@ def test_blocked_command_via_hook():
|
||||
project_dir = Path(tmpdir)
|
||||
|
||||
# Create minimal project structure
|
||||
autocoder_dir = project_dir / ".autocoder"
|
||||
autocoder_dir.mkdir()
|
||||
(autocoder_dir / "allowed_commands.yaml").write_text(
|
||||
autoforge_dir = project_dir / ".autoforge"
|
||||
autoforge_dir.mkdir()
|
||||
(autoforge_dir / "allowed_commands.yaml").write_text(
|
||||
"version: 1\ncommands: []"
|
||||
)
|
||||
|
||||
@@ -114,9 +114,9 @@ def test_allowed_command_via_hook():
|
||||
project_dir = Path(tmpdir)
|
||||
|
||||
# Create minimal project structure
|
||||
autocoder_dir = project_dir / ".autocoder"
|
||||
autocoder_dir.mkdir()
|
||||
(autocoder_dir / "allowed_commands.yaml").write_text(
|
||||
autoforge_dir = project_dir / ".autoforge"
|
||||
autoforge_dir.mkdir()
|
||||
(autoforge_dir / "allowed_commands.yaml").write_text(
|
||||
"version: 1\ncommands: []"
|
||||
)
|
||||
|
||||
@@ -145,9 +145,9 @@ def test_non_allowed_command_via_hook():
|
||||
project_dir = Path(tmpdir)
|
||||
|
||||
# Create minimal project structure
|
||||
autocoder_dir = project_dir / ".autocoder"
|
||||
autocoder_dir.mkdir()
|
||||
(autocoder_dir / "allowed_commands.yaml").write_text(
|
||||
autoforge_dir = project_dir / ".autoforge"
|
||||
autoforge_dir.mkdir()
|
||||
(autoforge_dir / "allowed_commands.yaml").write_text(
|
||||
"version: 1\ncommands: []"
|
||||
)
|
||||
|
||||
@@ -179,9 +179,9 @@ def test_project_config_allows_command():
|
||||
project_dir = Path(tmpdir)
|
||||
|
||||
# Create project config with swift allowed
|
||||
autocoder_dir = project_dir / ".autocoder"
|
||||
autocoder_dir.mkdir()
|
||||
(autocoder_dir / "allowed_commands.yaml").write_text("""version: 1
|
||||
autoforge_dir = project_dir / ".autoforge"
|
||||
autoforge_dir.mkdir()
|
||||
(autoforge_dir / "allowed_commands.yaml").write_text("""version: 1
|
||||
commands:
|
||||
- name: swift
|
||||
description: Swift compiler
|
||||
@@ -214,9 +214,9 @@ def test_pattern_matching():
|
||||
project_dir = Path(tmpdir)
|
||||
|
||||
# Create project config with swift* pattern
|
||||
autocoder_dir = project_dir / ".autocoder"
|
||||
autocoder_dir.mkdir()
|
||||
(autocoder_dir / "allowed_commands.yaml").write_text("""version: 1
|
||||
autoforge_dir = project_dir / ".autoforge"
|
||||
autoforge_dir.mkdir()
|
||||
(autoforge_dir / "allowed_commands.yaml").write_text("""version: 1
|
||||
commands:
|
||||
- name: swift*
|
||||
description: All Swift tools
|
||||
@@ -247,7 +247,7 @@ def test_org_blocklist_enforcement():
|
||||
with tempfile.TemporaryDirectory() as tmpproject:
|
||||
# Use context manager to safely set and restore HOME
|
||||
with temporary_home(tmphome):
|
||||
org_dir = Path(tmphome) / ".autocoder"
|
||||
org_dir = Path(tmphome) / ".autoforge"
|
||||
org_dir.mkdir()
|
||||
(org_dir / "config.yaml").write_text("""version: 1
|
||||
allowed_commands: []
|
||||
@@ -257,11 +257,11 @@ blocked_commands:
|
||||
""")
|
||||
|
||||
project_dir = Path(tmpproject)
|
||||
autocoder_dir = project_dir / ".autocoder"
|
||||
autocoder_dir.mkdir()
|
||||
autoforge_dir = project_dir / ".autoforge"
|
||||
autoforge_dir.mkdir()
|
||||
|
||||
# Try to allow terraform in project config (should fail - org blocked)
|
||||
(autocoder_dir / "allowed_commands.yaml").write_text("""version: 1
|
||||
(autoforge_dir / "allowed_commands.yaml").write_text("""version: 1
|
||||
commands:
|
||||
- name: terraform
|
||||
description: Infrastructure as code
|
||||
@@ -295,7 +295,7 @@ def test_org_allowlist_inheritance():
|
||||
with tempfile.TemporaryDirectory() as tmpproject:
|
||||
# Use context manager to safely set and restore HOME
|
||||
with temporary_home(tmphome):
|
||||
org_dir = Path(tmphome) / ".autocoder"
|
||||
org_dir = Path(tmphome) / ".autoforge"
|
||||
org_dir.mkdir()
|
||||
(org_dir / "config.yaml").write_text("""version: 1
|
||||
allowed_commands:
|
||||
@@ -305,9 +305,9 @@ blocked_commands: []
|
||||
""")
|
||||
|
||||
project_dir = Path(tmpproject)
|
||||
autocoder_dir = project_dir / ".autocoder"
|
||||
autocoder_dir.mkdir()
|
||||
(autocoder_dir / "allowed_commands.yaml").write_text(
|
||||
autoforge_dir = project_dir / ".autoforge"
|
||||
autoforge_dir.mkdir()
|
||||
(autoforge_dir / "allowed_commands.yaml").write_text(
|
||||
"version: 1\ncommands: []"
|
||||
)
|
||||
|
||||
@@ -336,9 +336,9 @@ def test_invalid_yaml_ignored():
|
||||
project_dir = Path(tmpdir)
|
||||
|
||||
# Create invalid YAML
|
||||
autocoder_dir = project_dir / ".autocoder"
|
||||
autocoder_dir.mkdir()
|
||||
(autocoder_dir / "allowed_commands.yaml").write_text("invalid: yaml: content:")
|
||||
autoforge_dir = project_dir / ".autoforge"
|
||||
autoforge_dir.mkdir()
|
||||
(autoforge_dir / "allowed_commands.yaml").write_text("invalid: yaml: content:")
|
||||
|
||||
# Try to run ls (should still work - falls back to defaults)
|
||||
input_data = {"tool_name": "Bash", "tool_input": {"command": "ls"}}
|
||||
@@ -365,13 +365,13 @@ def test_100_command_limit():
|
||||
project_dir = Path(tmpdir)
|
||||
|
||||
# Create config with 101 commands
|
||||
autocoder_dir = project_dir / ".autocoder"
|
||||
autocoder_dir.mkdir()
|
||||
autoforge_dir = project_dir / ".autoforge"
|
||||
autoforge_dir.mkdir()
|
||||
|
||||
commands = [
|
||||
f" - name: cmd{i}\n description: Command {i}" for i in range(101)
|
||||
]
|
||||
(autocoder_dir / "allowed_commands.yaml").write_text(
|
||||
(autoforge_dir / "allowed_commands.yaml").write_text(
|
||||
"version: 1\ncommands:\n" + "\n".join(commands)
|
||||
)
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" type="image/png" href="/logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AutoCoder</title>
|
||||
<title>AutoForge</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=Work+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=DM+Sans:wght@400;500;700&family=Space+Mono:wght@400;700&family=Outfit:wght@400;500;600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
20
ui/package-lock.json
generated
20
ui/package-lock.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "autocoder",
|
||||
"name": "autoforge-ui",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "autocoder",
|
||||
"name": "autoforge-ui",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
@@ -20,6 +20,7 @@
|
||||
"@xterm/addon-web-links": "^0.12.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"@xyflow/react": "^12.10.0",
|
||||
"autoforge-ai": "file:..",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -50,6 +51,17 @@
|
||||
"vite": "^7.3.0"
|
||||
}
|
||||
},
|
||||
"..": {
|
||||
"name": "autoforge-ai",
|
||||
"version": "0.1.0",
|
||||
"license": "AGPL-3.0",
|
||||
"bin": {
|
||||
"autoforge": "bin/autoforge.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||
@@ -3136,6 +3148,10 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/autoforge-ai": {
|
||||
"resolved": "..",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "autocoder",
|
||||
"name": "autoforge-ui",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
@@ -24,6 +24,7 @@
|
||||
"@xterm/addon-web-links": "^0.12.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"@xyflow/react": "^12.10.0",
|
||||
"autoforge-ai": "file:..",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
|
||||
BIN
ui/public/logo.png
Normal file
BIN
ui/public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 233 KiB |
@@ -28,14 +28,14 @@ import { ThemeSelector } from './components/ThemeSelector'
|
||||
import { ResetProjectModal } from './components/ResetProjectModal'
|
||||
import { ProjectSetupRequired } from './components/ProjectSetupRequired'
|
||||
import { getDependencyGraph, startAgent } from './lib/api'
|
||||
import { Loader2, Settings, Moon, Sun, RotateCcw } from 'lucide-react'
|
||||
import { Loader2, Settings, Moon, Sun, RotateCcw, BookOpen } from 'lucide-react'
|
||||
import type { Feature } from './lib/types'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
|
||||
const STORAGE_KEY = 'autocoder-selected-project'
|
||||
const VIEW_MODE_KEY = 'autocoder-view-mode'
|
||||
const STORAGE_KEY = 'autoforge-selected-project'
|
||||
const VIEW_MODE_KEY = 'autoforge-view-mode'
|
||||
|
||||
// Bottom padding for main content when debug panel is collapsed (40px header + 8px margin)
|
||||
const COLLAPSED_DEBUG_PANEL_CLEARANCE = 48
|
||||
@@ -263,9 +263,12 @@ function App() {
|
||||
<div className="max-w-7xl mx-auto px-4 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Logo and Title */}
|
||||
<h1 className="font-display text-2xl font-bold tracking-tight uppercase">
|
||||
AutoCoder
|
||||
</h1>
|
||||
<div className="flex items-center gap-3">
|
||||
<img src="/logo.png" alt="AutoForge" className="h-9 w-9 rounded-full" />
|
||||
<h1 className="font-display text-2xl font-bold tracking-tight uppercase">
|
||||
AutoForge
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<div className="flex items-center gap-4">
|
||||
@@ -335,6 +338,17 @@ function App() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Docs link */}
|
||||
<Button
|
||||
onClick={() => window.open('https://autoforge.cc', '_blank')}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
title="Documentation"
|
||||
aria-label="Open Documentation"
|
||||
>
|
||||
<BookOpen size={18} />
|
||||
</Button>
|
||||
|
||||
{/* Theme selector */}
|
||||
<ThemeSelector
|
||||
themes={themes}
|
||||
@@ -365,7 +379,7 @@ function App() {
|
||||
{!selectedProject ? (
|
||||
<div className="text-center mt-12">
|
||||
<h2 className="font-display text-2xl font-bold mb-2">
|
||||
Welcome to AutoCoder
|
||||
Welcome to AutoForge
|
||||
</h2>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Select a project from the dropdown above or create a new one to get started.
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
const ACTIVITY_COLLAPSED_KEY = 'autocoder-activity-collapsed'
|
||||
const ACTIVITY_COLLAPSED_KEY = 'autoforge-activity-collapsed'
|
||||
|
||||
interface AgentMissionControlProps {
|
||||
agents: ActiveAgent[]
|
||||
@@ -88,8 +88,8 @@ export function AgentMissionControl({
|
||||
{/* Content */}
|
||||
<div
|
||||
className={`
|
||||
transition-all duration-300 ease-out overflow-hidden
|
||||
${isExpanded ? 'max-h-[600px] opacity-100' : 'max-h-0 opacity-0'}
|
||||
transition-all duration-300 ease-out
|
||||
${isExpanded ? 'max-h-[600px] opacity-100 overflow-y-auto' : 'max-h-0 opacity-0 overflow-hidden'}
|
||||
`}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
|
||||
@@ -59,7 +59,7 @@ function DialogContent({
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] max-h-[calc(100vh-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none overflow-y-auto sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -52,8 +52,8 @@ export const THEMES: ThemeOption[] = [
|
||||
}
|
||||
]
|
||||
|
||||
const THEME_STORAGE_KEY = 'autocoder-theme'
|
||||
const DARK_MODE_STORAGE_KEY = 'autocoder-dark-mode'
|
||||
const THEME_STORAGE_KEY = 'autoforge-theme'
|
||||
const DARK_MODE_STORAGE_KEY = 'autoforge-dark-mode'
|
||||
|
||||
function getThemeClass(themeId: ThemeId): string {
|
||||
switch (themeId) {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { createRoot } from 'react-dom/client'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import App from './App'
|
||||
import './styles/globals.css'
|
||||
// Note: Custom theme removed - using shadcn/ui theming instead
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
|
||||
@@ -1134,6 +1134,143 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
Documentation Prose Typography
|
||||
============================================================================ */
|
||||
|
||||
.docs-prose {
|
||||
line-height: 1.7;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.docs-prose h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--foreground);
|
||||
margin-top: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid var(--border);
|
||||
scroll-margin-top: 5rem;
|
||||
}
|
||||
|
||||
.docs-prose h2:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.docs-prose h3 {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 600;
|
||||
color: var(--foreground);
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 0.75rem;
|
||||
scroll-margin-top: 5rem;
|
||||
}
|
||||
|
||||
.docs-prose p {
|
||||
margin-bottom: 1rem;
|
||||
max-width: 65ch;
|
||||
}
|
||||
|
||||
.docs-prose ul,
|
||||
.docs-prose ol {
|
||||
margin-bottom: 1rem;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.docs-prose ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.docs-prose ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
.docs-prose li {
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
.docs-prose li > ul,
|
||||
.docs-prose li > ol {
|
||||
margin-top: 0.375rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.docs-prose pre {
|
||||
background: var(--muted);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 1rem;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 1rem;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.docs-prose code:not(pre code) {
|
||||
background: var(--muted);
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.docs-prose table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.docs-prose th {
|
||||
background: var(--muted);
|
||||
font-weight: 600;
|
||||
color: var(--foreground);
|
||||
text-align: left;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.docs-prose td {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.docs-prose tr:nth-child(even) td {
|
||||
background: var(--muted);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.docs-prose blockquote {
|
||||
border-left: 4px solid var(--primary);
|
||||
padding-left: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
font-style: italic;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.docs-prose a {
|
||||
color: var(--primary);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.docs-prose a:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.docs-prose strong {
|
||||
color: var(--foreground);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.docs-prose hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
Scrollbar Styling
|
||||
============================================================================ */
|
||||
|
||||
@@ -26,11 +26,23 @@ export default defineConfig({
|
||||
'vendor-flow': ['@xyflow/react', 'dagre'],
|
||||
// Terminal emulator
|
||||
'vendor-xterm': ['@xterm/xterm', '@xterm/addon-fit', '@xterm/addon-web-links'],
|
||||
// UI components
|
||||
'vendor-ui': [
|
||||
// UI components - Radix UI
|
||||
'vendor-radix': [
|
||||
'@radix-ui/react-checkbox',
|
||||
'@radix-ui/react-dialog',
|
||||
'@radix-ui/react-dropdown-menu',
|
||||
'@radix-ui/react-label',
|
||||
'@radix-ui/react-separator',
|
||||
'@radix-ui/react-slot',
|
||||
'@radix-ui/react-switch',
|
||||
],
|
||||
// Icons and utilities
|
||||
'vendor-utils': [
|
||||
'lucide-react',
|
||||
'canvas-confetti',
|
||||
'class-variance-authority',
|
||||
'clsx',
|
||||
'tailwind-merge',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user