feat: Add Sequential Thinking to Code Review + Frontend Validation

Enhanced code review and frontend validation with intelligent triggers:

Code Review Agent Enhancement:
- Added Sequential Thinking MCP integration for complex issues
- Triggers on 2+ rejections or 3+ critical issues
- New escalation format with root cause analysis
- Comprehensive solution strategies with trade-off evaluation
- Educational feedback to break rejection cycles
- Files: .claude/agents/code-review.md (+308 lines)
- Docs: CODE_REVIEW_ST_ENHANCEMENT.md, CODE_REVIEW_ST_TESTING.md

Frontend Design Skill Enhancement:
- Automatic invocation for ANY UI change
- Comprehensive validation checklist (200+ checkpoints)
- 8 validation categories (visual, interactive, responsive, a11y, etc.)
- 3 validation levels (quick, standard, comprehensive)
- Integration with code review workflow
- Files: .claude/skills/frontend-design/SKILL.md (+120 lines)
- Docs: UI_VALIDATION_CHECKLIST.md (462 lines), AUTOMATIC_VALIDATION_ENHANCEMENT.md (587 lines)

Settings Optimization:
- Repaired .claude/settings.local.json (fixed m365 pattern)
- Reduced permissions from 49 to 33 (33% reduction)
- Removed duplicates, sorted alphabetically
- Created SETTINGS_PERMISSIONS.md documentation

Checkpoint Command Enhancement:
- Dual checkpoint system (git + database)
- Saves session context to API for cross-machine recall
- Includes git metadata in database context
- Files: .claude/commands/checkpoint.md (+139 lines)

Decision Rationale:
- Sequential Thinking MCP breaks rejection cycles by identifying root causes
- Automatic frontend validation catches UI issues before code review
- Dual checkpoints enable complete project memory across machines
- Settings optimization improves maintainability

Total: 1,200+ lines of documentation and enhancements

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-17 16:23:52 -07:00
parent 359c2cf1b4
commit 75ce1c2fd5
1089 changed files with 149506 additions and 5 deletions

View File

@@ -0,0 +1,2 @@
{"parentUuid":null,"isSidechain":true,"userType":"external","cwd":"C:\\Users\\MikeSwanson","sessionId":"70eae563-3813-4673-82a7-50f1f2566271","version":"2.1.2","gitBranch":"","agentId":"a0a67fa","type":"user","message":{"role":"user","content":"Warmup"},"uuid":"fcb15fcb-5509-4015-9f0d-23903040ef9b","timestamp":"2026-01-09T13:31:32.253Z"}
{"parentUuid":"fcb15fcb-5509-4015-9f0d-23903040ef9b","isSidechain":true,"userType":"external","cwd":"C:\\Users\\MikeSwanson","sessionId":"70eae563-3813-4673-82a7-50f1f2566271","version":"2.1.2","gitBranch":"","agentId":"a0a67fa","message":{"model":"claude-opus-4-5-20251101","id":"msg_01DbAZyhDMas25CMzwmL99cp","type":"message","role":"assistant","content":[{"type":"text","text":"I'm ready to help you explore a codebase and design implementation plans. This is a warmup, so I'm standing by for your requirements.\n\nPlease provide:\n1. **The requirements** or task you'd like me to analyze\n2. **Any specific files** you'd like me to start with (optional)\n3. **Any particular perspective** you'd like me to apply to the design process (optional)\n\nI'll thoroughly explore the codebase using read-only operations, understand the existing patterns and architecture, and create a detailed implementation plan for you."}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":10358,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":10358,"ephemeral_1h_input_tokens":0},"output_tokens":123,"service_tier":"standard"}},"requestId":"req_011CWwtQrErL6tuG9t1chkTE","type":"assistant","uuid":"6b2a61ca-cad4-4ffd-8cf7-6511ba4562e1","timestamp":"2026-01-09T13:31:36.631Z"}

View File

@@ -0,0 +1,2 @@
{"parentUuid":null,"isSidechain":true,"userType":"external","cwd":"C:\\Users\\MikeSwanson","sessionId":"70eae563-3813-4673-82a7-50f1f2566271","version":"2.1.2","gitBranch":"","agentId":"a4d1167","type":"user","message":{"role":"user","content":"Warmup"},"uuid":"5d887d03-90b6-465b-a167-e53df132b3dd","timestamp":"2026-01-09T13:31:32.231Z"}
{"parentUuid":"5d887d03-90b6-465b-a167-e53df132b3dd","isSidechain":true,"userType":"external","cwd":"C:\\Users\\MikeSwanson","sessionId":"70eae563-3813-4673-82a7-50f1f2566271","version":"2.1.2","gitBranch":"","agentId":"a4d1167","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01XwZqqEpyRKbJsx6MFJLjGN","type":"message","role":"assistant","content":[{"type":"text","text":"Hello! I'm ready to help you explore and search through your codebase. I'm Claude Code, your file search specialist.\n\nI can help you with:\n\n- **Finding files** using glob patterns (e.g., `*.js`, `src/**/*.tsx`)\n- **Searching code** with powerful regex patterns across your entire codebase\n- **Reading and analyzing** specific files you're interested in\n- **Git operations** like viewing status, logs, and diffs (read-only)\n\nWhat would you like me to search for or explore in your codebase? Just let me know:\n- What files or code patterns you're looking for\n- What you're trying to understand or analyze\n- How thorough you'd like the search to be (quick scan vs. comprehensive)\n\nI'm ready when you are!"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":10241,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":10241,"ephemeral_1h_input_tokens":0},"output_tokens":181,"service_tier":"standard"}},"requestId":"req_011CWwtQrFbX79cvwgaRc7BC","type":"assistant","uuid":"57fce297-e980-483d-91a7-b918ecd2f11d","timestamp":"2026-01-09T13:31:35.103Z"}

View File

@@ -0,0 +1,2 @@
{"parentUuid":null,"isSidechain":true,"userType":"external","cwd":"C:\\Users\\MikeSwanson","sessionId":"70eae563-3813-4673-82a7-50f1f2566271","version":"2.1.2","gitBranch":"","agentId":"a579942","type":"user","message":{"role":"user","content":"Warmup"},"uuid":"5418de26-ed26-4bf0-bdeb-e4732efe204f","timestamp":"2026-01-09T13:31:32.274Z"}
{"parentUuid":"5418de26-ed26-4bf0-bdeb-e4732efe204f","isSidechain":true,"userType":"external","cwd":"C:\\Users\\MikeSwanson","sessionId":"70eae563-3813-4673-82a7-50f1f2566271","version":"2.1.2","gitBranch":"","agentId":"a579942","message":{"model":"claude-opus-4-5-20251101","id":"msg_01YLvbegWobJn59YD8HppnV7","type":"message","role":"assistant","content":[{"type":"text","text":"I'm ready to help you with command execution tasks. This is a warmup, so I'm standing by for your actual request.\n\nWhat would you like me to do? I can help with:\n- Running bash/shell commands\n- Git operations\n- Package management (npm, pip, etc.)\n- Docker commands\n- File system operations via command line\n- And other terminal-based tasks\n\nJust let me know what you need."}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":3753,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":95,"service_tier":"standard"}},"requestId":"req_011CWwtQrduu6FtJ8yopmNRm","type":"assistant","uuid":"5bbcd247-67d6-42a2-a1a1-914bd2d20c9a","timestamp":"2026-01-09T13:31:36.219Z"}

View File

@@ -0,0 +1,2 @@
Exit code 127
/usr/bin/bash: line 1: sqlite3: command not found

View File

@@ -0,0 +1,2 @@
/c/Users/MikeSwanson/claude-projects/Autocode-remix/Autocode-fork/backend/autocoder_plus.db
/c/Users/MikeSwanson/claude-projects/Autocode-remix/Autocode-fork/features.db

View File

@@ -0,0 +1,3 @@
Before: 2 projects
Removed: ['guru-rmm']
After: 1 projects

View File

@@ -0,0 +1,101 @@
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\venv\Scripts\Activate.ps1
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Scripts\Activate.ps1
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\venv\Scripts\deactivate.bat
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Scripts\deactivate.bat
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\venv\Scripts\python.exe
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Scripts\python.exe
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\venv\Scripts\pythonw.exe
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Scripts\pythonw.exe
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\.env.example
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\.gitignore
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\CLAUDE.md
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\LICENSE.md
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\README.md
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\SAMPLE_PROMPT.md
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\agent.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\autonomous_agent_demo.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\client.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\progress.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\prompts.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\pyproject.toml
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\registry.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\requirements.txt
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\security.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\start.bat
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\start.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\start.sh
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\start_ui.bat
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\start_ui.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\start_ui.sh
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\test_security.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\.claude\commands\checkpoint.md
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\.claude\commands\create-spec.md
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\.claude\skills\frontend-design\LICENSE.txt
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\.claude\skills\frontend-design\SKILL.md
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\.claude\templates\app_spec.template.txt
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\.claude\templates\coding_prompt.template.md
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\.claude\templates\coding_prompt_yolo.template.md
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\.claude\templates\initializer_prompt.template.md
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\.github\workflows\ci.yml
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\api\__init__.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\api\database.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\api\migration.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\mcp_server\__init__.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\mcp_server\feature_mcp.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\server\__init__.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\server\main.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\server\schemas.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\server\websocket.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\server\routers\__init__.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\server\routers\agent.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\server\routers\assistant_chat.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\server\routers\features.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\server\routers\filesystem.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\server\routers\projects.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\server\routers\spec_creation.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\server\services\__init__.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\server\services\assistant_chat_session.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\server\services\assistant_database.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\server\services\process_manager.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\server\services\spec_chat_session.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\index.html
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\package.json
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\tsconfig.json
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\tsconfig.node.json
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\vite.config.ts
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\dist\vite.svg
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\public\vite.svg
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\App.tsx
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\main.tsx
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\vite-env.d.ts
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\components\AddFeatureForm.tsx
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\components\AgentControl.tsx
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\components\AgentThought.tsx
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\components\AssistantChat.tsx
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\components\AssistantFAB.tsx
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\components\AssistantPanel.tsx
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\components\ChatMessage.tsx
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\components\DebugLogViewer.tsx
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\components\FeatureCard.tsx
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\components\FeatureModal.tsx
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\components\FolderBrowser.tsx
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\components\KanbanBoard.tsx
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\components\KanbanColumn.tsx
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\components\NewProjectModal.tsx
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\components\ProgressDashboard.tsx
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\components\ProjectSelector.tsx
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\components\QuestionOptions.tsx
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\components\SetupWizard.tsx
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\components\SpecCreationChat.tsx
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\components\TypingIndicator.tsx
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\hooks\useAssistantChat.ts
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\hooks\useCelebration.ts
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\hooks\useFeatureSound.ts
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\hooks\useProjects.ts
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\hooks\useSpecChat.ts
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\hooks\useWebSocket.ts
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\lib\api.ts
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\lib\types.ts
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\styles\globals.css
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\venv\.gitignore
(Results are truncated. Consider using a more specific path or pattern.)

View File

@@ -0,0 +1,371 @@
1→"""
2→Project Registry Module
3→=======================
4→
5→Cross-platform project registry for storing project name to path mappings.
6→Uses SQLite database stored at ~/.autocoder/registry.db.
7→"""
8→
9→import logging
10→import os
11→import re
12→from contextlib import contextmanager
13→from datetime import datetime
14→from pathlib import Path
15→from typing import Any
16→
17→from sqlalchemy import Column, DateTime, String, create_engine
18→from sqlalchemy.ext.declarative import declarative_base
19→from sqlalchemy.orm import sessionmaker
20→
21→# Module logger
22→logger = logging.getLogger(__name__)
23→
24→
25→# =============================================================================
26→# Exceptions
27→# =============================================================================
28→
29→class RegistryError(Exception):
30→ """Base registry exception."""
31→ pass
32→
33→
34→class RegistryNotFound(RegistryError):
35→ """Registry file doesn't exist."""
36→ pass
37→
38→
39→class RegistryCorrupted(RegistryError):
40→ """Registry database is corrupted."""
41→ pass
42→
43→
44→class RegistryPermissionDenied(RegistryError):
45→ """Can't read/write registry file."""
46→ pass
47→
48→
49→# =============================================================================
50→# SQLAlchemy Model
51→# =============================================================================
52→
53→Base = declarative_base()
54→
55→
56→class Project(Base):
57→ """SQLAlchemy model for registered projects."""
58→ __tablename__ = "projects"
59→
60→ name = Column(String(50), primary_key=True, index=True)
61→ path = Column(String, nullable=False) # POSIX format for cross-platform
62→ created_at = Column(DateTime, nullable=False)
63→
64→
65→# =============================================================================
66→# Database Connection
67→# =============================================================================
68→
69→# Module-level singleton for database engine
70→_engine = None
71→_SessionLocal = None
72→
73→
74→def get_config_dir() -> Path:
75→ """
76→ Get the config directory: ~/.autocoder/
77→
78→ Returns:
79→ Path to ~/.autocoder/ (created if it doesn't exist)
80→ """
81→ config_dir = Path.home() / ".autocoder"
82→ config_dir.mkdir(parents=True, exist_ok=True)
83→ return config_dir
84→
85→
86→def get_registry_path() -> Path:
87→ """Get the path to the registry database."""
88→ return get_config_dir() / "registry.db"
89→
90→
91→def _get_engine():
92→ """
93→ Get or create the database engine (singleton pattern).
94→
95→ Returns:
96→ Tuple of (engine, SessionLocal)
97→ """
98→ global _engine, _SessionLocal
99→
100→ if _engine is None:
101→ db_path = get_registry_path()
102→ db_url = f"sqlite:///{db_path.as_posix()}"
103→ _engine = create_engine(db_url, connect_args={"check_same_thread": False})
104→ Base.metadata.create_all(bind=_engine)
105→ _SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=_engine)
106→ logger.debug("Initialized registry database at: %s", db_path)
107→
108→ return _engine, _SessionLocal
109→
110→
111→@contextmanager
112→def _get_session():
113→ """
114→ Context manager for database sessions with automatic commit/rollback.
115→
116→ Yields:
117→ SQLAlchemy session
118→ """
119→ _, SessionLocal = _get_engine()
120→ session = SessionLocal()
121→ try:
122→ yield session
123→ session.commit()
124→ except Exception:
125→ session.rollback()
126→ raise
127→ finally:
128→ session.close()
129→
130→
131→# =============================================================================
132→# Project CRUD Functions
133→# =============================================================================
134→
135→def register_project(name: str, path: Path) -> None:
136→ """
137→ Register a new project in the registry.
138→
139→ Args:
140→ name: The project name (unique identifier).
141→ path: The absolute path to the project directory.
142→
143→ Raises:
144→ ValueError: If project name is invalid or path is not absolute.
145→ RegistryError: If a project with that name already exists.
146→ """
147→ # Validate name
148→ if not re.match(r'^[a-zA-Z0-9_-]{1,50}$', name):
149→ raise ValueError(
150→ "Invalid project name. Use only letters, numbers, hyphens, "
151→ "and underscores (1-50 chars)."
152→ )
153→
154→ # Ensure path is absolute
155→ path = Path(path).resolve()
156→
157→ with _get_session() as session:
158→ existing = session.query(Project).filter(Project.name == name).first()
159→ if existing:
160→ logger.warning("Attempted to register duplicate project: %s", name)
161→ raise RegistryError(f"Project '{name}' already exists in registry")
162→
163→ project = Project(
164→ name=name,
165→ path=path.as_posix(),
166→ created_at=datetime.now()
167→ )
168→ session.add(project)
169→
170→ logger.info("Registered project '%s' at path: %s", name, path)
171→
172→
173→def unregister_project(name: str) -> bool:
174→ """
175→ Remove a project from the registry.
176→
177→ Args:
178→ name: The project name to remove.
179→
180→ Returns:
181→ True if removed, False if project wasn't found.
182→ """
183→ with _get_session() as session:
184→ project = session.query(Project).filter(Project.name == name).first()
185→ if not project:
186→ logger.debug("Attempted to unregister non-existent project: %s", name)
187→ return False
188→
189→ session.delete(project)
190→
191→ logger.info("Unregistered project: %s", name)
192→ return True
193→
194→
195→def get_project_path(name: str) -> Path | None:
196→ """
197→ Look up a project's path by name.
198→
199→ Args:
200→ name: The project name.
201→
202→ Returns:
203→ The project Path, or None if not found.
204→ """
205→ _, SessionLocal = _get_engine()
206→ session = SessionLocal()
207→ try:
208→ project = session.query(Project).filter(Project.name == name).first()
209→ if project is None:
210→ return None
211→ return Path(project.path)
212→ finally:
213→ session.close()
214→
215→
216→def list_registered_projects() -> dict[str, dict[str, Any]]:
217→ """
218→ Get all registered projects.
219→
220→ Returns:
221→ Dictionary mapping project names to their info dictionaries.
222→ """
223→ _, SessionLocal = _get_engine()
224→ session = SessionLocal()
225→ try:
226→ projects = session.query(Project).all()
227→ return {
228→ p.name: {
229→ "path": p.path,
230→ "created_at": p.created_at.isoformat() if p.created_at else None
231→ }
232→ for p in projects
233→ }
234→ finally:
235→ session.close()
236→
237→
238→def get_project_info(name: str) -> dict[str, Any] | None:
239→ """
240→ Get full info about a project.
241→
242→ Args:
243→ name: The project name.
244→
245→ Returns:
246→ Project info dictionary, or None if not found.
247→ """
248→ _, SessionLocal = _get_engine()
249→ session = SessionLocal()
250→ try:
251→ project = session.query(Project).filter(Project.name == name).first()
252→ if project is None:
253→ return None
254→ return {
255→ "path": project.path,
256→ "created_at": project.created_at.isoformat() if project.created_at else None
257→ }
258→ finally:
259→ session.close()
260→
261→
262→def update_project_path(name: str, new_path: Path) -> bool:
263→ """
264→ Update a project's path (for relocating projects).
265→
266→ Args:
267→ name: The project name.
268→ new_path: The new absolute path.
269→
270→ Returns:
271→ True if updated, False if project wasn't found.
272→ """
273→ new_path = Path(new_path).resolve()
274→
275→ with _get_session() as session:
276→ project = session.query(Project).filter(Project.name == name).first()
277→ if not project:
278→ return False
279→
280→ project.path = new_path.as_posix()
281→
282→ return True
283→
284→
285→# =============================================================================
286→# Validation Functions
287→# =============================================================================
288→
289→def validate_project_path(path: Path) -> tuple[bool, str]:
290→ """
291→ Validate that a project path is accessible and writable.
292→
293→ Args:
294→ path: The path to validate.
295→
296→ Returns:
297→ Tuple of (is_valid, error_message).
298→ """
299→ path = Path(path).resolve()
300→
301→ # Check if path exists
302→ if not path.exists():
303→ return False, f"Path does not exist: {path}"
304→
305→ # Check if it's a directory
306→ if not path.is_dir():
307→ return False, f"Path is not a directory: {path}"
308→
309→ # Check read permissions
310→ if not os.access(path, os.R_OK):
311→ return False, f"No read permission: {path}"
312→
313→ # Check write permissions
314→ if not os.access(path, os.W_OK):
315→ return False, f"No write permission: {path}"
316→
317→ return True, ""
318→
319→
320→def cleanup_stale_projects() -> list[str]:
321→ """
322→ Remove projects from registry whose paths no longer exist.
323→
324→ Returns:
325→ List of removed project names.
326→ """
327→ removed = []
328→
329→ with _get_session() as session:
330→ projects = session.query(Project).all()
331→ for project in projects:
332→ path = Path(project.path)
333→ if not path.exists():
334→ session.delete(project)
335→ removed.append(project.name)
336→
337→ if removed:
338→ logger.info("Cleaned up stale projects: %s", removed)
339→
340→ return removed
341→
342→
343→def list_valid_projects() -> list[dict[str, Any]]:
344→ """
345→ List all projects that have valid, accessible paths.
346→
347→ Returns:
348→ List of project info dicts with additional 'name' field.
349→ """
350→ _, SessionLocal = _get_engine()
351→ session = SessionLocal()
352→ try:
353→ projects = session.query(Project).all()
354→ valid = []
355→ for p in projects:
356→ path = Path(p.path)
357→ is_valid, _ = validate_project_path(path)
358→ if is_valid:
359→ valid.append({
360→ "name": p.name,
361→ "path": p.path,
362→ "created_at": p.created_at.isoformat() if p.created_at else None
363→ })
364→ return valid
365→ finally:
366→ session.close()
367→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

View File

@@ -0,0 +1,6 @@
{
"Autocode-fork": {
"path": "C:/Users/MikeSwanson/claude-projects/Autocode-remix/Autocode-fork",
"created_at": "2026-01-08T13:49:27.141292"
}
}

View File

