@nocville/npc 0.3.0 → 0.4.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 (38) hide show
  1. package/README.md +38 -18
  2. package/dist/cjs/cli.js +24 -3
  3. package/dist/cjs/core/base-npc.js +1 -0
  4. package/dist/cjs/core/config.js +23 -1
  5. package/dist/cjs/core/polling-npc.js +8 -0
  6. package/dist/cjs/core/user-config.js +2 -0
  7. package/dist/cjs/doctor.js +19 -0
  8. package/dist/cjs/index.js +2 -1
  9. package/dist/cjs/npcs/agent-common/session.js +3 -0
  10. package/dist/cjs/npcs/codex/watcher.js +1 -0
  11. package/dist/cjs/npcs/github-watcher/github.js +109 -0
  12. package/dist/cjs/npcs/office-characters/index.js +11 -1
  13. package/dist/cjs/npcs/web-page-watcher/basic.js +58 -0
  14. package/dist/cjs/npcs/web-page-watcher/capture.js +26 -0
  15. package/dist/cjs/npcs/web-page-watcher/chrome.js +62 -0
  16. package/dist/cli.js +24 -3
  17. package/dist/core/base-npc.js +1 -0
  18. package/dist/core/config.js +23 -1
  19. package/dist/core/polling-npc.d.ts +3 -0
  20. package/dist/core/polling-npc.js +8 -0
  21. package/dist/core/types.d.ts +2 -0
  22. package/dist/core/user-config.d.ts +1 -0
  23. package/dist/core/user-config.js +2 -0
  24. package/dist/doctor.js +19 -0
  25. package/dist/index.d.ts +1 -1
  26. package/dist/index.js +1 -1
  27. package/dist/npcs/agent-common/session.js +3 -0
  28. package/dist/npcs/codex/watcher.js +1 -0
  29. package/dist/npcs/github-watcher/github.d.ts +39 -0
  30. package/dist/npcs/github-watcher/github.js +103 -0
  31. package/dist/npcs/office-characters/index.d.ts +5 -0
  32. package/dist/npcs/office-characters/index.js +9 -0
  33. package/dist/npcs/web-page-watcher/basic.d.ts +16 -0
  34. package/dist/npcs/web-page-watcher/basic.js +52 -0
  35. package/dist/npcs/web-page-watcher/capture.js +26 -0
  36. package/dist/npcs/web-page-watcher/chrome.d.ts +46 -0
  37. package/dist/npcs/web-page-watcher/chrome.js +57 -0
  38. 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 | 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
