pyre-cli 9.5.2 → 10.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.
- package/README.md +73 -16
- package/dist/alertChannels-TPSQIJ7S.js +8 -0
- package/dist/{blender-7UT5J7IP.js → blender-FGH3ASEQ.js} +2 -2
- package/dist/brewHealth-P2GW5Y5C.js +176 -0
- package/dist/buildCmd-CDPZ2ZFA.js +38 -0
- package/dist/{checkCmd-T7O4KXXA.js → checkCmd-RKW6QGDR.js} +9 -11
- package/dist/{chunk-CXL4GYGQ.js → chunk-53O2RGSL.js} +44 -17
- package/dist/{chunk-3TE7MJLS.js → chunk-6UHZP4QP.js} +36 -10
- package/dist/chunk-GHRDW646.js +2506 -0
- package/dist/{prometheus-LFYHF6VO.js → chunk-HT4VQ4OH.js} +1 -0
- package/dist/chunk-M73HCQD2.js +95 -0
- package/dist/chunk-MZO5QKUR.js +32 -0
- package/dist/{chunk-25I533OF.js → chunk-VIACN2RU.js} +1 -1
- package/dist/chunk-Z2YV5HKV.js +87 -0
- package/dist/chunk-ZLVIVFSO.js +110 -0
- package/dist/{collectors-JB4OGUEF.js → collectors-H43PN4PS.js} +6 -3
- package/dist/{completions-VQMHQCXE.js → completions-JKYEJNGR.js} +34 -1
- package/dist/{config-KS5ZZ3VN.js → config-GYGJXU2Q.js} +9 -1
- package/dist/doctor-GYHFTNZQ.js +387 -0
- package/dist/explainCmd-MF6PIMLW.js +222 -0
- package/dist/extensions-XJGHG7X3.js +134 -0
- package/dist/index.js +460 -88
- package/dist/ioreport.py +72 -0
- package/dist/netUsageCmd-WKBTW2YG.js +90 -0
- package/dist/{pipeCmd-TYLJHQ5B.js → pipeCmd-7REDBLBU.js} +4 -3
- package/dist/profileProcCmd-N4H6MCBT.js +107 -0
- package/dist/prometheus-5BL5PVLC.js +6 -0
- package/dist/prometheusCmd-PL64BLN5.js +93 -0
- package/dist/replayCmd-Z3PYXY7V.js +164 -0
- package/dist/run-HOGQDDTA.js +8 -0
- package/dist/serveCmd-2H5VLF3P.js +153 -0
- package/dist/service-Q74BBILO.js +181 -0
- package/dist/smartCmd-ODWKSFNG.js +166 -0
- package/dist/syncCmd-A2W3QB3S.js +126 -0
- package/dist/topoCmd-DU2HHZNH.js +148 -0
- package/dist/watchdogCmd-UF53JPZB.js +161 -0
- package/dist/{wizard-Z3RGA3VB.js → wizard-TD7BGX6O.js} +1 -1
- package/install.ps1 +29 -0
- package/install.sh +10 -9
- package/package.json +7 -3
- package/src/ai/models.ts +138 -0
- package/src/alertChannels.ts +119 -0
- package/src/brewHealth.ts +157 -40
- package/src/buildCmd.ts +48 -0
- package/src/checkCmd.ts +5 -5
- package/src/completions.ts +35 -0
- package/src/doctor.ts +235 -7
- package/src/easy_features.test.ts +40 -0
- package/src/explainCmd.ts +261 -0
- package/src/extensions.ts +71 -7
- package/src/formatters/render.ts +34 -10
- package/src/formatters/types.ts +2 -0
- package/src/index.ts +251 -36
- package/src/live/input.ts +60 -0
- package/src/live/render.ts +67 -0
- package/src/live/state.ts +5 -0
- package/src/live/types.ts +1 -1
- package/src/monitors/batteryPredictor.ts +143 -0
- package/src/monitors/buildTracker.ts +121 -0
- package/src/monitors/collectors.ts +342 -178
- package/src/monitors/ioreport.py +72 -0
- package/src/monitors/platform/linux.ts +756 -0
- package/src/monitors/platform/windows.ts +519 -0
- package/src/monitors/run.ts +27 -3
- package/src/monitors/smc.ts +21 -1
- package/src/monitors/types.ts +15 -0
- package/src/netUsageCmd.ts +121 -0
- package/src/platform.test.ts +99 -0
- package/src/plugins/index.ts +143 -0
- package/src/profileProcCmd.ts +133 -0
- package/src/prometheusCmd.ts +107 -0
- package/src/replayCmd.ts +186 -0
- package/src/serveCmd.ts +180 -0
- package/src/service.ts +125 -7
- package/src/smartCmd.ts +205 -0
- package/src/state/config.ts +58 -17
- package/src/syncCmd.ts +166 -0
- package/src/topoCmd.ts +176 -0
- package/src/watchdogCmd.ts +198 -0
- package/src/xbar.ts +22 -11
- package/dist/brewHealth-F2BXF5M6.js +0 -74
- package/dist/chunk-FH5WDJBF.js +0 -15
- package/dist/chunk-N4WJVF5M.js +0 -1184
- package/dist/doctor-UV7TWIHX.js +0 -184
- package/dist/extensions-DVA5YLGH.js +0 -74
- package/dist/service-WTS3NBQU.js +0 -73
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* pyre -
|
|
4
|
-
* Monitors temps, cpu, memory, disk, battery, and system stats.
|
|
3
|
+
* pyre - Cross-platform system monitoring CLI
|
|
4
|
+
* Monitors temps, cpu, memory, disk, battery, and system stats on macOS, Linux, and Windows.
|
|
5
5
|
* Live mode is a full interactive dashboard: graphs, pause/resume,
|
|
6
6
|
* on-demand snapshot export, and continuous CSV logging.
|
|
7
7
|
* P2P mode enables live data streaming between two systems over TCP.
|
|
@@ -18,7 +18,7 @@ import { startLive, stopLive } from './live/index.js';
|
|
|
18
18
|
import { showSplash } from './splash.js';
|
|
19
19
|
import { P2PServer, P2PClient } from './p2p/index.js';
|
|
20
20
|
import { runDoctor, printDoctorReport } from './doctor.js';
|
|
21
|
-
import { generateZshCompletions, generateBashCompletions, generateFishCompletions } from './completions.js';
|
|
21
|
+
import { generateZshCompletions, generateBashCompletions, generateFishCompletions, generatePowerShellCompletions } from './completions.js';
|
|
22
22
|
import { runHistoryCommand } from './historyCmd.js';
|
|
23
23
|
import { runDiffCommand } from './diffCmd.js';
|
|
24
24
|
import { readConfig, CONFIG_FILE } from './state/config.js';
|
|
@@ -34,7 +34,7 @@ const program = new Command();
|
|
|
34
34
|
program
|
|
35
35
|
.name('pyre')
|
|
36
36
|
.version(pkg.version)
|
|
37
|
-
.description('
|
|
37
|
+
.description('Cross-platform system monitoring CLI: interactive live dashboard, stats, graphs, export, packet monitor, battery predictor')
|
|
38
38
|
.option('-j, --json', 'Output as JSON')
|
|
39
39
|
.option('--html', 'Output as HTML')
|
|
40
40
|
.option('--md', 'Output as Markdown')
|
|
@@ -79,7 +79,23 @@ program
|
|
|
79
79
|
.option('--no-wizard', 'Skip first-run interactive setup wizard')
|
|
80
80
|
.option('--install', 'Install pyre web as a background launchd agent')
|
|
81
81
|
.option('--uninstall', 'Uninstall pyre web launchd agent')
|
|
82
|
-
.option('--port <port>', 'Port number for web server mode', '3000')
|
|
82
|
+
.option('--port <port>', 'Port number for web server mode', '3000')
|
|
83
|
+
.option('--prometheus-port <port>', 'Port number for Prometheus exporter (default: 9090)', '9090')
|
|
84
|
+
.option('--speed <n>', 'Playback speed multiplier for pyre replay (default: 1)', '1')
|
|
85
|
+
.option('--llm-backend <backend>', 'AI backend: builtin (local zero-setup), ollama, or openai (default: builtin)', 'builtin')
|
|
86
|
+
.option('--llm-model <model>', 'AI model name for pyre explain (default: expert-rules-v1 / llama3.2 / gpt-4o-mini)')
|
|
87
|
+
.option('--api-key <key>', 'API key for OpenAI-compatible LLM backend (or set OPENAI_API_KEY env)')
|
|
88
|
+
.option('--alert-slack <url>', 'Slack webhook URL for alert notifications')
|
|
89
|
+
.option('--alert-discord <url>', 'Discord webhook URL for alert notifications')
|
|
90
|
+
.option('--alert-pushover-token <token>', 'Pushover application token for alert notifications')
|
|
91
|
+
.option('--alert-pushover-user <user>', 'Pushover user key for alert notifications')
|
|
92
|
+
.option('--alert-ntfy <url>', 'ntfy.sh topic URL for alert notifications (e.g. https://ntfy.sh/my-topic)')
|
|
93
|
+
.option('--watchdog-config <file>', 'Path to custom watchdog JSON configuration rules')
|
|
94
|
+
.option('--dry-run', 'Simulate actions in watchdog mode without executing kills')
|
|
95
|
+
.option('--top <n>', 'Number of items to display in netusage command (default: 15)')
|
|
96
|
+
.option('--raw', 'Print unparsed output for profiler')
|
|
97
|
+
.option('--sync-dir <dir>', 'Custom directory path for cloud profile/config sync')
|
|
98
|
+
.option('--scan', 'Perform active network sweep on LAN neighbors for topology map');
|
|
83
99
|
|
|
84
100
|
program.addHelpText('after', `
|
|
85
101
|
Commands:
|
|
@@ -87,27 +103,39 @@ program.addHelpText('after', `
|
|
|
87
103
|
check One-line plain-English health summary
|
|
88
104
|
pipe Continuous newline-delimited JSON stream for scripting
|
|
89
105
|
stress Synthetic CPU/GPU load generator
|
|
90
|
-
ui Launch the
|
|
106
|
+
ui Launch the live UI dashboard window
|
|
91
107
|
web Serve an auto-refreshing live web portal
|
|
92
|
-
ssh <host> Stream live stats from a remote
|
|
93
|
-
fleet <host1> [host2]... Multi-host live dashboard monitoring multiple
|
|
108
|
+
ssh <host> Stream live stats from a remote host over SSH
|
|
109
|
+
fleet <host1> [host2]... Multi-host live dashboard monitoring multiple hosts
|
|
94
110
|
bench <cmd> Log CPU, memory, network, power draw, and estimate kWh cost
|
|
95
111
|
benchmark Run a 1-minute CPU benchmark calculating digits of PI
|
|
96
112
|
anomalies Compute z-score anomalies and print plain-language digest
|
|
97
113
|
doctor Run system diagnostics
|
|
98
|
-
extensions System Extensions inspector
|
|
99
|
-
brew
|
|
114
|
+
extensions System Extensions / Driver inspector
|
|
115
|
+
brew Package manager health panel (brew/apt/pacman/winget)
|
|
100
116
|
update Check for pyre-cli updates
|
|
101
117
|
profile <save|load|list> Atomic configuration profile management
|
|
102
118
|
config <show|reset> View or reset persistent configuration file
|
|
103
119
|
history Graph historical resource trends from CSV logs
|
|
104
120
|
diff <file1> <file2> Compare two saved snapshot files side-by-side
|
|
105
121
|
info Concise hardware summary
|
|
106
|
-
completions <shell> Generate shell auto-completion scripts
|
|
122
|
+
completions <shell> Generate shell auto-completion scripts (zsh, bash, fish, powershell)
|
|
107
123
|
xbar Generate an xbar / SwiftBar menu bar plugin script
|
|
108
124
|
p2p <server|connect> Start a P2P server or connect to one
|
|
109
125
|
server Print commands for starting P2P server/client
|
|
110
126
|
blender Track active Blender render jobs and their progress
|
|
127
|
+
prometheus Start a Prometheus metrics exporter on localhost
|
|
128
|
+
smart S.M.A.R.T. disk health report (requires smartmontools)
|
|
129
|
+
replay <file> Play back a pyre CSV log file in the TUI
|
|
130
|
+
explain AI-powered plain-English anomaly explanation (Ollama/OpenAI)
|
|
131
|
+
alert test Send a test notification to all configured alert channels
|
|
132
|
+
netusage Real-time per-application network usage breakdown
|
|
133
|
+
serve Lightweight structured JSON REST API server
|
|
134
|
+
profile-proc <pid> CPU call-tree profiler and hot function sampler
|
|
135
|
+
watchdog Persistent autonomous monitoring daemon and rules engine
|
|
136
|
+
build Active compiler and build system tracker
|
|
137
|
+
topo Network topology mapper and pyre peer auto-discovery
|
|
138
|
+
sync <push|pull|status> Synchronize config profiles across Macs via iCloud/Drive
|
|
111
139
|
`);
|
|
112
140
|
|
|
113
141
|
program.parse(process.argv);
|
|
@@ -260,6 +288,158 @@ async function main() {
|
|
|
260
288
|
return;
|
|
261
289
|
}
|
|
262
290
|
|
|
291
|
+
if (cmd === 'prometheus') {
|
|
292
|
+
const { runPrometheusServer } = await import('./prometheusCmd.js');
|
|
293
|
+
const prometheusPort = parseInt(opts.prometheusPort, 10) || 9090;
|
|
294
|
+
const intervalSec = parseFloat(opts.interval) || 15;
|
|
295
|
+
await runPrometheusServer({ port: prometheusPort, interval: intervalSec, detailed: opts.detailed });
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (cmd === 'smart') {
|
|
300
|
+
const { runSmartCommand } = await import('./smartCmd.js');
|
|
301
|
+
await runSmartCommand();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (cmd === 'replay') {
|
|
306
|
+
const file = program.args[1];
|
|
307
|
+
if (!file) {
|
|
308
|
+
console.error(chalk.red(' ✖ Usage: pyre replay <logfile.csv>'));
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
const { runReplayCommand } = await import('./replayCmd.js');
|
|
312
|
+
await runReplayCommand({
|
|
313
|
+
file,
|
|
314
|
+
speed: parseFloat(opts.speed) || 1,
|
|
315
|
+
plain: opts.plain || opts.a11y,
|
|
316
|
+
});
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (cmd === 'explain') {
|
|
321
|
+
const { runExplainCommand } = await import('./explainCmd.js');
|
|
322
|
+
const backend = opts.llmBackend === 'openai' ? 'openai' : opts.llmBackend === 'ollama' ? 'ollama' : 'builtin';
|
|
323
|
+
await runExplainCommand({
|
|
324
|
+
since: opts.since || '7d',
|
|
325
|
+
backend,
|
|
326
|
+
model: opts.llmModel,
|
|
327
|
+
apiKey: opts.apiKey,
|
|
328
|
+
dir: opts.exportDir,
|
|
329
|
+
});
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (cmd === 'alert') {
|
|
334
|
+
const sub = program.args[1];
|
|
335
|
+
if (sub === 'test') {
|
|
336
|
+
const { sendTestAlert } = await import('./alertChannels.js');
|
|
337
|
+
const { writeConfig } = await import('./state/config.js');
|
|
338
|
+
// Persist any channel flags passed on CLI before sending test
|
|
339
|
+
if (opts.alertSlack || opts.alertDiscord || opts.alertPushoverToken || opts.alertPushoverUser || opts.alertNtfy) {
|
|
340
|
+
writeConfig({
|
|
341
|
+
slackAlertUrl: opts.alertSlack || config.slackAlertUrl,
|
|
342
|
+
discordAlertUrl: opts.alertDiscord || config.discordAlertUrl,
|
|
343
|
+
pushoverToken: opts.alertPushoverToken || config.pushoverToken,
|
|
344
|
+
pushoverUser: opts.alertPushoverUser || config.pushoverUser,
|
|
345
|
+
ntfyUrl: opts.alertNtfy || config.ntfyUrl,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
const testConfig = {
|
|
349
|
+
slackUrl: opts.alertSlack || config.slackAlertUrl || undefined,
|
|
350
|
+
discordUrl: opts.alertDiscord || config.discordAlertUrl || undefined,
|
|
351
|
+
pushoverToken: opts.alertPushoverToken || config.pushoverToken || undefined,
|
|
352
|
+
pushoverUser: opts.alertPushoverUser || config.pushoverUser || undefined,
|
|
353
|
+
ntfyUrl: opts.alertNtfy || config.ntfyUrl || undefined,
|
|
354
|
+
};
|
|
355
|
+
const hasAny = Object.values(testConfig).some(v => !!v);
|
|
356
|
+
if (!hasAny) {
|
|
357
|
+
console.log(chalk.yellow('\n ⚠ No alert channels configured.'));
|
|
358
|
+
console.log(chalk.dim(' Use --alert-slack <url>, --alert-discord <url>, --alert-ntfy <url>, etc.\n'));
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
console.log(chalk.bold('\n 🔥 pyre alert test — firing test notification...\n'));
|
|
362
|
+
await sendTestAlert(testConfig);
|
|
363
|
+
console.log(chalk.green(' ✔ Test alert sent to all configured channels.\n'));
|
|
364
|
+
} else {
|
|
365
|
+
console.log(chalk.yellow(' Usage: pyre alert test'));
|
|
366
|
+
}
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (cmd === 'netusage') {
|
|
371
|
+
const { runNetUsageCommand } = await import('./netUsageCmd.js');
|
|
372
|
+
await runNetUsageCommand({
|
|
373
|
+
top: opts.top ? parseInt(opts.top, 10) : 15,
|
|
374
|
+
sort: (opts.sort === 'rx' || opts.sort === 'tx') ? opts.sort : 'total',
|
|
375
|
+
json: !!opts.json,
|
|
376
|
+
});
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (cmd === 'serve') {
|
|
381
|
+
const { runServeCommand } = await import('./serveCmd.js');
|
|
382
|
+
const port = parseInt(opts.port, 10) || 8080;
|
|
383
|
+
await runServeCommand({
|
|
384
|
+
port,
|
|
385
|
+
apiKey: opts.apiKey,
|
|
386
|
+
detailed: opts.detailed,
|
|
387
|
+
});
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (cmd === 'profile-proc') {
|
|
392
|
+
const targetPid = parseInt(program.args[1], 10);
|
|
393
|
+
if (isNaN(targetPid)) {
|
|
394
|
+
console.error(chalk.red(' ✖ Usage: pyre profile-proc <pid> [--duration <sec>]'));
|
|
395
|
+
process.exit(1);
|
|
396
|
+
}
|
|
397
|
+
const { runProfileProcCommand } = await import('./profileProcCmd.js');
|
|
398
|
+
const durationSec = parseFloat(opts.duration) || 5;
|
|
399
|
+
await runProfileProcCommand({
|
|
400
|
+
pid: targetPid,
|
|
401
|
+
durationSec,
|
|
402
|
+
raw: !!opts.raw,
|
|
403
|
+
});
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (cmd === 'watchdog') {
|
|
408
|
+
const { runWatchdogCommand } = await import('./watchdogCmd.js');
|
|
409
|
+
await runWatchdogCommand({
|
|
410
|
+
configFile: opts.watchdogConfig,
|
|
411
|
+
dryRun: !!opts.dryRun,
|
|
412
|
+
interval: parseFloat(opts.interval) || 2,
|
|
413
|
+
});
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (cmd === 'build') {
|
|
418
|
+
const { runBuildCommand } = await import('./buildCmd.js');
|
|
419
|
+
await runBuildCommand({
|
|
420
|
+
json: !!opts.json,
|
|
421
|
+
});
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (cmd === 'topo') {
|
|
426
|
+
const { runTopoCommand } = await import('./topoCmd.js');
|
|
427
|
+
await runTopoCommand({
|
|
428
|
+
scan: !!opts.scan,
|
|
429
|
+
json: !!opts.json,
|
|
430
|
+
});
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (cmd === 'sync') {
|
|
435
|
+
const subcommand = program.args[1] || 'status';
|
|
436
|
+
const { runSyncCommand } = await import('./syncCmd.js');
|
|
437
|
+
await runSyncCommand(subcommand, {
|
|
438
|
+
syncDir: opts.syncDir,
|
|
439
|
+
});
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
263
443
|
if (cmd === 'brew') {
|
|
264
444
|
const { printBrewHealthReport } = await import('./brewHealth.js');
|
|
265
445
|
await printBrewHealthReport();
|
|
@@ -426,6 +606,12 @@ async function main() {
|
|
|
426
606
|
if (opts.webhookUrl) config.webhookUrl = opts.webhookUrl;
|
|
427
607
|
if (opts.alertCmd) config.alertCmd = opts.alertCmd;
|
|
428
608
|
if (opts.exportDir) config.exportDir = opts.exportDir;
|
|
609
|
+
// Persist new alert channel options to config if provided
|
|
610
|
+
if (opts.alertSlack) config.slackAlertUrl = opts.alertSlack;
|
|
611
|
+
if (opts.alertDiscord) config.discordAlertUrl = opts.alertDiscord;
|
|
612
|
+
if (opts.alertPushoverToken) config.pushoverToken = opts.alertPushoverToken;
|
|
613
|
+
if (opts.alertPushoverUser) config.pushoverUser = opts.alertPushoverUser;
|
|
614
|
+
if (opts.alertNtfy) config.ntfyUrl = opts.alertNtfy;
|
|
429
615
|
const tempUnit = opts.tempUnit === 'f' ? 'f' : 'c';
|
|
430
616
|
|
|
431
617
|
if (cmd === 'live' || (!isExportMode() && !opts.once && cmd !== 'p2p')) {
|
|
@@ -696,7 +882,7 @@ async function runInfoCommand(): Promise<void> {
|
|
|
696
882
|
const tmStatus = await getTimeMachineStatus();
|
|
697
883
|
|
|
698
884
|
const lines: string[] = [];
|
|
699
|
-
lines.push(chalk.bold(
|
|
885
|
+
lines.push(chalk.bold(` pyre info — System Information (${data.header.os})`));
|
|
700
886
|
lines.push('');
|
|
701
887
|
lines.push(` Hostname: ${data.header.hostname}`);
|
|
702
888
|
lines.push(` OS: ${data.header.os}`);
|
|
@@ -723,13 +909,15 @@ async function runInfoCommand(): Promise<void> {
|
|
|
723
909
|
}
|
|
724
910
|
}
|
|
725
911
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
912
|
+
if (process.platform === 'darwin') {
|
|
913
|
+
lines.push('');
|
|
914
|
+
lines.push(chalk.cyan.bold(' ⏱ Time Machine Backup:'));
|
|
915
|
+
if (!tmStatus.configured) {
|
|
916
|
+
lines.push(' • Not configured');
|
|
917
|
+
} else {
|
|
918
|
+
lines.push(` • Status: ${tmStatus.running ? chalk.yellow(`Backup in progress (${tmStatus.percent || 0}%)`) : chalk.green('Idle')}`);
|
|
919
|
+
lines.push(` • Latest Backup: ${tmStatus.latestBackup}`);
|
|
920
|
+
}
|
|
733
921
|
}
|
|
734
922
|
|
|
735
923
|
console.log(lines.join('\n'));
|
|
@@ -877,12 +1065,29 @@ async function runWebCommand(): Promise<number> {
|
|
|
877
1065
|
});
|
|
878
1066
|
}
|
|
879
1067
|
|
|
1068
|
+
async function openBrowserUrl(url: string): Promise<void> {
|
|
1069
|
+
const { spawn } = await import('node:child_process');
|
|
1070
|
+
const platform = process.platform;
|
|
1071
|
+
if (platform === 'win32') {
|
|
1072
|
+
spawn('cmd.exe', ['/c', 'start', '', url], { detached: true, stdio: 'ignore' });
|
|
1073
|
+
} else if (platform === 'darwin') {
|
|
1074
|
+
spawn('open', [url], { detached: true, stdio: 'ignore' });
|
|
1075
|
+
} else {
|
|
1076
|
+
spawn('xdg-open', [url], { detached: true, stdio: 'ignore' });
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
880
1080
|
async function runUiCommand(): Promise<void> {
|
|
881
1081
|
// Use a random ephemeral port to avoid collisions
|
|
882
1082
|
opts.port = '0';
|
|
883
1083
|
const port = await runWebCommand();
|
|
884
|
-
|
|
885
|
-
|
|
1084
|
+
const url = `http://localhost:${port}`;
|
|
1085
|
+
|
|
1086
|
+
if (process.platform === 'darwin') {
|
|
1087
|
+
const { run } = await import('./monitors/run.js');
|
|
1088
|
+
const hasSwift = (await run('which swift 2>/dev/null', '')).trim().length > 0;
|
|
1089
|
+
if (hasSwift) {
|
|
1090
|
+
const swiftScript = `
|
|
886
1091
|
import Cocoa
|
|
887
1092
|
import WebKit
|
|
888
1093
|
|
|
@@ -985,23 +1190,31 @@ app.delegate = delegate
|
|
|
985
1190
|
app.run()
|
|
986
1191
|
`;
|
|
987
1192
|
|
|
1193
|
+
const scriptPath = path.join(os.tmpdir(), 'pyre-ui.swift');
|
|
1194
|
+
fs.writeFileSync(scriptPath, swiftScript);
|
|
988
1195
|
|
|
989
|
-
|
|
990
|
-
|
|
1196
|
+
const { spawn } = await import('node:child_process');
|
|
1197
|
+
console.log(chalk.cyan(`\n Launching native UI window...`));
|
|
1198
|
+
const ui = spawn('swift', [scriptPath], { stdio: 'inherit' });
|
|
1199
|
+
|
|
1200
|
+
ui.on('close', () => {
|
|
1201
|
+
console.log(chalk.dim('UI window closed, shutting down server...'));
|
|
1202
|
+
process.exit(0);
|
|
1203
|
+
});
|
|
991
1204
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
});
|
|
1205
|
+
process.once('SIGINT', () => {
|
|
1206
|
+
ui.kill();
|
|
1207
|
+
process.exit(0);
|
|
1208
|
+
});
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1000
1212
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1213
|
+
// Cross-platform browser / app-mode launch for Linux, Windows, or macOS without Swift
|
|
1214
|
+
console.log(chalk.cyan(`\n Opening live UI dashboard at ${url}...`));
|
|
1215
|
+
await openBrowserUrl(url);
|
|
1216
|
+
console.log(chalk.dim(' Press Ctrl+C to stop server.\n'));
|
|
1217
|
+
await new Promise(() => {});
|
|
1005
1218
|
}
|
|
1006
1219
|
|
|
1007
1220
|
async function runBenchCommand(benchCmd: string): Promise<void> {
|
|
@@ -1021,7 +1234,9 @@ async function runBenchCommand(benchCmd: string): Promise<void> {
|
|
|
1021
1234
|
let accumulatedWattSeconds = 0;
|
|
1022
1235
|
let powerSamplesCount = 0;
|
|
1023
1236
|
|
|
1024
|
-
const child =
|
|
1237
|
+
const child = process.platform === 'win32'
|
|
1238
|
+
? spawn('cmd.exe', ['/c', benchCmd], { stdio: 'inherit' })
|
|
1239
|
+
: spawn('sh', ['-c', benchCmd]);
|
|
1025
1240
|
child.stdout.on('data', d => process.stdout.write(d));
|
|
1026
1241
|
child.stderr.on('data', d => process.stdout.write(d));
|
|
1027
1242
|
|
package/src/live/input.ts
CHANGED
|
@@ -16,6 +16,8 @@ import { render, footerLine, checkAlerts, invalidateTableCache, invalidateFrame
|
|
|
16
16
|
import type { LiveOptions, ExportFormat, InputMode, SortMode, GraphMode, ActivePanel } from './types.js';
|
|
17
17
|
import { startP2PServer } from '../p2p/index.js';
|
|
18
18
|
import { writeConfig } from '../state/config.js';
|
|
19
|
+
import { AVAILABLE_AI_MODELS } from '../ai/models.js';
|
|
20
|
+
import { loadPlugins, executePlugins } from '../plugins/index.js';
|
|
19
21
|
|
|
20
22
|
function persistConfig() {
|
|
21
23
|
writeConfig({
|
|
@@ -39,6 +41,8 @@ import { writeConfig } from '../state/config.js';
|
|
|
39
41
|
visiblePanels: { ...state.visiblePanels },
|
|
40
42
|
notificationsEnabled: state.notificationsEnabled,
|
|
41
43
|
dockerModeConfirmed: state.dockerModeConfirmed,
|
|
44
|
+
aiModel: state.aiModel,
|
|
45
|
+
aiBackend: state.aiBackend,
|
|
42
46
|
});
|
|
43
47
|
}
|
|
44
48
|
|
|
@@ -96,6 +100,25 @@ function handleInputModeKey(str: string, key: readline.Key) {
|
|
|
96
100
|
} else if (selected === 'Temperature Unit') {
|
|
97
101
|
state.tempUnit = state.tempUnit === 'c' ? 'f' : 'c';
|
|
98
102
|
setStatus(`Temperature unit: ${state.tempUnit.toUpperCase()}`);
|
|
103
|
+
} else if (selected === 'AI Model') {
|
|
104
|
+
const curIdx = AVAILABLE_AI_MODELS.findIndex(m => m.id === state.aiModel);
|
|
105
|
+
const nextIdx = (curIdx + 1) % AVAILABLE_AI_MODELS.length;
|
|
106
|
+
state.aiModel = AVAILABLE_AI_MODELS[nextIdx].id;
|
|
107
|
+
state.aiBackend = AVAILABLE_AI_MODELS[nextIdx].backend;
|
|
108
|
+
setStatus(`AI Model: ${AVAILABLE_AI_MODELS[nextIdx].name}`);
|
|
109
|
+
} else if (selected === 'Grid Panel Order') {
|
|
110
|
+
// Rotate panelLayout
|
|
111
|
+
const layouts = [
|
|
112
|
+
['mem', 'disk', 'net'],
|
|
113
|
+
['disk', 'mem', 'net'],
|
|
114
|
+
['net', 'mem', 'disk'],
|
|
115
|
+
['mem', 'net', 'disk'],
|
|
116
|
+
];
|
|
117
|
+
const currentStr = state.panelLayout.join(',');
|
|
118
|
+
const currentIdx = layouts.findIndex(l => l.join(',') === currentStr);
|
|
119
|
+
const nextIdx = (currentIdx + 1) % layouts.length;
|
|
120
|
+
state.panelLayout = layouts[nextIdx];
|
|
121
|
+
setStatus(`Panel layout: ${state.panelLayout.join(' → ')}`);
|
|
99
122
|
} else {
|
|
100
123
|
const toggleKey = getToggleKey(selected);
|
|
101
124
|
if (toggleKey) {
|
|
@@ -225,6 +248,37 @@ function handleInputModeKey(str: string, key: readline.Key) {
|
|
|
225
248
|
return;
|
|
226
249
|
}
|
|
227
250
|
|
|
251
|
+
if (state.inputMode === 'ai-model') {
|
|
252
|
+
if (isEscKey(key, str)) {
|
|
253
|
+
state.inputMode = null;
|
|
254
|
+
render();
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (isUpKey(key, str) || str === 'k') {
|
|
258
|
+
state.modelSelectionIndex = (state.modelSelectionIndex - 1 + AVAILABLE_AI_MODELS.length) % AVAILABLE_AI_MODELS.length;
|
|
259
|
+
render();
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (isDownKey(key, str) || str === 'j') {
|
|
263
|
+
state.modelSelectionIndex = (state.modelSelectionIndex + 1) % AVAILABLE_AI_MODELS.length;
|
|
264
|
+
render();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (isEnterKey(key, str) || str === ' ') {
|
|
268
|
+
const chosen = AVAILABLE_AI_MODELS[state.modelSelectionIndex];
|
|
269
|
+
state.aiModel = chosen.id;
|
|
270
|
+
state.aiBackend = chosen.backend;
|
|
271
|
+
state.inputMode = null;
|
|
272
|
+
setStatus(`Active AI Model: ${chosen.name}`, 4000);
|
|
273
|
+
invalidateTableCache();
|
|
274
|
+
invalidateFrame();
|
|
275
|
+
persistConfig();
|
|
276
|
+
render();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
228
282
|
if (state.inputMode === 'signal') {
|
|
229
283
|
if (isUpKey(key, str)) {
|
|
230
284
|
const idx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer as typeof state.SIGNAL_OPTIONS[number]);
|
|
@@ -488,6 +542,7 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
|
|
|
488
542
|
state.history.reset();
|
|
489
543
|
state.history.setMaxLen(Math.max(20, Math.min(200, state.termWidth - 30)));
|
|
490
544
|
|
|
545
|
+
await loadPlugins().catch(() => {});
|
|
491
546
|
const warmupPromise = doWarmup();
|
|
492
547
|
|
|
493
548
|
if (splashPromise) {
|
|
@@ -626,6 +681,11 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
|
|
|
626
681
|
state.customizerIndex = 0;
|
|
627
682
|
render();
|
|
628
683
|
break;
|
|
684
|
+
case 'm':
|
|
685
|
+
state.inputMode = 'ai-model';
|
|
686
|
+
state.modelSelectionIndex = Math.max(0, AVAILABLE_AI_MODELS.findIndex(m => m.id === state.aiModel));
|
|
687
|
+
render();
|
|
688
|
+
break;
|
|
629
689
|
case 'p':
|
|
630
690
|
state.paused = !state.paused;
|
|
631
691
|
setStatus(state.paused ? 'Paused' : 'Resumed');
|
package/src/live/render.ts
CHANGED
|
@@ -15,6 +15,9 @@ import { formatTable, formatGraphs, gridColumns, THEMES, type ThemeName, panel,
|
|
|
15
15
|
import { detectAnomalies } from '../anomalies.js';
|
|
16
16
|
import { state, setStatus, SIGNAL_OPTIONS, getToggleKey, MENU_OPTIONS } from './state.js';
|
|
17
17
|
import type { StatsData } from '../monitors/index.js';
|
|
18
|
+
import { sendAlert, type AlertChannelConfig } from '../alertChannels.js';
|
|
19
|
+
import { readConfig } from '../state/config.js';
|
|
20
|
+
import { AVAILABLE_AI_MODELS } from '../ai/models.js';
|
|
18
21
|
|
|
19
22
|
function formatTempForUnit(c: number): string {
|
|
20
23
|
if (state.tempUnit === 'f') {
|
|
@@ -61,6 +64,7 @@ function tableCacheParams(): string {
|
|
|
61
64
|
state.history.version,
|
|
62
65
|
JSON.stringify(state.visiblePanels),
|
|
63
66
|
JSON.stringify(state.panelLayout),
|
|
67
|
+
state.aiModel,
|
|
64
68
|
].join('|');
|
|
65
69
|
}
|
|
66
70
|
|
|
@@ -142,6 +146,11 @@ function renderCustomizerOverlay(scrollOffset: number, maxVisibleItems: number):
|
|
|
142
146
|
lines.push(`${prefix}${opt}: ${status}`);
|
|
143
147
|
} else if (opt === 'Temperature Unit') {
|
|
144
148
|
lines.push(`${prefix}${opt}: ${chalk.bold.green(state.tempUnit.toUpperCase())} [C, F]`);
|
|
149
|
+
} else if (opt === 'AI Model') {
|
|
150
|
+
const curModel = AVAILABLE_AI_MODELS.find(m => m.id === state.aiModel) || AVAILABLE_AI_MODELS[0];
|
|
151
|
+
lines.push(`${prefix}${opt}: ${chalk.bold.cyan(curModel.name)} ${chalk.dim(`[${curModel.badge}]`)}`);
|
|
152
|
+
} else if (opt === 'Grid Panel Order') {
|
|
153
|
+
lines.push(`${prefix}${opt}: ${chalk.bold.green(state.panelLayout.join(' → '))}`);
|
|
145
154
|
} else {
|
|
146
155
|
const toggleKey = getToggleKey(opt);
|
|
147
156
|
const isVisible = toggleKey ? (toggleKey === 'tree' ? state.treeView : state.visiblePanels[toggleKey] !== false) : true;
|
|
@@ -220,6 +229,10 @@ function render() {
|
|
|
220
229
|
writeFrame(renderDockerAlert(cols, rows));
|
|
221
230
|
return;
|
|
222
231
|
}
|
|
232
|
+
if (state.inputMode === 'ai-model') {
|
|
233
|
+
writeFrame(renderAiModelSelector(cols, rows));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
223
236
|
const lines: string[] = [];
|
|
224
237
|
const theme = THEMES[state.currentTheme] || THEMES.default;
|
|
225
238
|
|
|
@@ -262,6 +275,7 @@ function render() {
|
|
|
262
275
|
selectedProcessIndex: state.processSelectionIndex,
|
|
263
276
|
trackedPid: state.trackedPid,
|
|
264
277
|
inspectProcess: state.inspectingProcess,
|
|
278
|
+
aiModel: state.aiModel,
|
|
265
279
|
});
|
|
266
280
|
_cachedTableData = state.lastData;
|
|
267
281
|
_cachedTableParams = cacheParams;
|
|
@@ -377,6 +391,7 @@ function footerLine(): string[] {
|
|
|
377
391
|
['f', state.exportFormat],
|
|
378
392
|
['+/-', `${state.interval}s`],
|
|
379
393
|
['r', state.p2pServerRunning ? 'stop p2p' : 'start p2p'],
|
|
394
|
+
['m', `ai:${state.aiModel.split('-')[0]}`],
|
|
380
395
|
];
|
|
381
396
|
const fmt = (list: [string, string][]) => list.map(([k, label]) => `${chalk.hex('#50fa7b').bold(k)} ${chalk.dim(label)}`).join(chalk.dim(' | '));
|
|
382
397
|
const str1 = fmt(row1);
|
|
@@ -387,6 +402,7 @@ function footerLine(): string[] {
|
|
|
387
402
|
if (state.logging) badges.push(chalk.red.bold('● REC'));
|
|
388
403
|
if (state.activePanel !== 'grid') badges.push(chalk.cyan.bold(`◉ ${state.activePanel.toUpperCase()}`));
|
|
389
404
|
if (state.p2pServerRunning) badges.push(chalk.green.bold(`P2P ${state.p2pBind || '0.0.0.0'}:${state.p2pPort}`));
|
|
405
|
+
badges.push(chalk.magenta.bold(`AI:${state.aiModel}`));
|
|
390
406
|
const badgeStr = badges.join(' ') || ' ';
|
|
391
407
|
|
|
392
408
|
return [str1, str2, badgeStr];
|
|
@@ -458,6 +474,29 @@ function checkAlerts(data: StatsData) {
|
|
|
458
474
|
if (state.alertCmd) {
|
|
459
475
|
exec(state.alertCmd, { env: { ...process.env, PYRE_ALERT: alertMsg } }, () => {});
|
|
460
476
|
}
|
|
477
|
+
|
|
478
|
+
// Multi-channel alert fan-out (Slack, Discord, Pushover, ntfy)
|
|
479
|
+
const cfg = readConfig();
|
|
480
|
+
const channelConfig: AlertChannelConfig = {
|
|
481
|
+
slackUrl: cfg.slackAlertUrl || undefined,
|
|
482
|
+
discordUrl: cfg.discordAlertUrl || undefined,
|
|
483
|
+
pushoverToken: cfg.pushoverToken || undefined,
|
|
484
|
+
pushoverUser: cfg.pushoverUser || undefined,
|
|
485
|
+
ntfyUrl: cfg.ntfyUrl || undefined,
|
|
486
|
+
};
|
|
487
|
+
const hasChannels = Object.values(channelConfig).some(v => !!v);
|
|
488
|
+
if (hasChannels) {
|
|
489
|
+
const severity = hasCriticalAnomaly ? 'critical' : 'warning';
|
|
490
|
+
sendAlert({
|
|
491
|
+
metric: reasons[0]?.split(' ')[0] ?? 'System',
|
|
492
|
+
value: data.cpu.usage,
|
|
493
|
+
threshold: state.CPU_ALERT_PCT,
|
|
494
|
+
unit: '%',
|
|
495
|
+
host: data.header.hostname,
|
|
496
|
+
timestamp: new Date().toISOString(),
|
|
497
|
+
severity,
|
|
498
|
+
}, channelConfig).catch(() => {});
|
|
499
|
+
}
|
|
461
500
|
} else if (!hot && !anomalyTriggered && !watchdogTriggered) {
|
|
462
501
|
state.alerted = false;
|
|
463
502
|
}
|
|
@@ -595,4 +634,32 @@ function renderDockerAlert(cols: number, rows: number): string[] {
|
|
|
595
634
|
return out;
|
|
596
635
|
}
|
|
597
636
|
|
|
637
|
+
function renderAiModelSelector(cols: number, rows: number): string[] {
|
|
638
|
+
const out: string[] = [];
|
|
639
|
+
const content: string[] = [
|
|
640
|
+
chalk.bold.hex('#ff6004')('Choose AI Anomaly Explanation Model'),
|
|
641
|
+
chalk.dim('Select your preferred diagnosis engine. Offline models run entirely in-process.'),
|
|
642
|
+
'',
|
|
643
|
+
];
|
|
644
|
+
|
|
645
|
+
for (let i = 0; i < AVAILABLE_AI_MODELS.length; i++) {
|
|
646
|
+
const m = AVAILABLE_AI_MODELS[i];
|
|
647
|
+
const isSelected = i === state.modelSelectionIndex;
|
|
648
|
+
const isCurrent = m.id === state.aiModel;
|
|
649
|
+
const prefix = isSelected ? chalk.cyan.bold(' ▶ ') : ' ';
|
|
650
|
+
const curBadge = isCurrent ? chalk.bgGreen.black.bold(' ACTIVE ') : '';
|
|
651
|
+
const badgeStr = m.offline ? chalk.green(`[${m.badge}]`) : chalk.yellow(`[${m.badge}]`);
|
|
652
|
+
|
|
653
|
+
const nameStr = isSelected ? chalk.bgCyan.black.bold(` ${m.name} `) : chalk.bold.white(m.name);
|
|
654
|
+
content.push(`${prefix}${nameStr} ${badgeStr} ${curBadge}`);
|
|
655
|
+
content.push(chalk.dim(` ${m.description}`));
|
|
656
|
+
content.push('');
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
content.push(chalk.dim('Use ↑/↓ or J/K to navigate · Enter/Space to select · Esc to cancel'));
|
|
660
|
+
|
|
661
|
+
out.push(...panel('AI MODEL SELECTOR', content, cols, chalk.cyan, chalk.dim, Math.max(1, rows - 2)));
|
|
662
|
+
return out;
|
|
663
|
+
}
|
|
664
|
+
|
|
598
665
|
export { render, footerLine, renderCustomizerOverlay, checkAlerts, processRowBudget, invalidateTableCache, invalidateFrame };
|
package/src/live/state.ts
CHANGED
|
@@ -85,6 +85,8 @@ const state = {
|
|
|
85
85
|
'Splash Animation',
|
|
86
86
|
'Notifications',
|
|
87
87
|
'Temperature Unit',
|
|
88
|
+
'AI Model',
|
|
89
|
+
'Grid Panel Order',
|
|
88
90
|
'Toggle CPU',
|
|
89
91
|
'Toggle Memory',
|
|
90
92
|
'Toggle GPU',
|
|
@@ -145,6 +147,9 @@ const state = {
|
|
|
145
147
|
isDocker: isDockerEnv,
|
|
146
148
|
dockerModeConfirmed: config.dockerModeConfirmed || false,
|
|
147
149
|
dockerSelectionIndex: 0,
|
|
150
|
+
aiModel: config.aiModel || 'expert-rules-v1',
|
|
151
|
+
aiBackend: config.aiBackend || 'builtin',
|
|
152
|
+
modelSelectionIndex: 0,
|
|
148
153
|
};
|
|
149
154
|
|
|
150
155
|
function setStatus(msg: string, ms = 3000) {
|
package/src/live/types.ts
CHANGED
|
@@ -20,7 +20,7 @@ export interface LiveOptions {
|
|
|
20
20
|
|
|
21
21
|
export type ExportFormat = 'json' | 'csv' | 'tsv' | 'html' | 'md';
|
|
22
22
|
|
|
23
|
-
type InputMode = null | 'filter' | 'kill' | 'signal' | 'customizer' | 'p2p' | 'menu' | 'readme' | 'credits' | 'docker-alert';
|
|
23
|
+
type InputMode = null | 'filter' | 'kill' | 'signal' | 'customizer' | 'p2p' | 'menu' | 'readme' | 'credits' | 'docker-alert' | 'ai-model';
|
|
24
24
|
|
|
25
25
|
type SortMode = 'cpu' | 'mem' | 'pid' | 'user' | 'command' | 'state' | 'threads' | 'runtime';
|
|
26
26
|
|