claw-dashboard 1.8.4 → 2.0.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 (135) hide show
  1. package/.c8rc.json +38 -0
  2. package/.dockerignore +68 -0
  3. package/.github/dependabot.yml +45 -0
  4. package/.github/pull_request_template.md +39 -0
  5. package/.github/workflows/ci.yml +147 -0
  6. package/.github/workflows/release.yml +40 -0
  7. package/.github/workflows/security.yml +84 -0
  8. package/.husky/pre-commit +1 -0
  9. package/.planning/codebase/ARCHITECTURE.md +206 -0
  10. package/.planning/codebase/INTEGRATIONS.md +150 -0
  11. package/.planning/codebase/STACK.md +122 -0
  12. package/.planning/codebase/STRUCTURE.md +201 -0
  13. package/CHANGELOG.md +293 -0
  14. package/CONTRIBUTING.md +378 -0
  15. package/Dockerfile +54 -0
  16. package/FEATURES.md +195 -21
  17. package/README.md +83 -6
  18. package/TODO.md +28 -0
  19. package/build-cjs.js +127 -0
  20. package/cjs-shim.js +13 -0
  21. package/dist/clawdash +1729 -0
  22. package/dist/clawdash.meta.json +2236 -0
  23. package/dist/widgets.cjs +6511 -0
  24. package/docker-compose.yml +67 -0
  25. package/docs/API.md +1050 -0
  26. package/docs/PLUGINS.md +1504 -0
  27. package/esbuild.config.js +158 -0
  28. package/eslint.config.js +56 -0
  29. package/examples/plugins/README.md +122 -0
  30. package/examples/plugins/api-status/index.js +294 -0
  31. package/examples/plugins/api-status/plugin.json +19 -0
  32. package/examples/plugins/hello-world/index.js +104 -0
  33. package/examples/plugins/hello-world/plugin.json +15 -0
  34. package/examples/plugins/system-metrics-chart/index.js +339 -0
  35. package/examples/plugins/system-metrics-chart/plugin.json +18 -0
  36. package/examples/plugins/weather-widget/index.js +136 -0
  37. package/examples/plugins/weather-widget/plugin.json +19 -0
  38. package/index.cjs +23005 -0
  39. package/index.js +5331 -512
  40. package/jest.config.js +11 -0
  41. package/man/clawdash.1 +276 -0
  42. package/package.json +54 -5
  43. package/schemas/plugin-manifest.json +128 -0
  44. package/scripts/release.js +595 -0
  45. package/src/alerts.js +693 -0
  46. package/src/auto-save.js +584 -0
  47. package/src/cache.js +390 -0
  48. package/src/checksum.js +146 -0
  49. package/src/cli/args.js +110 -0
  50. package/src/cli/export-schedule.js +423 -0
  51. package/src/cli/export-snapshot.js +138 -0
  52. package/src/cli/help.js +69 -0
  53. package/src/cli/import-snapshot.js +230 -0
  54. package/src/cli/index.js +14 -0
  55. package/src/cli/list-templates.js +69 -0
  56. package/src/cli/validate-config.js +76 -0
  57. package/src/cli/validate-plugin.js +187 -0
  58. package/src/cli/version.js +15 -0
  59. package/src/config-validator.js +586 -0
  60. package/src/config-watcher.js +401 -0
  61. package/src/config.js +684 -0
  62. package/src/container-detector.js +499 -0
  63. package/src/database.js +734 -0
  64. package/src/differential-render.js +327 -0
  65. package/src/errors.js +169 -0
  66. package/src/export-scheduler.js +581 -0
  67. package/src/gateway-manager.js +685 -0
  68. package/src/hints.js +371 -0
  69. package/src/loading-states.js +509 -0
  70. package/src/logger.js +185 -0
  71. package/src/memory-pressure.js +467 -0
  72. package/src/performance-monitor.js +374 -0
  73. package/src/plugin-errors.js +586 -0
  74. package/src/plugin-manifest-validator.js +219 -0
  75. package/src/plugin-reload.js +481 -0
  76. package/src/plugin-scaffold.js +1941 -0
  77. package/src/plugin-validator.js +284 -0
  78. package/src/retry.js +218 -0
  79. package/src/security.js +860 -0
  80. package/src/snapshot.js +372 -0
  81. package/src/splash.js +115 -0
  82. package/src/theme-selector.js +411 -0
  83. package/src/themes.js +874 -0
  84. package/src/transitions.js +534 -0
  85. package/src/types.d.ts +174 -0
  86. package/src/validation.js +926 -0
  87. package/src/web-server.js +885 -0
  88. package/src/widgets/builtin-widgets.js +1057 -0
  89. package/src/widgets/config-processor.js +401 -0
  90. package/src/widgets/dependency-resolver.js +596 -0
  91. package/src/widgets/index.js +79 -0
  92. package/src/widgets/plugin-api.js +845 -0
  93. package/src/widgets/widget-error-boundary.js +773 -0
  94. package/src/widgets/widget-error-isolation.js +551 -0
  95. package/src/widgets/widget-loader.js +1437 -0
  96. package/src/workers/system-worker.js +130 -0
  97. package/src/workers/worker-pool.js +633 -0
  98. package/tests/alerts.test.js +437 -0
  99. package/tests/auto-save.test.js +529 -0
  100. package/tests/cache.test.js +317 -0
  101. package/tests/cli.test.js +351 -0
  102. package/tests/command-palette.test.js +320 -0
  103. package/tests/config-processor.test.js +452 -0
  104. package/tests/config-validator.test.js +710 -0
  105. package/tests/config-watcher.test.js +594 -0
  106. package/tests/config.test.js +755 -0
  107. package/tests/database.test.js +438 -0
  108. package/tests/errors.test.js +189 -0
  109. package/tests/example-plugins.test.js +624 -0
  110. package/tests/integration.test.js +1321 -0
  111. package/tests/loading-states.test.js +300 -0
  112. package/tests/manifest-validation-on-load.test.js +671 -0
  113. package/tests/performance-monitor.test.js +190 -0
  114. package/tests/plugin-api-rate-limit.test.js +302 -0
  115. package/tests/plugin-errors.test.js +311 -0
  116. package/tests/plugin-lifecycle-e2e.test.js +1036 -0
  117. package/tests/plugin-reload.test.js +764 -0
  118. package/tests/rate-limiter.test.js +539 -0
  119. package/tests/retry.test.js +308 -0
  120. package/tests/security.test.js +411 -0
  121. package/tests/settings-modal.test.js +226 -0
  122. package/tests/theme-selector.test.js +57 -0
  123. package/tests/utils.js +242 -0
  124. package/tests/utils.test.js +317 -0
  125. package/tests/validate-plugin-cli.test.js +359 -0
  126. package/tests/validation.test.js +837 -0
  127. package/tests/web-server.test.js +646 -0
  128. package/tests/widget-arrange-mode.test.js +183 -0
  129. package/tests/widget-config-hot-reload.test.js +465 -0
  130. package/tests/widget-dependency.test.js +591 -0
  131. package/tests/widget-error-boundary.test.js +749 -0
  132. package/tests/widget-error-isolation.test.js +469 -0
  133. package/tests/widget-integration.test.js +1147 -0
  134. package/tests/widget-refresh-intervals.test.js +284 -0
  135. package/tests/worker-pool.test.js +522 -0
