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.
Files changed (84) hide show
  1. package/.claude/commands/check-code.md +32 -0
  2. package/.claude/commands/checkpoint.md +40 -0
  3. package/.claude/commands/create-spec.md +613 -0
  4. package/.claude/commands/expand-project.md +234 -0
  5. package/.claude/commands/gsd-to-autoforge-spec.md +10 -0
  6. package/.claude/commands/review-pr.md +75 -0
  7. package/.claude/templates/app_spec.template.txt +331 -0
  8. package/.claude/templates/coding_prompt.template.md +265 -0
  9. package/.claude/templates/initializer_prompt.template.md +354 -0
  10. package/.claude/templates/testing_prompt.template.md +146 -0
  11. package/.env.example +64 -0
  12. package/LICENSE.md +676 -0
  13. package/README.md +423 -0
  14. package/agent.py +444 -0
  15. package/api/__init__.py +10 -0
  16. package/api/database.py +536 -0
  17. package/api/dependency_resolver.py +449 -0
  18. package/api/migration.py +156 -0
  19. package/auth.py +83 -0
  20. package/autoforge_paths.py +315 -0
  21. package/autonomous_agent_demo.py +293 -0
  22. package/bin/autoforge.js +3 -0
  23. package/client.py +607 -0
  24. package/env_constants.py +27 -0
  25. package/examples/OPTIMIZE_CONFIG.md +230 -0
  26. package/examples/README.md +531 -0
  27. package/examples/org_config.yaml +172 -0
  28. package/examples/project_allowed_commands.yaml +139 -0
  29. package/lib/cli.js +791 -0
  30. package/mcp_server/__init__.py +1 -0
  31. package/mcp_server/feature_mcp.py +988 -0
  32. package/package.json +53 -0
  33. package/parallel_orchestrator.py +1800 -0
  34. package/progress.py +247 -0
  35. package/prompts.py +427 -0
  36. package/pyproject.toml +17 -0
  37. package/rate_limit_utils.py +132 -0
  38. package/registry.py +614 -0
  39. package/requirements-prod.txt +14 -0
  40. package/security.py +959 -0
  41. package/server/__init__.py +17 -0
  42. package/server/main.py +261 -0
  43. package/server/routers/__init__.py +32 -0
  44. package/server/routers/agent.py +177 -0
  45. package/server/routers/assistant_chat.py +327 -0
  46. package/server/routers/devserver.py +309 -0
  47. package/server/routers/expand_project.py +239 -0
  48. package/server/routers/features.py +746 -0
  49. package/server/routers/filesystem.py +514 -0
  50. package/server/routers/projects.py +524 -0
  51. package/server/routers/schedules.py +356 -0
  52. package/server/routers/settings.py +127 -0
  53. package/server/routers/spec_creation.py +357 -0
  54. package/server/routers/terminal.py +453 -0
  55. package/server/schemas.py +593 -0
  56. package/server/services/__init__.py +36 -0
  57. package/server/services/assistant_chat_session.py +496 -0
  58. package/server/services/assistant_database.py +304 -0
  59. package/server/services/chat_constants.py +57 -0
  60. package/server/services/dev_server_manager.py +557 -0
  61. package/server/services/expand_chat_session.py +399 -0
  62. package/server/services/process_manager.py +657 -0
  63. package/server/services/project_config.py +475 -0
  64. package/server/services/scheduler_service.py +683 -0
  65. package/server/services/spec_chat_session.py +502 -0
  66. package/server/services/terminal_manager.py +756 -0
  67. package/server/utils/__init__.py +1 -0
  68. package/server/utils/process_utils.py +134 -0
  69. package/server/utils/project_helpers.py +32 -0
  70. package/server/utils/validation.py +54 -0
  71. package/server/websocket.py +903 -0
  72. package/start.py +456 -0
  73. package/ui/dist/assets/index-8W_wmZzz.js +168 -0
  74. package/ui/dist/assets/index-B47Ubhox.css +1 -0
  75. package/ui/dist/assets/vendor-flow-CVNK-_lx.js +7 -0
  76. package/ui/dist/assets/vendor-query-BUABzP5o.js +1 -0
  77. package/ui/dist/assets/vendor-radix-DTNNCg2d.js +45 -0
  78. package/ui/dist/assets/vendor-react-qkC6yhPU.js +1 -0
  79. package/ui/dist/assets/vendor-utils-COeKbHgx.js +2 -0
  80. package/ui/dist/assets/vendor-xterm-DP_gxef0.js +16 -0
  81. package/ui/dist/index.html +23 -0
  82. package/ui/dist/ollama.png +0 -0
  83. package/ui/dist/vite.svg +6 -0
  84. 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