@@ -0,0 +1,333 @@
1→"""
2→Spec Router
3→===========
4→
5→API endpoints for project specification management.
6→"""
7→
8→from datetime import datetime
9→from typing import Optional, List
10→
11→from fastapi import APIRouter, Depends, HTTPException, status
12→from pydantic import BaseModel, Field
13→from sqlalchemy import select, and_, func
14→from sqlalchemy.ext.asyncio import AsyncSession
15→
16→from ..database import get_db
17→from ..models import Project, SpecHistory, Feature
18→
19→router = APIRouter(prefix="/api/projects", tags=["spec"])
20→
21→
22→# ============================================================================
23→# Pydantic Schemas
24→# ============================================================================
25→
26→class SpecResponse(BaseModel):
27→ """Schema for current spec response."""
28→ project_id: int
29→ version: int
30→ content: str
31→ created_at: datetime
32→ change_summary: Optional[str]
33→
34→ class Config:
35→ from_attributes = True
36→
37→
38→class SpecHistoryResponse(BaseModel):
39→ """Schema for spec history entry."""
40→ id: int
41→ project_id: int
42→ version: int
43→ content: str
44→ created_at: datetime
45→ change_summary: Optional[str]
46→
47→ class Config:
48→ from_attributes = True
49→
50→
51→class SpecHistoryListResponse(BaseModel):
52→ """Schema for spec history list."""
53→ history: List[SpecHistoryResponse]
54→ count: int
55→
56→
57→class SpecUpdate(BaseModel):
58→ """Schema for updating a spec."""
59→ content: str = Field(..., min_length=1)
60→ change_summary: Optional[str] = None
61→
62→
63→class AffectedFeatureResponse(BaseModel):
64→ """Schema for an affected feature in change analysis."""
65→ id: int
66→ name: str
67→ category: str
68→ reason: str
69→
70→ class Config:
71→ from_attributes = True
72→
73→
74→class SpecAnalysisResponse(BaseModel):
75→ """Schema for spec change analysis response."""
76→ affected_features: List[AffectedFeatureResponse]
77→ affected_count: int
78→ change_summary: str
79→
80→
81→# ============================================================================
82→# API Endpoints
83→# ============================================================================
84→
85→@router.get("/{name}/spec", response_model=SpecResponse)
86→async def get_current_spec(name: str, db: AsyncSession = Depends(get_db)):
87→ """Get the current (latest) spec for a project."""
88→ # Verify project exists
89→ result = await db.execute(select(Project).where(Project.name == name))
90→ project = result.scalar_one_or_none()
91→
92→ if not project:
93→ raise HTTPException(
94→ status_code=status.HTTP_404_NOT_FOUND,
95→ detail=f"Project '{name}' not found"
96→ )
97→
98→ # Get latest spec version
99→ result = await db.execute(
100→ select(SpecHistory)
101→ .where(SpecHistory.project_id == project.id)
102→ .order_by(SpecHistory.version.desc())
103→ .limit(1)
104→ )
105→ spec = result.scalar_one_or_none()
106→
107→ if not spec:
108→ # Return empty spec if none exists
109→ return SpecResponse(
110→ project_id=project.id,
111→ version=0,
112→ content="",
113→ created_at=project.created_at,
114→ change_summary="No spec yet"
115→ )
116→
117→ return SpecResponse(
118→ project_id=spec.project_id,
119→ version=spec.version,
120→ content=spec.content,
121→ created_at=spec.created_at,
122→ change_summary=spec.change_summary
123→ )
124→
125→
126→@router.put("/{name}/spec", response_model=SpecResponse)
127→async def update_spec(name: str, spec_update: SpecUpdate, db: AsyncSession = Depends(get_db)):
128→ """Update the spec (creates a new version)."""
129→ # Verify project exists
130→ result = await db.execute(select(Project).where(Project.name == name))
131→ project = result.scalar_one_or_none()
132→
133→ if not project:
134→ raise HTTPException(
135→ status_code=status.HTTP_404_NOT_FOUND,
136→ detail=f"Project '{name}' not found"
137→ )
138→
139→ # Get current version
140→ result = await db.execute(
141→ select(func.max(SpecHistory.version))
142→ .where(SpecHistory.project_id == project.id)
143→ )
144→ current_version = result.scalar() or 0
145→
146→ # Create new version
147→ new_version = current_version + 1
148→ new_spec = SpecHistory(
149→ project_id=project.id,
150→ version=new_version,
151→ content=spec_update.content,
152→ change_summary=spec_update.change_summary,
153→ )
154→ db.add(new_spec)
155→
156→ # Update project's spec_version
157→ project.spec_version = new_version
158→ project.updated_at = datetime.utcnow()
159→
160→ await db.commit()
161→ await db.refresh(new_spec)
162→
163→ return SpecResponse(
164→ project_id=new_spec.project_id,
165→ version=new_spec.version,
166→ content=new_spec.content,
167→ created_at=new_spec.created_at,
168→ change_summary=new_spec.change_summary
169→ )
170→
171→
172→@router.get("/{name}/spec/history", response_model=SpecHistoryListResponse)
173→async def get_spec_history(name: str, db: AsyncSession = Depends(get_db)):
174→ """Get the spec version history for a project."""
175→ # Verify project exists
176→ result = await db.execute(select(Project).where(Project.name == name))
177→ project = result.scalar_one_or_none()
178→
179→ if not project:
180→ raise HTTPException(
181→ status_code=status.HTTP_404_NOT_FOUND,
182→ detail=f"Project '{name}' not found"
183→ )
184→
185→ # Get all spec versions
186→ result = await db.execute(
187→ select(SpecHistory)
188→ .where(SpecHistory.project_id == project.id)
189→ .order_by(SpecHistory.version.desc())
190→ )
191→ history = result.scalars().all()
192→
193→ return SpecHistoryListResponse(
194→ history=[SpecHistoryResponse.model_validate(h) for h in history],
195→ count=len(history)
196→ )
197→
198→
199→@router.post("/{name}/spec/rollback/{version}", response_model=SpecResponse)
200→async def rollback_spec(name: str, version: int, db: AsyncSession = Depends(get_db)):
201→ """Rollback to a previous spec version."""
202→ # Verify project exists
203→ result = await db.execute(select(Project).where(Project.name == name))
204→ project = result.scalar_one_or_none()
205→
206→ if not project:
207→ raise HTTPException(
208→ status_code=status.HTTP_404_NOT_FOUND,
209→ detail=f"Project '{name}' not found"
210→ )
211→
212→ # Get the target version
213→ result = await db.execute(
214→ select(SpecHistory).where(and_(
215→ SpecHistory.project_id == project.id,
216→ SpecHistory.version == version
217→ ))
218→ )
219→ target_spec = result.scalar_one_or_none()
220→
221→ if not target_spec:
222→ raise HTTPException(
223→ status_code=status.HTTP_404_NOT_FOUND,
224→ detail=f"Spec version {version} not found"
225→ )
226→
227→ # Get current version number
228→ result = await db.execute(
229→ select(func.max(SpecHistory.version))
230→ .where(SpecHistory.project_id == project.id)
231→ )
232→ current_version = result.scalar() or 0
233→
234→ # Create new version with the old content
235→ new_version = current_version + 1
236→ new_spec = SpecHistory(
237→ project_id=project.id,
238→ version=new_version,
239→ content=target_spec.content,
240→ change_summary=f"Rollback to version {version}",
241→ )
242→ db.add(new_spec)
243→
244→ # Update project's spec_version
245→ project.spec_version = new_version
246→ project.updated_at = datetime.utcnow()
247→
248→ await db.commit()
249→ await db.refresh(new_spec)
250→
251→ return SpecResponse(
252→ project_id=new_spec.project_id,
253→ version=new_spec.version,
254→ content=new_spec.content,
255→ created_at=new_spec.created_at,
256→ change_summary=new_spec.change_summary
257→ )
258→
259→
260→@router.post("/{name}/spec/analyze-changes", response_model=SpecAnalysisResponse)
261→async def analyze_spec_changes(name: str, spec_update: SpecUpdate, db: AsyncSession = Depends(get_db)):
262→ """Preview the impact of spec changes before applying them."""
263→ # Verify project exists
264→ result = await db.execute(select(Project).where(Project.name == name))
265→ project = result.scalar_one_or_none()
266→
267→ if not project:
268→ raise HTTPException(
269→ status_code=status.HTTP_404_NOT_FOUND,
270→ detail=f"Project '{name}' not found"
271→ )
272→
273→ # Get current spec content for comparison
274→ result = await db.execute(
275→ select(SpecHistory)
276→ .where(SpecHistory.project_id == project.id)
277→ .order_by(SpecHistory.version.desc())
278→ .limit(1)
279→ )
280→ current_spec = result.scalar_one_or_none()
281→ current_content = current_spec.content if current_spec else ""
282→
283→ # Get all features for this project
284→ result = await db.execute(
285→ select(Feature)
286→ .where(Feature.project_id == project.id)
287→ )
288→ features = result.scalars().all()
289→
290→ # Analyze which features might be affected by the spec change
291→ # Simple heuristic: features whose names or descriptions contain words that appear
292→ # in the changed parts of the spec
293→ affected = []
294→ new_content = spec_update.content.lower()
295→ old_content = current_content.lower()
296→
297→ # Find words that are new in the spec
298→ new_words = set(new_content.split()) - set(old_content.split())
299→ changed_words = set(new_content.split()) ^ set(old_content.split()) # symmetric difference
300→
301→ for feature in features:
302→ feature_text = f"{feature.name} {feature.description or ''} {feature.category}".lower()
303→ feature_words = set(feature_text.split())
304→
305→ # Check if any changed words appear in the feature
306→ matching_words = feature_words & changed_words
307→ if matching_words:
308→ affected.append(AffectedFeatureResponse(
309→ id=feature.id,
310→ name=feature.name,
311→ category=feature.category,
312→ reason=f"Contains changed terms: {', '.join(list(matching_words)[:3])}"
313→ ))
314→
315→ # Generate change summary
316→ if current_content:
317→ lines_old = len(current_content.split('\n'))
318→ lines_new = len(spec_update.content.split('\n'))
319→ line_diff = lines_new - lines_old
320→ change_summary = spec_update.change_summary or f"Spec change: {line_diff:+d} lines, {len(changed_words)} terms changed"
321→ else:
322→ change_summary = spec_update.change_summary or "Initial spec creation"
323→
324→ return SpecAnalysisResponse(
325→ affected_features=affected,
326→ affected_count=len(affected),
327→ change_summary=change_summary
328→ )
329→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

View File

@@ -0,0 +1,4 @@
/c/Users/MikeSwanson/.claude/projects/C--Users-MikeSwanson-claude-projects-Autocode-remix-Autocode-fork
/c/Users/MikeSwanson/AppData/Local/claude-cli-nodejs/Cache/C--Users-MikeSwanson-claude-projects-Autocode-remix-Autocode-fork
/c/Users/MikeSwanson/AppData/Local/Temp/claude/C--Users-MikeSwanson-claude-projects-Autocode-remix-Autocode-fork
/c/Users/MikeSwanson/claude-projects/Autocode-remix

View File

@@ -0,0 +1,26 @@
Resolved 16 packages in 1.21s
Downloading pygments (1.2MiB)
Downloading numpy (11.7MiB)
Downloading pydantic-core (1.9MiB)
Downloading pygments
Downloading pydantic-core
Downloading numpy
Prepared 14 packages in 1.87s
Installed 16 packages in 386ms
+ annotated-types==0.7.0
+ claude-monitor==3.1.0
+ markdown-it-py==4.0.0
+ mdurl==0.1.2
+ numpy==2.4.0
+ pydantic==2.12.5
+ pydantic-core==2.41.5
+ pydantic-settings==2.12.0
+ pygments==2.19.2
+ python-dotenv==1.2.1
+ pytz==2025.2
+ pyyaml==6.0.3
+ rich==14.2.0
+ typing-extensions==4.15.0
+ typing-inspection==0.4.2
+ tzdata==2025.3
Installed 5 executables: ccm, ccmonitor, claude-code-monitor, claude-monitor, cmonitor

View File

@@ -0,0 +1,389 @@
1→"""
2→Spec Creation Router
3→====================
4→
5→WebSocket and REST endpoints for interactive spec creation with Claude.
6→"""
7→
8→import json
9→import logging
10→import re
11→from pathlib import Path
12→from typing import Optional
13→
14→from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect
15→from pydantic import BaseModel, ValidationError
16→
17→from ..schemas import ImageAttachment
18→from ..services.spec_chat_session import (
19→ SpecChatSession,
20→ create_session,
21→ get_session,
22→ list_sessions,
23→ remove_session,
24→)
25→
26→logger = logging.getLogger(__name__)
27→
28→router = APIRouter(prefix="/api/spec", tags=["spec-creation"])
29→
30→# Root directory
31→ROOT_DIR = Path(__file__).parent.parent.parent
32→
33→
34→def _get_project_path(project_name: str) -> Path:
35→ """Get project path from registry."""
36→ import sys
37→ root = Path(__file__).parent.parent.parent
38→ if str(root) not in sys.path:
39→ sys.path.insert(0, str(root))
40→
41→ from registry import get_project_path
42→ return get_project_path(project_name)
43→
44→
45→def validate_project_name(name: str) -> bool:
46→ """Validate project name to prevent path traversal."""
47→ return bool(re.match(r'^[a-zA-Z0-9_-]{1,50}$', name))
48→
49→
50→# ============================================================================
51→# REST Endpoints
52→# ============================================================================
53→
54→class SpecSessionStatus(BaseModel):
55→ """Status of a spec creation session."""
56→ project_name: str
57→ is_active: bool
58→ is_complete: bool
59→ message_count: int
60→
61→
62→@router.get("/sessions", response_model=list[str])
63→async def list_spec_sessions():
64→ """List all active spec creation sessions."""
65→ return list_sessions()
66→
67→
68→@router.get("/sessions/{project_name}", response_model=SpecSessionStatus)
69→async def get_session_status(project_name: str):
70→ """Get status of a spec creation session."""
71→ if not validate_project_name(project_name):
72→ raise HTTPException(status_code=400, detail="Invalid project name")
73→
74→ session = get_session(project_name)
75→ if not session:
76→ raise HTTPException(status_code=404, detail="No active session for this project")
77→
78→ return SpecSessionStatus(
79→ project_name=project_name,
80→ is_active=True,
81→ is_complete=session.is_complete(),
82→ message_count=len(session.get_messages()),
83→ )
84→
85→
86→@router.delete("/sessions/{project_name}")
87→async def cancel_session(project_name: str):
88→ """Cancel and remove a spec creation session."""
89→ if not validate_project_name(project_name):
90→ raise HTTPException(status_code=400, detail="Invalid project name")
91→
92→ session = get_session(project_name)
93→ if not session:
94→ raise HTTPException(status_code=404, detail="No active session for this project")
95→
96→ await remove_session(project_name)
97→ return {"success": True, "message": "Session cancelled"}
98→
99→
100→class SpecFileStatus(BaseModel):
101→ """Status of spec files on disk (from .spec_status.json)."""
102→ exists: bool
103→ status: str # "complete" | "in_progress" | "not_started"
104→ feature_count: Optional[int] = None
105→ timestamp: Optional[str] = None
106→ files_written: list[str] = []
107→
108→
109→@router.get("/status/{project_name}", response_model=SpecFileStatus)
110→async def get_spec_file_status(project_name: str):
111→ """
112→ Get spec creation status by reading .spec_status.json from the project.
113→
114→ This is used for polling to detect when Claude has finished writing spec files.
115→ Claude writes this status file as the final step after completing all spec work.
116→ """
117→ if not validate_project_name(project_name):
118→ raise HTTPException(status_code=400, detail="Invalid project name")
119→
120→ project_dir = _get_project_path(project_name)
121→ if not project_dir:
122→ raise HTTPException(status_code=404, detail="Project not found in registry")
123→
124→ if not project_dir.exists():
125→ raise HTTPException(status_code=404, detail="Project directory not found")
126→
127→ status_file = project_dir / "prompts" / ".spec_status.json"
128→
129→ if not status_file.exists():
130→ return SpecFileStatus(
131→ exists=False,
132→ status="not_started",
133→ feature_count=None,
134→ timestamp=None,
135→ files_written=[],
136→ )
137→
138→ try:
139→ data = json.loads(status_file.read_text(encoding="utf-8"))
140→ return SpecFileStatus(
141→ exists=True,
142→ status=data.get("status", "unknown"),
143→ feature_count=data.get("feature_count"),
144→ timestamp=data.get("timestamp"),
145→ files_written=data.get("files_written", []),
146→ )
147→ except json.JSONDecodeError as e:
148→ logger.warning(f"Invalid JSON in spec status file: {e}")
149→ return SpecFileStatus(
150→ exists=True,
151→ status="error",
152→ feature_count=None,
153→ timestamp=None,
154→ files_written=[],
155→ )
156→ except Exception as e:
157→ logger.error(f"Error reading spec status file: {e}")
158→ raise HTTPException(status_code=500, detail="Failed to read status file")
159→
160→
161→# ============================================================================
162→# WebSocket Endpoint
163→# ============================================================================
164→
165→@router.websocket("/ws/{project_name}")
166→async def spec_chat_websocket(websocket: WebSocket, project_name: str):
167→ """
168→ WebSocket endpoint for interactive spec creation chat.
169→
170→ Message protocol:
171→
172→ Client -> Server:
173→ - {"type": "start"} - Start the spec creation session
174→ - {"type": "message", "content": "..."} - Send user message
175→ - {"type": "answer", "answers": {...}, "tool_id": "..."} - Answer structured question
176→ - {"type": "ping"} - Keep-alive ping
177→
178→ Server -> Client:
179→ - {"type": "text", "content": "..."} - Text chunk from Claude
180→ - {"type": "question", "questions": [...], "tool_id": "..."} - Structured question
181→ - {"type": "spec_complete", "path": "..."} - Spec file created
182→ - {"type": "file_written", "path": "..."} - Other file written
183→ - {"type": "complete"} - Session complete
184→ - {"type": "error", "content": "..."} - Error message
185→ - {"type": "pong"} - Keep-alive pong
186→ """
187→ if not validate_project_name(project_name):
188→ await websocket.close(code=4000, reason="Invalid project name")
189→ return
190→
191→ # Look up project directory from registry
192→ project_dir = _get_project_path(project_name)
193→ if not project_dir:
194→ await websocket.close(code=4004, reason="Project not found in registry")
195→ return
196→
197→ if not project_dir.exists():
198→ await websocket.close(code=4004, reason="Project directory not found")
199→ return
200→
201→ await websocket.accept()
202→
203→ session: Optional[SpecChatSession] = None
204→
205→ try:
206→ while True:
207→ try:
208→ # Receive message from client
209→ data = await websocket.receive_text()
210→ message = json.loads(data)
211→ msg_type = message.get("type")
212→
213→ if msg_type == "ping":
214→ await websocket.send_json({"type": "pong"})
215→ continue
216→
217→ elif msg_type == "start":
218→ # Create and start a new session
219→ # Check for import mode parameters
220→ is_import = message.get("is_import", False)
221→ codebase_analysis = message.get("codebase_analysis")
222→
223→ session = await create_session(
224→ project_name,
225→ project_dir,
226→ is_import=is_import,
227→ codebase_analysis=codebase_analysis,
228→ )
229→
230→ # Track spec completion state
231→ spec_complete_received = False
232→ spec_path = None
233→
234→ # Stream the initial greeting
235→ async for chunk in session.start():
236→ # Track spec_complete but don't send complete yet
237→ if chunk.get("type") == "spec_complete":
238→ spec_complete_received = True
239→ spec_path = chunk.get("path")
240→ await websocket.send_json(chunk)
241→ continue
242→
243→ # When response_done arrives, send complete if spec was done
244→ if chunk.get("type") == "response_done":
245→ await websocket.send_json(chunk)
246→ if spec_complete_received:
247→ await websocket.send_json({"type": "complete", "path": spec_path})
248→ continue
249→
250→ await websocket.send_json(chunk)
251→
252→ elif msg_type == "message":
253→ # User sent a message
254→ if not session:
255→ session = get_session(project_name)
256→ if not session:
257→ await websocket.send_json({
258→ "type": "error",
259→ "content": "No active session. Send 'start' first."
260→ })
261→ continue
262→
263→ user_content = message.get("content", "").strip()
264→
265→ # Parse attachments if present
266→ attachments: list[ImageAttachment] = []
267→ raw_attachments = message.get("attachments", [])
268→ if raw_attachments:
269→ try:
270→ for raw_att in raw_attachments:
271→ attachments.append(ImageAttachment(**raw_att))
272→ except (ValidationError, Exception) as e:
273→ logger.warning(f"Invalid attachment data: {e}")
274→ await websocket.send_json({
275→ "type": "error",
276→ "content": f"Invalid attachment: {str(e)}"
277→ })
278→ continue
279→
280→ # Allow empty content if attachments are present
281→ if not user_content and not attachments:
282→ await websocket.send_json({
283→ "type": "error",
284→ "content": "Empty message"
285→ })
286→ continue
287→
288→ # Track spec completion state
289→ spec_complete_received = False
290→ spec_path = None
291→
292→ # Stream Claude's response (with attachments if present)
293→ async for chunk in session.send_message(user_content, attachments if attachments else None):
294→ # Track spec_complete but don't send complete yet
295→ if chunk.get("type") == "spec_complete":
296→ spec_complete_received = True
297→ spec_path = chunk.get("path")
298→ await websocket.send_json(chunk)
299→ continue
300→
301→ # When response_done arrives, send complete if spec was done
302→ if chunk.get("type") == "response_done":
303→ await websocket.send_json(chunk)
304→ if spec_complete_received:
305→ await websocket.send_json({"type": "complete", "path": spec_path})
306→ continue
307→
308→ await websocket.send_json(chunk)
309→
310→ elif msg_type == "answer":
311→ # User answered a structured question
312→ if not session:
313→ session = get_session(project_name)
314→ if not session:
315→ await websocket.send_json({
316→ "type": "error",
317→ "content": "No active session"
318→ })
319→ continue
320→
321→ # Format the answers as a natural response
322→ answers = message.get("answers", {})
323→ if isinstance(answers, dict):
324→ # Convert structured answers to a message
325→ response_parts = []
326→ for question_idx, answer_value in answers.items():
327→ if isinstance(answer_value, list):
328→ response_parts.append(", ".join(answer_value))
329→ else:
330→ response_parts.append(str(answer_value))
331→ user_response = "; ".join(response_parts) if response_parts else "OK"
332→ else:
333→ user_response = str(answers)
334→
335→ # Track spec completion state
336→ spec_complete_received = False
337→ spec_path = None
338→
339→ # Stream Claude's response
340→ async for chunk in session.send_message(user_response):
341→ # Track spec_complete but don't send complete yet
342→ if chunk.get("type") == "spec_complete":
343→ spec_complete_received = True
344→ spec_path = chunk.get("path")
345→ await websocket.send_json(chunk)
346→ continue
347→
348→ # When response_done arrives, send complete if spec was done
349→ if chunk.get("type") == "response_done":
350→ await websocket.send_json(chunk)
351→ if spec_complete_received:
352→ await websocket.send_json({"type": "complete", "path": spec_path})
353→ continue
354→
355→ await websocket.send_json(chunk)
356→
357→ else:
358→ await websocket.send_json({
359→ "type": "error",
360→ "content": f"Unknown message type: {msg_type}"
361→ })
362→
363→ except json.JSONDecodeError:
364→ await websocket.send_json({
365→ "type": "error",
366→ "content": "Invalid JSON"
367→ })
368→
369→ except WebSocketDisconnect:
370→ logger.info(f"Spec chat WebSocket disconnected for {project_name}")
371→
372→ except Exception as e:
373→ logger.exception(f"Spec chat WebSocket error for {project_name}")
374→ try:
375→ await websocket.send_json({
376→ "type": "error",
377→ "content": f"Server error: {str(e)}"
378→ })
379→ except Exception:
380→ pass
381→
382→ finally:
383→ # Don't remove the session on disconnect - allow resume
384→ pass
385→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

View File

@@ -0,0 +1,7 @@
Cleaned 40 records from features
Cleaned 20 records from permissions
Cleaned 11 records from infrastructure
Cleaned 8 records from spec_history
Cleaned 10 records from permission_requests
Cleanup complete!

View File