package/src/themes.js ADDED
@@ -0,0 +1,874 @@
1
+ /**
2
+ * Theme definitions for the Claw Dashboard
3
+ * Contains multiple color themes for borders, text, gauges, charts, and alerts
4
+ */
5
+
6
+ import logger from './logger.js';
7
+ import fs from 'fs';
8
+ import { execSync } from 'child_process';
9
+ import os from 'os';
10
+
11
+ // Settings path for theme persistence
12
+ const SETTINGS_PATH = process.env.HOME + '/.openclaw/dashboard-settings.json';
13
+ const THEME_KEY = 'theme';
14
+
15
+ // Theme change listeners
16
+ const themeChangeListeners = new Set();
17
+ let systemThemeWatcher = null;
18
+
19
+ /**
20
+ * Detect system-wide dark/light mode (macOS)
21
+ * Uses AppleInterfaceStyle which returns "Dark" or "" (empty for light)
22
+ * @returns {string|null} 'dark', 'light', or null if unable to detect
23
+ */
24
+ function detectMacOSAppearance() {
25
+ try {
26
+ const result = execSync(
27
+ 'defaults read -g AppleInterfaceStyle 2>/dev/null || echo "Light"',
28
+ { encoding: 'utf8', timeout: 1000 }
29
+ );
30
+ const style = result.trim();
31
+ return style === 'Dark' ? 'dark' : 'light';
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Detect system theme on Linux using freedesktop settings
39
+ * @returns {string|null} 'dark', 'light', or null if unable to detect
40
+ */
41
+ function detectLinuxAppearance() {
42
+ try {
43
+ // Try gsettings for GNOME/GTK-based desktops
44
+ const result = execSync(
45
+ 'gsettings get org.gnome.desktop.interface color-scheme 2>/dev/null || echo "default"',
46
+ { encoding: 'utf8', timeout: 1000 }
47
+ );
48
+ const scheme = result.trim().replace(/'/g, '');
49
+ if (scheme === 'prefer-dark') return 'dark';
50
+ if (scheme === 'prefer-light') return 'light';
51
+
52
+ // Fallback: check GTK theme name
53
+ const themeResult = execSync(
54
+ 'gsettings get org.gnome.desktop.interface gtk-theme 2>/dev/null || echo ""',
55
+ { encoding: 'utf8', timeout: 1000 }
56
+ );
57
+ const theme = themeResult.trim().toLowerCase();
58
+ if (theme.includes('dark')) return 'dark';
59
+ if (theme.includes('light')) return 'light';
60
+
61
+ return null;
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Detect system theme using environment variables and COLORFGBG
69
+ * @returns {string|null} 'dark', 'light', or null
70
+ */
71
+ function detectFromEnvironment() {
72
+ // Check COLORFGBG which some terminals set (e.g., "15;0" means white on black)
73
+ const colorFgBg = process.env.COLORFGBG;
74
+ if (colorFgBg) {
75
+ const parts = colorFgBg.split(';');
76
+ if (parts.length >= 2) {
77
+ // Standard colors: 0-7 are dark, 8-15 are light/bright
78
+ const bgColor = parseInt(parts[1], 10);
79
+ if (bgColor >= 0 && bgColor <= 7) return 'dark'; // Dark background
80
+ if (bgColor >= 8 && bgColor <= 15) return 'light'; // Light background
81
+ }
82
+ }
83
+
84
+ // Check for explicit dark mode env vars
85
+ if (process.env.DARK_MODE === '1' || process.env.THEME === 'dark') {
86
+ return 'dark';
87
+ }
88
+ if (process.env.THEME === 'light') {
89
+ return 'light';
90
+ }
91
+
92
+ return null;
93
+ }
94
+
95
+ /**
96
+ * Detect system theme using multiple methods
97
+ * Falls back to terminal background detection if system detection fails
98
+ * @returns {string} 'light' or 'dark'
99
+ */
100
+ function detectSystemTheme() {
101
+ let theme = null;
102
+
103
+ // Try platform-specific detection first
104
+ const platform = os.platform();
105
+ if (platform === 'darwin') {
106
+ theme = detectMacOSAppearance();
107
+ } else if (platform === 'linux') {
108
+ theme = detectLinuxAppearance();
109
+ }
110
+
111
+ // Fall back to environment detection
112
+ if (!theme) {
113
+ theme = detectFromEnvironment();
114
+ }
115
+
116
+ // Final fallback to terminal detection
117
+ if (!theme) {
118
+ theme = detectTerminalBackground();
119
+ }
120
+
121
+ return theme;
122
+ }
123
+
124
+ /**
125
+ * Start watching for system theme changes (macOS and Linux)
126
+ * macOS: Uses polling every 2 seconds via AppleInterfaceStyle
127
+ * Linux: Uses fs.watch on dconf database for efficient monitoring
128
+ * @param {Function} callback - Called when theme changes with new theme ('dark' or 'light')
129
+ * @returns {Object|null} Watcher object with stop() method, or null if not supported
130
+ */
131
+ function startSystemThemeWatcher(callback) {
132
+ const platform = os.platform();
133
+
134
+ if (platform === 'darwin') {
135
+ return startMacOSThemeWatcher(callback);
136
+ } else if (platform === 'linux') {
137
+ return startLinuxThemeWatcher(callback);
138
+ }
139
+
140
+ logger.debug('System theme watching not supported on this platform');
141
+ return null;
142
+ }
143
+
144
+ /**
145
+ * macOS theme watcher using polling
146
+ * @param {Function} callback - Called when theme changes
147
+ * @returns {Object} Watcher object with stop() method
148
+ */
149
+ function startMacOSThemeWatcher(callback) {
150
+ let lastTheme = detectMacOSAppearance();
151
+
152
+ // Poll every 2 seconds for theme changes
153
+ const intervalId = setInterval(() => {
154
+ const currentTheme = detectMacOSAppearance();
155
+ if (currentTheme && currentTheme !== lastTheme) {
156
+ logger.info(`System theme changed: ${lastTheme} -> ${currentTheme}`);
157
+ lastTheme = currentTheme;
158
+ callback(currentTheme);
159
+ }
160
+ }, 2000);
161
+
162
+ return {
163
+ stop: () => {
164
+ clearInterval(intervalId);
165
+ logger.debug('macOS theme watcher stopped');
166
+ }
167
+ };
168
+ }
169
+
170
+ /**
171
+ * Linux theme watcher using gsettings monitoring
172
+ * Uses fs.watch on the dconf database file for efficient change detection
173
+ * Falls back to polling if file watching fails
174
+ * @param {Function} callback - Called when theme changes
175
+ * @returns {Object|null} Watcher object with stop() method, or null if not supported
176
+ */
177
+ function startLinuxThemeWatcher(callback) {
178
+ // Get the dconf database file path
179
+ const dconfPath = process.env.DCONF_PROFILE
180
+ ? `/etc/dconf/db/${process.env.DCONF_PROFILE}`
181
+ : `${process.env.HOME}/.config/dconf/user`;
182
+
183
+ let lastTheme = detectLinuxAppearance();
184
+ let watcher = null;
185
+ let intervalId = null;
186
+
187
+ // Try to use file watching for efficient monitoring
188
+ if (fs.existsSync(dconfPath)) {
189
+ try {
190
+ // Watch the dconf database file for changes
191
+ watcher = fs.watch(dconfPath, (eventType) => {
192
+ if (eventType === 'change') {
193
+ const currentTheme = detectLinuxAppearance();
194
+ if (currentTheme && currentTheme !== lastTheme) {
195
+ logger.info(`System theme changed: ${lastTheme} -> ${currentTheme}`);
196
+ lastTheme = currentTheme;
197
+ callback(currentTheme);
198
+ }
199
+ }
200
+ });
201
+
202
+ logger.debug(`Linux theme watcher started via fs.watch on ${dconfPath}`);
203
+ } catch (err) {
204
+ logger.debug(`Failed to watch dconf file: ${err.message}, falling back to polling`);
205
+ watcher = null;
206
+ }
207
+ }
208
+
209
+ // Fallback to polling if file watching failed or file doesn't exist
210
+ if (!watcher) {
211
+ intervalId = setInterval(() => {
212
+ const currentTheme = detectLinuxAppearance();
213
+ if (currentTheme && currentTheme !== lastTheme) {
214
+ logger.info(`System theme changed: ${lastTheme} -> ${currentTheme}`);
215
+ lastTheme = currentTheme;
216
+ callback(currentTheme);
217
+ }
218
+ }, 3000); // Poll every 3 seconds as fallback
219
+ }
220
+
221
+ return {
222
+ stop: () => {
223
+ if (watcher) {
224
+ watcher.close();
225
+ }
226
+ if (intervalId) {
227
+ clearInterval(intervalId);
228
+ }
229
+ logger.debug('Linux theme watcher stopped');
230
+ }
231
+ };
232
+ }
233
+
234
+ /**
235
+ * Register a callback to be called when the theme changes
236
+ * @param {Function} callback - Function(themeName) called when theme changes
237
+ * @returns {Function} Unsubscribe function
238
+ */
239
+ function onThemeChange(callback) {
240
+ themeChangeListeners.add(callback);
241
+ return () => themeChangeListeners.delete(callback);
242
+ }
243
+
244
+ /**
245
+ * Notify all registered listeners of a theme change
246
+ * @param {string} themeName - Name of the new theme
247
+ */
248
+ function notifyThemeChange(themeName) {
249
+ themeChangeListeners.forEach(callback => {
250
+ try {
251
+ callback(themeName);
252
+ } catch (err) {
253
+ logger.debug(`Theme change listener error: ${err.message}`);
254
+ }
255
+ });
256
+ }
257
+
258
+ /**
259
+ * Start automatic system theme detection and switching
260
+ * Only works when current theme is set to 'auto'
261
+ * @returns {Object|null} Watcher object or null if not started
262
+ */
263
+ function startAutoThemeDetection() {
264
+ // Stop existing watcher if any
265
+ if (systemThemeWatcher) {
266
+ systemThemeWatcher.stop();
267
+ systemThemeWatcher = null;
268
+ }
269
+
270
+ // Only start if theme is set to 'auto'
271
+ if (currentThemeName !== 'auto') {
272
+ return null;
273
+ }
274
+
275
+ systemThemeWatcher = startSystemThemeWatcher((newTheme) => {
276
+ // Re-detect and notify listeners
277
+ detectedBackground = newTheme;
278
+ notifyThemeChange('auto');
279
+ });
280
+
281
+ if (systemThemeWatcher) {
282
+ logger.info('Auto theme detection started - watching for system theme changes');
283
+ }
284
+
285
+ return systemThemeWatcher;
286
+ }
287
+
288
+ /**
289
+ * Stop automatic system theme detection
290
+ */
291
+ function stopAutoThemeDetection() {
292
+ if (systemThemeWatcher) {
293
+ systemThemeWatcher.stop();
294
+ systemThemeWatcher = null;
295
+ logger.debug('Auto theme detection stopped');
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Detect terminal background color (light or dark)
301
+ * @returns {string} 'light' or 'dark'
302
+ */
303
+ function detectTerminalBackground() {
304
+ try {
305
+ // Check common terminal environment variables
306
+ const termProgram = process.env.TERM_PROGRAM || '';
307
+ const colorTerm = process.env.COLORTERM || '';
308
+ const term = process.env.TERM || '';
309
+
310
+ // iTerm2 on macOS
311
+ if (termProgram === 'iTerm.app' || termProgram === 'vscode') {
312
+ // Try to get background color from iTerm
313
+ if (termProgram === 'iTerm.app') {
314
+ try {
315
+ const itermBg = execSync(
316
+ 'osascript -e \'tell app "System Events" to tell process "iTerm2" to get value of attribute "AXBackgroundColor" of window 1\'',
317
+ { encoding: 'utf8', timeout: 1000 }
318
+ );
319
+ if (itermBg && itermBg.trim()) {
320
+ // Parse RGB values - if dark, first values will be low
321
+ const rgb = itermBg.match(/\d+/g);
322
+ if (rgb && rgb.length >= 3) {
323
+ const brightness = (parseInt(rgb[0]) + parseInt(rgb[1]) + parseInt(rgb[2])) / (255 * 3);
324
+ return brightness < 0.5 ? 'dark' : 'light';
325
+ }
326
+ }
327
+ } catch {}
328
+ }
329
+
330
+ // VS Code terminal - assume dark
331
+ if (termProgram === 'vscode') {
332
+ return 'dark';
333
+ }
334
+ }
335
+
336
+ // Check for common light terminal indicators
337
+ const lightTermIndicators = ['-light', 'light'];
338
+ const isLightTerm = lightTermIndicators.some(ind =>
339
+ term.toLowerCase().includes(ind) || colorTerm.toLowerCase().includes(ind)
340
+ );
341
+
342
+ if (isLightTerm) {
343
+ return 'light';
344
+ }
345
+
346
+ // Check for common dark terminal indicators
347
+ const darkTermIndicators = ['-256color', 'dark', 'truecolor'];
348
+ const isDarkTerm = darkTermIndicators.some(ind =>
349
+ term.toLowerCase().includes(ind)
350
+ ) || termProgram !== '';
351
+
352
+ if (isDarkTerm) {
353
+ return 'dark';
354
+ }
355
+
356
+ // Check for explicit background color setting in iTerm
357
+ if (process.env.TERM_SESSION_ID) {
358
+ try {
359
+ const profile = execSync(
360
+ 'osascript -e \'tell app "iTerm2" to tell current session of current window to get background color\'',
361
+ { encoding: 'utf8', timeout: 1000 }
362
+ );
363
+ if (profile) {
364
+ const rgb = profile.match(/\d+/g);
365
+ if (rgb && rgb.length >= 3) {
366
+ const brightness = (parseInt(rgb[0]) + parseInt(rgb[1]) + parseInt(rgb[2])) / (255 * 3);
367
+ return brightness < 0.5 ? 'dark' : 'light';
368
+ }
369
+ }
370
+ } catch {}
371
+ }
372
+
373
+ // Check Apple Terminal
374
+ if (termProgram === 'Apple_Terminal') {
375
+ try {
376
+ const bgColor = execSync(
377
+ 'osascript -e \'tell app "Terminal" to get background color of window 1\'',
378
+ { encoding: 'utf8', timeout: 1000 }
379
+ );
380
+ if (bgColor) {
381
+ const rgb = bgColor.match(/\d+/g);
382
+ if (rgb && rgb.length >= 3) {
383
+ const brightness = (parseInt(rgb[0]) + parseInt(rgb[1]) + parseInt(rgb[2])) / (255 * 3);
384
+ return brightness < 0.5 ? 'dark' : 'light';
385
+ }
386
+ }
387
+ } catch {}
388
+ }
389
+
390
+ // Default to dark for unknown terminals (more common for developers)
391
+ return 'dark';
392
+ } catch (err) {
393
+ logger.debug(`Background detection failed: ${err.message}`);
394
+ return 'dark'; // Default to dark
395
+ }
396
+ }
397
+
398
+ // Theme definitions
399
+ const themes = {
400
+ default: {
401
+ name: 'Default',
402
+ colors: {
403
+ // Borders
404
+ border: {
405
+ sessions: 'blue',
406
+ logs: 'cyan',
407
+ cpu: 'cyan',
408
+ memory: 'magenta',
409
+ gpu: 'yellow',
410
+ network: 'brightCyan',
411
+ disk: 'green',
412
+ system: 'gray',
413
+ uptime: 'brightMagenta',
414
+ gateway: 'cyan',
415
+ help: 'brightCyan',
416
+ settings: 'brightCyan',
417
+ modal: 'brightBlue'
418
+ },
419
+ // Text
420
+ text: {
421
+ primary: 'white',
422
+ secondary: 'gray',
423
+ bright: 'brightWhite',
424
+ header: 'brightWhite'
425
+ },
426
+ // Status indicators
427
+ status: {
428
+ active: 'green',
429
+ idle: 'yellow',
430
+ stale: 'gray',
431
+ error: 'red',
432
+ warning: 'yellow',
433
+ success: 'green'
434
+ },
435
+ // Gauges and values
436
+ gauge: {
437
+ low: 'green',
438
+ medium: 'yellow',
439
+ high: 'red',
440
+ critical: 'brightRed'
441
+ },
442
+ // Charts
443
+ chart: {
444
+ line: 'cyan',
445
+ fill: 'blue',
446
+ grid: 'gray'
447
+ },
448
+ // Alerts
449
+ alert: {
450
+ info: 'cyan',
451
+ warning: 'yellow',
452
+ error: 'red',
453
+ success: 'green'
454
+ },
455
+ // Logo and title
456
+ branding: {
457
+ logo: 'brightCyan',
458
+ title: 'brightWhite',
459
+ clock: 'brightCyan'
460
+ },
461
+ // Footer
462
+ footer: {
463
+ bg: 'black',
464
+ fg: 'gray'
465
+ }
466
+ }
467
+ },
468
+
469
+ dark: {
470
+ name: 'Dark',
471
+ colors: {
472
+ // Borders - muted tones
473
+ border: {
474
+ sessions: 'cyan',
475
+ logs: 'blue',
476
+ cpu: 'green',
477
+ memory: 'magenta',
478
+ gpu: 'yellow',
479
+ network: 'cyan',
480
+ disk: 'green',
481
+ system: 'gray',
482
+ uptime: 'magenta',
483
+ gateway: 'cyan',
484
+ help: 'cyan',
485
+ settings: 'cyan',
486
+ modal: 'cyan'
487
+ },
488
+ // Text
489
+ text: {
490
+ primary: 'white',
491
+ secondary: 'black',
492
+ bright: 'brightWhite',
493
+ header: 'brightWhite'
494
+ },
495
+ // Status indicators
496
+ status: {
497
+ active: 'green',
498
+ idle: 'yellow',
499
+ stale: 'black',
500
+ error: 'red',
501
+ warning: 'yellow',
502
+ success: 'green'
503
+ },
504
+ // Gauges and values
505
+ gauge: {
506
+ low: 'green',
507
+ medium: 'yellow',
508
+ high: 'red',
509
+ critical: 'brightRed'
510
+ },
511
+ // Charts
512
+ chart: {
513
+ line: 'green',
514
+ fill: 'cyan',
515
+ grid: 'black'
516
+ },
517
+ // Alerts
518
+ alert: {
519
+ info: 'cyan',
520
+ warning: 'yellow',
521
+ error: 'red',
522
+ success: 'green'
523
+ },
524
+ // Logo and title
525
+ branding: {
526
+ logo: 'green',
527
+ title: 'brightGreen',
528
+ clock: 'green'
529
+ },
530
+ // Footer
531
+ footer: {
532
+ bg: 'black',
533
+ fg: 'green'
534
+ }
535
+ }
536
+ },
537
+
538
+ 'high-contrast': {
539
+ name: 'High Contrast',
540
+ colors: {
541
+ // Borders - bright on black
542
+ border: {
543
+ sessions: 'brightWhite',
544
+ logs: 'brightWhite',
545
+ cpu: 'brightWhite',
546
+ memory: 'brightWhite',
547
+ gpu: 'brightWhite',
548
+ network: 'brightWhite',
549
+ disk: 'brightWhite',
550
+ system: 'brightWhite',
551
+ uptime: 'brightWhite',
552
+ gateway: 'brightWhite',
553
+ help: 'brightWhite',
554
+ settings: 'brightWhite',
555
+ modal: 'brightWhite'
556
+ },
557
+ // Text
558
+ text: {
559
+ primary: 'white',
560
+ secondary: 'brightWhite',
561
+ bright: 'brightWhite',
562
+ header: 'brightWhite'
563
+ },
564
+ // Status indicators
565
+ status: {
566
+ active: 'brightGreen',
567
+ idle: 'brightYellow',
568
+ stale: 'brightWhite',
569
+ error: 'brightRed',
570
+ warning: 'brightYellow',
571
+ success: 'brightGreen'
572
+ },
573
+ // Gauges and values
574
+ gauge: {
575
+ low: 'brightGreen',
576
+ medium: 'brightYellow',
577
+ high: 'brightRed',
578
+ critical: 'brightRed'
579
+ },
580
+ // Charts
581
+ chart: {
582
+ line: 'brightWhite',
583
+ fill: 'brightWhite',
584
+ grid: 'brightWhite'
585
+ },
586
+ // Alerts
587
+ alert: {
588
+ info: 'brightCyan',
589
+ warning: 'brightYellow',
590
+ error: 'brightRed',
591
+ success: 'brightGreen'
592
+ },
593
+ // Logo and title
594
+ branding: {
595
+ logo: 'brightWhite',
596
+ title: 'brightWhite',
597
+ clock: 'brightWhite'
598
+ },
599
+ // Footer
600
+ footer: {
601
+ bg: 'white',
602
+ fg: 'black'
603
+ }
604
+ }
605
+ },
606
+
607
+ ocean: {
608
+ name: 'Ocean',
609
+ colors: {
610
+ // Borders - ocean blues
611
+ border: {
612
+ sessions: 'blue',
613
+ logs: 'cyan',
614
+ cpu: 'blue',
615
+ memory: 'cyan',
616
+ gpu: 'blue',
617
+ network: 'cyan',
618
+ disk: 'blue',
619
+ system: 'cyan',
620
+ uptime: 'blue',
621
+ gateway: 'cyan',
622
+ help: 'cyan',
623
+ settings: 'cyan',
624
+ modal: 'brightBlue'
625
+ },
626
+ // Text
627
+ text: {
628
+ primary: 'white',
629
+ secondary: 'blue',
630
+ bright: 'brightWhite',
631
+ header: 'brightCyan'
632
+ },
633
+ // Status indicators
634
+ status: {
635
+ active: 'cyan',
636
+ idle: 'blue',
637
+ stale: 'gray',
638
+ error: 'red',
639
+ warning: 'yellow',
640
+ success: 'green'
641
+ },
642
+ // Gauges and values
643
+ gauge: {
644
+ low: 'cyan',
645
+ medium: 'blue',
646
+ high: 'yellow',
647
+ critical: 'red'
648
+ },
649
+ // Charts
650
+ chart: {
651
+ line: 'cyan',
652
+ fill: 'blue',
653
+ grid: 'blue'
654
+ },
655
+ // Alerts
656
+ alert: {
657
+ info: 'cyan',
658
+ warning: 'yellow',
659
+ error: 'red',
660
+ success: 'green'
661
+ },
662
+ // Logo and title
663
+ branding: {
664
+ logo: 'cyan',
665
+ title: 'brightCyan',
666
+ clock: 'cyan'
667
+ },
668
+ // Footer
669
+ footer: {
670
+ bg: 'blue',
671
+ fg: 'cyan'
672
+ }
673
+ }
674
+ },
675
+
676
+ // Auto-detect theme - resolves to dark or light based on terminal background
677
+ auto: {
678
+ name: 'Auto-detect',
679
+ isAuto: true,
680
+ colors: null // Will be resolved dynamically
681
+ }
682
+ };
683
+
684
+ // Detected background state
685
+ let detectedBackground = null;
686
+
687
+ /**
688
+ * Get the detected system/theme background
689
+ * Uses system theme detection with terminal fallback
690
+ * @returns {string} 'light' or 'dark'
691
+ */
692
+ function getDetectedBackground() {
693
+ if (!detectedBackground) {
694
+ detectedBackground = detectSystemTheme();
695
+ logger.info(`Theme background detected: ${detectedBackground}`);
696
+ }
697
+ return detectedBackground;
698
+ }
699
+
700
+ /**
701
+ * Force re-detection of the system theme
702
+ * Useful after system theme changes
703
+ * @returns {string} Newly detected 'light' or 'dark'
704
+ */
705
+ function refreshDetectedBackground() {
706
+ const oldTheme = detectedBackground;
707
+ detectedBackground = detectSystemTheme();
708
+ if (oldTheme !== detectedBackground) {
709
+ logger.info(`Theme background changed: ${oldTheme} -> ${detectedBackground}`);
710
+ }
711
+ return detectedBackground;
712
+ }
713
+
714
+ /**
715
+ * Resolve auto theme to actual theme based on detection
716
+ * @returns {object} Resolved theme object
717
+ */
718
+ function resolveAutoTheme() {
719
+ const background = getDetectedBackground();
720
+ return background === 'light' ? themes.default : themes.dark;
721
+ }
722
+
723
+ // Current theme state
724
+ let currentThemeName = 'default';
725
+
726
+ /**
727
+ * Get the current theme
728
+ * @returns {object} Current theme object
729
+ */
730
+ function getCurrentTheme() {
731
+ if (currentThemeName === 'auto') {
732
+ return resolveAutoTheme();
733
+ }
734
+ return themes[currentThemeName] || themes.default;
735
+ }
736
+
737
+ /**
738
+ * Get current theme name
739
+ * @returns {string} Current theme name
740
+ */
741
+ function getThemeName() {
742
+ return currentThemeName;
743
+ }
744
+
745
+ /**
746
+ * Get theme by name
747
+ * @param {string} name - Theme name
748
+ * @returns {object} Theme object or null
749
+ */
750
+ function getTheme(name) {
751
+ if (name === 'auto') {
752
+ return themes.auto;
753
+ }
754
+ return themes[name] || null;
755
+ }
756
+
757
+ /**
758
+ * Get all available theme names
759
+ * @returns {string[]} Array of theme names
760
+ */
761
+ function getThemeNames() {
762
+ return Object.keys(themes);
763
+ }
764
+
765
+ /**
766
+ * Set the current theme
767
+ * @param {string} name - Theme name to set
768
+ * @returns {boolean} Success
769
+ */
770
+ function setTheme(name) {
771
+ if (!themes[name]) {
772
+ logger.warn(`Theme '${name}' not found, keeping current theme`);
773
+ return false;
774
+ }
775
+ currentThemeName = name;
776
+ const displayName = name === 'auto' ? `Auto-detect (${getDetectedBackground()})` : themes[name].name;
777
+ logger.info(`Theme changed to: ${displayName}`);
778
+ return true;
779
+ }
780
+
781
+ /**
782
+ * Cycle to the next theme
783
+ * @returns {string} New theme name
784
+ */
785
+ function cycleTheme() {
786
+ const themeNames = Object.keys(themes);
787
+ const currentIndex = themeNames.indexOf(currentThemeName);
788
+ const nextIndex = (currentIndex + 1) % themeNames.length;
789
+ currentThemeName = themeNames[nextIndex];
790
+ const displayName = currentThemeName === 'auto' ? `Auto-detect (${getDetectedBackground()})` : themes[currentThemeName].name;
791
+ logger.info(`Theme cycled to: ${displayName}`);
792
+ return currentThemeName;
793
+ }
794
+
795
+ /**
796
+ * Load saved theme from settings
797
+ */
798
+ function loadTheme() {
799
+ try {
800
+ const data = fs.readFileSync(SETTINGS_PATH, 'utf8');
801
+ const settings = JSON.parse(data);
802
+ if (settings[THEME_KEY] && themes[settings[THEME_KEY]]) {
803
+ currentThemeName = settings[THEME_KEY];
804
+ if (currentThemeName === 'auto') {
805
+ const bg = getDetectedBackground();
806
+ logger.info(`Loaded theme: Auto-detect (${bg} background)`);
807
+ } else {
808
+ logger.info(`Loaded theme: ${themes[currentThemeName].name}`);
809
+ }
810
+ }
811
+ } catch {
812
+ // File doesn't exist or invalid JSON - use default
813
+ }
814
+ }
815
+
816
+ /**
817
+ * Save current theme to settings
818
+ */
819
+ function saveTheme() {
820
+ try {
821
+ let settings = {};
822
+ try {
823
+ const data = fs.readFileSync(SETTINGS_PATH, 'utf8');
824
+ settings = JSON.parse(data);
825
+ } catch {}
826
+
827
+ settings[THEME_KEY] = currentThemeName;
828
+
829
+ const dir = process.env.HOME + '/.openclaw';
830
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
831
+ fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
832
+ } catch (err) {
833
+ logger.warn(`Failed to save theme: ${err.message}`);
834
+ }
835
+ }
836
+
837
+ /**
838
+ * Get a color from the current theme
839
+ * @param {string} category - Color category (border, text, status, gauge, chart, alert, branding, footer)
840
+ * @param {string} key - Key within the category
841
+ * @returns {string} Color value
842
+ */
843
+ function getColor(category, key) {
844
+ const theme = getCurrentTheme();
845
+ if (theme.colors[category] && theme.colors[category][key]) {
846
+ return theme.colors[category][key];
847
+ }
848
+ // Fallback to default theme
849
+ if (themes.default.colors[category] && themes.default.colors[category][key]) {
850
+ return themes.default.colors[category][key];
851
+ }
852
+ return 'white';
853
+ }
854
+
855
+ export default themes;
856
+ export {
857
+ themes,
858
+ getCurrentTheme,
859
+ getThemeName,
860
+ getTheme,
861
+ getThemeNames,
862
+ setTheme,
863
+ cycleTheme,
864
+ getColor,
865
+ loadTheme,
866
+ saveTheme,
867
+ getDetectedBackground,
868
+ refreshDetectedBackground,
869
+ detectSystemTheme,
870
+ startSystemThemeWatcher,
871
+ onThemeChange,
872
+ startAutoThemeDetection,
873
+ stopAutoThemeDetection
874
+ };