@kernel.chat/kbot 3.23.0 → 3.26.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 (85) hide show
  1. package/README.md +33 -22
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +3 -1
  4. package/dist/agent.js.map +1 -1
  5. package/dist/agents/trader.d.ts +32 -0
  6. package/dist/agents/trader.d.ts.map +1 -0
  7. package/dist/agents/trader.js +190 -0
  8. package/dist/agents/trader.js.map +1 -0
  9. package/dist/cli.js +219 -15
  10. package/dist/cli.js.map +1 -1
  11. package/dist/context.d.ts +4 -1
  12. package/dist/context.d.ts.map +1 -1
  13. package/dist/context.js +28 -1
  14. package/dist/context.js.map +1 -1
  15. package/dist/doctor.d.ts.map +1 -1
  16. package/dist/doctor.js +71 -1
  17. package/dist/doctor.js.map +1 -1
  18. package/dist/inference.d.ts +9 -0
  19. package/dist/inference.d.ts.map +1 -1
  20. package/dist/inference.js +38 -5
  21. package/dist/inference.js.map +1 -1
  22. package/dist/introspection.d.ts +17 -0
  23. package/dist/introspection.d.ts.map +1 -0
  24. package/dist/introspection.js +490 -0
  25. package/dist/introspection.js.map +1 -0
  26. package/dist/learned-router.d.ts.map +1 -1
  27. package/dist/learned-router.js +3 -0
  28. package/dist/learned-router.js.map +1 -1
  29. package/dist/machine.d.ts +85 -0
  30. package/dist/machine.d.ts.map +1 -0
  31. package/dist/machine.js +538 -0
  32. package/dist/machine.js.map +1 -0
  33. package/dist/matrix.d.ts.map +1 -1
  34. package/dist/matrix.js +11 -0
  35. package/dist/matrix.js.map +1 -1
  36. package/dist/provider-fallback.d.ts +6 -0
  37. package/dist/provider-fallback.d.ts.map +1 -1
  38. package/dist/provider-fallback.js +29 -0
  39. package/dist/provider-fallback.js.map +1 -1
  40. package/dist/synthesis-engine.d.ts +175 -0
  41. package/dist/synthesis-engine.d.ts.map +1 -0
  42. package/dist/synthesis-engine.js +783 -0
  43. package/dist/synthesis-engine.js.map +1 -0
  44. package/dist/tool-pipeline.d.ts +7 -1
  45. package/dist/tool-pipeline.d.ts.map +1 -1
  46. package/dist/tool-pipeline.js +39 -1
  47. package/dist/tool-pipeline.js.map +1 -1
  48. package/dist/tools/finance.d.ts +2 -0
  49. package/dist/tools/finance.d.ts.map +1 -0
  50. package/dist/tools/finance.js +1116 -0
  51. package/dist/tools/finance.js.map +1 -0
  52. package/dist/tools/finance.test.d.ts +2 -0
  53. package/dist/tools/finance.test.d.ts.map +1 -0
  54. package/dist/tools/finance.test.js +245 -0
  55. package/dist/tools/finance.test.js.map +1 -0
  56. package/dist/tools/index.d.ts.map +1 -1
  57. package/dist/tools/index.js +5 -0
  58. package/dist/tools/index.js.map +1 -1
  59. package/dist/tools/machine-tools.d.ts +2 -0
  60. package/dist/tools/machine-tools.d.ts.map +1 -0
  61. package/dist/tools/machine-tools.js +690 -0
  62. package/dist/tools/machine-tools.js.map +1 -0
  63. package/dist/tools/sentiment.d.ts +2 -0
  64. package/dist/tools/sentiment.d.ts.map +1 -0
  65. package/dist/tools/sentiment.js +513 -0
  66. package/dist/tools/sentiment.js.map +1 -0
  67. package/dist/tools/stocks.d.ts +2 -0
  68. package/dist/tools/stocks.d.ts.map +1 -0
  69. package/dist/tools/stocks.js +345 -0
  70. package/dist/tools/stocks.js.map +1 -0
  71. package/dist/tools/stocks.test.d.ts +2 -0
  72. package/dist/tools/stocks.test.d.ts.map +1 -0
  73. package/dist/tools/stocks.test.js +82 -0
  74. package/dist/tools/stocks.test.js.map +1 -0
  75. package/dist/tools/wallet.d.ts +2 -0
  76. package/dist/tools/wallet.d.ts.map +1 -0
  77. package/dist/tools/wallet.js +698 -0
  78. package/dist/tools/wallet.js.map +1 -0
  79. package/dist/tools/wallet.test.d.ts +2 -0
  80. package/dist/tools/wallet.test.d.ts.map +1 -0
  81. package/dist/tools/wallet.test.js +205 -0
  82. package/dist/tools/wallet.test.js.map +1 -0
  83. package/dist/ui.js +1 -1
  84. package/dist/ui.js.map +1 -1
  85. package/package.json +11 -3