@@ -0,0 +1,15 @@
The file C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\frontend\src\App.tsx has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
1→import { useState, useEffect } from 'react'
2→import { Routes, Route, useNavigate, useParams, Navigate, useLocation, Link } from 'react-router-dom'
3→import ProjectList from './components/ProjectList'
4→import KanbanBoard from './components/KanbanBoard'
5→import SpecEditor from './components/SpecEditor'
6→import PermissionEditor from './components/PermissionEditor'
7→import InfrastructureDashboard from './components/InfrastructureDashboard'
8→import ProjectSelector from './components/ProjectSelector'
9→import NotFound from './components/NotFound'
10→import SpecInterview from './components/SpecInterview'
11→
12→type ProjectView = 'kanban' | 'spec' | 'permissions' | 'infrastructure' | 'interview'
13→
14→interface HealthStatus {

View File

@@ -0,0 +1,79 @@
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\.gitattributes
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\.gitignore
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\.pre-commit-config.yaml
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\CHANGELOG.md
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\CONTRIBUTING.md
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\DEVELOPMENT.md
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\LICENSE
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\README.md
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\RELEASE.md
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\TROUBLESHOOTING.md
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\VERSION_MANAGEMENT.md
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\pyproject.toml
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\.github\FUNDING.yml
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\.github\workflows\lint.yml
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\.github\workflows\release.yml
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\.github\workflows\test.yml
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\.github\workflows\version-bump.yml
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\doc\sc.png
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\doc\scnew.png
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\__init__.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\__main__.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\_version.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\error_handling.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\cli\__init__.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\cli\bootstrap.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\cli\main.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\core\__init__.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\core\calculations.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\core\data_processors.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\core\models.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\core\p90_calculator.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\core\plans.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\core\pricing.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\core\settings.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\data\__init__.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\data\aggregator.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\data\analysis.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\data\analyzer.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\data\reader.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\monitoring\__init__.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\monitoring\data_manager.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\monitoring\orchestrator.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\monitoring\session_monitor.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\terminal\__init__.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\terminal\manager.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\terminal\themes.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\ui\__init__.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\ui\components.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\ui\display_controller.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\ui\layouts.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\ui\progress_bars.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\ui\session_display.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\ui\table_views.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\utils\__init__.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\utils\formatting.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\utils\model_utils.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\utils\notifications.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\utils\time_utils.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\claude_monitor\utils\timezone.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\tests\__init__.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\tests\conftest.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\tests\run_tests.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\tests\test_aggregator.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\tests\test_analysis.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\tests\test_calculations.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\tests\test_cli_main.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\tests\test_data_reader.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\tests\test_display_controller.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\tests\test_error_handling.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\tests\test_formatting.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\tests\test_monitoring_orchestrator.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\tests\test_pricing.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\tests\test_session_analyzer.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\tests\test_settings.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\tests\test_table_views.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\tests\test_time_utils.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\tests\test_timezone.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\tests\test_version.py
D:\Users\MikeSwanson\Downloads\Claude-Code-Usage-Monitor-main\src\tests\examples\api_examples.py

View File

@@ -0,0 +1,12 @@
total 95
drwxr-xr-x 1 AzureAD+MikeSwanson 4096 0 Jan 9 06:06 ./
drwxr-xr-x 1 AzureAD+MikeSwanson 4096 0 Jan 9 06:08 ../
drwxr-xr-x 1 AzureAD+MikeSwanson 4096 0 Jan 9 04:27 app/
-rw-r--r-- 1 AzureAD+MikeSwanson 4096 61440 Jan 9 06:06 autocoder_plus.db
-rwxr-xr-x 1 AzureAD+MikeSwanson 4096 370 Jan 8 17:09 check_routes.py*
-rwxr-xr-x 1 AzureAD+MikeSwanson 4096 746 Jan 8 15:38 check_tables.py*
-rwxr-xr-x 1 AzureAD+MikeSwanson 4096 689 Jan 8 22:35 migrate_add_depends_on.py*
-rw-r--r-- 1 AzureAD+MikeSwanson 4096 248 Jan 8 14:50 requirements.txt
-rwxr-xr-x 1 AzureAD+MikeSwanson 4096 393 Jan 8 16:40 test_import.py*
-rwxr-xr-x 1 AzureAD+MikeSwanson 4096 1650 Jan 8 17:12 test_imports.py*
drwxr-xr-x 1 AzureAD+MikeSwanson 4096 0 Jan 8 14:54 venv/

View File

@@ -0,0 +1,8 @@
Exit code 1
Traceback (most recent call last):
File "<string>", line 1, in <module>
from registry import cleanup_stale_projects, list_registered_projects; print('Before:', len(list_registered_projects()), 'projects'); removed = cleanup_stale_projects(); print('Removed:', removed); print('After:', len(list_registered_projects()), 'projects')
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\registry.py", line 17, in <module>
from sqlalchemy import Column, DateTime, String, create_engine
ModuleNotFoundError: No module named 'sqlalchemy'

View File

@@ -0,0 +1,4 @@
dir: cannot access '/s': No such file or directory
dir: cannot access '/b': No such file or directory
dir: cannot access 'C:UsersMikeSwanson*autocoder*': No such file or directory
dir: cannot access '2': No such file or directory

View File

@@ -0,0 +1,476 @@
1→/**
2→ * Hook for managing spec creation chat WebSocket connection
3→ */
4→
5→import { useState, useCallback, useRef, useEffect } from 'react'
6→import type { ChatMessage, ImageAttachment, SpecChatServerMessage, SpecQuestion } from '../lib/types'
7→import { getSpecStatus } from '../lib/api'
8→
9→type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'error'
10→
11→interface UseSpecChatOptions {
12→ projectName: string
13→ onComplete?: (specPath: string) => void
14→ onError?: (error: string) => void
15→}
16→
17→interface UseSpecChatReturn {
18→ messages: ChatMessage[]
19→ isLoading: boolean
20→ isComplete: boolean
21→ connectionStatus: ConnectionStatus
22→ currentQuestions: SpecQuestion[] | null
23→ currentToolId: string | null
24→ start: () => void
25→ sendMessage: (content: string, attachments?: ImageAttachment[]) => void
26→ sendAnswer: (answers: Record<string, string | string[]>) => void
27→ disconnect: () => void
28→}
29→
30→function generateId(): string {
31→ return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
32→}
33→
34→export function useSpecChat({
35→ projectName,
36→ onComplete,
37→ onError,
38→}: UseSpecChatOptions): UseSpecChatReturn {
39→ const [messages, setMessages] = useState<ChatMessage[]>([])
40→ const [isLoading, setIsLoading] = useState(false)
41→ const [isComplete, setIsComplete] = useState(false)
42→ const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>('disconnected')
43→ const [currentQuestions, setCurrentQuestions] = useState<SpecQuestion[] | null>(null)
44→ const [currentToolId, setCurrentToolId] = useState<string | null>(null)
45→
46→ const wsRef = useRef<WebSocket | null>(null)
47→ const currentAssistantMessageRef = useRef<string | null>(null)
48→ const reconnectAttempts = useRef(0)
49→ const maxReconnectAttempts = 3
50→ const pingIntervalRef = useRef<number | null>(null)
51→ const reconnectTimeoutRef = useRef<number | null>(null)
52→ const isCompleteRef = useRef(false)
53→
54→ // Keep isCompleteRef in sync with isComplete state
55→ useEffect(() => {
56→ isCompleteRef.current = isComplete
57→ }, [isComplete])
58→
59→ // Clean up on unmount
60→ useEffect(() => {
61→ return () => {
62→ if (pingIntervalRef.current) {
63→ clearInterval(pingIntervalRef.current)
64→ }
65→ if (reconnectTimeoutRef.current) {
66→ clearTimeout(reconnectTimeoutRef.current)
67→ }
68→ if (wsRef.current) {
69→ wsRef.current.close()
70→ }
71→ }
72→ }, [])
73→
74→ // Poll status file as fallback completion detection
75→ // Claude writes .spec_status.json when done with all spec work
76→ useEffect(() => {
77→ // Don't poll if already complete
78→ if (isComplete) return
79→
80→ // Start polling after initial delay (let WebSocket try first)
81→ const startDelay = setTimeout(() => {
82→ const pollInterval = setInterval(async () => {
83→ // Stop if already complete
84→ if (isCompleteRef.current) {
85→ clearInterval(pollInterval)
86→ return
87→ }
88→
89→ try {
90→ const status = await getSpecStatus(projectName)
91→
92→ if (status.exists && status.status === 'complete') {
93→ // Status file indicates completion - set complete state
94→ setIsComplete(true)
95→ setIsLoading(false)
96→
97→ // Mark any streaming message as done
98→ setMessages((prev) => {
99→ const lastMessage = prev[prev.length - 1]
100→ if (lastMessage?.role === 'assistant' && lastMessage.isStreaming) {
101→ return [
102→ ...prev.slice(0, -1),
103→ { ...lastMessage, isStreaming: false },
104→ ]
105→ }
106→ return prev
107→ })
108→
109→ // Add system message about completion
110→ setMessages((prev) => [
111→ ...prev,
112→ {
113→ id: generateId(),
114→ role: 'system',
115→ content: `Spec creation complete! Files written: ${status.files_written.join(', ')}${status.feature_count ? ` (${status.feature_count} features)` : ''}`,
116→ timestamp: new Date(),
117→ },
118→ ])
119→
120→ clearInterval(pollInterval)
121→ }
122→ } catch {
123→ // Silently ignore polling errors - WebSocket is primary mechanism
124→ }
125→ }, 3000) // Poll every 3 seconds
126→
127→ // Cleanup interval on unmount
128→ return () => clearInterval(pollInterval)
129→ }, 3000) // Start polling after 3 second delay
130→
131→ return () => clearTimeout(startDelay)
132→ }, [projectName, isComplete])
133→
134→ const connect = useCallback(() => {
135→ if (wsRef.current?.readyState === WebSocket.OPEN) {
136→ return
137→ }
138→
139→ setConnectionStatus('connecting')
140→
141→ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
142→ const host = window.location.host
143→ const wsUrl = `${protocol}//${host}/api/spec/ws/${encodeURIComponent(projectName)}`
144→
145→ const ws = new WebSocket(wsUrl)
146→ wsRef.current = ws
147→
148→ ws.onopen = () => {
149→ setConnectionStatus('connected')
150→ reconnectAttempts.current = 0
151→
152→ // Start ping interval to keep connection alive
153→ pingIntervalRef.current = window.setInterval(() => {
154→ if (ws.readyState === WebSocket.OPEN) {
155→ ws.send(JSON.stringify({ type: 'ping' }))
156→ }
157→ }, 30000)
158→ }
159→
160→ ws.onclose = () => {
161→ setConnectionStatus('disconnected')
162→ if (pingIntervalRef.current) {
163→ clearInterval(pingIntervalRef.current)
164→ pingIntervalRef.current = null
165→ }
166→
167→ // Attempt reconnection if not intentionally closed
168→ if (reconnectAttempts.current < maxReconnectAttempts && !isCompleteRef.current) {
169→ reconnectAttempts.current++
170→ const delay = Math.min(1000 * Math.pow(2, reconnectAttempts.current), 10000)
171→ reconnectTimeoutRef.current = window.setTimeout(connect, delay)
172→ }
173→ }
174→
175→ ws.onerror = () => {
176→ setConnectionStatus('error')
177→ onError?.('WebSocket connection error')
178→ }
179→
180→ ws.onmessage = (event) => {
181→ try {
182→ const data = JSON.parse(event.data) as SpecChatServerMessage
183→
184→ switch (data.type) {
185→ case 'text': {
186→ // Append text to current assistant message or create new one
187→ setMessages((prev) => {
188→ const lastMessage = prev[prev.length - 1]
189→ if (lastMessage?.role === 'assistant' && lastMessage.isStreaming) {
190→ // Append to existing streaming message
191→ return [
192→ ...prev.slice(0, -1),
193→ {
194→ ...lastMessage,
195→ content: lastMessage.content + data.content,
196→ },
197→ ]
198→ } else {
199→ // Create new assistant message
200→ currentAssistantMessageRef.current = generateId()
201→ return [
202→ ...prev,
203→ {
204→ id: currentAssistantMessageRef.current,
205→ role: 'assistant',
206→ content: data.content,
207→ timestamp: new Date(),
208→ isStreaming: true,
209→ },
210→ ]
211→ }
212→ })
213→ break
214→ }
215→
216→ case 'question': {
217→ // Show structured question UI
218→ setCurrentQuestions(data.questions)
219→ setCurrentToolId(data.tool_id || null)
220→ setIsLoading(false)
221→
222→ // Mark current message as done streaming
223→ setMessages((prev) => {
224→ const lastMessage = prev[prev.length - 1]
225→ if (lastMessage?.role === 'assistant' && lastMessage.isStreaming) {
226→ return [
227→ ...prev.slice(0, -1),
228→ {
229→ ...lastMessage,
230→ isStreaming: false,
231→ questions: data.questions,
232→ },
233→ ]
234→ }
235→ return prev
236→ })
237→ break
238→ }
239→
240→ case 'spec_complete': {
241→ setIsComplete(true)
242→ setIsLoading(false)
243→
244→ // Mark current message as done
245→ setMessages((prev) => {
246→ const lastMessage = prev[prev.length - 1]
247→ if (lastMessage?.role === 'assistant' && lastMessage.isStreaming) {
248→ return [
249→ ...prev.slice(0, -1),
250→ { ...lastMessage, isStreaming: false },
251→ ]
252→ }
253→ return prev
254→ })
255→
256→ // Add system message about spec completion
257→ setMessages((prev) => [
258→ ...prev,
259→ {
260→ id: generateId(),
261→ role: 'system',
262→ content: `Specification file created: ${data.path}`,
263→ timestamp: new Date(),
264→ },
265→ ])
266→
267→ // NOTE: Do NOT auto-call onComplete here!
268→ // User should click "Continue to Project" button to start the agent.
269→ // This matches the CLI behavior where user closes the chat manually.
270→ break
271→ }
272→
273→ case 'file_written': {
274→ // Optional: notify about other files being written
275→ setMessages((prev) => [
276→ ...prev,
277→ {
278→ id: generateId(),
279→ role: 'system',
280→ content: `File created: ${data.path}`,
281→ timestamp: new Date(),
282→ },
283→ ])
284→ break
285→ }
286→
287→ case 'complete': {
288→ setIsComplete(true)
289→ setIsLoading(false)
290→
291→ // Mark current message as done
292→ setMessages((prev) => {
293→ const lastMessage = prev[prev.length - 1]
294→ if (lastMessage?.role === 'assistant' && lastMessage.isStreaming) {
295→ return [
296→ ...prev.slice(0, -1),
297→ { ...lastMessage, isStreaming: false },
298→ ]
299→ }
300→ return prev
301→ })
302→ break
303→ }
304→
305→ case 'error': {
306→ setIsLoading(false)
307→ onError?.(data.content)
308→
309→ // Add error as system message
310→ setMessages((prev) => [
311→ ...prev,
312→ {
313→ id: generateId(),
314→ role: 'system',
315→ content: `Error: ${data.content}`,
316→ timestamp: new Date(),
317→ },
318→ ])
319→ break
320→ }
321→
322→ case 'pong': {
323→ // Keep-alive response, nothing to do
324→ break
325→ }
326→
327→ case 'response_done': {
328→ // Response complete - hide loading indicator and mark message as done
329→ setIsLoading(false)
330→
331→ // Mark current message as done streaming
332→ setMessages((prev) => {
333→ const lastMessage = prev[prev.length - 1]
334→ if (lastMessage?.role === 'assistant' && lastMessage.isStreaming) {
335→ return [
336→ ...prev.slice(0, -1),
337→ { ...lastMessage, isStreaming: false },
338→ ]
339→ }
340→ return prev
341→ })
342→ break
343→ }
344→ }
345→ } catch (e) {
346→ console.error('Failed to parse WebSocket message:', e)
347→ }
348→ }
349→ }, [projectName, onComplete, onError])
350→
351→ const start = useCallback(() => {
352→ connect()
353→
354→ // Wait for connection then send start message
355→ const checkAndSend = () => {
356→ if (wsRef.current?.readyState === WebSocket.OPEN) {
357→ setIsLoading(true)
358→ wsRef.current.send(JSON.stringify({ type: 'start' }))
359→ } else if (wsRef.current?.readyState === WebSocket.CONNECTING) {
360→ setTimeout(checkAndSend, 100)
361→ }
362→ }
363→
364→ setTimeout(checkAndSend, 100)
365→ }, [connect])
366→
367→ const sendMessage = useCallback((content: string, attachments?: ImageAttachment[]) => {
368→ if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
369→ onError?.('Not connected')
370→ return
371→ }
372→
373→ // Add user message to chat (with attachments for display)
374→ setMessages((prev) => [
375→ ...prev,
376→ {
377→ id: generateId(),
378→ role: 'user',
379→ content,
380→ attachments,
381→ timestamp: new Date(),
382→ },
383→ ])
384→
385→ // Clear current questions
386→ setCurrentQuestions(null)
387→ setCurrentToolId(null)
388→ setIsLoading(true)
389→
390→ // Build message payload
391→ const payload: { type: string; content: string; attachments?: Array<{ filename: string; mimeType: string; base64Data: string }> } = {
392→ type: 'message',
393→ content,
394→ }
395→
396→ // Add attachments if present (send base64 data, not preview URL)
397→ if (attachments && attachments.length > 0) {
398→ payload.attachments = attachments.map((a) => ({
399→ filename: a.filename,
400→ mimeType: a.mimeType,
401→ base64Data: a.base64Data,
402→ }))
403→ }
404→
405→ // Send to server
406→ wsRef.current.send(JSON.stringify(payload))
407→ }, [onError])
408→
409→ const sendAnswer = useCallback((answers: Record<string, string | string[]>) => {
410→ if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
411→ onError?.('Not connected')
412→ return
413→ }
414→
415→ // Format answers for display
416→ const answerText = Object.values(answers)
417→ .map((v) => (Array.isArray(v) ? v.join(', ') : v))
418→ .join('; ')
419→
420→ // Add user message
421→ setMessages((prev) => [
422→ ...prev,
423→ {
424→ id: generateId(),
425→ role: 'user',
426→ content: answerText,
427→ timestamp: new Date(),
428→ },
429→ ])
430→
431→ // Clear current questions
432→ setCurrentQuestions(null)
433→ setCurrentToolId(null)
434→ setIsLoading(true)
435→
436→ // Send to server
437→ wsRef.current.send(
438→ JSON.stringify({
439→ type: 'answer',
440→ answers,
441→ tool_id: currentToolId,
442→ })
443→ )
444→ }, [currentToolId, onError])
445→
446→ const disconnect = useCallback(() => {
447→ reconnectAttempts.current = maxReconnectAttempts // Prevent reconnection
448→ if (pingIntervalRef.current) {
449→ clearInterval(pingIntervalRef.current)
450→ pingIntervalRef.current = null
451→ }
452→ if (wsRef.current) {
453→ wsRef.current.close()
454→ wsRef.current = null
455→ }
456→ setConnectionStatus('disconnected')
457→ }, [])
458→
459→ return {
460→ messages,
461→ isLoading,
462→ isComplete,
463→ connectionStatus,
464→ currentQuestions,
465→ currentToolId,
466→ start,
467→ sendMessage,
468→ sendAnswer,
469→ disconnect,
470→ }
471→}
472→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

View File

