@nocville/npc 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -140,23 +140,24 @@ 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` | 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://…` |
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
+ | `slightly-inappropriate-hr` | People skills. Questionable discretion. | `nocville-npc slightly-inappropriate-hr` |
159
+ | `github-watcher` | New commits on a commit board | `nocville-npc github-watcher --repo=owner/name` |
160
+ | `web-page-watcher` | A web page rendered as a pixel-art TV | `nocville-npc web-page-watcher --url=https://…` |
160
161
 
161
162
  Run `nocville-npc --help` to see every flag. Poll intervals below 5 seconds are raised to 5
162
163
  seconds. A few notes:
@@ -184,7 +185,7 @@ seconds. A few notes:
184
185
  exterior doors for 15–30 seconds. Does not claim a desk. An explicit work-room rule
185
186
  still confines its route, so leave it unassigned for office-wide rounds.
186
187
  - **office characters**: `janitor`, `barista`, `office-cat`, `office-dog`,
187
- `plant-caretaker`, `it-technician` and `out-of-touch-executive` run without screens or
188
+ `plant-caretaker`, `it-technician`, `out-of-touch-executive`, and `slightly-inappropriate-hr` run without screens or
188
189
  external service credentials. They share the usual API key, name, service ID and map
189
190
  options. Run each as its own process. Their routes and conversation are synchronized by
190
191
  the server, and work-room rules apply. Missing furniture or coworkers cause quiet walks
package/dist/cjs/cli.js CHANGED
@@ -91,6 +91,7 @@ Monitoring NPC types:
91
91
  plant-caretaker Waters plants and reviews their performance
92
92
  it-technician Inspects tech gear and recommends reboots
93
93
  out-of-touch-executive Interrupts coworkers between long lounge breaks
94
+ slightly-inappropriate-hr Desk-based HR with questionable people skills
94
95
  github-watcher Call out new commits on a repo
95
96
  web-page-watcher Render a watched web page as a pixel-art TV
96
97
 
