tlc-claude-code 1.4.5 → 1.4.7

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.
@@ -101,18 +101,18 @@ services:
101
101
  working_dir: /project
102
102
  command: >
103
103
  sh -c "
104
- cd /tlc/dashboard && npm install && npm run build &&
105
- cd /tlc/server && npm install &&
106
- cd /project && node /tlc/server/index.js --proxy-only
104
+ npm install -g tlc-claude-code &&
105
+ TLC_DIR=$$(npm root -g)/tlc-claude-code &&
106
+ cd /project && node $$TLC_DIR/server/index.js --proxy-only
107
107
  "
108
108
  environment:
109
109
  - TLC_PORT=3147
110
110
  - TLC_PROXY_ONLY=true
111
111
  - TLC_APP_PORT=5001
112
+ - TLC_AUTH=false
112
113
  ports:
113
114
  - "${DASHBOARD_PORT:-3147}:3147"
114
115
  volumes:
115
- - ./server:/tlc/server
116
116
  - ..:/project
117
117
  depends_on:
118
118
  - app
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tlc-claude-code",
3
- "version": "1.4.5",
3
+ "version": "1.4.7",
4
4
  "description": "TLC - Test Led Coding for Claude Code",
5
5
  "bin": {
6
6
  "tlc": "./bin/tlc.js",
@@ -55,5 +55,8 @@
55
55
  "@playwright/test": "^1.58.1",
56
56
  "playwright": "^1.58.1",
57
57
  "text-to-image": "^8.0.1"
58
+ },
59
+ "dependencies": {
60
+ "cookie-parser": "^1.4.7"
58
61
  }
59
62
  }
@@ -951,7 +951,9 @@
951
951
  <span id="status-text">Connecting...</span>
952
952
  </div>
953
953
  <span style="color: #30363d">|</span>
954
- <span class="version">v1.4.2</span>
954
+ <span class="version">v1.4.5</span>
955
+ <span style="color: #30363d">|</span>
956
+ <button id="logout-btn" onclick="logout()" style="background: transparent; border: 1px solid #30363d; color: #8b949e; padding: 4px 12px; border-radius: 4px; cursor: pointer; font-size: 12px; display: none;">Logout</button>
955
957
  </div>
956
958
  </header>
957
959
 
@@ -1342,6 +1344,33 @@
1342
1344
  let appPort = 5001;
1343
1345
 
1344
1346
  const views = ['projects', 'tasks', 'chat', 'agents', 'preview', 'logs', 'github', 'health', 'router', 'settings'];
1347
+
1348
+ // Auth functions
1349
+ async function checkAuth() {
1350
+ try {
1351
+ const res = await fetch('/api/auth/status');
1352
+ const data = await res.json();
1353
+ if (data.authEnabled) {
1354
+ document.getElementById('logout-btn').style.display = 'inline-block';
1355
+ }
1356
+ } catch (e) {
1357
+ console.log('Auth check failed:', e);
1358
+ }
1359
+ }
1360
+
1361
+ async function logout() {
1362
+ try {
1363
+ await fetch('/api/auth/logout', { method: 'POST' });
1364
+ window.location.href = '/login.html';
1365
+ } catch (e) {
1366
+ console.error('Logout failed:', e);
1367
+ window.location.href = '/login.html';
1368
+ }
1369
+ }
1370
+
1371
+ // Check auth on load
1372
+ checkAuth();
1373
+
1345
1374
  const viewTitles = {
1346
1375
  projects: '📁 Projects',
1347
1376
  tasks: '📋 Tasks',
@@ -1822,19 +1851,63 @@
1822
1851
  if (data.coverage) {
1823
1852
  document.getElementById('stat-coverage').textContent = data.coverage + '%';
1824
1853
  }
1854
+ // Update app port for preview
1855
+ if (data.appPort) {
1856
+ updateAppPort(data.appPort);
1857
+ }
1825
1858
  } catch (e) {
1826
1859
  console.error('Failed to load stats:', e);
1827
1860
  }
