claw-dashboard 1.9.0 → 2.1.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 +5285 -566
  40. package/jest.config.js +11 -0
  41. package/man/clawdash.1 +276 -0
  42. package/package.json +54 -6
  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 +1934 -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 +1056 -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
@@ -0,0 +1,467 @@
1
+ /**
2
+ * Memory Pressure Detection Module
3
+ * Monitors dashboard process memory for long-running sessions
4
+ * Detects memory trends, sustained pressure, and potential leaks
5
+ */
6
+
7
+ import logger from './logger.js';
8
+ import config from './config.js';
9
+ import { RateLimiter } from './alerts.js';
10
+
11
+ const { MEMORY_PRESSURE } = config;
12
+
13
+ /**
14
+ * Memory pressure levels
15
+ */
16
+ export const PressureLevel = {
17
+ NONE: 'none',
18
+ ELEVATED: 'elevated',
19
+ WARNING: 'warning',
20
+ CRITICAL: 'critical',
21
+ EMERGENCY: 'emergency',
22
+ };
23
+
24
+ /**
25
+ * Memory trend directions
26
+ */
27
+ export const TrendDirection = {
28
+ STABLE: 'stable',
29
+ GROWING: 'growing',
30
+ SHRINKING: 'shrinking',
31
+ };
32
+
33
+ /**
34
+ * Memory sample for trend analysis
35
+ * @typedef {Object} MemorySample
36
+ * @property {number} timestamp - Unix timestamp
37
+ * @property {number} heapUsed - Heap used in MB
38
+ * @property {number} heapTotal - Heap total in MB
39
+ * @property {number} rss - RSS memory in MB
40
+ * @property {number} external - External memory in MB
41
+ */
42
+
43
+ /**
44
+ * Memory pressure state
45
+ * @typedef {Object} PressureState
46
+ * @property {string} level - Current pressure level
47
+ * @property {number} heapUsedMB - Current heap usage in MB
48
+ * @property {number} heapTotalMB - Current heap total in MB
49
+ * @property {number} usagePercent - Current usage percentage
50
+ * @property {string} trend - Memory trend direction
51
+ * @property {number} trendRateMB - Memory growth rate in MB/minute
52
+ * @property {boolean} sustained - Whether pressure has been sustained
53
+ * @property {number} sustainedDurationMs - How long pressure has been sustained
54
+ * @property {string[]} recommendations - Action recommendations
55
+ */
56
+
57
+ class MemoryPressureDetector {
58
+ constructor() {
59
+ /** @type {MemorySample[]} */
60
+ this.samples = [];
61
+ this.maxSamples = MEMORY_PRESSURE.TREND.SAMPLE_COUNT * 2;
62
+
63
+ // Pressure state tracking
64
+ this.currentLevel = PressureLevel.NONE;
65
+ this.sustainedSince = null;
66
+ this.lastCheck = Date.now();
67
+
68
+ // Trend tracking
69
+ this.lastTrend = TrendDirection.STABLE;
70
+ this.lastTrendRate = 0;
71
+
72
+ // Alert rate limiter (separate from threshold alerts)
73
+ this.rateLimiter = new RateLimiter({
74
+ enabled: true,
75
+ windowMs: 300000, // 5 minutes between pressure alerts
76
+ maxAlerts: 3,
77
+ alwaysAllowCritical: true,
78
+ });
79
+
80
+ // Statistics
81
+ this.stats = {
82
+ peakHeapMB: 0,
83
+ pressureEvents: 0,
84
+ sustainedEvents: 0,
85
+ lastPressureTime: null,
86
+ };
87
+
88
+ // Callbacks
89
+ this.onPressureChange = null;
90
+ this.onSustainedPressure = null;
91
+ this.onEmergency = null;
92
+
93
+ this.isRunning = false;
94
+ }
95
+
96
+ /**
97
+ * Start memory pressure monitoring
98
+ */
99
+ start() {
100
+ if (this.isRunning) return;
101
+ this.isRunning = true;
102
+ logger.debug('Memory pressure monitoring started');
103
+ }
104
+
105
+ /**
106
+ * Stop memory pressure monitoring
107
+ */
108
+ stop() {
109
+ this.isRunning = false;
110
+ logger.debug('Memory pressure monitoring stopped');
111
+ }
112
+
113
+ /**
114
+ * Record a memory sample
115
+ * @returns {MemorySample}
116
+ */
117
+ recordSample() {
118
+ const usage = process.memoryUsage();
119
+ const sample = {
120
+ timestamp: Date.now(),
121
+ heapUsed: Math.round(usage.heapUsed / 1024 / 1024),
122
+ heapTotal: Math.round(usage.heapTotal / 1024 / 1024),
123
+ rss: Math.round(usage.rss / 1024 / 1024),
124
+ external: Math.round((usage.external || 0) / 1024 / 1024),
125
+ };
126
+
127
+ this.samples.push(sample);
128
+ if (this.samples.length > this.maxSamples) {
129
+ this.samples.shift();
130
+ }
131
+
132
+ // Update peak stats
133
+ if (sample.heapUsed > this.stats.peakHeapMB) {
134
+ this.stats.peakHeapMB = sample.heapUsed;
135
+ }
136
+
137
+ return sample;
138
+ }
139
+
140
+ /**
141
+ * Analyze memory trend from samples
142
+ * @returns {{direction: string, rateMBPerMin: number}}
143
+ */
144
+ analyzeTrend() {
145
+ if (this.samples.length < MEMORY_PRESSURE.TREND.SAMPLE_COUNT) {
146
+ return { direction: TrendDirection.STABLE, rateMBPerMin: 0 };
147
+ }
148
+
149
+ const recentSamples = this.samples.slice(-MEMORY_PRESSURE.TREND.SAMPLE_COUNT);
150
+ const first = recentSamples[0];
151
+ const last = recentSamples[recentSamples.length - 1];
152
+ const durationMinutes = (last.timestamp - first.timestamp) / 60000;
153
+
154
+ if (durationMinutes < 0.5) {
155
+ return { direction: TrendDirection.STABLE, rateMBPerMin: 0 };
156
+ }
157
+
158
+ const growthMB = last.heapUsed - first.heapUsed;
159
+ const rateMBPerMin = growthMB / durationMinutes;
160
+
161
+ // Determine direction based on threshold
162
+ const threshold = MEMORY_PRESSURE.TREND.GROWTH_THRESHOLD_MB;
163
+ if (growthMB > threshold) {
164
+ return { direction: TrendDirection.GROWING, rateMBPerMin };
165
+ } else if (growthMB < -threshold) {
166
+ return { direction: TrendDirection.SHRINKING, rateMBPerMin };
167
+ }
168
+
169
+ return { direction: TrendDirection.STABLE, rateMBPerMin };
170
+ }
171
+
172
+ /**
173
+ * Determine pressure level from heap usage
174
+ * @param {number} heapUsedMB
175
+ * @returns {string}
176
+ */
177
+ getPressureLevel(heapUsedMB) {
178
+ const { THRESHOLDS } = MEMORY_PRESSURE;
179
+ if (heapUsedMB >= THRESHOLDS.EMERGENCY_MB) return PressureLevel.EMERGENCY;
180
+ if (heapUsedMB >= THRESHOLDS.CRITICAL_MB) return PressureLevel.CRITICAL;
181
+ if (heapUsedMB >= THRESHOLDS.WARNING_MB) return PressureLevel.WARNING;
182
+ // Elevated is 75% of warning threshold
183
+ if (heapUsedMB >= THRESHOLDS.WARNING_MB * 0.75) return PressureLevel.ELEVATED;
184
+ return PressureLevel.NONE;
185
+ }
186
+
187
+ /**
188
+ * Check for sustained pressure
189
+ * @param {string} level - Current pressure level
190
+ * @returns {{sustained: boolean, durationMs: number}}
191
+ */
192
+ checkSustainedPressure(level) {
193
+ const isElevated = level !== PressureLevel.NONE && level !== PressureLevel.ELEVATED;
194
+
195
+ if (!isElevated) {
196
+ this.sustainedSince = null;
197
+ return { sustained: false, durationMs: 0 };
198
+ }
199
+
200
+ if (!this.sustainedSince) {
201
+ this.sustainedSince = Date.now();
202
+ }
203
+
204
+ const durationMs = Date.now() - this.sustainedSince;
205
+ const sustained = durationMs >= MEMORY_PRESSURE.SUSTAINED.DURATION_MS;
206
+
207
+ return { sustained, durationMs };
208
+ }
209
+
210
+ /**
211
+ * Get recommendations based on pressure state
212
+ * @param {PressureState} state
213
+ * @returns {string[]}
214
+ */
215
+ getRecommendations(state) {
216
+ const recommendations = [];
217
+
218
+ if (state.level === PressureLevel.EMERGENCY) {
219
+ recommendations.push('Consider restarting the dashboard immediately');
220
+ recommendations.push('Check for memory leaks in custom widgets/plugins');
221
+ } else if (state.level === PressureLevel.CRITICAL) {
222
+ recommendations.push('Enable performance metrics to identify resource-heavy widgets');
223
+ recommendations.push('Consider disabling unused widgets');
224
+ if (state.trend === TrendDirection.GROWING) {
225
+ recommendations.push(`Memory growing at ${state.trendRateMB.toFixed(1)}MB/min - possible leak detected`);
226
+ }
227
+ } else if (state.level === PressureLevel.WARNING) {
228
+ if (state.trend === TrendDirection.GROWING) {
229
+ recommendations.push('Memory trend indicates potential leak - monitor closely');
230
+ }
231
+ recommendations.push('Dashboard memory is elevated but stable');
232
+ }
233
+
234
+ if (this.samples.length > MEMORY_PRESSURE.TREND.SAMPLE_COUNT) {
235
+ const ageMs = Date.now() - this.samples[0].timestamp;
236
+ const ageHours = ageMs / (1000 * 60 * 60);
237
+ if (ageHours > 24) {
238
+ recommendations.push(`Dashboard has been running for ${ageHours.toFixed(1)} hours - consider periodic restarts`);
239
+ }
240
+ }
241
+
242
+ return recommendations;
243
+ }
244
+
245
+ /**
246
+ * Perform memory pressure check
247
+ * @returns {PressureState}
248
+ */
249
+ check() {
250
+ if (!this.isRunning) {
251
+ this.start();
252
+ }
253
+
254
+ const sample = this.recordSample();
255
+ const trend = this.analyzeTrend();
256
+ const level = this.getPressureLevel(sample.heapUsed);
257
+ const { sustained, durationMs } = this.checkSustainedPressure(level);
258
+ const usagePercent = sample.heapTotal > 0
259
+ ? Math.round((sample.heapUsed / sample.heapTotal) * 100)
260
+ : 0;
261
+
262
+ const state = {
263
+ level,
264
+ heapUsedMB: sample.heapUsed,
265
+ heapTotalMB: sample.heapTotal,
266
+ usagePercent,
267
+ trend: trend.direction,
268
+ trendRateMB: trend.rateMBPerMin,
269
+ sustained,
270
+ sustainedDurationMs: durationMs,
271
+ recommendations: [],
272
+ };
273
+
274
+ state.recommendations = this.getRecommendations(state);
275
+
276
+ // Track state changes
277
+ if (level !== this.currentLevel) {
278
+ this.handleLevelChange(this.currentLevel, level, state);
279
+ this.currentLevel = level;
280
+ }
281
+
282
+ // Handle sustained pressure
283
+ if (sustained && durationMs >= MEMORY_PRESSURE.SUSTAINED.DURATION_MS) {
284
+ this.handleSustainedPressure(state);
285
+ }
286
+
287
+ this.lastTrend = trend.direction;
288
+ this.lastTrendRate = trend.rateMBPerMin;
289
+ this.lastCheck = Date.now();
290
+
291
+ return state;
292
+ }
293
+
294
+ /**
295
+ * Handle pressure level change
296
+ * @param {string} oldLevel
297
+ * @param {string} newLevel
298
+ * @param {PressureState} state
299
+ */
300
+ handleLevelChange(oldLevel, newLevel, state) {
301
+ const escalation = [
302
+ PressureLevel.NONE,
303
+ PressureLevel.ELEVATED,
304
+ PressureLevel.WARNING,
305
+ PressureLevel.CRITICAL,
306
+ PressureLevel.EMERGENCY,
307
+ ];
308
+
309
+ const oldIndex = escalation.indexOf(oldLevel);
310
+ const newIndex = escalation.indexOf(newLevel);
311
+
312
+ if (newIndex > oldIndex) {
313
+ // Escalating
314
+ logger.warn(`Memory pressure escalating: ${oldLevel} -> ${newLevel} (${state.heapUsedMB}MB)`);
315
+ this.stats.pressureEvents++;
316
+ this.stats.lastPressureTime = Date.now();
317
+
318
+ if (newLevel === PressureLevel.EMERGENCY && this.onEmergency) {
319
+ this.onEmergency(state);
320
+ }
321
+ } else {
322
+ // De-escalating
323
+ logger.info(`Memory pressure de-escalating: ${oldLevel} -> ${newLevel} (${state.heapUsedMB}MB)`);
324
+ }
325
+
326
+ if (this.onPressureChange) {
327
+ this.onPressureChange(oldLevel, newLevel, state);
328
+ }
329
+ }
330
+
331
+ /**
332
+ * Handle sustained pressure
333
+ * @param {PressureState} state
334
+ */
335
+ handleSustainedPressure(state) {
336
+ const rateLimitResult = this.rateLimiter.checkAndRecord('sustained-pressure', state.level);
337
+ if (!rateLimitResult.allowed) {
338
+ return;
339
+ }
340
+
341
+ logger.warn(`Sustained memory pressure detected: ${state.level} for ${(state.sustainedDurationMs / 1000).toFixed(0)}s`);
342
+ this.stats.sustainedEvents++;
343
+
344
+ if (this.onSustainedPressure) {
345
+ this.onSustainedPressure(state);
346
+ }
347
+
348
+ // Auto-actions
349
+ if (MEMORY_PRESSURE.ACTIONS.REQUEST_GC && global.gc) {
350
+ logger.debug('Requesting garbage collection');
351
+ try {
352
+ global.gc();
353
+ } catch (error) {
354
+ logger.debug('GC request failed:', error.message);
355
+ }
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Get memory pressure status for display
361
+ * @returns {Object}
362
+ */
363
+ getStatus() {
364
+ const latest = this.samples[this.samples.length - 1];
365
+ return {
366
+ isRunning: this.isRunning,
367
+ currentLevel: this.currentLevel,
368
+ samples: this.samples.length,
369
+ peakHeapMB: this.stats.peakHeapMB,
370
+ pressureEvents: this.stats.pressureEvents,
371
+ sustainedEvents: this.stats.sustainedEvents,
372
+ lastPressureTime: this.stats.lastPressureTime,
373
+ latest: latest || null,
374
+ trend: {
375
+ direction: this.lastTrend,
376
+ rateMBPerMin: this.lastTrendRate,
377
+ },
378
+ thresholds: MEMORY_PRESSURE.THRESHOLDS,
379
+ };
380
+ }
381
+
382
+ /**
383
+ * Get formatted status string for display
384
+ * @returns {string}
385
+ */
386
+ getStatusString() {
387
+ const status = this.getStatus();
388
+ const latest = status.latest;
389
+
390
+ if (!latest) {
391
+ return 'Memory pressure monitoring inactive';
392
+ }
393
+
394
+ const colors = {
395
+ [PressureLevel.NONE]: 'green-fg',
396
+ [PressureLevel.ELEVATED]: 'cyan-fg',
397
+ [PressureLevel.WARNING]: 'yellow-fg',
398
+ [PressureLevel.CRITICAL]: 'red-fg',
399
+ [PressureLevel.EMERGENCY]: 'red-fg',
400
+ };
401
+
402
+ const color = colors[this.currentLevel] || 'white-fg';
403
+ const trendIcon = this.lastTrend === TrendDirection.GROWING ? '↑' :
404
+ this.lastTrend === TrendDirection.SHRINKING ? '↓' : '→';
405
+
406
+ return `{${color}}MEM:${latest.heapUsed}MB ${trendIcon}{/${color}}`;
407
+ }
408
+
409
+ /**
410
+ * Check if memory pressure is currently elevated
411
+ * @returns {boolean}
412
+ */
413
+ isElevated() {
414
+ return this.currentLevel === PressureLevel.WARNING ||
415
+ this.currentLevel === PressureLevel.CRITICAL ||
416
+ this.currentLevel === PressureLevel.EMERGENCY;
417
+ }
418
+
419
+ /**
420
+ * Check if memory pressure is critical
421
+ * @returns {boolean}
422
+ */
423
+ isCritical() {
424
+ return this.currentLevel === PressureLevel.CRITICAL ||
425
+ this.currentLevel === PressureLevel.EMERGENCY;
426
+ }
427
+
428
+ /**
429
+ * Reset all statistics and samples
430
+ */
431
+ reset() {
432
+ this.samples = [];
433
+ this.currentLevel = PressureLevel.NONE;
434
+ this.sustainedSince = null;
435
+ this.lastTrend = TrendDirection.STABLE;
436
+ this.lastTrendRate = 0;
437
+ this.stats = {
438
+ peakHeapMB: 0,
439
+ pressureEvents: 0,
440
+ sustainedEvents: 0,
441
+ lastPressureTime: null,
442
+ };
443
+ this.rateLimiter.reset();
444
+ logger.debug('Memory pressure detector reset');
445
+ }
446
+
447
+ /**
448
+ * Get recommendations for current state
449
+ * @returns {string[]}
450
+ */
451
+ getCurrentRecommendations() {
452
+ if (this.samples.length === 0) {
453
+ return [];
454
+ }
455
+ const state = {
456
+ level: this.currentLevel,
457
+ trend: this.lastTrend,
458
+ trendRateMB: this.lastTrendRate,
459
+ heapUsedMB: this.samples[this.samples.length - 1]?.heapUsed || 0,
460
+ };
461
+ return this.getRecommendations(state);
462
+ }
463
+ }
464
+
465
+ // Export singleton instance
466
+ export default new MemoryPressureDetector();
467
+ export { MemoryPressureDetector };