@nocville/npc 0.2.2 → 0.3.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 +55 -17
- package/dist/cjs/cli.js +94 -39
- package/dist/cjs/core/base-npc.js +15 -0
- package/dist/cjs/core/config.js +19 -2
- package/dist/cjs/core/errors.js +23 -0
- package/dist/cjs/core/http.js +121 -0
- package/dist/cjs/core/pixel-art.js +110 -0
- package/dist/cjs/core/polling-npc.js +99 -14
- package/dist/cjs/core/text.js +13 -0
- package/dist/cjs/core/thresholds.js +29 -0
- package/dist/cjs/index.js +21 -2
- package/dist/cjs/npcs/agent-common/session.js +1 -0
- package/dist/cjs/npcs/agent-common/shim.js +8 -2
- package/dist/cjs/npcs/github-watcher/defaults.js +7 -1
- package/dist/cjs/npcs/github-watcher/github-api.js +82 -45
- package/dist/cjs/npcs/github-watcher/index.js +82 -36
- package/dist/cjs/npcs/internet-connectivity/checks.js +70 -67
- package/dist/cjs/npcs/internet-connectivity/defaults.js +5 -2
- package/dist/cjs/npcs/internet-connectivity/index.js +134 -131
- package/dist/cjs/npcs/log-watcher/buffer.js +61 -0
- package/dist/cjs/npcs/log-watcher/index.js +109 -103
- package/dist/cjs/npcs/log-watcher/tail.js +100 -53
- package/dist/cjs/npcs/office-characters/index.js +92 -0
- package/dist/cjs/npcs/process-watcher/index.js +60 -51
- package/dist/cjs/npcs/process-watcher/process-scanner.js +100 -68
- package/dist/cjs/npcs/receptionist/defaults.js +4 -0
- package/dist/cjs/npcs/receptionist/index.js +6 -93
- package/dist/cjs/npcs/receptionist/weather.js +42 -47
- package/dist/cjs/npcs/security/index.js +26 -0
- package/dist/cjs/npcs/system-monitor/collectors.js +66 -17
- package/dist/cjs/npcs/system-monitor/index.js +52 -28
- package/dist/cjs/npcs/web-page-watcher/basic-mode.js +30 -14
- package/dist/cjs/npcs/web-page-watcher/capture.js +23 -1
- package/dist/cjs/npcs/web-page-watcher/defaults.js +44 -2
- package/dist/cjs/npcs/web-page-watcher/index.js +119 -68
- package/dist/cjs/npcs/web-page-watcher/pixelate.js +2 -1
- package/dist/cli.js +95 -40
- package/dist/core/base-npc.d.ts +5 -0
- package/dist/core/base-npc.js +15 -0
- package/dist/core/config.d.ts +5 -0
- package/dist/core/config.js +18 -2
- package/dist/core/errors.d.ts +11 -0
- package/dist/core/errors.js +18 -0
- package/dist/core/http.d.ts +52 -0
- package/dist/core/http.js +111 -0
- package/dist/core/pixel-art.d.ts +38 -0
- package/dist/core/pixel-art.js +105 -0
- package/dist/core/polling-npc.d.ts +22 -1
- package/dist/core/polling-npc.js +98 -13
- package/dist/core/text.d.ts +6 -0
- package/dist/core/text.js +12 -0
- package/dist/core/thresholds.d.ts +9 -1
- package/dist/core/thresholds.js +27 -0
- package/dist/core/types.d.ts +5 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +6 -2
- package/dist/npcs/agent-common/session.js +1 -0
- package/dist/npcs/agent-common/shim.js +8 -2
- package/dist/npcs/github-watcher/defaults.d.ts +6 -0
- package/dist/npcs/github-watcher/defaults.js +7 -1
- package/dist/npcs/github-watcher/github-api.d.ts +21 -7
- package/dist/npcs/github-watcher/github-api.js +80 -45
- package/dist/npcs/github-watcher/index.d.ts +6 -0
- package/dist/npcs/github-watcher/index.js +83 -37
- package/dist/npcs/internet-connectivity/checks.d.ts +15 -18
- package/dist/npcs/internet-connectivity/checks.js +70 -67
- package/dist/npcs/internet-connectivity/defaults.d.ts +3 -0
- package/dist/npcs/internet-connectivity/defaults.js +5 -2
- package/dist/npcs/internet-connectivity/index.d.ts +19 -6
- package/dist/npcs/internet-connectivity/index.js +135 -133
- package/dist/npcs/log-watcher/buffer.d.ts +22 -0
- package/dist/npcs/log-watcher/buffer.js +58 -0
- package/dist/npcs/log-watcher/index.d.ts +22 -21
- package/dist/npcs/log-watcher/index.js +112 -106
- package/dist/npcs/log-watcher/tail.d.ts +23 -3
- package/dist/npcs/log-watcher/tail.js +101 -54
- package/dist/npcs/office-characters/index.d.ts +50 -0
- package/dist/npcs/office-characters/index.js +82 -0
- package/dist/npcs/process-watcher/index.d.ts +1 -1
- package/dist/npcs/process-watcher/index.js +61 -52
- package/dist/npcs/process-watcher/process-scanner.d.ts +31 -1
- package/dist/npcs/process-watcher/process-scanner.js +97 -70
- package/dist/npcs/receptionist/defaults.d.ts +4 -0
- package/dist/npcs/receptionist/defaults.js +4 -0
- package/dist/npcs/receptionist/index.d.ts +3 -12
- package/dist/npcs/receptionist/index.js +6 -93
- package/dist/npcs/receptionist/weather.d.ts +8 -6
- package/dist/npcs/receptionist/weather.js +39 -45
- package/dist/npcs/security/index.d.ts +12 -0
- package/dist/npcs/security/index.js +22 -0
- package/dist/npcs/system-monitor/collectors.d.ts +15 -3
- package/dist/npcs/system-monitor/collectors.js +64 -17
- package/dist/npcs/system-monitor/defaults.d.ts +1 -1
- package/dist/npcs/system-monitor/index.d.ts +2 -1
- package/dist/npcs/system-monitor/index.js +54 -30
- package/dist/npcs/web-page-watcher/basic-mode.d.ts +2 -2
- package/dist/npcs/web-page-watcher/basic-mode.js +30 -14
- package/dist/npcs/web-page-watcher/capture.d.ts +18 -0
- package/dist/npcs/web-page-watcher/capture.js +21 -1
- package/dist/npcs/web-page-watcher/defaults.d.ts +23 -0
- package/dist/npcs/web-page-watcher/defaults.js +41 -1
- package/dist/npcs/web-page-watcher/index.d.ts +18 -1
- package/dist/npcs/web-page-watcher/index.js +121 -70
- package/dist/npcs/web-page-watcher/pixelate.js +2 -1
- package/package.json +2 -2
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { PollingNPC } from '../../core/polling-npc.js';
|
|
2
|
+
import type { PollingNpcConfig } from '../../core/polling-npc.js';
|
|
3
|
+
import type { Archetype, BehaviorHints, CollectedMetrics } from '../../core/types.js';
|
|
4
|
+
/** Quiet heartbeats; the authoritative server drives routines and conversation. */
|
|
5
|
+
declare abstract class OfficeCharacterNPC extends PollingNPC {
|
|
6
|
+
constructor(config: PollingNpcConfig);
|
|
7
|
+
abstract get name(): NonNullable<BehaviorHints['role']>;
|
|
8
|
+
get archetype(): Archetype;
|
|
9
|
+
get behavior(): BehaviorHints;
|
|
10
|
+
protected collect(): Promise<CollectedMetrics>;
|
|
11
|
+
}
|
|
12
|
+
export declare class JanitorNPC extends OfficeCharacterNPC {
|
|
13
|
+
get name(): 'janitor';
|
|
14
|
+
protected get defaultDisplayName(): string;
|
|
15
|
+
}
|
|
16
|
+
export declare class BaristaNPC extends OfficeCharacterNPC {
|
|
17
|
+
get name(): 'barista';
|
|
18
|
+
protected get defaultDisplayName(): string;
|
|
19
|
+
}
|
|
20
|
+
export declare class OfficeCatNPC extends OfficeCharacterNPC {
|
|
21
|
+
get name(): 'office-cat';
|
|
22
|
+
protected get defaultDisplayName(): string;
|
|
23
|
+
}
|
|
24
|
+
export declare class OfficeDogNPC extends OfficeCharacterNPC {
|
|
25
|
+
get name(): 'office-dog';
|
|
26
|
+
protected get defaultDisplayName(): string;
|
|
27
|
+
}
|
|
28
|
+
export declare class PlantCaretakerNPC extends OfficeCharacterNPC {
|
|
29
|
+
get name(): 'plant-caretaker';
|
|
30
|
+
protected get defaultDisplayName(): string;
|
|
31
|
+
}
|
|
32
|
+
export declare class ITTechnicianNPC extends OfficeCharacterNPC {
|
|
33
|
+
get name(): 'it-technician';
|
|
34
|
+
protected get defaultDisplayName(): string;
|
|
35
|
+
}
|
|
36
|
+
export declare class OutOfTouchExecutiveNPC extends OfficeCharacterNPC {
|
|
37
|
+
get name(): 'out-of-touch-executive';
|
|
38
|
+
protected get defaultDisplayName(): string;
|
|
39
|
+
}
|
|
40
|
+
export declare const OFFICE_CHARACTER_NPCS: {
|
|
41
|
+
readonly janitor: typeof JanitorNPC;
|
|
42
|
+
readonly barista: typeof BaristaNPC;
|
|
43
|
+
readonly 'office-cat': typeof OfficeCatNPC;
|
|
44
|
+
readonly 'office-dog': typeof OfficeDogNPC;
|
|
45
|
+
readonly 'plant-caretaker': typeof PlantCaretakerNPC;
|
|
46
|
+
readonly 'it-technician': typeof ITTechnicianNPC;
|
|
47
|
+
readonly 'out-of-touch-executive': typeof OutOfTouchExecutiveNPC;
|
|
48
|
+
};
|
|
49
|
+
export {};
|
|
50
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { PollingNPC } from '../../core/polling-npc.js';
|
|
2
|
+
/** Quiet heartbeats; the authoritative server drives routines and conversation. */
|
|
3
|
+
class OfficeCharacterNPC extends PollingNPC {
|
|
4
|
+
constructor(config) {
|
|
5
|
+
super({ ...config, pollInterval: config.pollInterval ?? 60_000 });
|
|
6
|
+
}
|
|
7
|
+
get archetype() {
|
|
8
|
+
return 'custom';
|
|
9
|
+
}
|
|
10
|
+
get behavior() {
|
|
11
|
+
return { role: this.name, activity: 'idle', idlePose: 'stand' };
|
|
12
|
+
}
|
|
13
|
+
async collect() {
|
|
14
|
+
return { status: 'healthy', mood: 'happy', message: '', metrics: {} };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class JanitorNPC extends OfficeCharacterNPC {
|
|
18
|
+
get name() {
|
|
19
|
+
return 'janitor';
|
|
20
|
+
}
|
|
21
|
+
get defaultDisplayName() {
|
|
22
|
+
return 'Janitor';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export class BaristaNPC extends OfficeCharacterNPC {
|
|
26
|
+
get name() {
|
|
27
|
+
return 'barista';
|
|
28
|
+
}
|
|
29
|
+
get defaultDisplayName() {
|
|
30
|
+
return 'Barista';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export class OfficeCatNPC extends OfficeCharacterNPC {
|
|
34
|
+
get name() {
|
|
35
|
+
return 'office-cat';
|
|
36
|
+
}
|
|
37
|
+
get defaultDisplayName() {
|
|
38
|
+
return 'Office Cat';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export class OfficeDogNPC extends OfficeCharacterNPC {
|
|
42
|
+
get name() {
|
|
43
|
+
return 'office-dog';
|
|
44
|
+
}
|
|
45
|
+
get defaultDisplayName() {
|
|
46
|
+
return 'Office Dog';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export class PlantCaretakerNPC extends OfficeCharacterNPC {
|
|
50
|
+
get name() {
|
|
51
|
+
return 'plant-caretaker';
|
|
52
|
+
}
|
|
53
|
+
get defaultDisplayName() {
|
|
54
|
+
return 'Plant Caretaker';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export class ITTechnicianNPC extends OfficeCharacterNPC {
|
|
58
|
+
get name() {
|
|
59
|
+
return 'it-technician';
|
|
60
|
+
}
|
|
61
|
+
get defaultDisplayName() {
|
|
62
|
+
return 'IT Technician';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export class OutOfTouchExecutiveNPC extends OfficeCharacterNPC {
|
|
66
|
+
get name() {
|
|
67
|
+
return 'out-of-touch-executive';
|
|
68
|
+
}
|
|
69
|
+
get defaultDisplayName() {
|
|
70
|
+
return 'Out-of-Touch Executive';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export const OFFICE_CHARACTER_NPCS = {
|
|
74
|
+
janitor: JanitorNPC,
|
|
75
|
+
barista: BaristaNPC,
|
|
76
|
+
'office-cat': OfficeCatNPC,
|
|
77
|
+
'office-dog': OfficeDogNPC,
|
|
78
|
+
'plant-caretaker': PlantCaretakerNPC,
|
|
79
|
+
'it-technician': ITTechnicianNPC,
|
|
80
|
+
'out-of-touch-executive': OutOfTouchExecutiveNPC,
|
|
81
|
+
};
|
|
82
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -19,8 +19,8 @@ export declare class ProcessWatcherNPC extends PollingNPC {
|
|
|
19
19
|
get name(): string;
|
|
20
20
|
protected get defaultDisplayName(): string;
|
|
21
21
|
get archetype(): Archetype;
|
|
22
|
-
get heartbeatSeconds(): number;
|
|
23
22
|
get companion(): CompanionDecl;
|
|
24
23
|
protected collect(): Promise<CollectedMetrics>;
|
|
24
|
+
private scanFailed;
|
|
25
25
|
}
|
|
26
26
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PollingNPC } from '../../core/polling-npc.js';
|
|
2
|
-
import { worstStatus } from '../../core/thresholds.js';
|
|
3
|
-
import { pushHistory } from '../../core/screen.js';
|
|
2
|
+
import { evaluateThreshold, worstStatus, moodForStatus } from '../../core/thresholds.js';
|
|
3
|
+
import { statusColor, pushHistory } from '../../core/screen.js';
|
|
4
|
+
import { NpcConfigError } from '../../core/errors.js';
|
|
4
5
|
import { scanProcesses, detectTransitions } from './process-scanner.js';
|
|
5
6
|
import { DEFAULTS } from './defaults.js';
|
|
6
7
|
/** Max per-process panels on the companion dashboard. */
|
|
@@ -12,14 +13,16 @@ export class ProcessWatcherNPC extends PollingNPC {
|
|
|
12
13
|
cpuHistories = new Map();
|
|
13
14
|
previousRunning = null;
|
|
14
15
|
constructor(config) {
|
|
15
|
-
if (process.platform === 'win32') {
|
|
16
|
-
throw new Error('process-watcher is not supported on Windows (relies on the POSIX `ps` command). ' +
|
|
17
|
-
'Run it under WSL/Linux, or use system-monitor instead.');
|
|
18
|
-
}
|
|
19
16
|
super(config);
|
|
20
|
-
this.processes = config.processes;
|
|
17
|
+
this.processes = [...new Set(config.processes.map((p) => p.trim()).filter(Boolean))];
|
|
18
|
+
if (this.processes.length === 0) {
|
|
19
|
+
throw new NpcConfigError('process-watcher needs at least one process name (--processes)');
|
|
20
|
+
}
|
|
21
21
|
this.cpuWarn = config.cpuWarn ?? DEFAULTS.cpuWarn;
|
|
22
22
|
this.memWarn = config.memWarn ?? DEFAULTS.memWarn;
|
|
23
|
+
if (!(this.cpuWarn > 0) || !(this.memWarn > 0)) {
|
|
24
|
+
throw new NpcConfigError('--cpu-warn and --mem-warn must be positive numbers');
|
|
25
|
+
}
|
|
23
26
|
}
|
|
24
27
|
get name() {
|
|
25
28
|
return 'process-watcher';
|
|
@@ -30,9 +33,6 @@ export class ProcessWatcherNPC extends PollingNPC {
|
|
|
30
33
|
get archetype() {
|
|
31
34
|
return 'service';
|
|
32
35
|
}
|
|
33
|
-
get heartbeatSeconds() {
|
|
34
|
-
return 30;
|
|
35
|
-
}
|
|
36
36
|
get companion() {
|
|
37
37
|
return {
|
|
38
38
|
screenType: 'dashboard-wall',
|
|
@@ -42,22 +42,28 @@ export class ProcessWatcherNPC extends PollingNPC {
|
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
async collect() {
|
|
45
|
-
|
|
45
|
+
let infos;
|
|
46
|
+
try {
|
|
47
|
+
infos = await scanProcesses(this.processes);
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
// Not knowing is different from "everything died": report unknown and
|
|
51
|
+
// leave the transition baseline alone so recovery doesn't say "is back".
|
|
52
|
+
return this.scanFailed(err);
|
|
53
|
+
}
|
|
46
54
|
const statuses = [];
|
|
47
|
-
const
|
|
55
|
+
const problems = [];
|
|
48
56
|
const metrics = {};
|
|
49
57
|
const panels = [];
|
|
50
58
|
let totalRunning = 0;
|
|
51
|
-
let totalMissing = 0;
|
|
52
59
|
for (const info of infos) {
|
|
53
60
|
const safeKey = info.name.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
54
61
|
if (!info.running) {
|
|
55
62
|
statuses.push('critical');
|
|
56
|
-
|
|
63
|
+
problems.push(`${info.name} DOWN`);
|
|
57
64
|
metrics[`${safeKey}_running`] = 0;
|
|
58
65
|
metrics[`${safeKey}_cpu`] = 0;
|
|
59
66
|
metrics[`${safeKey}_mem_mb`] = 0;
|
|
60
|
-
totalMissing++;
|
|
61
67
|
if (panels.length < MAX_PANELS) {
|
|
62
68
|
panels.push({ label: info.name, value: 'DOWN', color: 'red' });
|
|
63
69
|
}
|
|
@@ -65,72 +71,75 @@ export class ProcessWatcherNPC extends PollingNPC {
|
|
|
65
71
|
}
|
|
66
72
|
totalRunning++;
|
|
67
73
|
const cpu = info.cpuPercent ?? 0;
|
|
74
|
+
const memMb = info.memMb ?? 0;
|
|
68
75
|
metrics[`${safeKey}_running`] = 1;
|
|
69
76
|
metrics[`${safeKey}_cpu`] = cpu;
|
|
70
|
-
metrics[`${safeKey}_mem_mb`] =
|
|
77
|
+
metrics[`${safeKey}_mem_mb`] = memMb;
|
|
71
78
|
const history = this.cpuHistories.get(info.name) ?? [];
|
|
72
79
|
pushHistory(history, cpu);
|
|
73
80
|
this.cpuHistories.set(info.name, history);
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
statuses.push('healthy');
|
|
87
|
-
messageParts.push(`${info.name}: OK`);
|
|
81
|
+
// Single warning thresholds: no critical level for resource use.
|
|
82
|
+
const cpuStatus = evaluateThreshold(cpu, { warning: this.cpuWarn, critical: Infinity });
|
|
83
|
+
const memStatus = evaluateThreshold(memMb, { warning: this.memWarn, critical: Infinity });
|
|
84
|
+
const status = worstStatus([cpuStatus, memStatus]);
|
|
85
|
+
statuses.push(status);
|
|
86
|
+
if (status !== 'healthy') {
|
|
87
|
+
const high = [];
|
|
88
|
+
if (cpuStatus !== 'healthy')
|
|
89
|
+
high.push(`cpu ${cpu}%`);
|
|
90
|
+
if (memStatus !== 'healthy')
|
|
91
|
+
high.push(`mem ${Math.round(memMb)}MB`);
|
|
92
|
+
problems.push(`${info.name} high ${high.join(', ')}`);
|
|
88
93
|
}
|
|
89
94
|
if (panels.length < MAX_PANELS) {
|
|
90
95
|
panels.push({
|
|
91
96
|
label: info.name,
|
|
92
97
|
value: `${cpu}%`,
|
|
93
|
-
color:
|
|
98
|
+
color: statusColor(status),
|
|
94
99
|
sparkline: [...history],
|
|
95
100
|
});
|
|
96
101
|
}
|
|
97
102
|
}
|
|
103
|
+
const totalMissing = infos.length - totalRunning;
|
|
98
104
|
metrics['total_watched'] = this.processes.length;
|
|
99
105
|
metrics['total_running'] = totalRunning;
|
|
100
106
|
metrics['total_missing'] = totalMissing;
|
|
101
107
|
// Transition speech: compare the running set to last poll's. On the first
|
|
102
108
|
// poll we only seed the set (no bogus "is back" for already-running procs).
|
|
103
109
|
const currentRunning = new Set(infos.filter((i) => i.running).map((i) => i.name));
|
|
104
|
-
|
|
105
|
-
if (this.previousRunning !== null) {
|
|
106
|
-
transitions = detectTransitions(this.previousRunning, currentRunning);
|
|
107
|
-
}
|
|
110
|
+
const transitions = this.previousRunning === null ? [] : detectTransitions(this.previousRunning, currentRunning);
|
|
108
111
|
this.previousRunning = currentRunning;
|
|
109
112
|
const status = worstStatus(statuses);
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
const summary = problems.length === 0
|
|
114
|
+
? `All ${infos.length} running`
|
|
115
|
+
: `${totalRunning}/${infos.length} running · ${problems.join(' · ')}`;
|
|
116
|
+
// A death/return takes over the bubble this cycle so it reads as an alert.
|
|
117
|
+
const message = transitions.length > 0 ? transitions.join(' · ') : summary;
|
|
114
118
|
return {
|
|
115
119
|
status,
|
|
116
|
-
mood,
|
|
120
|
+
mood: moodForStatus(status),
|
|
117
121
|
message,
|
|
118
122
|
subStatus: totalMissing > 0 ? 'process-down' : undefined,
|
|
119
|
-
screenData,
|
|
123
|
+
screenData: { layout: 'multi-metric', values: panels },
|
|
120
124
|
metrics,
|
|
121
125
|
};
|
|
122
126
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
127
|
+
scanFailed(err) {
|
|
128
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
129
|
+
this.logger.warn('Cannot list processes', { error: reason });
|
|
130
|
+
return {
|
|
131
|
+
status: 'unknown',
|
|
132
|
+
mood: moodForStatus('unknown'),
|
|
133
|
+
message: 'Cannot list processes (ps failed)',
|
|
134
|
+
subStatus: 'scan-failed',
|
|
135
|
+
screenData: {
|
|
136
|
+
layout: 'multi-metric',
|
|
137
|
+
values: this.processes
|
|
138
|
+
.slice(0, MAX_PANELS)
|
|
139
|
+
.map((name) => ({ label: name, value: '?', color: 'gray' })),
|
|
140
|
+
},
|
|
141
|
+
metrics: { total_watched: this.processes.length },
|
|
142
|
+
};
|
|
134
143
|
}
|
|
135
144
|
}
|
|
136
145
|
//# sourceMappingURL=index.js.map
|
|
@@ -2,6 +2,8 @@ export interface ProcessInfo {
|
|
|
2
2
|
name: string;
|
|
3
3
|
running: boolean;
|
|
4
4
|
pid?: number;
|
|
5
|
+
/** Instances matched (all are summed into cpu/mem). */
|
|
6
|
+
count?: number;
|
|
5
7
|
cpuPercent?: number;
|
|
6
8
|
memMb?: number;
|
|
7
9
|
}
|
|
@@ -11,9 +13,37 @@ export interface ProcessInfo {
|
|
|
11
13
|
* "<name> is back" when one returns.
|
|
12
14
|
*/
|
|
13
15
|
export declare function detectTransitions(prev: Set<string>, curr: Set<string>): string[];
|
|
16
|
+
export interface PsEntry {
|
|
17
|
+
pid: number;
|
|
18
|
+
cpu: number;
|
|
19
|
+
memKb: number;
|
|
20
|
+
/** Executable name (Linux) or path (macOS); may contain spaces. */
|
|
21
|
+
comm: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Parse header-less `ps -eo pid=,pcpu=,rss=,comm=` output. COMMAND is the rest
|
|
25
|
+
* of the line, so macOS paths with spaces ("Google Chrome") survive intact.
|
|
26
|
+
*/
|
|
27
|
+
export declare function parsePsOutput(stdout: string): PsEntry[];
|
|
28
|
+
/**
|
|
29
|
+
* Does a ps COMMAND entry match a watched name? Case-insensitive on the
|
|
30
|
+
* executable's basename:
|
|
31
|
+
* - exact: `nginx` matches `/usr/sbin/nginx`;
|
|
32
|
+
* - version suffix: `python` matches `python3` and `python3.11`;
|
|
33
|
+
* - Linux truncation: a name longer than 15 chars matches its 15-char prefix;
|
|
34
|
+
* - a name containing `/` must equal the full path (macOS reports paths).
|
|
35
|
+
* Unlike a substring match, `sh` does not match `bash` or `ssh-agent`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function matchesName(comm: string, searchName: string): boolean;
|
|
38
|
+
/** List every process. Throws if `ps` fails — callers must not read that as "all dead". */
|
|
39
|
+
export declare function listProcesses(): Promise<PsEntry[]>;
|
|
40
|
+
/** Windows instance suffixes (#1, #2) distinguish copies of the same executable. */
|
|
41
|
+
export declare function parseWindowsProcesses(stdout: string, cores?: number): PsEntry[];
|
|
42
|
+
/** Summarise each watched name against a process list (CPU and memory summed per name). */
|
|
43
|
+
export declare function matchProcesses(names: string[], entries: PsEntry[]): ProcessInfo[];
|
|
14
44
|
/**
|
|
15
45
|
* Scan for processes by name and return their status and resource usage.
|
|
16
|
-
*
|
|
46
|
+
* Uses `ps` on macOS/Linux and CIM on Windows. Throws when the process list is unavailable.
|
|
17
47
|
*/
|
|
18
48
|
export declare function scanProcesses(names: string[]): Promise<ProcessInfo[]>;
|
|
19
49
|
//# sourceMappingURL=process-scanner.d.ts.map
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
|
+
import { cpus } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
3
5
|
const execFileAsync = promisify(execFile);
|
|
6
|
+
/** Bounds on the `ps` call: a wedged or enormous process table can't stall or flood us. */
|
|
7
|
+
const PS_TIMEOUT_MS = 5000;
|
|
8
|
+
const PS_MAX_BUFFER = 16 * 1024 * 1024;
|
|
9
|
+
/** Linux truncates `comm` to 15 characters (TASK_COMM_LEN - 1). */
|
|
10
|
+
const LINUX_COMM_LEN = 15;
|
|
4
11
|
/**
|
|
5
12
|
* Compare the previous and current running-process sets and return speech
|
|
6
13
|
* strings for any transitions: "<name> died!" when a process disappears and
|
|
@@ -19,92 +26,112 @@ export function detectTransitions(prev, curr) {
|
|
|
19
26
|
return messages;
|
|
20
27
|
}
|
|
21
28
|
/**
|
|
22
|
-
* Parse
|
|
23
|
-
*
|
|
29
|
+
* Parse header-less `ps -eo pid=,pcpu=,rss=,comm=` output. COMMAND is the rest
|
|
30
|
+
* of the line, so macOS paths with spaces ("Google Chrome") survive intact.
|
|
24
31
|
*/
|
|
25
|
-
function parsePsOutput(stdout) {
|
|
26
|
-
const lines = stdout.trim().split('\n');
|
|
27
|
-
// Skip the header line
|
|
32
|
+
export function parsePsOutput(stdout) {
|
|
28
33
|
const entries = [];
|
|
29
|
-
for (
|
|
30
|
-
const
|
|
31
|
-
if (!
|
|
34
|
+
for (const line of stdout.split('\n')) {
|
|
35
|
+
const m = line.match(/^\s*(\d+)\s+([\d.]+)\s+(\d+)\s+(.+?)\s*$/);
|
|
36
|
+
if (!m)
|
|
32
37
|
continue;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const memKb = parseInt(parts[2], 10);
|
|
40
|
-
// COMMAND is everything from the 4th column onward
|
|
41
|
-
const comm = parts.slice(3).join(' ');
|
|
42
|
-
if (Number.isNaN(pid) || Number.isNaN(cpu) || Number.isNaN(memKb))
|
|
43
|
-
continue;
|
|
44
|
-
entries.push({ pid, cpu, memKb, comm });
|
|
38
|
+
entries.push({
|
|
39
|
+
pid: Number(m[1]),
|
|
40
|
+
cpu: Number(m[2]),
|
|
41
|
+
memKb: Number(m[3]),
|
|
42
|
+
comm: m[4],
|
|
43
|
+
});
|
|
45
44
|
}
|
|
46
45
|
return entries;
|
|
47
46
|
}
|
|
48
47
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
48
|
+
* Does a ps COMMAND entry match a watched name? Case-insensitive on the
|
|
49
|
+
* executable's basename:
|
|
50
|
+
* - exact: `nginx` matches `/usr/sbin/nginx`;
|
|
51
|
+
* - version suffix: `python` matches `python3` and `python3.11`;
|
|
52
|
+
* - Linux truncation: a name longer than 15 chars matches its 15-char prefix;
|
|
53
|
+
* - a name containing `/` must equal the full path (macOS reports paths).
|
|
54
|
+
* Unlike a substring match, `sh` does not match `bash` or `ssh-agent`.
|
|
52
55
|
*/
|
|
53
|
-
function matchesName(comm, searchName) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
export function matchesName(comm, searchName) {
|
|
57
|
+
if (/[\\/]/.test(searchName))
|
|
58
|
+
return comm === searchName;
|
|
59
|
+
const base = (comm.split(/[\\/]/).pop() ?? comm).toLowerCase().replace(/\.exe$/, '');
|
|
60
|
+
const name = searchName.toLowerCase().replace(/\.exe$/, '');
|
|
61
|
+
if (base === name)
|
|
57
62
|
return true;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return
|
|
63
|
+
if (name.length > LINUX_COMM_LEN && base === name.slice(0, LINUX_COMM_LEN))
|
|
64
|
+
return true;
|
|
65
|
+
return base.startsWith(name) && /^\d[\d.]*$/.test(base.slice(name.length));
|
|
61
66
|
}
|
|
62
|
-
/**
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
let entries;
|
|
77
|
-
try {
|
|
78
|
-
const { stdout } = await execFileAsync('ps', psArgs);
|
|
79
|
-
entries = parsePsOutput(stdout);
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
// If ps fails, report all processes as unknown / not running
|
|
83
|
-
return names.map((name) => ({ name, running: false }));
|
|
67
|
+
/** List every process. Throws if `ps` fails — callers must not read that as "all dead". */
|
|
68
|
+
export async function listProcesses() {
|
|
69
|
+
if (process.platform === 'win32') {
|
|
70
|
+
// Fixed command, no user strings interpolated. Performance counters report a
|
|
71
|
+
// sampling interval (Get-Process.CPU is lifetime CPU seconds, not a percent).
|
|
72
|
+
const executable = join(process.env['SystemRoot'] || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
73
|
+
const { stdout } = await execFileAsync(executable, [
|
|
74
|
+
'-NoProfile',
|
|
75
|
+
'-NonInteractive',
|
|
76
|
+
'-Command',
|
|
77
|
+
'$ErrorActionPreference="Stop"; Get-CimInstance Win32_PerfFormattedData_PerfProc_Process | Select-Object IDProcess,Name,PercentProcessorTime,WorkingSetPrivate | ConvertTo-Json -Compress',
|
|
78
|
+
], { timeout: PS_TIMEOUT_MS, maxBuffer: PS_MAX_BUFFER, windowsHide: true });
|
|
79
|
+
return parseWindowsProcesses(stdout, cpus().length);
|
|
84
80
|
}
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
81
|
+
const { stdout } = await execFileAsync('ps', ['-eo', 'pid=,pcpu=,rss=,comm='], {
|
|
82
|
+
timeout: PS_TIMEOUT_MS,
|
|
83
|
+
maxBuffer: PS_MAX_BUFFER,
|
|
84
|
+
// Some locales print %CPU with a decimal comma ("1,5"); force "1.5".
|
|
85
|
+
env: { ...process.env, LC_ALL: 'C' },
|
|
86
|
+
});
|
|
87
|
+
return parsePsOutput(stdout);
|
|
88
|
+
}
|
|
89
|
+
/** Windows instance suffixes (#1, #2) distinguish copies of the same executable. */
|
|
90
|
+
export function parseWindowsProcesses(stdout, cores = 1) {
|
|
91
|
+
const value = JSON.parse(stdout.replace(/^\uFEFF/, ''));
|
|
92
|
+
const rows = Array.isArray(value) ? value : value ? [value] : [];
|
|
93
|
+
return rows.flatMap((row) => {
|
|
94
|
+
const pid = Number(row.IDProcess);
|
|
95
|
+
const cpu = Number(row.PercentProcessorTime);
|
|
96
|
+
const bytes = Number(row.WorkingSetPrivate);
|
|
97
|
+
if (!(pid > 0) ||
|
|
98
|
+
!Number.isFinite(cpu) ||
|
|
99
|
+
!Number.isFinite(bytes) ||
|
|
100
|
+
typeof row.Name !== 'string')
|
|
101
|
+
return [];
|
|
102
|
+
return [
|
|
103
|
+
{
|
|
104
|
+
pid,
|
|
105
|
+
cpu: cpu / Math.max(1, cores),
|
|
106
|
+
memKb: bytes / 1024,
|
|
107
|
+
comm: row.Name.replace(/#\d+$/, ''),
|
|
108
|
+
},
|
|
109
|
+
];
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/** Summarise each watched name against a process list (CPU and memory summed per name). */
|
|
113
|
+
export function matchProcesses(names, entries) {
|
|
114
|
+
return names.map((name) => {
|
|
88
115
|
const matching = entries.filter((e) => matchesName(e.comm, name));
|
|
89
|
-
if (matching.length === 0)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
let totalCpu = 0;
|
|
95
|
-
let totalMemKb = 0;
|
|
96
|
-
for (const entry of matching) {
|
|
97
|
-
totalCpu += entry.cpu;
|
|
98
|
-
totalMemKb += entry.memKb;
|
|
99
|
-
}
|
|
100
|
-
results.push({
|
|
116
|
+
if (matching.length === 0)
|
|
117
|
+
return { name, running: false };
|
|
118
|
+
const totalCpu = matching.reduce((sum, e) => sum + e.cpu, 0);
|
|
119
|
+
const totalMemKb = matching.reduce((sum, e) => sum + e.memKb, 0);
|
|
120
|
+
return {
|
|
101
121
|
name,
|
|
102
122
|
running: true,
|
|
103
123
|
pid: matching[0].pid,
|
|
124
|
+
count: matching.length,
|
|
104
125
|
cpuPercent: Math.round(totalCpu * 100) / 100,
|
|
105
126
|
memMb: Math.round((totalMemKb / 1024) * 100) / 100,
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Scan for processes by name and return their status and resource usage.
|
|
132
|
+
* Uses `ps` on macOS/Linux and CIM on Windows. Throws when the process list is unavailable.
|
|
133
|
+
*/
|
|
134
|
+
export async function scanProcesses(names) {
|
|
135
|
+
return matchProcesses(names, await listProcesses());
|
|
109
136
|
}
|
|
110
137
|
//# sourceMappingURL=process-scanner.js.map
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export declare const DEFAULTS: {
|
|
2
2
|
pollInterval: number;
|
|
3
3
|
weatherRefreshMs: number;
|
|
4
|
+
/** Drop a reading this old (repeated refresh failures) rather than show stale weather. */
|
|
5
|
+
weatherMaxAgeMs: number;
|
|
6
|
+
/** Wait this long before retrying a failed geocode / IP geo / weather call. */
|
|
7
|
+
retryBackoffMs: number;
|
|
4
8
|
fetchTimeoutMs: number;
|
|
5
9
|
};
|
|
6
10
|
//# sourceMappingURL=defaults.d.ts.map
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export const DEFAULTS = {
|
|
2
2
|
pollInterval: 60000, // 60s clock tick
|
|
3
3
|
weatherRefreshMs: 15 * 60 * 1000, // refresh weather every 15 minutes
|
|
4
|
+
/** Drop a reading this old (repeated refresh failures) rather than show stale weather. */
|
|
5
|
+
weatherMaxAgeMs: 2 * 60 * 60 * 1000,
|
|
6
|
+
/** Wait this long before retrying a failed geocode / IP geo / weather call. */
|
|
7
|
+
retryBackoffMs: 5 * 60 * 1000,
|
|
4
8
|
fetchTimeoutMs: 5000,
|
|
5
9
|
};
|
|
6
10
|
//# sourceMappingURL=defaults.js.map
|
|
@@ -1,29 +1,20 @@
|
|
|
1
1
|
import { PollingNPC } from '../../core/polling-npc.js';
|
|
2
2
|
import type { PollingNpcConfig } from '../../core/polling-npc.js';
|
|
3
|
-
import type { CollectedMetrics, Archetype, BehaviorHints
|
|
3
|
+
import type { CollectedMetrics, Archetype, BehaviorHints } from '../../core/types.js';
|
|
4
4
|
export interface ReceptionistConfig extends PollingNpcConfig {
|
|
5
|
+
/** @deprecated Reception no longer uses weather or geolocation. Accepted for compatibility. */
|
|
5
6
|
lat?: number;
|
|
6
7
|
lon?: number;
|
|
7
8
|
location?: string;
|
|
8
|
-
/** Display temperature in Fahrenheit instead of Celsius. */
|
|
9
9
|
fahrenheit?: boolean;
|
|
10
|
+
ipGeo?: boolean;
|
|
10
11
|
}
|
|
11
12
|
export declare class ReceptionistNPC extends PollingNPC {
|
|
12
|
-
private explicitCoords;
|
|
13
|
-
private location?;
|
|
14
|
-
private fahrenheit;
|
|
15
|
-
private coords;
|
|
16
|
-
private geoAttempted;
|
|
17
|
-
private weather;
|
|
18
|
-
private lastWeatherFetch;
|
|
19
13
|
constructor(config: ReceptionistConfig);
|
|
20
14
|
get name(): string;
|
|
21
15
|
protected get defaultDisplayName(): string;
|
|
22
16
|
get archetype(): Archetype;
|
|
23
17
|
get behavior(): BehaviorHints;
|
|
24
|
-
get companion(): CompanionDecl;
|
|
25
18
|
protected collect(): Promise<CollectedMetrics>;
|
|
26
|
-
private refreshWeather;
|
|
27
|
-
private resolveCoords;
|
|
28
19
|
}
|
|
29
20
|
//# sourceMappingURL=index.d.ts.map
|