@@ -0,0 +1,690 @@
1
+ // kbot Machine Tools — Runtime system-awareness tools
2
+ //
3
+ // Exposes live machine metrics as tools the agent can call:
4
+ // system_profile, memory_pressure, gpu_status, process_top,
5
+ // thermal_state, disk_health, network_check
6
+ //
7
+ // Cross-platform: macOS + Linux. Each tool handles errors gracefully.
8
+ import { execSync } from 'node:child_process';
9
+ import { existsSync, readFileSync } from 'node:fs';
10
+ import { totalmem, freemem, platform as osPlatform } from 'node:os';
11
+ import { registerTool } from './index.js';
12
+ const plat = osPlatform();
13
+ /** Safe exec — returns stdout or empty string on failure */
14
+ function exec(cmd, timeoutMs = 5000) {
15
+ try {
16
+ return execSync(cmd, { encoding: 'utf-8', timeout: timeoutMs, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
17
+ }
18
+ catch {
19
+ return '';
20
+ }
21
+ }
22
+ /** Format bytes to human-readable */
23
+ function fmtBytes(bytes) {
24
+ if (bytes >= 1024 ** 4)
25
+ return `${(bytes / 1024 ** 4).toFixed(1)} TB`;
26
+ if (bytes >= 1024 ** 3)
27
+ return `${(bytes / 1024 ** 3).toFixed(1)} GB`;
28
+ if (bytes >= 1024 ** 2)
29
+ return `${(bytes / 1024 ** 2).toFixed(0)} MB`;
30
+ if (bytes >= 1024)
31
+ return `${(bytes / 1024).toFixed(0)} KB`;
32
+ return `${bytes} B`;
33
+ }
34
+ export function registerMachineTools() {
35
+ // ─────────────────────────────────────────────────────────────────────────
36
+ // 1. system_profile — Full machine profile (cached via probeMachine)
37
+ // ─────────────────────────────────────────────────────────────────────────
38
+ registerTool({
39
+ name: 'system_profile',
40
+ description: 'Returns the full machine profile: CPU, GPU, memory, disk, OS, displays, battery, network, dev tools, and AI capability assessment. ' +
41
+ 'Cached after first call — fast on subsequent invocations. Use this to understand what hardware and software the user has.',
42
+ parameters: {},
43
+ tier: 'free',
44
+ async execute() {
45
+ try {
46
+ const { probeMachine, formatMachineProfile } = await import('../machine.js');
47
+ const profile = await probeMachine();
48
+ return formatMachineProfile(profile);
49
+ }
50
+ catch (err) {
51
+ return `Error probing machine: ${err instanceof Error ? err.message : String(err)}`;
52
+ }
53
+ },
54
+ });
55
+ // ─────────────────────────────────────────────────────────────────────────
56
+ // 2. memory_pressure — Live memory check (not cached)
57
+ // ─────────────────────────────────────────────────────────────────────────
58
+ registerTool({
59
+ name: 'memory_pressure',
60
+ description: 'Returns live memory usage: total, free, used, and pressure level (low/moderate/high). ' +
61
+ 'On macOS, also returns detailed vm_stat page statistics (active, inactive, wired, compressed, speculative pages). ' +
62
+ 'On Linux, reads /proc/meminfo for detailed breakdown. Not cached — always returns current state.',
63
+ parameters: {},
64
+ tier: 'free',
65
+ async execute() {
66
+ try {
67
+ const totalBytes = totalmem();
68
+ const freeBytes = freemem();
69
+ const usedBytes = totalBytes - freeBytes;
70
+ const freePercent = (freeBytes / totalBytes) * 100;
71
+ const pressure = freePercent > 25 ? 'low' : freePercent > 10 ? 'moderate' : 'high';
72
+ const lines = [
73
+ 'Memory Status',
74
+ ` Total: ${fmtBytes(totalBytes)}`,
75
+ ` Used: ${fmtBytes(usedBytes)} (${(100 - freePercent).toFixed(1)}%)`,
76
+ ` Free: ${fmtBytes(freeBytes)} (${freePercent.toFixed(1)}%)`,
77
+ ` Pressure: ${pressure}`,
78
+ ];
79
+ if (plat === 'darwin') {
80
+ // vm_stat gives detailed page-level memory breakdown
81
+ const vmstat = exec('vm_stat');
82
+ if (vmstat) {
83
+ lines.push('');
84
+ lines.push('vm_stat Page Statistics:');
85
+ const pageSize = parseInt(exec('sysctl -n hw.pagesize') || '16384');
86
+ const getPages = (label) => {
87
+ const match = vmstat.match(new RegExp(`${label}:\\s+(\\d+)`, 'i'));
88
+ return match ? parseInt(match[1]) : 0;
89
+ };
90
+ const active = getPages('Pages active');
91
+ const inactive = getPages('Pages inactive');
92
+ const wired = getPages('Pages wired down');
93
+ const compressed = getPages('Pages occupied by compressor');
94
+ const speculative = getPages('Pages speculative');
95
+ const pageFree = getPages('Pages free');
96
+ const pageins = getPages('Pageins');
97
+ const pageouts = getPages('Pageouts');
98
+ const swapins = getPages('Swapins');
99
+ const swapouts = getPages('Swapouts');
100
+ lines.push(` Page size: ${pageSize} bytes`);
101
+ lines.push(` Active: ${fmtBytes(active * pageSize)}`);
102
+ lines.push(` Inactive: ${fmtBytes(inactive * pageSize)}`);
103
+ lines.push(` Wired: ${fmtBytes(wired * pageSize)}`);
104
+ lines.push(` Compressed: ${fmtBytes(compressed * pageSize)}`);
105
+ lines.push(` Speculative: ${fmtBytes(speculative * pageSize)}`);
106
+ lines.push(` Free pages: ${fmtBytes(pageFree * pageSize)}`);
107
+ lines.push(` Pageins: ${pageins}`);
108
+ lines.push(` Pageouts: ${pageouts}`);
109
+ lines.push(` Swapins: ${swapins}`);
110
+ lines.push(` Swapouts: ${swapouts}`);
111
+ // Memory pressure from macOS memory_pressure command
112
+ const mpressure = exec('memory_pressure 2>/dev/null | head -1', 3000);
113
+ if (mpressure) {
114
+ lines.push(` System: ${mpressure}`);
115
+ }
116
+ }
117
+ }
118
+ else if (plat === 'linux') {
119
+ // /proc/meminfo for detailed breakdown
120
+ try {
121
+ const meminfo = readFileSync('/proc/meminfo', 'utf-8');
122
+ lines.push('');
123
+ lines.push('/proc/meminfo:');
124
+ const getValue = (key) => {
125
+ const match = meminfo.match(new RegExp(`${key}:\\s+(.+)`, 'i'));
126
+ return match ? match[1].trim() : 'N/A';
127
+ };
128
+ lines.push(` MemTotal: ${getValue('MemTotal')}`);
129
+ lines.push(` MemFree: ${getValue('MemFree')}`);
130
+ lines.push(` MemAvailable: ${getValue('MemAvailable')}`);
131
+ lines.push(` Buffers: ${getValue('Buffers')}`);
132
+ lines.push(` Cached: ${getValue('Cached')}`);
133
+ lines.push(` SwapTotal: ${getValue('SwapTotal')}`);
134
+ lines.push(` SwapFree: ${getValue('SwapFree')}`);
135
+ lines.push(` Dirty: ${getValue('Dirty')}`);
136
+ lines.push(` Shmem: ${getValue('Shmem')}`);
137
+ }
138
+ catch {
139
+ lines.push(' (Could not read /proc/meminfo)');
140
+ }
141
+ }
142
+ return lines.join('\n');
143
+ }
144
+ catch (err) {
145
+ return `Error checking memory: ${err instanceof Error ? err.message : String(err)}`;
146
+ }
147
+ },
148
+ });
149
+ // ─────────────────────────────────────────────────────────────────────────
150
+ // 3. gpu_status — Live GPU utilization
151
+ // ─────────────────────────────────────────────────────────────────────────
152
+ registerTool({
153
+ name: 'gpu_status',
154
+ description: 'Returns live GPU utilization and status. ' +
155
+ 'macOS: uses ioreg to query GPU activity, power state, and performance statistics. ' +
156
+ 'Linux: uses nvidia-smi for NVIDIA GPUs (utilization, memory, temperature, power draw). ' +
157
+ 'Useful for checking if the GPU is available for local model inference.',
158
+ parameters: {},
159
+ tier: 'free',
160
+ async execute() {
161
+ try {
162
+ const lines = ['GPU Status'];
163
+ if (plat === 'darwin') {
164
+ // ioreg for GPU activity info
165
+ const gpuInfo = exec('ioreg -rc IOAccelerator 2>/dev/null | head -80', 5000);
166
+ if (gpuInfo) {
167
+ // Extract key GPU metrics
168
+ const perfStats = exec('ioreg -rc IOAccelerator -d 1 2>/dev/null', 5000);
169
+ // Get GPU model from system_profiler (more reliable)
170
+ const spDisplay = exec('system_profiler SPDisplaysDataType 2>/dev/null', 5000);
171
+ const chipsetMatch = spDisplay.match(/Chipset Model:\s*(.+)/i);
172
+ const metalMatch = spDisplay.match(/Metal Support:\s*(.+)/i);
173
+ const coresMatch = spDisplay.match(/Total Number of Cores:\s*(\d+)/i);
174
+ if (chipsetMatch)
175
+ lines.push(` Model: ${chipsetMatch[1].trim()}`);
176
+ if (coresMatch)
177
+ lines.push(` GPU Cores: ${coresMatch[1].trim()}`);
178
+ if (metalMatch)
179
+ lines.push(` Metal: ${metalMatch[1].trim()}`);
180
+ // Device utilization from ioreg
181
+ const utilizationMatch = perfStats.match(/"Device Utilization %"\s*=\s*(\d+)/i);
182
+ if (utilizationMatch) {
183
+ lines.push(` Utilization: ${utilizationMatch[1]}%`);
184
+ }
185
+ // Power state
186
+ const powerMatch = perfStats.match(/"PerformanceStatistics"[\s\S]*?"GPU Activity\(\%\)"\s*=\s*(\d+)/);
187
+ if (powerMatch) {
188
+ lines.push(` Activity: ${powerMatch[1]}%`);
189
+ }
190
+ // Check if GPU is in use by any process
191
+ const gpuProcesses = exec('ioreg -rc IOGPUDevice 2>/dev/null | grep -c "IOGPUDevice"', 3000);
192
+ if (gpuProcesses) {
193
+ lines.push(` GPU Devices: ${gpuProcesses}`);
194
+ }
195
+ }
196
+ else {
197
+ lines.push(' (Could not query GPU via ioreg)');
198
+ }
199
+ // Also check for eGPU
200
+ const egpu = exec('system_profiler SPThunderboltDataType 2>/dev/null | grep -i gpu', 3000);
201
+ if (egpu) {
202
+ lines.push('');
203
+ lines.push(' eGPU detected via Thunderbolt');
204
+ }
205
+ }
206
+ else if (plat === 'linux') {
207
+ // Try nvidia-smi first (NVIDIA GPUs)
208
+ const nvidiaSmi = exec('nvidia-smi --query-gpu=name,utilization.gpu,utilization.memory,memory.total,memory.used,memory.free,temperature.gpu,power.draw,power.limit --format=csv,noheader 2>/dev/null', 5000);
209
+ if (nvidiaSmi) {
210
+ for (const line of nvidiaSmi.split('\n').filter(Boolean)) {
211
+ const parts = line.split(',').map(s => s.trim());
212
+ if (parts.length >= 9) {
213
+ lines.push(` Model: ${parts[0]}`);
214
+ lines.push(` GPU Util: ${parts[1]}`);
215
+ lines.push(` Memory Util: ${parts[2]}`);
216
+ lines.push(` VRAM Total: ${parts[3]}`);
217
+ lines.push(` VRAM Used: ${parts[4]}`);
218
+ lines.push(` VRAM Free: ${parts[5]}`);
219
+ lines.push(` Temperature: ${parts[6]} C`);
220
+ lines.push(` Power Draw: ${parts[7]}`);
221
+ lines.push(` Power Limit: ${parts[8]}`);
222
+ }
223
+ }
224
+ // Running GPU processes
225
+ const procs = exec('nvidia-smi --query-compute-apps=pid,name,used_memory --format=csv,noheader 2>/dev/null', 3000);
226
+ if (procs) {
227
+ lines.push('');
228
+ lines.push(' GPU Processes:');
229
+ for (const proc of procs.split('\n').filter(Boolean)) {
230
+ lines.push(` ${proc.trim()}`);
231
+ }
232
+ }
233
+ }
234
+ else {
235
+ // Fallback: check lspci for any GPU
236
+ const lspci = exec('lspci 2>/dev/null | grep -iE "VGA|3D|Display"', 3000);
237
+ if (lspci) {
238
+ lines.push(' Detected GPUs (no live stats available):');
239
+ for (const line of lspci.split('\n').filter(Boolean)) {
240
+ const match = line.match(/:\s*(.+)/);
241
+ if (match)
242
+ lines.push(` ${match[1].trim()}`);
243
+ }
244
+ }
245
+ else {
246
+ lines.push(' No GPU detected');
247
+ }
248
+ }
249
+ }
250
+ else {
251
+ lines.push(` Unsupported platform: ${plat}`);
252
+ }
253
+ return lines.join('\n');
254
+ }
255
+ catch (err) {
256
+ return `Error checking GPU: ${err instanceof Error ? err.message : String(err)}`;
257
+ }
258
+ },
259
+ });
260
+ // ─────────────────────────────────────────────────────────────────────────
261
+ // 4. process_top — Top N processes by CPU or memory
262
+ // ─────────────────────────────────────────────────────────────────────────
263
+ registerTool({
264
+ name: 'process_top',
265
+ description: 'Returns the top N processes sorted by CPU usage or memory usage. ' +
266
+ 'Useful for diagnosing performance issues, finding runaway processes, or checking resource consumption. ' +
267
+ 'Defaults to top 10 by CPU.',
268
+ parameters: {
269
+ sort_by: {
270
+ type: 'string',
271
+ description: 'Sort criteria: "cpu" (default) or "memory"',
272
+ },
273
+ count: {
274
+ type: 'number',
275
+ description: 'Number of processes to return (default: 10, max: 50)',
276
+ },
277
+ },
278
+ tier: 'free',
279
+ async execute(args) {
280
+ try {
281
+ const sortBy = args.sort_by === 'memory' ? 'memory' : 'cpu';
282
+ const count = Math.min(Math.max(parseInt(String(args.count || '10')), 1), 50);
283
+ let output;
284
+ if (plat === 'darwin') {
285
+ // macOS ps doesn't support --sort, use pipe to sort
286
+ const sortFlag = sortBy === 'memory' ? '-m' : '-r';
287
+ // Use top in logging mode for accurate stats on macOS
288
+ const psOutput = exec(`ps aux ${sortFlag} | head -${count + 1}`, 5000);
289
+ if (psOutput) {
290
+ output = `Top ${count} processes by ${sortBy}:\n\n${psOutput}`;
291
+ }
292
+ else {
293
+ output = `Could not retrieve process list`;
294
+ }
295
+ }
296
+ else {
297
+ // Linux ps supports --sort
298
+ const sortFlag = sortBy === 'memory' ? '--sort=-pmem' : '--sort=-pcpu';
299
+ const psOutput = exec(`ps aux ${sortFlag} | head -${count + 1}`, 5000);
300
+ if (psOutput) {
301
+ output = `Top ${count} processes by ${sortBy}:\n\n${psOutput}`;
302
+ }
303
+ else {
304
+ output = `Could not retrieve process list`;
305
+ }
306
+ }
307
+ // Add load average
308
+ const loadAvg = exec('uptime');
309
+ if (loadAvg) {
310
+ output += `\n\nSystem load: ${loadAvg}`;
311
+ }
312
+ return output;
313
+ }
314
+ catch (err) {
315
+ return `Error listing processes: ${err instanceof Error ? err.message : String(err)}`;
316
+ }
317
+ },
318
+ });
319
+ // ─────────────────────────────────────────────────────────────────────────
320
+ // 5. thermal_state — CPU thermal throttling detection
321
+ // ─────────────────────────────────────────────────────────────────────────
322
+ registerTool({
323
+ name: 'thermal_state',
324
+ description: 'Detects CPU thermal throttling state. ' +
325
+ 'macOS: reads pmset thermal log and CPU die temperature via powermetrics (if available). ' +
326
+ 'Linux: reads /sys/class/thermal/ zone temperatures and throttle counts. ' +
327
+ 'Useful for diagnosing performance issues caused by overheating.',
328
+ parameters: {},
329
+ tier: 'free',
330
+ async execute() {
331
+ try {
332
+ const lines = ['Thermal State'];
333
+ if (plat === 'darwin') {
334
+ // pmset -g thermlog shows thermal throttling events
335
+ const thermLog = exec('pmset -g thermlog 2>/dev/null', 3000);
336
+ if (thermLog) {
337
+ lines.push('');
338
+ lines.push('Thermal Log (pmset):');
339
+ // Only include the last few lines to keep it concise
340
+ const thermLines = thermLog.split('\n').filter(Boolean);
341
+ for (const line of thermLines.slice(-10)) {
342
+ lines.push(` ${line}`);
343
+ }
344
+ }
345
+ else {
346
+ lines.push(' No thermal log available from pmset');
347
+ }
348
+ // Try to get CPU temperature via thermal sensor
349
+ // Note: powermetrics requires sudo, so we try ioreg instead
350
+ const thermalSensors = exec('ioreg -rc AppleSmartBattery 2>/dev/null | grep Temperature', 3000);
351
+ if (thermalSensors) {
352
+ const tempMatch = thermalSensors.match(/"Temperature"\s*=\s*(\d+)/);
353
+ if (tempMatch) {
354
+ // Battery temperature is reported in centi-degrees
355
+ const tempC = parseInt(tempMatch[1]) / 100;
356
+ lines.push(` Battery temp: ${tempC.toFixed(1)} C`);
357
+ }
358
+ }
359
+ // CPU thermal pressure (macOS 12+)
360
+ const thermalPressure = exec('sysctl -n kern.thermal_state 2>/dev/null', 2000);
361
+ if (thermalPressure) {
362
+ const states = {
363
+ '0': 'nominal (no throttling)',
364
+ '1': 'fair (minor throttling)',
365
+ '2': 'serious (significant throttling)',
366
+ '3': 'critical (maximum throttling)',
367
+ };
368
+ lines.push(` Thermal state: ${states[thermalPressure] || thermalPressure}`);
369
+ }
370
+ // CPU speed via sysctl
371
+ const cpuFreq = exec('sysctl -n hw.cpufrequency_max 2>/dev/null', 2000);
372
+ if (cpuFreq) {
373
+ const freqGHz = (parseInt(cpuFreq) / 1e9).toFixed(2);
374
+ lines.push(` CPU max freq: ${freqGHz} GHz`);
375
+ }
376
+ }
377
+ else if (plat === 'linux') {
378
+ // Read thermal zones
379
+ const thermalZonesExist = existsSync('/sys/class/thermal/thermal_zone0');
380
+ if (thermalZonesExist) {
381
+ lines.push('');
382
+ lines.push('Thermal Zones:');
383
+ for (let i = 0; i < 10; i++) {
384
+ const zonePath = `/sys/class/thermal/thermal_zone${i}`;
385
+ if (!existsSync(zonePath))
386
+ break;
387
+ try {
388
+ const type = readFileSync(`${zonePath}/type`, 'utf-8').trim();
389
+ const temp = parseInt(readFileSync(`${zonePath}/temp`, 'utf-8').trim());
390
+ const tempC = (temp / 1000).toFixed(1);
391
+ // Check for trip points
392
+ let tripInfo = '';
393
+ const tripPath = `${zonePath}/trip_point_0_temp`;
394
+ if (existsSync(tripPath)) {
395
+ const tripTemp = parseInt(readFileSync(tripPath, 'utf-8').trim());
396
+ tripInfo = ` (trip: ${(tripTemp / 1000).toFixed(0)} C)`;
397
+ }
398
+ lines.push(` Zone ${i} (${type}): ${tempC} C${tripInfo}`);
399
+ }
400
+ catch {
401
+ // Skip unreadable zones
402
+ }
403
+ }
404
+ }
405
+ else {
406
+ lines.push(' No thermal zones found in /sys/class/thermal/');
407
+ }
408
+ // Check CPU frequency scaling (throttling indicator)
409
+ const scalingGovernor = exec('cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor 2>/dev/null', 2000);
410
+ if (scalingGovernor) {
411
+ lines.push(` CPU governor: ${scalingGovernor}`);
412
+ }
413
+ const curFreq = exec('cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq 2>/dev/null', 2000);
414
+ const maxFreq = exec('cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq 2>/dev/null', 2000);
415
+ if (curFreq && maxFreq) {
416
+ const curMHz = (parseInt(curFreq) / 1000).toFixed(0);
417
+ const maxMHz = (parseInt(maxFreq) / 1000).toFixed(0);
418
+ const throttled = parseInt(curFreq) < parseInt(maxFreq) * 0.9;
419
+ lines.push(` CPU freq: ${curMHz} MHz / ${maxMHz} MHz${throttled ? ' [THROTTLED]' : ''}`);
420
+ }
421
+ // Check for GPU thermal throttling (NVIDIA)
422
+ const gpuTemp = exec('nvidia-smi --query-gpu=temperature.gpu,temperature.gpu.tlimit --format=csv,noheader 2>/dev/null', 3000);
423
+ if (gpuTemp) {
424
+ lines.push(` GPU temp: ${gpuTemp.trim()}`);
425
+ }
426
+ }
427
+ else {
428
+ lines.push(` Unsupported platform: ${plat}`);
429
+ }
430
+ return lines.join('\n');
431
+ }
432
+ catch (err) {
433
+ return `Error checking thermal state: ${err instanceof Error ? err.message : String(err)}`;
434
+ }
435
+ },
436
+ });
437
+ // ─────────────────────────────────────────────────────────────────────────
438
+ // 6. disk_health — Disk space + I/O stats
439
+ // ─────────────────────────────────────────────────────────────────────────
440
+ registerTool({
441
+ name: 'disk_health',
442
+ description: 'Returns disk space usage for all mounted filesystems (df -h) plus disk I/O statistics. ' +
443
+ 'macOS: uses iostat for I/O throughput. Linux: reads /proc/diskstats for I/O counters. ' +
444
+ 'Useful for diagnosing storage issues, full disks, or I/O bottlenecks.',
445
+ parameters: {
446
+ path: {
447
+ type: 'string',
448
+ description: 'Specific path to check (default: all mounted filesystems)',
449
+ },
450
+ },
451
+ tier: 'free',
452
+ async execute(args) {
453
+ try {
454
+ const targetPath = args.path || '';
455
+ const lines = ['Disk Health'];
456
+ // Disk space usage
457
+ lines.push('');
458
+ lines.push('Disk Space:');
459
+ const dfCmd = targetPath ? `df -h "${targetPath}"` : 'df -h';
460
+ const dfOutput = exec(dfCmd, 5000);
461
+ if (dfOutput) {
462
+ for (const line of dfOutput.split('\n')) {
463
+ lines.push(` ${line}`);
464
+ }
465
+ }
466
+ else {
467
+ lines.push(' Could not read disk space (df failed)');
468
+ }
469
+ // I/O statistics
470
+ lines.push('');
471
+ lines.push('Disk I/O:');
472
+ if (plat === 'darwin') {
473
+ // iostat on macOS
474
+ const iostat = exec('iostat -d -c 2 -w 1 2>/dev/null', 8000);
475
+ if (iostat) {
476
+ const iostatLines = iostat.split('\n').filter(Boolean);
477
+ // Take the last set of readings (second sample for delta)
478
+ for (const line of iostatLines.slice(-5)) {
479
+ lines.push(` ${line}`);
480
+ }
481
+ }
482
+ else {
483
+ lines.push(' iostat not available');
484
+ }
485
+ }
486
+ else if (plat === 'linux') {
487
+ // /proc/diskstats for I/O counters
488
+ try {
489
+ const diskstats = readFileSync('/proc/diskstats', 'utf-8');
490
+ lines.push(' Device Reads Writes In-progress');
491
+ // Filter to show only interesting devices (sd*, nvme*, vd*)
492
+ for (const line of diskstats.split('\n')) {
493
+ const parts = line.trim().split(/\s+/);
494
+ if (parts.length >= 14) {
495
+ const device = parts[2];
496
+ if (/^(sd[a-z]|nvme\d+n\d+|vd[a-z])$/.test(device)) {
497
+ const reads = parts[3];
498
+ const writes = parts[7];
499
+ const inProgress = parts[11];
500
+ lines.push(` ${device.padEnd(13)}${reads.padStart(10)} ${writes.padStart(10)} ${inProgress.padStart(13)}`);
501
+ }
502
+ }
503
+ }
504
+ }
505
+ catch {
506
+ // Fallback to iostat
507
+ const iostat = exec('iostat -d 2>/dev/null', 5000);
508
+ if (iostat) {
509
+ for (const line of iostat.split('\n').filter(Boolean)) {
510
+ lines.push(` ${line}`);
511
+ }
512
+ }
513
+ else {
514
+ lines.push(' No I/O stats available');
515
+ }
516
+ }
517
+ }
518
+ // Warn about critically full disks
519
+ if (dfOutput) {
520
+ const criticalDisks = [];
521
+ for (const line of dfOutput.split('\n').slice(1)) {
522
+ const parts = line.trim().split(/\s+/);
523
+ if (parts.length >= 5) {
524
+ const usedPercent = parseInt(parts[4]);
525
+ if (usedPercent >= 90) {
526
+ criticalDisks.push(`${parts[5] || parts[0]}: ${parts[4]} used`);
527
+ }
528
+ }
529
+ }
530
+ if (criticalDisks.length > 0) {
531
+ lines.push('');
532
+ lines.push('WARNING — Critical disk usage:');
533
+ for (const d of criticalDisks) {
534
+ lines.push(` ${d}`);
535
+ }
536
+ }
537
+ }
538
+ return lines.join('\n');
539
+ }
540
+ catch (err) {
541
+ return `Error checking disk health: ${err instanceof Error ? err.message : String(err)}`;
542
+ }
543
+ },
544
+ });
545
+ // ─────────────────────────────────────────────────────────────────────────
546
+ // 7. network_check — Connectivity check
547
+ // ─────────────────────────────────────────────────────────────────────────
548
+ registerTool({
549
+ name: 'network_check',
550
+ description: 'Performs a connectivity check: ping test (1.1.1.1 and 8.8.8.8), DNS resolution, WiFi signal strength, ' +
551
+ 'and active connection count. Useful for diagnosing network issues or verifying internet access before API calls.',
552
+ parameters: {
553
+ host: {
554
+ type: 'string',
555
+ description: 'Optional host to ping (default: 1.1.1.1)',
556
+ },
557
+ },
558
+ tier: 'free',
559
+ async execute(args) {
560
+ try {
561
+ const host = args.host || '1.1.1.1';
562
+ const lines = ['Network Check'];
563
+ // Ping test
564
+ lines.push('');
565
+ lines.push('Connectivity:');
566
+ const pingResult = exec(`ping -c 3 -W 3 ${host} 2>/dev/null`, 10000);
567
+ if (pingResult) {
568
+ // Extract summary line
569
+ const summaryMatch = pingResult.match(/(\d+) packets transmitted.*?(\d+) (?:packets )?received/);
570
+ const rttMatch = pingResult.match(/(?:rtt|round-trip).*?=\s*([^\n]+)/);
571
+ if (summaryMatch) {
572
+ const sent = summaryMatch[1];
573
+ const recv = summaryMatch[2];
574
+ const loss = sent !== recv ? ` (${parseInt(sent) - parseInt(recv)} lost)` : '';
575
+ lines.push(` Ping ${host}: ${recv}/${sent} packets received${loss}`);
576
+ }
577
+ if (rttMatch) {
578
+ lines.push(` RTT: ${rttMatch[1].trim()}`);
579
+ }
580
+ }
581
+ else {
582
+ lines.push(` Ping ${host}: FAILED (no response)`);
583
+ }
584
+ // DNS resolution test
585
+ lines.push('');
586
+ lines.push('DNS Resolution:');
587
+ const dnsTargets = ['google.com', 'cloudflare.com', 'github.com'];
588
+ for (const target of dnsTargets) {
589
+ const dnsResult = exec(`dig +short ${target} A 2>/dev/null | head -1`, 3000);
590
+ if (dnsResult) {
591
+ lines.push(` ${target.padEnd(18)} ${dnsResult}`);
592
+ }
593
+ else {
594
+ // Fallback to host command
595
+ const hostResult = exec(`host -W 2 ${target} 2>/dev/null | head -1`, 3000);
596
+ if (hostResult && hostResult.includes('has address')) {
597
+ const ip = hostResult.match(/has address\s+(.+)/)?.[1] || 'resolved';
598
+ lines.push(` ${target.padEnd(18)} ${ip}`);
599
+ }
600
+ else {
601
+ lines.push(` ${target.padEnd(18)} FAILED`);
602
+ }
603
+ }
604
+ }
605
+ // WiFi signal strength
606
+ lines.push('');
607
+ lines.push('WiFi:');
608
+ if (plat === 'darwin') {
609
+ const wifiInfo = exec('/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -I 2>/dev/null', 3000);
610
+ if (wifiInfo) {
611
+ const ssid = wifiInfo.match(/\bSSID:\s*(.+)/i)?.[1]?.trim();
612
+ const rssi = wifiInfo.match(/agrCtlRSSI:\s*(-?\d+)/)?.[1];
613
+ const noise = wifiInfo.match(/agrCtlNoise:\s*(-?\d+)/)?.[1];
614
+ const channel = wifiInfo.match(/channel:\s*(.+)/i)?.[1]?.trim();
615
+ const txRate = wifiInfo.match(/lastTxRate:\s*(\d+)/)?.[1];
616
+ if (ssid)
617
+ lines.push(` SSID: ${ssid}`);
618
+ if (rssi) {
619
+ const rssiNum = parseInt(rssi);
620
+ const quality = rssiNum >= -50 ? 'excellent' : rssiNum >= -60 ? 'good' : rssiNum >= -70 ? 'fair' : 'poor';
621
+ lines.push(` Signal: ${rssi} dBm (${quality})`);
622
+ }
623
+ if (noise)
624
+ lines.push(` Noise: ${noise} dBm`);
625
+ if (channel)
626
+ lines.push(` Channel: ${channel}`);
627
+ if (txRate)
628
+ lines.push(` Tx Rate: ${txRate} Mbps`);
629
+ }
630
+ else {
631
+ // Fallback: networksetup
632
+ const ssid = exec('networksetup -getairportnetwork en0 2>/dev/null', 3000);
633
+ const ssidName = ssid.match(/Current Wi-Fi Network:\s*(.+)/)?.[1];
634
+ lines.push(` SSID: ${ssidName || 'not connected'}`);
635
+ }
636
+ }
637
+ else if (plat === 'linux') {
638
+ const iwconfig = exec('iwconfig 2>/dev/null | grep -A5 "ESSID"', 3000);
639
+ if (iwconfig) {
640
+ const ssid = iwconfig.match(/ESSID:"(.+?)"/)?.[1];
641
+ const signalMatch = iwconfig.match(/Signal level[=:](-?\d+)/i);
642
+ const bitRate = iwconfig.match(/Bit Rate[=:](\S+)/i)?.[1];
643
+ if (ssid)
644
+ lines.push(` SSID: ${ssid}`);
645
+ if (signalMatch) {
646
+ const rssi = parseInt(signalMatch[1]);
647
+ const quality = rssi >= -50 ? 'excellent' : rssi >= -60 ? 'good' : rssi >= -70 ? 'fair' : 'poor';
648
+ lines.push(` Signal: ${signalMatch[1]} dBm (${quality})`);
649
+ }
650
+ if (bitRate)
651
+ lines.push(` Bit Rate: ${bitRate}`);
652
+ }
653
+ else {
654
+ const nmcli = exec('nmcli -t -f active,ssid,signal dev wifi 2>/dev/null | grep "^yes"', 3000);
655
+ if (nmcli) {
656
+ const parts = nmcli.split(':');
657
+ if (parts.length >= 3) {
658
+ lines.push(` SSID: ${parts[1]}`);
659
+ lines.push(` Signal: ${parts[2]}%`);
660
+ }
661
+ }
662
+ else {
663
+ lines.push(' WiFi info not available');
664
+ }
665
+ }
666
+ }
667
+ // Active connections count
668
+ lines.push('');
669
+ lines.push('Active Connections:');
670
+ if (plat === 'darwin') {
671
+ const established = exec('netstat -an 2>/dev/null | grep -c ESTABLISHED', 3000);
672
+ const listening = exec('netstat -an 2>/dev/null | grep -c LISTEN', 3000);
673
+ lines.push(` Established: ${established || '0'}`);
674
+ lines.push(` Listening: ${listening || '0'}`);
675
+ }
676
+ else {
677
+ const established = exec('ss -t state established 2>/dev/null | tail -n +2 | wc -l', 3000);
678
+ const listening = exec('ss -t state listening 2>/dev/null | tail -n +2 | wc -l', 3000);
679
+ lines.push(` Established: ${established?.trim() || '0'}`);
680
+ lines.push(` Listening: ${listening?.trim() || '0'}`);
681
+ }
682
+ return lines.join('\n');
683
+ }
684
+ catch (err) {
685
+ return `Error checking network: ${err instanceof Error ? err.message : String(err)}`;
686
+ }
687
+ },
688
+ });
689
+ }
690
+ //# sourceMappingURL=machine-tools.js.map