@@ -0,0 +1,423 @@
1→/**
2→ * New Project Modal Component
3→ *
4→ * Multi-step modal for creating new projects:
5→ * 1. Enter project name
6→ * 2. Select project folder
7→ * 3. Choose spec method (Claude or manual)
8→ * 4a. If Claude: Show SpecCreationChat
9→ * 4b. If manual: Create project and close
10→ */
11→
12→import { useState } from 'react'
13→import { X, Bot, FileEdit, ArrowRight, ArrowLeft, Loader2, CheckCircle2, Folder } from 'lucide-react'
14→import { useCreateProject } from '../hooks/useProjects'
15→import { SpecCreationChat } from './SpecCreationChat'
16→import { FolderBrowser } from './FolderBrowser'
17→import { startAgent } from '../lib/api'
18→
19→type InitializerStatus = 'idle' | 'starting' | 'error'
20→
21→type Step = 'name' | 'folder' | 'method' | 'chat' | 'complete'
22→type SpecMethod = 'claude' | 'manual'
23→
24→interface NewProjectModalProps {
25→ isOpen: boolean
26→ onClose: () => void
27→ onProjectCreated: (projectName: string) => void
28→}
29→
30→export function NewProjectModal({
31→ isOpen,
32→ onClose,
33→ onProjectCreated,
34→}: NewProjectModalProps) {
35→ const [step, setStep] = useState<Step>('name')
36→ const [projectName, setProjectName] = useState('')
37→ const [projectPath, setProjectPath] = useState<string | null>(null)
38→ const [_specMethod, setSpecMethod] = useState<SpecMethod | null>(null)
39→ const [error, setError] = useState<string | null>(null)
40→ const [initializerStatus, setInitializerStatus] = useState<InitializerStatus>('idle')
41→ const [initializerError, setInitializerError] = useState<string | null>(null)
42→ const [yoloModeSelected, setYoloModeSelected] = useState(false)
43→
44→ // Suppress unused variable warning - specMethod may be used in future
45→ void _specMethod
46→
47→ const createProject = useCreateProject()
48→
49→ if (!isOpen) return null
50→
51→ const handleNameSubmit = (e: React.FormEvent) => {
52→ e.preventDefault()
53→ const trimmed = projectName.trim()
54→
55→ if (!trimmed) {
56→ setError('Please enter a project name')
57→ return
58→ }
59→
60→ if (!/^[a-zA-Z0-9_-]+$/.test(trimmed)) {
61→ setError('Project name can only contain letters, numbers, hyphens, and underscores')
62→ return
63→ }
64→
65→ setError(null)
66→ setStep('folder')
67→ }
68→
69→ const handleFolderSelect = (path: string) => {
70→ // Append project name to the selected path
71→ const fullPath = path.endsWith('/') ? `${path}${projectName.trim()}` : `${path}/${projectName.trim()}`
72→ setProjectPath(fullPath)
73→ setStep('method')
74→ }
75→
76→ const handleFolderCancel = () => {
77→ setStep('name')
78→ }
79→
80→ const handleMethodSelect = async (method: SpecMethod) => {
81→ setSpecMethod(method)
82→
83→ if (!projectPath) {
84→ setError('Please select a project folder first')
85→ setStep('folder')
86→ return
87→ }
88→
89→ if (method === 'manual') {
90→ // Create project immediately with manual method
91→ try {
92→ const project = await createProject.mutateAsync({
93→ name: projectName.trim(),
94→ path: projectPath,
95→ specMethod: 'manual',
96→ })
97→ setStep('complete')
98→ setTimeout(() => {
99→ onProjectCreated(project.name)
100→ handleClose()
101→ }, 1500)
102→ } catch (err: unknown) {
103→ setError(err instanceof Error ? err.message : 'Failed to create project')
104→ }
105→ } else {
106→ // Create project then show chat
107→ try {
108→ await createProject.mutateAsync({
109→ name: projectName.trim(),
110→ path: projectPath,
111→ specMethod: 'claude',
112→ })
113→ setStep('chat')
114→ } catch (err: unknown) {
115→ setError(err instanceof Error ? err.message : 'Failed to create project')
116→ }
117→ }
118→ }
119→
120→ const handleSpecComplete = async (_specPath: string, yoloMode: boolean = false) => {
121→ // Save yoloMode for retry
122→ setYoloModeSelected(yoloMode)
123→ // Auto-start the initializer agent
124→ setInitializerStatus('starting')
125→ try {
126→ await startAgent(projectName.trim(), yoloMode)
127→ // Success - navigate to project
128→ setStep('complete')
129→ setTimeout(() => {
130→ onProjectCreated(projectName.trim())
131→ handleClose()
132→ }, 1500)
133→ } catch (err) {
134→ setInitializerStatus('error')
135→ setInitializerError(err instanceof Error ? err.message : 'Failed to start agent')
136→ }
137→ }
138→
139→ const handleRetryInitializer = () => {
140→ setInitializerError(null)
141→ setInitializerStatus('idle')
142→ handleSpecComplete('', yoloModeSelected)
143→ }
144→
145→ const handleChatCancel = () => {
146→ // Go back to method selection but keep the project
147→ setStep('method')
148→ setSpecMethod(null)
149→ }
150→
151→ const handleExitToProject = () => {
152→ // Exit chat and go directly to project - user can start agent manually
153→ onProjectCreated(projectName.trim())
154→ handleClose()
155→ }
156→
157→ const handleClose = () => {
158→ setStep('name')
159→ setProjectName('')
160→ setProjectPath(null)
161→ setSpecMethod(null)
162→ setError(null)
163→ setInitializerStatus('idle')
164→ setInitializerError(null)
165→ setYoloModeSelected(false)
166→ onClose()
167→ }
168→
169→ const handleBack = () => {
170→ if (step === 'method') {
171→ setStep('folder')
172→ setSpecMethod(null)
173→ } else if (step === 'folder') {
174→ setStep('name')
175→ setProjectPath(null)
176→ }
177→ }
178→
179→ // Full-screen chat view
180→ if (step === 'chat') {
181→ return (
182→ <div className="fixed inset-0 z-50 bg-[var(--color-neo-bg)]">
183→ <SpecCreationChat
184→ projectName={projectName.trim()}
185→ onComplete={handleSpecComplete}
186→ onCancel={handleChatCancel}
187→ onExitToProject={handleExitToProject}
188→ initializerStatus={initializerStatus}
189→ initializerError={initializerError}
190→ onRetryInitializer={handleRetryInitializer}
191→ />
192→ </div>
193→ )
194→ }
195→
196→ // Folder step uses larger modal
197→ if (step === 'folder') {
198→ return (
199→ <div className="neo-modal-backdrop" onClick={handleClose}>
200→ <div
201→ className="neo-modal w-full max-w-3xl max-h-[85vh] flex flex-col"
202→ onClick={(e) => e.stopPropagation()}
203→ >
204→ {/* Header */}
205→ <div className="flex items-center justify-between p-4 border-b-3 border-[var(--color-neo-border)]">
206→ <div className="flex items-center gap-3">
207→ <Folder size={24} className="text-[var(--color-neo-progress)]" />
208→ <div>
209→ <h2 className="font-display font-bold text-xl text-[#1a1a1a]">
210→ Select Project Location
211→ </h2>
212→ <p className="text-sm text-[#4a4a4a]">
213→ A folder named <span className="font-bold font-mono">{projectName}</span> will be created inside the selected directory
214→ </p>
215→ </div>
216→ </div>
217→ <button
218→ onClick={handleClose}
219→ className="neo-btn neo-btn-ghost p-2"
220→ >
221→ <X size={20} />
222→ </button>
223→ </div>
224→
225→ {/* Folder Browser */}
226→ <div className="flex-1 overflow-hidden">
227→ <FolderBrowser
228→ onSelect={handleFolderSelect}
229→ onCancel={handleFolderCancel}
230→ />
231→ </div>
232→ </div>
233→ </div>
234→ )
235→ }
236→
237→ return (
238→ <div className="neo-modal-backdrop" onClick={handleClose}>
239→ <div
240→ className="neo-modal w-full max-w-lg"
241→ onClick={(e) => e.stopPropagation()}
242→ >
243→ {/* Header */}
244→ <div className="flex items-center justify-between p-4 border-b-3 border-[var(--color-neo-border)]">
245→ <h2 className="font-display font-bold text-xl text-[#1a1a1a]">
246→ {step === 'name' && 'Create New Project'}
247→ {step === 'method' && 'Choose Setup Method'}
248→ {step === 'complete' && 'Project Created!'}
249→ </h2>
250→ <button
251→ onClick={handleClose}
252→ className="neo-btn neo-btn-ghost p-2"
253→ >
254→ <X size={20} />
255→ </button>
256→ </div>
257→
258→ {/* Content */}
259→ <div className="p-6">
260→ {/* Step 1: Project Name */}
261→ {step === 'name' && (
262→ <form onSubmit={handleNameSubmit}>
263→ <div className="mb-6">
264→ <label className="block font-bold mb-2 text-[#1a1a1a]">
265→ Project Name
266→ </label>
267→ <input
268→ type="text"
269→ value={projectName}
270→ onChange={(e) => setProjectName(e.target.value)}
271→ placeholder="my-awesome-app"
272→ className="neo-input"
273→ pattern="^[a-zA-Z0-9_-]+$"
274→ autoFocus
275→ />
276→ <p className="text-sm text-[var(--color-neo-text-secondary)] mt-2">
277→ Use letters, numbers, hyphens, and underscores only.
278→ </p>
279→ </div>
280→
281→ {error && (
282→ <div className="mb-4 p-3 bg-[var(--color-neo-danger)] text-white text-sm border-2 border-[var(--color-neo-border)]">
283→ {error}
284→ </div>
285→ )}
286→
287→ <div className="flex justify-end">
288→ <button
289→ type="submit"
290→ className="neo-btn neo-btn-primary"
291→ disabled={!projectName.trim()}
292→ >
293→ Next
294→ <ArrowRight size={16} />
295→ </button>
296→ </div>
297→ </form>
298→ )}
299→
300→ {/* Step 2: Spec Method */}
301→ {step === 'method' && (
302→ <div>
303→ <p className="text-[var(--color-neo-text-secondary)] mb-6">
304→ How would you like to define your project?
305→ </p>
306→
307→ <div className="space-y-4">
308→ {/* Claude option */}
309→ <button
310→ onClick={() => handleMethodSelect('claude')}
311→ disabled={createProject.isPending}
312→ className={`
313→ w-full text-left p-4
314→ border-3 border-[var(--color-neo-border)]
315→ bg-white
316→ shadow-[4px_4px_0px_rgba(0,0,0,1)]
317→ hover:translate-x-[-2px] hover:translate-y-[-2px]
318→ hover:shadow-[6px_6px_0px_rgba(0,0,0,1)]
319→ transition-all duration-150
320→ disabled:opacity-50 disabled:cursor-not-allowed
321→ `}
322→ >
323→ <div className="flex items-start gap-4">
324→ <div className="p-2 bg-[var(--color-neo-progress)] border-2 border-[var(--color-neo-border)] shadow-[2px_2px_0px_rgba(0,0,0,1)]">
325→ <Bot size={24} className="text-white" />
326→ </div>
327→ <div className="flex-1">
328→ <div className="flex items-center gap-2">
329→ <span className="font-bold text-lg text-[#1a1a1a]">Create with Claude</span>
330→ <span className="neo-badge bg-[var(--color-neo-done)] text-xs">
331→ Recommended
332→ </span>
333→ </div>
334→ <p className="text-sm text-[var(--color-neo-text-secondary)] mt-1">
335→ Interactive conversation to define features and generate your app specification automatically.
336→ </p>
337→ </div>
338→ </div>
339→ </button>
340→
341→ {/* Manual option */}
342→ <button
343→ onClick={() => handleMethodSelect('manual')}
344→ disabled={createProject.isPending}
345→ className={`
346→ w-full text-left p-4
347→ border-3 border-[var(--color-neo-border)]
348→ bg-white
349→ shadow-[4px_4px_0px_rgba(0,0,0,1)]
350→ hover:translate-x-[-2px] hover:translate-y-[-2px]
351→ hover:shadow-[6px_6px_0px_rgba(0,0,0,1)]
352→ transition-all duration-150
353→ disabled:opacity-50 disabled:cursor-not-allowed
354→ `}
355→ >
356→ <div className="flex items-start gap-4">
357→ <div className="p-2 bg-[var(--color-neo-pending)] border-2 border-[var(--color-neo-border)] shadow-[2px_2px_0px_rgba(0,0,0,1)]">
358→ <FileEdit size={24} />
359→ </div>
360→ <div className="flex-1">
361→ <span className="font-bold text-lg text-[#1a1a1a]">Edit Templates Manually</span>
362→ <p className="text-sm text-[var(--color-neo-text-secondary)] mt-1">
363→ Edit the template files directly. Best for developers who want full control.
364→ </p>
365→ </div>
366→ </div>
367→ </button>
368→ </div>
369→
370→ {error && (
371→ <div className="mt-4 p-3 bg-[var(--color-neo-danger)] text-white text-sm border-2 border-[var(--color-neo-border)]">
372→ {error}
373→ </div>
374→ )}
375→
376→ {createProject.isPending && (
377→ <div className="mt-4 flex items-center justify-center gap-2 text-[var(--color-neo-text-secondary)]">
378→ <Loader2 size={16} className="animate-spin" />
379→ <span>Creating project...</span>
380→ </div>
381→ )}
382→
383→ <div className="flex justify-start mt-6">
384→ <button
385→ onClick={handleBack}
386→ className="neo-btn neo-btn-ghost"
387→ disabled={createProject.isPending}
388→ >
389→ <ArrowLeft size={16} />
390→ Back
391→ </button>
392→ </div>
393→ </div>
394→ )}
395→
396→ {/* Step 3: Complete */}
397→ {step === 'complete' && (
398→ <div className="text-center py-8">
399→ <div className="inline-flex items-center justify-center w-16 h-16 bg-[var(--color-neo-done)] border-3 border-[var(--color-neo-border)] shadow-[4px_4px_0px_rgba(0,0,0,1)] mb-4">
400→ <CheckCircle2 size={32} />
401→ </div>
402→ <h3 className="font-display font-bold text-xl mb-2">
403→ {projectName}
404→ </h3>
405→ <p className="text-[var(--color-neo-text-secondary)]">
406→ Your project has been created successfully!
407→ </p>
408→ <div className="mt-4 flex items-center justify-center gap-2">
409→ <Loader2 size={16} className="animate-spin" />
410→ <span className="text-sm">Redirecting...</span>
411→ </div>
412→ </div>
413→ )}
414→ </div>
415→ </div>
416→ </div>
417→ )
418→}
419→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

View File

