autoforge-ai 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/check-code.md +32 -0
- package/.claude/commands/checkpoint.md +40 -0
- package/.claude/commands/create-spec.md +613 -0
- package/.claude/commands/expand-project.md +234 -0
- package/.claude/commands/gsd-to-autoforge-spec.md +10 -0
- package/.claude/commands/review-pr.md +75 -0
- package/.claude/templates/app_spec.template.txt +331 -0
- package/.claude/templates/coding_prompt.template.md +265 -0
- package/.claude/templates/initializer_prompt.template.md +354 -0
- package/.claude/templates/testing_prompt.template.md +146 -0
- package/.env.example +64 -0
- package/LICENSE.md +676 -0
- package/README.md +423 -0
- package/agent.py +444 -0
- package/api/__init__.py +10 -0
- package/api/database.py +536 -0
- package/api/dependency_resolver.py +449 -0
- package/api/migration.py +156 -0
- package/auth.py +83 -0
- package/autoforge_paths.py +315 -0
- package/autonomous_agent_demo.py +293 -0
- package/bin/autoforge.js +3 -0
- package/client.py +607 -0
- package/env_constants.py +27 -0
- package/examples/OPTIMIZE_CONFIG.md +230 -0
- package/examples/README.md +531 -0
- package/examples/org_config.yaml +172 -0
- package/examples/project_allowed_commands.yaml +139 -0
- package/lib/cli.js +791 -0
- package/mcp_server/__init__.py +1 -0
- package/mcp_server/feature_mcp.py +988 -0
- package/package.json +53 -0
- package/parallel_orchestrator.py +1800 -0
- package/progress.py +247 -0
- package/prompts.py +427 -0
- package/pyproject.toml +17 -0
- package/rate_limit_utils.py +132 -0
- package/registry.py +614 -0
- package/requirements-prod.txt +14 -0
- package/security.py +959 -0
- package/server/__init__.py +17 -0
- package/server/main.py +261 -0
- package/server/routers/__init__.py +32 -0
- package/server/routers/agent.py +177 -0
- package/server/routers/assistant_chat.py +327 -0
- package/server/routers/devserver.py +309 -0
- package/server/routers/expand_project.py +239 -0
- package/server/routers/features.py +746 -0
- package/server/routers/filesystem.py +514 -0
- package/server/routers/projects.py +524 -0
- package/server/routers/schedules.py +356 -0
- package/server/routers/settings.py +127 -0
- package/server/routers/spec_creation.py +357 -0
- package/server/routers/terminal.py +453 -0
- package/server/schemas.py +593 -0
- package/server/services/__init__.py +36 -0
- package/server/services/assistant_chat_session.py +496 -0
- package/server/services/assistant_database.py +304 -0
- package/server/services/chat_constants.py +57 -0
- package/server/services/dev_server_manager.py +557 -0
- package/server/services/expand_chat_session.py +399 -0
- package/server/services/process_manager.py +657 -0
- package/server/services/project_config.py +475 -0
- package/server/services/scheduler_service.py +683 -0
- package/server/services/spec_chat_session.py +502 -0
- package/server/services/terminal_manager.py +756 -0
- package/server/utils/__init__.py +1 -0
- package/server/utils/process_utils.py +134 -0
- package/server/utils/project_helpers.py +32 -0
- package/server/utils/validation.py +54 -0
- package/server/websocket.py +903 -0
- package/start.py +456 -0
- package/ui/dist/assets/index-8W_wmZzz.js +168 -0
- package/ui/dist/assets/index-B47Ubhox.css +1 -0
- package/ui/dist/assets/vendor-flow-CVNK-_lx.js +7 -0
- package/ui/dist/assets/vendor-query-BUABzP5o.js +1 -0
- package/ui/dist/assets/vendor-radix-DTNNCg2d.js +45 -0
- package/ui/dist/assets/vendor-react-qkC6yhPU.js +1 -0
- package/ui/dist/assets/vendor-utils-COeKbHgx.js +2 -0
- package/ui/dist/assets/vendor-xterm-DP_gxef0.js +16 -0
- package/ui/dist/index.html +23 -0
- package/ui/dist/ollama.png +0 -0
- package/ui/dist/vite.svg +6 -0
- package/ui/package.json +57 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Expand Project Router
|
|
3
|
+
=====================
|
|
4
|
+
|
|
5
|
+
WebSocket and REST endpoints for interactive project expansion with Claude.
|
|
6
|
+
Allows adding multiple features to existing projects via natural language.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect
|
|
14
|
+
from pydantic import BaseModel, ValidationError
|
|
15
|
+
|
|
16
|
+
from ..schemas import ImageAttachment
|
|
17
|
+
from ..services.expand_chat_session import (
|
|
18
|
+
ExpandChatSession,
|
|
19
|
+
create_expand_session,
|
|
20
|
+
get_expand_session,
|
|
21
|
+
list_expand_sessions,
|
|
22
|
+
remove_expand_session,
|
|
23
|
+
)
|
|
24
|
+
from ..utils.project_helpers import get_project_path as _get_project_path
|
|
25
|
+
from ..utils.validation import validate_project_name
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
router = APIRouter(prefix="/api/expand", tags=["expand-project"])
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ============================================================================
|
|
34
|
+
# REST Endpoints
|
|
35
|
+
# ============================================================================
|
|
36
|
+
|
|
37
|
+
class ExpandSessionStatus(BaseModel):
|
|
38
|
+
"""Status of an expansion session."""
|
|
39
|
+
project_name: str
|
|
40
|
+
is_active: bool
|
|
41
|
+
is_complete: bool
|
|
42
|
+
features_created: int
|
|
43
|
+
message_count: int
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@router.get("/sessions", response_model=list[str])
|
|
47
|
+
async def list_expand_sessions_endpoint():
|
|
48
|
+
"""List all active expansion sessions."""
|
|
49
|
+
return list_expand_sessions()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@router.get("/sessions/{project_name}", response_model=ExpandSessionStatus)
|
|
53
|
+
async def get_expand_session_status(project_name: str):
|
|
54
|
+
"""Get status of an expansion session."""
|
|
55
|
+
project_name = validate_project_name(project_name)
|
|
56
|
+
|
|
57
|
+
session = get_expand_session(project_name)
|
|
58
|
+
if not session:
|
|
59
|
+
raise HTTPException(status_code=404, detail="No active expansion session for this project")
|
|
60
|
+
|
|
61
|
+
return ExpandSessionStatus(
|
|
62
|
+
project_name=project_name,
|
|
63
|
+
is_active=True,
|
|
64
|
+
is_complete=session.is_complete(),
|
|
65
|
+
features_created=session.get_features_created(),
|
|
66
|
+
message_count=len(session.get_messages()),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@router.delete("/sessions/{project_name}")
|
|
71
|
+
async def cancel_expand_session(project_name: str):
|
|
72
|
+
"""Cancel and remove an expansion session."""
|
|
73
|
+
project_name = validate_project_name(project_name)
|
|
74
|
+
|
|
75
|
+
session = get_expand_session(project_name)
|
|
76
|
+
if not session:
|
|
77
|
+
raise HTTPException(status_code=404, detail="No active expansion session for this project")
|
|
78
|
+
|
|
79
|
+
await remove_expand_session(project_name)
|
|
80
|
+
return {"success": True, "message": "Expansion session cancelled"}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ============================================================================
|
|
84
|
+
# WebSocket Endpoint
|
|
85
|
+
# ============================================================================
|
|
86
|
+
|
|
87
|
+
@router.websocket("/ws/{project_name}")
|
|
88
|
+
async def expand_project_websocket(websocket: WebSocket, project_name: str):
|
|
89
|
+
"""
|
|
90
|
+
WebSocket endpoint for interactive project expansion chat.
|
|
91
|
+
|
|
92
|
+
Message protocol:
|
|
93
|
+
|
|
94
|
+
Client -> Server:
|
|
95
|
+
- {"type": "start"} - Start the expansion session
|
|
96
|
+
- {"type": "message", "content": "..."} - Send user message
|
|
97
|
+
- {"type": "ping"} - Keep-alive ping
|
|
98
|
+
|
|
99
|
+
Server -> Client:
|
|
100
|
+
- {"type": "text", "content": "..."} - Text chunk from Claude
|
|
101
|
+
- {"type": "features_created", "count": N, "features": [...]} - Features added
|
|
102
|
+
- {"type": "expansion_complete", "total_added": N} - Session complete
|
|
103
|
+
- {"type": "response_done"} - Response complete
|
|
104
|
+
- {"type": "error", "content": "..."} - Error message
|
|
105
|
+
- {"type": "pong"} - Keep-alive pong
|
|
106
|
+
"""
|
|
107
|
+
try:
|
|
108
|
+
project_name = validate_project_name(project_name)
|
|
109
|
+
except HTTPException:
|
|
110
|
+
await websocket.close(code=4000, reason="Invalid project name")
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
# Look up project directory from registry
|
|
114
|
+
project_dir = _get_project_path(project_name)
|
|
115
|
+
if not project_dir:
|
|
116
|
+
await websocket.close(code=4004, reason="Project not found in registry")
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
if not project_dir.exists():
|
|
120
|
+
await websocket.close(code=4004, reason="Project directory not found")
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
# Verify project has app_spec.txt
|
|
124
|
+
from autoforge_paths import get_prompts_dir
|
|
125
|
+
spec_path = get_prompts_dir(project_dir) / "app_spec.txt"
|
|
126
|
+
if not spec_path.exists():
|
|
127
|
+
await websocket.close(code=4004, reason="Project has no spec. Create spec first.")
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
await websocket.accept()
|
|
131
|
+
|
|
132
|
+
session: Optional[ExpandChatSession] = None
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
while True:
|
|
136
|
+
try:
|
|
137
|
+
# Receive message from client
|
|
138
|
+
data = await websocket.receive_text()
|
|
139
|
+
message = json.loads(data)
|
|
140
|
+
msg_type = message.get("type")
|
|
141
|
+
|
|
142
|
+
if msg_type == "ping":
|
|
143
|
+
await websocket.send_json({"type": "pong"})
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
elif msg_type == "start":
|
|
147
|
+
# Check if session already exists (idempotent start)
|
|
148
|
+
existing_session = get_expand_session(project_name)
|
|
149
|
+
if existing_session:
|
|
150
|
+
session = existing_session
|
|
151
|
+
await websocket.send_json({
|
|
152
|
+
"type": "text",
|
|
153
|
+
"content": "Resuming existing expansion session. What would you like to add?"
|
|
154
|
+
})
|
|
155
|
+
await websocket.send_json({"type": "response_done"})
|
|
156
|
+
else:
|
|
157
|
+
# Create and start a new expansion session
|
|
158
|
+
session = await create_expand_session(project_name, project_dir)
|
|
159
|
+
|
|
160
|
+
# Stream the initial greeting
|
|
161
|
+
async for chunk in session.start():
|
|
162
|
+
await websocket.send_json(chunk)
|
|
163
|
+
|
|
164
|
+
elif msg_type == "message":
|
|
165
|
+
# User sent a message
|
|
166
|
+
if not session:
|
|
167
|
+
session = get_expand_session(project_name)
|
|
168
|
+
if not session:
|
|
169
|
+
await websocket.send_json({
|
|
170
|
+
"type": "error",
|
|
171
|
+
"content": "No active session. Send 'start' first."
|
|
172
|
+
})
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
user_content = message.get("content", "").strip()
|
|
176
|
+
|
|
177
|
+
# Parse attachments if present
|
|
178
|
+
attachments: list[ImageAttachment] = []
|
|
179
|
+
raw_attachments = message.get("attachments", [])
|
|
180
|
+
if raw_attachments:
|
|
181
|
+
try:
|
|
182
|
+
for raw_att in raw_attachments:
|
|
183
|
+
attachments.append(ImageAttachment(**raw_att))
|
|
184
|
+
except (ValidationError, Exception) as e:
|
|
185
|
+
logger.warning(f"Invalid attachment data: {e}")
|
|
186
|
+
await websocket.send_json({
|
|
187
|
+
"type": "error",
|
|
188
|
+
"content": "Invalid attachment format"
|
|
189
|
+
})
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
# Allow empty content if attachments are present
|
|
193
|
+
if not user_content and not attachments:
|
|
194
|
+
await websocket.send_json({
|
|
195
|
+
"type": "error",
|
|
196
|
+
"content": "Empty message"
|
|
197
|
+
})
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
# Stream Claude's response
|
|
201
|
+
async for chunk in session.send_message(user_content, attachments if attachments else None):
|
|
202
|
+
await websocket.send_json(chunk)
|
|
203
|
+
|
|
204
|
+
elif msg_type == "done":
|
|
205
|
+
# User is done adding features
|
|
206
|
+
if session:
|
|
207
|
+
await websocket.send_json({
|
|
208
|
+
"type": "expansion_complete",
|
|
209
|
+
"total_added": session.get_features_created()
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
else:
|
|
213
|
+
await websocket.send_json({
|
|
214
|
+
"type": "error",
|
|
215
|
+
"content": f"Unknown message type: {msg_type}"
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
except json.JSONDecodeError:
|
|
219
|
+
await websocket.send_json({
|
|
220
|
+
"type": "error",
|
|
221
|
+
"content": "Invalid JSON"
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
except WebSocketDisconnect:
|
|
225
|
+
logger.info(f"Expand chat WebSocket disconnected for {project_name}")
|
|
226
|
+
|
|
227
|
+
except Exception:
|
|
228
|
+
logger.exception(f"Expand chat WebSocket error for {project_name}")
|
|
229
|
+
try:
|
|
230
|
+
await websocket.send_json({
|
|
231
|
+
"type": "error",
|
|
232
|
+
"content": "Internal server error"
|
|
233
|
+
})
|
|
234
|
+
except Exception:
|
|
235
|
+
pass
|
|
236
|
+
|
|
237
|
+
finally:
|
|
238
|
+
# Don't remove the session on disconnect - allow resume
|
|
239
|
+
pass
|