autoforge-ai 0.1.3 → 0.1.5
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/.env.example +7 -35
- package/README.md +7 -31
- package/client.py +4 -3
- package/env_constants.py +1 -0
- package/package.json +1 -1
- package/registry.py +149 -4
- package/server/routers/agent.py +1 -1
- package/server/routers/assistant_chat.py +10 -4
- package/server/routers/expand_project.py +8 -2
- package/server/routers/settings.py +76 -19
- package/server/routers/spec_creation.py +13 -7
- package/server/routers/terminal.py +14 -8
- package/server/schemas.py +43 -5
- package/server/services/assistant_chat_session.py +7 -11
- package/server/services/expand_chat_session.py +6 -11
- package/server/services/process_manager.py +58 -2
- package/server/services/spec_chat_session.py +6 -11
- package/server/websocket.py +8 -5
- package/ui/dist/assets/index-CCu7z6o1.css +1 -0
- package/ui/dist/assets/index-DOPvjpbF.js +97 -0
- package/ui/dist/assets/vendor-utils-ZeeSylek.js +2 -0
- package/ui/dist/index.html +3 -3
- package/ui/dist/assets/index-CNq40B6c.js +0 -97
- package/ui/dist/assets/index-InF2n2n-.css +0 -1
- package/ui/dist/assets/vendor-utils-Cj4T6W23.js +0 -2
|
@@ -26,7 +26,7 @@ from ..services.terminal_manager import (
|
|
|
26
26
|
stop_terminal_session,
|
|
27
27
|
)
|
|
28
28
|
from ..utils.project_helpers import get_project_path as _get_project_path
|
|
29
|
-
from ..utils.validation import is_valid_project_name
|
|
29
|
+
from ..utils.validation import is_valid_project_name
|
|
30
30
|
|
|
31
31
|
logger = logging.getLogger(__name__)
|
|
32
32
|
|
|
@@ -89,7 +89,7 @@ async def list_project_terminals(project_name: str) -> list[TerminalInfoResponse
|
|
|
89
89
|
Returns:
|
|
90
90
|
List of terminal info objects
|
|
91
91
|
"""
|
|
92
|
-
if not
|
|
92
|
+
if not is_valid_project_name(project_name):
|
|
93
93
|
raise HTTPException(status_code=400, detail="Invalid project name")
|
|
94
94
|
|
|
95
95
|
project_dir = _get_project_path(project_name)
|
|
@@ -122,7 +122,7 @@ async def create_project_terminal(
|
|
|
122
122
|
Returns:
|
|
123
123
|
The created terminal info
|
|
124
124
|
"""
|
|
125
|
-
if not
|
|
125
|
+
if not is_valid_project_name(project_name):
|
|
126
126
|
raise HTTPException(status_code=400, detail="Invalid project name")
|
|
127
127
|
|
|
128
128
|
project_dir = _get_project_path(project_name)
|
|
@@ -148,7 +148,7 @@ async def rename_project_terminal(
|
|
|
148
148
|
Returns:
|
|
149
149
|
The updated terminal info
|
|
150
150
|
"""
|
|
151
|
-
if not
|
|
151
|
+
if not is_valid_project_name(project_name):
|
|
152
152
|
raise HTTPException(status_code=400, detail="Invalid project name")
|
|
153
153
|
|
|
154
154
|
if not validate_terminal_id(terminal_id):
|
|
@@ -180,7 +180,7 @@ async def delete_project_terminal(project_name: str, terminal_id: str) -> dict:
|
|
|
180
180
|
Returns:
|
|
181
181
|
Success message
|
|
182
182
|
"""
|
|
183
|
-
if not
|
|
183
|
+
if not is_valid_project_name(project_name):
|
|
184
184
|
raise HTTPException(status_code=400, detail="Invalid project name")
|
|
185
185
|
|
|
186
186
|
if not validate_terminal_id(terminal_id):
|
|
@@ -221,8 +221,12 @@ async def terminal_websocket(websocket: WebSocket, project_name: str, terminal_i
|
|
|
221
221
|
- {"type": "pong"} - Keep-alive response
|
|
222
222
|
- {"type": "error", "message": "..."} - Error message
|
|
223
223
|
"""
|
|
224
|
+
# Always accept WebSocket first to avoid opaque 403 errors
|
|
225
|
+
await websocket.accept()
|
|
226
|
+
|
|
224
227
|
# Validate project name
|
|
225
|
-
if not
|
|
228
|
+
if not is_valid_project_name(project_name):
|
|
229
|
+
await websocket.send_json({"type": "error", "message": "Invalid project name"})
|
|
226
230
|
await websocket.close(
|
|
227
231
|
code=TerminalCloseCode.INVALID_PROJECT_NAME, reason="Invalid project name"
|
|
228
232
|
)
|
|
@@ -230,6 +234,7 @@ async def terminal_websocket(websocket: WebSocket, project_name: str, terminal_i
|
|
|
230
234
|
|
|
231
235
|
# Validate terminal ID
|
|
232
236
|
if not validate_terminal_id(terminal_id):
|
|
237
|
+
await websocket.send_json({"type": "error", "message": "Invalid terminal ID"})
|
|
233
238
|
await websocket.close(
|
|
234
239
|
code=TerminalCloseCode.INVALID_PROJECT_NAME, reason="Invalid terminal ID"
|
|
235
240
|
)
|
|
@@ -238,6 +243,7 @@ async def terminal_websocket(websocket: WebSocket, project_name: str, terminal_i
|
|
|
238
243
|
# Look up project directory from registry
|
|
239
244
|
project_dir = _get_project_path(project_name)
|
|
240
245
|
if not project_dir:
|
|
246
|
+
await websocket.send_json({"type": "error", "message": "Project not found in registry"})
|
|
241
247
|
await websocket.close(
|
|
242
248
|
code=TerminalCloseCode.PROJECT_NOT_FOUND,
|
|
243
249
|
reason="Project not found in registry",
|
|
@@ -245,6 +251,7 @@ async def terminal_websocket(websocket: WebSocket, project_name: str, terminal_i
|
|
|
245
251
|
return
|
|
246
252
|
|
|
247
253
|
if not project_dir.exists():
|
|
254
|
+
await websocket.send_json({"type": "error", "message": "Project directory not found"})
|
|
248
255
|
await websocket.close(
|
|
249
256
|
code=TerminalCloseCode.PROJECT_NOT_FOUND,
|
|
250
257
|
reason="Project directory not found",
|
|
@@ -254,14 +261,13 @@ async def terminal_websocket(websocket: WebSocket, project_name: str, terminal_i
|
|
|
254
261
|
# Verify terminal exists in metadata
|
|
255
262
|
terminal_info = get_terminal_info(project_name, terminal_id)
|
|
256
263
|
if not terminal_info:
|
|
264
|
+
await websocket.send_json({"type": "error", "message": "Terminal not found"})
|
|
257
265
|
await websocket.close(
|
|
258
266
|
code=TerminalCloseCode.PROJECT_NOT_FOUND,
|
|
259
267
|
reason="Terminal not found",
|
|
260
268
|
)
|
|
261
269
|
return
|
|
262
270
|
|
|
263
|
-
await websocket.accept()
|
|
264
|
-
|
|
265
271
|
# Get or create terminal session for this project/terminal
|
|
266
272
|
session = get_terminal_session(project_name, project_dir, terminal_id)
|
|
267
273
|
|
package/server/schemas.py
CHANGED
|
@@ -391,15 +391,35 @@ class ModelInfo(BaseModel):
|
|
|
391
391
|
name: str
|
|
392
392
|
|
|
393
393
|
|
|
394
|
+
class ProviderInfo(BaseModel):
|
|
395
|
+
"""Information about an API provider."""
|
|
396
|
+
id: str
|
|
397
|
+
name: str
|
|
398
|
+
base_url: str | None = None
|
|
399
|
+
models: list[ModelInfo]
|
|
400
|
+
default_model: str
|
|
401
|
+
requires_auth: bool = False
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
class ProvidersResponse(BaseModel):
|
|
405
|
+
"""Response schema for available providers list."""
|
|
406
|
+
providers: list[ProviderInfo]
|
|
407
|
+
current: str
|
|
408
|
+
|
|
409
|
+
|
|
394
410
|
class SettingsResponse(BaseModel):
|
|
395
411
|
"""Response schema for global settings."""
|
|
396
412
|
yolo_mode: bool = False
|
|
397
413
|
model: str = DEFAULT_MODEL
|
|
398
|
-
glm_mode: bool = False # True
|
|
399
|
-
ollama_mode: bool = False # True
|
|
414
|
+
glm_mode: bool = False # True when api_provider is "glm"
|
|
415
|
+
ollama_mode: bool = False # True when api_provider is "ollama"
|
|
400
416
|
testing_agent_ratio: int = 1 # Regression testing agents (0-3)
|
|
401
417
|
playwright_headless: bool = True
|
|
402
418
|
batch_size: int = 3 # Features per coding agent batch (1-3)
|
|
419
|
+
api_provider: str = "claude"
|
|
420
|
+
api_base_url: str | None = None
|
|
421
|
+
api_has_auth_token: bool = False # Never expose actual token
|
|
422
|
+
api_model: str | None = None
|
|
403
423
|
|
|
404
424
|
|
|
405
425
|
class ModelsResponse(BaseModel):
|
|
@@ -415,12 +435,30 @@ class SettingsUpdate(BaseModel):
|
|
|
415
435
|
testing_agent_ratio: int | None = None # 0-3
|
|
416
436
|
playwright_headless: bool | None = None
|
|
417
437
|
batch_size: int | None = None # Features per agent batch (1-3)
|
|
438
|
+
api_provider: str | None = None
|
|
439
|
+
api_base_url: str | None = Field(None, max_length=500)
|
|
440
|
+
api_auth_token: str | None = Field(None, max_length=500) # Write-only, never returned
|
|
441
|
+
api_model: str | None = Field(None, max_length=200)
|
|
442
|
+
|
|
443
|
+
@field_validator('api_base_url')
|
|
444
|
+
@classmethod
|
|
445
|
+
def validate_api_base_url(cls, v: str | None) -> str | None:
|
|
446
|
+
if v is not None and v.strip():
|
|
447
|
+
v = v.strip()
|
|
448
|
+
if not v.startswith(("http://", "https://")):
|
|
449
|
+
raise ValueError("api_base_url must start with http:// or https://")
|
|
450
|
+
return v
|
|
418
451
|
|
|
419
452
|
@field_validator('model')
|
|
420
453
|
@classmethod
|
|
421
|
-
def validate_model(cls, v: str | None) -> str | None:
|
|
422
|
-
if v is not None
|
|
423
|
-
|
|
454
|
+
def validate_model(cls, v: str | None, info) -> str | None: # type: ignore[override]
|
|
455
|
+
if v is not None:
|
|
456
|
+
# Skip VALID_MODELS check when using an alternative API provider
|
|
457
|
+
api_provider = info.data.get("api_provider")
|
|
458
|
+
if api_provider and api_provider != "claude":
|
|
459
|
+
return v
|
|
460
|
+
if v not in VALID_MODELS:
|
|
461
|
+
raise ValueError(f"Invalid model. Must be one of: {VALID_MODELS}")
|
|
424
462
|
return v
|
|
425
463
|
|
|
426
464
|
@field_validator('testing_agent_ratio')
|
|
@@ -25,7 +25,7 @@ from .assistant_database import (
|
|
|
25
25
|
create_conversation,
|
|
26
26
|
get_messages,
|
|
27
27
|
)
|
|
28
|
-
from .chat_constants import
|
|
28
|
+
from .chat_constants import ROOT_DIR
|
|
29
29
|
|
|
30
30
|
# Load environment variables from .env file if present
|
|
31
31
|
load_dotenv()
|
|
@@ -157,7 +157,7 @@ class AssistantChatSession:
|
|
|
157
157
|
"""
|
|
158
158
|
Manages a read-only assistant conversation for a project.
|
|
159
159
|
|
|
160
|
-
Uses Claude Opus
|
|
160
|
+
Uses Claude Opus with only read-only tools enabled.
|
|
161
161
|
Persists conversation history to SQLite.
|
|
162
162
|
"""
|
|
163
163
|
|
|
@@ -258,15 +258,11 @@ class AssistantChatSession:
|
|
|
258
258
|
system_cli = shutil.which("claude")
|
|
259
259
|
|
|
260
260
|
# Build environment overrides for API configuration
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
# Determine model from environment or use default
|
|
268
|
-
# This allows using alternative APIs (e.g., GLM via z.ai) that may not support Claude model names
|
|
269
|
-
model = os.getenv("ANTHROPIC_DEFAULT_OPUS_MODEL", "claude-opus-4-5-20251101")
|
|
261
|
+
from registry import DEFAULT_MODEL, get_effective_sdk_env
|
|
262
|
+
sdk_env = get_effective_sdk_env()
|
|
263
|
+
|
|
264
|
+
# Determine model from SDK env (provider-aware) or fallback to env/default
|
|
265
|
+
model = sdk_env.get("ANTHROPIC_DEFAULT_OPUS_MODEL") or os.getenv("ANTHROPIC_DEFAULT_OPUS_MODEL", DEFAULT_MODEL)
|
|
270
266
|
|
|
271
267
|
try:
|
|
272
268
|
logger.info("Creating ClaudeSDKClient...")
|
|
@@ -22,7 +22,7 @@ from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
|
|
|
22
22
|
from dotenv import load_dotenv
|
|
23
23
|
|
|
24
24
|
from ..schemas import ImageAttachment
|
|
25
|
-
from .chat_constants import
|
|
25
|
+
from .chat_constants import ROOT_DIR, make_multimodal_message
|
|
26
26
|
|
|
27
27
|
# Load environment variables from .env file if present
|
|
28
28
|
load_dotenv()
|
|
@@ -154,16 +154,11 @@ class ExpandChatSession:
|
|
|
154
154
|
system_prompt = skill_content.replace("$ARGUMENTS", project_path)
|
|
155
155
|
|
|
156
156
|
# Build environment overrides for API configuration
|
|
157
|
-
|
|
158
|
-
sdk_env
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
sdk_env[var] = value
|
|
163
|
-
|
|
164
|
-
# Determine model from environment or use default
|
|
165
|
-
# This allows using alternative APIs (e.g., GLM via z.ai) that may not support Claude model names
|
|
166
|
-
model = os.getenv("ANTHROPIC_DEFAULT_OPUS_MODEL", "claude-opus-4-5-20251101")
|
|
157
|
+
from registry import DEFAULT_MODEL, get_effective_sdk_env
|
|
158
|
+
sdk_env = get_effective_sdk_env()
|
|
159
|
+
|
|
160
|
+
# Determine model from SDK env (provider-aware) or fallback to env/default
|
|
161
|
+
model = sdk_env.get("ANTHROPIC_DEFAULT_OPUS_MODEL") or os.getenv("ANTHROPIC_DEFAULT_OPUS_MODEL", DEFAULT_MODEL)
|
|
167
162
|
|
|
168
163
|
# Build MCP servers config for feature creation
|
|
169
164
|
mcp_servers = {
|
|
@@ -227,6 +227,46 @@ class AgentProcessManager:
|
|
|
227
227
|
"""Remove lock file."""
|
|
228
228
|
self.lock_file.unlink(missing_ok=True)
|
|
229
229
|
|
|
230
|
+
def _cleanup_stale_features(self) -> None:
|
|
231
|
+
"""Clear in_progress flag for all features when agent stops/crashes.
|
|
232
|
+
|
|
233
|
+
When the agent process exits (normally or crash), any features left
|
|
234
|
+
with in_progress=True were being worked on and didn't complete.
|
|
235
|
+
Reset them so they can be picked up on next agent start.
|
|
236
|
+
"""
|
|
237
|
+
try:
|
|
238
|
+
from autoforge_paths import get_features_db_path
|
|
239
|
+
features_db = get_features_db_path(self.project_dir)
|
|
240
|
+
if not features_db.exists():
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
from sqlalchemy import create_engine
|
|
244
|
+
from sqlalchemy.orm import sessionmaker
|
|
245
|
+
|
|
246
|
+
from api.database import Feature
|
|
247
|
+
|
|
248
|
+
engine = create_engine(f"sqlite:///{features_db}")
|
|
249
|
+
Session = sessionmaker(bind=engine)
|
|
250
|
+
session = Session()
|
|
251
|
+
try:
|
|
252
|
+
stuck = session.query(Feature).filter(
|
|
253
|
+
Feature.in_progress == True, # noqa: E712
|
|
254
|
+
Feature.passes == False, # noqa: E712
|
|
255
|
+
).all()
|
|
256
|
+
if stuck:
|
|
257
|
+
for f in stuck:
|
|
258
|
+
f.in_progress = False
|
|
259
|
+
session.commit()
|
|
260
|
+
logger.info(
|
|
261
|
+
"Cleaned up %d stuck feature(s) for %s",
|
|
262
|
+
len(stuck), self.project_name,
|
|
263
|
+
)
|
|
264
|
+
finally:
|
|
265
|
+
session.close()
|
|
266
|
+
engine.dispose()
|
|
267
|
+
except Exception as e:
|
|
268
|
+
logger.warning("Failed to cleanup features for %s: %s", self.project_name, e)
|
|
269
|
+
|
|
230
270
|
async def _broadcast_output(self, line: str) -> None:
|
|
231
271
|
"""Broadcast output line to all registered callbacks."""
|
|
232
272
|
with self._callbacks_lock:
|
|
@@ -288,6 +328,7 @@ class AgentProcessManager:
|
|
|
288
328
|
self.status = "crashed"
|
|
289
329
|
elif self.status == "running":
|
|
290
330
|
self.status = "stopped"
|
|
331
|
+
self._cleanup_stale_features()
|
|
291
332
|
self._remove_lock()
|
|
292
333
|
|
|
293
334
|
async def start(
|
|
@@ -305,7 +346,7 @@ class AgentProcessManager:
|
|
|
305
346
|
|
|
306
347
|
Args:
|
|
307
348
|
yolo_mode: If True, run in YOLO mode (skip testing agents)
|
|
308
|
-
model: Model to use (e.g., claude-opus-4-
|
|
349
|
+
model: Model to use (e.g., claude-opus-4-6)
|
|
309
350
|
parallel_mode: DEPRECATED - ignored, always uses unified orchestrator
|
|
310
351
|
max_concurrency: Max concurrent coding agents (1-5, default 1)
|
|
311
352
|
testing_agent_ratio: Number of regression testing agents (0-3, default 1)
|
|
@@ -320,6 +361,9 @@ class AgentProcessManager:
|
|
|
320
361
|
if not self._check_lock():
|
|
321
362
|
return False, "Another agent instance is already running for this project"
|
|
322
363
|
|
|
364
|
+
# Clean up features stuck from a previous crash/stop
|
|
365
|
+
self._cleanup_stale_features()
|
|
366
|
+
|
|
323
367
|
# Store for status queries
|
|
324
368
|
self.yolo_mode = yolo_mode
|
|
325
369
|
self.model = model
|
|
@@ -359,12 +403,22 @@ class AgentProcessManager:
|
|
|
359
403
|
# stdin=DEVNULL prevents blocking if Claude CLI or child process tries to read stdin
|
|
360
404
|
# CREATE_NO_WINDOW on Windows prevents console window pop-ups
|
|
361
405
|
# PYTHONUNBUFFERED ensures output isn't delayed
|
|
406
|
+
# Build subprocess environment with API provider settings
|
|
407
|
+
from registry import get_effective_sdk_env
|
|
408
|
+
api_env = get_effective_sdk_env()
|
|
409
|
+
subprocess_env = {
|
|
410
|
+
**os.environ,
|
|
411
|
+
"PYTHONUNBUFFERED": "1",
|
|
412
|
+
"PLAYWRIGHT_HEADLESS": "true" if playwright_headless else "false",
|
|
413
|
+
**api_env,
|
|
414
|
+
}
|
|
415
|
+
|
|
362
416
|
popen_kwargs: dict[str, Any] = {
|
|
363
417
|
"stdin": subprocess.DEVNULL,
|
|
364
418
|
"stdout": subprocess.PIPE,
|
|
365
419
|
"stderr": subprocess.STDOUT,
|
|
366
420
|
"cwd": str(self.project_dir),
|
|
367
|
-
"env":
|
|
421
|
+
"env": subprocess_env,
|
|
368
422
|
}
|
|
369
423
|
if sys.platform == "win32":
|
|
370
424
|
popen_kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
|
|
@@ -425,6 +479,7 @@ class AgentProcessManager:
|
|
|
425
479
|
result.children_terminated, result.children_killed
|
|
426
480
|
)
|
|
427
481
|
|
|
482
|
+
self._cleanup_stale_features()
|
|
428
483
|
self._remove_lock()
|
|
429
484
|
self.status = "stopped"
|
|
430
485
|
self.process = None
|
|
@@ -502,6 +557,7 @@ class AgentProcessManager:
|
|
|
502
557
|
if poll is not None:
|
|
503
558
|
# Process has terminated
|
|
504
559
|
if self.status in ("running", "paused"):
|
|
560
|
+
self._cleanup_stale_features()
|
|
505
561
|
self.status = "crashed"
|
|
506
562
|
self._remove_lock()
|
|
507
563
|
return False
|
|
@@ -19,7 +19,7 @@ from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
|
|
|
19
19
|
from dotenv import load_dotenv
|
|
20
20
|
|
|
21
21
|
from ..schemas import ImageAttachment
|
|
22
|
-
from .chat_constants import
|
|
22
|
+
from .chat_constants import ROOT_DIR, make_multimodal_message
|
|
23
23
|
|
|
24
24
|
# Load environment variables from .env file if present
|
|
25
25
|
load_dotenv()
|
|
@@ -140,16 +140,11 @@ class SpecChatSession:
|
|
|
140
140
|
system_cli = shutil.which("claude")
|
|
141
141
|
|
|
142
142
|
# Build environment overrides for API configuration
|
|
143
|
-
|
|
144
|
-
sdk_env
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
sdk_env[var] = value
|
|
149
|
-
|
|
150
|
-
# Determine model from environment or use default
|
|
151
|
-
# This allows using alternative APIs (e.g., GLM via z.ai) that may not support Claude model names
|
|
152
|
-
model = os.getenv("ANTHROPIC_DEFAULT_OPUS_MODEL", "claude-opus-4-5-20251101")
|
|
143
|
+
from registry import DEFAULT_MODEL, get_effective_sdk_env
|
|
144
|
+
sdk_env = get_effective_sdk_env()
|
|
145
|
+
|
|
146
|
+
# Determine model from SDK env (provider-aware) or fallback to env/default
|
|
147
|
+
model = sdk_env.get("ANTHROPIC_DEFAULT_OPUS_MODEL") or os.getenv("ANTHROPIC_DEFAULT_OPUS_MODEL", DEFAULT_MODEL)
|
|
153
148
|
|
|
154
149
|
try:
|
|
155
150
|
self.client = ClaudeSDKClient(
|
package/server/websocket.py
CHANGED
|
@@ -640,9 +640,7 @@ class ConnectionManager:
|
|
|
640
640
|
self._lock = asyncio.Lock()
|
|
641
641
|
|
|
642
642
|
async def connect(self, websocket: WebSocket, project_name: str):
|
|
643
|
-
"""
|
|
644
|
-
await websocket.accept()
|
|
645
|
-
|
|
643
|
+
"""Register a WebSocket connection for a project (must already be accepted)."""
|
|
646
644
|
async with self._lock:
|
|
647
645
|
if project_name not in self.active_connections:
|
|
648
646
|
self.active_connections[project_name] = set()
|
|
@@ -727,16 +725,22 @@ async def project_websocket(websocket: WebSocket, project_name: str):
|
|
|
727
725
|
- Agent status changes
|
|
728
726
|
- Agent stdout/stderr lines
|
|
729
727
|
"""
|
|
728
|
+
# Always accept WebSocket first to avoid opaque 403 errors
|
|
729
|
+
await websocket.accept()
|
|
730
|
+
|
|
730
731
|
if not validate_project_name(project_name):
|
|
732
|
+
await websocket.send_json({"type": "error", "content": "Invalid project name"})
|
|
731
733
|
await websocket.close(code=4000, reason="Invalid project name")
|
|
732
734
|
return
|
|
733
735
|
|
|
734
736
|
project_dir = _get_project_path(project_name)
|
|
735
737
|
if not project_dir:
|
|
738
|
+
await websocket.send_json({"type": "error", "content": "Project not found in registry"})
|
|
736
739
|
await websocket.close(code=4004, reason="Project not found in registry")
|
|
737
740
|
return
|
|
738
741
|
|
|
739
742
|
if not project_dir.exists():
|
|
743
|
+
await websocket.send_json({"type": "error", "content": "Project directory not found"})
|
|
740
744
|
await websocket.close(code=4004, reason="Project directory not found")
|
|
741
745
|
return
|
|
742
746
|
|
|
@@ -879,8 +883,7 @@ async def project_websocket(websocket: WebSocket, project_name: str):
|
|
|
879
883
|
break
|
|
880
884
|
except json.JSONDecodeError:
|
|
881
885
|
logger.warning(f"Invalid JSON from WebSocket: {data[:100] if data else 'empty'}")
|
|
882
|
-
except Exception
|
|
883
|
-
logger.warning(f"WebSocket error: {e}")
|
|
886
|
+
except Exception:
|
|
884
887
|
break
|
|
885
888
|
|
|
886
889
|
finally:
|