@@ -0,0 +1,467 @@
1→/**
2→ * Spec Creation Chat Component
3→ *
4→ * Full chat interface for interactive spec creation with Claude.
5→ * Handles the 7-phase conversation flow for creating app specifications.
6→ */
7→
8→import { useCallback, useEffect, useRef, useState } from 'react'
9→import { Send, X, CheckCircle2, AlertCircle, Wifi, WifiOff, RotateCcw, Loader2, ArrowRight, Zap, Paperclip, ExternalLink } from 'lucide-react'
10→import { useSpecChat } from '../hooks/useSpecChat'
11→import { ChatMessage } from './ChatMessage'
12→import { QuestionOptions } from './QuestionOptions'
13→import { TypingIndicator } from './TypingIndicator'
14→import type { ImageAttachment } from '../lib/types'
15→
16→// Image upload validation constants
17→const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5 MB
18→const ALLOWED_TYPES = ['image/jpeg', 'image/png']
19→
20→type InitializerStatus = 'idle' | 'starting' | 'error'
21→
22→interface SpecCreationChatProps {
23→ projectName: string
24→ onComplete: (specPath: string, yoloMode?: boolean) => void
25→ onCancel: () => void
26→ onExitToProject: () => void // Exit to project without starting agent
27→ initializerStatus?: InitializerStatus
28→ initializerError?: string | null
29→ onRetryInitializer?: () => void
30→}
31→
32→export function SpecCreationChat({
33→ projectName,
34→ onComplete,
35→ onCancel,
36→ onExitToProject,
37→ initializerStatus = 'idle',
38→ initializerError = null,
39→ onRetryInitializer,
40→}: SpecCreationChatProps) {
41→ const [input, setInput] = useState('')
42→ const [error, setError] = useState<string | null>(null)
43→ const [yoloEnabled, setYoloEnabled] = useState(false)
44→ const [pendingAttachments, setPendingAttachments] = useState<ImageAttachment[]>([])
45→ const messagesEndRef = useRef<HTMLDivElement>(null)
46→ const inputRef = useRef<HTMLInputElement>(null)
47→ const fileInputRef = useRef<HTMLInputElement>(null)
48→
49→ const {
50→ messages,
51→ isLoading,
52→ isComplete,
53→ connectionStatus,
54→ currentQuestions,
55→ start,
56→ sendMessage,
57→ sendAnswer,
58→ disconnect,
59→ } = useSpecChat({
60→ projectName,
61→ onComplete,
62→ onError: (err) => setError(err),
63→ })
64→
65→ // Start the chat session when component mounts
66→ useEffect(() => {
67→ start()
68→
69→ return () => {
70→ disconnect()
71→ }
72→ }, []) // eslint-disable-line react-hooks/exhaustive-deps
73→
74→ // Scroll to bottom when messages change
75→ useEffect(() => {
76→ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
77→ }, [messages, currentQuestions, isLoading])
78→
79→ // Focus input when not loading and no questions
80→ useEffect(() => {
81→ if (!isLoading && !currentQuestions && inputRef.current) {
82→ inputRef.current.focus()
83→ }
84→ }, [isLoading, currentQuestions])
85→
86→ const handleSendMessage = () => {
87→ const trimmed = input.trim()
88→ // Allow sending if there's text OR attachments
89→ if ((!trimmed && pendingAttachments.length === 0) || isLoading) return
90→
91→ // Detect /exit command - exit to project without sending to Claude
92→ if (/^\s*\/exit\s*$/i.test(trimmed)) {
93→ setInput('')
94→ onExitToProject()
95→ return
96→ }
97→
98→ sendMessage(trimmed, pendingAttachments.length > 0 ? pendingAttachments : undefined)
99→ setInput('')
100→ setPendingAttachments([]) // Clear attachments after sending
101→ }
102→
103→ const handleKeyDown = (e: React.KeyboardEvent) => {
104→ if (e.key === 'Enter' && !e.shiftKey) {
105→ e.preventDefault()
106→ handleSendMessage()
107→ }
108→ }
109→
110→ const handleAnswerSubmit = (answers: Record<string, string | string[]>) => {
111→ sendAnswer(answers)
112→ }
113→
114→ // File handling for image attachments
115→ const handleFileSelect = useCallback((files: FileList | null) => {
116→ if (!files) return
117→
118→ Array.from(files).forEach((file) => {
119→ // Validate file type
120→ if (!ALLOWED_TYPES.includes(file.type)) {
121→ setError(`Invalid file type: ${file.name}. Only JPEG and PNG are supported.`)
122→ return
123→ }
124→
125→ // Validate file size
126→ if (file.size > MAX_FILE_SIZE) {
127→ setError(`File too large: ${file.name}. Maximum size is 5 MB.`)
128→ return
129→ }
130→
131→ // Read and convert to base64
132→ const reader = new FileReader()
133→ reader.onload = (e) => {
134→ const dataUrl = e.target?.result as string
135→ // dataUrl is "data:image/png;base64,XXXXXX"
136→ const base64Data = dataUrl.split(',')[1]
137→
138→ const attachment: ImageAttachment = {
139→ id: `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
140→ filename: file.name,
141→ mimeType: file.type as 'image/jpeg' | 'image/png',
142→ base64Data,
143→ previewUrl: dataUrl,
144→ size: file.size,
145→ }
146→
147→ setPendingAttachments((prev) => [...prev, attachment])
148→ }
149→ reader.readAsDataURL(file)
150→ })
151→ }, [])
152→
153→ const handleRemoveAttachment = useCallback((id: string) => {
154→ setPendingAttachments((prev) => prev.filter((a) => a.id !== id))
155→ }, [])
156→
157→ const handleDrop = useCallback(
158→ (e: React.DragEvent) => {
159→ e.preventDefault()
160→ handleFileSelect(e.dataTransfer.files)
161→ },
162→ [handleFileSelect]
163→ )
164→
165→ const handleDragOver = useCallback((e: React.DragEvent) => {
166→ e.preventDefault()
167→ }, [])
168→
169→ // Connection status indicator
170→ const ConnectionIndicator = () => {
171→ switch (connectionStatus) {
172→ case 'connected':
173→ return (
174→ <span className="flex items-center gap-1 text-xs text-[var(--color-neo-done)]">
175→ <Wifi size={12} />
176→ Connected
177→ </span>
178→ )
179→ case 'connecting':
180→ return (
181→ <span className="flex items-center gap-1 text-xs text-[var(--color-neo-pending)]">
182→ <Wifi size={12} className="animate-pulse" />
183→ Connecting...
184→ </span>
185→ )
186→ case 'error':
187→ return (
188→ <span className="flex items-center gap-1 text-xs text-[var(--color-neo-danger)]">
189→ <WifiOff size={12} />
190→ Error
191→ </span>
192→ )
193→ default:
194→ return (
195→ <span className="flex items-center gap-1 text-xs text-[var(--color-neo-text-secondary)]">
196→ <WifiOff size={12} />
197→ Disconnected
198→ </span>
199→ )
200→ }
201→ }
202→
203→ return (
204→ <div className="flex flex-col h-full bg-[var(--color-neo-bg)]">
205→ {/* Header */}
206→ <div className="flex items-center justify-between p-4 border-b-3 border-[var(--color-neo-border)] bg-white">
207→ <div className="flex items-center gap-3">
208→ <h2 className="font-display font-bold text-lg text-[#1a1a1a]">
209→ Create Spec: {projectName}
210→ </h2>
211→ <ConnectionIndicator />
212→ </div>
213→
214→ <div className="flex items-center gap-2">
215→ {isComplete && (
216→ <span className="flex items-center gap-1 text-sm text-[var(--color-neo-done)] font-bold">
217→ <CheckCircle2 size={16} />
218→ Complete
219→ </span>
220→ )}
221→
222→ {/* Exit to Project - always visible escape hatch */}
223→ <button
224→ onClick={onExitToProject}
225→ className="neo-btn neo-btn-ghost text-sm py-2"
226→ title="Exit chat and go to project (you can start the agent manually)"
227→ >
228→ <ExternalLink size={16} />
229→ Exit to Project
230→ </button>
231→
232→ <button
233→ onClick={onCancel}
234→ className="neo-btn neo-btn-ghost p-2"
235→ title="Cancel"
236→ >
237→ <X size={20} />
238→ </button>
239→ </div>
240→ </div>
241→
242→ {/* Error banner */}
243→ {error && (
244→ <div className="flex items-center gap-2 p-3 bg-[var(--color-neo-danger)] text-white border-b-3 border-[var(--color-neo-border)]">
245→ <AlertCircle size={16} />
246→ <span className="flex-1 text-sm">{error}</span>
247→ <button
248→ onClick={() => setError(null)}
249→ className="p-1 hover:bg-white/20 rounded"
250→ >
251→ <X size={14} />
252→ </button>
253→ </div>
254→ )}
255→
256→ {/* Messages area */}
257→ <div className="flex-1 overflow-y-auto py-4">
258→ {messages.length === 0 && !isLoading && (
259→ <div className="flex flex-col items-center justify-center h-full text-center p-8">
260→ <div className="neo-card p-6 max-w-md">
261→ <h3 className="font-display font-bold text-lg mb-2">
262→ Starting Spec Creation
263→ </h3>
264→ <p className="text-sm text-[var(--color-neo-text-secondary)]">
265→ Connecting to Claude to help you create your app specification...
266→ </p>
267→ {connectionStatus === 'error' && (
268→ <button
269→ onClick={start}
270→ className="neo-btn neo-btn-primary mt-4 text-sm"
271→ >
272→ <RotateCcw size={14} />
273→ Retry Connection
274→ </button>
275→ )}
276→ </div>
277→ </div>
278→ )}
279→
280→ {messages.map((message) => (
281→ <ChatMessage key={message.id} message={message} />
282→ ))}
283→
284→ {/* Structured questions */}
285→ {currentQuestions && currentQuestions.length > 0 && (
286→ <QuestionOptions
287→ questions={currentQuestions}
288→ onSubmit={handleAnswerSubmit}
289→ disabled={isLoading}
290→ />
291→ )}
292→
293→ {/* Typing indicator - don't show when we have questions (waiting for user) */}
294→ {isLoading && !currentQuestions && <TypingIndicator />}
295→
296→ {/* Scroll anchor */}
297→ <div ref={messagesEndRef} />
298→ </div>
299→
300→ {/* Input area */}
301→ {!isComplete && (
302→ <div
303→ className="p-4 border-t-3 border-[var(--color-neo-border)] bg-white"
304→ onDrop={handleDrop}
305→ onDragOver={handleDragOver}
306→ >
307→ {/* Attachment previews */}
308→ {pendingAttachments.length > 0 && (
309→ <div className="flex flex-wrap gap-2 mb-3">
310→ {pendingAttachments.map((attachment) => (
311→ <div
312→ key={attachment.id}
313→ className="relative group border-2 border-[var(--color-neo-border)] p-1 bg-white shadow-[2px_2px_0px_rgba(0,0,0,1)]"
314→ >
315→ <img
316→ src={attachment.previewUrl}
317→ alt={attachment.filename}
318→ className="w-16 h-16 object-cover"
319→ />
320→ <button
321→ onClick={() => handleRemoveAttachment(attachment.id)}
322→ className="absolute -top-2 -right-2 bg-[var(--color-neo-danger)] text-white rounded-full p-0.5 border-2 border-[var(--color-neo-border)] hover:scale-110 transition-transform"
323→ title="Remove attachment"
324→ >
325→ <X size={12} />
326→ </button>
327→ <span className="text-xs truncate block max-w-16 mt-1 text-center">
328→ {attachment.filename.length > 10
329→ ? `${attachment.filename.substring(0, 7)}...`
330→ : attachment.filename}
331→ </span>
332→ </div>
333→ ))}
334→ </div>
335→ )}
336→
337→ <div className="flex gap-3">
338→ {/* Hidden file input */}
339→ <input
340→ ref={fileInputRef}
341→ type="file"
342→ accept="image/jpeg,image/png"
343→ multiple
344→ onChange={(e) => handleFileSelect(e.target.files)}
345→ className="hidden"
346→ />
347→
348→ {/* Attach button */}
349→ <button
350→ onClick={() => fileInputRef.current?.click()}
351→ disabled={connectionStatus !== 'connected'}
352→ className="neo-btn neo-btn-ghost p-3"
353→ title="Attach image (JPEG, PNG - max 5MB)"
354→ >
355→ <Paperclip size={18} />
356→ </button>
357→
358→ <input
359→ ref={inputRef}
360→ type="text"
361→ value={input}
362→ onChange={(e) => setInput(e.target.value)}
363→ onKeyDown={handleKeyDown}
364→ placeholder={
365→ currentQuestions
366→ ? 'Or type a custom response...'
367→ : pendingAttachments.length > 0
368→ ? 'Add a message with your image(s)...'
369→ : 'Type your response... (or /exit to go to project)'
370→ }
371→ className="neo-input flex-1"
372→ disabled={(isLoading && !currentQuestions) || connectionStatus !== 'connected'}
373→ />
374→ <button
375→ onClick={handleSendMessage}
376→ disabled={
377→ (!input.trim() && pendingAttachments.length === 0) ||
378→ (isLoading && !currentQuestions) ||
379→ connectionStatus !== 'connected'
380→ }
381→ className="neo-btn neo-btn-primary px-6"
382→ >
383→ <Send size={18} />
384→ </button>
385→ </div>
386→
387→ {/* Help text */}
388→ <p className="text-xs text-[var(--color-neo-text-secondary)] mt-2">
389→ Press Enter to send. Drag & drop or click <Paperclip size={12} className="inline" /> to attach images (JPEG/PNG, max 5MB).
390→ </p>
391→ </div>
392→ )}
393→
394→ {/* Completion footer */}
395→ {isComplete && (
396→ <div className={`p-4 border-t-3 border-[var(--color-neo-border)] ${
397→ initializerStatus === 'error' ? 'bg-[var(--color-neo-danger)]' : 'bg-[var(--color-neo-done)]'
398→ }`}>
399→ <div className="flex items-center justify-between">
400→ <div className="flex items-center gap-2">
401→ {initializerStatus === 'starting' ? (
402→ <>
403→ <Loader2 size={20} className="animate-spin" />
404→ <span className="font-bold">
405→ Starting agent{yoloEnabled ? ' (YOLO mode)' : ''}...
406→ </span>
407→ </>
408→ ) : initializerStatus === 'error' ? (
409→ <>
410→ <AlertCircle size={20} className="text-white" />
411→ <span className="font-bold text-white">
412→ {initializerError || 'Failed to start agent'}
413→ </span>
414→ </>
415→ ) : (
416→ <>
417→ <CheckCircle2 size={20} />
418→ <span className="font-bold">Specification created successfully!</span>
419→ </>
420→ )}
421→ </div>
422→ <div className="flex items-center gap-2">
423→ {initializerStatus === 'error' && onRetryInitializer && (
424→ <button
425→ onClick={onRetryInitializer}
426→ className="neo-btn bg-white"
427→ >
428→ <RotateCcw size={14} />
429→ Retry
430→ </button>
431→ )}
432→ {initializerStatus === 'idle' && (
433→ <>
434→ {/* YOLO Mode Toggle */}
435→ <button
436→ onClick={() => setYoloEnabled(!yoloEnabled)}
437→ className={`neo-btn text-sm py-2 px-3 ${
438→ yoloEnabled ? 'neo-btn-warning' : 'bg-white'
439→ }`}
440→ title="YOLO Mode: Skip testing for rapid prototyping"
441→ >
442→ <Zap size={16} className={yoloEnabled ? 'text-yellow-900' : ''} />
443→ <span className={yoloEnabled ? 'text-yellow-900 font-bold' : ''}>
444→ YOLO
445→ </span>
446→ </button>
447→ <button
448→ onClick={() => onComplete('', yoloEnabled)}
449→ className="neo-btn neo-btn-primary"
450→ >
451→ Continue to Project
452→ <ArrowRight size={16} />
453→ </button>
454→ </>
455→ )}
456→ </div>
457→ </div>
458→ </div>
459→ )}
460→ </div>
461→ )
462→}
463→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

View File

@@ -0,0 +1,2 @@
Deleted 23 test projects
Remaining projects: 0

View File

@@ -0,0 +1,2 @@
/c/Users/MikeSwanson/.autocoder
/c/Users/MikeSwanson/claude-projects/Autocode-remix/Autocode-fork/autocoder-master

View File

@@ -0,0 +1,199 @@
1→# CLAUDE.md
2→
3→This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4→
5→## Project Overview
6→
7→This is an autonomous coding agent system with a React-based UI. It uses the Claude Agent SDK to build complete applications over multiple sessions using a two-agent pattern:
8→
9→1. **Initializer Agent** - First session reads an app spec and creates features in a SQLite database
10→2. **Coding Agent** - Subsequent sessions implement features one by one, marking them as passing
11→
12→## Commands
13→
14→### Quick Start (Recommended)
15→
16→```bash
17→# Windows - launches CLI menu
18→start.bat
19→
20→# macOS/Linux
21→./start.sh
22→
23→# Launch Web UI (serves pre-built React app)
24→start_ui.bat # Windows
25→./start_ui.sh # macOS/Linux
26→```
27→
28→### Python Backend (Manual)
29→
30→```bash
31→# Create and activate virtual environment
32→python -m venv venv
33→venv\Scripts\activate # Windows
34→source venv/bin/activate # macOS/Linux
35→
36→# Install dependencies
37→pip install -r requirements.txt
38→
39→# Run the main CLI launcher
40→python start.py
41→
42→# Run agent directly for a project (use absolute path or registered name)
43→python autonomous_agent_demo.py --project-dir C:/Projects/my-app
44→python autonomous_agent_demo.py --project-dir my-app # if registered
45→
46→# YOLO mode: rapid prototyping without browser testing
47→python autonomous_agent_demo.py --project-dir my-app --yolo
48→```
49→
50→### YOLO Mode (Rapid Prototyping)
51→
52→YOLO mode skips all testing for faster feature iteration:
53→
54→```bash
55→# CLI
56→python autonomous_agent_demo.py --project-dir my-app --yolo
57→
58→# UI: Toggle the lightning bolt button before starting the agent
59→```
60→
61→**What's different in YOLO mode:**
62→- No regression testing (skips `feature_get_for_regression`)
63→- No Playwright MCP server (browser automation disabled)
64→- Features marked passing after lint/type-check succeeds
65→- Faster iteration for prototyping
66→
67→**What's the same:**
68→- Lint and type-check still run to verify code compiles
69→- Feature MCP server for tracking progress
70→- All other development tools available
71→
72→**When to use:** Early prototyping when you want to quickly scaffold features without verification overhead. Switch back to standard mode for production-quality development.
73→
74→### React UI (in ui/ directory)
75→
76→```bash
77→cd ui
78→npm install
79→npm run dev # Development server (hot reload)
80→npm run build # Production build (required for start_ui.bat)
81→npm run lint # Run ESLint
82→```
83→
84→**Note:** The `start_ui.bat` script serves the pre-built UI from `ui/dist/`. After making UI changes, run `npm run build` in the `ui/` directory.
85→
86→## Architecture
87→
88→### Core Python Modules
89→
90→- `start.py` - CLI launcher with project creation/selection menu
91→- `autonomous_agent_demo.py` - Entry point for running the agent
92→- `agent.py` - Agent session loop using Claude Agent SDK
93→- `client.py` - ClaudeSDKClient configuration with security hooks and MCP servers
94→- `security.py` - Bash command allowlist validation (ALLOWED_COMMANDS whitelist)
95→- `prompts.py` - Prompt template loading with project-specific fallback
96→- `progress.py` - Progress tracking, database queries, webhook notifications
97→- `registry.py` - Project registry for mapping names to paths (cross-platform)
98→
99→### Project Registry
100→
101→Projects can be stored in any directory. The registry maps project names to paths using SQLite:
102→- **All platforms**: `~/.autocoder/registry.db`
103→
104→The registry uses:
105→- SQLite database with SQLAlchemy ORM
106→- POSIX path format (forward slashes) for cross-platform compatibility
107→- SQLite's built-in transaction handling for concurrency safety
108→
109→### Server API (server/)
110→
111→The FastAPI server provides REST endpoints for the UI:
112→
113→- `server/routers/projects.py` - Project CRUD with registry integration
114→- `server/routers/features.py` - Feature management
115→- `server/routers/agent.py` - Agent control (start/stop/pause/resume)
116→- `server/routers/filesystem.py` - Filesystem browser API with security controls
117→- `server/routers/spec_creation.py` - WebSocket for interactive spec creation
118→
119→### Feature Management
120→
121→Features are stored in SQLite (`features.db`) via SQLAlchemy. The agent interacts with features through an MCP server:
122→
123→- `mcp_server/feature_mcp.py` - MCP server exposing feature management tools
124→- `api/database.py` - SQLAlchemy models (Feature table with priority, category, name, description, steps, passes)
125→
126→MCP tools available to the agent:
127→- `feature_get_stats` - Progress statistics
128→- `feature_get_next` - Get highest-priority pending feature
129→- `feature_get_for_regression` - Random passing features for regression testing
130→- `feature_mark_passing` - Mark feature complete
131→- `feature_skip` - Move feature to end of queue
132→- `feature_create_bulk` - Initialize all features (used by initializer)
133→
134→### React UI (ui/)
135→
136→- Tech stack: React 18, TypeScript, TanStack Query, Tailwind CSS v4, Radix UI
137→- `src/App.tsx` - Main app with project selection, kanban board, agent controls
138→- `src/hooks/useWebSocket.ts` - Real-time updates via WebSocket
139→- `src/hooks/useProjects.ts` - React Query hooks for API calls
140→- `src/lib/api.ts` - REST API client
141→- `src/lib/types.ts` - TypeScript type definitions
142→- `src/components/FolderBrowser.tsx` - Server-side filesystem browser for project folder selection
143→- `src/components/NewProjectModal.tsx` - Multi-step project creation wizard
144→
145→### Project Structure for Generated Apps
146→
147→Projects can be stored in any directory (registered in `~/.autocoder/registry.db`). Each project contains:
148→- `prompts/app_spec.txt` - Application specification (XML format)
149→- `prompts/initializer_prompt.md` - First session prompt
150→- `prompts/coding_prompt.md` - Continuation session prompt
151→- `features.db` - SQLite database with feature test cases
152→- `.agent.lock` - Lock file to prevent multiple agent instances
153→
154→### Security Model
155→
156→Defense-in-depth approach configured in `client.py`:
157→1. OS-level sandbox for bash commands
158→2. Filesystem restricted to project directory only
159→3. Bash commands validated against `ALLOWED_COMMANDS` in `security.py`
160→
161→## Claude Code Integration
162→
163→- `.claude/commands/create-spec.md` - `/create-spec` slash command for interactive spec creation
164→- `.claude/skills/frontend-design/SKILL.md` - Skill for distinctive UI design
165→- `.claude/templates/` - Prompt templates copied to new projects
166→
167→## Key Patterns
168→
169→### Prompt Loading Fallback Chain
170→
171→1. Project-specific: `{project_dir}/prompts/{name}.md`
172→2. Base template: `.claude/templates/{name}.template.md`
173→
174→### Agent Session Flow
175→
176→1. Check if `features.db` has features (determines initializer vs coding agent)
177→2. Create ClaudeSDKClient with security settings
178→3. Send prompt and stream response
179→4. Auto-continue with 3-second delay between sessions
180→
181→### Real-time UI Updates
182→
183→The UI receives updates via WebSocket (`/ws/projects/{project_name}`):
184→- `progress` - Test pass counts
185→- `agent_status` - Running/paused/stopped/crashed
186→- `log` - Agent output lines (streamed from subprocess stdout)
187→- `feature_update` - Feature status changes
188→
189→### Design System
190→
191→The UI uses a **neobrutalism** design with Tailwind CSS v4:
192→- CSS variables defined in `ui/src/styles/globals.css` via `@theme` directive
193→- Custom animations: `animate-slide-in`, `animate-pulse-neo`, `animate-shimmer`
194→- Color tokens: `--color-neo-pending` (yellow), `--color-neo-progress` (cyan), `--color-neo-done` (green)
195→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

View File

@@ -0,0 +1,101 @@
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\app\__init__.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\app\config.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\__init__.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\__main__.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\__pip-runner__.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\__init__.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\build_env.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\cache.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\configuration.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\exceptions.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\main.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\pyproject.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\self_outdated_check.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\wheel_builder.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\cli\__init__.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\cli\autocompletion.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\cli\base_command.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\cli\cmdoptions.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\cli\command_context.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\cli\index_command.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\cli\main.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\cli\main_parser.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\cli\parser.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\cli\progress_bars.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\cli\req_command.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\cli\spinners.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\cli\status_codes.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\commands\__init__.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\commands\cache.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\commands\check.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\commands\completion.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\commands\configuration.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\commands\debug.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\commands\download.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\commands\freeze.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\commands\hash.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\commands\help.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\commands\index.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\commands\inspect.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\commands\install.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\commands\list.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\commands\lock.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\commands\search.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\commands\show.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\commands\uninstall.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\commands\wheel.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\distributions\__init__.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\distributions\base.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\distributions\installed.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\distributions\sdist.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\distributions\wheel.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\index\__init__.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\index\collector.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\index\package_finder.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\index\sources.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\locations\__init__.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\locations\_distutils.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\locations\_sysconfig.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\locations\base.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\metadata\__init__.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\metadata\_json.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\metadata\base.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\metadata\pkg_resources.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\metadata\importlib\__init__.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\metadata\importlib\_compat.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\metadata\importlib\_dists.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\metadata\importlib\_envs.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\models\__init__.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\models\candidate.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\models\direct_url.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\models\format_control.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\models\index.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\models\installation_report.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\models\link.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\models\pylock.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\models\scheme.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\models\search_scope.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\models\selection_prefs.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\models\target_python.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\models\wheel.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\network\__init__.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\network\auth.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\network\cache.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\network\download.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\network\lazy_wheel.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\network\session.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\network\utils.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\network\xmlrpc.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\operations\__init__.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\operations\check.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\operations\freeze.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\operations\prepare.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\operations\build\__init__.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\operations\build\build_tracker.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\operations\build\metadata.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\operations\build\metadata_editable.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\operations\build\metadata_legacy.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\operations\build\wheel.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\operations\build\wheel_editable.py
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\venv\Lib\site-packages\pip\_internal\operations\build\wheel_legacy.py
(Results are truncated. Consider using a more specific path or pattern.)

View File

@@ -0,0 +1,32 @@
The file C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\frontend\src\App.tsx has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
330→ Spec Interview
331→ </button>
332→ </div>
333→ </div>
334→ {currentView === 'kanban' ? (
335→ <KanbanBoard projectName={projectName} />
336→ ) : currentView === 'spec' ? (
337→ <SpecEditor
338→ projectName={projectName}
339→ onBack={() => handleViewChange('kanban')}
340→ />
341→ ) : currentView === 'permissions' ? (
342→ <PermissionEditor
343→ projectName={projectName}
344→ onBack={() => handleViewChange('kanban')}
345→ />
346→ ) : currentView === 'infrastructure' ? (
347→ <InfrastructureDashboard
348→ projectName={projectName}
349→ onBack={() => handleViewChange('kanban')}
350→ />
351→ ) : currentView === 'interview' ? (
352→ <SpecInterview
353→ projectName={projectName}
354→ onBack={() => handleViewChange('kanban')}
355→ />
356→ ) : null}
357→ </div>
358→ )
359→}
360→

View File

@@ -0,0 +1 @@
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\server\routers\spec_creation.py

View File

@@ -0,0 +1,542 @@
1→/**
2→ * Spec Creation Chat Component
3→ *
4→ * Full chat interface for interactive spec creation with Claude.
5→ * Handles the 7-phase conversation flow for creating app specifications.
6→ */
7→
8→import { useCallback, useEffect, useRef, useState } from 'react'
9→import { Send, X, CheckCircle2, AlertCircle, Wifi, WifiOff, RotateCcw, Loader2, ArrowRight, Zap, Paperclip, ExternalLink } from 'lucide-react'
10→import { useSpecChat } from '../hooks/useSpecChat'
11→import { ChatMessage } from './ChatMessage'
12→import { QuestionOptions } from './QuestionOptions'
13→import { TypingIndicator } from './TypingIndicator'
14→import type { ImageAttachment } from '../lib/types'
15→import type { CodebaseAnalysis } from '../lib/api'
16→
17→// Image upload validation constants
18→const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5 MB
19→const ALLOWED_TYPES = ['image/jpeg', 'image/png']
20→
21→type InitializerStatus = 'idle' | 'starting' | 'error'
22→
23→interface SpecCreationChatProps {
24→ projectName: string
25→ onComplete: (specPath: string, yoloMode?: boolean) => void
26→ onCancel: () => void
27→ onExitToProject: () => void // Exit to project without starting agent
28→ initializerStatus?: InitializerStatus
29→ initializerError?: string | null
30→ onRetryInitializer?: () => void
31→ /** If true, use import-spec skill for existing codebases */
32→ isImport?: boolean
33→ /** Codebase analysis results from import */
34→ codebaseAnalysis?: CodebaseAnalysis | null
35→}
36→
37→// Format codebase analysis for Claude context
38→function formatAnalysisForClaude(analysis: CodebaseAnalysis): string {
39→ const lines: string[] = [
40→ '# EXISTING PROJECT ANALYSIS',
41→ '',
42→ `**Project Directory:** ${analysis.project_name || 'Unknown'}`,
43→ '',
44→ ]
45→
46→ if (Object.keys(analysis.detected_stack).length > 0) {
47→ lines.push('## Detected Technology Stack')
48→ for (const [category, tech] of Object.entries(analysis.detected_stack)) {
49→ lines.push(`- **${category}:** ${tech}`)
50→ }
51→ lines.push('')
52→ }
53→
54→ if (analysis.directories.length > 0) {
55→ lines.push('## Directory Structure')
56→ lines.push('Top-level directories:')
57→ for (const dir of analysis.directories.slice(0, 10)) {
58→ lines.push(`- \`${dir}/\``)
59→ }
60→ if (analysis.directories.length > 10) {
61→ lines.push(`- ... and ${analysis.directories.length - 10} more`)
62→ }
63→ lines.push('')
64→ }
65→
66→ if (Object.keys(analysis.file_types).length > 0) {
67→ lines.push('## File Statistics')
68→ lines.push(`Total code files: ${analysis.total_files}`)
69→ lines.push('')
70→ lines.push('Files by type:')
71→ for (const [ext, count] of Object.entries(analysis.file_types).slice(0, 8)) {
72→ lines.push(`- \`${ext}\`: ${count} files`)
73→ }
74→ lines.push('')
75→ }
76→
77→ if (analysis.entry_points.length > 0) {
78→ lines.push('## Entry Points')
79→ for (const ep of analysis.entry_points.slice(0, 5)) {
80→ lines.push(`- ${ep}`)
81→ }
82→ lines.push('')
83→ }
84→
85→ if (analysis.claude_md_content) {
86→ lines.push('## Project Documentation (from CLAUDE.md)')
87→ lines.push('')
88→ lines.push(analysis.claude_md_content)
89→ lines.push('')
90→ } else if (analysis.readme_content) {
91→ lines.push('## Project Documentation (from README.md)')
92→ lines.push('')
93→ lines.push(analysis.readme_content)
94→ lines.push('')
95→ }
96→
97→ lines.push('---')
98→ lines.push('')
99→
100→ return lines.join('\n')
101→}
102→
103→export function SpecCreationChat({
104→ projectName,
105→ onComplete,
106→ onCancel,
107→ onExitToProject,
108→ initializerStatus = 'idle',
109→ initializerError = null,
110→ onRetryInitializer,
111→ isImport = false,
112→ codebaseAnalysis = null,
113→}: SpecCreationChatProps) {
114→ const [input, setInput] = useState('')
115→ const [error, setError] = useState<string | null>(null)
116→ const [yoloEnabled, setYoloEnabled] = useState(false)
117→ const [pendingAttachments, setPendingAttachments] = useState<ImageAttachment[]>([])
118→ const messagesEndRef = useRef<HTMLDivElement>(null)
119→ const inputRef = useRef<HTMLInputElement>(null)
120→ const fileInputRef = useRef<HTMLInputElement>(null)
121→
122→ const {
123→ messages,
124→ isLoading,
125→ isComplete,
126→ connectionStatus,
127→ currentQuestions,
128→ start,
129→ sendMessage,
130→ sendAnswer,
131→ disconnect,
132→ } = useSpecChat({
133→ projectName,
134→ onComplete,
135→ onError: (err) => setError(err),
136→ isImport,
137→ codebaseAnalysis: codebaseAnalysis ? formatAnalysisForClaude(codebaseAnalysis) : null,
138→ })
139→
140→ // Start the chat session when component mounts
141→ useEffect(() => {
142→ start()
143→
144→ return () => {
145→ disconnect()
146→ }
147→ }, []) // eslint-disable-line react-hooks/exhaustive-deps
148→
149→ // Scroll to bottom when messages change
150→ useEffect(() => {
151→ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
152→ }, [messages, currentQuestions, isLoading])
153→
154→ // Focus input when not loading and no questions
155→ useEffect(() => {
156→ if (!isLoading && !currentQuestions && inputRef.current) {
157→ inputRef.current.focus()
158→ }
159→ }, [isLoading, currentQuestions])
160→
161→ const handleSendMessage = () => {
162→ const trimmed = input.trim()
163→ // Allow sending if there's text OR attachments
164→ if ((!trimmed && pendingAttachments.length === 0) || isLoading) return
165→
166→ // Detect /exit command - exit to project without sending to Claude
167→ if (/^\s*\/exit\s*$/i.test(trimmed)) {
168→ setInput('')
169→ onExitToProject()
170→ return
171→ }
172→
173→ sendMessage(trimmed, pendingAttachments.length > 0 ? pendingAttachments : undefined)
174→ setInput('')
175→ setPendingAttachments([]) // Clear attachments after sending
176→ }
177→
178→ const handleKeyDown = (e: React.KeyboardEvent) => {
179→ if (e.key === 'Enter' && !e.shiftKey) {
180→ e.preventDefault()
181→ handleSendMessage()
182→ }
183→ }
184→
185→ const handleAnswerSubmit = (answers: Record<string, string | string[]>) => {
186→ sendAnswer(answers)
187→ }
188→
189→ // File handling for image attachments
190→ const handleFileSelect = useCallback((files: FileList | null) => {
191→ if (!files) return
192→
193→ Array.from(files).forEach((file) => {
194→ // Validate file type
195→ if (!ALLOWED_TYPES.includes(file.type)) {
196→ setError(`Invalid file type: ${file.name}. Only JPEG and PNG are supported.`)
197→ return
198→ }
199→
200→ // Validate file size
201→ if (file.size > MAX_FILE_SIZE) {
202→ setError(`File too large: ${file.name}. Maximum size is 5 MB.`)
203→ return
204→ }
205→
206→ // Read and convert to base64
207→ const reader = new FileReader()
208→ reader.onload = (e) => {
209→ const dataUrl = e.target?.result as string
210→ // dataUrl is "data:image/png;base64,XXXXXX"
211→ const base64Data = dataUrl.split(',')[1]
212→
213→ const attachment: ImageAttachment = {
214→ id: `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
215→ filename: file.name,
216→ mimeType: file.type as 'image/jpeg' | 'image/png',
217→ base64Data,
218→ previewUrl: dataUrl,
219→ size: file.size,
220→ }
221→
222→ setPendingAttachments((prev) => [...prev, attachment])
223→ }
224→ reader.readAsDataURL(file)
225→ })
226→ }, [])
227→
228→ const handleRemoveAttachment = useCallback((id: string) => {
229→ setPendingAttachments((prev) => prev.filter((a) => a.id !== id))
230→ }, [])
231→
232→ const handleDrop = useCallback(
233→ (e: React.DragEvent) => {
234→ e.preventDefault()
235→ handleFileSelect(e.dataTransfer.files)
236→ },
237→ [handleFileSelect]
238→ )
239→
240→ const handleDragOver = useCallback((e: React.DragEvent) => {
241→ e.preventDefault()
242→ }, [])
243→
244→ // Connection status indicator
245→ const ConnectionIndicator = () => {
246→ switch (connectionStatus) {
247→ case 'connected':
248→ return (
249→ <span className="flex items-center gap-1 text-xs text-[var(--color-neo-done)]">
250→ <Wifi size={12} />
251→ Connected
252→ </span>
253→ )
254→ case 'connecting':
255→ return (
256→ <span className="flex items-center gap-1 text-xs text-[var(--color-neo-pending)]">
257→ <Wifi size={12} className="animate-pulse" />
258→ Connecting...
259→ </span>
260→ )
261→ case 'error':
262→ return (
263→ <span className="flex items-center gap-1 text-xs text-[var(--color-neo-danger)]">
264→ <WifiOff size={12} />
265→ Error
266→ </span>
267→ )
268→ default:
269→ return (
270→ <span className="flex items-center gap-1 text-xs text-[var(--color-neo-text-secondary)]">
271→ <WifiOff size={12} />
272→ Disconnected
273→ </span>
274→ )
275→ }
276→ }
277→
278→ return (
279→ <div className="flex flex-col h-full bg-[var(--color-neo-bg)]">
280→ {/* Header */}
281→ <div className="flex items-center justify-between p-4 border-b-3 border-[var(--color-neo-border)] bg-white">
282→ <div className="flex items-center gap-3">
283→ <h2 className="font-display font-bold text-lg text-[#1a1a1a]">
284→ {isImport ? 'Import Spec' : 'Create Spec'}: {projectName}
285→ </h2>
286→ <ConnectionIndicator />
287→ </div>
288→
289→ <div className="flex items-center gap-2">
290→ {isComplete && (
291→ <span className="flex items-center gap-1 text-sm text-[var(--color-neo-done)] font-bold">
292→ <CheckCircle2 size={16} />
293→ Complete
294→ </span>
295→ )}
296→
297→ {/* Exit to Project - always visible escape hatch */}
298→ <button
299→ onClick={onExitToProject}
300→ className="neo-btn neo-btn-ghost text-sm py-2"
301→ title="Exit chat and go to project (you can start the agent manually)"
302→ >
303→ <ExternalLink size={16} />
304→ Exit to Project
305→ </button>
306→
307→ <button
308→ onClick={onCancel}
309→ className="neo-btn neo-btn-ghost p-2"
310→ title="Cancel"
311→ >
312→ <X size={20} />
313→ </button>
314→ </div>
315→ </div>
316→
317→ {/* Error banner */}
318→ {error && (
319→ <div className="flex items-center gap-2 p-3 bg-[var(--color-neo-danger)] text-white border-b-3 border-[var(--color-neo-border)]">
320→ <AlertCircle size={16} />
321→ <span className="flex-1 text-sm">{error}</span>
322→ <button
323→ onClick={() => setError(null)}
324→ className="p-1 hover:bg-white/20 rounded"
325→ >
326→ <X size={14} />
327→ </button>
328→ </div>
329→ )}
330→
331→ {/* Messages area */}
332→ <div className="flex-1 overflow-y-auto py-4">
333→ {messages.length === 0 && !isLoading && (
334→ <div className="flex flex-col items-center justify-center h-full text-center p-8">
335→ <div className="neo-card p-6 max-w-md">
336→ <h3 className="font-display font-bold text-lg mb-2">
337→ Starting Spec Creation
338→ </h3>
339→ <p className="text-sm text-[var(--color-neo-text-secondary)]">
340→ Connecting to Claude to help you create your app specification...
341→ </p>
342→ {connectionStatus === 'error' && (
343→ <button
344→ onClick={start}
345→ className="neo-btn neo-btn-primary mt-4 text-sm"
346→ >
347→ <RotateCcw size={14} />
348→ Retry Connection
349→ </button>
350→ )}
351→ </div>
352→ </div>
353→ )}
354→
355→ {messages.map((message) => (
356→ <ChatMessage key={message.id} message={message} />
357→ ))}
358→
359→ {/* Structured questions */}
360→ {currentQuestions && currentQuestions.length > 0 && (
361→ <QuestionOptions
362→ questions={currentQuestions}
363→ onSubmit={handleAnswerSubmit}
364→ disabled={isLoading}
365→ />
366→ )}
367→
368→ {/* Typing indicator - don't show when we have questions (waiting for user) */}
369→ {isLoading && !currentQuestions && <TypingIndicator />}
370→
371→ {/* Scroll anchor */}
372→ <div ref={messagesEndRef} />
373→ </div>
374→
375→ {/* Input area */}
376→ {!isComplete && (
377→ <div
378→ className="p-4 border-t-3 border-[var(--color-neo-border)] bg-white"
379→ onDrop={handleDrop}
380→ onDragOver={handleDragOver}
381→ >
382→ {/* Attachment previews */}
383→ {pendingAttachments.length > 0 && (
384→ <div className="flex flex-wrap gap-2 mb-3">
385→ {pendingAttachments.map((attachment) => (
386→ <div
387→ key={attachment.id}
388→ className="relative group border-2 border-[var(--color-neo-border)] p-1 bg-white shadow-[2px_2px_0px_rgba(0,0,0,1)]"
389→ >
390→ <img
391→ src={attachment.previewUrl}
392→ alt={attachment.filename}
393→ className="w-16 h-16 object-cover"
394→ />
395→ <button
396→ onClick={() => handleRemoveAttachment(attachment.id)}
397→ className="absolute -top-2 -right-2 bg-[var(--color-neo-danger)] text-white rounded-full p-0.5 border-2 border-[var(--color-neo-border)] hover:scale-110 transition-transform"
398→ title="Remove attachment"
399→ >
400→ <X size={12} />
401→ </button>
402→ <span className="text-xs truncate block max-w-16 mt-1 text-center">
403→ {attachment.filename.length > 10
404→ ? `${attachment.filename.substring(0, 7)}...`
405→ : attachment.filename}
406→ </span>
407→ </div>
408→ ))}
409→ </div>
410→ )}
411→
412→ <div className="flex gap-3">
413→ {/* Hidden file input */}
414→ <input
415→ ref={fileInputRef}
416→ type="file"
417→ accept="image/jpeg,image/png"
418→ multiple
419→ onChange={(e) => handleFileSelect(e.target.files)}
420→ className="hidden"
421→ />
422→
423→ {/* Attach button */}
424→ <button
425→ onClick={() => fileInputRef.current?.click()}
426→ disabled={connectionStatus !== 'connected'}
427→ className="neo-btn neo-btn-ghost p-3"
428→ title="Attach image (JPEG, PNG - max 5MB)"
429→ >
430→ <Paperclip size={18} />
431→ </button>
432→
433→ <input
434→ ref={inputRef}
435→ type="text"
436→ value={input}
437→ onChange={(e) => setInput(e.target.value)}
438→ onKeyDown={handleKeyDown}
439→ placeholder={
440→ currentQuestions
441→ ? 'Or type a custom response...'
442→ : pendingAttachments.length > 0
443→ ? 'Add a message with your image(s)...'
444→ : 'Type your response... (or /exit to go to project)'
445→ }
446→ className="neo-input flex-1"
447→ disabled={(isLoading && !currentQuestions) || connectionStatus !== 'connected'}
448→ />
449→ <button
450→ onClick={handleSendMessage}
451→ disabled={
452→ (!input.trim() && pendingAttachments.length === 0) ||
453→ (isLoading && !currentQuestions) ||
454→ connectionStatus !== 'connected'
455→ }
456→ className="neo-btn neo-btn-primary px-6"
457→ >
458→ <Send size={18} />
459→ </button>
460→ </div>
461→
462→ {/* Help text */}
463→ <p className="text-xs text-[var(--color-neo-text-secondary)] mt-2">
464→ Press Enter to send. Drag & drop or click <Paperclip size={12} className="inline" /> to attach images (JPEG/PNG, max 5MB).
465→ </p>
466→ </div>
467→ )}
468→
469→ {/* Completion footer */}
470→ {isComplete && (
471→ <div className={`p-4 border-t-3 border-[var(--color-neo-border)] ${
472→ initializerStatus === 'error' ? 'bg-[var(--color-neo-danger)]' : 'bg-[var(--color-neo-done)]'
473→ }`}>
474→ <div className="flex items-center justify-between">
475→ <div className="flex items-center gap-2">
476→ {initializerStatus === 'starting' ? (
477→ <>
478→ <Loader2 size={20} className="animate-spin" />
479→ <span className="font-bold">
480→ Starting agent{yoloEnabled ? ' (YOLO mode)' : ''}...
481→ </span>
482→ </>
483→ ) : initializerStatus === 'error' ? (
484→ <>
485→ <AlertCircle size={20} className="text-white" />
486→ <span className="font-bold text-white">
487→ {initializerError || 'Failed to start agent'}
488→ </span>
489→ </>
490→ ) : (
491→ <>
492→ <CheckCircle2 size={20} />
493→ <span className="font-bold">Specification created successfully!</span>
494→ </>
495→ )}
496→ </div>
497→ <div className="flex items-center gap-2">
498→ {initializerStatus === 'error' && onRetryInitializer && (
499→ <button
500→ onClick={onRetryInitializer}
501→ className="neo-btn bg-white"
502→ >
503→ <RotateCcw size={14} />
504→ Retry
505→ </button>
506→ )}
507→ {initializerStatus === 'idle' && (
508→ <>
509→ {/* YOLO Mode Toggle */}
510→ <button
511→ onClick={() => setYoloEnabled(!yoloEnabled)}
512→ className={`neo-btn text-sm py-2 px-3 ${
513→ yoloEnabled ? 'neo-btn-warning' : 'bg-white'
514→ }`}
515→ title="YOLO Mode: Skip testing for rapid prototyping"
516→ >
517→ <Zap size={16} className={yoloEnabled ? 'text-yellow-900' : ''} />
518→ <span className={yoloEnabled ? 'text-yellow-900 font-bold' : ''}>
519→ YOLO
520→ </span>
521→ </button>
522→ <button
523→ onClick={() => onComplete('', yoloEnabled)}
524→ className="neo-btn neo-btn-primary"
525→ >
526→ Continue to Project
527→ <ArrowRight size={16} />
528→ </button>
529→ </>
530→ )}
531→ </div>
532→ </div>
533→ </div>
534→ )}
535→ </div>
536→ )
537→}
538→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

View File

@@ -0,0 +1,13 @@
total 57
drwxr-xr-x 1 AzureAD+MikeSwanson 4096 0 Jan 9 04:27 ./
drwxr-xr-x 1 AzureAD+MikeSwanson 4096 0 Jan 9 06:06 ../
-rw-r--r-- 1 AzureAD+MikeSwanson 4096 196 Jan 8 14:51 __init__.py
drwxr-xr-x 1 AzureAD+MikeSwanson 4096 0 Jan 9 09:37 __pycache__/
-rw-r--r-- 1 AzureAD+MikeSwanson 4096 852 Jan 8 14:51 config.py
-rw-r--r-- 1 AzureAD+MikeSwanson 4096 1072 Jan 8 15:28 database.py
-rw-r--r-- 1 AzureAD+MikeSwanson 4096 6457 Jan 9 04:27 main.py
-rw-r--r-- 1 AzureAD+MikeSwanson 4096 6431 Jan 9 03:58 main.py.bak
-rw-r--r-- 1 AzureAD+MikeSwanson 4096 6460 Jan 9 04:14 main_v2.py
drwxr-xr-x 1 AzureAD+MikeSwanson 4096 0 Jan 8 22:30 models/
-rw-r--r-- 1 AzureAD+MikeSwanson 4096 2815 Jan 8 20:38 path_security.py
drwxr-xr-x 1 AzureAD+MikeSwanson 4096 0 Jan 9 04:40 routers/

View File

@@ -0,0 +1,496 @@
1→/**
2→ * Hook for managing spec creation chat WebSocket connection
3→ */
4→
5→import { useState, useCallback, useRef, useEffect } from 'react'
6→import type { ChatMessage, ImageAttachment, SpecChatServerMessage, SpecQuestion } from '../lib/types'
7→import { getSpecStatus } from '../lib/api'
8→
9→type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'error'
10→
11→interface UseSpecChatOptions {
12→ projectName: string
13→ onComplete?: (specPath: string) => void
14→ onError?: (error: string) => void
15→ /** If true, use import-spec skill instead of create-spec */
16→ isImport?: boolean
17→ /** Formatted codebase analysis for import mode */
18→ codebaseAnalysis?: string | null
19→}
20→
21→interface UseSpecChatReturn {
22→ messages: ChatMessage[]
23→ isLoading: boolean
24→ isComplete: boolean
25→ connectionStatus: ConnectionStatus
26→ currentQuestions: SpecQuestion[] | null
27→ currentToolId: string | null
28→ start: () => void
29→ sendMessage: (content: string, attachments?: ImageAttachment[]) => void
30→ sendAnswer: (answers: Record<string, string | string[]>) => void
31→ disconnect: () => void
32→}
33→
34→function generateId(): string {
35→ return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
36→}
37→
38→export function useSpecChat({
39→ projectName,
40→ onComplete,
41→ onError,
42→ isImport = false,
43→ codebaseAnalysis = null,
44→}: UseSpecChatOptions): UseSpecChatReturn {
45→ const [messages, setMessages] = useState<ChatMessage[]>([])
46→ const [isLoading, setIsLoading] = useState(false)
47→ const [isComplete, setIsComplete] = useState(false)
48→ const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>('disconnected')
49→ const [currentQuestions, setCurrentQuestions] = useState<SpecQuestion[] | null>(null)
50→ const [currentToolId, setCurrentToolId] = useState<string | null>(null)
51→
52→ const wsRef = useRef<WebSocket | null>(null)
53→ const currentAssistantMessageRef = useRef<string | null>(null)
54→ const reconnectAttempts = useRef(0)
55→ const maxReconnectAttempts = 3
56→ const pingIntervalRef = useRef<number | null>(null)
57→ const reconnectTimeoutRef = useRef<number | null>(null)
58→ const isCompleteRef = useRef(false)
59→
60→ // Keep isCompleteRef in sync with isComplete state
61→ useEffect(() => {
62→ isCompleteRef.current = isComplete
63→ }, [isComplete])
64→
65→ // Clean up on unmount
66→ useEffect(() => {
67→ return () => {
68→ if (pingIntervalRef.current) {
69→ clearInterval(pingIntervalRef.current)
70→ }
71→ if (reconnectTimeoutRef.current) {
72→ clearTimeout(reconnectTimeoutRef.current)
73→ }
74→ if (wsRef.current) {
75→ wsRef.current.close()
76→ }
77→ }
78→ }, [])
79→
80→ // Poll status file as fallback completion detection
81→ // Claude writes .spec_status.json when done with all spec work
82→ useEffect(() => {
83→ // Don't poll if already complete
84→ if (isComplete) return
85→
86→ // Start polling after initial delay (let WebSocket try first)
87→ const startDelay = setTimeout(() => {
88→ const pollInterval = setInterval(async () => {
89→ // Stop if already complete
90→ if (isCompleteRef.current) {
91→ clearInterval(pollInterval)
92→ return
93→ }
94→
95→ try {
96→ const status = await getSpecStatus(projectName)
97→
98→ if (status.exists && status.status === 'complete') {
99→ // Status file indicates completion - set complete state
100→ setIsComplete(true)
101→ setIsLoading(false)
102→
103→ // Mark any streaming message as done
104→ setMessages((prev) => {
105→ const lastMessage = prev[prev.length - 1]
106→ if (lastMessage?.role === 'assistant' && lastMessage.isStreaming) {
107→ return [
108→ ...prev.slice(0, -1),
109→ { ...lastMessage, isStreaming: false },
110→ ]
111→ }
112→ return prev
113→ })
114→
115→ // Add system message about completion
116→ setMessages((prev) => [
117→ ...prev,
118→ {
119→ id: generateId(),
120→ role: 'system',
121→ content: `Spec creation complete! Files written: ${status.files_written.join(', ')}${status.feature_count ? ` (${status.feature_count} features)` : ''}`,
122→ timestamp: new Date(),
123→ },
124→ ])
125→
126→ clearInterval(pollInterval)
127→ }
128→ } catch {
129→ // Silently ignore polling errors - WebSocket is primary mechanism
130→ }
131→ }, 3000) // Poll every 3 seconds
132→
133→ // Cleanup interval on unmount
134→ return () => clearInterval(pollInterval)
135→ }, 3000) // Start polling after 3 second delay
136→
137→ return () => clearTimeout(startDelay)
138→ }, [projectName, isComplete])
139→
140→ const connect = useCallback(() => {
141→ if (wsRef.current?.readyState === WebSocket.OPEN) {
142→ return
143→ }
144→
145→ setConnectionStatus('connecting')
146→
147→ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
148→ const host = window.location.host
149→ const wsUrl = `${protocol}//${host}/api/spec/ws/${encodeURIComponent(projectName)}`
150→
151→ const ws = new WebSocket(wsUrl)
152→ wsRef.current = ws
153→
154→ ws.onopen = () => {
155→ setConnectionStatus('connected')
156→ reconnectAttempts.current = 0
157→
158→ // Start ping interval to keep connection alive
159→ pingIntervalRef.current = window.setInterval(() => {
160→ if (ws.readyState === WebSocket.OPEN) {
161→ ws.send(JSON.stringify({ type: 'ping' }))
162→ }
163→ }, 30000)
164→ }
165→
166→ ws.onclose = () => {
167→ setConnectionStatus('disconnected')
168→ if (pingIntervalRef.current) {
169→ clearInterval(pingIntervalRef.current)
170→ pingIntervalRef.current = null
171→ }
172→
173→ // Attempt reconnection if not intentionally closed
174→ if (reconnectAttempts.current < maxReconnectAttempts && !isCompleteRef.current) {
175→ reconnectAttempts.current++
176→ const delay = Math.min(1000 * Math.pow(2, reconnectAttempts.current), 10000)
177→ reconnectTimeoutRef.current = window.setTimeout(connect, delay)
178→ }
179→ }
180→
181→ ws.onerror = () => {
182→ setConnectionStatus('error')
183→ onError?.('WebSocket connection error')
184→ }
185→
186→ ws.onmessage = (event) => {
187→ try {
188→ const data = JSON.parse(event.data) as SpecChatServerMessage
189→
190→ switch (data.type) {
191→ case 'text': {
192→ // Append text to current assistant message or create new one
193→ setMessages((prev) => {
194→ const lastMessage = prev[prev.length - 1]
195→ if (lastMessage?.role === 'assistant' && lastMessage.isStreaming) {
196→ // Append to existing streaming message
197→ return [
198→ ...prev.slice(0, -1),
199→ {
200→ ...lastMessage,
201→ content: lastMessage.content + data.content,
202→ },
203→ ]
204→ } else {
205→ // Create new assistant message
206→ currentAssistantMessageRef.current = generateId()
207→ return [
208→ ...prev,
209→ {
210→ id: currentAssistantMessageRef.current,
211→ role: 'assistant',
212→ content: data.content,
213→ timestamp: new Date(),
214→ isStreaming: true,
215→ },
216→ ]
217→ }
218→ })
219→ break
220→ }
221→
222→ case 'question': {
223→ // Show structured question UI
224→ setCurrentQuestions(data.questions)
225→ setCurrentToolId(data.tool_id || null)
226→ setIsLoading(false)
227→
228→ // Mark current message as done streaming
229→ setMessages((prev) => {
230→ const lastMessage = prev[prev.length - 1]
231→ if (lastMessage?.role === 'assistant' && lastMessage.isStreaming) {
232→ return [
233→ ...prev.slice(0, -1),
234→ {
235→ ...lastMessage,
236→ isStreaming: false,
237→ questions: data.questions,
238→ },
239→ ]
240→ }
241→ return prev
242→ })
243→ break
244→ }
245→
246→ case 'spec_complete': {
247→ setIsComplete(true)
248→ setIsLoading(false)
249→
250→ // Mark current message as done
251→ setMessages((prev) => {
252→ const lastMessage = prev[prev.length - 1]
253→ if (lastMessage?.role === 'assistant' && lastMessage.isStreaming) {
254→ return [
255→ ...prev.slice(0, -1),
256→ { ...lastMessage, isStreaming: false },
257→ ]
258→ }
259→ return prev
260→ })
261→
262→ // Add system message about spec completion
263→ setMessages((prev) => [
264→ ...prev,
265→ {
266→ id: generateId(),
267→ role: 'system',
268→ content: `Specification file created: ${data.path}`,
269→ timestamp: new Date(),
270→ },
271→ ])
272→
273→ // NOTE: Do NOT auto-call onComplete here!
274→ // User should click "Continue to Project" button to start the agent.
275→ // This matches the CLI behavior where user closes the chat manually.
276→ break
277→ }
278→
279→ case 'file_written': {
280→ // Optional: notify about other files being written
281→ setMessages((prev) => [
282→ ...prev,
283→ {
284→ id: generateId(),
285→ role: 'system',
286→ content: `File created: ${data.path}`,
287→ timestamp: new Date(),
288→ },
289→ ])
290→ break
291→ }
292→
293→ case 'complete': {
294→ setIsComplete(true)
295→ setIsLoading(false)
296→
297→ // Mark current message as done
298→ setMessages((prev) => {
299→ const lastMessage = prev[prev.length - 1]
300→ if (lastMessage?.role === 'assistant' && lastMessage.isStreaming) {
301→ return [
302→ ...prev.slice(0, -1),
303→ { ...lastMessage, isStreaming: false },
304→ ]
305→ }
306→ return prev
307→ })
308→ break
309→ }
310→
311→ case 'error': {
312→ setIsLoading(false)
313→ onError?.(data.content)
314→
315→ // Add error as system message
316→ setMessages((prev) => [
317→ ...prev,
318→ {
319→ id: generateId(),
320→ role: 'system',
321→ content: `Error: ${data.content}`,
322→ timestamp: new Date(),
323→ },
324→ ])
325→ break
326→ }
327→
328→ case 'pong': {
329→ // Keep-alive response, nothing to do
330→ break
331→ }
332→
333→ case 'response_done': {
334→ // Response complete - hide loading indicator and mark message as done
335→ setIsLoading(false)
336→
337→ // Mark current message as done streaming
338→ setMessages((prev) => {
339→ const lastMessage = prev[prev.length - 1]
340→ if (lastMessage?.role === 'assistant' && lastMessage.isStreaming) {
341→ return [
342→ ...prev.slice(0, -1),
343→ { ...lastMessage, isStreaming: false },
344→ ]
345→ }
346→ return prev
347→ })
348→ break
349→ }
350→ }
351→ } catch (e) {
352→ console.error('Failed to parse WebSocket message:', e)
353→ }
354→ }
355→ }, [projectName, onComplete, onError])
356→
357→ const start = useCallback(() => {
358→ connect()
359→
360→ // Wait for connection then send start message
361→ const checkAndSend = () => {
362→ if (wsRef.current?.readyState === WebSocket.OPEN) {
363→ setIsLoading(true)
364→ // Include import mode and codebase analysis in start message
365→ const startMessage: {
366→ type: string
367→ is_import?: boolean
368→ codebase_analysis?: string
369→ } = { type: 'start' }
370→
371→ if (isImport) {
372→ startMessage.is_import = true
373→ if (codebaseAnalysis) {
374→ startMessage.codebase_analysis = codebaseAnalysis
375→ }
376→ }
377→
378→ wsRef.current.send(JSON.stringify(startMessage))
379→ } else if (wsRef.current?.readyState === WebSocket.CONNECTING) {
380→ setTimeout(checkAndSend, 100)
381→ }
382→ }
383→
384→ setTimeout(checkAndSend, 100)
385→ }, [connect, isImport, codebaseAnalysis])
386→
387→ const sendMessage = useCallback((content: string, attachments?: ImageAttachment[]) => {
388→ if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
389→ onError?.('Not connected')
390→ return
391→ }
392→
393→ // Add user message to chat (with attachments for display)
394→ setMessages((prev) => [
395→ ...prev,
396→ {
397→ id: generateId(),
398→ role: 'user',
399→ content,
400→ attachments,
401→ timestamp: new Date(),
402→ },
403→ ])
404→
405→ // Clear current questions
406→ setCurrentQuestions(null)
407→ setCurrentToolId(null)
408→ setIsLoading(true)
409→
410→ // Build message payload
411→ const payload: { type: string; content: string; attachments?: Array<{ filename: string; mimeType: string; base64Data: string }> } = {
412→ type: 'message',
413→ content,
414→ }
415→
416→ // Add attachments if present (send base64 data, not preview URL)
417→ if (attachments && attachments.length > 0) {
418→ payload.attachments = attachments.map((a) => ({
419→ filename: a.filename,
420→ mimeType: a.mimeType,
421→ base64Data: a.base64Data,
422→ }))
423→ }
424→
425→ // Send to server
426→ wsRef.current.send(JSON.stringify(payload))
427→ }, [onError])
428→
429→ const sendAnswer = useCallback((answers: Record<string, string | string[]>) => {
430→ if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
431→ onError?.('Not connected')
432→ return
433→ }
434→
435→ // Format answers for display
436→ const answerText = Object.values(answers)
437→ .map((v) => (Array.isArray(v) ? v.join(', ') : v))
438→ .join('; ')
439→
440→ // Add user message
441→ setMessages((prev) => [
442→ ...prev,
443→ {
444→ id: generateId(),
445→ role: 'user',
446→ content: answerText,
447→ timestamp: new Date(),
448→ },
449→ ])
450→
451→ // Clear current questions
452→ setCurrentQuestions(null)
453→ setCurrentToolId(null)
454→ setIsLoading(true)
455→
456→ // Send to server
457→ wsRef.current.send(
458→ JSON.stringify({
459→ type: 'answer',
460→ answers,
461→ tool_id: currentToolId,
462→ })
463→ )
464→ }, [currentToolId, onError])
465→
466→ const disconnect = useCallback(() => {
467→ reconnectAttempts.current = maxReconnectAttempts // Prevent reconnection
468→ if (pingIntervalRef.current) {
469→ clearInterval(pingIntervalRef.current)
470→ pingIntervalRef.current = null
471→ }
472→ if (wsRef.current) {
473→ wsRef.current.close()
474→ wsRef.current = null
475→ }
476→ setConnectionStatus('disconnected')
477→ }, [])
478→
479→ return {
480→ messages,
481→ isLoading,
482→ isComplete,
483→ connectionStatus,
484→ currentQuestions,
485→ currentToolId,
486→ start,
487→ sendMessage,
488→ sendAnswer,
489→ disconnect,
490→ }
491→}
492→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

View File

@@ -0,0 +1,345 @@
1→"""
2→Projects Router
3→===============
4→
5→API endpoints for project management.
6→Uses project registry for path lookups instead of fixed generations/ directory.
7→"""
8→
9→import re
10→import shutil
11→from pathlib import Path
12→
13→from fastapi import APIRouter, HTTPException
14→
15→from ..schemas import (
16→ ProjectCreate,
17→ ProjectDetail,
18→ ProjectPrompts,
19→ ProjectPromptsUpdate,
20→ ProjectStats,
21→ ProjectSummary,
22→)
23→
24→# Lazy imports to avoid circular dependencies
25→_imports_initialized = False
26→_check_spec_exists = None
27→_scaffold_project_prompts = None
28→_get_project_prompts_dir = None
29→_count_passing_tests = None
30→
31→
32→def _init_imports():
33→ """Lazy import of project-level modules."""
34→ global _imports_initialized, _check_spec_exists
35→ global _scaffold_project_prompts, _get_project_prompts_dir
36→ global _count_passing_tests
37→
38→ if _imports_initialized:
39→ return
40→
41→ import sys
42→ root = Path(__file__).parent.parent.parent
43→ if str(root) not in sys.path:
44→ sys.path.insert(0, str(root))
45→
46→ from progress import count_passing_tests
47→ from prompts import get_project_prompts_dir, scaffold_project_prompts
48→ from start import check_spec_exists
49→
50→ _check_spec_exists = check_spec_exists
51→ _scaffold_project_prompts = scaffold_project_prompts
52→ _get_project_prompts_dir = get_project_prompts_dir
53→ _count_passing_tests = count_passing_tests
54→ _imports_initialized = True
55→
56→
57→def _get_registry_functions():
58→ """Get registry functions with lazy import."""
59→ import sys
60→ root = Path(__file__).parent.parent.parent
61→ if str(root) not in sys.path:
62→ sys.path.insert(0, str(root))
63→
64→ from registry import (
65→ get_project_path,
66→ list_registered_projects,
67→ register_project,
68→ unregister_project,
69→ validate_project_path,
70→ )
71→ return register_project, unregister_project, get_project_path, list_registered_projects, validate_project_path
72→
73→
74→router = APIRouter(prefix="/api/projects", tags=["projects"])
75→
76→
77→def validate_project_name(name: str) -> str:
78→ """Validate and sanitize project name to prevent path traversal."""
79→ if not re.match(r'^[a-zA-Z0-9_-]{1,50}$', name):
80→ raise HTTPException(
81→ status_code=400,
82→ detail="Invalid project name. Use only letters, numbers, hyphens, and underscores (1-50 chars)."
83→ )
84→ return name
85→
86→
87→def get_project_stats(project_dir: Path) -> ProjectStats:
88→ """Get statistics for a project."""
89→ _init_imports()
90→ passing, in_progress, total = _count_passing_tests(project_dir)
91→ percentage = (passing / total * 100) if total > 0 else 0.0
92→ return ProjectStats(
93→ passing=passing,
94→ in_progress=in_progress,
95→ total=total,
96→ percentage=round(percentage, 1)
97→ )
98→
99→
100→@router.get("", response_model=list[ProjectSummary])
101→async def list_projects():
102→ """List all registered projects."""
103→ _init_imports()
104→ _, _, _, list_registered_projects, validate_project_path = _get_registry_functions()
105→
106→ projects = list_registered_projects()
107→ result = []
108→
109→ for name, info in projects.items():
110→ project_dir = Path(info["path"])
111→
112→ # Skip if path no longer exists
113→ is_valid, _ = validate_project_path(project_dir)
114→ if not is_valid:
115→ continue
116→
117→ has_spec = _check_spec_exists(project_dir)
118→ stats = get_project_stats(project_dir)
119→
120→ result.append(ProjectSummary(
121→ name=name,
122→ path=info["path"],
123→ has_spec=has_spec,
124→ stats=stats,
125→ ))
126→
127→ return result
128→
129→
130→@router.post("", response_model=ProjectSummary)
131→async def create_project(project: ProjectCreate):
132→ """Create a new project at the specified path."""
133→ _init_imports()
134→ register_project, _, get_project_path, _, _ = _get_registry_functions()
135→
136→ name = validate_project_name(project.name)
137→ project_path = Path(project.path).resolve()
138→
139→ # Check if project name already registered
140→ existing = get_project_path(name)
141→ if existing:
142→ raise HTTPException(
143→ status_code=409,
144→ detail=f"Project '{name}' already exists at {existing}"
145→ )
146→
147→ # Security: Check if path is in a blocked location
148→ from .filesystem import is_path_blocked
149→ if is_path_blocked(project_path):
150→ raise HTTPException(
151→ status_code=403,
152→ detail="Cannot create project in system or sensitive directory"
153→ )
154→
155→ # Validate the path is usable
156→ if project_path.exists():
157→ if not project_path.is_dir():
158→ raise HTTPException(
159→ status_code=400,
160→ detail="Path exists but is not a directory"
161→ )
162→ else:
163→ # Create the directory
164→ try:
165→ project_path.mkdir(parents=True, exist_ok=True)
166→ except OSError as e:
167→ raise HTTPException(
168→ status_code=500,
169→ detail=f"Failed to create directory: {e}"
170→ )
171→
172→ # Scaffold prompts
173→ _scaffold_project_prompts(project_path)
174→
175→ # Register in registry
176→ try:
177→ register_project(name, project_path)
178→ except Exception as e:
179→ raise HTTPException(
180→ status_code=500,
181→ detail=f"Failed to register project: {e}"
182→ )
183→
184→ return ProjectSummary(
185→ name=name,
186→ path=project_path.as_posix(),
187→ has_spec=False, # Just created, no spec yet
188→ stats=ProjectStats(passing=0, total=0, percentage=0.0),
189→ )
190→
191→
192→@router.get("/{name}", response_model=ProjectDetail)
193→async def get_project(name: str):
194→ """Get detailed information about a project."""
195→ _init_imports()
196→ _, _, get_project_path, _, _ = _get_registry_functions()
197→
198→ name = validate_project_name(name)
199→ project_dir = get_project_path(name)
200→
201→ if not project_dir:
202→ raise HTTPException(status_code=404, detail=f"Project '{name}' not found in registry")
203→
204→ if not project_dir.exists():
205→ raise HTTPException(status_code=404, detail=f"Project directory no longer exists: {project_dir}")
206→
207→ has_spec = _check_spec_exists(project_dir)
208→ stats = get_project_stats(project_dir)
209→ prompts_dir = _get_project_prompts_dir(project_dir)
210→
211→ return ProjectDetail(
212→ name=name,
213→ path=project_dir.as_posix(),
214→ has_spec=has_spec,
215→ stats=stats,
216→ prompts_dir=str(prompts_dir),
217→ )
218→
219→
220→@router.delete("/{name}")
221→async def delete_project(name: str, delete_files: bool = False):
222→ """
223→ Delete a project from the registry.
224→
225→ Args:
226→ name: Project name to delete
227→ delete_files: If True, also delete the project directory and files
228→ """
229→ _init_imports()
230→ _, unregister_project, get_project_path, _, _ = _get_registry_functions()
231→
232→ name = validate_project_name(name)
233→ project_dir = get_project_path(name)
234→
235→ if not project_dir:
236→ raise HTTPException(status_code=404, detail=f"Project '{name}' not found")
237→
238→ # Check if agent is running
239→ lock_file = project_dir / ".agent.lock"
240→ if lock_file.exists():
241→ raise HTTPException(
242→ status_code=409,
243→ detail="Cannot delete project while agent is running. Stop the agent first."
244→ )
245→
246→ # Optionally delete files
247→ if delete_files and project_dir.exists():
248→ try:
249→ shutil.rmtree(project_dir)
250→ except Exception as e:
251→ raise HTTPException(status_code=500, detail=f"Failed to delete project files: {e}")
252→
253→ # Unregister from registry
254→ unregister_project(name)
255→
256→ return {
257→ "success": True,
258→ "message": f"Project '{name}' deleted" + (" (files removed)" if delete_files else " (files preserved)")
259→ }
260→
261→
262→@router.get("/{name}/prompts", response_model=ProjectPrompts)
263→async def get_project_prompts(name: str):
264→ """Get the content of project prompt files."""
265→ _init_imports()
266→ _, _, get_project_path, _, _ = _get_registry_functions()
267→
268→ name = validate_project_name(name)
269→ project_dir = get_project_path(name)
270→
271→ if not project_dir:
272→ raise HTTPException(status_code=404, detail=f"Project '{name}' not found")
273→
274→ if not project_dir.exists():
275→ raise HTTPException(status_code=404, detail="Project directory not found")
276→
277→ prompts_dir = _get_project_prompts_dir(project_dir)
278→
279→ def read_file(filename: str) -> str:
280→ filepath = prompts_dir / filename
281→ if filepath.exists():
282→ try:
283→ return filepath.read_text(encoding="utf-8")
284→ except Exception:
285→ return ""
286→ return ""
287→
288→ return ProjectPrompts(
289→ app_spec=read_file("app_spec.txt"),
290→ initializer_prompt=read_file("initializer_prompt.md"),
291→ coding_prompt=read_file("coding_prompt.md"),
292→ )
293→
294→
295→@router.put("/{name}/prompts")
296→async def update_project_prompts(name: str, prompts: ProjectPromptsUpdate):
297→ """Update project prompt files."""
298→ _init_imports()
299→ _, _, get_project_path, _, _ = _get_registry_functions()
300→
301→ name = validate_project_name(name)
302→ project_dir = get_project_path(name)
303→
304→ if not project_dir:
305→ raise HTTPException(status_code=404, detail=f"Project '{name}' not found")
306→
307→ if not project_dir.exists():
308→ raise HTTPException(status_code=404, detail="Project directory not found")
309→
310→ prompts_dir = _get_project_prompts_dir(project_dir)
311→ prompts_dir.mkdir(parents=True, exist_ok=True)
312→
313→ def write_file(filename: str, content: str | None):
314→ if content is not None:
315→ filepath = prompts_dir / filename
316→ filepath.write_text(content, encoding="utf-8")
317→
318→ write_file("app_spec.txt", prompts.app_spec)
319→ write_file("initializer_prompt.md", prompts.initializer_prompt)
320→ write_file("coding_prompt.md", prompts.coding_prompt)
321→
322→ return {"success": True, "message": "Prompts updated"}
323→
324→
325→@router.get("/{name}/stats", response_model=ProjectStats)
326→async def get_project_stats_endpoint(name: str):
327→ """Get current progress statistics for a project."""
328→ _init_imports()
329→ _, _, get_project_path, _, _ = _get_registry_functions()
330→
331→ name = validate_project_name(name)
332→ project_dir = get_project_path(name)
333→
334→ if not project_dir:
335→ raise HTTPException(status_code=404, detail=f"Project '{name}' not found")
336→
337→ if not project_dir.exists():
338→ raise HTTPException(status_code=404, detail="Project directory not found")
339→
340→ return get_project_stats(project_dir)
341→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

View File

@@ -0,0 +1 @@
C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\components\SpecCreationChat.tsx

View File

@@ -0,0 +1 @@
File created successfully at: C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\frontend\src\components\SpecInterview.tsx

View File

@@ -0,0 +1,26 @@
Tables: ['projects', 'features', 'permissions', 'infrastructure', 'spec_history', 'permission_requests']
Projects (23 total):
ID 1: test-project -> /tmp/test-project
ID 2: test-project-001 -> /tmp/test
ID 3: test-project-002 -> /tmp/test2
ID 4: detail-test -> /tmp/detail-test
ID 5: update-test -> /tmp/updated-path
ID 6: original-project -> /tmp/original
ID 7: duplicated-project -> /tmp/original
ID 8: test-update-feature -> /tmp/test-update
ID 9: stats-test-project -> /tmp/stats-test
ID 10: analyze-test -> /tmp/analyze-test
ID 11: infra-test -> /tmp/infra-test
ID 12: conn-test -> /tmp/conn-test
ID 13: discover-test -> /tmp/discover-test
ID 14: discover-test2 -> /tmp/discover-test2
ID 15: wizard-test-001 -> /tmp/wizard-test-001
ID 16: test-project-duplicated -> /tmp/updated-settings-path
ID 17: kanban-test -> /tmp/kanban-test
ID 18: progress-ring-test -> /tmp/progress-ring-test
ID 19: <script>alert(1)</script> -> /tmp/xss-test
ID 20: duplicate-api-test -> /tmp/detail-test
ID 21: dup-source-test -> /tmp/dup-source
ID 22: dup-target-test-v2 -> /tmp/dup-source
ID 23: file-conflict-test -> /tmp/file-conflict-test

View File

@@ -0,0 +1,331 @@
1→# AutoCoder
2→
3→[![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-FFDD00?style=flat&logo=buy-me-a-coffee&logoColor=black)](https://www.buymeacoffee.com/leonvanzyl)
4→
5→A long-running autonomous coding agent powered by the Claude Agent SDK. This tool can build complete applications over multiple sessions using a two-agent pattern (initializer + coding agent). Includes a React-based UI for monitoring progress in real-time.
6→
7→## Video Tutorial
8→
9→[![Watch the tutorial](https://img.youtube.com/vi/lGWFlpffWk4/hqdefault.jpg)](https://youtu.be/lGWFlpffWk4)
10→
11→> **[Watch the setup and usage guide →](https://youtu.be/lGWFlpffWk4)**
12→
13→---
14→
15→## Prerequisites
16→
17→### Claude Code CLI (Required)
18→
19→This project requires the Claude Code CLI to be installed. Install it using one of these methods:
20→
21→**macOS / Linux:**
22→```bash
23→curl -fsSL https://claude.ai/install.sh | bash
24→```
25→
26→**Windows (PowerShell):**
27→```powershell
28→irm https://claude.ai/install.ps1 | iex
29→```
30→
31→### Authentication
32→
33→You need one of the following:
34→
35→- **Claude Pro/Max Subscription** - Use `claude login` to authenticate (recommended)
36→- **Anthropic API Key** - Pay-per-use from https://console.anthropic.com/
37→
38→---
39→
40→## Quick Start
41→
42→### Option 1: Web UI (Recommended)
43→
44→**Windows:**
45→```cmd
46→start_ui.bat
47→```
48→
49→**macOS / Linux:**
50→```bash
51→./start_ui.sh
52→```
53→
54→This launches the React-based web UI at `http://localhost:5173` with:
55→- Project selection and creation
56→- Kanban board view of features
57→- Real-time agent output streaming
58→- Start/pause/stop controls
59→
60→### Option 2: CLI Mode
61→
62→**Windows:**
63→```cmd
64→start.bat
65→```
66→
67→**macOS / Linux:**
68→```bash
69→./start.sh
70→```
71→
72→The start script will:
73→1. Check if Claude CLI is installed
74→2. Check if you're authenticated (prompt to run `claude login` if not)
75→3. Create a Python virtual environment
76→4. Install dependencies
77→5. Launch the main menu
78→
79→### Creating or Continuing a Project
80→
81→You'll see options to:
82→- **Create new project** - Start a fresh project with AI-assisted spec generation
83→- **Continue existing project** - Resume work on a previous project
84→
85→For new projects, you can use the built-in `/create-spec` command to interactively create your app specification with Claude's help.
86→
87→---
88→
89→## How It Works
90→
91→### Two-Agent Pattern
92→
93→1. **Initializer Agent (First Session):** Reads your app specification, creates features in a SQLite database (`features.db`), sets up the project structure, and initializes git.
94→
95→2. **Coding Agent (Subsequent Sessions):** Picks up where the previous session left off, implements features one by one, and marks them as passing in the database.
96→
97→### Feature Management
98→
99→Features are stored in SQLite via SQLAlchemy and managed through an MCP server that exposes tools to the agent:
100→- `feature_get_stats` - Progress statistics
101→- `feature_get_next` - Get highest-priority pending feature
102→- `feature_get_for_regression` - Random passing features for regression testing
103→- `feature_mark_passing` - Mark feature complete
104→- `feature_skip` - Move feature to end of queue
105→- `feature_create_bulk` - Initialize all features (used by initializer)
106→
107→### Session Management
108→
109→- Each session runs with a fresh context window
110→- Progress is persisted via SQLite database and git commits
111→- The agent auto-continues between sessions (3 second delay)
112→- Press `Ctrl+C` to pause; run the start script again to resume
113→
114→---
115→
116→## Important Timing Expectations
117→
118→> **Note: Building complete applications takes time!**
119→
120→- **First session (initialization):** The agent generates feature test cases. This takes several minutes and may appear to hang - this is normal.
121→
122→- **Subsequent sessions:** Each coding iteration can take **5-15 minutes** depending on complexity.
123→
124→- **Full app:** Building all features typically requires **many hours** of total runtime across multiple sessions.
125→
126→**Tip:** The feature count in the prompts determines scope. For faster demos, you can modify your app spec to target fewer features (e.g., 20-50 features for a quick demo).
127→
128→---
129→
130→## Project Structure
131→
132→```
133→autonomous-coding/
134→├── start.bat # Windows CLI start script
135→├── start.sh # macOS/Linux CLI start script
136→├── start_ui.bat # Windows Web UI start script
137→├── start_ui.sh # macOS/Linux Web UI start script
138→├── start.py # CLI menu and project management
139→├── start_ui.py # Web UI backend (FastAPI server launcher)
140→├── autonomous_agent_demo.py # Agent entry point
141→├── agent.py # Agent session logic
142→├── client.py # Claude SDK client configuration
143→├── security.py # Bash command allowlist and validation
144→├── progress.py # Progress tracking utilities
145→├── prompts.py # Prompt loading utilities
146→├── api/
147→│ └── database.py # SQLAlchemy models (Feature table)
148→├── mcp_server/
149→│ └── feature_mcp.py # MCP server for feature management tools
150→├── server/
151→│ ├── main.py # FastAPI REST API server
152→│ ├── websocket.py # WebSocket handler for real-time updates
153→│ ├── schemas.py # Pydantic schemas
154→│ ├── routers/ # API route handlers
155→│ └── services/ # Business logic services
156→├── ui/ # React frontend
157→│ ├── src/
158→│ │ ├── App.tsx # Main app component
159→│ │ ├── hooks/ # React Query and WebSocket hooks
160→│ │ └── lib/ # API client and types
161→│ ├── package.json
162→│ └── vite.config.ts
163→├── .claude/
164→│ ├── commands/
165→│ │ └── create-spec.md # /create-spec slash command
166→│ ├── skills/ # Claude Code skills
167→│ └── templates/ # Prompt templates
168→├── generations/ # Generated projects go here
169→├── requirements.txt # Python dependencies
170→└── .env # Optional configuration (N8N webhook)
171→```
172→
173→---
174→
175→## Generated Project Structure
176→
177→After the agent runs, your project directory will contain:
178→
179→```
180→generations/my_project/
181→├── features.db # SQLite database (feature test cases)
182→├── prompts/
183→│ ├── app_spec.txt # Your app specification
184→│ ├── initializer_prompt.md # First session prompt
185→│ └── coding_prompt.md # Continuation session prompt
186→├── init.sh # Environment setup script
187→├── claude-progress.txt # Session progress notes
188→└── [application files] # Generated application code
189→```
190→
191→---
192→
193→## Running the Generated Application
194→
195→After the agent completes (or pauses), you can run the generated application:
196→
197→```bash
198→cd generations/my_project
199→
200→# Run the setup script created by the agent
201→./init.sh
202→
203→# Or manually (typical for Node.js apps):
204→npm install
205→npm run dev
206→```
207→
208→The application will typically be available at `http://localhost:3000` or similar.
209→
210→---
211→
212→## Security Model
213→
214→This project uses a defense-in-depth security approach (see `security.py` and `client.py`):
215→
216→1. **OS-level Sandbox:** Bash commands run in an isolated environment
217→2. **Filesystem Restrictions:** File operations restricted to the project directory only
218→3. **Bash Allowlist:** Only specific commands are permitted:
219→ - File inspection: `ls`, `cat`, `head`, `tail`, `wc`, `grep`
220→ - Node.js: `npm`, `node`
221→ - Version control: `git`
222→ - Process management: `ps`, `lsof`, `sleep`, `pkill` (dev processes only)
223→
224→Commands not in the allowlist are blocked by the security hook.
225→
226→---
227→
228→## Web UI Development
229→
230→The React UI is located in the `ui/` directory.
231→
232→### Development Mode
233→
234→```bash
235→cd ui
236→npm install
237→npm run dev # Development server with hot reload
238→```
239→
240→### Building for Production
241→
242→```bash
243→cd ui
244→npm run build # Builds to ui/dist/
245→```
246→
247→**Note:** The `start_ui.bat`/`start_ui.sh` scripts serve the pre-built UI from `ui/dist/`. After making UI changes, run `npm run build` to see them when using the start scripts.
248→
249→### Tech Stack
250→
251→- React 18 with TypeScript
252→- TanStack Query for data fetching
253→- Tailwind CSS v4 with neobrutalism design
254→- Radix UI components
255→- WebSocket for real-time updates
256→
257→### Real-time Updates
258→
259→The UI receives live updates via WebSocket (`/ws/projects/{project_name}`):
260→- `progress` - Test pass counts
261→- `agent_status` - Running/paused/stopped/crashed
262→- `log` - Agent output lines (streamed from subprocess stdout)
263→- `feature_update` - Feature status changes
264→
265→---
266→
267→## Configuration (Optional)
268→
269→### N8N Webhook Integration
270→
271→The agent can send progress notifications to an N8N webhook. Create a `.env` file:
272→
273→```bash
274→# Optional: N8N webhook for progress notifications
275→PROGRESS_N8N_WEBHOOK_URL=https://your-n8n-instance.com/webhook/your-webhook-id
276→```
277→
278→When test progress increases, the agent sends:
279→
280→```json
281→{
282→ "event": "test_progress",
283→ "passing": 45,
284→ "total": 200,
285→ "percentage": 22.5,
286→ "project": "my_project",
287→ "timestamp": "2025-01-15T14:30:00.000Z"
288→}
289→```
290→
291→---
292→
293→## Customization
294→
295→### Changing the Application
296→
297→Use the `/create-spec` command when creating a new project, or manually edit the files in your project's `prompts/` directory:
298→- `app_spec.txt` - Your application specification
299→- `initializer_prompt.md` - Controls feature generation
300→
301→### Modifying Allowed Commands
302→
303→Edit `security.py` to add or remove commands from `ALLOWED_COMMANDS`.
304→
305→---
306→
307→## Troubleshooting
308→
309→**"Claude CLI not found"**
310→Install the Claude Code CLI using the instructions in the Prerequisites section.
311→
312→**"Not authenticated with Claude"**
313→Run `claude login` to authenticate. The start script will prompt you to do this automatically.
314→
315→**"Appears to hang on first run"**
316→This is normal. The initializer agent is generating detailed test cases, which takes significant time. Watch for `[Tool: ...]` output to confirm the agent is working.
317→
318→**"Command blocked by security hook"**
319→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`.
320→
321→---
322→
323→## License
324→
325→This project is licensed under the GNU Affero General Public License v3.0 - see the [LICENSE.md](LICENSE.md) file for details.
326→Copyright (C) 2026 Leon van Zyl (https://leonvanzyl.com)
327→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

View File

@@ -0,0 +1,205 @@
1→"""
2→WebSocket Router
3→================
4→
5→WebSocket endpoints for real-time updates.
6→"""
7→
8→import asyncio
9→import json
10→from datetime import datetime
11→from typing import Dict, Set
12→
13→from fastapi import APIRouter, WebSocket, WebSocketDisconnect
14→from sqlalchemy import select
15→
16→from ..database import async_session_maker
17→from ..models import Project
18→
19→router = APIRouter()
20→
21→
22→# ============================================================================
23→# Connection Manager
24→# ============================================================================
25→
26→class ConnectionManager:
27→ """Manages WebSocket connections for projects."""
28→
29→ def __init__(self):
30→ # Map project name to set of active connections
31→ self.active_connections: Dict[str, Set[WebSocket]] = {}
32→ # Track heartbeat tasks
33→ self.heartbeat_tasks: Dict[WebSocket, asyncio.Task] = {}
34→
35→ async def connect(self, websocket: WebSocket, project_name: str) -> bool:
36→ """Accept connection and add to project's connection list."""
37→ await websocket.accept()
38→
39→ if project_name not in self.active_connections:
40→ self.active_connections[project_name] = set()
41→
42→ self.active_connections[project_name].add(websocket)
43→
44→ # Start heartbeat task
45→ task = asyncio.create_task(self._heartbeat_loop(websocket, project_name))
46→ self.heartbeat_tasks[websocket] = task
47→
48→ return True
49→
50→ def disconnect(self, websocket: WebSocket, project_name: str):
51→ """Remove connection from project's connection list."""
52→ if project_name in self.active_connections:
53→ self.active_connections[project_name].discard(websocket)
54→ if not self.active_connections[project_name]:
55→ del self.active_connections[project_name]
56→
57→ # Cancel heartbeat task
58→ if websocket in self.heartbeat_tasks:
59→ self.heartbeat_tasks[websocket].cancel()
60→ del self.heartbeat_tasks[websocket]
61→
62→ async def broadcast_to_project(self, project_name: str, message: dict):
63→ """Send message to all connections for a project."""
64→ if project_name in self.active_connections:
65→ disconnected = set()
66→ for connection in self.active_connections[project_name]:
67→ try:
68→ await connection.send_json(message)
69→ except Exception:
70→ disconnected.add(connection)
71→
72→ # Clean up disconnected connections
73→ for conn in disconnected:
74→ self.disconnect(conn, project_name)
75→
76→ async def _heartbeat_loop(self, websocket: WebSocket, project_name: str):
77→ """Send periodic heartbeat messages."""
78→ try:
79→ while True:
80→ await asyncio.sleep(30) # Send heartbeat every 30 seconds
81→ try:
82→ await websocket.send_json({
83→ "type": "heartbeat",
84→ "timestamp": datetime.utcnow().isoformat(),
85→ "project": project_name
86→ })
87→ except Exception:
88→ break
89→ except asyncio.CancelledError:
90→ pass
91→
92→
93→# Global connection manager
94→manager = ConnectionManager()
95→
96→
97→# ============================================================================
98→# WebSocket Endpoints
99→# ============================================================================
100→
101→@router.websocket("/ws/projects/{project_name}")
102→async def websocket_endpoint(websocket: WebSocket, project_name: str):
103→ """
104→ WebSocket endpoint for real-time project updates.
105→
106→ Message types:
107→ - heartbeat: Periodic keepalive messages
108→ - agent_log: Log output from agents
109→ - agent_status: Agent status changes
110→ - feature_update: Feature status changes
111→ - permission_request: New permission request from agent
112→ """
113→ # Verify project exists
114→ async with async_session_maker() as session:
115→ result = await session.execute(
116→ select(Project).where(Project.name == project_name)
117→ )
118→ project = result.scalar_one_or_none()
119→
120→ if not project:
121→ await websocket.close(code=4004, reason=f"Project '{project_name}' not found")
122→ return
123→
124→ # Accept connection
125→ await manager.connect(websocket, project_name)
126→
127→ # Send initial connection confirmation
128→ await websocket.send_json({
129→ "type": "connected",
130→ "project": project_name,
131→ "timestamp": datetime.utcnow().isoformat(),
132→ "message": f"Connected to project '{project_name}'"
133→ })
134→
135→ try:
136→ while True:
137→ # Wait for messages from client
138→ data = await websocket.receive_text()
139→ try:
140→ message = json.loads(data)
141→ # Handle client messages if needed
142→ if message.get("type") == "ping":
143→ await websocket.send_json({
144→ "type": "pong",
145→ "timestamp": datetime.utcnow().isoformat()
146→ })
147→ except json.JSONDecodeError:
148→ await websocket.send_json({
149→ "type": "error",
150→ "message": "Invalid JSON"
151→ })
152→ except WebSocketDisconnect:
153→ manager.disconnect(websocket, project_name)
154→
155→
156→# ============================================================================
157→# Utility Functions for Broadcasting
158→# ============================================================================
159→
160→async def broadcast_agent_log(project_name: str, agent_id: str, log_type: str, content: str):
161→ """Broadcast agent log message to all project connections."""
162→ await manager.broadcast_to_project(project_name, {
163→ "type": "agent_log",
164→ "agent_id": agent_id,
165→ "log_type": log_type, # "info", "error", "tool", "permission"
166→ "content": content,
167→ "timestamp": datetime.utcnow().isoformat()
168→ })
169→
170→
171→async def broadcast_agent_status(project_name: str, agent_id: str, status: str):
172→ """Broadcast agent status change to all project connections."""
173→ await manager.broadcast_to_project(project_name, {
174→ "type": "agent_status",
175→ "agent_id": agent_id,
176→ "status": status, # "running", "paused", "stopped", "stalled"
177→ "timestamp": datetime.utcnow().isoformat()
178→ })
179→
180→
181→async def broadcast_feature_update(project_name: str, feature_id: int, changes: dict):
182→ """Broadcast feature update to all project connections."""
183→ await manager.broadcast_to_project(project_name, {
184→ "type": "feature_update",
185→ "feature_id": feature_id,
186→ "changes": changes,
187→ "timestamp": datetime.utcnow().isoformat()
188→ })
189→
190→
191→async def broadcast_permission_request(project_name: str, request_id: int, permission_type: str, permission_value: str, agent_id: str):
192→ """Broadcast permission request to all project connections."""
193→ await manager.broadcast_to_project(project_name, {
194→ "type": "permission_request",
195→ "request_id": request_id,
196→ "permission_type": permission_type,
197→ "permission_value": permission_value,
198→ "agent_id": agent_id,
199→ "timestamp": datetime.utcnow().isoformat()
200→ })
201→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

View File

@@ -0,0 +1 @@
<tool_use_error>Directory does not exist: C:\Users\MikeSwanson\remix</tool_use_error>

View File

@@ -0,0 +1,333 @@
1→"""
2→Spec Router
3→===========
4→
5→API endpoints for project specification management.
6→"""
7→
8→from datetime import datetime
9→from typing import Optional, List
10→
11→from fastapi import APIRouter, Depends, HTTPException, status
12→from pydantic import BaseModel, Field
13→from sqlalchemy import select, and_, func
14→from sqlalchemy.ext.asyncio import AsyncSession
15→
16→from ..database import get_db
17→from ..models import Project, SpecHistory, Feature
18→
19→router = APIRouter(prefix="/api/projects", tags=["spec"])
20→
21→
22→# ============================================================================
23→# Pydantic Schemas
24→# ============================================================================
25→
26→class SpecResponse(BaseModel):
27→ """Schema for current spec response."""
28→ project_id: int
29→ version: int
30→ content: str
31→ created_at: datetime
32→ change_summary: Optional[str]
33→
34→ class Config:
35→ from_attributes = True
36→
37→
38→class SpecHistoryResponse(BaseModel):
39→ """Schema for spec history entry."""
40→ id: int
41→ project_id: int
42→ version: int
43→ content: str
44→ created_at: datetime
45→ change_summary: Optional[str]
46→
47→ class Config:
48→ from_attributes = True
49→
50→
51→class SpecHistoryListResponse(BaseModel):
52→ """Schema for spec history list."""
53→ history: List[SpecHistoryResponse]
54→ count: int
55→
56→
57→class SpecUpdate(BaseModel):
58→ """Schema for updating a spec."""
59→ content: str = Field(..., min_length=1)
60→ change_summary: Optional[str] = None
61→
62→
63→class AffectedFeatureResponse(BaseModel):
64→ """Schema for an affected feature in change analysis."""
65→ id: int
66→ name: str
67→ category: str
68→ reason: str
69→
70→ class Config:
71→ from_attributes = True
72→
73→
74→class SpecAnalysisResponse(BaseModel):
75→ """Schema for spec change analysis response."""
76→ affected_features: List[AffectedFeatureResponse]
77→ affected_count: int
78→ change_summary: str
79→
80→
81→# ============================================================================
82→# API Endpoints
83→# ============================================================================
84→
85→@router.get("/{name}/spec", response_model=SpecResponse)
86→async def get_current_spec(name: str, db: AsyncSession = Depends(get_db)):
87→ """Get the current (latest) spec for a project."""
88→ # Verify project exists
89→ result = await db.execute(select(Project).where(Project.name == name))
90→ project = result.scalar_one_or_none()
91→
92→ if not project:
93→ raise HTTPException(
94→ status_code=status.HTTP_404_NOT_FOUND,
95→ detail=f"Project '{name}' not found"
96→ )
97→
98→ # Get latest spec version
99→ result = await db.execute(
100→ select(SpecHistory)
101→ .where(SpecHistory.project_id == project.id)
102→ .order_by(SpecHistory.version.desc())
103→ .limit(1)
104→ )
105→ spec = result.scalar_one_or_none()
106→
107→ if not spec:
108→ # Return empty spec if none exists
109→ return SpecResponse(
110→ project_id=project.id,
111→ version=0,
112→ content="",
113→ created_at=project.created_at,
114→ change_summary="No spec yet"
115→ )
116→
117→ return SpecResponse(
118→ project_id=spec.project_id,
119→ version=spec.version,
120→ content=spec.content,
121→ created_at=spec.created_at,
122→ change_summary=spec.change_summary
123→ )
124→
125→
126→@router.put("/{name}/spec", response_model=SpecResponse)
127→async def update_spec(name: str, spec_update: SpecUpdate, db: AsyncSession = Depends(get_db)):
128→ """Update the spec (creates a new version)."""
129→ # Verify project exists
130→ result = await db.execute(select(Project).where(Project.name == name))
131→ project = result.scalar_one_or_none()
132→
133→ if not project:
134→ raise HTTPException(
135→ status_code=status.HTTP_404_NOT_FOUND,
136→ detail=f"Project '{name}' not found"
137→ )
138→
139→ # Get current version
140→ result = await db.execute(
141→ select(func.max(SpecHistory.version))
142→ .where(SpecHistory.project_id == project.id)
143→ )
144→ current_version = result.scalar() or 0
145→
146→ # Create new version
147→ new_version = current_version + 1
148→ new_spec = SpecHistory(
149→ project_id=project.id,
150→ version=new_version,
151→ content=spec_update.content,
152→ change_summary=spec_update.change_summary,
153→ )
154→ db.add(new_spec)
155→
156→ # Update project's spec_version
157→ project.spec_version = new_version
158→ project.updated_at = datetime.utcnow()
159→
160→ await db.commit()
161→ await db.refresh(new_spec)
162→
163→ return SpecResponse(
164→ project_id=new_spec.project_id,
165→ version=new_spec.version,
166→ content=new_spec.content,
167→ created_at=new_spec.created_at,
168→ change_summary=new_spec.change_summary
169→ )
170→
171→
172→@router.get("/{name}/spec/history", response_model=SpecHistoryListResponse)
173→async def get_spec_history(name: str, db: AsyncSession = Depends(get_db)):
174→ """Get the spec version history for a project."""
175→ # Verify project exists
176→ result = await db.execute(select(Project).where(Project.name == name))
177→ project = result.scalar_one_or_none()
178→
179→ if not project:
180→ raise HTTPException(
181→ status_code=status.HTTP_404_NOT_FOUND,
182→ detail=f"Project '{name}' not found"
183→ )
184→
185→ # Get all spec versions
186→ result = await db.execute(
187→ select(SpecHistory)
188→ .where(SpecHistory.project_id == project.id)
189→ .order_by(SpecHistory.version.desc())
190→ )
191→ history = result.scalars().all()
192→
193→ return SpecHistoryListResponse(
194→ history=[SpecHistoryResponse.model_validate(h) for h in history],
195→ count=len(history)
196→ )
197→
198→
199→@router.post("/{name}/spec/rollback/{version}", response_model=SpecResponse)
200→async def rollback_spec(name: str, version: int, db: AsyncSession = Depends(get_db)):
201→ """Rollback to a previous spec version."""
202→ # Verify project exists
203→ result = await db.execute(select(Project).where(Project.name == name))
204→ project = result.scalar_one_or_none()
205→
206→ if not project:
207→ raise HTTPException(
208→ status_code=status.HTTP_404_NOT_FOUND,
209→ detail=f"Project '{name}' not found"
210→ )
211→
212→ # Get the target version
213→ result = await db.execute(
214→ select(SpecHistory).where(and_(
215→ SpecHistory.project_id == project.id,
216→ SpecHistory.version == version
217→ ))
218→ )
219→ target_spec = result.scalar_one_or_none()
220→
221→ if not target_spec:
222→ raise HTTPException(
223→ status_code=status.HTTP_404_NOT_FOUND,
224→ detail=f"Spec version {version} not found"
225→ )
226→
227→ # Get current version number
228→ result = await db.execute(
229→ select(func.max(SpecHistory.version))
230→ .where(SpecHistory.project_id == project.id)
231→ )
232→ current_version = result.scalar() or 0
233→
234→ # Create new version with the old content
235→ new_version = current_version + 1
236→ new_spec = SpecHistory(
237→ project_id=project.id,
238→ version=new_version,
239→ content=target_spec.content,
240→ change_summary=f"Rollback to version {version}",
241→ )
242→ db.add(new_spec)
243→
244→ # Update project's spec_version
245→ project.spec_version = new_version
246→ project.updated_at = datetime.utcnow()
247→
248→ await db.commit()
249→ await db.refresh(new_spec)
250→
251→ return SpecResponse(
252→ project_id=new_spec.project_id,
253→ version=new_spec.version,
254→ content=new_spec.content,
255→ created_at=new_spec.created_at,
256→ change_summary=new_spec.change_summary
257→ )
258→
259→
260→@router.post("/{name}/spec/analyze-changes", response_model=SpecAnalysisResponse)
261→async def analyze_spec_changes(name: str, spec_update: SpecUpdate, db: AsyncSession = Depends(get_db)):
262→ """Preview the impact of spec changes before applying them."""
263→ # Verify project exists
264→ result = await db.execute(select(Project).where(Project.name == name))
265→ project = result.scalar_one_or_none()
266→
267→ if not project:
268→ raise HTTPException(
269→ status_code=status.HTTP_404_NOT_FOUND,
270→ detail=f"Project '{name}' not found"
271→ )
272→
273→ # Get current spec content for comparison
274→ result = await db.execute(
275→ select(SpecHistory)
276→ .where(SpecHistory.project_id == project.id)
277→ .order_by(SpecHistory.version.desc())
278→ .limit(1)
279→ )
280→ current_spec = result.scalar_one_or_none()
281→ current_content = current_spec.content if current_spec else ""
282→
283→ # Get all features for this project
284→ result = await db.execute(
285→ select(Feature)
286→ .where(Feature.project_id == project.id)
287→ )
288→ features = result.scalars().all()
289→
290→ # Analyze which features might be affected by the spec change
291→ # Simple heuristic: features whose names or descriptions contain words that appear
292→ # in the changed parts of the spec
293→ affected = []
294→ new_content = spec_update.content.lower()
295→ old_content = current_content.lower()
296→
297→ # Find words that are new in the spec
298→ new_words = set(new_content.split()) - set(old_content.split())
299→ changed_words = set(new_content.split()) ^ set(old_content.split()) # symmetric difference
300→
301→ for feature in features:
302→ feature_text = f"{feature.name} {feature.description or ''} {feature.category}".lower()
303→ feature_words = set(feature_text.split())
304→
305→ # Check if any changed words appear in the feature
306→ matching_words = feature_words & changed_words
307→ if matching_words:
308→ affected.append(AffectedFeatureResponse(
309→ id=feature.id,
310→ name=feature.name,
311→ category=feature.category,
312→ reason=f"Contains changed terms: {', '.join(list(matching_words)[:3])}"
313→ ))
314→
315→ # Generate change summary
316→ if current_content:
317→ lines_old = len(current_content.split('\n'))
318→ lines_new = len(spec_update.content.split('\n'))
319→ line_diff = lines_new - lines_old
320→ change_summary = spec_update.change_summary or f"Spec change: {line_diff:+d} lines, {len(changed_words)} terms changed"
321→ else:
322→ change_summary = spec_update.change_summary or "Initial spec creation"
323→
324→ return SpecAnalysisResponse(
325→ affected_features=affected,
326→ affected_count=len(affected),
327→ change_summary=change_summary
328→ )
329→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

View File

@@ -0,0 +1,226 @@
1→"""
2→Projects Router
3→===============
4→
5→API endpoints for project management.
6→"""
7→
8→from datetime import datetime
9→from typing import Optional, List
10→
11→from fastapi import APIRouter, Depends, HTTPException, status
12→from pydantic import BaseModel, Field
13→from sqlalchemy import select
14→from sqlalchemy.ext.asyncio import AsyncSession
15→
16→from ..database import get_db
17→from ..models import Project, Permission, Infrastructure
18→from ..path_security import validate_project_path
19→
20→router = APIRouter(prefix="/api/projects", tags=["projects"])
21→
22→
23→# ============================================================================
24→# Pydantic Schemas
25→# ============================================================================
26→
27→class ProjectCreate(BaseModel):
28→ """Schema for creating a new project."""
29→ name: str = Field(..., min_length=1, max_length=255, description="Unique project name")
30→ path: str = Field(..., min_length=1, max_length=1024, description="Project directory path")
31→
32→
33→class ProjectUpdate(BaseModel):
34→ """Schema for updating a project."""
35→ path: Optional[str] = Field(None, min_length=1, max_length=1024)
36→
37→
38→class ProjectResponse(BaseModel):
39→ """Schema for project response."""
40→ id: int
41→ name: str
42→ path: str
43→ created_at: datetime
44→ updated_at: datetime
45→ spec_version: int
46→
47→ class Config:
48→ from_attributes = True
49→
50→
51→class ProjectListResponse(BaseModel):
52→ """Schema for project list response."""
53→ projects: List[ProjectResponse]
54→ count: int
55→
56→
57→# ============================================================================
58→# API Endpoints
59→# ============================================================================
60→
61→@router.get("", response_model=ProjectListResponse)
62→async def list_projects(db: AsyncSession = Depends(get_db)):
63→ """List all projects."""
64→ result = await db.execute(select(Project).order_by(Project.name))
65→ projects = result.scalars().all()
66→ return ProjectListResponse(
67→ projects=[ProjectResponse.model_validate(p) for p in projects],
68→ count=len(projects)
69→ )
70→
71→
72→@router.post("", response_model=ProjectResponse, status_code=status.HTTP_201_CREATED)
73→async def create_project(project: ProjectCreate, db: AsyncSession = Depends(get_db)):
74→ """Create a new project."""
75→ # Validate path for security
76→ validate_project_path(project.path)
77→ # Check if project with this name already exists
78→ result = await db.execute(select(Project).where(Project.name == project.name))
79→ if result.scalar_one_or_none():
80→ raise HTTPException(
81→ status_code=status.HTTP_409_CONFLICT,
82→ detail=f"Project with name '{project.name}' already exists"
83→ )
84→
85→ # Create new project
86→ db_project = Project(
87→ name=project.name,
88→ path=project.path,
89→ )
90→ db.add(db_project)
91→ await db.commit()
92→ await db.refresh(db_project)
93→
94→ return ProjectResponse.model_validate(db_project)
95→
96→
97→@router.get("/{name}", response_model=ProjectResponse)
98→async def get_project(name: str, db: AsyncSession = Depends(get_db)):
99→ """Get a project by name."""
100→ result = await db.execute(select(Project).where(Project.name == name))
101→ project = result.scalar_one_or_none()
102→
103→ if not project:
104→ raise HTTPException(
105→ status_code=status.HTTP_404_NOT_FOUND,
106→ detail=f"Project '{name}' not found"
107→ )
108→
109→ return ProjectResponse.model_validate(project)
110→
111→
112→@router.put("/{name}", response_model=ProjectResponse)
113→async def update_project(name: str, project_update: ProjectUpdate, db: AsyncSession = Depends(get_db)):
114→ """Update a project by name."""
115→ result = await db.execute(select(Project).where(Project.name == name))
116→ project = result.scalar_one_or_none()
117→
118→ if not project:
119→ raise HTTPException(
120→ status_code=status.HTTP_404_NOT_FOUND,
121→ detail=f"Project '{name}' not found"
122→ )
123→
124→ if project_update.path is not None:
125→ # Validate path for security
126→ validate_project_path(project_update.path)
127→ project.path = project_update.path
128→
129→ project.updated_at = datetime.utcnow()
130→ await db.commit()
131→ await db.refresh(project)
132→
133→ return ProjectResponse.model_validate(project)
134→
135→
136→@router.delete("/{name}", status_code=status.HTTP_204_NO_CONTENT)
137→async def delete_project(name: str, db: AsyncSession = Depends(get_db)):
138→ """Delete a project by name."""
139→ result = await db.execute(select(Project).where(Project.name == name))
140→ project = result.scalar_one_or_none()
141→
142→ if not project:
143→ raise HTTPException(
144→ status_code=status.HTTP_404_NOT_FOUND,
145→ detail=f"Project '{name}' not found"
146→ )
147→
148→ await db.delete(project)
149→ await db.commit()
150→
151→
152→class DuplicateProjectRequest(BaseModel):
153→ new_name: str
154→
155→
156→@router.post("/{name}/duplicate", response_model=ProjectResponse, status_code=status.HTTP_201_CREATED)
157→async def duplicate_project(name: str, request: DuplicateProjectRequest, db: AsyncSession = Depends(get_db)):
158→ """Duplicate a project with a new name, including permissions and infrastructure."""
159→ new_name = request.new_name
160→ # Get source project
161→ result = await db.execute(select(Project).where(Project.name == name))
162→ source_project = result.scalar_one_or_none()
163→
164→ if not source_project:
165→ raise HTTPException(
166→ status_code=status.HTTP_404_NOT_FOUND,
167→ detail=f"Project '{name}' not found"
168→ )
169→
170→ # Check if new name already exists
171→ result = await db.execute(select(Project).where(Project.name == new_name))
172→ if result.scalar_one_or_none():
173→ raise HTTPException(
174→ status_code=status.HTTP_409_CONFLICT,
175→ detail=f"Project with name '{new_name}' already exists"
176→ )
177→
178→ # Create duplicate project
179→ new_project = Project(
180→ name=new_name,
181→ path=source_project.path,
182→ )
183→ db.add(new_project)
184→ await db.flush() # Get the new project ID without committing
185→
186→ # Copy permissions from source project
187→ permissions_result = await db.execute(
188→ select(Permission).where(Permission.project_id == source_project.id)
189→ )
190→ source_permissions = permissions_result.scalars().all()
191→ for perm in source_permissions:
192→ new_perm = Permission(
193→ project_id=new_project.id,
194→ permission_type=perm.permission_type,
195→ permission_value=perm.permission_value,
196→ granted=perm.granted,
197→ )
198→ db.add(new_perm)
199→
200→ # Copy infrastructure from source project
201→ infra_result = await db.execute(
202→ select(Infrastructure).where(Infrastructure.project_id == source_project.id)
203→ )
204→ source_infrastructure = infra_result.scalars().all()
205→ for infra in source_infrastructure:
206→ new_infra = Infrastructure(
207→ project_id=new_project.id,
208→ service_type=infra.service_type,
209→ service_name=infra.service_name,
210→ host=infra.host,
211→ port=infra.port,
212→ credentials_ref=infra.credentials_ref,
213→ health_check_url=infra.health_check_url,
214→ is_healthy=False, # Reset health status for new project
215→ )
216→ db.add(new_infra)
217→
218→ await db.commit()
219→ await db.refresh(new_project)
220→
221→ return ProjectResponse.model_validate(new_project)
222→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>