@nocville/npc 0.2.2 → 0.2.3
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 +23 -5
- package/dist/cjs/cli.js +55 -34
- package/dist/cjs/core/base-npc.js +8 -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 +10 -2
- package/dist/cjs/npcs/agent-common/session.js +1 -0
- 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/process-watcher/index.js +61 -51
- package/dist/cjs/npcs/process-watcher/process-scanner.js +63 -69
- package/dist/cjs/npcs/receptionist/defaults.js +4 -0
- package/dist/cjs/npcs/receptionist/index.js +45 -19
- package/dist/cjs/npcs/receptionist/weather.js +42 -47
- 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 +56 -35
- package/dist/core/base-npc.d.ts +5 -0
- package/dist/core/base-npc.js +8 -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/index.d.ts +5 -2
- package/dist/index.js +4 -2
- package/dist/npcs/agent-common/session.js +1 -0
- 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/process-watcher/index.d.ts +1 -1
- package/dist/npcs/process-watcher/index.js +62 -52
- package/dist/npcs/process-watcher/process-scanner.d.ts +29 -1
- package/dist/npcs/process-watcher/process-scanner.js +61 -71
- package/dist/npcs/receptionist/defaults.d.ts +4 -0
- package/dist/npcs/receptionist/defaults.js +4 -0
- package/dist/npcs/receptionist/index.d.ts +12 -2
- package/dist/npcs/receptionist/index.js +46 -20
- package/dist/npcs/receptionist/weather.d.ts +8 -6
- package/dist/npcs/receptionist/weather.js +39 -45
- 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
|
@@ -150,16 +150,34 @@ on your map automatically and despawns when you press Ctrl-C. Once you've run
|
|
|
150
150
|
| `github-watcher` | New commits on a commit board | `nocville-npc github-watcher --repo=owner/name` |
|
|
151
151
|
| `web-page-watcher` | A web page rendered as a pixel-art TV | `nocville-npc web-page-watcher --url=https://…` |
|
|
152
152
|
|
|
153
|
-
Run `nocville-npc --help` to see every flag.
|
|
154
|
-
|
|
153
|
+
Run `nocville-npc --help` to see every flag. Poll intervals below 5 seconds are raised to 5
|
|
154
|
+
seconds. A few notes:
|
|
155
|
+
|
|
156
|
+
- **system-monitor**: `--disk-path=/data` picks the volume; thresholds are percentages
|
|
157
|
+
(`--cpu-warn`/`--cpu-crit`, `--mem-…`, `--disk-…`). On macOS, memory counts the file cache as
|
|
158
|
+
available, like Activity Monitor.
|
|
159
|
+
- **internet-connectivity**: `--dns-hosts`, `--http-urls` (http/https only) and
|
|
160
|
+
`--latency-hosts` take comma-separated lists; latency targets accept `host:port` or
|
|
161
|
+
`[ipv6]:port`. `--timeout=<ms>` bounds each check (default 5000).
|
|
162
|
+
- **process-watcher**: matches executable names, so `--processes=python` also finds `python3.11`
|
|
163
|
+
but `sh` does not match `bash`. Use a full path to match one binary exactly. macOS and Linux
|
|
164
|
+
only.
|
|
155
165
|
- **log-watcher**: `--error-pattern="ERROR|FATAL" --window=300 --error-warn=5 --error-crit=20`.
|
|
166
|
+
Files are followed across log rotation and may be created after the watcher starts. Lines
|
|
167
|
+
shown in the office have anything that looks like a secret (tokens, passwords, keys, URL
|
|
168
|
+
credentials) redacted, but treat the screen as visible to your whole team.
|
|
156
169
|
- **receptionist**: weather comes from the keyless open-meteo API. Set the place with
|
|
157
|
-
`--lat=52.5 --lon=13.4` or `--location
|
|
170
|
+
`--lat=52.5 --lon=13.4` or `--location=…`. Without either, it estimates your location from
|
|
171
|
+
your public IP via ipapi.co; pass `--no-ip-geo` to turn that off and show only the time. Add
|
|
158
172
|
`--fahrenheit` for °F.
|
|
159
|
-
- **github-watcher**: uses ETag polling.
|
|
173
|
+
- **github-watcher**: uses ETag polling. Set `GITHUB_TOKEN` for private repos and a higher rate
|
|
174
|
+
limit (`--token` also works, but flags are visible to other users in the process list).
|
|
175
|
+
Without a token it polls every 90 seconds to stay within GitHub's 60 requests per hour.
|
|
160
176
|
- **web-page-watcher**: the pixel-art TV needs `npm i puppeteer-core` and a local
|
|
161
177
|
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
|
|
178
|
+
title/status ticker. Other options: `--mode=auto|screenshot|basic` and `--screen=160x120`
|
|
179
|
+
(up to 320x240). Only `http(s)` URLs are accepted. Chrome runs with its sandbox unless you're
|
|
180
|
+
root or pass `--no-chrome-sandbox`; only disable it for pages you trust.
|
|
163
181
|
|
|
164
182
|
## Programmatic use
|
|
165
183
|
|
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`
|
|
@@ -112,17 +113,23 @@ Internet Connectivity Options:
|
|
|
112
113
|
--poll-interval=<ms> Poll interval in ms (default: 30000)
|
|
113
114
|
--dns-hosts=<list> Comma-separated DNS hosts (default: google.com,cloudflare.com)
|
|
114
115
|
--http-urls=<list> Comma-separated HTTP URLs to check
|
|
115
|
-
--latency-hosts=<list> Comma-separated
|
|
116
|
+
--latency-hosts=<list> Comma-separated TCP latency targets: host, host:port or
|
|
117
|
+
[ipv6]:port (default: 8.8.8.8:443,1.1.1.1:443;
|
|
118
|
+
port 80 when omitted)
|
|
116
119
|
--slow-threshold=<ms> Slow response threshold in ms (default: 1000)
|
|
120
|
+
--timeout=<ms> Per-check timeout in ms (default: 5000)
|
|
117
121
|
|
|
118
122
|
Process Watcher Options:
|
|
119
123
|
--poll-interval=<ms> Poll interval in ms (default: 15000)
|
|
120
|
-
--processes=<list> Comma-separated
|
|
124
|
+
--processes=<list> Comma-separated executable names to watch (python
|
|
125
|
+
also matches python3.11; a /path matches exactly)
|
|
121
126
|
--cpu-warn=<n> CPU warning threshold % (default: 80)
|
|
122
127
|
--mem-warn=<n> Memory warning threshold MB (default: 500)
|
|
123
128
|
|
|
124
129
|
Log Watcher Options:
|
|
125
|
-
--files=<list> Comma-separated log file paths
|
|
130
|
+
--files=<list> Comma-separated log file paths (followed across
|
|
131
|
+
rotation; files may appear later). Lines shown on
|
|
132
|
+
the screen have secrets redacted.
|
|
126
133
|
--error-pattern=<regex> Error pattern (default: ERROR|FATAL)
|
|
127
134
|
--warning-pattern=<regex> Warning pattern (default: WARN)
|
|
128
135
|
--window=<seconds> Sliding window in seconds (default: 300)
|
|
@@ -133,23 +140,30 @@ Log Watcher Options:
|
|
|
133
140
|
Receptionist Options:
|
|
134
141
|
--poll-interval=<ms> Clock tick in ms (default: 60000)
|
|
135
142
|
--lat=<n> --lon=<n> Explicit coordinates for weather
|
|
136
|
-
--location=<name> Place name to geocode (
|
|
143
|
+
--location=<name> Place name to geocode (open-meteo)
|
|
144
|
+
--no-ip-geo Without --lat/--lon or --location, don't look up
|
|
145
|
+
this machine's IP location (ipapi.co); show time only
|
|
137
146
|
--fahrenheit Show temperature in °F (default: °C)
|
|
138
147
|
|
|
139
148
|
GitHub Watcher Options:
|
|
140
149
|
--repo=<owner/name> Repository to watch (required)
|
|
141
150
|
--branch=<name> Branch to watch (default: repo default branch)
|
|
142
|
-
--token=<token> GitHub token (
|
|
143
|
-
|
|
151
|
+
--token=<token> GitHub token (prefer the GITHUB_TOKEN env: flags are
|
|
152
|
+
visible in the process list)
|
|
153
|
+
--poll-interval=<ms> Poll interval in ms (default: 60000 with a token,
|
|
154
|
+
90000 without, to stay under GitHub's 60/hour)
|
|
144
155
|
|
|
145
156
|
Web Page Watcher Options:
|
|
146
|
-
--url=<url>
|
|
157
|
+
--url=<url> http(s) page to watch (required)
|
|
147
158
|
--mode=<auto|screenshot|basic> Render mode (default: auto)
|
|
148
159
|
--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>
|
|
160
|
+
--viewport=<WxH> Browser viewport (default: screen x 8, max 4096x4096)
|
|
161
|
+
--screen=<WxH> TV pixel resolution (default: 160x120, max 320x240)
|
|
162
|
+
--wait-until=<event> load, domcontentloaded, networkidle0 or networkidle2
|
|
163
|
+
(default: networkidle2)
|
|
152
164
|
--chrome-path=<path> Chrome executable (or CHROME_PATH env)
|
|
165
|
+
--no-chrome-sandbox Run Chrome without its sandbox (only for trusted
|
|
166
|
+
pages; automatic when running as root)
|
|
153
167
|
(needs puppeteer-core + Chrome for the pixel-art TV; falls back to basic)
|
|
154
168
|
`.trim();
|
|
155
169
|
async function main() {
|
|
@@ -310,18 +324,10 @@ async function main() {
|
|
|
310
324
|
if (!config.serviceId) {
|
|
311
325
|
config.serviceId = 'internet-connectivity';
|
|
312
326
|
}
|
|
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
327
|
const { InternetConnectivityNPC } = await Promise.resolve().then(() => __importStar(require('./npcs/internet-connectivity/index.js')));
|
|
318
328
|
const npc = new InternetConnectivityNPC({
|
|
319
329
|
...config,
|
|
320
|
-
|
|
321
|
-
dnsHosts,
|
|
322
|
-
httpUrls,
|
|
323
|
-
latencyHosts,
|
|
324
|
-
slowThreshold: (0, config_js_1.numberFlag)(parsed.flags, 'slow-threshold'),
|
|
330
|
+
...InternetConnectivityNPC.configFromFlags(parsed.flags),
|
|
325
331
|
});
|
|
326
332
|
logger.info('Starting internet-connectivity NPC');
|
|
327
333
|
await npc.start();
|
|
@@ -331,8 +337,8 @@ async function main() {
|
|
|
331
337
|
if (!config.serviceId) {
|
|
332
338
|
config.serviceId = 'process-watcher';
|
|
333
339
|
}
|
|
334
|
-
const
|
|
335
|
-
if (!
|
|
340
|
+
const processes = (0, config_js_1.listFlag)(parsed.flags, 'processes');
|
|
341
|
+
if (!processes) {
|
|
336
342
|
console.error('Error: --processes is required (comma-separated list)');
|
|
337
343
|
process.exit(1);
|
|
338
344
|
}
|
|
@@ -340,7 +346,7 @@ async function main() {
|
|
|
340
346
|
const npc = new ProcessWatcherNPC({
|
|
341
347
|
...config,
|
|
342
348
|
pollInterval: (0, config_js_1.numberFlag)(parsed.flags, 'poll-interval'),
|
|
343
|
-
processes
|
|
349
|
+
processes,
|
|
344
350
|
cpuWarn: (0, config_js_1.numberFlag)(parsed.flags, 'cpu-warn'),
|
|
345
351
|
memWarn: (0, config_js_1.numberFlag)(parsed.flags, 'mem-warn'),
|
|
346
352
|
});
|
|
@@ -349,8 +355,8 @@ async function main() {
|
|
|
349
355
|
break;
|
|
350
356
|
}
|
|
351
357
|
case 'log-watcher': {
|
|
352
|
-
const
|
|
353
|
-
if (!
|
|
358
|
+
const files = (0, config_js_1.listFlag)(parsed.flags, 'files');
|
|
359
|
+
if (!files) {
|
|
354
360
|
console.error('Error: --files is required (comma-separated list)');
|
|
355
361
|
process.exit(1);
|
|
356
362
|
}
|
|
@@ -360,7 +366,7 @@ async function main() {
|
|
|
360
366
|
const { LogWatcherNPC } = await Promise.resolve().then(() => __importStar(require('./npcs/log-watcher/index.js')));
|
|
361
367
|
const npc = new LogWatcherNPC({
|
|
362
368
|
...config,
|
|
363
|
-
files
|
|
369
|
+
files,
|
|
364
370
|
errorPattern: (0, config_js_1.stringFlag)(parsed.flags, 'error-pattern'),
|
|
365
371
|
warningPattern: (0, config_js_1.stringFlag)(parsed.flags, 'warning-pattern'),
|
|
366
372
|
windowSeconds: (0, config_js_1.numberFlag)(parsed.flags, 'window'),
|
|
@@ -384,6 +390,7 @@ async function main() {
|
|
|
384
390
|
lon: (0, config_js_1.numberFlag)(parsed.flags, 'lon'),
|
|
385
391
|
location: (0, config_js_1.stringFlag)(parsed.flags, 'location'),
|
|
386
392
|
fahrenheit: (0, config_js_1.boolFlag)(parsed.flags, 'fahrenheit') ?? false,
|
|
393
|
+
ipGeo: (0, config_js_1.boolFlag)(parsed.flags, 'no-ip-geo') !== true,
|
|
387
394
|
});
|
|
388
395
|
logger.info('Starting receptionist NPC');
|
|
389
396
|
await npc.start();
|
|
@@ -400,11 +407,15 @@ async function main() {
|
|
|
400
407
|
config.serviceId = `github-${shortName.replace(/[^a-zA-Z0-9_-]/g, '-')}`.slice(0, 100);
|
|
401
408
|
}
|
|
402
409
|
const { GitHubWatcherNPC } = await Promise.resolve().then(() => __importStar(require('./npcs/github-watcher/index.js')));
|
|
410
|
+
const tokenFlag = (0, config_js_1.stringFlag)(parsed.flags, 'token');
|
|
411
|
+
if (tokenFlag) {
|
|
412
|
+
logger.warn('--token is visible to other local users in the process list; prefer GITHUB_TOKEN');
|
|
413
|
+
}
|
|
403
414
|
const npc = new GitHubWatcherNPC({
|
|
404
415
|
...config,
|
|
405
416
|
repo,
|
|
406
417
|
branch: (0, config_js_1.stringFlag)(parsed.flags, 'branch'),
|
|
407
|
-
token:
|
|
418
|
+
token: tokenFlag ?? process.env['GITHUB_TOKEN'],
|
|
408
419
|
pollInterval: (0, config_js_1.numberFlag)(parsed.flags, 'poll-interval'),
|
|
409
420
|
});
|
|
410
421
|
logger.info('Starting github-watcher NPC');
|
|
@@ -420,22 +431,28 @@ async function main() {
|
|
|
420
431
|
const { hostnameOf } = await Promise.resolve().then(() => __importStar(require('./npcs/web-page-watcher/basic-mode.js')));
|
|
421
432
|
const { parseDimensions } = await Promise.resolve().then(() => __importStar(require('./npcs/web-page-watcher/defaults.js')));
|
|
422
433
|
const { WebPageWatcherNPC } = await Promise.resolve().then(() => __importStar(require('./npcs/web-page-watcher/index.js')));
|
|
423
|
-
const parseWxH = (
|
|
434
|
+
const parseWxH = (flag) => {
|
|
435
|
+
const v = (0, config_js_1.stringFlag)(parsed.flags, flag);
|
|
436
|
+
if (v === undefined)
|
|
437
|
+
return undefined;
|
|
438
|
+
const dims = parseDimensions(v);
|
|
439
|
+
if (!dims)
|
|
440
|
+
throw new errors_js_1.NpcConfigError(`--${flag} must look like 160x120`);
|
|
441
|
+
return dims;
|
|
442
|
+
};
|
|
424
443
|
if (!config.serviceId) {
|
|
425
444
|
config.serviceId = `webpage-${hostnameOf(url)}`
|
|
426
445
|
.replace(/[^a-zA-Z0-9_.-]/g, '-')
|
|
427
446
|
.slice(0, 100);
|
|
428
447
|
}
|
|
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'));
|
|
448
|
+
const viewport = parseWxH('viewport');
|
|
449
|
+
const screen = parseWxH('screen');
|
|
435
450
|
const npc = new WebPageWatcherNPC({
|
|
436
451
|
...config,
|
|
437
452
|
url,
|
|
438
|
-
|
|
453
|
+
// Validated (with a clear error) by the constructor.
|
|
454
|
+
mode: (0, config_js_1.stringFlag)(parsed.flags, 'mode'),
|
|
455
|
+
chromeSandbox: (0, config_js_1.boolFlag)(parsed.flags, 'no-chrome-sandbox') !== true,
|
|
439
456
|
chromePath: (0, config_js_1.stringFlag)(parsed.flags, 'chrome-path'),
|
|
440
457
|
waitUntil: (0, config_js_1.stringFlag)(parsed.flags, 'wait-until'),
|
|
441
458
|
viewportWidth: viewport?.width,
|
|
@@ -578,6 +595,10 @@ main().catch((err) => {
|
|
|
578
595
|
process.stdout.write('{}\n');
|
|
579
596
|
return;
|
|
580
597
|
}
|
|
598
|
+
if (err instanceof errors_js_1.NpcConfigError) {
|
|
599
|
+
console.error(`Error: ${err.message}`);
|
|
600
|
+
process.exit(1);
|
|
601
|
+
}
|
|
581
602
|
console.error('Fatal error:', err);
|
|
582
603
|
process.exit(1);
|
|
583
604
|
});
|
|
@@ -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,
|
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
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.encodePixelArt = encodePixelArt;
|
|
4
|
+
exports.pixelArtFromPng = pixelArtFromPng;
|
|
5
|
+
exports.pixelDevice = pixelDevice;
|
|
6
|
+
const node_fs_1 = require("node:fs");
|
|
7
|
+
const pngjs_1 = require("pngjs");
|
|
8
|
+
/**
|
|
9
|
+
* Helpers for custom pixel-art companions (`screenType: 'pixel-device'`): your NPC's own
|
|
10
|
+
* device, sign or machine, drawn frameless beside it in the office.
|
|
11
|
+
*
|
|
12
|
+
* Art is 32 pixels per tile (a 2×3-tile device is 64×96, like the office furniture) and is
|
|
13
|
+
* sent as `pixel-image` screen data: a palette of up to 255 colours plus one byte per pixel.
|
|
14
|
+
* Fully transparent pixels (alpha < 128) become see-through, and several frames stacked top
|
|
15
|
+
* to bottom animate at `fps`. Push a new image whenever its state changes (e.g. an LED turns
|
|
16
|
+
* red); the server rate-limits pushes, so animate with frames rather than many pushes.
|
|
17
|
+
*/
|
|
18
|
+
/** Largest single screen-data value the API accepts (base64 pixels). */
|
|
19
|
+
const MAX_VALUE = 220_000;
|
|
20
|
+
const MAX_COLORS = 255;
|
|
21
|
+
const MAX_FRAMES = 16;
|
|
22
|
+
/** Encode RGBA frames as `pixel-image` screen data. Throws with a fix-it message. */
|
|
23
|
+
function encodePixelArt(art) {
|
|
24
|
+
const { width, height, frames } = art;
|
|
25
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1)
|
|
26
|
+
throw new Error('Pixel art needs a positive whole-number width and height.');
|
|
27
|
+
if (width > 192 || height > 192)
|
|
28
|
+
throw new Error(`Pixel art is ${width}×${height}; keep it within 192×192 (6×6 tiles).`);
|
|
29
|
+
if (frames.length < 1 || frames.length > MAX_FRAMES)
|
|
30
|
+
throw new Error(`Pixel art needs 1–${MAX_FRAMES} frames (got ${frames.length}).`);
|
|
31
|
+
const size = width * height;
|
|
32
|
+
for (const frame of frames)
|
|
33
|
+
if (frame.length !== size * 4)
|
|
34
|
+
throw new Error(`Each frame must be ${width}×${height} RGBA (${size * 4} bytes).`);
|
|
35
|
+
// Index 0 is reserved for transparency when any pixel is see-through.
|
|
36
|
+
const transparent = frames.some((f) => {
|
|
37
|
+
for (let i = 3; i < f.length; i += 4)
|
|
38
|
+
if (f[i] < 128)
|
|
39
|
+
return true;
|
|
40
|
+
return false;
|
|
41
|
+
});
|
|
42
|
+
const colors = new Map();
|
|
43
|
+
const palette = transparent ? [0, 0, 0] : [];
|
|
44
|
+
const pixels = Buffer.alloc(size * frames.length);
|
|
45
|
+
frames.forEach((frame, f) => {
|
|
46
|
+
for (let i = 0; i < size; i++) {
|
|
47
|
+
const r = frame[i * 4], g = frame[i * 4 + 1], b = frame[i * 4 + 2];
|
|
48
|
+
if (transparent && frame[i * 4 + 3] < 128) {
|
|
49
|
+
pixels[f * size + i] = 0;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const key = (r << 16) | (g << 8) | b;
|
|
53
|
+
let index = colors.get(key);
|
|
54
|
+
if (index === undefined) {
|
|
55
|
+
index = palette.length / 3;
|
|
56
|
+
if (index > MAX_COLORS)
|
|
57
|
+
throw new Error(`Pixel art uses more than ${MAX_COLORS} colours; reduce the palette (pixel art usually needs 8–32).`);
|
|
58
|
+
colors.set(key, index);
|
|
59
|
+
palette.push(r, g, b);
|
|
60
|
+
}
|
|
61
|
+
pixels[f * size + i] = index;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
const tokens = [`idx8`, `${width}x${height}`, `p${palette.length / 3}`];
|
|
65
|
+
if (transparent)
|
|
66
|
+
tokens.push('t0');
|
|
67
|
+
if (frames.length > 1) {
|
|
68
|
+
const fps = Math.min(12, Math.max(1, Math.round(art.fps ?? 4)));
|
|
69
|
+
tokens.push(`f${frames.length}`, `fps${fps}`);
|
|
70
|
+
}
|
|
71
|
+
const encoded = pixels.toString('base64');
|
|
72
|
+
if (encoded.length > MAX_VALUE)
|
|
73
|
+
throw new Error('Pixel art is too large to send; use fewer frames or a smaller size.');
|
|
74
|
+
return {
|
|
75
|
+
layout: 'pixel-image',
|
|
76
|
+
values: [
|
|
77
|
+
{ label: 'fmt', value: tokens.join(';') },
|
|
78
|
+
{ label: 'palette', value: Buffer.from(palette).toString('base64') },
|
|
79
|
+
{ label: 'pixels', value: encoded },
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Load a PNG (path or bytes) as pixel-device screen data. For animation, stack frames
|
|
85
|
+
* top to bottom in one PNG and pass `frameHeight`.
|
|
86
|
+
*/
|
|
87
|
+
function pixelArtFromPng(png, opts = {}) {
|
|
88
|
+
const image = pngjs_1.PNG.sync.read(typeof png === 'string' ? (0, node_fs_1.readFileSync)(png) : png);
|
|
89
|
+
const frameHeight = opts.frameHeight ?? image.height;
|
|
90
|
+
if (frameHeight < 1 || image.height % frameHeight !== 0)
|
|
91
|
+
throw new Error(`PNG height ${image.height} is not a whole number of ${frameHeight}px frames.`);
|
|
92
|
+
const frameBytes = image.width * frameHeight * 4;
|
|
93
|
+
const frames = [];
|
|
94
|
+
for (let offset = 0; offset < image.data.length; offset += frameBytes)
|
|
95
|
+
frames.push(image.data.subarray(offset, offset + frameBytes));
|
|
96
|
+
return encodePixelArt({ width: image.width, height: frameHeight, frames, fps: opts.fps });
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Companion declaration for a pixel device: `size` in tiles (art is 32px per tile).
|
|
100
|
+
* Freestanding by default so it sits on the floor next to the NPC's home.
|
|
101
|
+
*/
|
|
102
|
+
function pixelDevice(opts) {
|
|
103
|
+
return {
|
|
104
|
+
screenType: 'pixel-device',
|
|
105
|
+
size: { w: opts.w, h: opts.h },
|
|
106
|
+
placement: opts.placement ?? 'freestanding',
|
|
107
|
+
...(opts.label ? { label: opts.label } : {}),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=pixel-art.js.map
|