@@ -417,7 +418,8 @@ async function main() {
417
418
  case 'office-dog':
418
419
  case 'plant-caretaker':
419
420
  case 'it-technician':
420
- case 'out-of-touch-executive': {
421
+ case 'out-of-touch-executive':
422
+ case 'slightly-inappropriate-hr': {
421
423
  if (!config.serviceId)
422
424
  config.serviceId = parsed.command;
423
425
  const { OFFICE_CHARACTER_NPCS } = await Promise.resolve().then(() => __importStar(require('./npcs/office-characters/index.js')));
@@ -23,8 +23,10 @@ class PollingNPC extends base_npc_js_1.BaseNPC {
23
23
  lastPushed = null;
24
24
  lastPushAt = 0;
25
25
  pollInterval;
26
+ onCollection;
26
27
  constructor(config) {
27
28
  super(config);
29
+ this.onCollection = config.onCollection;
28
30
  const requested = config.pollInterval ?? 15000;
29
31
  this.pollInterval = Number.isFinite(requested)
30
32
  ? Math.max(exports.MIN_POLL_INTERVAL_MS, requested)
@@ -117,6 +119,12 @@ class PollingNPC extends base_npc_js_1.BaseNPC {
117
119
  // Stopped mid-collection: never push after (or race) the offline push.
118
120
  if (this.state !== 'running')
119
121
  return;
122
+ try {
123
+ this.onCollection?.(metrics);
124
+ }
125
+ catch {
126
+ /* Diagnostics cannot stop publishing. */
127
+ }
120
128
  await this.pushMetrics(metrics);
121
129
  this.lastPushed = metrics;
122
130
  this.lastPushAt = Date.now();
package/dist/cjs/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ReceptionistNPC = exports.LogWatcherNPC = exports.ProcessWatcherNPC = exports.InternetConnectivityNPC = exports.ensureSessionFilterTemplate = exports.sessionPublishDecision = exports.sessionFilterPath = exports.loadSessionFilter = exports.pixelDevice = exports.pixelArtFromPng = exports.encodePixelArt = exports.userConfigPath = exports.saveUserConfig = exports.loadUserConfig = exports.redactSecrets = exports.shortPath = exports.shortCommand = exports.describeToolUse = exports.CodexWatcher = exports.parseRolloutLine = exports.uninstallCodexHooks = exports.setupCodexHooks = exports.codexTransition = exports.handleCodexHookEvent = exports.handleCodexCommand = exports.claudeTransition = exports.processHookEvent = exports.uninstallHooks = exports.setupHooks = exports.handleHookEvent = exports.handleClaudeCodeCommand = exports.ClaudeCodeNPC = exports.SystemMonitorNPC = exports.pushHistory = exports.statusColor = exports.NpcConfigError = exports.sanitizeText = exports.truncate = exports.moodForStatus = exports.worstStatus = exports.evaluateThreshold = exports.numberFlag = exports.boolFlag = exports.stringFlag = exports.resolveConfig = exports.parseArgs = exports.Logger = exports.EventNPC = exports.PollingNPC = exports.BaseNPC = void 0;
4
- exports.OFFICE_CHARACTER_NPCS = exports.OutOfTouchExecutiveNPC = exports.ITTechnicianNPC = exports.PlantCaretakerNPC = exports.OfficeDogNPC = exports.OfficeCatNPC = exports.BaristaNPC = exports.JanitorNPC = exports.SecurityNPC = exports.normalizeCursorEvent = exports.isCursorInvocation = exports.CURSOR_HOOK_EVENTS = exports.installCursorHooksInto = exports.uninstallCursorHooks = exports.setupCursorHooks = exports.handleCursorHookEvent = exports.cursorServiceId = exports.CURSOR_ADAPTER = exports.CURSOR_DEFAULTS = exports.GEMINI_HOOK_EVENTS = exports.installGeminiHooksInto = exports.uninstallGeminiHooks = exports.setupGeminiHooks = exports.handleGeminiHookEvent = exports.geminiServiceId = exports.geminiTransition = exports.GEMINI_ADAPTER = exports.GEMINI_DEFAULTS = exports.WebPageWatcherNPC = exports.GitHubWatcherNPC = void 0;
4
+ exports.OFFICE_CHARACTER_NPCS = exports.SlightlyInappropriateHRNPC = exports.OutOfTouchExecutiveNPC = exports.ITTechnicianNPC = exports.PlantCaretakerNPC = exports.OfficeDogNPC = exports.OfficeCatNPC = exports.BaristaNPC = exports.JanitorNPC = exports.SecurityNPC = exports.normalizeCursorEvent = exports.isCursorInvocation = exports.CURSOR_HOOK_EVENTS = exports.installCursorHooksInto = exports.uninstallCursorHooks = exports.setupCursorHooks = exports.handleCursorHookEvent = exports.cursorServiceId = exports.CURSOR_ADAPTER = exports.CURSOR_DEFAULTS = exports.GEMINI_HOOK_EVENTS = exports.installGeminiHooksInto = exports.uninstallGeminiHooks = exports.setupGeminiHooks = exports.handleGeminiHookEvent = exports.geminiServiceId = exports.geminiTransition = exports.GEMINI_ADAPTER = exports.GEMINI_DEFAULTS = exports.WebPageWatcherNPC = exports.GitHubWatcherNPC = void 0;
5
5
  // Core
6
6
  var base_npc_js_1 = require("./core/base-npc.js");
7
7
  Object.defineProperty(exports, "BaseNPC", { enumerable: true, get: function () { return base_npc_js_1.BaseNPC; } });
@@ -110,5 +110,6 @@ Object.defineProperty(exports, "OfficeDogNPC", { enumerable: true, get: function
110
110
  Object.defineProperty(exports, "PlantCaretakerNPC", { enumerable: true, get: function () { return index_js_13.PlantCaretakerNPC; } });
111
111
  Object.defineProperty(exports, "ITTechnicianNPC", { enumerable: true, get: function () { return index_js_13.ITTechnicianNPC; } });
112
112
  Object.defineProperty(exports, "OutOfTouchExecutiveNPC", { enumerable: true, get: function () { return index_js_13.OutOfTouchExecutiveNPC; } });
113
+ Object.defineProperty(exports, "SlightlyInappropriateHRNPC", { enumerable: true, get: function () { return index_js_13.SlightlyInappropriateHRNPC; } });
113
114
  Object.defineProperty(exports, "OFFICE_CHARACTER_NPCS", { enumerable: true, get: function () { return index_js_13.OFFICE_CHARACTER_NPCS; } });
114
115
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fetchCommits = fetchCommits;
4
+ exports.commitsWithin = commitsWithin;
5
+ exports.relativeTime = relativeTime;
6
+ exports.newCommitsSince = newCommitsSince;
7
+ function toNum(v) {
8
+ if (v === null)
9
+ return null;
10
+ const n = Number(v);
11
+ return Number.isNaN(n) ? null : n;
12
+ }
13
+ /**
14
+ * Fetch recent commits from the GitHub REST API using conditional requests.
15
+ * Passing the previous ETag yields a 304 (which does not count against the
16
+ * unauthenticated 60/hour limit) when nothing has changed. Never throws —
17
+ * network errors surface as `status: 0`.
18
+ */
19
+ async function fetchCommits(repo, opts = {}) {
20
+ const perPage = opts.perPage ?? 10;
21
+ const url = `https://api.github.com/repos/${repo}/commits?per_page=${perPage}`;
22
+ const headers = {
23
+ Accept: 'application/vnd.github+json',
24
+ 'User-Agent': 'nocville-npc',
25
+ 'X-GitHub-Api-Version': '2022-11-28',
26
+ };
27
+ if (opts.token)
28
+ headers['Authorization'] = `Bearer ${opts.token}`;
29
+ if (opts.etag)
30
+ headers['If-None-Match'] = opts.etag;
31
+ const controller = new AbortController();
32
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 5000);
33
+ try {
34
+ const res = await fetch(url, { headers, signal: controller.signal });
35
+ const rateRemaining = toNum(res.headers.get('x-ratelimit-remaining'));
36
+ const etag = res.headers.get('etag');
37
+ if (res.status === 304) {
38
+ return { notModified: true, commits: [], etag: opts.etag ?? null, rateRemaining, status: 304 };
39
+ }
40
+ if (!res.ok) {
41
+ // Drain the body so the socket can be reused.
42
+ await res.text().catch(() => undefined);
43
+ return { notModified: false, commits: [], etag, rateRemaining, status: res.status };
44
+ }
45
+ const body = (await res.json());
46
+ const commits = body.map((c) => parseCommit(c));
47
+ return { notModified: false, commits, etag, rateRemaining, status: res.status };
48
+ }
49
+ catch {
50
+ return { notModified: false, commits: [], etag: null, rateRemaining: null, status: 0 };
51
+ }
52
+ finally {
53
+ clearTimeout(timer);
54
+ }
55
+ }
56
+ function parseCommit(c) {
57
+ const commit = c['commit'] ?? {};
58
+ const commitAuthor = commit['author'] ?? {};
59
+ const ghAuthor = c['author'] ?? {};
60
+ const message = String(commit['message'] ?? '').split('\n')[0] ?? '';
61
+ return {
62
+ sha: String(c['sha'] ?? ''),
63
+ author: String(commitAuthor['name'] ?? ghAuthor['login'] ?? 'unknown'),
64
+ message,
65
+ date: String(commitAuthor['date'] ?? ''),
66
+ };
67
+ }
68
+ /** Count how many commits landed within the last `hours`. */
69
+ function commitsWithin(commits, hours, now = Date.now()) {
70
+ const cutoff = now - hours * 3600_000;
71
+ return commits.filter((c) => {
72
+ const t = Date.parse(c.date);
73
+ return Number.isFinite(t) && t >= cutoff;
74
+ }).length;
75
+ }
76
+ /** Short relative-time string like "3m ago", "2h ago", "1d ago". */
77
+ function relativeTime(iso, now = Date.now()) {
78
+ const t = Date.parse(iso);
79
+ if (!Number.isFinite(t))
80
+ return 'unknown';
81
+ const secs = Math.max(0, Math.round((now - t) / 1000));
82
+ if (secs < 60)
83
+ return `${secs}s ago`;
84
+ const mins = Math.round(secs / 60);
85
+ if (mins < 60)
86
+ return `${mins}m ago`;
87
+ const hrs = Math.round(mins / 60);
88
+ if (hrs < 24)
89
+ return `${hrs}h ago`;
90
+ const days = Math.round(hrs / 24);
91
+ return `${days}d ago`;
92
+ }
93
+ /**
94
+ * Return the commits newer than `lastSeenSha`, newest-first. When the sha isn't
95
+ * present in the window (more than a page of new commits, or rewritten history)
96
+ * every fetched commit is treated as new. An undefined sha means "not seeded
97
+ * yet" — returns empty so the caller can seed without a spurious announcement.
98
+ */
99
+ function newCommitsSince(commits, lastSeenSha) {
100
+ if (!lastSeenSha)
101
+ return [];
102
+ const idx = commits.findIndex((c) => c.sha === lastSeenSha);
103
+ if (idx === 0)
104
+ return []; // newest is already seen
105
+ if (idx === -1)
106
+ return commits; // fell out of the window — treat all as new
107
+ return commits.slice(0, idx);
108
+ }
109
+ //# sourceMappingURL=github.js.map
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.OFFICE_CHARACTER_NPCS = exports.OutOfTouchExecutiveNPC = exports.ITTechnicianNPC = exports.PlantCaretakerNPC = exports.OfficeDogNPC = exports.OfficeCatNPC = exports.BaristaNPC = exports.JanitorNPC = void 0;
3
+ exports.OFFICE_CHARACTER_NPCS = exports.SlightlyInappropriateHRNPC = exports.OutOfTouchExecutiveNPC = exports.ITTechnicianNPC = exports.PlantCaretakerNPC = exports.OfficeDogNPC = exports.OfficeCatNPC = exports.BaristaNPC = exports.JanitorNPC = void 0;
4
4
  const polling_npc_js_1 = require("../../core/polling-npc.js");
5
5
  /** Quiet heartbeats; the authoritative server drives routines and conversation. */
6
6
  class OfficeCharacterNPC extends polling_npc_js_1.PollingNPC {
@@ -80,6 +80,15 @@ class OutOfTouchExecutiveNPC extends OfficeCharacterNPC {
80
80
  }
81
81
  }
82
82
  exports.OutOfTouchExecutiveNPC = OutOfTouchExecutiveNPC;
83
+ class SlightlyInappropriateHRNPC extends OfficeCharacterNPC {
84
+ get name() {
85
+ return 'slightly-inappropriate-hr';
86
+ }
87
+ get defaultDisplayName() {
88
+ return 'Slightly Inappropriate HR';
89
+ }
90
+ }
91
+ exports.SlightlyInappropriateHRNPC = SlightlyInappropriateHRNPC;
83
92
  exports.OFFICE_CHARACTER_NPCS = {
84
93
  janitor: JanitorNPC,
85
94
  barista: BaristaNPC,
@@ -88,5 +97,6 @@ exports.OFFICE_CHARACTER_NPCS = {
88
97
  'plant-caretaker': PlantCaretakerNPC,
89
98
  'it-technician': ITTechnicianNPC,
90
99
  'out-of-touch-executive': OutOfTouchExecutiveNPC,
100
+ 'slightly-inappropriate-hr': SlightlyInappropriateHRNPC,
91
101
  };
92
102
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractTitle = extractTitle;
4
+ exports.statusFromHttp = statusFromHttp;
5
+ exports.basicCheck = basicCheck;
6
+ exports.hostnameOf = hostnameOf;
7
+ /** Extract the <title> text from an HTML string (empty if none). */
8
+ function extractTitle(html) {
9
+ const m = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
10
+ return m ? m[1].replace(/\s+/g, ' ').trim().slice(0, 80) : '';
11
+ }
12
+ /** Map an HTTP status to NPC status: 2xx/3xx ok, 4xx/timeout warn, 5xx/refused crit. */
13
+ function statusFromHttp(code, timedOut = false) {
14
+ if (code >= 200 && code < 400)
15
+ return 'healthy';
16
+ if (code >= 400 && code < 500)
17
+ return 'warning';
18
+ if (code >= 500)
19
+ return 'critical';
20
+ return timedOut ? 'warning' : 'critical'; // 0 = network error
21
+ }
22
+ /** Fetch a page and extract HTTP status + <title> (basic, no browser). */
23
+ async function basicCheck(url, timeoutMs = 8000) {
24
+ const controller = new AbortController();
25
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
26
+ try {
27
+ const res = await fetch(url, {
28
+ signal: controller.signal,
29
+ headers: { 'User-Agent': 'nocville-npc' },
30
+ redirect: 'follow',
31
+ });
32
+ let title = '';
33
+ try {
34
+ title = extractTitle(await res.text());
35
+ }
36
+ catch {
37
+ // body unavailable; leave title blank
38
+ }
39
+ return { status: res.status, title, ok: res.ok, timedOut: false };
40
+ }
41
+ catch (err) {
42
+ const timedOut = err instanceof Error && err.name === 'AbortError';
43
+ return { status: 0, title: '', ok: false, timedOut };
44
+ }
45
+ finally {
46
+ clearTimeout(timer);
47
+ }
48
+ }
49
+ /** Best-effort hostname for a URL, falling back to the raw string. */
50
+ function hostnameOf(url) {
51
+ try {
52
+ return new URL(url).hostname;
53
+ }
54
+ catch {
55
+ return url.slice(0, 40);
56
+ }
57
+ }
58
+ //# sourceMappingURL=basic.js.map
@@ -5,6 +5,7 @@ exports.chromeArgs = chromeArgs;
5
5
  exports.runningAsRoot = runningAsRoot;
6
6
  exports.loadPuppeteer = loadPuppeteer;
7
7
  exports.browserConnected = browserConnected;
8
+ const node_path_1 = require("node:path");
8
9
  const node_fs_1 = require("node:fs");
9
10
  const node_child_process_1 = require("node:child_process");
10
11
  const MAC_PATHS = [
@@ -50,6 +51,25 @@ function findChrome(explicit) {
50
51
  return p;
51
52
  return null;
52
53
  }
54
+ if (process.platform === 'win32') {
55
+ const roots = [
56
+ process.env['LOCALAPPDATA'],
57
+ process.env['PROGRAMFILES'],
58
+ process.env['PROGRAMFILES(X86)'],
59
+ ].filter((p) => !!p);
60
+ for (const root of roots)
61
+ for (const relative of [
62
+ ['Google', 'Chrome', 'Application', 'chrome.exe'],
63
+ ['Microsoft', 'Edge', 'Application', 'msedge.exe'],
64
+ ['Chromium', 'Application', 'chrome.exe'],
65
+ ['BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'],
66
+ ]) {
67
+ const path = (0, node_path_1.join)(root, ...relative);
68
+ if ((0, node_fs_1.existsSync)(path))
69
+ return path;
70
+ }
71
+ return null;
72
+ }
53
73
  if (process.platform === 'linux') {
54
74
  for (const bin of LINUX_BINARIES) {
55
75
  const resolved = which(bin);
@@ -83,6 +103,12 @@ function runningAsRoot() {
83
103
  */
84
104
  async function loadPuppeteer() {
85
105
  try {
106
+ // Prefer CommonJS in bundled runtimes so Electron resolves through its
107
+ // ASAR loader. Native ESM installations use the optional dynamic import below.
108
+ if (typeof require === 'function') {
109
+ const mod = require('puppeteer-core');
110
+ return mod.default ?? mod;
111
+ }
86
112
  const dynImport = new Function('m', 'return import(m)');
87
113
  const mod = await dynImport('puppeteer-core');
88
114
  return (mod.default ?? mod);
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.findChrome = findChrome;
4
+ exports.loadPuppeteer = loadPuppeteer;
5
+ exports.browserConnected = browserConnected;
6
+ const node_fs_1 = require("node:fs");
7
+ const CHROME_PATHS = {
8
+ darwin: [
9
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
10
+ '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
11
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
12
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
13
+ ],
14
+ linux: [
15
+ '/usr/bin/google-chrome',
16
+ '/usr/bin/google-chrome-stable',
17
+ '/usr/bin/chromium',
18
+ '/usr/bin/chromium-browser',
19
+ '/usr/bin/microsoft-edge',
20
+ '/snap/bin/chromium',
21
+ ],
22
+ };
23
+ /** Find a usable Chrome/Chromium executable: explicit path → env → known paths. */
24
+ function findChrome(explicit) {
25
+ const envPath = process.env['CHROME_PATH'];
26
+ const candidates = [
27
+ ...(explicit ? [explicit] : []),
28
+ ...(envPath ? [envPath] : []),
29
+ ...(CHROME_PATHS[process.platform] ?? []),
30
+ ];
31
+ for (const p of candidates) {
32
+ if (p && (0, node_fs_1.existsSync)(p))
33
+ return p;
34
+ }
35
+ return null;
36
+ }
37
+ /**
38
+ * Load the optional `puppeteer-core` peer dependency without a static import,
39
+ * so the package builds and runs when it isn't installed. Returns null if the
40
+ * module is absent.
41
+ */
42
+ async function loadPuppeteer() {
43
+ try {
44
+ // Indirect dynamic import so TypeScript/bundlers don't require the module
45
+ // at build time.
46
+ const dynImport = new Function('m', 'return import(m)');
47
+ const mod = await dynImport('puppeteer-core');
48
+ return (mod.default ?? mod);
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ /** True if the browser reports a live connection. */
55
+ function browserConnected(browser) {
56
+ if (typeof browser.connected === 'boolean')
57
+ return browser.connected;
58
+ if (typeof browser.isConnected === 'function')
59
+ return browser.isConnected();
60
+ return true;
61
+ }
62
+ //# sourceMappingURL=chrome.js.map
package/dist/cli.js CHANGED
@@ -56,6 +56,7 @@ Monitoring NPC types:
56
56
  plant-caretaker Waters plants and reviews their performance
57
57
  it-technician Inspects tech gear and recommends reboots
58
58
  out-of-touch-executive Interrupts coworkers between long lounge breaks
59
+ slightly-inappropriate-hr Desk-based HR with questionable people skills
59
60
  github-watcher Call out new commits on a repo
60
61
  web-page-watcher Render a watched web page as a pixel-art TV
61
62
 
@@ -382,7 +383,8 @@ async function main() {
382
383
  case 'office-dog':
383
384
  case 'plant-caretaker':
384
385
  case 'it-technician':
385
- case 'out-of-touch-executive': {
386
+ case 'out-of-touch-executive':
387
+ case 'slightly-inappropriate-hr': {
386
388
  if (!config.serviceId)
387
389
  config.serviceId = parsed.command;
388
390
  const { OFFICE_CHARACTER_NPCS } = await import('./npcs/office-characters/index.js');
@@ -2,6 +2,8 @@ import { BaseNPC } from './base-npc.js';
2
2
  import type { NpcConfig, CollectedMetrics } from './types.js';
3
3
  export interface PollingNpcConfig extends NpcConfig {
4
4
  pollInterval?: number;
5
+ /** Local diagnostics; never included in published payloads. */
6
+ onCollection?: (metrics: CollectedMetrics) => void;
5
7
  }
6
8
  /**
7
9
  * Lower bound for any poll interval, so a typo can't exhaust the API's
@@ -19,6 +21,7 @@ export declare abstract class PollingNPC extends BaseNPC {
19
21
  private lastPushed;
20
22
  private lastPushAt;
21
23
  protected pollInterval: number;
24
+ private onCollection?;
22
25
  constructor(config: PollingNpcConfig);
23
26
  /** Scale staleness to the poll cadence: the server marks unknown at ~2x the interval. */
24
27
  get heartbeatSeconds(): number;
@@ -20,8 +20,10 @@ export class PollingNPC extends BaseNPC {
20
20
  lastPushed = null;
21
21
  lastPushAt = 0;
22
22
  pollInterval;
23
+ onCollection;
23
24
  constructor(config) {
24
25
  super(config);
26
+ this.onCollection = config.onCollection;
25
27
  const requested = config.pollInterval ?? 15000;
26
28
  this.pollInterval = Number.isFinite(requested)
27
29
  ? Math.max(MIN_POLL_INTERVAL_MS, requested)
@@ -114,6 +116,12 @@ export class PollingNPC extends BaseNPC {
114
116
  // Stopped mid-collection: never push after (or race) the offline push.
115
117
  if (this.state !== 'running')
116
118
  return;
119
+ try {
120
+ this.onCollection?.(metrics);
121
+ }
122
+ catch {
123
+ /* Diagnostics cannot stop publishing. */
124
+ }
117
125
  await this.pushMetrics(metrics);
118
126
  this.lastPushed = metrics;
119
127
  this.lastPushAt = Date.now();
package/dist/index.d.ts CHANGED
@@ -42,5 +42,5 @@ export { CURSOR_DEFAULTS, CURSOR_ADAPTER, cursorServiceId, handleCursorHookEvent
42
42
  export type { CursorSetupOptions } from './npcs/cursor/index.js';
43
43
  export { isCursorInvocation, normalizeCursorEvent } from './npcs/cursor/normalize.js';
44
44
  export { SecurityNPC } from './npcs/security/index.js';
45
- export { JanitorNPC, BaristaNPC, OfficeCatNPC, OfficeDogNPC, PlantCaretakerNPC, ITTechnicianNPC, OutOfTouchExecutiveNPC, OFFICE_CHARACTER_NPCS, } from './npcs/office-characters/index.js';
45
+ export { JanitorNPC, BaristaNPC, OfficeCatNPC, OfficeDogNPC, PlantCaretakerNPC, ITTechnicianNPC, OutOfTouchExecutiveNPC, SlightlyInappropriateHRNPC, OFFICE_CHARACTER_NPCS, } from './npcs/office-characters/index.js';
46
46
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -26,5 +26,5 @@ export { GEMINI_DEFAULTS, GEMINI_ADAPTER, geminiTransition, geminiServiceId, han
26
26
  export { CURSOR_DEFAULTS, CURSOR_ADAPTER, cursorServiceId, handleCursorHookEvent, setupCursorHooks, uninstallCursorHooks, installCursorHooksInto, CURSOR_HOOK_EVENTS, } from './npcs/cursor/index.js';
27
27
  export { isCursorInvocation, normalizeCursorEvent } from './npcs/cursor/normalize.js';
28
28
  export { SecurityNPC } from './npcs/security/index.js';
29
- export { JanitorNPC, BaristaNPC, OfficeCatNPC, OfficeDogNPC, PlantCaretakerNPC, ITTechnicianNPC, OutOfTouchExecutiveNPC, OFFICE_CHARACTER_NPCS, } from './npcs/office-characters/index.js';
29
+ export { JanitorNPC, BaristaNPC, OfficeCatNPC, OfficeDogNPC, PlantCaretakerNPC, ITTechnicianNPC, OutOfTouchExecutiveNPC, SlightlyInappropriateHRNPC, OFFICE_CHARACTER_NPCS, } from './npcs/office-characters/index.js';
30
30
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,39 @@
1
+ export interface CommitInfo {
2
+ sha: string;
3
+ author: string;
4
+ message: string;
5
+ date: string;
6
+ }
7
+ export interface FetchCommitsResult {
8
+ /** 304 Not Modified — the ETag matched, nothing changed, no rate-limit cost. */
9
+ notModified: boolean;
10
+ commits: CommitInfo[];
11
+ etag: string | null;
12
+ rateRemaining: number | null;
13
+ status: number;
14
+ }
15
+ export interface FetchOptions {
16
+ token?: string;
17
+ etag?: string | null;
18
+ timeoutMs?: number;
19
+ perPage?: number;
20
+ }
21
+ /**
22
+ * Fetch recent commits from the GitHub REST API using conditional requests.
23
+ * Passing the previous ETag yields a 304 (which does not count against the
24
+ * unauthenticated 60/hour limit) when nothing has changed. Never throws —
25
+ * network errors surface as `status: 0`.
26
+ */
27
+ export declare function fetchCommits(repo: string, opts?: FetchOptions): Promise<FetchCommitsResult>;
28
+ /** Count how many commits landed within the last `hours`. */
29
+ export declare function commitsWithin(commits: CommitInfo[], hours: number, now?: number): number;
30
+ /** Short relative-time string like "3m ago", "2h ago", "1d ago". */
31
+ export declare function relativeTime(iso: string, now?: number): string;
32
+ /**
33
+ * Return the commits newer than `lastSeenSha`, newest-first. When the sha isn't
34
+ * present in the window (more than a page of new commits, or rewritten history)
35
+ * every fetched commit is treated as new. An undefined sha means "not seeded
36
+ * yet" — returns empty so the caller can seed without a spurious announcement.
37
+ */
38
+ export declare function newCommitsSince(commits: CommitInfo[], lastSeenSha: string | null): CommitInfo[];
39
+ //# sourceMappingURL=github.d.ts.map
@@ -0,0 +1,103 @@
1
+ function toNum(v) {
2
+ if (v === null)
3
+ return null;
4
+ const n = Number(v);
5
+ return Number.isNaN(n) ? null : n;
6
+ }
7
+ /**
8
+ * Fetch recent commits from the GitHub REST API using conditional requests.
9
+ * Passing the previous ETag yields a 304 (which does not count against the
10
+ * unauthenticated 60/hour limit) when nothing has changed. Never throws —
11
+ * network errors surface as `status: 0`.
12
+ */
13
+ export async function fetchCommits(repo, opts = {}) {
14
+ const perPage = opts.perPage ?? 10;
15
+ const url = `https://api.github.com/repos/${repo}/commits?per_page=${perPage}`;
16
+ const headers = {
17
+ Accept: 'application/vnd.github+json',
18
+ 'User-Agent': 'nocville-npc',
19
+ 'X-GitHub-Api-Version': '2022-11-28',
20
+ };
21
+ if (opts.token)
22
+ headers['Authorization'] = `Bearer ${opts.token}`;
23
+ if (opts.etag)
24
+ headers['If-None-Match'] = opts.etag;
25
+ const controller = new AbortController();
26
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 5000);
27
+ try {
28
+ const res = await fetch(url, { headers, signal: controller.signal });
29
+ const rateRemaining = toNum(res.headers.get('x-ratelimit-remaining'));
30
+ const etag = res.headers.get('etag');
31
+ if (res.status === 304) {
32
+ return { notModified: true, commits: [], etag: opts.etag ?? null, rateRemaining, status: 304 };
33
+ }
34
+ if (!res.ok) {
35
+ // Drain the body so the socket can be reused.
36
+ await res.text().catch(() => undefined);
37
+ return { notModified: false, commits: [], etag, rateRemaining, status: res.status };
38
+ }
39
+ const body = (await res.json());
40
+ const commits = body.map((c) => parseCommit(c));
41
+ return { notModified: false, commits, etag, rateRemaining, status: res.status };
42
+ }
43
+ catch {
44
+ return { notModified: false, commits: [], etag: null, rateRemaining: null, status: 0 };
45
+ }
46
+ finally {
47
+ clearTimeout(timer);
48
+ }
49
+ }
50
+ function parseCommit(c) {
51
+ const commit = c['commit'] ?? {};
52
+ const commitAuthor = commit['author'] ?? {};
53
+ const ghAuthor = c['author'] ?? {};
54
+ const message = String(commit['message'] ?? '').split('\n')[0] ?? '';
55
+ return {
56
+ sha: String(c['sha'] ?? ''),
57
+ author: String(commitAuthor['name'] ?? ghAuthor['login'] ?? 'unknown'),
58
+ message,
59
+ date: String(commitAuthor['date'] ?? ''),
60
+ };
61
+ }
62
+ /** Count how many commits landed within the last `hours`. */
63
+ export function commitsWithin(commits, hours, now = Date.now()) {
64
+ const cutoff = now - hours * 3600_000;
65
+ return commits.filter((c) => {
66
+ const t = Date.parse(c.date);
67
+ return Number.isFinite(t) && t >= cutoff;
68
+ }).length;
69
+ }
70
+ /** Short relative-time string like "3m ago", "2h ago", "1d ago". */
71
+ export function relativeTime(iso, now = Date.now()) {
72
+ const t = Date.parse(iso);
73
+ if (!Number.isFinite(t))
74
+ return 'unknown';
75
+ const secs = Math.max(0, Math.round((now - t) / 1000));
76
+ if (secs < 60)
77
+ return `${secs}s ago`;
78
+ const mins = Math.round(secs / 60);
79
+ if (mins < 60)
80
+ return `${mins}m ago`;
81
+ const hrs = Math.round(mins / 60);
82
+ if (hrs < 24)
83
+ return `${hrs}h ago`;
84
+ const days = Math.round(hrs / 24);
85
+ return `${days}d ago`;
86
+ }
87
+ /**
88
+ * Return the commits newer than `lastSeenSha`, newest-first. When the sha isn't
89
+ * present in the window (more than a page of new commits, or rewritten history)
90
+ * every fetched commit is treated as new. An undefined sha means "not seeded
91
+ * yet" — returns empty so the caller can seed without a spurious announcement.
92
+ */
93
+ export function newCommitsSince(commits, lastSeenSha) {
94
+ if (!lastSeenSha)
95
+ return [];
96
+ const idx = commits.findIndex((c) => c.sha === lastSeenSha);
97
+ if (idx === 0)
98
+ return []; // newest is already seen
99
+ if (idx === -1)
100
+ return commits; // fell out of the window — treat all as new
101
+ return commits.slice(0, idx);
102
+ }
103
+ //# sourceMappingURL=github.js.map
@@ -37,6 +37,10 @@ export declare class OutOfTouchExecutiveNPC extends OfficeCharacterNPC {
37
37
  get name(): 'out-of-touch-executive';
38
38
  protected get defaultDisplayName(): string;
39
39
  }
40
+ export declare class SlightlyInappropriateHRNPC extends OfficeCharacterNPC {
41
+ get name(): 'slightly-inappropriate-hr';
42
+ protected get defaultDisplayName(): string;
43
+ }
40
44
  export declare const OFFICE_CHARACTER_NPCS: {
41
45
  readonly janitor: typeof JanitorNPC;
42
46
  readonly barista: typeof BaristaNPC;
@@ -45,6 +49,7 @@ export declare const OFFICE_CHARACTER_NPCS: {
45
49
  readonly 'plant-caretaker': typeof PlantCaretakerNPC;
46
50
  readonly 'it-technician': typeof ITTechnicianNPC;
47
51
  readonly 'out-of-touch-executive': typeof OutOfTouchExecutiveNPC;
52
+ readonly 'slightly-inappropriate-hr': typeof SlightlyInappropriateHRNPC;
48
53
  };
49
54
  export {};
50
55
  //# sourceMappingURL=index.d.ts.map
@@ -70,6 +70,14 @@ export class OutOfTouchExecutiveNPC extends OfficeCharacterNPC {
70
70
  return 'Out-of-Touch Executive';
71
71
  }
72
72
  }
73
+ export class SlightlyInappropriateHRNPC extends OfficeCharacterNPC {
74
+ get name() {
75
+ return 'slightly-inappropriate-hr';
76
+ }
77
+ get defaultDisplayName() {
78
+ return 'Slightly Inappropriate HR';
79
+ }
80
+ }
73
81
  export const OFFICE_CHARACTER_NPCS = {
74
82
  janitor: JanitorNPC,
75
83
  barista: BaristaNPC,
@@ -78,5 +86,6 @@ export const OFFICE_CHARACTER_NPCS = {
78
86
  'plant-caretaker': PlantCaretakerNPC,
79
87
  'it-technician': ITTechnicianNPC,
80
88
  'out-of-touch-executive': OutOfTouchExecutiveNPC,
89
+ 'slightly-inappropriate-hr': SlightlyInappropriateHRNPC,
81
90
  };
82
91
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,16 @@
1
+ import type { Status } from '../../core/types.js';
2
+ export interface BasicCheck {
3
+ status: number;
4
+ title: string;
5
+ ok: boolean;
6
+ timedOut: boolean;
7
+ }
8
+ /** Extract the <title> text from an HTML string (empty if none). */
9
+ export declare function extractTitle(html: string): string;
10
+ /** Map an HTTP status to NPC status: 2xx/3xx ok, 4xx/timeout warn, 5xx/refused crit. */
11
+ export declare function statusFromHttp(code: number, timedOut?: boolean): Status;
12
+ /** Fetch a page and extract HTTP status + <title> (basic, no browser). */
13
+ export declare function basicCheck(url: string, timeoutMs?: number): Promise<BasicCheck>;
14
+ /** Best-effort hostname for a URL, falling back to the raw string. */
15
+ export declare function hostnameOf(url: string): string;
16
+ //# sourceMappingURL=basic.d.ts.map
@@ -0,0 +1,52 @@
1
+ /** Extract the <title> text from an HTML string (empty if none). */
2
+ export function extractTitle(html) {
3
+ const m = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
4
+ return m ? m[1].replace(/\s+/g, ' ').trim().slice(0, 80) : '';
5
+ }
6
+ /** Map an HTTP status to NPC status: 2xx/3xx ok, 4xx/timeout warn, 5xx/refused crit. */
7
+ export function statusFromHttp(code, timedOut = false) {
8
+ if (code >= 200 && code < 400)
9
+ return 'healthy';
10
+ if (code >= 400 && code < 500)
11
+ return 'warning';
12
+ if (code >= 500)
13
+ return 'critical';
14
+ return timedOut ? 'warning' : 'critical'; // 0 = network error
15
+ }
16
+ /** Fetch a page and extract HTTP status + <title> (basic, no browser). */
17
+ export async function basicCheck(url, timeoutMs = 8000) {
18
+ const controller = new AbortController();
19
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
20
+ try {
21
+ const res = await fetch(url, {
22
+ signal: controller.signal,
23
+ headers: { 'User-Agent': 'nocville-npc' },
24
+ redirect: 'follow',
25
+ });
26
+ let title = '';
27
+ try {
28
+ title = extractTitle(await res.text());
29
+ }
30
+ catch {
31
+ // body unavailable; leave title blank
32
+ }
33
+ return { status: res.status, title, ok: res.ok, timedOut: false };
34
+ }
35
+ catch (err) {
36
+ const timedOut = err instanceof Error && err.name === 'AbortError';
37
+ return { status: 0, title: '', ok: false, timedOut };
38
+ }
39
+ finally {
40
+ clearTimeout(timer);
41
+ }
42
+ }
43
+ /** Best-effort hostname for a URL, falling back to the raw string. */
44
+ export function hostnameOf(url) {
45
+ try {
46
+ return new URL(url).hostname;
47
+ }
48
+ catch {
49
+ return url.slice(0, 40);
50
+ }
51
+ }
52
+ //# sourceMappingURL=basic.js.map
@@ -1,3 +1,4 @@
1
+ import { join } from 'node:path';
1
2
  import { existsSync } from 'node:fs';
2
3
  import { execFileSync } from 'node:child_process';
3
4
  const MAC_PATHS = [
@@ -43,6 +44,25 @@ export function findChrome(explicit) {
43
44
  return p;
44
45
  return null;
45
46
  }
47
+ if (process.platform === 'win32') {
48
+ const roots = [
49
+ process.env['LOCALAPPDATA'],
50
+ process.env['PROGRAMFILES'],
51
+ process.env['PROGRAMFILES(X86)'],
52
+ ].filter((p) => !!p);
53
+ for (const root of roots)
54
+ for (const relative of [
55
+ ['Google', 'Chrome', 'Application', 'chrome.exe'],
56
+ ['Microsoft', 'Edge', 'Application', 'msedge.exe'],
57
+ ['Chromium', 'Application', 'chrome.exe'],
58
+ ['BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'],
59
+ ]) {
60
+ const path = join(root, ...relative);
61
+ if (existsSync(path))
62
+ return path;
63
+ }
64
+ return null;
65
+ }
46
66
  if (process.platform === 'linux') {
47
67
  for (const bin of LINUX_BINARIES) {
48
68
  const resolved = which(bin);
@@ -76,6 +96,12 @@ export function runningAsRoot() {
76
96
  */
77
97
  export async function loadPuppeteer() {
78
98
  try {
99
+ // Prefer CommonJS in bundled runtimes so Electron resolves through its
100
+ // ASAR loader. Native ESM installations use the optional dynamic import below.
101
+ if (typeof require === 'function') {
102
+ const mod = require('puppeteer-core');
103
+ return mod.default ?? mod;
104
+ }
79
105
  const dynImport = new Function('m', 'return import(m)');
80
106
  const mod = await dynImport('puppeteer-core');
81
107
  return (mod.default ?? mod);
@@ -0,0 +1,46 @@
1
+ export interface PuppeteerViewport {
2
+ width: number;
3
+ height: number;
4
+ deviceScaleFactor?: number;
5
+ }
6
+ export interface PuppeteerResponse {
7
+ status(): number;
8
+ }
9
+ export interface PuppeteerPage {
10
+ setViewport(v: PuppeteerViewport): Promise<void>;
11
+ goto(url: string, opts?: {
12
+ waitUntil?: string;
13
+ timeout?: number;
14
+ }): Promise<PuppeteerResponse | null>;
15
+ screenshot(opts: {
16
+ type?: string;
17
+ fullPage?: boolean;
18
+ }): Promise<Buffer | Uint8Array>;
19
+ close(): Promise<void>;
20
+ }
21
+ export interface PuppeteerBrowser {
22
+ newPage(): Promise<PuppeteerPage>;
23
+ close(): Promise<void>;
24
+ /** puppeteer >= 20 exposes `connected`; older versions expose isConnected(). */
25
+ connected?: boolean;
26
+ isConnected?(): boolean;
27
+ on(event: string, cb: () => void): void;
28
+ }
29
+ export interface PuppeteerModule {
30
+ launch(opts: {
31
+ executablePath?: string;
32
+ headless?: boolean | 'new';
33
+ args?: string[];
34
+ }): Promise<PuppeteerBrowser>;
35
+ }
36
+ /** Find a usable Chrome/Chromium executable: explicit path → env → known paths. */
37
+ export declare function findChrome(explicit?: string): string | null;
38
+ /**
39
+ * Load the optional `puppeteer-core` peer dependency without a static import,
40
+ * so the package builds and runs when it isn't installed. Returns null if the
41
+ * module is absent.
42
+ */
43
+ export declare function loadPuppeteer(): Promise<PuppeteerModule | null>;
44
+ /** True if the browser reports a live connection. */
45
+ export declare function browserConnected(browser: PuppeteerBrowser): boolean;
46
+ //# sourceMappingURL=chrome.d.ts.map
@@ -0,0 +1,57 @@
1
+ import { existsSync } from 'node:fs';
2
+ const CHROME_PATHS = {
3
+ darwin: [
4
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
5
+ '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
6
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
7
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
8
+ ],
9
+ linux: [
10
+ '/usr/bin/google-chrome',
11
+ '/usr/bin/google-chrome-stable',
12
+ '/usr/bin/chromium',
13
+ '/usr/bin/chromium-browser',
14
+ '/usr/bin/microsoft-edge',
15
+ '/snap/bin/chromium',
16
+ ],
17
+ };
18
+ /** Find a usable Chrome/Chromium executable: explicit path → env → known paths. */
19
+ export function findChrome(explicit) {
20
+ const envPath = process.env['CHROME_PATH'];
21
+ const candidates = [
22
+ ...(explicit ? [explicit] : []),
23
+ ...(envPath ? [envPath] : []),
24
+ ...(CHROME_PATHS[process.platform] ?? []),
25
+ ];
26
+ for (const p of candidates) {
27
+ if (p && existsSync(p))
28
+ return p;
29
+ }
30
+ return null;
31
+ }
32
+ /**
33
+ * Load the optional `puppeteer-core` peer dependency without a static import,
34
+ * so the package builds and runs when it isn't installed. Returns null if the
35
+ * module is absent.
36
+ */
37
+ export async function loadPuppeteer() {
38
+ try {
39
+ // Indirect dynamic import so TypeScript/bundlers don't require the module
40
+ // at build time.
41
+ const dynImport = new Function('m', 'return import(m)');
42
+ const mod = await dynImport('puppeteer-core');
43
+ return (mod.default ?? mod);
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ }
49
+ /** True if the browser reports a live connection. */
50
+ export function browserConnected(browser) {
51
+ if (typeof browser.connected === 'boolean')
52
+ return browser.connected;
53
+ if (typeof browser.isConnected === 'function')
54
+ return browser.isConnected();
55
+ return true;
56
+ }
57
+ //# sourceMappingURL=chrome.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocville/npc",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Put your Claude Code and Codex sessions (and your servers) in the Nocville office as live NPCs",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -24,7 +24,7 @@
24
24
  ],
25
25
  "dependencies": {
26
26
  "pngjs": "^7.0.0",
27
- "@nocville/node": "^0.3.0"
27
+ "@nocville/node": "^0.3.1"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "puppeteer-core": "*"