ralphblaster-agent 0.1.1

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 (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +294 -0
  3. package/bin/agent-dashboard.sh +168 -0
  4. package/bin/monitor-agent.sh +264 -0
  5. package/bin/ralphblaster.js +247 -0
  6. package/package.json +64 -0
  7. package/postinstall-colored.js +66 -0
  8. package/src/api-client.js +764 -0
  9. package/src/claude-plugin/.claude-plugin/plugin.json +9 -0
  10. package/src/claude-plugin/README.md +42 -0
  11. package/src/claude-plugin/skills/ralph/SKILL.md +259 -0
  12. package/src/commands/add-project.js +257 -0
  13. package/src/commands/init.js +79 -0
  14. package/src/config-file-manager.js +84 -0
  15. package/src/config.js +66 -0
  16. package/src/error-window.js +86 -0
  17. package/src/executor/claude-runner.js +716 -0
  18. package/src/executor/error-handler.js +65 -0
  19. package/src/executor/git-helper.js +196 -0
  20. package/src/executor/index.js +296 -0
  21. package/src/executor/job-handlers/clarifying-questions.js +213 -0
  22. package/src/executor/job-handlers/code-execution.js +145 -0
  23. package/src/executor/job-handlers/prd-generation.js +259 -0
  24. package/src/executor/path-helper.js +74 -0
  25. package/src/executor/prompt-validator.js +51 -0
  26. package/src/executor.js +4 -0
  27. package/src/index.js +342 -0
  28. package/src/logger.js +193 -0
  29. package/src/logging/README.md +93 -0
  30. package/src/logging/config.js +179 -0
  31. package/src/logging/destinations/README.md +290 -0
  32. package/src/logging/destinations/api-destination-unbatched.js +118 -0
  33. package/src/logging/destinations/api-destination.js +40 -0
  34. package/src/logging/destinations/base-destination.js +85 -0
  35. package/src/logging/destinations/batched-destination.js +198 -0
  36. package/src/logging/destinations/console-destination.js +172 -0
  37. package/src/logging/destinations/file-destination.js +208 -0
  38. package/src/logging/destinations/index.js +29 -0
  39. package/src/logging/destinations/progress-batch-destination-unbatched.js +92 -0
  40. package/src/logging/destinations/progress-batch-destination.js +41 -0
  41. package/src/logging/formatter.js +288 -0
  42. package/src/logging/log-manager.js +426 -0
  43. package/src/progress-throttle.js +101 -0
  44. package/src/system-monitor.js +64 -0
  45. package/src/utils/format.js +16 -0
  46. package/src/utils/log-file-helper.js +265 -0
  47. package/src/utils/progress-parser.js +250 -0
  48. package/src/worktree-manager.js +255 -0
@@ -0,0 +1,426 @@
1
+ /**
2
+ * LogManager - Coordinates multiple log destinations
3
+ *
4
+ * Acts as the main logging coordinator that routes log calls to all registered
5
+ * destinations (console, file, API, etc.) in parallel. Manages destination
6
+ * lifecycle and handles per-destination errors gracefully.
7
+ *
8
+ * Key features:
9
+ * - Dependency injection - destinations passed in, not created internally
10
+ * - Destination-agnostic - doesn't know about specific destination types
11
+ * - Parallel writes - logs to all destinations concurrently
12
+ * - Graceful error handling - destination failures don't affect other destinations
13
+ * - Context management - setJobContext/clearJobContext configures all destinations
14
+ * - Lifecycle management - setup, flush, and teardown coordination
15
+ *
16
+ * Example usage:
17
+ * ```javascript
18
+ * const { ConsoleDestination, ApiDestination } = require('./destinations');
19
+ * const loggingConfig = require('./config');
20
+ *
21
+ * const manager = new LogManager([
22
+ * new ConsoleDestination({ colors: loggingConfig.consoleColors }),
23
+ * new ApiDestination({ apiClient, jobId })
24
+ * ]);
25
+ *
26
+ * await manager.info('Server started', { port: 3000 });
27
+ * await manager.close();
28
+ * ```
29
+ */
30
+
31
+ class LogManager {
32
+ /**
33
+ * Create a new LogManager
34
+ * @param {Array<BaseDestination>} destinations - Array of log destinations to coordinate
35
+ * @param {Object} [config={}] - Manager configuration
36
+ * @param {string} [config.agentId=null] - Agent ID for multi-agent support
37
+ */
38
+ constructor(destinations = [], config = {}) {
39
+ if (!Array.isArray(destinations)) {
40
+ throw new Error('LogManager requires an array of destinations');
41
+ }
42
+
43
+ this.destinations = destinations;
44
+ this.agentId = config.agentId || null;
45
+ this.jobContext = {
46
+ jobId: null,
47
+ globalContext: {}
48
+ };
49
+ this.isShuttingDown = false;
50
+ }
51
+
52
+ /**
53
+ * Write a log entry to all destinations
54
+ * Routes the log to all destinations in parallel, handling errors gracefully.
55
+ * Failed destinations don't affect other destinations.
56
+ * @param {string} level - Log level ('error', 'warn', 'info', 'debug')
57
+ * @param {string} message - Log message
58
+ * @param {Object} [data={}] - Structured metadata
59
+ * @returns {Promise<void>}
60
+ * @private
61
+ */
62
+ async write(level, message, data = {}) {
63
+ // Merge global context with log data
64
+ const enrichedData = { ...this.jobContext.globalContext, ...data };
65
+
66
+ // Add agent ID to metadata if set
67
+ if (this.agentId) {
68
+ enrichedData.agentId = this.agentId;
69
+ }
70
+
71
+ // Add job ID to metadata if set
72
+ if (this.jobContext.jobId) {
73
+ enrichedData.jobId = this.jobContext.jobId;
74
+ }
75
+
76
+ // Write to all destinations in parallel
77
+ const writePromises = this.destinations.map(destination => {
78
+ // Check if destination should handle this log level
79
+ if (destination.shouldLog && !destination.shouldLog(level)) {
80
+ return Promise.resolve();
81
+ }
82
+
83
+ // Write to destination, catching errors to prevent cascading failures
84
+ return destination.write(level, message, enrichedData)
85
+ .catch(error => {
86
+ // Let destination handle its own errors
87
+ if (typeof destination.handleError === 'function') {
88
+ destination.handleError(error, level, message);
89
+ }
90
+ });
91
+ });
92
+
93
+ // Wait for all writes to complete (or fail gracefully)
94
+ await Promise.all(writePromises);
95
+ }
96
+
97
+ /**
98
+ * Log an error message
99
+ * Logs to all destinations that accept error level.
100
+ * @param {string} message - Error message
101
+ * @param {Object} [data={}] - Optional structured metadata
102
+ * @returns {Promise<void>}
103
+ * @example
104
+ * await manager.error('Database connection failed', { error: err.message })
105
+ */
106
+ async error(message, data = {}) {
107
+ await this.write('error', message, data);
108
+ }
109
+
110
+ /**
111
+ * Log a warning message
112
+ * Logs to all destinations that accept warn level.
113
+ * @param {string} message - Warning message
114
+ * @param {Object} [data={}] - Optional structured metadata
115
+ * @returns {Promise<void>}
116
+ * @example
117
+ * await manager.warn('Disk space low', { available: '5%' })
118
+ */
119
+ async warn(message, data = {}) {
120
+ await this.write('warn', message, data);
121
+ }
122
+
123
+ /**
124
+ * Log an info message
125
+ * Logs to all destinations that accept info level.
126
+ * @param {string} message - Info message
127
+ * @param {Object} [data={}] - Optional structured metadata
128
+ * @returns {Promise<void>}
129
+ * @example
130
+ * await manager.info('Server started', { port: 3000 })
131
+ */
132
+ async info(message, data = {}) {
133
+ await this.write('info', message, data);
134
+ }
135
+
136
+ /**
137
+ * Log a debug message
138
+ * Logs to all destinations that accept debug level.
139
+ * @param {string} message - Debug message
140
+ * @param {Object} [data={}] - Optional structured metadata
141
+ * @returns {Promise<void>}
142
+ * @example
143
+ * await manager.debug('Request received', { headers, body })
144
+ */
145
+ async debug(message, data = {}) {
146
+ await this.write('debug', message, data);
147
+ }
148
+
149
+ /**
150
+ * Set agent ID for multi-agent support
151
+ * Adds agent ID to all subsequent logs across all destinations.
152
+ * @param {string} id - Agent ID (e.g., 'agent-1', 'agent-2')
153
+ */
154
+ setAgentId(id) {
155
+ this.agentId = id;
156
+ }
157
+
158
+ /**
159
+ * Set job context for logging
160
+ * Configures all destinations with job context. The job ID will be
161
+ * included in all subsequent logs.
162
+ * @param {number} jobId - Job ID to associate with logs
163
+ * @param {Object} [context={}] - Optional global context to add to all logs
164
+ * @example
165
+ * manager.setJobContext(123, { component: 'worktree' })
166
+ */
167
+ setJobContext(jobId, context = {}) {
168
+ this.jobContext.jobId = jobId;
169
+ this.jobContext.globalContext = context;
170
+
171
+ // Notify destinations of context change (if they support it)
172
+ this.destinations.forEach(destination => {
173
+ if (typeof destination.setJobContext === 'function') {
174
+ destination.setJobContext(jobId, context);
175
+ }
176
+ });
177
+ }
178
+
179
+ /**
180
+ * Clear job context
181
+ * Clears the job context and flushes all destinations to ensure
182
+ * no logs are lost. Call this when a job completes.
183
+ * @returns {Promise<void>}
184
+ */
185
+ async clearJobContext() {
186
+ // Flush all destinations before clearing context
187
+ await this.flush();
188
+
189
+ this.jobContext.jobId = null;
190
+ this.jobContext.globalContext = {};
191
+
192
+ // Notify destinations of context clear (if they support it)
193
+ const clearPromises = this.destinations.map(destination => {
194
+ if (typeof destination.clearJobContext === 'function') {
195
+ return destination.clearJobContext().catch(() => {
196
+ // Silently handle errors during context clear
197
+ });
198
+ }
199
+ return Promise.resolve();
200
+ });
201
+
202
+ await Promise.all(clearPromises);
203
+ }
204
+
205
+ /**
206
+ * Set global context that will be included in all subsequent logs
207
+ * @param {string|Object} keyOrContext - Key name or object with multiple keys
208
+ * @param {*} [value] - Value (only if first param is a string)
209
+ * @example
210
+ * manager.setContext('component', 'worktree')
211
+ * manager.setContext({ component: 'worktree', operation: 'create' })
212
+ */
213
+ setContext(keyOrContext, value) {
214
+ if (typeof keyOrContext === 'object') {
215
+ this.jobContext.globalContext = {
216
+ ...this.jobContext.globalContext,
217
+ ...keyOrContext
218
+ };
219
+ } else {
220
+ this.jobContext.globalContext[keyOrContext] = value;
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Create a child logger with additional context
226
+ * Context is additive - child inherits parent context and adds its own.
227
+ * @param {Object} context - Additional context for this child logger
228
+ * @returns {Object} Child logger with all parent methods
229
+ * @example
230
+ * const worktreeLogger = manager.child({ component: 'worktree' })
231
+ * await worktreeLogger.info('Creating worktree') // Includes component: 'worktree'
232
+ */
233
+ child(context) {
234
+ const childContext = { ...this.jobContext.globalContext, ...context };
235
+
236
+ return {
237
+ error: (msg, data) => this.write('error', msg, { ...childContext, ...data }),
238
+ warn: (msg, data) => this.write('warn', msg, { ...childContext, ...data }),
239
+ info: (msg, data) => this.write('info', msg, { ...childContext, ...data }),
240
+ debug: (msg, data) => this.write('debug', msg, { ...childContext, ...data }),
241
+ event: (eventType, data) => this.event(eventType, { ...childContext, ...data }),
242
+ startTimer: (operation) => this.startTimer(operation, childContext),
243
+ measure: (operation, fn) => this.measure(operation, fn, childContext),
244
+ child: (additionalContext) => this.child({ ...childContext, ...additionalContext })
245
+ };
246
+ }
247
+
248
+ /**
249
+ * Log a semantic event with structured metadata
250
+ * Automatically determines log level based on action (failed/error = error, else info).
251
+ * @param {string} eventType - Event type in format 'category.action' (e.g., 'worktree.created')
252
+ * @param {Object} [data={}] - Additional event data
253
+ * @returns {Promise<void>}
254
+ * @example
255
+ * await manager.event('worktree.created', { path: '/path/to/worktree', duration: 3200 })
256
+ */
257
+ async event(eventType, data = {}) {
258
+ const [category, action] = eventType.split('.');
259
+ const eventData = {
260
+ eventType,
261
+ category,
262
+ action,
263
+ ...data
264
+ };
265
+
266
+ // Determine level based on action
267
+ const level = action === 'failed' || action === 'error' ? 'error' : 'info';
268
+
269
+ // Format message from action
270
+ const message = action ? action.charAt(0).toUpperCase() + action.slice(1) : eventType;
271
+
272
+ await this.write(level, message, eventData);
273
+ }
274
+
275
+ /**
276
+ * Start a performance timer for an operation
277
+ * Returns a timer object with a done() method to log completion with duration.
278
+ * @param {string} operation - Operation name (e.g., 'worktree.create')
279
+ * @param {Object} [initialContext={}] - Initial context for the operation
280
+ * @returns {Object} Timer object with done() method
281
+ * @example
282
+ * const timer = manager.startTimer('worktree.create')
283
+ * // ... do work ...
284
+ * timer.done({ path: '/path/to/worktree' }) // Logs duration automatically
285
+ */
286
+ startTimer(operation, initialContext = {}) {
287
+ const startTime = Date.now();
288
+
289
+ return {
290
+ done: async (data = {}) => {
291
+ const duration = Date.now() - startTime;
292
+ await this.event(`${operation}.complete`, {
293
+ ...initialContext,
294
+ ...data,
295
+ duration,
296
+ durationMs: duration
297
+ });
298
+ return duration;
299
+ }
300
+ };
301
+ }
302
+
303
+ /**
304
+ * Measure and log an async operation automatically
305
+ * Logs operation start, measures duration, and logs completion with success status.
306
+ * @param {string} operation - Operation name (e.g., 'prd.conversion')
307
+ * @param {Function} fn - Async function to measure
308
+ * @param {Object} [context={}] - Additional context
309
+ * @returns {Promise<*>} Result of the async function
310
+ * @example
311
+ * const result = await manager.measure('prd.conversion', async () => {
312
+ * return await convertPRD(job)
313
+ * })
314
+ * // Logs: prd.conversion.started, then prd.conversion.complete with duration
315
+ */
316
+ async measure(operation, fn, context = {}) {
317
+ await this.event(`${operation}.started`, context);
318
+ const timer = this.startTimer(operation, context);
319
+
320
+ try {
321
+ const result = await fn();
322
+ await timer.done({ success: true });
323
+ return result;
324
+ } catch (error) {
325
+ await timer.done({ success: false, error: error.message });
326
+ throw error;
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Flush all buffered logs across all destinations
332
+ * Ensures all pending logs are written immediately.
333
+ * Safe to call multiple times.
334
+ * @returns {Promise<void>}
335
+ */
336
+ async flush() {
337
+ const flushPromises = this.destinations.map(destination => {
338
+ if (typeof destination.flush === 'function') {
339
+ return destination.flush().catch(error => {
340
+ // Let destination handle its own flush errors
341
+ if (typeof destination.handleError === 'function') {
342
+ destination.handleError(error, 'error', 'Failed to flush destination');
343
+ }
344
+ });
345
+ }
346
+ return Promise.resolve();
347
+ });
348
+
349
+ await Promise.all(flushPromises);
350
+ }
351
+
352
+ /**
353
+ * Close all destinations and release resources
354
+ * Flushes all pending logs, stops timers, closes connections, etc.
355
+ * Call this during shutdown to ensure no logs are lost.
356
+ * @returns {Promise<void>}
357
+ */
358
+ async close() {
359
+ this.isShuttingDown = true;
360
+
361
+ const closePromises = this.destinations.map(destination => {
362
+ if (typeof destination.close === 'function') {
363
+ return destination.close().catch(error => {
364
+ // Let destination handle its own close errors
365
+ if (typeof destination.handleError === 'function') {
366
+ destination.handleError(error, 'error', 'Failed to close destination');
367
+ }
368
+ });
369
+ }
370
+ return Promise.resolve();
371
+ });
372
+
373
+ await Promise.all(closePromises);
374
+ }
375
+
376
+ /**
377
+ * Add a destination to the manager
378
+ * Useful for dynamically adding destinations after initialization.
379
+ * @param {BaseDestination} destination - Destination to add
380
+ */
381
+ addDestination(destination) {
382
+ if (!destination) {
383
+ throw new Error('Cannot add null or undefined destination');
384
+ }
385
+
386
+ this.destinations.push(destination);
387
+
388
+ // If we have active context, notify the new destination
389
+ if (this.jobContext.jobId && typeof destination.setJobContext === 'function') {
390
+ destination.setJobContext(this.jobContext.jobId, this.jobContext.globalContext);
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Remove a destination from the manager
396
+ * Flushes and closes the destination before removing it.
397
+ * @param {BaseDestination} destination - Destination to remove
398
+ * @returns {Promise<void>}
399
+ */
400
+ async removeDestination(destination) {
401
+ const index = this.destinations.indexOf(destination);
402
+ if (index === -1) {
403
+ return;
404
+ }
405
+
406
+ // Close the destination before removing
407
+ if (typeof destination.close === 'function') {
408
+ await destination.close().catch(() => {
409
+ // Silently handle close errors during removal
410
+ });
411
+ }
412
+
413
+ this.destinations.splice(index, 1);
414
+ }
415
+
416
+ /**
417
+ * Get count of registered destinations
418
+ * Useful for testing and debugging.
419
+ * @returns {number} Number of destinations
420
+ */
421
+ getDestinationCount() {
422
+ return this.destinations.length;
423
+ }
424
+ }
425
+
426
+ module.exports = LogManager;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * ProgressThrottle - Adaptive throttling for progress updates
3
+ *
4
+ * Adjusts throttle interval based on update rate:
5
+ * - Low rate (<5/sec): 100ms throttle (responsive)
6
+ * - Medium rate (5-20/sec): 200ms throttle (balanced)
7
+ * - High rate (>20/sec): 500ms throttle (reduced load)
8
+ *
9
+ * This prevents API overload during high-output jobs while maintaining
10
+ * responsiveness for low-output jobs.
11
+ */
12
+ class ProgressThrottle {
13
+ constructor() {
14
+ // Sliding window for rate calculation (5 seconds)
15
+ this.WINDOW_MS = 5000;
16
+
17
+ // Throttle intervals based on rate
18
+ this.INTERVALS = {
19
+ MIN: 100, // Low rate: minimal throttling
20
+ MEDIUM: 200, // Medium rate: moderate throttling
21
+ MAX: 500 // High rate: aggressive throttling
22
+ };
23
+
24
+ // Rate thresholds (updates per second)
25
+ this.RATE_THRESHOLDS = {
26
+ LOW: 5, // Below 5 updates/sec = low rate
27
+ MEDIUM: 20 // Above 20 updates/sec = high rate
28
+ };
29
+
30
+ // Tracking state
31
+ this.recentUpdates = []; // Timestamps of recent updates
32
+ this.lastUpdate = 0; // Timestamp of last update
33
+ }
34
+
35
+ /**
36
+ * Get current throttle interval based on recent update rate
37
+ * @returns {number} Throttle interval in milliseconds
38
+ */
39
+ getInterval() {
40
+ const now = Date.now();
41
+
42
+ // Clean up old updates (beyond 5-second window)
43
+ this.recentUpdates = this.recentUpdates.filter(
44
+ timestamp => now - timestamp < this.WINDOW_MS
45
+ );
46
+
47
+ // If no recent updates, use minimum interval
48
+ if (this.recentUpdates.length === 0) {
49
+ return this.INTERVALS.MIN;
50
+ }
51
+
52
+ // Calculate actual time span of recent updates
53
+ const oldestUpdate = Math.min(...this.recentUpdates);
54
+ const timeSpanMs = now - oldestUpdate;
55
+
56
+ // If all updates happened very recently (< 1 second), use that time span
57
+ // Otherwise use the full window for rate calculation
58
+ const effectiveWindowMs = Math.max(timeSpanMs, 1000); // At least 1 second
59
+ const effectiveWindowSeconds = effectiveWindowMs / 1000;
60
+
61
+ // Calculate updates per second based on effective window
62
+ const updatesPerSecond = this.recentUpdates.length / effectiveWindowSeconds;
63
+
64
+ // Determine interval based on rate
65
+ if (updatesPerSecond > this.RATE_THRESHOLDS.MEDIUM) {
66
+ return this.INTERVALS.MAX; // High rate: aggressive throttling
67
+ } else if (updatesPerSecond > this.RATE_THRESHOLDS.LOW) {
68
+ return this.INTERVALS.MEDIUM; // Medium rate: moderate throttling
69
+ } else {
70
+ return this.INTERVALS.MIN; // Low rate: minimal throttling
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Check if update should be throttled
76
+ * @returns {boolean} True if should throttle (skip), false if should allow
77
+ */
78
+ shouldThrottle() {
79
+ const now = Date.now();
80
+ const interval = this.getInterval();
81
+ const timeSinceLastUpdate = now - this.lastUpdate;
82
+
83
+ return timeSinceLastUpdate < interval;
84
+ }
85
+
86
+ /**
87
+ * Record an update (call this when sending an update)
88
+ */
89
+ recordUpdate() {
90
+ const now = Date.now();
91
+ this.recentUpdates.push(now);
92
+ this.lastUpdate = now;
93
+
94
+ // Cleanup old updates to prevent memory growth
95
+ this.recentUpdates = this.recentUpdates.filter(
96
+ timestamp => now - timestamp < this.WINDOW_MS
97
+ );
98
+ }
99
+ }
100
+
101
+ module.exports = ProgressThrottle;
@@ -0,0 +1,64 @@
1
+ const os = require('os');
2
+
3
+ /**
4
+ * SystemMonitor - Monitors system resources to determine agent capacity
5
+ *
6
+ * Adjusts agent capacity based on available memory and CPU load:
7
+ * - High resources (>40% mem, <0.6 CPU): capacity = 5
8
+ * - Medium resources (>20% mem, <0.8 CPU): capacity = 2
9
+ * - Low resources (<20% mem OR >0.8 CPU): capacity = 1
10
+ *
11
+ * This prevents agents from overwhelming systems that are already under load.
12
+ */
13
+ class SystemMonitor {
14
+ constructor() {
15
+ // Thresholds for capacity determination
16
+ this.THRESHOLDS = {
17
+ // Memory thresholds (as fraction of total memory)
18
+ MEM_LOW: 0.2, // Below 20% free = low resources
19
+ MEM_MEDIUM: 0.4, // Below 40% free = medium resources
20
+
21
+ // CPU load thresholds (as fraction per CPU)
22
+ CPU_MEDIUM: 0.6, // Above 0.6 per CPU = medium load
23
+ CPU_HIGH: 0.8, // Above 0.8 per CPU = high load
24
+
25
+ // Capacity levels
26
+ CAPACITY_MIN: 1,
27
+ CAPACITY_MEDIUM: 2,
28
+ CAPACITY_MAX: 5
29
+ };
30
+ }
31
+
32
+ /**
33
+ * Get current system capacity based on resources
34
+ * @returns {number} Capacity level (1, 2, or 5)
35
+ */
36
+ getCapacity() {
37
+ const freeMem = os.freemem();
38
+ const totalMem = os.totalmem();
39
+ const cpus = os.cpus();
40
+ const cpuCount = (cpus && cpus.length) || 1; // Default to 1 to avoid division by zero
41
+ const loadAvgArray = os.loadavg();
42
+ const loadAvg = (loadAvgArray && loadAvgArray[0]) || 0; // 1-minute load average
43
+
44
+ // Calculate resource metrics
45
+ const memFraction = freeMem / totalMem;
46
+ const cpuLoadPerCore = loadAvg / cpuCount;
47
+
48
+ // Determine capacity based on thresholds
49
+ // Low resources: return minimum capacity
50
+ if (memFraction < this.THRESHOLDS.MEM_LOW || cpuLoadPerCore > this.THRESHOLDS.CPU_HIGH) {
51
+ return this.THRESHOLDS.CAPACITY_MIN;
52
+ }
53
+
54
+ // Medium resources: return medium capacity
55
+ if (memFraction < this.THRESHOLDS.MEM_MEDIUM || cpuLoadPerCore > this.THRESHOLDS.CPU_MEDIUM) {
56
+ return this.THRESHOLDS.CAPACITY_MEDIUM;
57
+ }
58
+
59
+ // High resources: return maximum capacity
60
+ return this.THRESHOLDS.CAPACITY_MAX;
61
+ }
62
+ }
63
+
64
+ module.exports = SystemMonitor;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Format duration in human-readable form
3
+ * @param {number} ms - Duration in milliseconds
4
+ * @returns {string} Formatted duration
5
+ */
6
+ function formatDuration(ms) {
7
+ if (ms < 1000) return `${ms}ms`;
8
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
9
+ const minutes = Math.floor(ms / 60000);
10
+ const seconds = Math.floor((ms % 60000) / 1000);
11
+ return `${minutes}m ${seconds}s`;
12
+ }
13
+
14
+ module.exports = {
15
+ formatDuration
16
+ };