loki-mode 6.53.0 → 6.54.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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/bin/postinstall.js +29 -0
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/mcp/__init__.py +1 -1
- package/package.json +11 -2
- package/web-app/Dockerfile +59 -0
- package/web-app/alembic.ini +43 -0
- package/web-app/auth.py +249 -0
- package/web-app/crypto.py +83 -0
- package/web-app/deploy/k8s/purple-lab/configmap.yaml +8 -0
- package/web-app/deploy/k8s/purple-lab/deployment.yaml +69 -0
- package/web-app/deploy/k8s/purple-lab/hpa.yaml +24 -0
- package/web-app/deploy/k8s/purple-lab/ingress.yaml +30 -0
- package/web-app/deploy/k8s/purple-lab/networkpolicy.yaml +82 -0
- package/web-app/deploy/k8s/purple-lab/pdb.yaml +11 -0
- package/web-app/deploy/k8s/purple-lab/postgres.yaml +84 -0
- package/web-app/deploy/k8s/purple-lab/pvc.yaml +10 -0
- package/web-app/deploy/k8s/purple-lab/secret.yaml +13 -0
- package/web-app/deploy/k8s/purple-lab/service.yaml +13 -0
- package/web-app/deploy/k8s/purple-lab/serviceaccount.yaml +7 -0
- package/web-app/dist/assets/{Badge-CnWBUi7C.js → Badge-COZdpB3a.js} +1 -1
- package/web-app/dist/assets/{Button-5ThWFbkO.js → Button-BBb9-LaD.js} +1 -1
- package/web-app/dist/assets/{Card-CcTmaOCN.js → Card-k2U78J9a.js} +1 -1
- package/web-app/dist/assets/{HomePage-Dx4Ae0hu.js → HomePage-BsDnbkbC.js} +1 -1
- package/web-app/dist/assets/{LoginPage-CRffqZNo.js → LoginPage-DbI9D-2B.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-B1QZ92yR.js → NotFoundPage-Cpvhx05J.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-BVnDGxXk.js → ProjectPage-D1TEIKzX.js} +12 -12
- package/web-app/dist/assets/{ProjectsPage-2Fi6cKB-.js → ProjectsPage-D07q5yXi.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-DOzGoyLv.js → SettingsPage-rj4gED1n.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-B-f1Gfbg.js → TemplatesPage-zBW2RQne.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-DrKIbiB8.js → TerminalOutput-D3phVPIe.js} +1 -1
- package/web-app/dist/assets/{arrow-left-CFG0TEkb.js → arrow-left-Al_UelcY.js} +1 -1
- package/web-app/dist/assets/{clock-C-GPrW5k.js → clock-DSbHSd8C.js} +1 -1
- package/web-app/dist/assets/{external-link-ujbkNBY4.js → external-link-D24QKyPn.js} +1 -1
- package/web-app/dist/assets/{index-B8gGcUMo.js → index-BL1cnbvX.js} +2 -2
- package/web-app/dist/index.html +1 -1
- package/web-app/docker-compose.purple-lab.yml +76 -0
- package/web-app/migrations/env.py +103 -0
- package/web-app/migrations/script.py.mako +25 -0
- package/web-app/migrations/versions/.gitkeep +0 -0
- package/web-app/migrations/versions/001_initial_schema.py +118 -0
- package/web-app/models.py +140 -0
- package/web-app/requirements.txt +27 -0
- package/web-app/server.py +8 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Purple Lab database models.
|
|
2
|
+
|
|
3
|
+
Provides SQLAlchemy models for multi-user cloud deployment.
|
|
4
|
+
When no DATABASE_URL is configured, the system falls back to
|
|
5
|
+
file-based storage (local development mode).
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
import uuid
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
|
|
11
|
+
from sqlalchemy import (
|
|
12
|
+
Boolean,
|
|
13
|
+
Column,
|
|
14
|
+
DateTime,
|
|
15
|
+
ForeignKey,
|
|
16
|
+
Integer,
|
|
17
|
+
JSON,
|
|
18
|
+
String,
|
|
19
|
+
Text,
|
|
20
|
+
)
|
|
21
|
+
from sqlalchemy.dialects.postgresql import UUID
|
|
22
|
+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
|
23
|
+
from sqlalchemy.orm import DeclarativeBase, relationship
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Base(DeclarativeBase):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class User(Base):
|
|
31
|
+
__tablename__ = "users"
|
|
32
|
+
|
|
33
|
+
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
34
|
+
email = Column(String(255), unique=True, nullable=False, index=True)
|
|
35
|
+
name = Column(String(255))
|
|
36
|
+
avatar_url = Column(String(500))
|
|
37
|
+
provider = Column(String(50)) # "github", "google", "email"
|
|
38
|
+
provider_id = Column(String(255)) # External provider user ID
|
|
39
|
+
password_hash = Column(String(255), nullable=True) # For email/password auth
|
|
40
|
+
created_at = Column(DateTime, default=datetime.utcnow)
|
|
41
|
+
last_login = Column(DateTime)
|
|
42
|
+
is_active = Column(Boolean, default=True)
|
|
43
|
+
|
|
44
|
+
sessions = relationship("Session", back_populates="user")
|
|
45
|
+
projects = relationship("Project", back_populates="user")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Project(Base):
|
|
49
|
+
__tablename__ = "projects"
|
|
50
|
+
|
|
51
|
+
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
52
|
+
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
|
|
53
|
+
name = Column(String(255), nullable=False)
|
|
54
|
+
description = Column(Text)
|
|
55
|
+
project_dir = Column(String(500), nullable=False)
|
|
56
|
+
created_at = Column(DateTime, default=datetime.utcnow)
|
|
57
|
+
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
58
|
+
|
|
59
|
+
user = relationship("User", back_populates="projects")
|
|
60
|
+
sessions = relationship("Session", back_populates="project")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Session(Base):
|
|
64
|
+
__tablename__ = "sessions"
|
|
65
|
+
|
|
66
|
+
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
67
|
+
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
|
|
68
|
+
project_id = Column(UUID(as_uuid=True), ForeignKey("projects.id"), nullable=True)
|
|
69
|
+
prd_content = Column(Text)
|
|
70
|
+
provider = Column(String(50), default="claude")
|
|
71
|
+
mode = Column(String(50), default="standard")
|
|
72
|
+
status = Column(String(50), default="created") # created, running, paused, completed, failed
|
|
73
|
+
started_at = Column(DateTime, default=datetime.utcnow)
|
|
74
|
+
ended_at = Column(DateTime, nullable=True)
|
|
75
|
+
metadata_json = Column(JSON, default=dict)
|
|
76
|
+
|
|
77
|
+
user = relationship("User", back_populates="sessions")
|
|
78
|
+
project = relationship("Project", back_populates="sessions")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class Secret(Base):
|
|
82
|
+
__tablename__ = "secrets"
|
|
83
|
+
|
|
84
|
+
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
85
|
+
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
|
|
86
|
+
key = Column(String(255), nullable=False)
|
|
87
|
+
encrypted_value = Column(Text, nullable=False) # Fernet encrypted
|
|
88
|
+
created_at = Column(DateTime, default=datetime.utcnow)
|
|
89
|
+
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class AuditLog(Base):
|
|
93
|
+
__tablename__ = "audit_log"
|
|
94
|
+
|
|
95
|
+
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
96
|
+
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
|
|
97
|
+
action = Column(String(100), nullable=False) # "session.start", "file.save", etc.
|
|
98
|
+
resource_type = Column(String(50)) # "session", "file", "secret"
|
|
99
|
+
resource_id = Column(String(255))
|
|
100
|
+
details = Column(JSON, default=dict)
|
|
101
|
+
ip_address = Column(String(45))
|
|
102
|
+
created_at = Column(DateTime, default=datetime.utcnow)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
# Database connection state
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
DATABASE_URL: str | None = None
|
|
110
|
+
engine = None
|
|
111
|
+
async_session_factory: async_sessionmaker | None = None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
async def init_db(database_url: str | None = None) -> bool:
|
|
115
|
+
"""Initialize database connection. Returns False if no URL configured (file-based fallback)."""
|
|
116
|
+
global DATABASE_URL, engine, async_session_factory
|
|
117
|
+
|
|
118
|
+
url = database_url or os.environ.get("DATABASE_URL")
|
|
119
|
+
if not url:
|
|
120
|
+
return False # No database configured, use file-based fallback
|
|
121
|
+
|
|
122
|
+
DATABASE_URL = url
|
|
123
|
+
engine = create_async_engine(url, echo=False)
|
|
124
|
+
async_session_factory = async_sessionmaker(
|
|
125
|
+
engine, class_=AsyncSession, expire_on_commit=False
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
async with engine.begin() as conn:
|
|
129
|
+
await conn.run_sync(Base.metadata.create_all)
|
|
130
|
+
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
async def get_db():
|
|
135
|
+
"""Get database session. Yields None if no database configured."""
|
|
136
|
+
if async_session_factory is None:
|
|
137
|
+
yield None
|
|
138
|
+
return
|
|
139
|
+
async with async_session_factory() as session:
|
|
140
|
+
yield session
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Core -- required for server.py to start
|
|
2
|
+
fastapi>=0.100.0
|
|
3
|
+
uvicorn[standard]>=0.20.0
|
|
4
|
+
pydantic>=2.0.0
|
|
5
|
+
httpx>=0.24.0
|
|
6
|
+
|
|
7
|
+
# Terminal -- PTY-based interactive terminal
|
|
8
|
+
pexpect>=4.8.0
|
|
9
|
+
|
|
10
|
+
# File watcher -- live file-change notifications via WebSocket
|
|
11
|
+
watchdog>=3.0.0
|
|
12
|
+
|
|
13
|
+
# Database (optional) -- only needed for multi-user cloud deployment.
|
|
14
|
+
# Without DATABASE_URL set, the server uses file-based storage.
|
|
15
|
+
sqlalchemy[asyncio]>=2.0.0
|
|
16
|
+
asyncpg>=0.28.0
|
|
17
|
+
aiosqlite>=0.19.0
|
|
18
|
+
alembic>=1.12.0
|
|
19
|
+
|
|
20
|
+
# Auth (optional) -- only needed when DATABASE_URL is configured.
|
|
21
|
+
# Imports are guarded by try/except so the server starts without them.
|
|
22
|
+
python-jose[cryptography]>=3.3.0
|
|
23
|
+
passlib[bcrypt]>=1.7.4
|
|
24
|
+
|
|
25
|
+
# Encryption (optional) -- Fernet encryption for user secrets.
|
|
26
|
+
# Needed only for the /api/secrets endpoints in cloud mode.
|
|
27
|
+
cryptography>=41.0.0
|
package/web-app/server.py
CHANGED
|
@@ -2308,6 +2308,14 @@ async def chat_session(session_id: str, req: ChatRequest) -> JSONResponse:
|
|
|
2308
2308
|
break
|
|
2309
2309
|
# Strip ANSI escape codes for clean display
|
|
2310
2310
|
clean = re.sub(r'\x1b\[[0-9;]*[a-zA-Z]', '', raw_line.rstrip("\n"))
|
|
2311
|
+
# Filter out noisy tool-use output lines from Claude
|
|
2312
|
+
stripped = clean.strip()
|
|
2313
|
+
if stripped in ("[Tool: Read]", "[Tool: Bash]", "[Tool: Write]",
|
|
2314
|
+
"[Tool: Edit]", "[Tool: Grep]", "[Tool: Glob]",
|
|
2315
|
+
"[Result]", "[Thinking]"):
|
|
2316
|
+
continue
|
|
2317
|
+
if not stripped:
|
|
2318
|
+
continue
|
|
2311
2319
|
task.output_lines.append(clean)
|
|
2312
2320
|
proc.stdout.close()
|
|
2313
2321
|
|