dev3000 0.0.42 → 0.0.44

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.
@@ -212,24 +212,20 @@ export async function POST(request: NextRequest) {
212
212
  const { action, replayData, speed = 1 } = body;
213
213
 
214
214
  if (action === 'execute') {
215
- // Generate CDP commands for replay
216
- const cdpCommands = generateCDPCommands(replayData, speed);
217
-
218
- // Try to execute the commands via CDP
215
+ // Execute replay via MCP server's execute_browser_action tool
219
216
  try {
220
- const result = await executeCDPCommands(cdpCommands);
217
+ const result = await executeBrowserActions(replayData, speed);
221
218
  return NextResponse.json({
222
219
  success: true,
223
- message: 'Replay executed successfully',
220
+ message: 'Replay executed successfully via MCP server',
224
221
  result: result,
225
- totalCommands: cdpCommands.length
222
+ totalEvents: result.totalEvents,
223
+ executedEvents: result.executed
226
224
  });
227
225
  } catch (error) {
228
- // Fallback: return commands for manual execution
229
226
  return NextResponse.json({
230
227
  success: false,
231
- message: 'CDP execution failed, returning commands for manual execution',
232
- commands: cdpCommands,
228
+ message: 'MCP server execution failed',
233
229
  error: error instanceof Error ? error.message : 'Unknown error'
234
230
  });
235
231
  }
@@ -245,168 +241,139 @@ export async function POST(request: NextRequest) {
245
241
  }
246
242
  }
247
243
 
248
- interface CDPCommand {
249
- method: string;
250
- params: any;
251
- delay: number;
252
- description: string;
253
- }
254
244
 
255
- function generateCDPCommands(replayData: ReplayData, speed: number): CDPCommand[] {
256
- const events = [
257
- ...replayData.interactions.map(i => ({ ...i, eventType: 'interaction' })),
258
- ...replayData.navigations.map(n => ({ ...n, eventType: 'navigation' }))
259
- ].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
260
-
261
- const commands: CDPCommand[] = [];
262
- const startTime = new Date(replayData.startTime).getTime();
263
-
264
- for (const event of events) {
265
- const eventTime = new Date(event.timestamp).getTime();
266
- const delay = Math.max(0, (eventTime - startTime) / speed);
267
-
268
- if (event.eventType === 'navigation') {
269
- commands.push({
270
- method: 'Page.navigate',
271
- params: { url: event.url },
272
- delay: delay,
273
- description: `Navigate to ${event.url}`
274
- });
275
- } else if (event.eventType === 'interaction') {
276
- if ('type' in event && event.type === 'CLICK' && event.x !== undefined && event.y !== undefined) {
277
- // Mouse down
278
- commands.push({
279
- method: 'Input.dispatchMouseEvent',
280
- params: {
281
- type: 'mousePressed',
282
- x: event.x,
283
- y: event.y,
284
- button: 'left',
285
- clickCount: 1
286
- },
287
- delay: delay,
288
- description: `Click at (${event.x}, ${event.y}) on ${event.target}`
289
- });
290
-
291
- // Mouse up (after small delay)
292
- commands.push({
293
- method: 'Input.dispatchMouseEvent',
294
- params: {
295
- type: 'mouseReleased',
296
- x: event.x,
297
- y: event.y,
298
- button: 'left',
299
- clickCount: 1
300
- },
301
- delay: 50, // 50ms between down and up
302
- description: `Release click at (${event.x}, ${event.y})`
303
- });
304
- } else if ('type' in event && event.type === 'SCROLL' && event.x !== undefined && event.y !== undefined) {
305
- commands.push({
306
- method: 'Runtime.evaluate',
307
- params: {
308
- expression: `window.scrollTo({left: ${event.x}, top: ${event.y}, behavior: 'smooth'})`
309
- },
310
- delay: delay,
311
- description: `Scroll to (${event.x}, ${event.y})`
312
- });
313
- } else if ('type' in event && event.type === 'KEY' && event.key) {
314
- // Key down
315
- commands.push({
316
- method: 'Input.dispatchKeyEvent',
317
- params: {
318
- type: 'keyDown',
319
- key: event.key,
320
- text: event.key.length === 1 ? event.key : undefined
321
- },
322
- delay: delay,
323
- description: `Key down: ${event.key}`
324
- });
325
-
326
- // Key up
327
- commands.push({
328
- method: 'Input.dispatchKeyEvent',
329
- params: {
330
- type: 'keyUp',
331
- key: event.key
332
- },
333
- delay: 50,
334
- description: `Key up: ${event.key}`
335
- });
336
- }
337
- }
338
- }
339
-
340
- return commands;
341
- }
342
-
343
- async function executeCDPCommands(commands: CDPCommand[]): Promise<any> {
344
- const WebSocket = require('ws');
345
-
245
+ async function executeBrowserActions(replayData: ReplayData, speed: number): Promise<any> {
346
246
  try {
347
- // Connect to Chrome DevTools Protocol on port 9222
348
- const response = await fetch('http://localhost:9222/json/version');
349
- const versionData = await response.json();
350
- const wsUrl = versionData.webSocketDebuggerUrl;
247
+ // Get MCP server URL from environment (defaults to local MCP server)
248
+ const mcpServerUrl = process.env.MCP_SERVER_URL || 'http://localhost:3684';
249
+
250
+ const events = [
251
+ ...replayData.interactions.map(i => ({ ...i, eventType: 'interaction' })),
252
+ ...replayData.navigations.map(n => ({ ...n, eventType: 'navigation' }))
253
+ ].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
351
254
 
352
- return new Promise((resolve, reject) => {
353
- const ws = new WebSocket(wsUrl);
354
- let commandIndex = 0;
355
- let nextId = 1;
356
- const results: any[] = [];
255
+ const results: any[] = [];
256
+ const startTime = new Date(replayData.startTime).getTime();
257
+
258
+ // Execute events sequentially with proper timing
259
+ for (let i = 0; i < events.length; i++) {
260
+ const event = events[i];
261
+ const eventTime = new Date(event.timestamp).getTime();
262
+ const delay = Math.max(0, (eventTime - startTime) / speed);
357
263
 
358
- ws.on('open', () => {
359
- // Execute commands sequentially with proper timing
360
- const executeNext = () => {
361
- if (commandIndex >= commands.length) {
362
- ws.close();
363
- resolve({ executed: commandIndex, results });
364
- return;
365
- }
366
-
367
- const command = commands[commandIndex];
368
- const message = {
369
- id: nextId++,
370
- method: command.method,
371
- params: command.params
372
- };
373
-
374
- ws.send(JSON.stringify(message));
375
- commandIndex++;
376
-
377
- // Schedule next command with delay
378
- setTimeout(executeNext, command.delay || 100);
379
- };
380
-
381
- // Start executing commands
382
- executeNext();
383
- });
264
+ // Wait for the calculated delay
265
+ if (delay > 0) {
266
+ await new Promise(resolve => setTimeout(resolve, delay));
267
+ }
384
268
 
385
- ws.on('message', (data: any) => {
386
- try {
387
- const response = JSON.parse(data.toString());
388
- if (response.id) {
389
- results.push(response);
269
+ try {
270
+ let response;
271
+
272
+ if (event.eventType === 'navigation') {
273
+ // Navigate to URL
274
+ response = await fetch(`${mcpServerUrl}/mcp`, {
275
+ method: 'POST',
276
+ headers: { 'Content-Type': 'application/json' },
277
+ body: JSON.stringify({
278
+ jsonrpc: '2.0',
279
+ id: i + 1,
280
+ method: 'tools/call',
281
+ params: {
282
+ name: 'execute_browser_action',
283
+ arguments: {
284
+ action: 'navigate',
285
+ url: event.url
286
+ }
287
+ }
288
+ })
289
+ });
290
+ } else if (event.eventType === 'interaction') {
291
+ if ('type' in event && event.type === 'CLICK' && event.x !== undefined && event.y !== undefined) {
292
+ // Click action
293
+ response = await fetch(`${mcpServerUrl}/mcp`, {
294
+ method: 'POST',
295
+ headers: { 'Content-Type': 'application/json' },
296
+ body: JSON.stringify({
297
+ jsonrpc: '2.0',
298
+ id: i + 1,
299
+ method: 'tools/call',
300
+ params: {
301
+ name: 'execute_browser_action',
302
+ arguments: {
303
+ action: 'click',
304
+ x: event.x,
305
+ y: event.y
306
+ }
307
+ }
308
+ })
309
+ });
310
+ } else if ('type' in event && event.type === 'SCROLL' && event.x !== undefined && event.y !== undefined) {
311
+ // Scroll action
312
+ response = await fetch(`${mcpServerUrl}/mcp`, {
313
+ method: 'POST',
314
+ headers: { 'Content-Type': 'application/json' },
315
+ body: JSON.stringify({
316
+ jsonrpc: '2.0',
317
+ id: i + 1,
318
+ method: 'tools/call',
319
+ params: {
320
+ name: 'execute_browser_action',
321
+ arguments: {
322
+ action: 'scroll',
323
+ x: 0,
324
+ y: 0,
325
+ deltaX: event.x,
326
+ deltaY: event.y
327
+ }
328
+ }
329
+ })
330
+ });
331
+ } else if ('type' in event && event.type === 'KEY' && event.key) {
332
+ // Type action
333
+ response = await fetch(`${mcpServerUrl}/mcp`, {
334
+ method: 'POST',
335
+ headers: { 'Content-Type': 'application/json' },
336
+ body: JSON.stringify({
337
+ jsonrpc: '2.0',
338
+ id: i + 1,
339
+ method: 'tools/call',
340
+ params: {
341
+ name: 'execute_browser_action',
342
+ arguments: {
343
+ action: 'type',
344
+ text: event.key
345
+ }
346
+ }
347
+ })
348
+ });
390
349
  }
391
- } catch (error) {
392
- // Ignore parsing errors for events
393
350
  }
394
- });
395
-
396
- ws.on('error', reject);
397
- ws.on('close', () => {
398
- if (commandIndex < commands.length) {
399
- reject(new Error(`Connection closed after executing ${commandIndex}/${commands.length} commands`));
351
+
352
+ if (response) {
353
+ const result = await response.json();
354
+ results.push({
355
+ event,
356
+ result,
357
+ description: `${event.eventType}: ${event.eventType === 'navigation' ? event.url : ('type' in event ? event.type : 'unknown')}`
358
+ });
400
359
  }
401
- });
402
-
403
- // Timeout after 30 seconds
404
- setTimeout(() => {
405
- ws.close();
406
- reject(new Error('Replay execution timed out'));
407
- }, 30000);
408
- });
360
+
361
+ } catch (error) {
362
+ results.push({
363
+ event,
364
+ error: error instanceof Error ? error.message : 'Unknown error',
365
+ description: `Failed: ${event.eventType}`
366
+ });
367
+ }
368
+ }
369
+
370
+ return {
371
+ executed: results.length,
372
+ results,
373
+ totalEvents: events.length
374
+ };
375
+
409
376
  } catch (error) {
410
- throw new Error(`Failed to connect to CDP: ${error}`);
377
+ throw new Error(`Failed to execute replay via MCP server: ${error}`);
411
378
  }
412
379
  }
@@ -4,12 +4,21 @@ export default function RootLayout({
4
4
  children,
5
5
  }: any) {
6
6
  return (
7
- <html lang="en">
7
+ <html lang="en" className="h-full">
8
8
  <head>
9
9
  <title>🎯 dev3000</title>
10
10
  <script src="https://cdn.tailwindcss.com"></script>
11
+ <script
12
+ dangerouslySetInnerHTML={{
13
+ __html: `
14
+ tailwind.config = {
15
+ darkMode: 'class',
16
+ }
17
+ `,
18
+ }}
19
+ />
11
20
  </head>
12
- <body>{children}</body>
21
+ <body className="h-full">{children}</body>
13
22
  </html>
14
23
  );
15
24
  }
@@ -3,6 +3,51 @@
3
3
  import { useState, useEffect, useRef, useMemo } from 'react';
4
4
  import { LogEntry, LogsApiResponse, ConfigApiResponse, LogFile, LogListResponse } from '@/types';
5
5
 
6
+ // Hook for dark mode with system preference detection
7
+ function useDarkMode() {
8
+ const [darkMode, setDarkMode] = useState<boolean>(() => {
9
+ if (typeof window !== 'undefined') {
10
+ // Check localStorage first
11
+ const saved = localStorage.getItem('dev3000-dark-mode');
12
+ if (saved !== null) {
13
+ return JSON.parse(saved);
14
+ }
15
+ // Default to system preference
16
+ return window.matchMedia('(prefers-color-scheme: dark)').matches;
17
+ }
18
+ return false;
19
+ });
20
+
21
+ useEffect(() => {
22
+ // Save to localStorage
23
+ localStorage.setItem('dev3000-dark-mode', JSON.stringify(darkMode));
24
+
25
+ // Apply dark class to document
26
+ if (darkMode) {
27
+ document.documentElement.classList.add('dark');
28
+ } else {
29
+ document.documentElement.classList.remove('dark');
30
+ }
31
+ }, [darkMode]);
32
+
33
+ // Listen for system theme changes
34
+ useEffect(() => {
35
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
36
+ const handler = (e: MediaQueryListEvent) => {
37
+ // Only update if no explicit choice has been made
38
+ const saved = localStorage.getItem('dev3000-dark-mode');
39
+ if (saved === null) {
40
+ setDarkMode(e.matches);
41
+ }
42
+ };
43
+
44
+ mediaQuery.addEventListener('change', handler);
45
+ return () => mediaQuery.removeEventListener('change', handler);
46
+ }, []);
47
+
48
+ return [darkMode, setDarkMode] as const;
49
+ }
50
+
6
51
  export function parseLogEntries(logContent: string): LogEntry[] {
7
52
  // Split by timestamp pattern - each timestamp starts a new log entry
8
53
  const timestampPattern = /\[(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)\] \[([^\]]+)\] /;
@@ -75,7 +120,7 @@ function URLRenderer({ url, maxLength = 60 }: { url: string, maxLength?: number
75
120
  href={url}
76
121
  target="_blank"
77
122
  rel="noopener noreferrer"
78
- className="text-blue-600 hover:text-blue-800 underline"
123
+ className="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 underline"
79
124
  >
80
125
  {url}
81
126
  </a>
@@ -92,13 +137,13 @@ function URLRenderer({ url, maxLength = 60 }: { url: string, maxLength?: number
92
137
  href={url}
93
138
  target="_blank"
94
139
  rel="noopener noreferrer"
95
- className="text-blue-600 hover:text-blue-800 underline"
140
+ className="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 underline"
96
141
  >
97
142
  {url}
98
143
  </a>
99
144
  <button
100
145
  onClick={() => setIsExpanded(false)}
101
- className="ml-2 text-xs text-gray-500 hover:text-gray-700 px-1 py-0.5 rounded hover:bg-gray-100"
146
+ className="ml-2 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 px-1 py-0.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700"
102
147
  >
103
148
  [collapse]
104
149
  </button>
@@ -109,13 +154,13 @@ function URLRenderer({ url, maxLength = 60 }: { url: string, maxLength?: number
109
154
  href={url}
110
155
  target="_blank"
111
156
  rel="noopener noreferrer"
112
- className="text-blue-600 hover:text-blue-800 underline"
157
+ className="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 underline"
113
158
  >
114
159
  {truncated}
115
160
  </a>
116
161
  <button
117
162
  onClick={() => setIsExpanded(true)}
118
- className="ml-1 text-xs text-gray-500 hover:text-gray-700 px-1 py-0.5 rounded hover:bg-gray-100"
163
+ className="ml-1 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 px-1 py-0.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700"
119
164
  >
120
165
  [expand]
121
166
  </button>
@@ -247,17 +292,17 @@ function ObjectRenderer({ content }: { content: string }) {
247
292
  }
248
293
 
249
294
  function LogEntryComponent({ entry }: { entry: LogEntry }) {
250
- // Parse log type from message patterns
295
+ // Parse log type from message patterns with dark mode support
251
296
  const parseLogType = (message: string) => {
252
- if (message.includes('[INTERACTION]')) return { type: 'INTERACTION', color: 'bg-purple-50 border-purple-200', tag: 'bg-purple-100 text-purple-800' };
253
- if (message.includes('[CONSOLE ERROR]')) return { type: 'ERROR', color: 'bg-red-50 border-red-200', tag: 'bg-red-100 text-red-800' };
254
- if (message.includes('[CONSOLE WARN]')) return { type: 'WARNING', color: 'bg-yellow-50 border-yellow-200', tag: 'bg-yellow-100 text-yellow-800' };
255
- if (message.includes('[SCREENSHOT]')) return { type: 'SCREENSHOT', color: 'bg-blue-50 border-blue-200', tag: 'bg-blue-100 text-blue-800' };
256
- if (message.includes('[NAVIGATION]')) return { type: 'NAVIGATION', color: 'bg-indigo-50 border-indigo-200', tag: 'bg-indigo-100 text-indigo-800' };
257
- if (message.includes('[NETWORK ERROR]')) return { type: 'NETWORK', color: 'bg-red-50 border-red-200', tag: 'bg-red-100 text-red-800' };
258
- if (message.includes('[NETWORK REQUEST]')) return { type: 'NETWORK', color: 'bg-gray-50 border-gray-200', tag: 'bg-gray-100 text-gray-700' };
259
- if (message.includes('[PAGE ERROR]')) return { type: 'ERROR', color: 'bg-red-50 border-red-200', tag: 'bg-red-100 text-red-800' };
260
- return { type: 'DEFAULT', color: 'border-gray-200', tag: 'bg-gray-100 text-gray-700' };
297
+ if (message.includes('[INTERACTION]')) return { type: 'INTERACTION', color: 'bg-purple-50 dark:bg-purple-900/20 border-purple-200 dark:border-purple-800', tag: 'bg-purple-100 dark:bg-purple-800 text-purple-800 dark:text-purple-200' };
298
+ if (message.includes('[CONSOLE ERROR]')) return { type: 'ERROR', color: 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800', tag: 'bg-red-100 dark:bg-red-800 text-red-800 dark:text-red-200' };
299
+ if (message.includes('[CONSOLE WARN]')) return { type: 'WARNING', color: 'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800', tag: 'bg-yellow-100 dark:bg-yellow-800 text-yellow-800 dark:text-yellow-200' };
300
+ if (message.includes('[SCREENSHOT]')) return { type: 'SCREENSHOT', color: 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800', tag: 'bg-blue-100 dark:bg-blue-800 text-blue-800 dark:text-blue-200' };
301
+ if (message.includes('[NAVIGATION]')) return { type: 'NAVIGATION', color: 'bg-indigo-50 dark:bg-indigo-900/20 border-indigo-200 dark:border-indigo-800', tag: 'bg-indigo-100 dark:bg-indigo-800 text-indigo-800 dark:text-indigo-200' };
302
+ if (message.includes('[NETWORK ERROR]')) return { type: 'NETWORK', color: 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800', tag: 'bg-red-100 dark:bg-red-800 text-red-800 dark:text-red-200' };
303
+ if (message.includes('[NETWORK REQUEST]')) return { type: 'NETWORK', color: 'bg-gray-50 dark:bg-gray-800/50 border-gray-200 dark:border-gray-700', tag: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300' };
304
+ if (message.includes('[PAGE ERROR]')) return { type: 'ERROR', color: 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800', tag: 'bg-red-100 dark:bg-red-800 text-red-800 dark:text-red-200' };
305
+ return { type: 'DEFAULT', color: 'border-gray-200 dark:border-gray-700', tag: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300' };
261
306
  };
262
307
 
263
308
  const logTypeInfo = parseLogType(entry.message);
@@ -350,19 +395,19 @@ function LogEntryComponent({ entry }: { entry: LogEntry }) {
350
395
  {/* Table-like layout using CSS Grid */}
351
396
  <div className="grid grid-cols-[auto_auto_1fr] gap-3 items-start">
352
397
  {/* Column 1: Timestamp */}
353
- <div className="text-xs text-gray-500 font-mono whitespace-nowrap">
398
+ <div className="text-xs text-gray-500 dark:text-gray-400 font-mono whitespace-nowrap pt-1">
354
399
  {new Date(entry.timestamp).toLocaleTimeString()}
355
400
  </div>
356
401
 
357
402
  {/* Column 2: Source */}
358
403
  <div className={`px-2 py-1 rounded text-xs font-medium whitespace-nowrap ${
359
- entry.source === 'SERVER' ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800'
404
+ entry.source === 'SERVER' ? 'bg-blue-100 dark:bg-blue-800 text-blue-800 dark:text-blue-200' : 'bg-green-100 dark:bg-green-800 text-green-800 dark:text-green-200'
360
405
  }`}>
361
406
  {entry.source}
362
407
  </div>
363
408
 
364
409
  {/* Column 3: Message content */}
365
- <div className="font-mono text-sm min-w-0">
410
+ <div className="font-mono text-sm min-w-0 text-gray-900 dark:text-gray-100">
366
411
  {renderMessage(entry.message)}
367
412
  </div>
368
413
  </div>
@@ -386,6 +431,7 @@ interface LogsClientProps {
386
431
  }
387
432
 
388
433
  export default function LogsClient({ version }: LogsClientProps) {
434
+ const [darkMode, setDarkMode] = useDarkMode();
389
435
  const [logs, setLogs] = useState<LogEntry[]>([]);
390
436
  const [mode, setMode] = useState<'head' | 'tail'>('tail');
391
437
  const [isAtBottom, setIsAtBottom] = useState(true);
@@ -607,16 +653,38 @@ export default function LogsClient({ version }: LogsClientProps) {
607
653
  }
608
654
  };
609
655
 
610
- const loadReplayPreview = async () => {
611
- try {
612
- const response = await fetch('/api/replay?action=parse');
613
- if (response.ok) {
614
- const data = await response.json();
615
- setReplayEvents(data.interactions || []);
616
- }
617
- } catch (error) {
618
- console.error('Error loading replay preview:', error);
619
- }
656
+ const loadReplayPreview = () => {
657
+ // Extract interactions from current logs instead of making API call
658
+ const interactions = logs
659
+ .filter(log => log.message.includes('[INTERACTION]'))
660
+ .map(log => {
661
+ const match = log.message.match(/\[INTERACTION\] (.+)/);
662
+ if (match) {
663
+ try {
664
+ // Try parsing as JSON (new format)
665
+ const data = JSON.parse(match[1]);
666
+ return {
667
+ timestamp: log.timestamp,
668
+ type: data.type,
669
+ details: data
670
+ };
671
+ } catch {
672
+ // Fallback to old format parsing
673
+ const oldMatch = match[1].match(/(CLICK|TAP|SCROLL|KEY) (.+)/);
674
+ if (oldMatch) {
675
+ return {
676
+ timestamp: log.timestamp,
677
+ type: oldMatch[1],
678
+ details: oldMatch[2]
679
+ };
680
+ }
681
+ }
682
+ }
683
+ return null;
684
+ })
685
+ .filter(Boolean);
686
+
687
+ setReplayEvents(interactions);
620
688
  };
621
689
 
622
690
  const handleRotateLog = async () => {
@@ -674,15 +742,15 @@ export default function LogsClient({ version }: LogsClientProps) {
674
742
  }, [logs, filters]);
675
743
 
676
744
  return (
677
- <div className="h-screen bg-gray-50 flex flex-col">
745
+ <div className="h-screen bg-gray-50 dark:bg-gray-900 flex flex-col transition-colors">
678
746
  {/* Header - Fixed */}
679
- <div className="bg-white shadow-sm border-b flex-none z-10">
747
+ <div className="bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700 flex-none z-10">
680
748
  <div className="max-w-7xl mx-auto px-4 py-3">
681
749
  <div className="flex items-center justify-between">
682
750
  <div className="flex items-center gap-2 sm:gap-4">
683
751
  <div className="flex items-center gap-1">
684
- <h1 className="text-xl sm:text-2xl font-bold text-gray-900 whitespace-nowrap">dev3000</h1>
685
- <span className="text-xs text-gray-400 whitespace-nowrap">(v{version})</span>
752
+ <h1 className="text-xl sm:text-2xl font-bold text-gray-900 dark:text-white whitespace-nowrap">dev3000</h1>
753
+ <span className="text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap">(v{version})</span>
686
754
  </div>
687
755
 
688
756
  {/* Log File Selector */}
@@ -690,11 +758,11 @@ export default function LogsClient({ version }: LogsClientProps) {
690
758
  <div className="relative" ref={dropdownRef}>
691
759
  <button
692
760
  onClick={() => setShowLogSelector(!showLogSelector)}
693
- className="flex items-center gap-2 px-3 py-1 text-sm text-gray-600 hover:text-gray-900 hover:bg-gray-50 rounded-md transition-colors"
761
+ className="flex items-center gap-2 px-3 py-1 text-sm text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-gray-700 rounded-md transition-colors"
694
762
  >
695
763
  <span className="font-mono text-xs whitespace-nowrap">
696
764
  {isInitialLoading && !currentLogFile ? (
697
- <div className="h-4 bg-gray-200 rounded animate-pulse" style={{width: '220px'}} />
765
+ <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" style={{width: '220px'}} />
698
766
  ) : (
699
767
  currentLogFile ? currentLogFile.split('/').pop() : 'dev3000.log'
700
768
  )}
@@ -711,9 +779,9 @@ export default function LogsClient({ version }: LogsClientProps) {
711
779
 
712
780
  {/* Dropdown */}
713
781
  {showLogSelector && availableLogs.length > 1 && (
714
- <div className="absolute top-full left-0 mt-1 bg-white border border-gray-200 rounded-md shadow-lg z-20 min-w-80">
782
+ <div className="absolute top-full left-0 mt-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-md shadow-lg z-20 min-w-80">
715
783
  <div className="py-1 max-h-60 overflow-y-auto">
716
- <div className="px-3 py-2 text-xs font-medium text-gray-500 border-b">
784
+ <div className="px-3 py-2 text-xs font-medium text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-600">
717
785
  {projectName} logs ({availableLogs.length})
718
786
  </div>
719
787
  {availableLogs.map((logFile) => (
@@ -919,6 +987,25 @@ export default function LogsClient({ version }: LogsClientProps) {
919
987
  Tail
920
988
  </button>
921
989
  </div>
990
+
991
+ {/* Dark Mode Toggle */}
992
+ <button
993
+ onClick={() => setDarkMode(!darkMode)}
994
+ className="p-2 rounded-md text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
995
+ title={darkMode ? 'Switch to light mode' : 'Switch to dark mode'}
996
+ >
997
+ {darkMode ? (
998
+ // Sun icon for light mode
999
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1000
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
1001
+ </svg>
1002
+ ) : (
1003
+ // Moon icon for dark mode
1004
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1005
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
1006
+ </svg>
1007
+ )}
1008
+ </button>
922
1009
  </div>
923
1010
  </div>
924
1011
  </div>
@@ -938,15 +1025,15 @@ export default function LogsClient({ version }: LogsClientProps) {
938
1025
  </div>
939
1026
  ) : logs.length === 0 ? (
940
1027
  <div className="text-center py-12">
941
- <div className="text-gray-400 text-lg">📝 No logs yet</div>
942
- <div className="text-gray-500 text-sm mt-2">
1028
+ <div className="text-gray-400 dark:text-gray-500 text-lg">📝 No logs yet</div>
1029
+ <div className="text-gray-500 dark:text-gray-400 text-sm mt-2">
943
1030
  Logs will appear here as your development server runs
944
1031
  </div>
945
1032
  </div>
946
1033
  ) : filteredLogs.length === 0 ? (
947
1034
  <div className="text-center py-12">
948
- <div className="text-gray-400 text-lg">🔍 No logs match current filters</div>
949
- <div className="text-gray-500 text-sm mt-2">
1035
+ <div className="text-gray-400 dark:text-gray-500 text-lg">🔍 No logs match current filters</div>
1036
+ <div className="text-gray-500 dark:text-gray-400 text-sm mt-2">
950
1037
  Try adjusting your filter settings to see more logs
951
1038
  </div>
952
1039
  </div>
@@ -962,17 +1049,17 @@ export default function LogsClient({ version }: LogsClientProps) {
962
1049
  </div>
963
1050
 
964
1051
  {/* Footer - Fixed */}
965
- <div className="border-t border-gray-200 bg-gray-50 flex-none">
1052
+ <div className="border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 flex-none">
966
1053
  <div className="max-w-7xl mx-auto px-4 py-3 flex items-center justify-between">
967
1054
  <div className="flex items-center gap-3">
968
1055
  {isLoadingNew && (
969
1056
  <div className="flex items-center gap-1">
970
1057
  <div className="w-3 h-3 border border-gray-300 border-t-blue-500 rounded-full animate-spin"></div>
971
- <span className="text-xs text-gray-500">Loading...</span>
1058
+ <span className="text-xs text-gray-500 dark:text-gray-400">Loading...</span>
972
1059
  </div>
973
1060
  )}
974
1061
  {!isLoadingNew && lastFetched && (
975
- <span className="text-xs text-gray-400 font-mono">
1062
+ <span className="text-xs text-gray-400 dark:text-gray-500 font-mono">
976
1063
  Last updated {lastFetched.toLocaleTimeString()}
977
1064
  </span>
978
1065
  )}
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "dev3000",
3
- "version": "0.0.42",
3
+ "version": "0.0.44",
4
4
  "description": "AI-powered development tools with browser monitoring and MCP server integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
8
- "dev3000": "dist/cli.js"
8
+ "dev3000": "dist/cli.js",
9
+ "d3k": "dist/cli.js"
9
10
  },
10
11
  "files": [
11
12
  "dist/",