@covibes/zeroshot 1.0.2 → 1.1.3

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.
@@ -0,0 +1,546 @@
1
+ /**
2
+ * ProcessMetrics - Cross-platform real-time process monitoring
3
+ *
4
+ * Provides:
5
+ * - CPU usage (percent)
6
+ * - Memory usage (MB)
7
+ * - Network I/O (bytes/sec)
8
+ * - Process state (running, sleeping, etc.)
9
+ * - Child process aggregation
10
+ *
11
+ * Supports:
12
+ * - Linux: /proc filesystem + ss
13
+ * - macOS: ps + lsof
14
+ */
15
+
16
+ const { execSync } = require('child_process');
17
+ const fs = require('fs');
18
+
19
+ const PLATFORM = process.platform;
20
+
21
+ /**
22
+ * @typedef {Object} ProcessMetrics
23
+ * @property {number} pid - Process ID
24
+ * @property {boolean} exists - Whether process exists
25
+ * @property {number} cpuPercent - CPU usage (0-100)
26
+ * @property {number} memoryMB - Memory usage in MB
27
+ * @property {string} state - Process state (R=running, S=sleeping, etc.)
28
+ * @property {number} threads - Thread count
29
+ * @property {Object} network - Network activity
30
+ * @property {number} network.established - Established connections
31
+ * @property {boolean} network.hasActivity - Has data in flight
32
+ * @property {number} network.sendQueueBytes - Bytes in send queue
33
+ * @property {number} network.recvQueueBytes - Bytes in receive queue
34
+ * @property {number} childCount - Number of child processes
35
+ * @property {number} timestamp - Measurement timestamp
36
+ */
37
+
38
+ /**
39
+ * Get all child PIDs for a process (recursive)
40
+ * @param {number} pid - Parent process ID
41
+ * @returns {number[]} Array of child PIDs
42
+ */
43
+ function getChildPids(pid) {
44
+ const children = [];
45
+
46
+ try {
47
+ if (PLATFORM === 'darwin') {
48
+ // macOS: Use pgrep with -P flag
49
+ const output = execSync(`pgrep -P ${pid} 2>/dev/null`, {
50
+ encoding: 'utf8',
51
+ timeout: 2000,
52
+ });
53
+ const pids = output.trim().split('\n').filter(Boolean).map(Number);
54
+ children.push(...pids);
55
+
56
+ // Recursively get grandchildren
57
+ for (const childPid of pids) {
58
+ children.push(...getChildPids(childPid));
59
+ }
60
+ } else {
61
+ // Linux: Read /proc/{pid}/task/{tid}/children
62
+ const taskPath = `/proc/${pid}/task`;
63
+ if (fs.existsSync(taskPath)) {
64
+ const tids = fs.readdirSync(taskPath);
65
+ for (const tid of tids) {
66
+ const childrenPath = `/proc/${pid}/task/${tid}/children`;
67
+ if (fs.existsSync(childrenPath)) {
68
+ const childPids = fs.readFileSync(childrenPath, 'utf8')
69
+ .trim()
70
+ .split(/\s+/)
71
+ .filter(Boolean)
72
+ .map(Number);
73
+ children.push(...childPids);
74
+
75
+ // Recursively get grandchildren
76
+ for (const childPid of childPids) {
77
+ children.push(...getChildPids(childPid));
78
+ }
79
+ }
80
+ }
81
+ }
82
+ }
83
+ } catch {
84
+ // Ignore errors (process may have exited)
85
+ }
86
+
87
+ return [...new Set(children)]; // Dedupe
88
+ }
89
+
90
+ /**
91
+ * Get metrics for a single process (Linux)
92
+ * @param {number} pid - Process ID
93
+ * @returns {Object|null} Metrics or null if process doesn't exist
94
+ */
95
+ function getProcessMetricsLinux(pid) {
96
+ try {
97
+ const statPath = `/proc/${pid}/stat`;
98
+ if (!fs.existsSync(statPath)) {
99
+ return null;
100
+ }
101
+
102
+ const stat = fs.readFileSync(statPath, 'utf8');
103
+ const parts = stat.split(' ');
104
+
105
+ // state is field 3 (index 2)
106
+ const state = parts[2];
107
+
108
+ // utime (14) + stime (15) = CPU ticks
109
+ const utime = parseInt(parts[13], 10);
110
+ const stime = parseInt(parts[14], 10);
111
+ const cpuTicks = utime + stime;
112
+
113
+ // Read status for memory and threads
114
+ const status = fs.readFileSync(`/proc/${pid}/status`, 'utf8');
115
+ const vmRss = status.match(/VmRSS:\s+(\d+)/)?.[1] || '0';
116
+ const threads = status.match(/Threads:\s+(\d+)/)?.[1] || '1';
117
+
118
+ return {
119
+ pid,
120
+ exists: true,
121
+ state,
122
+ cpuTicks,
123
+ memoryKB: parseInt(vmRss, 10),
124
+ threads: parseInt(threads, 10),
125
+ };
126
+ } catch {
127
+ return null;
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Get metrics for a single process (macOS)
133
+ * @param {number} pid - Process ID
134
+ * @returns {Object|null} Metrics or null if process doesn't exist
135
+ */
136
+ function getProcessMetricsDarwin(pid) {
137
+ try {
138
+ // ps -p PID -o %cpu=,rss=,state=
139
+ const output = execSync(`ps -p ${pid} -o %cpu=,rss=,state= 2>/dev/null`, {
140
+ encoding: 'utf8',
141
+ timeout: 2000,
142
+ });
143
+
144
+ if (!output.trim()) {
145
+ return null;
146
+ }
147
+
148
+ const parts = output.trim().split(/\s+/);
149
+ const cpuPercent = parseFloat(parts[0]) || 0;
150
+ const rssKB = parseInt(parts[1], 10) || 0;
151
+ const state = parts[2]?.[0] || 'S'; // First char (R, S, etc.)
152
+
153
+ return {
154
+ pid,
155
+ exists: true,
156
+ state,
157
+ cpuPercent, // macOS ps gives us percent directly
158
+ memoryKB: rssKB,
159
+ threads: 1, // ps doesn't give thread count easily
160
+ };
161
+ } catch {
162
+ return null;
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Get network state for a process (Linux)
168
+ * Uses ss -tip to get extended TCP info including cumulative bytes sent/received
169
+ * @param {number} pid - Process ID
170
+ * @returns {Object} Network state
171
+ */
172
+ function getNetworkStateLinux(pid) {
173
+ const result = {
174
+ established: 0,
175
+ hasActivity: false,
176
+ sendQueueBytes: 0,
177
+ recvQueueBytes: 0,
178
+ bytesSent: 0, // Cumulative bytes sent across all sockets
179
+ bytesReceived: 0, // Cumulative bytes received across all sockets
180
+ };
181
+
182
+ try {
183
+ // Use ss -tip to get extended TCP info with bytes_sent/bytes_received
184
+ // -t = TCP only, -i = show internal TCP info, -p = show process
185
+ const output = execSync(`ss -tip 2>/dev/null | grep -A1 "pid=${pid}," || true`, {
186
+ encoding: 'utf8',
187
+ timeout: 3000,
188
+ });
189
+
190
+ if (!output.trim()) {
191
+ return result;
192
+ }
193
+
194
+ const lines = output.trim().split('\n');
195
+
196
+ for (let i = 0; i < lines.length; i++) {
197
+ const line = lines[i];
198
+
199
+ // Parse socket line: State Recv-Q Send-Q Local:Port Peer:Port Process
200
+ const match = line.match(/^(\S+)\s+(\d+)\s+(\d+)\s+/);
201
+ if (match) {
202
+ const state = match[1];
203
+ const recvQ = parseInt(match[2], 10);
204
+ const sendQ = parseInt(match[3], 10);
205
+
206
+ if (state === 'ESTAB') {
207
+ result.established++;
208
+ }
209
+
210
+ result.recvQueueBytes += recvQ;
211
+ result.sendQueueBytes += sendQ;
212
+
213
+ if (recvQ > 0 || sendQ > 0) {
214
+ result.hasActivity = true;
215
+ }
216
+ }
217
+
218
+ // Parse extended TCP info line (follows socket line)
219
+ // Contains: bytes_sent:N bytes_received:N (and other metrics)
220
+ const bytesSentMatch = line.match(/bytes_sent:(\d+)/);
221
+ const bytesReceivedMatch = line.match(/bytes_received:(\d+)/);
222
+
223
+ if (bytesSentMatch) {
224
+ result.bytesSent += parseInt(bytesSentMatch[1], 10);
225
+ result.hasActivity = true;
226
+ }
227
+ if (bytesReceivedMatch) {
228
+ result.bytesReceived += parseInt(bytesReceivedMatch[1], 10);
229
+ result.hasActivity = true;
230
+ }
231
+ }
232
+ } catch {
233
+ // Ignore errors
234
+ }
235
+
236
+ return result;
237
+ }
238
+
239
+ /**
240
+ * Get network state for a process (macOS)
241
+ * Note: macOS doesn't expose per-socket byte counts like Linux's ss -tip
242
+ * We return 0 for bytesSent/bytesReceived (not available without dtrace/nettop)
243
+ * @param {number} pid - Process ID
244
+ * @returns {Object} Network state
245
+ */
246
+ function getNetworkStateDarwin(pid) {
247
+ const result = {
248
+ established: 0,
249
+ hasActivity: false,
250
+ sendQueueBytes: 0,
251
+ recvQueueBytes: 0,
252
+ bytesSent: 0, // Not available on macOS without root/dtrace
253
+ bytesReceived: 0, // Not available on macOS without root/dtrace
254
+ };
255
+
256
+ try {
257
+ // lsof -i -n -P for network connections
258
+ const output = execSync(`lsof -i -n -P -a -p ${pid} 2>/dev/null || true`, {
259
+ encoding: 'utf8',
260
+ timeout: 3000,
261
+ });
262
+
263
+ if (!output.trim()) {
264
+ return result;
265
+ }
266
+
267
+ const lines = output.trim().split('\n').slice(1); // Skip header
268
+
269
+ for (const line of lines) {
270
+ const parts = line.split(/\s+/);
271
+ // Look for ESTABLISHED connections
272
+ if (parts.includes('ESTABLISHED') || parts.some(p => p.includes('->'))) {
273
+ result.established++;
274
+ result.hasActivity = true; // lsof doesn't show queue sizes, assume activity
275
+ }
276
+ }
277
+ } catch {
278
+ // Ignore errors
279
+ }
280
+
281
+ return result;
282
+ }
283
+
284
+ /**
285
+ * Get real-time metrics for a process and its children
286
+ * @param {number} pid - Process ID
287
+ * @param {Object} [options] - Options
288
+ * @param {number} [options.samplePeriodMs=1000] - Sample period for rate calculations (Linux only)
289
+ * @returns {Promise<ProcessMetrics>}
290
+ */
291
+ function getProcessMetrics(pid, options = {}) {
292
+ const samplePeriodMs = options.samplePeriodMs || 1000;
293
+
294
+ if (PLATFORM === 'darwin') {
295
+ return getProcessMetricsDarwinAggregated(pid);
296
+ }
297
+
298
+ return getProcessMetricsLinuxAggregated(pid, samplePeriodMs);
299
+ }
300
+
301
+ /**
302
+ * Get aggregated metrics for process tree (Linux)
303
+ * @param {number} pid - Root process ID
304
+ * @param {number} samplePeriodMs - Sample period for CPU calculation
305
+ * @returns {Promise<ProcessMetrics>}
306
+ */
307
+ async function getProcessMetricsLinuxAggregated(pid, samplePeriodMs) {
308
+ // Get initial CPU sample
309
+ const allPids = [pid, ...getChildPids(pid)];
310
+ const t0Metrics = {};
311
+
312
+ for (const p of allPids) {
313
+ const m = getProcessMetricsLinux(p);
314
+ if (m) t0Metrics[p] = m;
315
+ }
316
+
317
+ if (Object.keys(t0Metrics).length === 0) {
318
+ return {
319
+ pid,
320
+ exists: false,
321
+ cpuPercent: 0,
322
+ memoryMB: 0,
323
+ state: 'X',
324
+ threads: 0,
325
+ network: { established: 0, hasActivity: false, sendQueueBytes: 0, recvQueueBytes: 0, bytesSent: 0, bytesReceived: 0 },
326
+ childCount: 0,
327
+ timestamp: Date.now(),
328
+ };
329
+ }
330
+
331
+ // Wait for sample period
332
+ await new Promise(r => setTimeout(r, samplePeriodMs));
333
+
334
+ // Get second CPU sample
335
+ const t1Metrics = {};
336
+ const currentPids = [pid, ...getChildPids(pid)];
337
+
338
+ for (const p of currentPids) {
339
+ const m = getProcessMetricsLinux(p);
340
+ if (m) t1Metrics[p] = m;
341
+ }
342
+
343
+ // Calculate aggregated metrics
344
+ let totalCpuTicksDelta = 0;
345
+ let totalMemoryKB = 0;
346
+ let totalThreads = 0;
347
+ let rootState = 'S';
348
+
349
+ for (const p of Object.keys(t1Metrics)) {
350
+ const t1 = t1Metrics[p];
351
+ const t0 = t0Metrics[p];
352
+
353
+ if (t0 && t1) {
354
+ totalCpuTicksDelta += t1.cpuTicks - t0.cpuTicks;
355
+ }
356
+
357
+ totalMemoryKB += t1.memoryKB;
358
+ totalThreads += t1.threads;
359
+
360
+ if (p === String(pid)) {
361
+ rootState = t1.state;
362
+ }
363
+ }
364
+
365
+ // Calculate CPU percent
366
+ const clockTicks = 100; // Usually 100 on Linux
367
+ const cpuSeconds = totalCpuTicksDelta / clockTicks;
368
+ const sampleSeconds = samplePeriodMs / 1000;
369
+ const cpuPercent = (cpuSeconds / sampleSeconds) * 100;
370
+
371
+ // Get network state for all processes
372
+ let network = { established: 0, hasActivity: false, sendQueueBytes: 0, recvQueueBytes: 0, bytesSent: 0, bytesReceived: 0 };
373
+ for (const p of Object.keys(t1Metrics)) {
374
+ const netState = getNetworkStateLinux(parseInt(p, 10));
375
+ network.established += netState.established;
376
+ network.sendQueueBytes += netState.sendQueueBytes;
377
+ network.recvQueueBytes += netState.recvQueueBytes;
378
+ network.bytesSent += netState.bytesSent;
379
+ network.bytesReceived += netState.bytesReceived;
380
+ if (netState.hasActivity) network.hasActivity = true;
381
+ }
382
+
383
+ return {
384
+ pid,
385
+ exists: true,
386
+ cpuPercent: parseFloat(cpuPercent.toFixed(1)),
387
+ memoryMB: parseFloat((totalMemoryKB / 1024).toFixed(1)),
388
+ state: rootState,
389
+ threads: totalThreads,
390
+ network,
391
+ childCount: Object.keys(t1Metrics).length - 1,
392
+ timestamp: Date.now(),
393
+ };
394
+ }
395
+
396
+ /**
397
+ * Get aggregated metrics for process tree (macOS)
398
+ * @param {number} pid - Root process ID
399
+ * @returns {Promise<ProcessMetrics>}
400
+ */
401
+ function getProcessMetricsDarwinAggregated(pid) {
402
+ const allPids = [pid, ...getChildPids(pid)];
403
+ let totalCpuPercent = 0;
404
+ let totalMemoryKB = 0;
405
+ let totalThreads = 0;
406
+ let rootState = 'S';
407
+ let existsCount = 0;
408
+
409
+ for (const p of allPids) {
410
+ const m = getProcessMetricsDarwin(p);
411
+ if (m) {
412
+ existsCount++;
413
+ totalCpuPercent += m.cpuPercent;
414
+ totalMemoryKB += m.memoryKB;
415
+ totalThreads += m.threads;
416
+
417
+ if (p === pid) {
418
+ rootState = m.state;
419
+ }
420
+ }
421
+ }
422
+
423
+ if (existsCount === 0) {
424
+ return {
425
+ pid,
426
+ exists: false,
427
+ cpuPercent: 0,
428
+ memoryMB: 0,
429
+ state: 'X',
430
+ threads: 0,
431
+ network: { established: 0, hasActivity: false, sendQueueBytes: 0, recvQueueBytes: 0 },
432
+ childCount: 0,
433
+ timestamp: Date.now(),
434
+ };
435
+ }
436
+
437
+ // Get network state
438
+ let network = { established: 0, hasActivity: false, sendQueueBytes: 0, recvQueueBytes: 0, bytesSent: 0, bytesReceived: 0 };
439
+ for (const p of allPids) {
440
+ const netState = getNetworkStateDarwin(p);
441
+ network.established += netState.established;
442
+ network.bytesSent += netState.bytesSent;
443
+ network.bytesReceived += netState.bytesReceived;
444
+ if (netState.hasActivity) network.hasActivity = true;
445
+ }
446
+
447
+ return {
448
+ pid,
449
+ exists: true,
450
+ cpuPercent: parseFloat(totalCpuPercent.toFixed(1)),
451
+ memoryMB: parseFloat((totalMemoryKB / 1024).toFixed(1)),
452
+ state: rootState,
453
+ threads: totalThreads,
454
+ network,
455
+ childCount: existsCount - 1,
456
+ timestamp: Date.now(),
457
+ };
458
+ }
459
+
460
+ /**
461
+ * Format metrics for display
462
+ * @param {ProcessMetrics} metrics
463
+ * @returns {string} Formatted string
464
+ */
465
+ function formatMetrics(metrics) {
466
+ if (!metrics.exists) {
467
+ return '(process exited)';
468
+ }
469
+
470
+ const parts = [];
471
+
472
+ // CPU
473
+ parts.push(`CPU: ${metrics.cpuPercent}%`);
474
+
475
+ // Memory
476
+ parts.push(`Mem: ${metrics.memoryMB}MB`);
477
+
478
+ // Network
479
+ if (metrics.network.established > 0) {
480
+ parts.push(`Net: ${metrics.network.established} conn`);
481
+ if (metrics.network.hasActivity) {
482
+ parts.push('↕');
483
+ }
484
+ }
485
+
486
+ // Children
487
+ if (metrics.childCount > 0) {
488
+ parts.push(`+${metrics.childCount} child`);
489
+ }
490
+
491
+ return parts.join(' │ ');
492
+ }
493
+
494
+ /**
495
+ * Get state icon for process state
496
+ * @param {string} state - Process state char
497
+ * @returns {string} Icon
498
+ */
499
+ function getStateIcon(state) {
500
+ switch (state) {
501
+ case 'R': return '🟢'; // Running
502
+ case 'S': return '🔵'; // Sleeping
503
+ case 'D': return '🟡'; // Disk wait
504
+ case 'Z': return '💀'; // Zombie
505
+ case 'T': return '⏸️'; // Stopped
506
+ case 'X': return '❌'; // Dead
507
+ default: return '⚪';
508
+ }
509
+ }
510
+
511
+ /**
512
+ * Check if platform is supported for full metrics
513
+ * @returns {boolean}
514
+ */
515
+ function isPlatformSupported() {
516
+ return PLATFORM === 'linux' || PLATFORM === 'darwin';
517
+ }
518
+
519
+ /**
520
+ * Get platform-specific metrics provider info
521
+ * @returns {Object}
522
+ */
523
+ function getPlatformInfo() {
524
+ return {
525
+ platform: PLATFORM,
526
+ supported: isPlatformSupported(),
527
+ cpuSource: PLATFORM === 'linux' ? '/proc/stat' : 'ps',
528
+ memorySource: PLATFORM === 'linux' ? '/proc/status' : 'ps',
529
+ networkSource: PLATFORM === 'linux' ? 'ss' : 'lsof',
530
+ ioSupported: PLATFORM === 'linux', // I/O only on Linux
531
+ };
532
+ }
533
+
534
+ module.exports = {
535
+ getProcessMetrics,
536
+ getChildPids,
537
+ formatMetrics,
538
+ getStateIcon,
539
+ isPlatformSupported,
540
+ getPlatformInfo,
541
+ // Export internal functions for testing
542
+ getProcessMetricsLinux,
543
+ getProcessMetricsDarwin,
544
+ getNetworkStateLinux,
545
+ getNetworkStateDarwin,
546
+ };