@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
package/README.md
CHANGED
|
@@ -140,26 +140,64 @@ on your map automatically and despawns when you press Ctrl-C. Once you've run
|
|
|
140
140
|
# --debug Verbose logging
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
-
| NPC
|
|
144
|
-
|
|
|
145
|
-
| `system-monitor`
|
|
146
|
-
| `internet-connectivity`
|
|
147
|
-
| `process-watcher`
|
|
148
|
-
| `log-watcher`
|
|
149
|
-
| `receptionist`
|
|
150
|
-
| `
|
|
151
|
-
| `
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
143
|
+
| NPC | What it does | Example |
|
|
144
|
+
| ------------------------ | ---------------------------------------------------------- | ------------------------------------------------------ |
|
|
145
|
+
| `system-monitor` | CPU / memory / disk / load with sparklines | `nocville-npc system-monitor --cpu-warn=75` |
|
|
146
|
+
| `internet-connectivity` | DNS, HTTP and TCP latency checks | `nocville-npc internet-connectivity` |
|
|
147
|
+
| `process-watcher` | Watches named processes, calls out deaths | `nocville-npc process-watcher --processes=nginx,redis` |
|
|
148
|
+
| `log-watcher` | Error bursts in log files, last lines on a monitor | `nocville-npc log-watcher --files=/var/log/app.log` |
|
|
149
|
+
| `receptionist` | Lobby greetings, check-ins and front desk jokes | `nocville-npc receptionist` |
|
|
150
|
+
| `security` | Indoor/outdoor patrols and exterior door watch | `nocville-npc security` |
|
|
151
|
+
| `janitor` | Mops suspicious spots and complains about bugs everywhere. | `nocville-npc janitor` |
|
|
152
|
+
| `barista` | Your coffee order is getting a little out of hand. | `nocville-npc barista` |
|
|
153
|
+
| `office-cat` | Your desk belongs to the cat now. | `nocville-npc office-cat` |
|
|
154
|
+
| `office-dog` | Chief morale officer. Easily distracted. | `nocville-npc office-dog` |
|
|
155
|
+
| `plant-caretaker` | Every fern deserves a performance review. | `nocville-npc plant-caretaker` |
|
|
156
|
+
| `it-technician` | Have you tried turning it off and on again? | `nocville-npc it-technician` |
|
|
157
|
+
| `out-of-touch-executive` | Big ideas. Long coffee breaks. Your overtime. | `nocville-npc out-of-touch-executive` |
|
|
158
|
+
| `github-watcher` | New commits on a commit board | `nocville-npc github-watcher --repo=owner/name` |
|
|
159
|
+
| `web-page-watcher` | A web page rendered as a pixel-art TV | `nocville-npc web-page-watcher --url=https://…` |
|
|
160
|
+
|
|
161
|
+
Run `nocville-npc --help` to see every flag. Poll intervals below 5 seconds are raised to 5
|
|
162
|
+
seconds. A few notes:
|
|
163
|
+
|
|
164
|
+
- **system-monitor**: `--disk-path=/data` picks the volume; thresholds are percentages
|
|
165
|
+
(`--cpu-warn`/`--cpu-crit`, `--mem-…`, `--disk-…`). On macOS, memory counts the file cache as
|
|
166
|
+
available, like Activity Monitor.
|
|
167
|
+
- **internet-connectivity**: `--dns-hosts`, `--http-urls` (http/https only) and
|
|
168
|
+
`--latency-hosts` take comma-separated lists; latency targets accept `host:port` or
|
|
169
|
+
`[ipv6]:port`. `--timeout=<ms>` bounds each check (default 5000).
|
|
170
|
+
- **process-watcher**: matches executable names, so `--processes=python` also finds `python3.11`
|
|
171
|
+
but `sh` does not match `bash`. Works on macOS and Linux via `ps`, and Windows via
|
|
172
|
+
PowerShell/CIM performance counters. Windows accepts names with or without `.exe` and
|
|
173
|
+
reports CPU as a percentage of total machine capacity. Full-path matching is available
|
|
174
|
+
where the OS reports paths (macOS); use executable names on Windows and Linux.
|
|
155
175
|
- **log-watcher**: `--error-pattern="ERROR|FATAL" --window=300 --error-warn=5 --error-crit=20`.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
- **
|
|
176
|
+
Files are followed across log rotation and may be created after the watcher starts. Lines
|
|
177
|
+
shown in the office have anything that looks like a secret (tokens, passwords, keys, URL
|
|
178
|
+
credentials) redacted, but treat the screen as visible to your whole team.
|
|
179
|
+
- **receptionist**: claims a free desk in a room named Lobby, Reception, Foyer or Entrance
|
|
180
|
+
(or its assigned work room). Without a desk, uses an open spot. Greets each arrival,
|
|
181
|
+
checks on a teammate every 2–4 minutes and makes a desk-side joke every 1–2 minutes.
|
|
182
|
+
No screen, weather requests or geolocation; old weather options are accepted but ignored.
|
|
183
|
+
- **security**: continuously visits rooms and reachable outdoor areas, pausing beside
|
|
184
|
+
exterior doors for 15–30 seconds. Does not claim a desk. An explicit work-room rule
|
|
185
|
+
still confines its route, so leave it unassigned for office-wide rounds.
|
|
186
|
+
- **office characters**: `janitor`, `barista`, `office-cat`, `office-dog`,
|
|
187
|
+
`plant-caretaker`, `it-technician` and `out-of-touch-executive` run without screens or
|
|
188
|
+
external service credentials. They share the usual API key, name, service ID and map
|
|
189
|
+
options. Run each as its own process. Their routes and conversation are synchronized by
|
|
190
|
+
the server, and work-room rules apply. Missing furniture or coworkers cause quiet walks
|
|
191
|
+
or floor rests instead. The technician's reboots and executive's requests are pretend.
|
|
192
|
+
Animals have custom walk, sleep and tail animations; staff carry their tools or coffee.
|
|
193
|
+
- **github-watcher**: uses ETag polling. Set `GITHUB_TOKEN` for private repos and a higher rate
|
|
194
|
+
limit (`--token` also works, but flags are visible to other users in the process list).
|
|
195
|
+
Without a token it polls every 90 seconds to stay within GitHub's 60 requests per hour.
|
|
160
196
|
- **web-page-watcher**: the pixel-art TV needs `npm i puppeteer-core` and a local
|
|
161
197
|
Chrome/Chromium (`--chrome-path` or `CHROME_PATH`). Without them it falls back to a
|
|
162
|
-
title/status ticker. Other options: `--mode=auto|screenshot|basic` and `--screen=160x120
|
|
198
|
+
title/status ticker. Other options: `--mode=auto|screenshot|basic` and `--screen=160x120`
|
|
199
|
+
(up to 320x240). Only `http(s)` URLs are accepted. Chrome runs with its sandbox unless you're
|
|
200
|
+
root or pass `--no-chrome-sandbox`; only disable it for pages you trust.
|
|
163
201
|
|
|
164
202
|
## Programmatic use
|
|
165
203
|
|
package/dist/cjs/cli.js
CHANGED
|
@@ -38,6 +38,7 @@ const node_fs_1 = require("node:fs");
|
|
|
38
38
|
const node_os_1 = require("node:os");
|
|
39
39
|
const node_path_1 = require("node:path");
|
|
40
40
|
const config_js_1 = require("./core/config.js");
|
|
41
|
+
const errors_js_1 = require("./core/errors.js");
|
|
41
42
|
const logger_js_1 = require("./core/logger.js");
|
|
42
43
|
const session_filter_js_1 = require("./core/session-filter.js");
|
|
43
44
|
// NPC modules are imported lazily per command: `claude-code hook` / `codex hook`
|
|
@@ -81,7 +82,15 @@ Monitoring NPC types:
|
|
|
81
82
|
internet-connectivity Monitor network connectivity
|
|
82
83
|
process-watcher Watch for running processes
|
|
83
84
|
log-watcher Watch log files for errors
|
|
84
|
-
receptionist
|
|
85
|
+
receptionist Lobby greeter with office check-ins and jokes
|
|
86
|
+
security Indoor/outdoor patrols and exterior door watch
|
|
87
|
+
janitor Mops suspicious spots and hunts office bugs
|
|
88
|
+
barista Takes increasingly absurd coffee orders
|
|
89
|
+
office-cat Naps on desks and supervises keyboards
|
|
90
|
+
office-dog Naps, barks at coworkers and runs around
|
|
91
|
+
plant-caretaker Waters plants and reviews their performance
|
|
92
|
+
it-technician Inspects tech gear and recommends reboots
|
|
93
|
+
out-of-touch-executive Interrupts coworkers between long lounge breaks
|
|
85
94
|
github-watcher Call out new commits on a repo
|
|
86
95
|
web-page-watcher Render a watched web page as a pixel-art TV
|
|
87
96
|
|
|
@@ -112,17 +121,23 @@ Internet Connectivity Options:
|
|
|
112
121
|
--poll-interval=<ms> Poll interval in ms (default: 30000)
|
|
113
122
|
--dns-hosts=<list> Comma-separated DNS hosts (default: google.com,cloudflare.com)
|
|
114
123
|
--http-urls=<list> Comma-separated HTTP URLs to check
|
|
115
|
-
--latency-hosts=<list> Comma-separated
|
|
124
|
+
--latency-hosts=<list> Comma-separated TCP latency targets: host, host:port or
|
|
125
|
+
[ipv6]:port (default: 8.8.8.8:443,1.1.1.1:443;
|
|
126
|
+
port 80 when omitted)
|
|
116
127
|
--slow-threshold=<ms> Slow response threshold in ms (default: 1000)
|
|
128
|
+
--timeout=<ms> Per-check timeout in ms (default: 5000)
|
|
117
129
|
|
|
118
130
|
Process Watcher Options:
|
|
119
131
|
--poll-interval=<ms> Poll interval in ms (default: 15000)
|
|
120
|
-
--processes=<list> Comma-separated
|
|
132
|
+
--processes=<list> Comma-separated executable names to watch (python
|
|
133
|
+
also matches python3.11; a /path matches exactly)
|
|
121
134
|
--cpu-warn=<n> CPU warning threshold % (default: 80)
|
|
122
135
|
--mem-warn=<n> Memory warning threshold MB (default: 500)
|
|
123
136
|
|
|
124
137
|
Log Watcher Options:
|
|
125
|
-
--files=<list> Comma-separated log file paths
|
|
138
|
+
--files=<list> Comma-separated log file paths (followed across
|
|
139
|
+
rotation; files may appear later). Lines shown on
|
|
140
|
+
the screen have secrets redacted.
|
|
126
141
|
--error-pattern=<regex> Error pattern (default: ERROR|FATAL)
|
|
127
142
|
--warning-pattern=<regex> Warning pattern (default: WARN)
|
|
128
143
|
--window=<seconds> Sliding window in seconds (default: 300)
|
|
@@ -130,26 +145,28 @@ Log Watcher Options:
|
|
|
130
145
|
--error-crit=<n> Error count for critical status (default: 20)
|
|
131
146
|
--report-interval=<ms> Report interval in ms (default: 15000)
|
|
132
147
|
|
|
133
|
-
|
|
134
|
-
--poll-interval=<ms>
|
|
135
|
-
--lat=<n> --lon=<n> Explicit coordinates for weather
|
|
136
|
-
--location=<name> Place name to geocode (else IP geo)
|
|
137
|
-
--fahrenheit Show temperature in °F (default: °C)
|
|
148
|
+
Office Character Options:
|
|
149
|
+
--poll-interval=<ms> Heartbeat interval in ms (default: 60000)
|
|
138
150
|
|
|
139
151
|
GitHub Watcher Options:
|
|
140
152
|
--repo=<owner/name> Repository to watch (required)
|
|
141
153
|
--branch=<name> Branch to watch (default: repo default branch)
|
|
142
|
-
--token=<token> GitHub token (
|
|
143
|
-
|
|
154
|
+
--token=<token> GitHub token (prefer the GITHUB_TOKEN env: flags are
|
|
155
|
+
visible in the process list)
|
|
156
|
+
--poll-interval=<ms> Poll interval in ms (default: 60000 with a token,
|
|
157
|
+
90000 without, to stay under GitHub's 60/hour)
|
|
144
158
|
|
|
145
159
|
Web Page Watcher Options:
|
|
146
|
-
--url=<url>
|
|
160
|
+
--url=<url> http(s) page to watch (required)
|
|
147
161
|
--mode=<auto|screenshot|basic> Render mode (default: auto)
|
|
148
162
|
--interval=<ms> Poll interval in ms (default: 60000, floor 15000)
|
|
149
|
-
--viewport=<WxH> Browser viewport (default: screen x 8)
|
|
150
|
-
--screen=<WxH> TV pixel resolution (default: 160x120
|
|
151
|
-
--wait-until=<event>
|
|
163
|
+
--viewport=<WxH> Browser viewport (default: screen x 8, max 4096x4096)
|
|
164
|
+
--screen=<WxH> TV pixel resolution (default: 160x120, max 320x240)
|
|
165
|
+
--wait-until=<event> load, domcontentloaded, networkidle0 or networkidle2
|
|
166
|
+
(default: networkidle2)
|
|
152
167
|
--chrome-path=<path> Chrome executable (or CHROME_PATH env)
|
|
168
|
+
--no-chrome-sandbox Run Chrome without its sandbox (only for trusted
|
|
169
|
+
pages; automatic when running as root)
|
|
153
170
|
(needs puppeteer-core + Chrome for the pixel-art TV; falls back to basic)
|
|
154
171
|
`.trim();
|
|
155
172
|
async function main() {
|
|
@@ -310,18 +327,10 @@ async function main() {
|
|
|
310
327
|
if (!config.serviceId) {
|
|
311
328
|
config.serviceId = 'internet-connectivity';
|
|
312
329
|
}
|
|
313
|
-
const dnsHosts = (0, config_js_1.stringFlag)(parsed.flags, 'dns-hosts')?.split(',');
|
|
314
|
-
const httpUrls = (0, config_js_1.stringFlag)(parsed.flags, 'http-urls')?.split(',');
|
|
315
|
-
const latencyHostsRaw = (0, config_js_1.stringFlag)(parsed.flags, 'latency-hosts')?.split(',');
|
|
316
|
-
const latencyHosts = latencyHostsRaw?.map((h) => ({ host: h.trim(), port: 80 }));
|
|
317
330
|
const { InternetConnectivityNPC } = await Promise.resolve().then(() => __importStar(require('./npcs/internet-connectivity/index.js')));
|
|
318
331
|
const npc = new InternetConnectivityNPC({
|
|
319
332
|
...config,
|
|
320
|
-
|
|
321
|
-
dnsHosts,
|
|
322
|
-
httpUrls,
|
|
323
|
-
latencyHosts,
|
|
324
|
-
slowThreshold: (0, config_js_1.numberFlag)(parsed.flags, 'slow-threshold'),
|
|
333
|
+
...InternetConnectivityNPC.configFromFlags(parsed.flags),
|
|
325
334
|
});
|
|
326
335
|
logger.info('Starting internet-connectivity NPC');
|
|
327
336
|
await npc.start();
|
|
@@ -331,8 +340,8 @@ async function main() {
|
|
|
331
340
|
if (!config.serviceId) {
|
|
332
341
|
config.serviceId = 'process-watcher';
|
|
333
342
|
}
|
|
334
|
-
const
|
|
335
|
-
if (!
|
|
343
|
+
const processes = (0, config_js_1.listFlag)(parsed.flags, 'processes');
|
|
344
|
+
if (!processes) {
|
|
336
345
|
console.error('Error: --processes is required (comma-separated list)');
|
|
337
346
|
process.exit(1);
|
|
338
347
|
}
|
|
@@ -340,7 +349,7 @@ async function main() {
|
|
|
340
349
|
const npc = new ProcessWatcherNPC({
|
|
341
350
|
...config,
|
|
342
351
|
pollInterval: (0, config_js_1.numberFlag)(parsed.flags, 'poll-interval'),
|
|
343
|
-
processes
|
|
352
|
+
processes,
|
|
344
353
|
cpuWarn: (0, config_js_1.numberFlag)(parsed.flags, 'cpu-warn'),
|
|
345
354
|
memWarn: (0, config_js_1.numberFlag)(parsed.flags, 'mem-warn'),
|
|
346
355
|
});
|
|
@@ -349,8 +358,8 @@ async function main() {
|
|
|
349
358
|
break;
|
|
350
359
|
}
|
|
351
360
|
case 'log-watcher': {
|
|
352
|
-
const
|
|
353
|
-
if (!
|
|
361
|
+
const files = (0, config_js_1.listFlag)(parsed.flags, 'files');
|
|
362
|
+
if (!files) {
|
|
354
363
|
console.error('Error: --files is required (comma-separated list)');
|
|
355
364
|
process.exit(1);
|
|
356
365
|
}
|
|
@@ -360,7 +369,7 @@ async function main() {
|
|
|
360
369
|
const { LogWatcherNPC } = await Promise.resolve().then(() => __importStar(require('./npcs/log-watcher/index.js')));
|
|
361
370
|
const npc = new LogWatcherNPC({
|
|
362
371
|
...config,
|
|
363
|
-
files
|
|
372
|
+
files,
|
|
364
373
|
errorPattern: (0, config_js_1.stringFlag)(parsed.flags, 'error-pattern'),
|
|
365
374
|
warningPattern: (0, config_js_1.stringFlag)(parsed.flags, 'warning-pattern'),
|
|
366
375
|
windowSeconds: (0, config_js_1.numberFlag)(parsed.flags, 'window'),
|
|
@@ -384,11 +393,43 @@ async function main() {
|
|
|
384
393
|
lon: (0, config_js_1.numberFlag)(parsed.flags, 'lon'),
|
|
385
394
|
location: (0, config_js_1.stringFlag)(parsed.flags, 'location'),
|
|
386
395
|
fahrenheit: (0, config_js_1.boolFlag)(parsed.flags, 'fahrenheit') ?? false,
|
|
396
|
+
ipGeo: (0, config_js_1.boolFlag)(parsed.flags, 'no-ip-geo') !== true,
|
|
387
397
|
});
|
|
388
398
|
logger.info('Starting receptionist NPC');
|
|
389
399
|
await npc.start();
|
|
390
400
|
break;
|
|
391
401
|
}
|
|
402
|
+
case 'security': {
|
|
403
|
+
if (!config.serviceId)
|
|
404
|
+
config.serviceId = 'security';
|
|
405
|
+
const { SecurityNPC } = await Promise.resolve().then(() => __importStar(require('./npcs/security/index.js')));
|
|
406
|
+
const npc = new SecurityNPC({
|
|
407
|
+
...config,
|
|
408
|
+
pollInterval: (0, config_js_1.numberFlag)(parsed.flags, 'poll-interval'),
|
|
409
|
+
});
|
|
410
|
+
logger.info('Starting security NPC');
|
|
411
|
+
await npc.start();
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
case 'janitor':
|
|
415
|
+
case 'barista':
|
|
416
|
+
case 'office-cat':
|
|
417
|
+
case 'office-dog':
|
|
418
|
+
case 'plant-caretaker':
|
|
419
|
+
case 'it-technician':
|
|
420
|
+
case 'out-of-touch-executive': {
|
|
421
|
+
if (!config.serviceId)
|
|
422
|
+
config.serviceId = parsed.command;
|
|
423
|
+
const { OFFICE_CHARACTER_NPCS } = await Promise.resolve().then(() => __importStar(require('./npcs/office-characters/index.js')));
|
|
424
|
+
const NPCClass = OFFICE_CHARACTER_NPCS[parsed.command];
|
|
425
|
+
const npc = new NPCClass({
|
|
426
|
+
...config,
|
|
427
|
+
pollInterval: (0, config_js_1.numberFlag)(parsed.flags, 'poll-interval'),
|
|
428
|
+
});
|
|
429
|
+
logger.info(`Starting ${parsed.command} NPC`);
|
|
430
|
+
await npc.start();
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
392
433
|
case 'github-watcher': {
|
|
393
434
|
const repo = (0, config_js_1.stringFlag)(parsed.flags, 'repo');
|
|
394
435
|
if (!repo) {
|
|
@@ -400,11 +441,15 @@ async function main() {
|
|
|
400
441
|
config.serviceId = `github-${shortName.replace(/[^a-zA-Z0-9_-]/g, '-')}`.slice(0, 100);
|
|
401
442
|
}
|
|
402
443
|
const { GitHubWatcherNPC } = await Promise.resolve().then(() => __importStar(require('./npcs/github-watcher/index.js')));
|
|
444
|
+
const tokenFlag = (0, config_js_1.stringFlag)(parsed.flags, 'token');
|
|
445
|
+
if (tokenFlag) {
|
|
446
|
+
logger.warn('--token is visible to other local users in the process list; prefer GITHUB_TOKEN');
|
|
447
|
+
}
|
|
403
448
|
const npc = new GitHubWatcherNPC({
|
|
404
449
|
...config,
|
|
405
450
|
repo,
|
|
406
451
|
branch: (0, config_js_1.stringFlag)(parsed.flags, 'branch'),
|
|
407
|
-
token:
|
|
452
|
+
token: tokenFlag ?? process.env['GITHUB_TOKEN'],
|
|
408
453
|
pollInterval: (0, config_js_1.numberFlag)(parsed.flags, 'poll-interval'),
|
|
409
454
|
});
|
|
410
455
|
logger.info('Starting github-watcher NPC');
|
|
@@ -420,22 +465,28 @@ async function main() {
|
|
|
420
465
|
const { hostnameOf } = await Promise.resolve().then(() => __importStar(require('./npcs/web-page-watcher/basic-mode.js')));
|
|
421
466
|
const { parseDimensions } = await Promise.resolve().then(() => __importStar(require('./npcs/web-page-watcher/defaults.js')));
|
|
422
467
|
const { WebPageWatcherNPC } = await Promise.resolve().then(() => __importStar(require('./npcs/web-page-watcher/index.js')));
|
|
423
|
-
const parseWxH = (
|
|
468
|
+
const parseWxH = (flag) => {
|
|
469
|
+
const v = (0, config_js_1.stringFlag)(parsed.flags, flag);
|
|
470
|
+
if (v === undefined)
|
|
471
|
+
return undefined;
|
|
472
|
+
const dims = parseDimensions(v);
|
|
473
|
+
if (!dims)
|
|
474
|
+
throw new errors_js_1.NpcConfigError(`--${flag} must look like 160x120`);
|
|
475
|
+
return dims;
|
|
476
|
+
};
|
|
424
477
|
if (!config.serviceId) {
|
|
425
478
|
config.serviceId = `webpage-${hostnameOf(url)}`
|
|
426
479
|
.replace(/[^a-zA-Z0-9_.-]/g, '-')
|
|
427
480
|
.slice(0, 100);
|
|
428
481
|
}
|
|
429
|
-
const
|
|
430
|
-
const
|
|
431
|
-
? modeFlag
|
|
432
|
-
: undefined;
|
|
433
|
-
const viewport = parseWxH((0, config_js_1.stringFlag)(parsed.flags, 'viewport'));
|
|
434
|
-
const screen = parseWxH((0, config_js_1.stringFlag)(parsed.flags, 'screen'));
|
|
482
|
+
const viewport = parseWxH('viewport');
|
|
483
|
+
const screen = parseWxH('screen');
|
|
435
484
|
const npc = new WebPageWatcherNPC({
|
|
436
485
|
...config,
|
|
437
486
|
url,
|
|
438
|
-
|
|
487
|
+
// Validated (with a clear error) by the constructor.
|
|
488
|
+
mode: (0, config_js_1.stringFlag)(parsed.flags, 'mode'),
|
|
489
|
+
chromeSandbox: (0, config_js_1.boolFlag)(parsed.flags, 'no-chrome-sandbox') !== true,
|
|
439
490
|
chromePath: (0, config_js_1.stringFlag)(parsed.flags, 'chrome-path'),
|
|
440
491
|
waitUntil: (0, config_js_1.stringFlag)(parsed.flags, 'wait-until'),
|
|
441
492
|
viewportWidth: viewport?.width,
|
|
@@ -578,6 +629,10 @@ main().catch((err) => {
|
|
|
578
629
|
process.stdout.write('{}\n');
|
|
579
630
|
return;
|
|
580
631
|
}
|
|
632
|
+
if (err instanceof errors_js_1.NpcConfigError) {
|
|
633
|
+
console.error(`Error: ${err.message}`);
|
|
634
|
+
process.exit(1);
|
|
635
|
+
}
|
|
581
636
|
console.error('Fatal error:', err);
|
|
582
637
|
process.exit(1);
|
|
583
638
|
});
|
|
@@ -42,6 +42,13 @@ class BaseNPC {
|
|
|
42
42
|
const name = this.displayName;
|
|
43
43
|
return name ? (0, text_js_1.truncate)(name, 32) : undefined;
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* NPC type sent as the push `kind` (groups NPCs for per-type rules such as "all process
|
|
47
|
+
* watchers work in the Server Room"). Defaults to `name` when it is a valid slug.
|
|
48
|
+
*/
|
|
49
|
+
get kind() {
|
|
50
|
+
return /^[a-z0-9][a-z0-9-]{0,39}$/.test(this.name) ? this.name : undefined;
|
|
51
|
+
}
|
|
45
52
|
/** NPC archetype — steers movement speed and default behavior. Override per NPC. */
|
|
46
53
|
get archetype() {
|
|
47
54
|
return 'service';
|
|
@@ -116,6 +123,7 @@ class BaseNPC {
|
|
|
116
123
|
message: metrics.message ? (0, text_js_1.truncate)(metrics.message, 500) : undefined,
|
|
117
124
|
metrics: metrics.metrics,
|
|
118
125
|
archetype: this.archetype,
|
|
126
|
+
kind: this.kind,
|
|
119
127
|
subStatus: metrics.subStatus,
|
|
120
128
|
screenData: metrics.screenData,
|
|
121
129
|
behavior: Object.keys(behavior).length > 0 ? behavior : undefined,
|
|
@@ -123,6 +131,13 @@ class BaseNPC {
|
|
|
123
131
|
alertConfig: { heartbeatInterval: this.heartbeatSeconds },
|
|
124
132
|
map: this.config.map,
|
|
125
133
|
});
|
|
134
|
+
// An observer cannot interrupt monitoring or retries.
|
|
135
|
+
try {
|
|
136
|
+
this.config.onPushResult?.({ success: result.success, error: result.error });
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
/* observer only */
|
|
140
|
+
}
|
|
126
141
|
if (!result.success) {
|
|
127
142
|
this.reportPushFailure(result.error);
|
|
128
143
|
}
|
package/dist/cjs/core/config.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.parseArgs = parseArgs;
|
|
|
4
4
|
exports.resolveConfig = resolveConfig;
|
|
5
5
|
exports.stringFlag = stringFlag;
|
|
6
6
|
exports.boolFlag = boolFlag;
|
|
7
|
+
exports.listFlag = listFlag;
|
|
7
8
|
exports.numberFlag = numberFlag;
|
|
8
9
|
const user_config_js_1 = require("./user-config.js");
|
|
9
10
|
/**
|
|
@@ -96,11 +97,27 @@ function boolFlag(flags, key) {
|
|
|
96
97
|
return false;
|
|
97
98
|
return undefined;
|
|
98
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* A comma-separated list flag (`--processes=nginx,redis`): entries trimmed,
|
|
102
|
+
* empties dropped, duplicates removed. Undefined when the flag is absent or empty.
|
|
103
|
+
*/
|
|
104
|
+
function listFlag(flags, key) {
|
|
105
|
+
const raw = stringFlag(flags, key);
|
|
106
|
+
if (raw === undefined)
|
|
107
|
+
return undefined;
|
|
108
|
+
const items = [
|
|
109
|
+
...new Set(raw
|
|
110
|
+
.split(',')
|
|
111
|
+
.map((s) => s.trim())
|
|
112
|
+
.filter(Boolean)),
|
|
113
|
+
];
|
|
114
|
+
return items.length > 0 ? items : undefined;
|
|
115
|
+
}
|
|
99
116
|
function numberFlag(flags, key) {
|
|
100
117
|
const val = flags[key];
|
|
101
|
-
if (typeof val === 'string') {
|
|
118
|
+
if (typeof val === 'string' && val.trim() !== '') {
|
|
102
119
|
const n = Number(val);
|
|
103
|
-
if (
|
|
120
|
+
if (Number.isFinite(n))
|
|
104
121
|
return n;
|
|
105
122
|
}
|
|
106
123
|
return undefined;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NpcConfigError = void 0;
|
|
4
|
+
exports.assertPosix = assertPosix;
|
|
5
|
+
/**
|
|
6
|
+
* A user-facing configuration problem (bad flag value, unsupported platform).
|
|
7
|
+
* The CLI prints only its message and exits 1 — no stack trace — so throw it
|
|
8
|
+
* for anything the user can fix by changing how they run the NPC.
|
|
9
|
+
*/
|
|
10
|
+
class NpcConfigError extends Error {
|
|
11
|
+
constructor(message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'NpcConfigError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
exports.NpcConfigError = NpcConfigError;
|
|
17
|
+
/** Throw an NpcConfigError on Windows, for NPCs that rely on POSIX tools or paths. */
|
|
18
|
+
function assertPosix(npcName, hint) {
|
|
19
|
+
if (process.platform === 'win32') {
|
|
20
|
+
throw new NpcConfigError(`${npcName} is not supported on Windows. ${hint}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Small, bounded HTTP helpers for NPCs that read untrusted remote content
|
|
4
|
+
* (weather/geo APIs, GitHub, watched web pages). Every call has a timeout and
|
|
5
|
+
* every body read has a byte cap, so a slow or hostile server can neither hang
|
|
6
|
+
* a collection nor balloon memory.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.DEFAULT_MAX_BODY_BYTES = exports.NPC_USER_AGENT = void 0;
|
|
10
|
+
exports.isHttpUrl = isHttpUrl;
|
|
11
|
+
exports.displayUrl = displayUrl;
|
|
12
|
+
exports.readTextCapped = readTextCapped;
|
|
13
|
+
exports.discardBody = discardBody;
|
|
14
|
+
exports.fetchJson = fetchJson;
|
|
15
|
+
exports.asRecord = asRecord;
|
|
16
|
+
exports.finiteNumber = finiteNumber;
|
|
17
|
+
/** The `User-Agent` sent by every NPC request. */
|
|
18
|
+
exports.NPC_USER_AGENT = 'nocville-npc';
|
|
19
|
+
/** Default cap for a response body read into memory. */
|
|
20
|
+
exports.DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
|
|
21
|
+
/** True for an absolute http(s) URL (anything else — file:, data:, javascript: — is refused). */
|
|
22
|
+
function isHttpUrl(value) {
|
|
23
|
+
try {
|
|
24
|
+
const url = new URL(value);
|
|
25
|
+
return url.protocol === 'http:' || url.protocol === 'https:';
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** A URL rendered for logs/screens: scheme, host and path only — never credentials or query. */
|
|
32
|
+
function displayUrl(value) {
|
|
33
|
+
try {
|
|
34
|
+
const url = new URL(value);
|
|
35
|
+
return `${url.protocol}//${url.host}${url.pathname === '/' ? '' : url.pathname}`;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return '(invalid url)';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Read a response body as UTF-8, keeping at most `maxBytes`. Streams when the
|
|
43
|
+
* body is available (cancelling the rest); otherwise falls back to `text()`
|
|
44
|
+
* after rejecting an oversized declared Content-Length. Returns the (possibly
|
|
45
|
+
* truncated) text and whether it was truncated.
|
|
46
|
+
*/
|
|
47
|
+
async function readTextCapped(res, maxBytes = exports.DEFAULT_MAX_BODY_BYTES) {
|
|
48
|
+
const reader = res.body?.getReader();
|
|
49
|
+
if (!reader) {
|
|
50
|
+
const declared = Number(res.headers?.get('content-length') ?? NaN);
|
|
51
|
+
if (Number.isFinite(declared) && declared > maxBytes)
|
|
52
|
+
return { text: '', truncated: true };
|
|
53
|
+
const text = await res.text();
|
|
54
|
+
return text.length > maxBytes
|
|
55
|
+
? { text: text.slice(0, maxBytes), truncated: true }
|
|
56
|
+
: { text, truncated: false };
|
|
57
|
+
}
|
|
58
|
+
const chunks = [];
|
|
59
|
+
let total = 0;
|
|
60
|
+
let truncated = false;
|
|
61
|
+
try {
|
|
62
|
+
for (;;) {
|
|
63
|
+
const { done, value } = await reader.read();
|
|
64
|
+
if (done)
|
|
65
|
+
break;
|
|
66
|
+
if (total + value.byteLength > maxBytes) {
|
|
67
|
+
chunks.push(value.subarray(0, maxBytes - total));
|
|
68
|
+
total = maxBytes;
|
|
69
|
+
truncated = true;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
chunks.push(value);
|
|
73
|
+
total += value.byteLength;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
if (truncated)
|
|
78
|
+
await reader.cancel().catch(() => undefined);
|
|
79
|
+
reader.releaseLock();
|
|
80
|
+
}
|
|
81
|
+
return { text: new TextDecoder().decode(Buffer.concat(chunks)), truncated };
|
|
82
|
+
}
|
|
83
|
+
/** Discard a response body without reading it (frees the connection). */
|
|
84
|
+
async function discardBody(res) {
|
|
85
|
+
await res.body?.cancel().catch(() => undefined);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* GET a JSON document with a timeout and size cap. Returns null on any network
|
|
89
|
+
* error, non-2xx status, oversized body or malformed JSON — callers degrade
|
|
90
|
+
* gracefully rather than throw.
|
|
91
|
+
*/
|
|
92
|
+
async function fetchJson(url, opts) {
|
|
93
|
+
try {
|
|
94
|
+
const res = await fetch(url, {
|
|
95
|
+
signal: AbortSignal.timeout(opts.timeoutMs),
|
|
96
|
+
headers: { 'User-Agent': exports.NPC_USER_AGENT, Accept: 'application/json', ...opts.headers },
|
|
97
|
+
});
|
|
98
|
+
if (!res.ok) {
|
|
99
|
+
await discardBody(res);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const { text, truncated } = await readTextCapped(res, opts.maxBytes ?? 256 * 1024);
|
|
103
|
+
if (truncated)
|
|
104
|
+
return null;
|
|
105
|
+
return JSON.parse(text);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** Narrow an unknown JSON value to a plain object (arrays and null excluded). */
|
|
112
|
+
function asRecord(value) {
|
|
113
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
114
|
+
? value
|
|
115
|
+
: null;
|
|
116
|
+
}
|
|
117
|
+
/** A finite number from an unknown JSON value, else null. */
|
|
118
|
+
function finiteNumber(value) {
|
|
119
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=http.js.map
|