@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 +19 -18
- package/dist/cjs/cli.js +3 -1
- package/dist/cjs/core/polling-npc.js +8 -0
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/npcs/github-watcher/github.js +109 -0
- package/dist/cjs/npcs/office-characters/index.js +11 -1
- package/dist/cjs/npcs/web-page-watcher/basic.js +58 -0
- package/dist/cjs/npcs/web-page-watcher/capture.js +26 -0
- package/dist/cjs/npcs/web-page-watcher/chrome.js +62 -0
- package/dist/cli.js +3 -1
- package/dist/core/polling-npc.d.ts +3 -0
- package/dist/core/polling-npc.js +8 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/npcs/github-watcher/github.d.ts +39 -0
- package/dist/npcs/github-watcher/github.js +103 -0
- package/dist/npcs/office-characters/index.d.ts +5 -0
- package/dist/npcs/office-characters/index.js +9 -0
- package/dist/npcs/web-page-watcher/basic.d.ts +16 -0
- package/dist/npcs/web-page-watcher/basic.js +52 -0
- package/dist/npcs/web-page-watcher/capture.js +26 -0
- package/dist/npcs/web-page-watcher/chrome.d.ts +46 -0
- package/dist/npcs/web-page-watcher/chrome.js +57 -0
- package/package.json +2 -2
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
|
|
144
|
-
|
|
|
145
|
-
| `system-monitor`
|
|
146
|
-
| `internet-connectivity`
|
|
147
|
-
| `process-watcher`
|
|
148
|
-
| `log-watcher`
|
|
149
|
-
| `receptionist`
|
|
150
|
-
| `security`
|
|
151
|
-
| `janitor`
|
|
152
|
-
| `barista`
|
|
153
|
-
| `office-cat`
|
|
154
|
-
| `office-dog`
|
|
155
|
-
| `plant-caretaker`
|
|
156
|
-
| `it-technician`
|
|
157
|
-
| `out-of-touch-executive`
|
|
158
|
-
| `
|
|
159
|
-
| `
|
|
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
|
|
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;
|
package/dist/core/polling-npc.js
CHANGED
|
@@ -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.
|
|
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.
|
|
27
|
+
"@nocville/node": "^0.3.1"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"puppeteer-core": "*"
|