@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.
Files changed (105) hide show
  1. package/README.md +55 -17
  2. package/dist/cjs/cli.js +94 -39
  3. package/dist/cjs/core/base-npc.js +15 -0
  4. package/dist/cjs/core/config.js +19 -2
  5. package/dist/cjs/core/errors.js +23 -0
  6. package/dist/cjs/core/http.js +121 -0
  7. package/dist/cjs/core/pixel-art.js +110 -0
  8. package/dist/cjs/core/polling-npc.js +99 -14
  9. package/dist/cjs/core/text.js +13 -0
  10. package/dist/cjs/core/thresholds.js +29 -0
  11. package/dist/cjs/index.js +21 -2
  12. package/dist/cjs/npcs/agent-common/session.js +1 -0
  13. package/dist/cjs/npcs/agent-common/shim.js +8 -2
  14. package/dist/cjs/npcs/github-watcher/defaults.js +7 -1
  15. package/dist/cjs/npcs/github-watcher/github-api.js +82 -45
  16. package/dist/cjs/npcs/github-watcher/index.js +82 -36
  17. package/dist/cjs/npcs/internet-connectivity/checks.js +70 -67
  18. package/dist/cjs/npcs/internet-connectivity/defaults.js +5 -2
  19. package/dist/cjs/npcs/internet-connectivity/index.js +134 -131
  20. package/dist/cjs/npcs/log-watcher/buffer.js +61 -0
  21. package/dist/cjs/npcs/log-watcher/index.js +109 -103
  22. package/dist/cjs/npcs/log-watcher/tail.js +100 -53
  23. package/dist/cjs/npcs/office-characters/index.js +92 -0
  24. package/dist/cjs/npcs/process-watcher/index.js +60 -51
  25. package/dist/cjs/npcs/process-watcher/process-scanner.js +100 -68
  26. package/dist/cjs/npcs/receptionist/defaults.js +4 -0
  27. package/dist/cjs/npcs/receptionist/index.js +6 -93
  28. package/dist/cjs/npcs/receptionist/weather.js +42 -47
  29. package/dist/cjs/npcs/security/index.js +26 -0
  30. package/dist/cjs/npcs/system-monitor/collectors.js +66 -17
  31. package/dist/cjs/npcs/system-monitor/index.js +52 -28
  32. package/dist/cjs/npcs/web-page-watcher/basic-mode.js +30 -14
  33. package/dist/cjs/npcs/web-page-watcher/capture.js +23 -1
  34. package/dist/cjs/npcs/web-page-watcher/defaults.js +44 -2
  35. package/dist/cjs/npcs/web-page-watcher/index.js +119 -68
  36. package/dist/cjs/npcs/web-page-watcher/pixelate.js +2 -1
  37. package/dist/cli.js +95 -40
  38. package/dist/core/base-npc.d.ts +5 -0
  39. package/dist/core/base-npc.js +15 -0
  40. package/dist/core/config.d.ts +5 -0
  41. package/dist/core/config.js +18 -2
  42. package/dist/core/errors.d.ts +11 -0
  43. package/dist/core/errors.js +18 -0
  44. package/dist/core/http.d.ts +52 -0
  45. package/dist/core/http.js +111 -0
  46. package/dist/core/pixel-art.d.ts +38 -0
  47. package/dist/core/pixel-art.js +105 -0
  48. package/dist/core/polling-npc.d.ts +22 -1
  49. package/dist/core/polling-npc.js +98 -13
  50. package/dist/core/text.d.ts +6 -0
  51. package/dist/core/text.js +12 -0
  52. package/dist/core/thresholds.d.ts +9 -1
  53. package/dist/core/thresholds.js +27 -0
  54. package/dist/core/types.d.ts +5 -0
  55. package/dist/index.d.ts +7 -2
  56. package/dist/index.js +6 -2
  57. package/dist/npcs/agent-common/session.js +1 -0
  58. package/dist/npcs/agent-common/shim.js +8 -2
  59. package/dist/npcs/github-watcher/defaults.d.ts +6 -0
  60. package/dist/npcs/github-watcher/defaults.js +7 -1
  61. package/dist/npcs/github-watcher/github-api.d.ts +21 -7
  62. package/dist/npcs/github-watcher/github-api.js +80 -45
  63. package/dist/npcs/github-watcher/index.d.ts +6 -0
  64. package/dist/npcs/github-watcher/index.js +83 -37
  65. package/dist/npcs/internet-connectivity/checks.d.ts +15 -18
  66. package/dist/npcs/internet-connectivity/checks.js +70 -67
  67. package/dist/npcs/internet-connectivity/defaults.d.ts +3 -0
  68. package/dist/npcs/internet-connectivity/defaults.js +5 -2
  69. package/dist/npcs/internet-connectivity/index.d.ts +19 -6
  70. package/dist/npcs/internet-connectivity/index.js +135 -133
  71. package/dist/npcs/log-watcher/buffer.d.ts +22 -0
  72. package/dist/npcs/log-watcher/buffer.js +58 -0
  73. package/dist/npcs/log-watcher/index.d.ts +22 -21
  74. package/dist/npcs/log-watcher/index.js +112 -106
  75. package/dist/npcs/log-watcher/tail.d.ts +23 -3
  76. package/dist/npcs/log-watcher/tail.js +101 -54
  77. package/dist/npcs/office-characters/index.d.ts +50 -0
  78. package/dist/npcs/office-characters/index.js +82 -0
  79. package/dist/npcs/process-watcher/index.d.ts +1 -1
  80. package/dist/npcs/process-watcher/index.js +61 -52
  81. package/dist/npcs/process-watcher/process-scanner.d.ts +31 -1
  82. package/dist/npcs/process-watcher/process-scanner.js +97 -70
  83. package/dist/npcs/receptionist/defaults.d.ts +4 -0
  84. package/dist/npcs/receptionist/defaults.js +4 -0
  85. package/dist/npcs/receptionist/index.d.ts +3 -12
  86. package/dist/npcs/receptionist/index.js +6 -93
  87. package/dist/npcs/receptionist/weather.d.ts +8 -6
  88. package/dist/npcs/receptionist/weather.js +39 -45
  89. package/dist/npcs/security/index.d.ts +12 -0
  90. package/dist/npcs/security/index.js +22 -0
  91. package/dist/npcs/system-monitor/collectors.d.ts +15 -3
  92. package/dist/npcs/system-monitor/collectors.js +64 -17
  93. package/dist/npcs/system-monitor/defaults.d.ts +1 -1
  94. package/dist/npcs/system-monitor/index.d.ts +2 -1
  95. package/dist/npcs/system-monitor/index.js +54 -30
  96. package/dist/npcs/web-page-watcher/basic-mode.d.ts +2 -2
  97. package/dist/npcs/web-page-watcher/basic-mode.js +30 -14
  98. package/dist/npcs/web-page-watcher/capture.d.ts +18 -0
  99. package/dist/npcs/web-page-watcher/capture.js +21 -1
  100. package/dist/npcs/web-page-watcher/defaults.d.ts +23 -0
  101. package/dist/npcs/web-page-watcher/defaults.js +41 -1
  102. package/dist/npcs/web-page-watcher/index.d.ts +18 -1
  103. package/dist/npcs/web-page-watcher/index.js +121 -70
  104. package/dist/npcs/web-page-watcher/pixelate.js +2 -1
  105. 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 | 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` | Time and weather at the entrance, greets players | `nocville-npc receptionist --location="Berlin"` |
150
- | `github-watcher` | New commits on a commit board | `nocville-npc github-watcher --repo=owner/name` |
151
- | `web-page-watcher` | A web page rendered as a pixel-art TV | `nocville-npc web-page-watcher --url=https://…` |
152
-
153
- Run `nocville-npc --help` to see every flag. A few notes:
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
- - **receptionist**: weather comes from the keyless open-meteo API. Set the place with
157
- `--lat=52.5 --lon=13.4` or `--location=…` (otherwise it uses IP geolocation). Add
158
- `--fahrenheit` for °F.
159
- - **github-watcher**: uses ETag polling. Pass `--token=$GITHUB_TOKEN` for a higher rate limit.
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 Greeter showing time + local weather
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 hosts for latency checks
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 process names to watch
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
- Receptionist Options:
134
- --poll-interval=<ms> Clock tick in ms (default: 60000)
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 (or GITHUB_TOKEN env)
143
- --poll-interval=<ms> Poll interval in ms (default: 60000)
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> Page to watch (required)
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; e.g. 160x100)
151
- --wait-until=<event> Puppeteer waitUntil (default: networkidle2)
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
- pollInterval: (0, config_js_1.numberFlag)(parsed.flags, 'poll-interval'),
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 processesStr = (0, config_js_1.stringFlag)(parsed.flags, 'processes');
335
- if (!processesStr) {
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: processesStr.split(','),
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 filesStr = (0, config_js_1.stringFlag)(parsed.flags, 'files');
353
- if (!filesStr) {
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: filesStr.split(','),
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: (0, config_js_1.stringFlag)(parsed.flags, 'token') ?? process.env['GITHUB_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 = (v) => v ? (parseDimensions(v) ?? undefined) : undefined;
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 modeFlag = (0, config_js_1.stringFlag)(parsed.flags, 'mode');
430
- const mode = modeFlag === 'auto' || modeFlag === 'screenshot' || modeFlag === 'basic'
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
- mode,
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
  }
@@ -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 (!Number.isNaN(n))
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