@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
|
@@ -2,14 +2,23 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.WebPageWatcherNPC = void 0;
|
|
4
4
|
const polling_npc_js_1 = require("../../core/polling-npc.js");
|
|
5
|
+
const thresholds_js_1 = require("../../core/thresholds.js");
|
|
6
|
+
const screen_js_1 = require("../../core/screen.js");
|
|
7
|
+
const errors_js_1 = require("../../core/errors.js");
|
|
8
|
+
const http_js_1 = require("../../core/http.js");
|
|
5
9
|
const defaults_js_1 = require("./defaults.js");
|
|
6
10
|
const pixelate_js_1 = require("./pixelate.js");
|
|
7
11
|
const capture_js_1 = require("./capture.js");
|
|
8
12
|
const basic_mode_js_1 = require("./basic-mode.js");
|
|
13
|
+
/** Thrown when a capture exceeds its deadline (the browser is then recycled). */
|
|
14
|
+
class CaptureTimeout extends Error {
|
|
15
|
+
}
|
|
9
16
|
class WebPageWatcherNPC extends polling_npc_js_1.PollingNPC {
|
|
10
17
|
url;
|
|
18
|
+
host;
|
|
11
19
|
mode;
|
|
12
20
|
chromePath;
|
|
21
|
+
chromeSandbox;
|
|
13
22
|
viewportW;
|
|
14
23
|
viewportH;
|
|
15
24
|
screenW;
|
|
@@ -17,7 +26,10 @@ class WebPageWatcherNPC extends polling_npc_js_1.PollingNPC {
|
|
|
17
26
|
waitUntil;
|
|
18
27
|
puppeteer = null;
|
|
19
28
|
browser = null;
|
|
20
|
-
|
|
29
|
+
/** puppeteer-core or Chrome is missing: never retried. */
|
|
30
|
+
browserMissing = false;
|
|
31
|
+
/** Chrome failed to launch: retry after this time (epoch ms). */
|
|
32
|
+
nextLaunchAttempt = 0;
|
|
21
33
|
chromeHintShown = false;
|
|
22
34
|
lastHash = 0;
|
|
23
35
|
changedPollsLeft = 0;
|
|
@@ -27,22 +39,41 @@ class WebPageWatcherNPC extends polling_npc_js_1.PollingNPC {
|
|
|
27
39
|
// Floor the poll interval — screenshots are expensive.
|
|
28
40
|
pollInterval: Math.max(config.pollInterval ?? defaults_js_1.DEFAULTS.pollInterval, defaults_js_1.DEFAULTS.minPollInterval),
|
|
29
41
|
});
|
|
42
|
+
// The page is rendered and shown to the whole office: http(s) only, never
|
|
43
|
+
// file:, data:, chrome: or javascript: URLs.
|
|
44
|
+
if (!(0, http_js_1.isHttpUrl)(config.url)) {
|
|
45
|
+
throw new errors_js_1.NpcConfigError('--url must be an http(s) URL');
|
|
46
|
+
}
|
|
30
47
|
this.url = config.url;
|
|
31
|
-
this.
|
|
48
|
+
this.host = (0, basic_mode_js_1.hostnameOf)(config.url);
|
|
49
|
+
const mode = config.mode ?? 'auto';
|
|
50
|
+
if (!defaults_js_1.WATCH_MODES.includes(mode)) {
|
|
51
|
+
throw new errors_js_1.NpcConfigError(`--mode must be one of ${defaults_js_1.WATCH_MODES.join(', ')}`);
|
|
52
|
+
}
|
|
53
|
+
this.mode = mode;
|
|
54
|
+
const waitUntil = config.waitUntil ?? defaults_js_1.DEFAULTS.waitUntil;
|
|
55
|
+
if (!defaults_js_1.WAIT_UNTIL.includes(waitUntil)) {
|
|
56
|
+
throw new errors_js_1.NpcConfigError(`--wait-until must be one of ${defaults_js_1.WAIT_UNTIL.join(', ')}`);
|
|
57
|
+
}
|
|
58
|
+
this.waitUntil = waitUntil;
|
|
32
59
|
this.chromePath = config.chromePath;
|
|
60
|
+
this.chromeSandbox = config.chromeSandbox ?? true;
|
|
33
61
|
this.screenW = config.screenWidth ?? defaults_js_1.DEFAULTS.screenW;
|
|
34
62
|
this.screenH = config.screenHeight ?? defaults_js_1.DEFAULTS.screenH;
|
|
35
63
|
// Derive the viewport from the screen size for native downscaling unless the
|
|
36
64
|
// user overrides it (in which case pngToPixelImage box-filters to screen).
|
|
37
65
|
this.viewportW = config.viewportWidth ?? this.screenW * defaults_js_1.DEFAULTS.scaleDivisor;
|
|
38
66
|
this.viewportH = config.viewportHeight ?? this.screenH * defaults_js_1.DEFAULTS.scaleDivisor;
|
|
39
|
-
|
|
67
|
+
const sizeProblem = (0, defaults_js_1.screenSizeProblem)(this.screenW, this.screenH) ??
|
|
68
|
+
(0, defaults_js_1.viewportProblem)(this.viewportW, this.viewportH);
|
|
69
|
+
if (sizeProblem)
|
|
70
|
+
throw new errors_js_1.NpcConfigError(sizeProblem);
|
|
40
71
|
}
|
|
41
72
|
get name() {
|
|
42
73
|
return 'web-page-watcher';
|
|
43
74
|
}
|
|
44
75
|
get defaultDisplayName() {
|
|
45
|
-
return
|
|
76
|
+
return this.host;
|
|
46
77
|
}
|
|
47
78
|
get archetype() {
|
|
48
79
|
return 'custom';
|
|
@@ -52,43 +83,58 @@ class WebPageWatcherNPC extends polling_npc_js_1.PollingNPC {
|
|
|
52
83
|
screenType: 'web-view',
|
|
53
84
|
size: { w: 4, h: 3 },
|
|
54
85
|
placement: 'adjacent-wall',
|
|
55
|
-
label:
|
|
86
|
+
label: this.host,
|
|
56
87
|
};
|
|
57
88
|
}
|
|
58
89
|
async collect() {
|
|
59
90
|
if (this.mode === 'basic')
|
|
60
|
-
return this.collectBasic();
|
|
91
|
+
return this.collectBasic(false);
|
|
61
92
|
const browser = await this.ensureBrowser();
|
|
62
93
|
if (!browser) {
|
|
63
|
-
// 'auto' silently degrades; explicit 'screenshot' warns once.
|
|
64
94
|
this.maybeShowChromeHint();
|
|
65
|
-
return this.collectBasic();
|
|
95
|
+
return this.collectBasic(true);
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
return await this.withDeadline(this.collectScreenshot(browser));
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
if (this.state !== 'running')
|
|
102
|
+
throw err; // shutting down: nothing to report
|
|
103
|
+
// A crashed or wedged browser: recycle it and report this cycle from a plain fetch.
|
|
104
|
+
this.logger.warn('Screenshot failed; using a basic check this cycle', {
|
|
105
|
+
error: err instanceof Error ? err.message : String(err),
|
|
106
|
+
});
|
|
107
|
+
await this.closeBrowser();
|
|
108
|
+
return this.collectBasic(true);
|
|
66
109
|
}
|
|
67
|
-
return this.collectScreenshot(browser);
|
|
68
110
|
}
|
|
69
111
|
async onStop() {
|
|
112
|
+
// Close Chrome first: it aborts an in-flight capture, so the base class
|
|
113
|
+
// doesn't wait out a slow page load before pushing offline.
|
|
114
|
+
await this.closeBrowser();
|
|
70
115
|
await super.onStop();
|
|
71
|
-
if (this.browser) {
|
|
72
|
-
try {
|
|
73
|
-
await this.browser.close();
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
// best effort
|
|
77
|
-
}
|
|
78
|
-
this.browser = null;
|
|
79
|
-
}
|
|
80
116
|
}
|
|
81
117
|
// --- Screenshot mode ---
|
|
118
|
+
withDeadline(work) {
|
|
119
|
+
const ms = defaults_js_1.DEFAULTS.navTimeoutMs + defaults_js_1.DEFAULTS.captureGraceMs;
|
|
120
|
+
let timer;
|
|
121
|
+
const deadline = new Promise((_, reject) => {
|
|
122
|
+
timer = setTimeout(() => reject(new CaptureTimeout(`capture exceeded ${ms}ms`)), ms);
|
|
123
|
+
});
|
|
124
|
+
return Promise.race([work, deadline]).finally(() => clearTimeout(timer));
|
|
125
|
+
}
|
|
82
126
|
async collectScreenshot(browser) {
|
|
83
127
|
const page = await browser.newPage();
|
|
84
128
|
try {
|
|
85
|
-
//
|
|
86
|
-
|
|
129
|
+
// A page that alert()s would block navigation and the screenshot.
|
|
130
|
+
page.on?.('dialog', (dialog) => void dialog.dismiss().catch(() => undefined));
|
|
131
|
+
// deviceScaleFactor 1/8 makes Chrome rasterize the viewport down to the
|
|
132
|
+
// screen size natively (e.g. 1280×960 → 160×120); pngToPixelImage
|
|
87
133
|
// box-filters as a fallback if it doesn't.
|
|
88
134
|
await page.setViewport({
|
|
89
135
|
width: this.viewportW,
|
|
90
136
|
height: this.viewportH,
|
|
91
|
-
deviceScaleFactor:
|
|
137
|
+
deviceScaleFactor: 1 / defaults_js_1.DEFAULTS.scaleDivisor,
|
|
92
138
|
});
|
|
93
139
|
let httpStatus = 0;
|
|
94
140
|
let timedOut = false;
|
|
@@ -116,15 +162,14 @@ class WebPageWatcherNPC extends polling_npc_js_1.PollingNPC {
|
|
|
116
162
|
else if (this.changedPollsLeft > 0)
|
|
117
163
|
this.changedPollsLeft--;
|
|
118
164
|
const status = (0, basic_mode_js_1.statusFromHttp)(httpStatus, timedOut);
|
|
119
|
-
const host = (0, basic_mode_js_1.hostnameOf)(this.url);
|
|
120
165
|
const message = changed
|
|
121
166
|
? 'Page updated!'
|
|
122
167
|
: httpStatus
|
|
123
|
-
? `${host} ${httpStatus < 400 ? 'OK' : 'ERR'} (${httpStatus}, ${loadMs}ms)`
|
|
124
|
-
: `${host} unreachable`;
|
|
168
|
+
? `${this.host} ${httpStatus < 400 ? 'OK' : 'ERR'} (${httpStatus}, ${loadMs}ms)`
|
|
169
|
+
: `${this.host} ${timedOut ? 'timed out' : 'unreachable'}`;
|
|
125
170
|
return {
|
|
126
171
|
status,
|
|
127
|
-
mood:
|
|
172
|
+
mood: (0, thresholds_js_1.moodForStatus)(status),
|
|
128
173
|
message,
|
|
129
174
|
subStatus: this.changedPollsLeft > 0 ? 'changed' : undefined,
|
|
130
175
|
screenData,
|
|
@@ -141,74 +186,92 @@ class WebPageWatcherNPC extends polling_npc_js_1.PollingNPC {
|
|
|
141
186
|
}
|
|
142
187
|
}
|
|
143
188
|
async ensureBrowser() {
|
|
144
|
-
if (this.
|
|
189
|
+
if (this.browserMissing || Date.now() < this.nextLaunchAttempt)
|
|
145
190
|
return null;
|
|
146
191
|
if (this.browser && (0, capture_js_1.browserConnected)(this.browser))
|
|
147
192
|
return this.browser;
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (!this.puppeteer) {
|
|
151
|
-
this.puppeteer = await (0, capture_js_1.loadPuppeteer)();
|
|
152
|
-
}
|
|
193
|
+
this.browser = null; // absent or disconnected — (re)launch
|
|
194
|
+
this.puppeteer ??= await (0, capture_js_1.loadPuppeteer)();
|
|
153
195
|
const chromePath = (0, capture_js_1.findChrome)(this.chromePath);
|
|
154
196
|
if (!this.puppeteer || !chromePath) {
|
|
155
|
-
this.
|
|
197
|
+
this.browserMissing = true;
|
|
156
198
|
return null;
|
|
157
199
|
}
|
|
200
|
+
const { args, sandboxed } = (0, capture_js_1.chromeArgs)({
|
|
201
|
+
sandbox: this.chromeSandbox,
|
|
202
|
+
isRoot: (0, capture_js_1.runningAsRoot)(),
|
|
203
|
+
});
|
|
204
|
+
if (!sandboxed) {
|
|
205
|
+
this.logger.warn('Chrome sandbox disabled' +
|
|
206
|
+
(this.chromeSandbox ? ' (running as root)' : '') +
|
|
207
|
+
': only watch pages you trust');
|
|
208
|
+
}
|
|
158
209
|
try {
|
|
159
|
-
|
|
210
|
+
const browser = await this.puppeteer.launch({
|
|
160
211
|
executablePath: chromePath,
|
|
161
212
|
headless: true,
|
|
162
|
-
args
|
|
213
|
+
args,
|
|
163
214
|
});
|
|
164
|
-
|
|
165
|
-
this.browser
|
|
215
|
+
browser.on('disconnected', () => {
|
|
216
|
+
if (this.browser === browser)
|
|
217
|
+
this.browser = null;
|
|
166
218
|
});
|
|
167
|
-
|
|
219
|
+
// Stopped while Chrome was starting: don't leave it running.
|
|
220
|
+
if (this.state !== 'running') {
|
|
221
|
+
await browser.close().catch(() => undefined);
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
this.browser = browser;
|
|
225
|
+
return browser;
|
|
168
226
|
}
|
|
169
227
|
catch (err) {
|
|
170
|
-
this.logger.warn('Failed to launch Chrome', {
|
|
228
|
+
this.logger.warn('Failed to launch Chrome; retrying in 10 minutes', {
|
|
171
229
|
error: err instanceof Error ? err.message : String(err),
|
|
172
230
|
});
|
|
173
|
-
this.
|
|
231
|
+
this.nextLaunchAttempt = Date.now() + defaults_js_1.DEFAULTS.relaunchBackoffMs;
|
|
174
232
|
return null;
|
|
175
233
|
}
|
|
176
234
|
}
|
|
235
|
+
async closeBrowser() {
|
|
236
|
+
const browser = this.browser;
|
|
237
|
+
this.browser = null;
|
|
238
|
+
if (browser)
|
|
239
|
+
await browser.close().catch(() => undefined);
|
|
240
|
+
}
|
|
177
241
|
maybeShowChromeHint() {
|
|
178
|
-
if (this.chromeHintShown)
|
|
242
|
+
if (this.chromeHintShown || !this.browserMissing)
|
|
179
243
|
return;
|
|
180
244
|
this.chromeHintShown = true;
|
|
181
245
|
this.logger.warn('Chrome/puppeteer-core unavailable — falling back to basic mode. ' +
|
|
182
246
|
'Install `puppeteer-core` and Google Chrome (or set --chrome-path/CHROME_PATH) for the pixel-art TV.');
|
|
183
247
|
}
|
|
184
248
|
// --- Basic mode ---
|
|
185
|
-
|
|
249
|
+
/**
|
|
250
|
+
* Plain fetch: HTTP status + page title on a ticker. When this is a fallback
|
|
251
|
+
* from screenshot mode (no Chrome), a healthy page is reported as a warning
|
|
252
|
+
* to nudge the Chrome setup; an explicit `--mode=basic` reports it healthy.
|
|
253
|
+
*/
|
|
254
|
+
async collectBasic(fallback) {
|
|
186
255
|
const loadStart = Date.now();
|
|
187
|
-
const check = await (0, basic_mode_js_1.basicCheck)(this.url);
|
|
256
|
+
const check = await (0, basic_mode_js_1.basicCheck)(this.url, defaults_js_1.DEFAULTS.navTimeoutMs);
|
|
188
257
|
const loadMs = Date.now() - loadStart;
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
status
|
|
194
|
-
const host = (0, basic_mode_js_1.hostnameOf)(this.url);
|
|
195
|
-
const message = check.title ? `${host}: ${check.title}` : `${host} (${check.status || 'ERR'})`;
|
|
258
|
+
const httpStatus = (0, basic_mode_js_1.statusFromHttp)(check.status, check.timedOut);
|
|
259
|
+
const status = fallback && httpStatus === 'healthy' ? 'warning' : httpStatus;
|
|
260
|
+
const message = check.title
|
|
261
|
+
? `${this.host}: ${check.title}`
|
|
262
|
+
: `${this.host} (${check.status || (check.timedOut ? 'timed out' : 'ERR')})`;
|
|
196
263
|
return {
|
|
197
264
|
status,
|
|
198
|
-
mood:
|
|
265
|
+
mood: (0, thresholds_js_1.moodForStatus)(status),
|
|
199
266
|
message,
|
|
200
267
|
screenData: {
|
|
201
268
|
layout: 'ticker',
|
|
202
269
|
values: [
|
|
203
|
-
{ label: 'URL', value: host },
|
|
270
|
+
{ label: 'URL', value: this.host },
|
|
204
271
|
{
|
|
205
272
|
label: 'HTTP',
|
|
206
273
|
value: check.status ? String(check.status) : 'ERR',
|
|
207
|
-
color:
|
|
208
|
-
? 'green'
|
|
209
|
-
: check.status >= 500 || check.status === 0
|
|
210
|
-
? 'red'
|
|
211
|
-
: 'amber',
|
|
274
|
+
color: (0, screen_js_1.statusColor)(httpStatus),
|
|
212
275
|
},
|
|
213
276
|
{ label: 'Title', value: check.title || '(no title)' },
|
|
214
277
|
{ label: 'Mode', value: 'basic' },
|
|
@@ -219,16 +282,4 @@ class WebPageWatcherNPC extends polling_npc_js_1.PollingNPC {
|
|
|
219
282
|
}
|
|
220
283
|
}
|
|
221
284
|
exports.WebPageWatcherNPC = WebPageWatcherNPC;
|
|
222
|
-
function moodFor(status) {
|
|
223
|
-
switch (status) {
|
|
224
|
-
case 'healthy':
|
|
225
|
-
return 'happy';
|
|
226
|
-
case 'warning':
|
|
227
|
-
return 'concerned';
|
|
228
|
-
case 'critical':
|
|
229
|
-
return 'stressed';
|
|
230
|
-
default:
|
|
231
|
-
return 'idle';
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
285
|
//# sourceMappingURL=index.js.map
|
|
@@ -176,6 +176,7 @@ function pngToPixelImage(pngBuffer, maxColors = 32, targetW = exports.TARGET_W,
|
|
|
176
176
|
const { palette, indices } = quantize(rgba, w * h, maxColors);
|
|
177
177
|
const screenData = buildPixelImageScreen(w, h, palette, indices);
|
|
178
178
|
const byteSize = screenData.values.reduce((sum, v) => sum + v.value.length, 0);
|
|
179
|
-
|
|
179
|
+
// Hash the palette too: a recolour with the same layout is still a change.
|
|
180
|
+
return { screenData, byteSize, hash: fnv1a(Buffer.concat([palette, indices])) };
|
|
180
181
|
}
|
|
181
182
|
//# sourceMappingURL=pixelate.js.map
|
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
|
-
import { parseArgs, resolveConfig, stringFlag, numberFlag, boolFlag } from './core/config.js';
|
|
5
|
+
import { parseArgs, resolveConfig, stringFlag, numberFlag, boolFlag, listFlag, } from './core/config.js';
|
|
6
|
+
import { NpcConfigError } from './core/errors.js';
|
|
6
7
|
import { Logger } from './core/logger.js';
|
|
7
8
|
import { ensureSessionFilterTemplate, loadSessionFilter, sessionPublishDecision, } from './core/session-filter.js';
|
|
8
9
|
// NPC modules are imported lazily per command: `claude-code hook` / `codex hook`
|
|
@@ -46,7 +47,15 @@ Monitoring NPC types:
|
|
|
46
47
|
internet-connectivity Monitor network connectivity
|
|
47
48
|
process-watcher Watch for running processes
|
|
48
49
|
log-watcher Watch log files for errors
|
|
49
|
-
receptionist
|
|
50
|
+
receptionist Lobby greeter with office check-ins and jokes
|
|
51
|
+
security Indoor/outdoor patrols and exterior door watch
|
|
52
|
+
janitor Mops suspicious spots and hunts office bugs
|
|
53
|
+
barista Takes increasingly absurd coffee orders
|
|
54
|
+
office-cat Naps on desks and supervises keyboards
|
|
55
|
+
office-dog Naps, barks at coworkers and runs around
|
|
56
|
+
plant-caretaker Waters plants and reviews their performance
|
|
57
|
+
it-technician Inspects tech gear and recommends reboots
|
|
58
|
+
out-of-touch-executive Interrupts coworkers between long lounge breaks
|
|
50
59
|
github-watcher Call out new commits on a repo
|
|
51
60
|
web-page-watcher Render a watched web page as a pixel-art TV
|
|
52
61
|
|
|
@@ -77,17 +86,23 @@ Internet Connectivity Options:
|
|
|
77
86
|
--poll-interval=<ms> Poll interval in ms (default: 30000)
|
|
78
87
|
--dns-hosts=<list> Comma-separated DNS hosts (default: google.com,cloudflare.com)
|
|
79
88
|
--http-urls=<list> Comma-separated HTTP URLs to check
|
|
80
|
-
--latency-hosts=<list> Comma-separated
|
|
89
|
+
--latency-hosts=<list> Comma-separated TCP latency targets: host, host:port or
|
|
90
|
+
[ipv6]:port (default: 8.8.8.8:443,1.1.1.1:443;
|
|
91
|
+
port 80 when omitted)
|
|
81
92
|
--slow-threshold=<ms> Slow response threshold in ms (default: 1000)
|
|
93
|
+
--timeout=<ms> Per-check timeout in ms (default: 5000)
|
|
82
94
|
|
|
83
95
|
Process Watcher Options:
|
|
84
96
|
--poll-interval=<ms> Poll interval in ms (default: 15000)
|
|
85
|
-
--processes=<list> Comma-separated
|
|
97
|
+
--processes=<list> Comma-separated executable names to watch (python
|
|
98
|
+
also matches python3.11; a /path matches exactly)
|
|
86
99
|
--cpu-warn=<n> CPU warning threshold % (default: 80)
|
|
87
100
|
--mem-warn=<n> Memory warning threshold MB (default: 500)
|
|
88
101
|
|
|
89
102
|
Log Watcher Options:
|
|
90
|
-
--files=<list> Comma-separated log file paths
|
|
103
|
+
--files=<list> Comma-separated log file paths (followed across
|
|
104
|
+
rotation; files may appear later). Lines shown on
|
|
105
|
+
the screen have secrets redacted.
|
|
91
106
|
--error-pattern=<regex> Error pattern (default: ERROR|FATAL)
|
|
92
107
|
--warning-pattern=<regex> Warning pattern (default: WARN)
|
|
93
108
|
--window=<seconds> Sliding window in seconds (default: 300)
|
|
@@ -95,26 +110,28 @@ Log Watcher Options:
|
|
|
95
110
|
--error-crit=<n> Error count for critical status (default: 20)
|
|
96
111
|
--report-interval=<ms> Report interval in ms (default: 15000)
|
|
97
112
|
|
|
98
|
-
|
|
99
|
-
--poll-interval=<ms>
|
|
100
|
-
--lat=<n> --lon=<n> Explicit coordinates for weather
|
|
101
|
-
--location=<name> Place name to geocode (else IP geo)
|
|
102
|
-
--fahrenheit Show temperature in °F (default: °C)
|
|
113
|
+
Office Character Options:
|
|
114
|
+
--poll-interval=<ms> Heartbeat interval in ms (default: 60000)
|
|
103
115
|
|
|
104
116
|
GitHub Watcher Options:
|
|
105
117
|
--repo=<owner/name> Repository to watch (required)
|
|
106
118
|
--branch=<name> Branch to watch (default: repo default branch)
|
|
107
|
-
--token=<token> GitHub token (
|
|
108
|
-
|
|
119
|
+
--token=<token> GitHub token (prefer the GITHUB_TOKEN env: flags are
|
|
120
|
+
visible in the process list)
|
|
121
|
+
--poll-interval=<ms> Poll interval in ms (default: 60000 with a token,
|
|
122
|
+
90000 without, to stay under GitHub's 60/hour)
|
|
109
123
|
|
|
110
124
|
Web Page Watcher Options:
|
|
111
|
-
--url=<url>
|
|
125
|
+
--url=<url> http(s) page to watch (required)
|
|
112
126
|
--mode=<auto|screenshot|basic> Render mode (default: auto)
|
|
113
127
|
--interval=<ms> Poll interval in ms (default: 60000, floor 15000)
|
|
114
|
-
--viewport=<WxH> Browser viewport (default: screen x 8)
|
|
115
|
-
--screen=<WxH> TV pixel resolution (default: 160x120
|
|
116
|
-
--wait-until=<event>
|
|
128
|
+
--viewport=<WxH> Browser viewport (default: screen x 8, max 4096x4096)
|
|
129
|
+
--screen=<WxH> TV pixel resolution (default: 160x120, max 320x240)
|
|
130
|
+
--wait-until=<event> load, domcontentloaded, networkidle0 or networkidle2
|
|
131
|
+
(default: networkidle2)
|
|
117
132
|
--chrome-path=<path> Chrome executable (or CHROME_PATH env)
|
|
133
|
+
--no-chrome-sandbox Run Chrome without its sandbox (only for trusted
|
|
134
|
+
pages; automatic when running as root)
|
|
118
135
|
(needs puppeteer-core + Chrome for the pixel-art TV; falls back to basic)
|
|
119
136
|
`.trim();
|
|
120
137
|
async function main() {
|
|
@@ -275,18 +292,10 @@ async function main() {
|
|
|
275
292
|
if (!config.serviceId) {
|
|
276
293
|
config.serviceId = 'internet-connectivity';
|
|
277
294
|
}
|
|
278
|
-
const dnsHosts = stringFlag(parsed.flags, 'dns-hosts')?.split(',');
|
|
279
|
-
const httpUrls = stringFlag(parsed.flags, 'http-urls')?.split(',');
|
|
280
|
-
const latencyHostsRaw = stringFlag(parsed.flags, 'latency-hosts')?.split(',');
|
|
281
|
-
const latencyHosts = latencyHostsRaw?.map((h) => ({ host: h.trim(), port: 80 }));
|
|
282
295
|
const { InternetConnectivityNPC } = await import('./npcs/internet-connectivity/index.js');
|
|
283
296
|
const npc = new InternetConnectivityNPC({
|
|
284
297
|
...config,
|
|
285
|
-
|
|
286
|
-
dnsHosts,
|
|
287
|
-
httpUrls,
|
|
288
|
-
latencyHosts,
|
|
289
|
-
slowThreshold: numberFlag(parsed.flags, 'slow-threshold'),
|
|
298
|
+
...InternetConnectivityNPC.configFromFlags(parsed.flags),
|
|
290
299
|
});
|
|
291
300
|
logger.info('Starting internet-connectivity NPC');
|
|
292
301
|
await npc.start();
|
|
@@ -296,8 +305,8 @@ async function main() {
|
|
|
296
305
|
if (!config.serviceId) {
|
|
297
306
|
config.serviceId = 'process-watcher';
|
|
298
307
|
}
|
|
299
|
-
const
|
|
300
|
-
if (!
|
|
308
|
+
const processes = listFlag(parsed.flags, 'processes');
|
|
309
|
+
if (!processes) {
|
|
301
310
|
console.error('Error: --processes is required (comma-separated list)');
|
|
302
311
|
process.exit(1);
|
|
303
312
|
}
|
|
@@ -305,7 +314,7 @@ async function main() {
|
|
|
305
314
|
const npc = new ProcessWatcherNPC({
|
|
306
315
|
...config,
|
|
307
316
|
pollInterval: numberFlag(parsed.flags, 'poll-interval'),
|
|
308
|
-
processes
|
|
317
|
+
processes,
|
|
309
318
|
cpuWarn: numberFlag(parsed.flags, 'cpu-warn'),
|
|
310
319
|
memWarn: numberFlag(parsed.flags, 'mem-warn'),
|
|
311
320
|
});
|
|
@@ -314,8 +323,8 @@ async function main() {
|
|
|
314
323
|
break;
|
|
315
324
|
}
|
|
316
325
|
case 'log-watcher': {
|
|
317
|
-
const
|
|
318
|
-
if (!
|
|
326
|
+
const files = listFlag(parsed.flags, 'files');
|
|
327
|
+
if (!files) {
|
|
319
328
|
console.error('Error: --files is required (comma-separated list)');
|
|
320
329
|
process.exit(1);
|
|
321
330
|
}
|
|
@@ -325,7 +334,7 @@ async function main() {
|
|
|
325
334
|
const { LogWatcherNPC } = await import('./npcs/log-watcher/index.js');
|
|
326
335
|
const npc = new LogWatcherNPC({
|
|
327
336
|
...config,
|
|
328
|
-
files
|
|
337
|
+
files,
|
|
329
338
|
errorPattern: stringFlag(parsed.flags, 'error-pattern'),
|
|
330
339
|
warningPattern: stringFlag(parsed.flags, 'warning-pattern'),
|
|
331
340
|
windowSeconds: numberFlag(parsed.flags, 'window'),
|
|
@@ -349,11 +358,43 @@ async function main() {
|
|
|
349
358
|
lon: numberFlag(parsed.flags, 'lon'),
|
|
350
359
|
location: stringFlag(parsed.flags, 'location'),
|
|
351
360
|
fahrenheit: boolFlag(parsed.flags, 'fahrenheit') ?? false,
|
|
361
|
+
ipGeo: boolFlag(parsed.flags, 'no-ip-geo') !== true,
|
|
352
362
|
});
|
|
353
363
|
logger.info('Starting receptionist NPC');
|
|
354
364
|
await npc.start();
|
|
355
365
|
break;
|
|
356
366
|
}
|
|
367
|
+
case 'security': {
|
|
368
|
+
if (!config.serviceId)
|
|
369
|
+
config.serviceId = 'security';
|
|
370
|
+
const { SecurityNPC } = await import('./npcs/security/index.js');
|
|
371
|
+
const npc = new SecurityNPC({
|
|
372
|
+
...config,
|
|
373
|
+
pollInterval: numberFlag(parsed.flags, 'poll-interval'),
|
|
374
|
+
});
|
|
375
|
+
logger.info('Starting security NPC');
|
|
376
|
+
await npc.start();
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
case 'janitor':
|
|
380
|
+
case 'barista':
|
|
381
|
+
case 'office-cat':
|
|
382
|
+
case 'office-dog':
|
|
383
|
+
case 'plant-caretaker':
|
|
384
|
+
case 'it-technician':
|
|
385
|
+
case 'out-of-touch-executive': {
|
|
386
|
+
if (!config.serviceId)
|
|
387
|
+
config.serviceId = parsed.command;
|
|
388
|
+
const { OFFICE_CHARACTER_NPCS } = await import('./npcs/office-characters/index.js');
|
|
389
|
+
const NPCClass = OFFICE_CHARACTER_NPCS[parsed.command];
|
|
390
|
+
const npc = new NPCClass({
|
|
391
|
+
...config,
|
|
392
|
+
pollInterval: numberFlag(parsed.flags, 'poll-interval'),
|
|
393
|
+
});
|
|
394
|
+
logger.info(`Starting ${parsed.command} NPC`);
|
|
395
|
+
await npc.start();
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
357
398
|
case 'github-watcher': {
|
|
358
399
|
const repo = stringFlag(parsed.flags, 'repo');
|
|
359
400
|
if (!repo) {
|
|
@@ -365,11 +406,15 @@ async function main() {
|
|
|
365
406
|
config.serviceId = `github-${shortName.replace(/[^a-zA-Z0-9_-]/g, '-')}`.slice(0, 100);
|
|
366
407
|
}
|
|
367
408
|
const { GitHubWatcherNPC } = await import('./npcs/github-watcher/index.js');
|
|
409
|
+
const tokenFlag = stringFlag(parsed.flags, 'token');
|
|
410
|
+
if (tokenFlag) {
|
|
411
|
+
logger.warn('--token is visible to other local users in the process list; prefer GITHUB_TOKEN');
|
|
412
|
+
}
|
|
368
413
|
const npc = new GitHubWatcherNPC({
|
|
369
414
|
...config,
|
|
370
415
|
repo,
|
|
371
416
|
branch: stringFlag(parsed.flags, 'branch'),
|
|
372
|
-
token:
|
|
417
|
+
token: tokenFlag ?? process.env['GITHUB_TOKEN'],
|
|
373
418
|
pollInterval: numberFlag(parsed.flags, 'poll-interval'),
|
|
374
419
|
});
|
|
375
420
|
logger.info('Starting github-watcher NPC');
|
|
@@ -385,22 +430,28 @@ async function main() {
|
|
|
385
430
|
const { hostnameOf } = await import('./npcs/web-page-watcher/basic-mode.js');
|
|
386
431
|
const { parseDimensions } = await import('./npcs/web-page-watcher/defaults.js');
|
|
387
432
|
const { WebPageWatcherNPC } = await import('./npcs/web-page-watcher/index.js');
|
|
388
|
-
const parseWxH = (
|
|
433
|
+
const parseWxH = (flag) => {
|
|
434
|
+
const v = stringFlag(parsed.flags, flag);
|
|
435
|
+
if (v === undefined)
|
|
436
|
+
return undefined;
|
|
437
|
+
const dims = parseDimensions(v);
|
|
438
|
+
if (!dims)
|
|
439
|
+
throw new NpcConfigError(`--${flag} must look like 160x120`);
|
|
440
|
+
return dims;
|
|
441
|
+
};
|
|
389
442
|
if (!config.serviceId) {
|
|
390
443
|
config.serviceId = `webpage-${hostnameOf(url)}`
|
|
391
444
|
.replace(/[^a-zA-Z0-9_.-]/g, '-')
|
|
392
445
|
.slice(0, 100);
|
|
393
446
|
}
|
|
394
|
-
const
|
|
395
|
-
const
|
|
396
|
-
? modeFlag
|
|
397
|
-
: undefined;
|
|
398
|
-
const viewport = parseWxH(stringFlag(parsed.flags, 'viewport'));
|
|
399
|
-
const screen = parseWxH(stringFlag(parsed.flags, 'screen'));
|
|
447
|
+
const viewport = parseWxH('viewport');
|
|
448
|
+
const screen = parseWxH('screen');
|
|
400
449
|
const npc = new WebPageWatcherNPC({
|
|
401
450
|
...config,
|
|
402
451
|
url,
|
|
403
|
-
|
|
452
|
+
// Validated (with a clear error) by the constructor.
|
|
453
|
+
mode: stringFlag(parsed.flags, 'mode'),
|
|
454
|
+
chromeSandbox: boolFlag(parsed.flags, 'no-chrome-sandbox') !== true,
|
|
404
455
|
chromePath: stringFlag(parsed.flags, 'chrome-path'),
|
|
405
456
|
waitUntil: stringFlag(parsed.flags, 'wait-until'),
|
|
406
457
|
viewportWidth: viewport?.width,
|
|
@@ -543,6 +594,10 @@ main().catch((err) => {
|
|
|
543
594
|
process.stdout.write('{}\n');
|
|
544
595
|
return;
|
|
545
596
|
}
|
|
597
|
+
if (err instanceof NpcConfigError) {
|
|
598
|
+
console.error(`Error: ${err.message}`);
|
|
599
|
+
process.exit(1);
|
|
600
|
+
}
|
|
546
601
|
console.error('Fatal error:', err);
|
|
547
602
|
process.exit(1);
|
|
548
603
|
});
|
package/dist/core/base-npc.d.ts
CHANGED
|
@@ -20,6 +20,11 @@ export declare abstract class BaseNPC {
|
|
|
20
20
|
protected get defaultDisplayName(): string | null;
|
|
21
21
|
/** The banner name to send, clamped to the API's 32-char limit, or undefined. */
|
|
22
22
|
private pushName;
|
|
23
|
+
/**
|
|
24
|
+
* NPC type sent as the push `kind` (groups NPCs for per-type rules such as "all process
|
|
25
|
+
* watchers work in the Server Room"). Defaults to `name` when it is a valid slug.
|
|
26
|
+
*/
|
|
27
|
+
get kind(): string | undefined;
|
|
23
28
|
/** NPC archetype — steers movement speed and default behavior. Override per NPC. */
|
|
24
29
|
get archetype(): Archetype;
|
|
25
30
|
/** Base behavior hints sent on every push (merged with per-cycle overrides). */
|
package/dist/core/base-npc.js
CHANGED
|
@@ -39,6 +39,13 @@ export class BaseNPC {
|
|
|
39
39
|
const name = this.displayName;
|
|
40
40
|
return name ? truncate(name, 32) : undefined;
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* NPC type sent as the push `kind` (groups NPCs for per-type rules such as "all process
|
|
44
|
+
* watchers work in the Server Room"). Defaults to `name` when it is a valid slug.
|
|
45
|
+
*/
|
|
46
|
+
get kind() {
|
|
47
|
+
return /^[a-z0-9][a-z0-9-]{0,39}$/.test(this.name) ? this.name : undefined;
|
|
48
|
+
}
|
|
42
49
|
/** NPC archetype — steers movement speed and default behavior. Override per NPC. */
|
|
43
50
|
get archetype() {
|
|
44
51
|
return 'service';
|
|
@@ -113,6 +120,7 @@ export class BaseNPC {
|
|
|
113
120
|
message: metrics.message ? truncate(metrics.message, 500) : undefined,
|
|
114
121
|
metrics: metrics.metrics,
|
|
115
122
|
archetype: this.archetype,
|
|
123
|
+
kind: this.kind,
|
|
116
124
|
subStatus: metrics.subStatus,
|
|
117
125
|
screenData: metrics.screenData,
|
|
118
126
|
behavior: Object.keys(behavior).length > 0 ? behavior : undefined,
|
|
@@ -120,6 +128,13 @@ export class BaseNPC {
|
|
|
120
128
|
alertConfig: { heartbeatInterval: this.heartbeatSeconds },
|
|
121
129
|
map: this.config.map,
|
|
122
130
|
});
|
|
131
|
+
// An observer cannot interrupt monitoring or retries.
|
|
132
|
+
try {
|
|
133
|
+
this.config.onPushResult?.({ success: result.success, error: result.error });
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
/* observer only */
|
|
137
|
+
}
|
|
123
138
|
if (!result.success) {
|
|
124
139
|
this.reportPushFailure(result.error);
|
|
125
140
|
}
|
package/dist/core/config.d.ts
CHANGED
|
@@ -18,5 +18,10 @@ export declare function parseArgs(argv: string[]): ParsedArgs;
|
|
|
18
18
|
export declare function resolveConfig(flags: Record<string, string | boolean>, overrides?: Partial<NpcConfig>): NpcConfig;
|
|
19
19
|
export declare function stringFlag(flags: Record<string, string | boolean>, key: string): string | undefined;
|
|
20
20
|
export declare function boolFlag(flags: Record<string, string | boolean>, key: string): boolean | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* A comma-separated list flag (`--processes=nginx,redis`): entries trimmed,
|
|
23
|
+
* empties dropped, duplicates removed. Undefined when the flag is absent or empty.
|
|
24
|
+
*/
|
|
25
|
+
export declare function listFlag(flags: Record<string, string | boolean>, key: string): string[] | undefined;
|
|
21
26
|
export declare function numberFlag(flags: Record<string, string | boolean>, key: string): number | undefined;
|
|
22
27
|
//# sourceMappingURL=config.d.ts.map
|