@@ -225,3 +226,22 @@ class MyNPC extends PollingNPC {
225
226
 
226
227
  `nocville-npc --help` lists every command and flag, and `nocville-npc doctor` checks that your
227
228
  key and connection work. More at [nocville.com](https://nocville.com).
229
+
230
+ ## Encrypted Cloudflare workspaces
231
+
232
+ Download and verify the producer workspace settings in team privacy settings, then:
233
+
234
+ ```sh
235
+ nocville-npc init --workspace=/absolute/path/workspace.json
236
+ # Supply the agent key through NOCVILLE_API_KEY or the existing init prompt.
237
+ nocville-npc system-monitor
238
+ ```
239
+
240
+ `--workspace`, `NOCVILLE_WORKSPACE`, and the saved workspace path are supported in
241
+ that order. Constructor `workspace` overrides them. The first publication displays
242
+ a signing-device fingerprint for the team owner to approve. Approval is shared by
243
+ the local NPCs using this credential, including coding-agent hooks and watchdogs.
244
+ The workspace pins the HTTPS origin and opaque map ID; changing either requires
245
+ matching verified workspace settings. The `doctor` command uses encrypted enrollment
246
+ and an offline probe for these workspaces. The Node SDK README describes private
247
+ profile storage and the encryption boundary.
package/dist/cjs/cli.js CHANGED
@@ -91,10 +91,12 @@ 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
 
97
98
  Common Options:
99
+ --workspace=<file> Verified producer workspace settings from team privacy settings.
98
100
  --api-key=<key> Team API key (or NOCVILLE_API_KEY env)
99
101
  --api-url=<url> Nocville API URL (or NOCVILLE_API_URL env).
100
102
  Default: https://nocville.com. For local dev use
@@ -417,7 +419,8 @@ async function main() {
417
419
  case 'office-dog':
418
420
  case 'plant-caretaker':
419
421
  case 'it-technician':
420
- case 'out-of-touch-executive': {
422
+ case 'out-of-touch-executive':
423
+ case 'slightly-inappropriate-hr': {
421
424
  if (!config.serviceId)
422
425
  config.serviceId = parsed.command;
423
426
  const { OFFICE_CHARACTER_NPCS } = await Promise.resolve().then(() => __importStar(require('./npcs/office-characters/index.js')));
@@ -510,7 +513,20 @@ async function runInit(flags, config) {
510
513
  let apiKey = (0, config_js_1.stringFlag)(flags, 'api-key');
511
514
  const apiUrl = (0, config_js_1.stringFlag)(flags, 'api-url');
512
515
  const map = (0, config_js_1.stringFlag)(flags, 'map');
513
- if (!apiKey && !apiUrl && !map) {
516
+ const workspace = config.workspace;
517
+ if (workspace)
518
+ apiKey = apiKey || config.apiKey;
519
+ if (workspace && !apiKey) {
520
+ const { resolveApiKey } = await Promise.resolve().then(() => __importStar(require('./core/api-key.js')));
521
+ const resolved = await resolveApiKey({ flags });
522
+ if (!resolved.ok) {
523
+ console.error(resolved.error);
524
+ process.exitCode = 1;
525
+ return;
526
+ }
527
+ apiKey = resolved.key;
528
+ }
529
+ if (!apiKey && !apiUrl && !map && !workspace) {
514
530
  const saved = loadUserConfig();
515
531
  if (saved.apiKey && !process.env['NOCVILLE_API_KEY']?.trim()) {
516
532
  console.log('Usage: nocville-npc init --api-key=<key> [--api-url=<url>] [--map=<name>]');
@@ -526,7 +542,12 @@ async function runInit(flags, config) {
526
542
  }
527
543
  apiKey = resolved.key;
528
544
  }
529
- const path = saveUserConfig({ apiKey, apiUrl, map });
545
+ const path = saveUserConfig({
546
+ apiKey,
547
+ apiUrl: workspace ? config.apiUrl : apiUrl,
548
+ map: workspace ? config.map : map,
549
+ workspace,
550
+ });
530
551
  const saved = loadUserConfig();
531
552
  console.log(`✓ Saved ${path} (mode 0600)`);
532
553
  console.log(` API: ${saved.apiUrl ?? 'https://nocville.com'} key: ${maskKey(saved.apiKey)}`);
@@ -17,6 +17,7 @@ class BaseNPC {
17
17
  // We can't access `this.name` here since it's abstract, so use a placeholder
18
18
  this.logger = new logger_js_1.Logger('npc', config.debug);
19
19
  this.client = new node_1.NocvilleClient({
20
+ workspace: config.workspace,
20
21
  apiUrl: config.apiUrl,
21
22
  apiKey: config.apiKey,
22
23
  serviceId: config.serviceId,
@@ -6,6 +6,8 @@ exports.stringFlag = stringFlag;
6
6
  exports.boolFlag = boolFlag;
7
7
  exports.listFlag = listFlag;
8
8
  exports.numberFlag = numberFlag;
9
+ const node_1 = require("@nocville/node");
10
+ const node_path_1 = require("node:path");
9
11
  const user_config_js_1 = require("./user-config.js");
10
12
  /**
11
13
  * Simple hand-rolled CLI arg parser.
@@ -56,9 +58,16 @@ function parseArgs(argv) {
56
58
  */
57
59
  function resolveConfig(flags, overrides) {
58
60
  const saved = (0, user_config_js_1.loadUserConfig)();
61
+ const workspacePath = overrides?.workspace ||
62
+ stringFlag(flags, 'workspace') ||
63
+ process.env['NOCVILLE_WORKSPACE'] ||
64
+ saved.workspace;
65
+ const workspace = workspacePath ? (0, node_path_1.resolve)(workspacePath) : undefined;
66
+ const enrollment = workspace ? (0, node_1.loadProducerWorkspace)(workspace) : undefined;
59
67
  const apiUrl = overrides?.apiUrl ||
60
68
  stringFlag(flags, 'api-url') ||
61
69
  process.env['NOCVILLE_API_URL'] ||
70
+ enrollment?.apiUrl ||
62
71
  saved.apiUrl ||
63
72
  'https://nocville.com';
64
73
  const apiKey = overrides?.apiKey ||
@@ -76,10 +85,23 @@ function resolveConfig(flags, overrides) {
76
85
  const map = overrides?.map ||
77
86
  stringFlag(flags, 'map') ||
78
87
  process.env['NOCVILLE_MAP'] ||
88
+ enrollment?.encryption.mapId ||
79
89
  saved.map ||
80
90
  undefined;
81
91
  const name = overrides?.name || stringFlag(flags, 'name') || process.env['NOCVILLE_NAME'] || undefined;
82
- return { apiUrl, apiKey, serviceId, debug, map, name };
92
+ if (enrollment && apiUrl.replace(/\/+$/, '') !== enrollment.apiUrl)
93
+ throw new Error('API origin does not match the verified workspace');
94
+ if (enrollment && map && map !== enrollment.encryption.mapId)
95
+ throw new Error('Encrypted producers use the map ID in their workspace settings');
96
+ return {
97
+ apiUrl,
98
+ apiKey,
99
+ serviceId,
100
+ debug,
101
+ map: enrollment?.encryption.mapId ?? map,
102
+ name,
103
+ ...(workspace ? { workspace } : {}),
104
+ };
83
105
  }
84
106
  function stringFlag(flags, key) {
85
107
  const val = flags[key];
@@ -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();
@@ -25,6 +25,8 @@ function loadUserConfig(path = userConfigPath()) {
25
25
  try {
26
26
  const raw = JSON.parse((0, node_fs_1.readFileSync)(path, 'utf-8'));
27
27
  const out = {};
28
+ if (typeof raw['workspace'] === 'string' && raw['workspace'])
29
+ out.workspace = raw['workspace'];
28
30
  if (typeof raw['apiUrl'] === 'string' && raw['apiUrl'])
29
31
  out.apiUrl = raw['apiUrl'];
30
32
  if (typeof raw['apiKey'] === 'string' && raw['apiKey'])
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.runDoctor = runDoctor;
4
4
  exports.hasCodexHookWatcherOverlap = hasCodexHookWatcherOverlap;
5
+ const node_1 = require("@nocville/node");
5
6
  const node_crypto_1 = require("node:crypto");
6
7
  const node_fs_1 = require("node:fs");
7
8
  const liveness_js_1 = require("./npcs/agent-common/liveness.js");
@@ -43,6 +44,24 @@ async function runDoctor(config) {
43
44
  if (!hasKey) {
44
45
  console.log(' [SKIP] POST /api/v1/status — no --api-key / NOCVILLE_API_KEY set');
45
46
  }
47
+ else if (config.workspace || config.apiKey.includes('.team_')) {
48
+ // Exercise the locally encrypted path; never send a plaintext probe to a v2 workspace.
49
+ try {
50
+ const client = new node_1.NocvilleClient({
51
+ workspace: config.workspace,
52
+ apiUrl: config.apiUrl,
53
+ apiKey: config.apiKey,
54
+ serviceId: 'nocville-doctor',
55
+ retries: 0,
56
+ });
57
+ const result = await client.pushStatus({ status: 'unknown', offline: true });
58
+ pushOk = result.success;
59
+ console.log(` [${pushOk ? 'OK' : 'FAIL'}] Encrypted producer: ${result.error ?? 'accepted'}`);
60
+ }
61
+ catch (error) {
62
+ console.log(` [FAIL] Encrypted producer: ${error.message}`);
63
+ }
64
+ }
46
65
  else {
47
66
  const start = Date.now();
48
67
  const t = withTimeout(5000);
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
@@ -263,6 +263,7 @@ function buildAgentPayload(adapter, state, config) {
263
263
  }
264
264
  async function defaultPush(config, serviceId, payload, timeoutMs) {
265
265
  const client = new node_1.NocvilleClient({
266
+ workspace: config.workspace,
266
267
  apiUrl: config.apiUrl,
267
268
  apiKey: config.apiKey,
268
269
  serviceId,
@@ -381,6 +382,8 @@ function spawnDetachedAgent(adapter, config, args, cliPath = process.argv[1]) {
381
382
  if (!cliPath)
382
383
  return undefined;
383
384
  const env = { ...process.env };
385
+ if (config.workspace)
386
+ env['NOCVILLE_WORKSPACE'] = config.workspace;
384
387
  env['NOCVILLE_API_URL'] = config.apiUrl;
385
388
  env['NOCVILLE_API_KEY'] = config.apiKey;
386
389
  if (config.map)
@@ -339,6 +339,7 @@ class CodexWatcher {
339
339
  let client = this.clients.get(serviceId);
340
340
  if (!client) {
341
341
  client = new node_1.NocvilleClient({
342
+ workspace: this.config.workspace,
342
343
  apiUrl: this.config.apiUrl,
343
344
  apiKey: this.config.apiKey,
344
345
  serviceId,
@@ -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