@nocville/npc 0.0.0-stage → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +268 -2
- package/dist/cjs/cli.js +584 -0
- package/dist/cjs/core/api-key.js +69 -0
- package/dist/cjs/core/base-npc.js +160 -0
- package/dist/cjs/core/config.js +108 -0
- package/dist/cjs/core/event-npc.js +14 -0
- package/dist/cjs/core/logger.js +39 -0
- package/dist/cjs/core/polling-npc.js +65 -0
- package/dist/cjs/core/screen.js +28 -0
- package/dist/cjs/core/session-filter.js +150 -0
- package/dist/cjs/core/text.js +15 -0
- package/dist/cjs/core/thresholds.js +48 -0
- package/dist/cjs/core/types.js +10 -0
- package/dist/cjs/core/user-config.js +92 -0
- package/dist/cjs/doctor.js +130 -0
- package/dist/cjs/index.js +95 -0
- package/dist/cjs/npcs/agent-common/activity.js +291 -0
- package/dist/cjs/npcs/agent-common/git-stats.js +71 -0
- package/dist/cjs/npcs/agent-common/hook-runner.js +252 -0
- package/dist/cjs/npcs/agent-common/hook-settings.js +279 -0
- package/dist/cjs/npcs/agent-common/liveness.js +112 -0
- package/dist/cjs/npcs/agent-common/lock.js +99 -0
- package/dist/cjs/npcs/agent-common/runtime.js +154 -0
- package/dist/cjs/npcs/agent-common/session.js +412 -0
- package/dist/cjs/npcs/agent-common/shim.js +96 -0
- package/dist/cjs/npcs/agent-common/watchdog.js +149 -0
- package/dist/cjs/npcs/claude-code/defaults.js +20 -0
- package/dist/cjs/npcs/claude-code/hook-handler.js +139 -0
- package/dist/cjs/npcs/claude-code/index.js +104 -0
- package/dist/cjs/npcs/claude-code/setup.js +133 -0
- package/dist/cjs/npcs/claude-code/state-machine.js +130 -0
- package/dist/cjs/npcs/codex/defaults.js +32 -0
- package/dist/cjs/npcs/codex/hook-handler.js +116 -0
- package/dist/cjs/npcs/codex/index.js +92 -0
- package/dist/cjs/npcs/codex/rollout.js +246 -0
- package/dist/cjs/npcs/codex/setup.js +104 -0
- package/dist/cjs/npcs/codex/watcher.js +361 -0
- package/dist/cjs/npcs/cursor/defaults.js +15 -0
- package/dist/cjs/npcs/cursor/hook-handler.js +65 -0
- package/dist/cjs/npcs/cursor/index.js +72 -0
- package/dist/cjs/npcs/cursor/normalize.js +54 -0
- package/dist/cjs/npcs/cursor/setup.js +98 -0
- package/dist/cjs/npcs/gemini/defaults.js +15 -0
- package/dist/cjs/npcs/gemini/hook-handler.js +93 -0
- package/dist/cjs/npcs/gemini/index.js +69 -0
- package/dist/cjs/npcs/gemini/setup.js +98 -0
- package/dist/cjs/npcs/github-watcher/defaults.js +11 -0
- package/dist/cjs/npcs/github-watcher/format.js +21 -0
- package/dist/cjs/npcs/github-watcher/github-api.js +126 -0
- package/dist/cjs/npcs/github-watcher/index.js +148 -0
- package/dist/cjs/npcs/internet-connectivity/checks.js +92 -0
- package/dist/cjs/npcs/internet-connectivity/defaults.js +22 -0
- package/dist/cjs/npcs/internet-connectivity/index.js +251 -0
- package/dist/cjs/npcs/log-watcher/buffer.js +40 -0
- package/dist/cjs/npcs/log-watcher/defaults.js +12 -0
- package/dist/cjs/npcs/log-watcher/index.js +161 -0
- package/dist/cjs/npcs/log-watcher/tail.js +81 -0
- package/dist/cjs/npcs/process-watcher/defaults.js +9 -0
- package/dist/cjs/npcs/process-watcher/index.js +140 -0
- package/dist/cjs/npcs/process-watcher/process-scanner.js +114 -0
- package/dist/cjs/npcs/receptionist/defaults.js +9 -0
- package/dist/cjs/npcs/receptionist/index.js +120 -0
- package/dist/cjs/npcs/receptionist/weather.js +129 -0
- package/dist/cjs/npcs/system-monitor/collectors.js +68 -0
- package/dist/cjs/npcs/system-monitor/defaults.js +13 -0
- package/dist/cjs/npcs/system-monitor/index.js +105 -0
- package/dist/cjs/npcs/web-page-watcher/basic-mode.js +58 -0
- package/dist/cjs/npcs/web-page-watcher/capture.js +80 -0
- package/dist/cjs/npcs/web-page-watcher/defaults.js +31 -0
- package/dist/cjs/npcs/web-page-watcher/index.js +234 -0
- package/dist/cjs/npcs/web-page-watcher/pixelate.js +181 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +549 -0
- package/dist/core/api-key.d.ts +34 -0
- package/dist/core/api-key.js +64 -0
- package/dist/core/base-npc.d.ts +41 -0
- package/dist/core/base-npc.js +156 -0
- package/dist/core/config.d.ts +22 -0
- package/dist/core/config.js +101 -0
- package/dist/core/event-npc.d.ts +8 -0
- package/dist/core/event-npc.js +10 -0
- package/dist/core/logger.d.ts +11 -0
- package/dist/core/logger.js +35 -0
- package/dist/core/polling-npc.d.ts +18 -0
- package/dist/core/polling-npc.js +61 -0
- package/dist/core/screen.d.ts +11 -0
- package/dist/core/screen.js +24 -0
- package/dist/core/session-filter.d.ts +59 -0
- package/dist/core/session-filter.js +143 -0
- package/dist/core/text.d.ts +6 -0
- package/dist/core/text.js +12 -0
- package/dist/core/thresholds.d.ts +10 -0
- package/dist/core/thresholds.js +44 -0
- package/dist/core/types.d.ts +60 -0
- package/dist/core/types.js +7 -0
- package/dist/core/user-config.d.ts +43 -0
- package/dist/core/user-config.js +85 -0
- package/dist/doctor.d.ts +8 -0
- package/dist/doctor.js +126 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +26 -0
- package/dist/npcs/agent-common/activity.d.ts +42 -0
- package/dist/npcs/agent-common/activity.js +280 -0
- package/dist/npcs/agent-common/git-stats.d.ts +11 -0
- package/dist/npcs/agent-common/git-stats.js +67 -0
- package/dist/npcs/agent-common/hook-runner.d.ts +62 -0
- package/dist/npcs/agent-common/hook-runner.js +245 -0
- package/dist/npcs/agent-common/hook-settings.d.ts +75 -0
- package/dist/npcs/agent-common/hook-settings.js +265 -0
- package/dist/npcs/agent-common/liveness.d.ts +12 -0
- package/dist/npcs/agent-common/liveness.js +105 -0
- package/dist/npcs/agent-common/lock.d.ts +6 -0
- package/dist/npcs/agent-common/lock.js +95 -0
- package/dist/npcs/agent-common/runtime.d.ts +16 -0
- package/dist/npcs/agent-common/runtime.js +149 -0
- package/dist/npcs/agent-common/session.d.ts +192 -0
- package/dist/npcs/agent-common/session.js +390 -0
- package/dist/npcs/agent-common/shim.d.ts +4 -0
- package/dist/npcs/agent-common/shim.js +90 -0
- package/dist/npcs/agent-common/watchdog.d.ts +16 -0
- package/dist/npcs/agent-common/watchdog.js +144 -0
- package/dist/npcs/claude-code/defaults.d.ts +15 -0
- package/dist/npcs/claude-code/defaults.js +17 -0
- package/dist/npcs/claude-code/hook-handler.d.ts +40 -0
- package/dist/npcs/claude-code/hook-handler.js +131 -0
- package/dist/npcs/claude-code/index.d.ts +36 -0
- package/dist/npcs/claude-code/index.js +92 -0
- package/dist/npcs/claude-code/setup.d.ts +58 -0
- package/dist/npcs/claude-code/setup.js +121 -0
- package/dist/npcs/claude-code/state-machine.d.ts +28 -0
- package/dist/npcs/claude-code/state-machine.js +125 -0
- package/dist/npcs/codex/defaults.d.ts +24 -0
- package/dist/npcs/codex/defaults.js +28 -0
- package/dist/npcs/codex/hook-handler.d.ts +24 -0
- package/dist/npcs/codex/hook-handler.js +110 -0
- package/dist/npcs/codex/index.d.ts +21 -0
- package/dist/npcs/codex/index.js +76 -0
- package/dist/npcs/codex/rollout.d.ts +39 -0
- package/dist/npcs/codex/rollout.js +242 -0
- package/dist/npcs/codex/setup.d.ts +37 -0
- package/dist/npcs/codex/setup.js +96 -0
- package/dist/npcs/codex/watcher.d.ts +65 -0
- package/dist/npcs/codex/watcher.js +356 -0
- package/dist/npcs/cursor/defaults.d.ts +9 -0
- package/dist/npcs/cursor/defaults.js +11 -0
- package/dist/npcs/cursor/hook-handler.d.ts +8 -0
- package/dist/npcs/cursor/hook-handler.js +60 -0
- package/dist/npcs/cursor/index.d.ts +18 -0
- package/dist/npcs/cursor/index.js +57 -0
- package/dist/npcs/cursor/normalize.d.ts +6 -0
- package/dist/npcs/cursor/normalize.js +49 -0
- package/dist/npcs/cursor/setup.d.ts +29 -0
- package/dist/npcs/cursor/setup.js +90 -0
- package/dist/npcs/gemini/defaults.d.ts +9 -0
- package/dist/npcs/gemini/defaults.js +11 -0
- package/dist/npcs/gemini/hook-handler.d.ts +10 -0
- package/dist/npcs/gemini/hook-handler.js +87 -0
- package/dist/npcs/gemini/index.d.ts +17 -0
- package/dist/npcs/gemini/index.js +56 -0
- package/dist/npcs/gemini/setup.d.ts +29 -0
- package/dist/npcs/gemini/setup.js +90 -0
- package/dist/npcs/github-watcher/defaults.d.ts +8 -0
- package/dist/npcs/github-watcher/defaults.js +8 -0
- package/dist/npcs/github-watcher/format.d.ts +3 -0
- package/dist/npcs/github-watcher/format.js +18 -0
- package/dist/npcs/github-watcher/github-api.d.ts +55 -0
- package/dist/npcs/github-watcher/github-api.js +120 -0
- package/dist/npcs/github-watcher/index.d.ts +35 -0
- package/dist/npcs/github-watcher/index.js +144 -0
- package/dist/npcs/internet-connectivity/checks.d.ts +24 -0
- package/dist/npcs/internet-connectivity/checks.js +84 -0
- package/dist/npcs/internet-connectivity/defaults.d.ts +20 -0
- package/dist/npcs/internet-connectivity/defaults.js +19 -0
- package/dist/npcs/internet-connectivity/index.d.ts +55 -0
- package/dist/npcs/internet-connectivity/index.js +246 -0
- package/dist/npcs/log-watcher/buffer.d.ts +18 -0
- package/dist/npcs/log-watcher/buffer.js +34 -0
- package/dist/npcs/log-watcher/defaults.d.ts +9 -0
- package/dist/npcs/log-watcher/defaults.js +9 -0
- package/dist/npcs/log-watcher/index.d.ts +45 -0
- package/dist/npcs/log-watcher/index.js +157 -0
- package/dist/npcs/log-watcher/tail.d.ts +9 -0
- package/dist/npcs/log-watcher/tail.js +78 -0
- package/dist/npcs/process-watcher/defaults.d.ts +6 -0
- package/dist/npcs/process-watcher/defaults.js +6 -0
- package/dist/npcs/process-watcher/index.d.ts +26 -0
- package/dist/npcs/process-watcher/index.js +136 -0
- package/dist/npcs/process-watcher/process-scanner.d.ts +19 -0
- package/dist/npcs/process-watcher/process-scanner.js +110 -0
- package/dist/npcs/receptionist/defaults.d.ts +6 -0
- package/dist/npcs/receptionist/defaults.js +6 -0
- package/dist/npcs/receptionist/index.d.ts +29 -0
- package/dist/npcs/receptionist/index.js +116 -0
- package/dist/npcs/receptionist/weather.d.ts +37 -0
- package/dist/npcs/receptionist/weather.js +121 -0
- package/dist/npcs/system-monitor/collectors.d.ts +45 -0
- package/dist/npcs/system-monitor/collectors.js +60 -0
- package/dist/npcs/system-monitor/defaults.d.ts +7 -0
- package/dist/npcs/system-monitor/defaults.js +10 -0
- package/dist/npcs/system-monitor/index.d.ts +27 -0
- package/dist/npcs/system-monitor/index.js +101 -0
- package/dist/npcs/web-page-watcher/basic-mode.d.ts +16 -0
- package/dist/npcs/web-page-watcher/basic-mode.js +52 -0
- package/dist/npcs/web-page-watcher/capture.d.ts +49 -0
- package/dist/npcs/web-page-watcher/capture.js +75 -0
- package/dist/npcs/web-page-watcher/defaults.d.ts +19 -0
- package/dist/npcs/web-page-watcher/defaults.js +27 -0
- package/dist/npcs/web-page-watcher/index.d.ts +42 -0
- package/dist/npcs/web-page-watcher/index.js +230 -0
- package/dist/npcs/web-page-watcher/pixelate.d.ts +36 -0
- package/dist/npcs/web-page-watcher/pixelate.js +172 -0
- package/package.json +73 -3
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { PollingNPC } from '../../core/polling-npc.js';
|
|
2
|
+
import { worstStatus } from '../../core/thresholds.js';
|
|
3
|
+
import { pushHistory } from '../../core/screen.js';
|
|
4
|
+
import { scanProcesses, detectTransitions } from './process-scanner.js';
|
|
5
|
+
import { DEFAULTS } from './defaults.js';
|
|
6
|
+
/** Max per-process panels on the companion dashboard. */
|
|
7
|
+
const MAX_PANELS = 8;
|
|
8
|
+
export class ProcessWatcherNPC extends PollingNPC {
|
|
9
|
+
processes;
|
|
10
|
+
cpuWarn;
|
|
11
|
+
memWarn;
|
|
12
|
+
cpuHistories = new Map();
|
|
13
|
+
previousRunning = null;
|
|
14
|
+
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
|
+
super(config);
|
|
20
|
+
this.processes = config.processes;
|
|
21
|
+
this.cpuWarn = config.cpuWarn ?? DEFAULTS.cpuWarn;
|
|
22
|
+
this.memWarn = config.memWarn ?? DEFAULTS.memWarn;
|
|
23
|
+
}
|
|
24
|
+
get name() {
|
|
25
|
+
return 'process-watcher';
|
|
26
|
+
}
|
|
27
|
+
get defaultDisplayName() {
|
|
28
|
+
return 'Processes';
|
|
29
|
+
}
|
|
30
|
+
get archetype() {
|
|
31
|
+
return 'service';
|
|
32
|
+
}
|
|
33
|
+
get heartbeatSeconds() {
|
|
34
|
+
return 30;
|
|
35
|
+
}
|
|
36
|
+
get companion() {
|
|
37
|
+
return {
|
|
38
|
+
screenType: 'dashboard-wall',
|
|
39
|
+
size: { w: 2, h: 2 },
|
|
40
|
+
placement: 'on-desk',
|
|
41
|
+
label: 'Processes',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async collect() {
|
|
45
|
+
const infos = await scanProcesses(this.processes);
|
|
46
|
+
const statuses = [];
|
|
47
|
+
const messageParts = [];
|
|
48
|
+
const metrics = {};
|
|
49
|
+
const panels = [];
|
|
50
|
+
let totalRunning = 0;
|
|
51
|
+
let totalMissing = 0;
|
|
52
|
+
for (const info of infos) {
|
|
53
|
+
const safeKey = info.name.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
54
|
+
if (!info.running) {
|
|
55
|
+
statuses.push('critical');
|
|
56
|
+
messageParts.push(`${info.name}: NOT FOUND`);
|
|
57
|
+
metrics[`${safeKey}_running`] = 0;
|
|
58
|
+
metrics[`${safeKey}_cpu`] = 0;
|
|
59
|
+
metrics[`${safeKey}_mem_mb`] = 0;
|
|
60
|
+
totalMissing++;
|
|
61
|
+
if (panels.length < MAX_PANELS) {
|
|
62
|
+
panels.push({ label: info.name, value: 'DOWN', color: 'red' });
|
|
63
|
+
}
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
totalRunning++;
|
|
67
|
+
const cpu = info.cpuPercent ?? 0;
|
|
68
|
+
metrics[`${safeKey}_running`] = 1;
|
|
69
|
+
metrics[`${safeKey}_cpu`] = cpu;
|
|
70
|
+
metrics[`${safeKey}_mem_mb`] = info.memMb ?? 0;
|
|
71
|
+
const history = this.cpuHistories.get(info.name) ?? [];
|
|
72
|
+
pushHistory(history, cpu);
|
|
73
|
+
this.cpuHistories.set(info.name, history);
|
|
74
|
+
const cpuHigh = cpu >= this.cpuWarn;
|
|
75
|
+
const memHigh = (info.memMb ?? 0) >= this.memWarn;
|
|
76
|
+
if (cpuHigh || memHigh) {
|
|
77
|
+
statuses.push('warning');
|
|
78
|
+
const warnings = [];
|
|
79
|
+
if (cpuHigh)
|
|
80
|
+
warnings.push(`cpu=${cpu}%`);
|
|
81
|
+
if (memHigh)
|
|
82
|
+
warnings.push(`mem=${info.memMb}MB`);
|
|
83
|
+
messageParts.push(`${info.name}: WARN (${warnings.join(', ')})`);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
statuses.push('healthy');
|
|
87
|
+
messageParts.push(`${info.name}: OK`);
|
|
88
|
+
}
|
|
89
|
+
if (panels.length < MAX_PANELS) {
|
|
90
|
+
panels.push({
|
|
91
|
+
label: info.name,
|
|
92
|
+
value: `${cpu}%`,
|
|
93
|
+
color: cpuHigh || memHigh ? 'amber' : 'green',
|
|
94
|
+
sparkline: [...history],
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
metrics['total_watched'] = this.processes.length;
|
|
99
|
+
metrics['total_running'] = totalRunning;
|
|
100
|
+
metrics['total_missing'] = totalMissing;
|
|
101
|
+
// Transition speech: compare the running set to last poll's. On the first
|
|
102
|
+
// poll we only seed the set (no bogus "is back" for already-running procs).
|
|
103
|
+
const currentRunning = new Set(infos.filter((i) => i.running).map((i) => i.name));
|
|
104
|
+
let transitions = [];
|
|
105
|
+
if (this.previousRunning !== null) {
|
|
106
|
+
transitions = detectTransitions(this.previousRunning, currentRunning);
|
|
107
|
+
}
|
|
108
|
+
this.previousRunning = currentRunning;
|
|
109
|
+
const status = worstStatus(statuses);
|
|
110
|
+
const mood = deriveMood(status);
|
|
111
|
+
// A death message takes over the bubble this cycle so it reads as an alert.
|
|
112
|
+
const message = transitions.length > 0 ? transitions.join(' · ') : messageParts.join(' | ');
|
|
113
|
+
const screenData = { layout: 'multi-metric', values: panels };
|
|
114
|
+
return {
|
|
115
|
+
status,
|
|
116
|
+
mood,
|
|
117
|
+
message,
|
|
118
|
+
subStatus: totalMissing > 0 ? 'process-down' : undefined,
|
|
119
|
+
screenData,
|
|
120
|
+
metrics,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function deriveMood(status) {
|
|
125
|
+
switch (status) {
|
|
126
|
+
case 'healthy':
|
|
127
|
+
return 'happy';
|
|
128
|
+
case 'warning':
|
|
129
|
+
return 'concerned';
|
|
130
|
+
case 'critical':
|
|
131
|
+
return 'stressed';
|
|
132
|
+
default:
|
|
133
|
+
return 'idle';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface ProcessInfo {
|
|
2
|
+
name: string;
|
|
3
|
+
running: boolean;
|
|
4
|
+
pid?: number;
|
|
5
|
+
cpuPercent?: number;
|
|
6
|
+
memMb?: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Compare the previous and current running-process sets and return speech
|
|
10
|
+
* strings for any transitions: "<name> died!" when a process disappears and
|
|
11
|
+
* "<name> is back" when one returns.
|
|
12
|
+
*/
|
|
13
|
+
export declare function detectTransitions(prev: Set<string>, curr: Set<string>): string[];
|
|
14
|
+
/**
|
|
15
|
+
* Scan for processes by name and return their status and resource usage.
|
|
16
|
+
* Works on macOS and Linux by using `ps` to list all processes.
|
|
17
|
+
*/
|
|
18
|
+
export declare function scanProcesses(names: string[]): Promise<ProcessInfo[]>;
|
|
19
|
+
//# sourceMappingURL=process-scanner.d.ts.map
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
/**
|
|
5
|
+
* Compare the previous and current running-process sets and return speech
|
|
6
|
+
* strings for any transitions: "<name> died!" when a process disappears and
|
|
7
|
+
* "<name> is back" when one returns.
|
|
8
|
+
*/
|
|
9
|
+
export function detectTransitions(prev, curr) {
|
|
10
|
+
const messages = [];
|
|
11
|
+
for (const name of prev) {
|
|
12
|
+
if (!curr.has(name))
|
|
13
|
+
messages.push(`${name} died!`);
|
|
14
|
+
}
|
|
15
|
+
for (const name of curr) {
|
|
16
|
+
if (!prev.has(name))
|
|
17
|
+
messages.push(`${name} is back`);
|
|
18
|
+
}
|
|
19
|
+
return messages;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Parse the output of `ps` into structured entries.
|
|
23
|
+
* Expects columns: PID, %CPU, RSS (KB), COMMAND
|
|
24
|
+
*/
|
|
25
|
+
function parsePsOutput(stdout) {
|
|
26
|
+
const lines = stdout.trim().split('\n');
|
|
27
|
+
// Skip the header line
|
|
28
|
+
const entries = [];
|
|
29
|
+
for (let i = 1; i < lines.length; i++) {
|
|
30
|
+
const line = lines[i].trim();
|
|
31
|
+
if (!line)
|
|
32
|
+
continue;
|
|
33
|
+
// Fields are whitespace-separated: PID, %CPU, RSS, COMMAND (may contain spaces)
|
|
34
|
+
const parts = line.split(/\s+/);
|
|
35
|
+
if (parts.length < 4)
|
|
36
|
+
continue;
|
|
37
|
+
const pid = parseInt(parts[0], 10);
|
|
38
|
+
const cpu = parseFloat(parts[1]);
|
|
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 });
|
|
45
|
+
}
|
|
46
|
+
return entries;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Match a process name against a ps COMMAND column.
|
|
50
|
+
* Matches if the basename of the command path equals the search name,
|
|
51
|
+
* or if the command contains the search name.
|
|
52
|
+
*/
|
|
53
|
+
function matchesName(comm, searchName) {
|
|
54
|
+
// Get the basename of the command (strip path and arguments)
|
|
55
|
+
const basename = comm.split('/').pop()?.split(/\s/)[0] ?? '';
|
|
56
|
+
if (basename === searchName)
|
|
57
|
+
return true;
|
|
58
|
+
// Also check if the full command string contains the name
|
|
59
|
+
// (handles cases like "python my-script.py" when searching for "python")
|
|
60
|
+
return comm.toLowerCase().includes(searchName.toLowerCase());
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Scan for processes by name and return their status and resource usage.
|
|
64
|
+
* Works on macOS and Linux by using `ps` to list all processes.
|
|
65
|
+
*/
|
|
66
|
+
export async function scanProcesses(names) {
|
|
67
|
+
let psArgs;
|
|
68
|
+
if (process.platform === 'darwin') {
|
|
69
|
+
// macOS: -e selects all processes, -o specifies output columns
|
|
70
|
+
psArgs = ['-eo', 'pid,pcpu,rss,comm'];
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
// Linux: -eo with similar columns
|
|
74
|
+
psArgs = ['-eo', 'pid,pcpu,rss,comm'];
|
|
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 }));
|
|
84
|
+
}
|
|
85
|
+
const results = [];
|
|
86
|
+
for (const name of names) {
|
|
87
|
+
// Find all matching entries for this process name
|
|
88
|
+
const matching = entries.filter((e) => matchesName(e.comm, name));
|
|
89
|
+
if (matching.length === 0) {
|
|
90
|
+
results.push({ name, running: false });
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
// Aggregate: take the first matching PID, sum CPU and memory across all instances
|
|
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({
|
|
101
|
+
name,
|
|
102
|
+
running: true,
|
|
103
|
+
pid: matching[0].pid,
|
|
104
|
+
cpuPercent: Math.round(totalCpu * 100) / 100,
|
|
105
|
+
memMb: Math.round((totalMemKb / 1024) * 100) / 100,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return results;
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=process-scanner.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { PollingNPC } from '../../core/polling-npc.js';
|
|
2
|
+
import type { PollingNpcConfig } from '../../core/polling-npc.js';
|
|
3
|
+
import type { CollectedMetrics, Archetype, BehaviorHints, CompanionDecl } from '../../core/types.js';
|
|
4
|
+
export interface ReceptionistConfig extends PollingNpcConfig {
|
|
5
|
+
lat?: number;
|
|
6
|
+
lon?: number;
|
|
7
|
+
location?: string;
|
|
8
|
+
/** Display temperature in Fahrenheit instead of Celsius. */
|
|
9
|
+
fahrenheit?: boolean;
|
|
10
|
+
}
|
|
11
|
+
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
|
+
constructor(config: ReceptionistConfig);
|
|
20
|
+
get name(): string;
|
|
21
|
+
protected get defaultDisplayName(): string;
|
|
22
|
+
get archetype(): Archetype;
|
|
23
|
+
get behavior(): BehaviorHints;
|
|
24
|
+
get companion(): CompanionDecl;
|
|
25
|
+
protected collect(): Promise<CollectedMetrics>;
|
|
26
|
+
private refreshWeather;
|
|
27
|
+
private resolveCoords;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { PollingNPC } from '../../core/polling-npc.js';
|
|
2
|
+
import { DEFAULTS } from './defaults.js';
|
|
3
|
+
import { fetchWeather, geocodeLocation, autoGeo, formatClock, toFahrenheit } from './weather.js';
|
|
4
|
+
export class ReceptionistNPC extends PollingNPC {
|
|
5
|
+
explicitCoords;
|
|
6
|
+
location;
|
|
7
|
+
fahrenheit;
|
|
8
|
+
coords = null;
|
|
9
|
+
geoAttempted = false;
|
|
10
|
+
weather = null;
|
|
11
|
+
lastWeatherFetch = 0;
|
|
12
|
+
constructor(config) {
|
|
13
|
+
super({ ...config, pollInterval: config.pollInterval ?? DEFAULTS.pollInterval });
|
|
14
|
+
this.explicitCoords =
|
|
15
|
+
config.lat !== undefined && config.lon !== undefined
|
|
16
|
+
? { lat: config.lat, lon: config.lon }
|
|
17
|
+
: null;
|
|
18
|
+
this.location = config.location;
|
|
19
|
+
this.fahrenheit = config.fahrenheit ?? false;
|
|
20
|
+
}
|
|
21
|
+
get name() {
|
|
22
|
+
return 'receptionist';
|
|
23
|
+
}
|
|
24
|
+
get defaultDisplayName() {
|
|
25
|
+
return 'Reception';
|
|
26
|
+
}
|
|
27
|
+
get archetype() {
|
|
28
|
+
return 'custom';
|
|
29
|
+
}
|
|
30
|
+
get behavior() {
|
|
31
|
+
return {
|
|
32
|
+
greetPlayers: true,
|
|
33
|
+
patrol: { everyMinutes: [15, 30], stops: 5 },
|
|
34
|
+
idle: 'stay',
|
|
35
|
+
home: { near: 'entrance' },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
get companion() {
|
|
39
|
+
return {
|
|
40
|
+
screenType: 'welcome-sign',
|
|
41
|
+
size: { w: 2, h: 1 },
|
|
42
|
+
placement: 'adjacent-wall',
|
|
43
|
+
label: 'Welcome',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
async collect() {
|
|
47
|
+
await this.refreshWeather();
|
|
48
|
+
const now = new Date();
|
|
49
|
+
const { day, date, time } = formatClock(now);
|
|
50
|
+
const values = [
|
|
51
|
+
{ label: 'time', value: time },
|
|
52
|
+
{ label: 'day', value: day },
|
|
53
|
+
{ label: 'date', value: date },
|
|
54
|
+
];
|
|
55
|
+
let message = `${day} ${time}`;
|
|
56
|
+
const metrics = { weather_ok: this.weather ? 1 : 0 };
|
|
57
|
+
if (this.weather) {
|
|
58
|
+
const temp = this.fahrenheit
|
|
59
|
+
? `${toFahrenheit(this.weather.tempC)}°F`
|
|
60
|
+
: `${this.weather.tempC}°C`;
|
|
61
|
+
message += ` • ${temp} ${this.weather.text}`;
|
|
62
|
+
values.push({ label: 'temp', value: temp }, { label: 'condition', value: this.weather.text }, { label: 'icon', value: this.weather.icon });
|
|
63
|
+
metrics['temp_c'] = this.weather.tempC;
|
|
64
|
+
metrics['weather_code'] = this.weather.code;
|
|
65
|
+
metrics['wind_kph'] = this.weather.windKph;
|
|
66
|
+
}
|
|
67
|
+
// The receptionist is a greeter/clock — it stays healthy even when weather
|
|
68
|
+
// is unavailable (degraded time-only mode).
|
|
69
|
+
return {
|
|
70
|
+
status: 'healthy',
|
|
71
|
+
mood: 'happy',
|
|
72
|
+
message,
|
|
73
|
+
screenData: { layout: 'welcome-sign', values },
|
|
74
|
+
metrics,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
async refreshWeather() {
|
|
78
|
+
if (!this.coords) {
|
|
79
|
+
await this.resolveCoords();
|
|
80
|
+
}
|
|
81
|
+
if (!this.coords)
|
|
82
|
+
return;
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
if (this.weather && now - this.lastWeatherFetch < DEFAULTS.weatherRefreshMs)
|
|
85
|
+
return;
|
|
86
|
+
const w = await fetchWeather(this.coords, DEFAULTS.fetchTimeoutMs);
|
|
87
|
+
if (w) {
|
|
88
|
+
this.weather = w;
|
|
89
|
+
this.lastWeatherFetch = now;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async resolveCoords() {
|
|
93
|
+
if (this.explicitCoords) {
|
|
94
|
+
this.coords = this.explicitCoords;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// Only attempt geocoding/geo-IP once per (failed) resolution to avoid
|
|
98
|
+
// hammering the keyless endpoints every poll.
|
|
99
|
+
if (this.geoAttempted)
|
|
100
|
+
return;
|
|
101
|
+
this.geoAttempted = true;
|
|
102
|
+
if (this.location) {
|
|
103
|
+
this.coords = await geocodeLocation(this.location, DEFAULTS.fetchTimeoutMs);
|
|
104
|
+
if (this.coords)
|
|
105
|
+
return;
|
|
106
|
+
this.logger.warn(`Could not geocode "${this.location}", falling back to IP geo`);
|
|
107
|
+
}
|
|
108
|
+
this.coords = await autoGeo(DEFAULTS.fetchTimeoutMs);
|
|
109
|
+
if (!this.coords) {
|
|
110
|
+
this.logger.warn('Weather geo unavailable — running in time-only mode');
|
|
111
|
+
// Allow a later retry.
|
|
112
|
+
this.geoAttempted = false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyless weather + geolocation via open-meteo, with ipapi.co IP fallback.
|
|
3
|
+
* All network helpers return null on failure so the receptionist can degrade
|
|
4
|
+
* to a time-only mode without erroring.
|
|
5
|
+
*/
|
|
6
|
+
export interface Coords {
|
|
7
|
+
lat: number;
|
|
8
|
+
lon: number;
|
|
9
|
+
label?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface Weather {
|
|
12
|
+
tempC: number;
|
|
13
|
+
code: number;
|
|
14
|
+
text: string;
|
|
15
|
+
icon: string;
|
|
16
|
+
windKph: number;
|
|
17
|
+
}
|
|
18
|
+
/** Map a WMO weather code to display text + a renderer glyph keyword. */
|
|
19
|
+
export declare function describeWmo(code: number): {
|
|
20
|
+
text: string;
|
|
21
|
+
icon: string;
|
|
22
|
+
};
|
|
23
|
+
/** Resolve a place name to coordinates via the open-meteo geocoding API. */
|
|
24
|
+
export declare function geocodeLocation(name: string, timeoutMs?: number): Promise<Coords | null>;
|
|
25
|
+
/** Best-effort IP geolocation via ipapi.co (keyless). */
|
|
26
|
+
export declare function autoGeo(timeoutMs?: number): Promise<Coords | null>;
|
|
27
|
+
/** Fetch current temperature, weather code, and wind for coordinates. */
|
|
28
|
+
export declare function fetchWeather(coords: Coords, timeoutMs?: number): Promise<Weather | null>;
|
|
29
|
+
/** Convert Celsius to Fahrenheit, rounded. */
|
|
30
|
+
export declare function toFahrenheit(c: number): number;
|
|
31
|
+
/** Format a Date into day/date/time parts for the welcome sign. */
|
|
32
|
+
export declare function formatClock(d: Date): {
|
|
33
|
+
day: string;
|
|
34
|
+
date: string;
|
|
35
|
+
time: string;
|
|
36
|
+
};
|
|
37
|
+
//# sourceMappingURL=weather.d.ts.map
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyless weather + geolocation via open-meteo, with ipapi.co IP fallback.
|
|
3
|
+
* All network helpers return null on failure so the receptionist can degrade
|
|
4
|
+
* to a time-only mode without erroring.
|
|
5
|
+
*/
|
|
6
|
+
// WMO weather-interpretation codes → human text + a glyph keyword the
|
|
7
|
+
// welcome-sign renderer understands (matched by /sun|clear|partly|rain|snow|storm|fog/).
|
|
8
|
+
const WMO = {
|
|
9
|
+
0: { text: 'Clear', icon: 'clear' },
|
|
10
|
+
1: { text: 'Mainly clear', icon: 'partly' },
|
|
11
|
+
2: { text: 'Partly cloudy', icon: 'partly' },
|
|
12
|
+
3: { text: 'Overcast', icon: 'cloud' },
|
|
13
|
+
45: { text: 'Fog', icon: 'fog' },
|
|
14
|
+
48: { text: 'Rime fog', icon: 'fog' },
|
|
15
|
+
51: { text: 'Light drizzle', icon: 'drizzle' },
|
|
16
|
+
53: { text: 'Drizzle', icon: 'drizzle' },
|
|
17
|
+
55: { text: 'Heavy drizzle', icon: 'drizzle' },
|
|
18
|
+
56: { text: 'Freezing drizzle', icon: 'drizzle' },
|
|
19
|
+
57: { text: 'Freezing drizzle', icon: 'drizzle' },
|
|
20
|
+
61: { text: 'Light rain', icon: 'rain' },
|
|
21
|
+
63: { text: 'Rain', icon: 'rain' },
|
|
22
|
+
65: { text: 'Heavy rain', icon: 'rain' },
|
|
23
|
+
66: { text: 'Freezing rain', icon: 'rain' },
|
|
24
|
+
67: { text: 'Freezing rain', icon: 'rain' },
|
|
25
|
+
71: { text: 'Light snow', icon: 'snow' },
|
|
26
|
+
73: { text: 'Snow', icon: 'snow' },
|
|
27
|
+
75: { text: 'Heavy snow', icon: 'snow' },
|
|
28
|
+
77: { text: 'Snow grains', icon: 'snow' },
|
|
29
|
+
80: { text: 'Rain showers', icon: 'shower' },
|
|
30
|
+
81: { text: 'Rain showers', icon: 'shower' },
|
|
31
|
+
82: { text: 'Violent showers', icon: 'shower' },
|
|
32
|
+
85: { text: 'Snow showers', icon: 'snow' },
|
|
33
|
+
86: { text: 'Snow showers', icon: 'snow' },
|
|
34
|
+
95: { text: 'Thunderstorm', icon: 'storm' },
|
|
35
|
+
96: { text: 'Thunderstorm', icon: 'storm' },
|
|
36
|
+
99: { text: 'Thunderstorm', icon: 'storm' },
|
|
37
|
+
};
|
|
38
|
+
/** Map a WMO weather code to display text + a renderer glyph keyword. */
|
|
39
|
+
export function describeWmo(code) {
|
|
40
|
+
return WMO[code] ?? { text: 'Unknown', icon: 'cloud' };
|
|
41
|
+
}
|
|
42
|
+
async function fetchJson(url, timeoutMs) {
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetch(url, {
|
|
47
|
+
signal: controller.signal,
|
|
48
|
+
headers: { 'User-Agent': 'nocville-npc' },
|
|
49
|
+
});
|
|
50
|
+
if (!res.ok)
|
|
51
|
+
return null;
|
|
52
|
+
return (await res.json());
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Resolve a place name to coordinates via the open-meteo geocoding API. */
|
|
62
|
+
export async function geocodeLocation(name, timeoutMs = 5000) {
|
|
63
|
+
const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(name)}&count=1`;
|
|
64
|
+
const res = await fetchJson(url, timeoutMs);
|
|
65
|
+
const first = res?.['results']?.[0];
|
|
66
|
+
if (!first || typeof first['latitude'] !== 'number')
|
|
67
|
+
return null;
|
|
68
|
+
return {
|
|
69
|
+
lat: first['latitude'],
|
|
70
|
+
lon: first['longitude'],
|
|
71
|
+
label: first['name'] ?? name,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** Best-effort IP geolocation via ipapi.co (keyless). */
|
|
75
|
+
export async function autoGeo(timeoutMs = 5000) {
|
|
76
|
+
const res = await fetchJson('https://ipapi.co/json/', timeoutMs);
|
|
77
|
+
if (!res || typeof res['latitude'] !== 'number')
|
|
78
|
+
return null;
|
|
79
|
+
return {
|
|
80
|
+
lat: res['latitude'],
|
|
81
|
+
lon: res['longitude'],
|
|
82
|
+
label: res['city'],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** Fetch current temperature, weather code, and wind for coordinates. */
|
|
86
|
+
export async function fetchWeather(coords, timeoutMs = 5000) {
|
|
87
|
+
const url = `https://api.open-meteo.com/v1/forecast?latitude=${coords.lat}` +
|
|
88
|
+
`&longitude=${coords.lon}¤t=temperature_2m,weather_code,wind_speed_10m`;
|
|
89
|
+
const res = await fetchJson(url, timeoutMs);
|
|
90
|
+
const cur = res?.['current'];
|
|
91
|
+
if (!cur || typeof cur['temperature_2m'] !== 'number')
|
|
92
|
+
return null;
|
|
93
|
+
const code = Number(cur['weather_code'] ?? 0);
|
|
94
|
+
const { text, icon } = describeWmo(code);
|
|
95
|
+
return {
|
|
96
|
+
tempC: Math.round(cur['temperature_2m']),
|
|
97
|
+
code,
|
|
98
|
+
text,
|
|
99
|
+
icon,
|
|
100
|
+
windKph: Math.round(Number(cur['wind_speed_10m'] ?? 0)),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** Convert Celsius to Fahrenheit, rounded. */
|
|
104
|
+
export function toFahrenheit(c) {
|
|
105
|
+
return Math.round((c * 9) / 5 + 32);
|
|
106
|
+
}
|
|
107
|
+
const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
108
|
+
/** Format a Date into day/date/time parts for the welcome sign. */
|
|
109
|
+
export function formatClock(d) {
|
|
110
|
+
const day = DAYS[d.getDay()];
|
|
111
|
+
let h = d.getHours();
|
|
112
|
+
const m = d.getMinutes();
|
|
113
|
+
const ampm = h >= 12 ? 'PM' : 'AM';
|
|
114
|
+
h = h % 12;
|
|
115
|
+
if (h === 0)
|
|
116
|
+
h = 12;
|
|
117
|
+
const time = `${h}:${String(m).padStart(2, '0')} ${ampm}`;
|
|
118
|
+
const date = `${d.getMonth() + 1}/${d.getDate()}`;
|
|
119
|
+
return { day, date, time };
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=weather.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface CpuMetrics {
|
|
2
|
+
percent: number;
|
|
3
|
+
loadAvg1m: number;
|
|
4
|
+
}
|
|
5
|
+
export interface MemMetrics {
|
|
6
|
+
percent: number;
|
|
7
|
+
usedGb: number;
|
|
8
|
+
totalGb: number;
|
|
9
|
+
}
|
|
10
|
+
export interface DiskMetrics {
|
|
11
|
+
percent: number;
|
|
12
|
+
usedGb: number;
|
|
13
|
+
totalGb: number;
|
|
14
|
+
}
|
|
15
|
+
export interface CpuSampler {
|
|
16
|
+
/** Return CPU usage since the previous sample (or since boot on the first call). */
|
|
17
|
+
sample(): CpuMetrics;
|
|
18
|
+
}
|
|
19
|
+
interface CpuTimes {
|
|
20
|
+
user: number;
|
|
21
|
+
nice: number;
|
|
22
|
+
sys: number;
|
|
23
|
+
idle: number;
|
|
24
|
+
irq: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Stateful CPU sampler that computes usage from the delta between consecutive
|
|
28
|
+
* `os.cpus()` snapshots — no blocking sleep. The first sample reports usage
|
|
29
|
+
* since system boot; subsequent samples report usage since the previous call.
|
|
30
|
+
*
|
|
31
|
+
* `readCpus`/`readLoad` are injectable for testing.
|
|
32
|
+
*/
|
|
33
|
+
export declare function createCpuSampler(readCpus?: () => Array<{
|
|
34
|
+
times: CpuTimes;
|
|
35
|
+
}>, readLoad?: () => number): CpuSampler;
|
|
36
|
+
/**
|
|
37
|
+
* Collect memory usage from OS.
|
|
38
|
+
*/
|
|
39
|
+
export declare function collectMemory(): MemMetrics;
|
|
40
|
+
/**
|
|
41
|
+
* Collect disk usage via fs.statfs (Node 18.15+).
|
|
42
|
+
*/
|
|
43
|
+
export declare function collectDisk(path: string): Promise<DiskMetrics>;
|
|
44
|
+
export {};
|
|
45
|
+
//# sourceMappingURL=collectors.d.ts.map
|