1828
1861
  }
1829
1862
 
1863
+ async function refreshProject() {
1864
+ try {
1865
+ const res = await fetch('/api/project');
1866
+ const data = await res.json();
1867
+
1868
+ document.getElementById('project-name').textContent = data.name || 'Unknown Project';
1869
+ document.getElementById('project-desc').textContent = data.description || data.projectDir || '';
1870
+
1871
+ if (data.phase && data.phaseName) {
1872
+ document.getElementById('phase-badge').textContent = `Phase ${data.phase}: ${data.phaseName}`;
1873
+ } else if (data.branch) {
1874
+ document.getElementById('phase-badge').textContent = `Branch: ${data.branch}`;
1875
+ }
1876
+
1877
+ // Update progress bar
1878
+ const progress = data.tasks?.progress || 0;
1879
+ document.getElementById('progress-fill').style.width = `${progress}%`;
1880
+
1881
+ // Update stats if available
1882
+ if (data.tasks) {
1883
+ document.getElementById('stat-passing').textContent = data.tasks.done || 0;
1884
+ document.getElementById('stat-failing').textContent = (data.tasks.total - data.tasks.done) || 0;
1885
+ }
1886
+ } catch (e) {
1887
+ console.error('Failed to load project:', e);
1888
+ }
1889
+ }
1890
+
1830
1891
  async function refreshTasks() {
1831
1892
  try {
1832
1893
  const res = await fetch('/api/tasks');
1833
- const data = await res.json();
1894
+ const json = await res.json();
1895
+ // Handle both { items: [...] } and direct array
1896
+ const data = json.items || json || [];
1897
+
1898
+ // Normalize status values: done -> completed, working -> in_progress
1899
+ const normalizeStatus = (s) => {
1900
+ if (s === 'done' || s === 'complete') return 'completed';
1901
+ if (s === 'working') return 'in_progress';
1902
+ if (s === 'available') return 'pending';
1903
+ return s || 'pending';
1904
+ };
1905
+
1906
+ const tasks = data.map(t => ({ ...t, status: normalizeStatus(t.status) }));
1834
1907
 
1835
- const pending = data.filter(t => t.status === 'pending');
1836
- const inProgress = data.filter(t => t.status === 'in_progress');
1837
- const completed = data.filter(t => t.status === 'completed');
1908
+ const pending = tasks.filter(t => t.status === 'pending');
1909
+ const inProgress = tasks.filter(t => t.status === 'in_progress');
1910
+ const completed = tasks.filter(t => t.status === 'completed');
1838
1911
 
1839
1912
  document.getElementById('tasks-pending').innerHTML = pending.map(t => `
1840
1913
  <div class="task-item">
@@ -1959,6 +2032,7 @@
1959
2032
 
1960
2033
  // Refresh All
1961
2034
  function refreshAll() {
2035
+ refreshProject();
1962
2036
  refreshStats();
1963
2037
  refreshTasks();
1964
2038
  refreshGitHub();
@@ -0,0 +1,262 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>TLC Dashboard - Login</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
16
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
17
+ min-height: 100vh;
18
+ display: flex;
19
+ align-items: center;
20
+ justify-content: center;
21
+ color: #e4e4e7;
22
+ }
23
+
24
+ .login-container {
25
+ background: rgba(30, 30, 46, 0.95);
26
+ border: 1px solid #3f3f5a;
27
+ border-radius: 12px;
28
+ padding: 40px;
29
+ width: 100%;
30
+ max-width: 400px;
31
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
32
+ }
33
+
34
+ .logo {
35
+ text-align: center;
36
+ margin-bottom: 30px;
37
+ }
38
+
39
+ .logo pre {
40
+ font-size: 10px;
41
+ line-height: 1.1;
42
+ color: #60a5fa;
43
+ font-family: monospace;
44
+ }
45
+
46
+ .logo h1 {
47
+ font-size: 18px;
48
+ color: #a5b4fc;
49
+ margin-top: 10px;
50
+ font-weight: 500;
51
+ }
52
+
53
+ .form-group {
54
+ margin-bottom: 20px;
55
+ }
56
+
57
+ label {
58
+ display: block;
59
+ margin-bottom: 8px;
60
+ font-size: 14px;
61
+ color: #9ca3af;
62
+ }
63
+
64
+ input {
65
+ width: 100%;
66
+ padding: 12px 16px;
67
+ background: #27273a;
68
+ border: 1px solid #3f3f5a;
69
+ border-radius: 8px;
70
+ color: #e4e4e7;
71
+ font-size: 16px;
72
+ transition: border-color 0.2s, box-shadow 0.2s;
73
+ }
74
+
75
+ input:focus {
76
+ outline: none;
77
+ border-color: #60a5fa;
78
+ box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.2);
79
+ }
80
+
81
+ input::placeholder {
82
+ color: #6b7280;
83
+ }
84
+
85
+ button {
86
+ width: 100%;
87
+ padding: 14px;
88
+ background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
89
+ border: none;
90
+ border-radius: 8px;
91
+ color: white;
92
+ font-size: 16px;
93
+ font-weight: 600;
94
+ cursor: pointer;
95
+ transition: transform 0.2s, box-shadow 0.2s;
96
+ }
97
+
98
+ button:hover {
99
+ transform: translateY(-1px);
100
+ box-shadow: 0 4px 20px rgba(59, 130, 246, 0.4);
101
+ }
102
+
103
+ button:active {
104
+ transform: translateY(0);
105
+ }
106
+
107
+ button:disabled {
108
+ opacity: 0.6;
109
+ cursor: not-allowed;
110
+ transform: none;
111
+ }
112
+
113
+ .error {
114
+ background: rgba(239, 68, 68, 0.15);
115
+ border: 1px solid rgba(239, 68, 68, 0.3);
116
+ color: #f87171;
117
+ padding: 12px;
118
+ border-radius: 8px;
119
+ margin-bottom: 20px;
120
+ font-size: 14px;
121
+ display: none;
122
+ }
123
+
124
+ .error.visible {
125
+ display: block;
126
+ }
127
+
128
+ .footer {
129
+ text-align: center;
130
+ margin-top: 24px;
131
+ font-size: 12px;
132
+ color: #6b7280;
133
+ }
134
+
135
+ .footer a {
136
+ color: #60a5fa;
137
+ text-decoration: none;
138
+ }
139
+
140
+ .setup-hint {
141
+ background: rgba(96, 165, 250, 0.1);
142
+ border: 1px solid rgba(96, 165, 250, 0.2);
143
+ border-radius: 8px;
144
+ padding: 16px;
145
+ margin-bottom: 24px;
146
+ font-size: 13px;
147
+ line-height: 1.5;
148
+ }
149
+
150
+ .setup-hint code {
151
+ background: rgba(0, 0, 0, 0.3);
152
+ padding: 2px 6px;
153
+ border-radius: 4px;
154
+ font-family: monospace;
155
+ font-size: 12px;
156
+ }
157
+ </style>
158
+ </head>
159
+ <body>
160
+ <div class="login-container">
161
+ <div class="logo">
162
+ <pre>
163
+ ████████╗██╗ ██████╗
164
+ ╚══██╔══╝██║ ██╔════╝
165
+ ██║ ██║ ██║
166
+ ██║ ██║ ██║
167
+ ██║ ███████╗╚██████╗
168
+ ╚═╝ ╚══════╝ ╚═════╝</pre>
169
+ <h1>Dashboard Login</h1>
170
+ </div>
171
+
172
+ <div id="setup-hint" class="setup-hint" style="display: none;">
173
+ <strong>Setup Required</strong><br>
174
+ Set credentials in <code>.tlc.json</code>:
175
+ <pre style="margin-top: 8px; background: rgba(0,0,0,0.3); padding: 8px; border-radius: 4px;">
176
+ {
177
+ "auth": {
178
+ "adminEmail": "you@example.com",
179
+ "adminPassword": "your-password"
180
+ }
181
+ }</pre>
182
+ Or use environment variables:<br>
183
+ <code>TLC_ADMIN_EMAIL</code> / <code>TLC_ADMIN_PASSWORD</code>
184
+ </div>
185
+
186
+ <div id="error" class="error"></div>
187
+
188
+ <form id="login-form">
189
+ <div class="form-group">
190
+ <label for="email">Email</label>
191
+ <input type="email" id="email" name="email" placeholder="admin@localhost" required>
192
+ </div>
193
+
194
+ <div class="form-group">
195
+ <label for="password">Password</label>
196
+ <input type="password" id="password" name="password" placeholder="Enter your password" required>
197
+ </div>
198
+
199
+ <button type="submit" id="submit-btn">Sign In</button>
200
+ </form>
201
+
202
+ <div class="footer">
203
+ TLC Dev Server &middot; <a href="https://github.com/anthropics/tlc">Documentation</a>
204
+ </div>
205
+ </div>
206
+
207
+ <script>
208
+ const form = document.getElementById('login-form');
209
+ const errorEl = document.getElementById('error');
210
+ const submitBtn = document.getElementById('submit-btn');
211
+ const setupHint = document.getElementById('setup-hint');
212
+
213
+ // Check if this is first visit with failed login
214
+ const urlParams = new URLSearchParams(window.location.search);
215
+ if (urlParams.get('setup') === 'true') {
216
+ setupHint.style.display = 'block';
217
+ }
218
+
219
+ form.addEventListener('submit', async (e) => {
220
+ e.preventDefault();
221
+
222
+ const email = document.getElementById('email').value;
223
+ const password = document.getElementById('password').value;
224
+
225
+ errorEl.classList.remove('visible');
226
+ submitBtn.disabled = true;
227
+ submitBtn.textContent = 'Signing in...';
228
+
229
+ try {
230
+ const res = await fetch('/api/auth/login', {
231
+ method: 'POST',
232
+ headers: { 'Content-Type': 'application/json' },
233
+ body: JSON.stringify({ email, password }),
234
+ });
235
+
236
+ const data = await res.json();
237
+
238
+ if (!res.ok) {
239
+ throw new Error(data.error || 'Login failed');
240
+ }
241
+
242
+ // Redirect to dashboard
243
+ window.location.href = '/';
244
+ } catch (err) {
245
+ errorEl.textContent = err.message;
246
+ errorEl.classList.add('visible');
247
+
248
+ // Show setup hint on first failed login
249
+ if (err.message === 'Invalid credentials') {
250
+ setupHint.style.display = 'block';
251
+ }
252
+ } finally {
253
+ submitBtn.disabled = false;
254
+ submitBtn.textContent = 'Sign In';
255
+ }
256
+ });
257
+
258
+ // Auto-focus email field
259
+ document.getElementById('email').focus();
260
+ </script>
261
+ </body>
262
+ </html>
package/server/index.js CHANGED
@@ -12,6 +12,14 @@ const chokidar = require('chokidar');
12
12
  const { detectProject } = require('./lib/project-detector');
13
13
  const { parsePlan, parseBugs } = require('./lib/plan-parser');
14
14
  const { autoProvision, stopDatabase } = require('./lib/auto-database');
15
+ const {
16
+ createUserStore,
17
+ createAuthMiddleware,
18
+ generateJWT,
19
+ verifyJWT,
20
+ hashPassword,
21
+ verifyPassword,
22
+ } = require('./lib/auth-system');
15
23
 
16
24
  // Handle PGlite WASM crashes gracefully
17
25
  process.on('uncaughtException', (err) => {
@@ -48,6 +56,149 @@ const wss = new WebSocketServer({ server });
48
56
 
49
57
  // Middleware
50
58
  app.use(express.json());
59
+ const cookieParser = require('cookie-parser');
60
+ app.use(cookieParser());
61
+
62
+ // ============================================
63
+ // Authentication Setup
64
+ // ============================================
65
+ const userStore = createUserStore();
66
+ const JWT_SECRET = process.env.TLC_JWT_SECRET || 'tlc-dashboard-secret-change-in-production';
67
+ const AUTH_ENABLED = process.env.TLC_AUTH !== 'false';
68
+
69
+ // Initialize admin user from config or environment
70
+ async function initializeAuth() {
71
+ const tlcConfigPath = path.join(PROJECT_DIR, '.tlc.json');
72
+ let adminEmail = process.env.TLC_ADMIN_EMAIL || 'admin@localhost';
73
+ let adminPassword = process.env.TLC_ADMIN_PASSWORD;
74
+
75
+ // Try to read from .tlc.json
76
+ if (fs.existsSync(tlcConfigPath)) {
77
+ try {
78
+ const config = JSON.parse(fs.readFileSync(tlcConfigPath, 'utf-8'));
79
+ if (config.auth?.adminEmail) adminEmail = config.auth.adminEmail;
80
+ if (config.auth?.adminPassword) adminPassword = config.auth.adminPassword;
81
+ } catch (e) {
82
+ // Ignore parse errors
83
+ }
84
+ }
85
+
86
+ // Create admin user if password is set
87
+ if (adminPassword) {
88
+ try {
89
+ await userStore.createUser({
90
+ email: adminEmail,
91
+ password: adminPassword,
92
+ name: 'Admin',
93
+ role: 'admin',
94
+ }, { skipValidation: true }); // Dev tool - allow simple passwords
95
+ console.log(`[TLC] Admin user initialized: ${adminEmail}`);
96
+ } catch (e) {
97
+ if (!e.message.includes('already registered')) {
98
+ console.error('[TLC] Failed to create admin user:', e.message);
99
+ }
100
+ }
101
+ }
102
+ }
103
+
104
+ // Auth middleware for protected routes
105
+ const authMiddleware = createAuthMiddleware({
106
+ userStore,
107
+ jwtSecret: JWT_SECRET,
108
+ requireAuth: true,
109
+ });
110
+
111
+ // Public paths that don't require auth
112
+ const publicPaths = ['/api/auth/login', '/api/auth/status', '/login.html', '/login'];
113
+
114
+ // Apply auth to API routes (except public paths)
115
+ app.use((req, res, next) => {
116
+ // Skip auth if disabled
117
+ if (!AUTH_ENABLED) return next();
118
+
119
+ // Allow public paths
120
+ if (publicPaths.some(p => req.path === p || req.path.startsWith(p))) {
121
+ return next();
122
+ }
123
+
124
+ // Allow static assets
125
+ if (req.path.match(/\.(js|css|png|jpg|ico|svg|woff|woff2)$/)) {
126
+ return next();
127
+ }
128
+
129
+ // Check for auth cookie or header
130
+ const token = req.cookies?.tlc_token || req.headers.authorization?.replace('Bearer ', '');
131
+
132
+ if (!token) {
133
+ // Redirect browser requests to login, return 401 for API
134
+ if (req.path.startsWith('/api/')) {
135
+ return res.status(401).json({ error: 'Authentication required' });
136
+ }
137
+ return res.redirect('/login.html');
138
+ }
139
+
140
+ // Verify token
141
+ const payload = verifyJWT(token, JWT_SECRET);
142
+ if (!payload) {
143
+ if (req.path.startsWith('/api/')) {
144
+ return res.status(401).json({ error: 'Invalid or expired token' });
145
+ }
146
+ return res.redirect('/login.html');
147
+ }
148
+
149
+ // Attach user to request
150
+ req.user = payload;
151
+ next();
152
+ });
153
+
154
+ // Auth routes
155
+ app.get('/api/auth/status', (req, res) => {
156
+ res.json({ authEnabled: AUTH_ENABLED });
157
+ });
158
+
159
+ app.post('/api/auth/login', async (req, res) => {
160
+ const { email, password } = req.body;
161
+
162
+ if (!email || !password) {
163
+ return res.status(400).json({ error: 'Email and password required' });
164
+ }
165
+
166
+ const user = await userStore.authenticate(email, password);
167
+ if (!user) {
168
+ return res.status(401).json({ error: 'Invalid credentials' });
169
+ }
170
+
171
+ // Generate JWT
172
+ const token = generateJWT(
173
+ { sub: user.id, email: user.email, role: user.role, name: user.name },
174
+ JWT_SECRET,
175
+ { expiresIn: 86400 * 7 } // 7 days
176
+ );
177
+
178
+ // Set cookie
179
+ res.cookie('tlc_token', token, {
180
+ httpOnly: true,
181
+ secure: process.env.NODE_ENV === 'production',
182
+ sameSite: 'lax',
183
+ maxAge: 86400 * 7 * 1000, // 7 days
184
+ });
185
+
186
+ res.json({ success: true, user: { email: user.email, name: user.name, role: user.role } });
187
+ });
188
+
189
+ app.post('/api/auth/logout', (req, res) => {
190
+ res.clearCookie('tlc_token');
191
+ res.json({ success: true });
192
+ });
193
+
194
+ app.get('/api/auth/me', (req, res) => {
195
+ if (!req.user) {
196
+ return res.status(401).json({ error: 'Not authenticated' });
197
+ }
198
+ res.json({ user: req.user });
199
+ });
200
+
201
+ // Serve static files (after auth middleware)
51
202
  app.use(express.static(path.join(__dirname, 'dashboard')));
52
203
 
53
204
  // Broadcast to all WebSocket clients
@@ -203,6 +354,86 @@ function runTests() {
203
354
  }
204
355
 
205
356
  // API Routes
357
+
358
+ // Project info endpoint - returns real project data
359
+ app.get('/api/project', (req, res) => {
360
+ try {
361
+ const { execSync } = require('child_process');
362
+
363
+ // Get project name and description from package.json or .tlc.json
364
+ let projectName = 'Unknown Project';
365
+ let projectDesc = '';
366
+ let version = '';
367
+
368
+ const pkgPath = path.join(PROJECT_DIR, 'package.json');
369
+ if (fs.existsSync(pkgPath)) {
370
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
371
+ projectName = pkg.name || projectName;
372
+ projectDesc = pkg.description || '';
373
+ version = pkg.version || '';
374
+ }
375
+
376
+ const tlcPath = path.join(PROJECT_DIR, '.tlc.json');
377
+ if (fs.existsSync(tlcPath)) {
378
+ const tlc = JSON.parse(fs.readFileSync(tlcPath, 'utf-8'));
379
+ if (tlc.project) projectName = tlc.project;
380
+ if (tlc.description) projectDesc = tlc.description;
381
+ }
382
+
383
+ // Get git info
384
+ let branch = 'unknown';
385
+ let lastCommit = null;
386
+ try {
387
+ branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: PROJECT_DIR, encoding: 'utf-8' }).trim();
388
+ const commitInfo = execSync('git log -1 --pretty=format:"%h|%s|%ar"', { cwd: PROJECT_DIR, encoding: 'utf-8' }).trim();
389
+ const [hash, message, time] = commitInfo.split('|');
390
+ lastCommit = { hash, message, time };
391
+ } catch (e) {
392
+ // Not a git repo
393
+ }
394
+
395
+ // Get phase info
396
+ const plan = parsePlan(PROJECT_DIR);
397
+
398
+ // Count phases from roadmap
399
+ let totalPhases = 0;
400
+ let completedPhases = 0;
401
+ const roadmapPath = path.join(PROJECT_DIR, '.planning', 'ROADMAP.md');
402
+ if (fs.existsSync(roadmapPath)) {
403
+ const content = fs.readFileSync(roadmapPath, 'utf-8');
404
+ const phases = content.match(/##\s+Phase\s+\d+/g) || [];
405
+ totalPhases = phases.length;
406
+ const completed = content.match(/##\s+Phase\s+\d+[^[]*\[x\]/gi) || [];
407
+ completedPhases = completed.length;
408
+ }
409
+
410
+ // Calculate progress
411
+ const tasksDone = plan.tasks?.filter(t => t.status === 'done' || t.status === 'complete').length || 0;
412
+ const tasksTotal = plan.tasks?.length || 0;
413
+ const progress = tasksTotal > 0 ? Math.round((tasksDone / tasksTotal) * 100) : 0;
414
+
415
+ res.json({
416
+ name: projectName,
417
+ description: projectDesc,
418
+ version,
419
+ branch,
420
+ lastCommit,
421
+ phase: plan.currentPhase,
422
+ phaseName: plan.currentPhaseName,
423
+ totalPhases,
424
+ completedPhases,
425
+ tasks: {
426
+ total: tasksTotal,
427
+ done: tasksDone,
428
+ progress
429
+ },
430
+ projectDir: PROJECT_DIR
431
+ });
432
+ } catch (err) {
433
+ res.status(500).json({ error: err.message });
434
+ }
435
+ });
436
+
206
437
  app.get('/api/status', (req, res) => {
207
438
  const bugs = parseBugs(PROJECT_DIR);
208
439
  const plan = parsePlan(PROJECT_DIR);
@@ -834,9 +1065,17 @@ async function main() {
834
1065
  TLC Dev Server
835
1066
  `);
836
1067
 
1068
+ // Initialize authentication
1069
+ await initializeAuth();
1070
+
837
1071
  server.listen(TLC_PORT, () => {
838
1072
  console.log(` Dashboard: http://localhost:${TLC_PORT}`);
839
1073
  console.log(` Share: http://${getLocalIP()}:${TLC_PORT}`);
1074
+ if (AUTH_ENABLED) {
1075
+ console.log(` Auth: ENABLED (set TLC_AUTH=false to disable)`);
1076
+ } else {
1077
+ console.log(` Auth: DISABLED`);
1078
+ }
840
1079
  console.log('');
841
1080
  });
842
1081
 
@@ -318,8 +318,9 @@ function createUserStore() {
318
318
 
319
319
  return {
320
320
  // User methods
321
- async createUser(data) {
322
- if (!validateEmail(data.email)) {
321
+ async createUser(data, options = {}) {
322
+ // Skip email validation for dev setup (allows plain usernames)
323
+ if (!options.skipValidation && !validateEmail(data.email) && data.email.includes('@')) {
323
324
  throw new Error('Invalid email format');
324
325
  }
325
326
 
@@ -331,9 +332,12 @@ function createUserStore() {
331
332
  throw new Error('Email already registered');
332
333
  }
333
334
 
334
- const passwordValidation = validatePassword(data.password);
335
- if (!passwordValidation.valid) {
336
- throw new Error(passwordValidation.errors.join(', '));
335
+ // Skip password validation for dev/config-based setup
336
+ if (!options.skipValidation) {
337
+ const passwordValidation = validatePassword(data.password);
338
+ if (!passwordValidation.valid) {
339
+ throw new Error(passwordValidation.errors.join(', '));
340
+ }
337
341
  }
338
342
 
339
343
  const user = createUser(data);
@@ -3,8 +3,8 @@ import {
3
3
  findOrphanedAgents,
4
4
  resetCleanup,
5
5
  } from './agent-cleanup.js';
6
- import { getAgentRegistry, resetRegistry } from './server/lib/agent-registry.js';
7
- import { STATES } from './server/lib/agent-state.js';
6
+ import { getAgentRegistry, resetRegistry } from './agent-registry.js';
7
+ import { STATES } from './agent-state.js';
8
8
 
9
9
  describe('debug', () => {
10
10
  const BASE_TIME = new Date('2025-01-01T12:00:00Z').getTime();
@@ -53,41 +53,84 @@ function parsePlan(projectDir) {
53
53
 
54
54
  /**
55
55
  * Parse task entries from PLAN.md content
56
- * Supports formats:
56
+ * Supports multiple formats:
57
57
  * ### Task 1: Title [ ]
58
58
  * ### Task 1: Title [>@user]
59
59
  * ### Task 1: Title [x@user]
60
+ * - [ ] Task description
61
+ * - [x] Completed task
62
+ * - [>] In progress task
63
+ * ## Task 1: Title
60
64
  */
61
65
  function parseTasksFromPlan(content) {
62
66
  const tasks = [];
63
- const taskRegex = /###\s+Task\s+(\d+)[:\s]+(.+?)\s*\[([^\]]*)\]/g;
64
67
 
68
+ // Format 1: ### Task N: Title [status]
69
+ const taskRegex1 = /###\s+Task\s+(\d+)[:\s]+(.+?)\s*\[([^\]]*)\]/g;
65
70
  let match;
66
- while ((match = taskRegex.exec(content)) !== null) {
71
+ while ((match = taskRegex1.exec(content)) !== null) {
67
72
  const [, num, title, statusMarker] = match;
73
+ tasks.push(parseTaskEntry(num, title, statusMarker));
74
+ }
75
+
76
+ // Format 2: Checkbox format - [ ] Task or - [x] Task
77
+ if (tasks.length === 0) {
78
+ const checkboxRegex = /^[-*]\s*\[([ x>])\]\s*(.+)$/gm;
79
+ let num = 1;
80
+ while ((match = checkboxRegex.exec(content)) !== null) {
81
+ const [, marker, title] = match;
82
+ // Skip if title looks like a sub-item or criterion
83
+ if (title.match(/^(Has|Should|Must|Can|Is|Are|The)\s/i)) continue;
84
+ const statusMarker = marker === 'x' ? 'x' : marker === '>' ? '>' : ' ';
85
+ tasks.push(parseTaskEntry(num++, title, statusMarker));
86
+ }
87
+ }
88
+
89
+ // Format 3: ## Task N: Title (without status marker)
90
+ if (tasks.length === 0) {
91
+ const taskRegex3 = /##\s+Task\s+(\d+)[:\s]+(.+?)$/gm;
92
+ while ((match = taskRegex3.exec(content)) !== null) {
93
+ const [, num, title] = match;
94
+ tasks.push(parseTaskEntry(num, title, ' '));
95
+ }
96
+ }
97
+
98
+ // Format 4: Numbered list - 1. Task title
99
+ if (tasks.length === 0) {
100
+ const numberedRegex = /^(\d+)\.\s+(.+)$/gm;
101
+ while ((match = numberedRegex.exec(content)) !== null) {
102
+ const [, num, title] = match;
103
+ // Skip if looks like a sub-point
104
+ if (title.length < 10) continue;
105
+ tasks.push(parseTaskEntry(num, title, ' '));
106
+ }
107
+ }
68
108
 
69
- let status = 'available';
70
- let owner = null;
109
+ return tasks;
110
+ }
71
111
 
72
- if (statusMarker.startsWith('x')) {
112
+ function parseTaskEntry(num, title, statusMarker) {
113
+ let status = 'pending';
114
+ let owner = null;
115
+
116
+ if (typeof statusMarker === 'string') {
117
+ if (statusMarker.startsWith('x') || statusMarker === 'x') {
73
118
  status = 'done';
74
119
  const ownerMatch = statusMarker.match(/@(\w+)/);
75
120
  if (ownerMatch) owner = ownerMatch[1];
76
- } else if (statusMarker.startsWith('>')) {
77
- status = 'working';
121
+ } else if (statusMarker.startsWith('>') || statusMarker === '>') {
122
+ status = 'in_progress';
78
123
  const ownerMatch = statusMarker.match(/@(\w+)/);
79
124
  if (ownerMatch) owner = ownerMatch[1];
80
125
  }
81
-
82
- tasks.push({
83
- num: parseInt(num),
84
- title: title.trim(),
85
- status,
86
- owner
87
- });
88
126
  }
89
127
 
90
- return tasks;
128
+ return {
129
+ num: parseInt(num),
130
+ title: title.trim(),
131
+ status,
132
+ owner
133
+ };
91
134
  }
92
135
 
93
136
  /**