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.
Files changed (46) hide show
  1. package/SKILL.md +2 -2
  2. package/VERSION +1 -1
  3. package/bin/postinstall.js +29 -0
  4. package/dashboard/__init__.py +1 -1
  5. package/docs/INSTALLATION.md +1 -1
  6. package/mcp/__init__.py +1 -1
  7. package/package.json +11 -2
  8. package/web-app/Dockerfile +59 -0
  9. package/web-app/alembic.ini +43 -0
  10. package/web-app/auth.py +249 -0
  11. package/web-app/crypto.py +83 -0
  12. package/web-app/deploy/k8s/purple-lab/configmap.yaml +8 -0
  13. package/web-app/deploy/k8s/purple-lab/deployment.yaml +69 -0
  14. package/web-app/deploy/k8s/purple-lab/hpa.yaml +24 -0
  15. package/web-app/deploy/k8s/purple-lab/ingress.yaml +30 -0
  16. package/web-app/deploy/k8s/purple-lab/networkpolicy.yaml +82 -0
  17. package/web-app/deploy/k8s/purple-lab/pdb.yaml +11 -0
  18. package/web-app/deploy/k8s/purple-lab/postgres.yaml +84 -0
  19. package/web-app/deploy/k8s/purple-lab/pvc.yaml +10 -0
  20. package/web-app/deploy/k8s/purple-lab/secret.yaml +13 -0
  21. package/web-app/deploy/k8s/purple-lab/service.yaml +13 -0
  22. package/web-app/deploy/k8s/purple-lab/serviceaccount.yaml +7 -0
  23. package/web-app/dist/assets/{Badge-CnWBUi7C.js → Badge-COZdpB3a.js} +1 -1
  24. package/web-app/dist/assets/{Button-5ThWFbkO.js → Button-BBb9-LaD.js} +1 -1
  25. package/web-app/dist/assets/{Card-CcTmaOCN.js → Card-k2U78J9a.js} +1 -1
  26. package/web-app/dist/assets/{HomePage-Dx4Ae0hu.js → HomePage-BsDnbkbC.js} +1 -1
  27. package/web-app/dist/assets/{LoginPage-CRffqZNo.js → LoginPage-DbI9D-2B.js} +1 -1
  28. package/web-app/dist/assets/{NotFoundPage-B1QZ92yR.js → NotFoundPage-Cpvhx05J.js} +1 -1
  29. package/web-app/dist/assets/{ProjectPage-BVnDGxXk.js → ProjectPage-D1TEIKzX.js} +12 -12
  30. package/web-app/dist/assets/{ProjectsPage-2Fi6cKB-.js → ProjectsPage-D07q5yXi.js} +1 -1
  31. package/web-app/dist/assets/{SettingsPage-DOzGoyLv.js → SettingsPage-rj4gED1n.js} +1 -1
  32. package/web-app/dist/assets/{TemplatesPage-B-f1Gfbg.js → TemplatesPage-zBW2RQne.js} +1 -1
  33. package/web-app/dist/assets/{TerminalOutput-DrKIbiB8.js → TerminalOutput-D3phVPIe.js} +1 -1
  34. package/web-app/dist/assets/{arrow-left-CFG0TEkb.js → arrow-left-Al_UelcY.js} +1 -1
  35. package/web-app/dist/assets/{clock-C-GPrW5k.js → clock-DSbHSd8C.js} +1 -1
  36. package/web-app/dist/assets/{external-link-ujbkNBY4.js → external-link-D24QKyPn.js} +1 -1
  37. package/web-app/dist/assets/{index-B8gGcUMo.js → index-BL1cnbvX.js} +2 -2
  38. package/web-app/dist/index.html +1 -1
  39. package/web-app/docker-compose.purple-lab.yml +76 -0
  40. package/web-app/migrations/env.py +103 -0
  41. package/web-app/migrations/script.py.mako +25 -0
  42. package/web-app/migrations/versions/.gitkeep +0 -0
  43. package/web-app/migrations/versions/001_initial_schema.py +118 -0
  44. package/web-app/models.py +140 -0
  45. package/web-app/requirements.txt +27 -0
  46. 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