@nocville/npc 0.0.0-stage → 0.2.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.
Files changed (213) hide show
  1. package/README.md +268 -2
  2. package/dist/cjs/cli.js +584 -0
  3. package/dist/cjs/core/api-key.js +69 -0
  4. package/dist/cjs/core/base-npc.js +160 -0
  5. package/dist/cjs/core/config.js +108 -0
  6. package/dist/cjs/core/event-npc.js +14 -0
  7. package/dist/cjs/core/logger.js +39 -0
  8. package/dist/cjs/core/polling-npc.js +65 -0
  9. package/dist/cjs/core/screen.js +28 -0
  10. package/dist/cjs/core/session-filter.js +150 -0
  11. package/dist/cjs/core/text.js +15 -0
  12. package/dist/cjs/core/thresholds.js +48 -0
  13. package/dist/cjs/core/types.js +10 -0
  14. package/dist/cjs/core/user-config.js +92 -0
  15. package/dist/cjs/doctor.js +130 -0
  16. package/dist/cjs/index.js +95 -0
  17. package/dist/cjs/npcs/agent-common/activity.js +291 -0
  18. package/dist/cjs/npcs/agent-common/git-stats.js +71 -0
  19. package/dist/cjs/npcs/agent-common/hook-runner.js +252 -0
  20. package/dist/cjs/npcs/agent-common/hook-settings.js +279 -0
  21. package/dist/cjs/npcs/agent-common/liveness.js +112 -0
  22. package/dist/cjs/npcs/agent-common/lock.js +99 -0
  23. package/dist/cjs/npcs/agent-common/runtime.js +154 -0
  24. package/dist/cjs/npcs/agent-common/session.js +412 -0
  25. package/dist/cjs/npcs/agent-common/shim.js +96 -0
  26. package/dist/cjs/npcs/agent-common/watchdog.js +149 -0
  27. package/dist/cjs/npcs/claude-code/defaults.js +20 -0
  28. package/dist/cjs/npcs/claude-code/hook-handler.js +139 -0
  29. package/dist/cjs/npcs/claude-code/index.js +104 -0
  30. package/dist/cjs/npcs/claude-code/setup.js +133 -0
  31. package/dist/cjs/npcs/claude-code/state-machine.js +130 -0
  32. package/dist/cjs/npcs/codex/defaults.js +32 -0
  33. package/dist/cjs/npcs/codex/hook-handler.js +116 -0
  34. package/dist/cjs/npcs/codex/index.js +92 -0
  35. package/dist/cjs/npcs/codex/rollout.js +246 -0
  36. package/dist/cjs/npcs/codex/setup.js +104 -0
  37. package/dist/cjs/npcs/codex/watcher.js +361 -0
  38. package/dist/cjs/npcs/cursor/defaults.js +15 -0
  39. package/dist/cjs/npcs/cursor/hook-handler.js +65 -0
  40. package/dist/cjs/npcs/cursor/index.js +72 -0
  41. package/dist/cjs/npcs/cursor/normalize.js +54 -0
  42. package/dist/cjs/npcs/cursor/setup.js +98 -0
  43. package/dist/cjs/npcs/gemini/defaults.js +15 -0
  44. package/dist/cjs/npcs/gemini/hook-handler.js +93 -0
  45. package/dist/cjs/npcs/gemini/index.js +69 -0
  46. package/dist/cjs/npcs/gemini/setup.js +98 -0
  47. package/dist/cjs/npcs/github-watcher/defaults.js +11 -0
  48. package/dist/cjs/npcs/github-watcher/format.js +21 -0
  49. package/dist/cjs/npcs/github-watcher/github-api.js +126 -0
  50. package/dist/cjs/npcs/github-watcher/index.js +148 -0
  51. package/dist/cjs/npcs/internet-connectivity/checks.js +92 -0
  52. package/dist/cjs/npcs/internet-connectivity/defaults.js +22 -0
  53. package/dist/cjs/npcs/internet-connectivity/index.js +251 -0
  54. package/dist/cjs/npcs/log-watcher/buffer.js +40 -0
  55. package/dist/cjs/npcs/log-watcher/defaults.js +12 -0
  56. package/dist/cjs/npcs/log-watcher/index.js +161 -0
  57. package/dist/cjs/npcs/log-watcher/tail.js +81 -0
  58. package/dist/cjs/npcs/process-watcher/defaults.js +9 -0
  59. package/dist/cjs/npcs/process-watcher/index.js +140 -0
  60. package/dist/cjs/npcs/process-watcher/process-scanner.js +114 -0
  61. package/dist/cjs/npcs/receptionist/defaults.js +9 -0
  62. package/dist/cjs/npcs/receptionist/index.js +120 -0
  63. package/dist/cjs/npcs/receptionist/weather.js +129 -0
  64. package/dist/cjs/npcs/system-monitor/collectors.js +68 -0
  65. package/dist/cjs/npcs/system-monitor/defaults.js +13 -0
  66. package/dist/cjs/npcs/system-monitor/index.js +105 -0
  67. package/dist/cjs/npcs/web-page-watcher/basic-mode.js +58 -0
  68. package/dist/cjs/npcs/web-page-watcher/capture.js +80 -0
  69. package/dist/cjs/npcs/web-page-watcher/defaults.js +31 -0
  70. package/dist/cjs/npcs/web-page-watcher/index.js +234 -0
  71. package/dist/cjs/npcs/web-page-watcher/pixelate.js +181 -0
  72. package/dist/cjs/package.json +1 -0
  73. package/dist/cli.d.ts +3 -0
  74. package/dist/cli.js +549 -0
  75. package/dist/core/api-key.d.ts +34 -0
  76. package/dist/core/api-key.js +64 -0
  77. package/dist/core/base-npc.d.ts +41 -0
  78. package/dist/core/base-npc.js +156 -0
  79. package/dist/core/config.d.ts +22 -0
  80. package/dist/core/config.js +101 -0
  81. package/dist/core/event-npc.d.ts +8 -0
  82. package/dist/core/event-npc.js +10 -0
  83. package/dist/core/logger.d.ts +11 -0
  84. package/dist/core/logger.js +35 -0
  85. package/dist/core/polling-npc.d.ts +18 -0
  86. package/dist/core/polling-npc.js +61 -0
  87. package/dist/core/screen.d.ts +11 -0
  88. package/dist/core/screen.js +24 -0
  89. package/dist/core/session-filter.d.ts +59 -0
  90. package/dist/core/session-filter.js +143 -0
  91. package/dist/core/text.d.ts +6 -0
  92. package/dist/core/text.js +12 -0
  93. package/dist/core/thresholds.d.ts +10 -0
  94. package/dist/core/thresholds.js +44 -0
  95. package/dist/core/types.d.ts +60 -0
  96. package/dist/core/types.js +7 -0
  97. package/dist/core/user-config.d.ts +43 -0
  98. package/dist/core/user-config.js +85 -0
  99. package/dist/doctor.d.ts +8 -0
  100. package/dist/doctor.js +126 -0
  101. package/dist/index.d.ts +41 -0
  102. package/dist/index.js +26 -0
  103. package/dist/npcs/agent-common/activity.d.ts +42 -0
  104. package/dist/npcs/agent-common/activity.js +280 -0
  105. package/dist/npcs/agent-common/git-stats.d.ts +11 -0
  106. package/dist/npcs/agent-common/git-stats.js +67 -0
  107. package/dist/npcs/agent-common/hook-runner.d.ts +62 -0
  108. package/dist/npcs/agent-common/hook-runner.js +245 -0
  109. package/dist/npcs/agent-common/hook-settings.d.ts +75 -0
  110. package/dist/npcs/agent-common/hook-settings.js +265 -0
  111. package/dist/npcs/agent-common/liveness.d.ts +12 -0
  112. package/dist/npcs/agent-common/liveness.js +105 -0
  113. package/dist/npcs/agent-common/lock.d.ts +6 -0
  114. package/dist/npcs/agent-common/lock.js +95 -0
  115. package/dist/npcs/agent-common/runtime.d.ts +16 -0
  116. package/dist/npcs/agent-common/runtime.js +149 -0
  117. package/dist/npcs/agent-common/session.d.ts +192 -0
  118. package/dist/npcs/agent-common/session.js +390 -0
  119. package/dist/npcs/agent-common/shim.d.ts +4 -0
  120. package/dist/npcs/agent-common/shim.js +90 -0
  121. package/dist/npcs/agent-common/watchdog.d.ts +16 -0
  122. package/dist/npcs/agent-common/watchdog.js +144 -0
  123. package/dist/npcs/claude-code/defaults.d.ts +15 -0
  124. package/dist/npcs/claude-code/defaults.js +17 -0
  125. package/dist/npcs/claude-code/hook-handler.d.ts +40 -0
  126. package/dist/npcs/claude-code/hook-handler.js +131 -0
  127. package/dist/npcs/claude-code/index.d.ts +36 -0
  128. package/dist/npcs/claude-code/index.js +92 -0
  129. package/dist/npcs/claude-code/setup.d.ts +58 -0
  130. package/dist/npcs/claude-code/setup.js +121 -0
  131. package/dist/npcs/claude-code/state-machine.d.ts +28 -0
  132. package/dist/npcs/claude-code/state-machine.js +125 -0
  133. package/dist/npcs/codex/defaults.d.ts +24 -0
  134. package/dist/npcs/codex/defaults.js +28 -0
  135. package/dist/npcs/codex/hook-handler.d.ts +24 -0
  136. package/dist/npcs/codex/hook-handler.js +110 -0
  137. package/dist/npcs/codex/index.d.ts +21 -0
  138. package/dist/npcs/codex/index.js +76 -0
  139. package/dist/npcs/codex/rollout.d.ts +39 -0
  140. package/dist/npcs/codex/rollout.js +242 -0
  141. package/dist/npcs/codex/setup.d.ts +37 -0
  142. package/dist/npcs/codex/setup.js +96 -0
  143. package/dist/npcs/codex/watcher.d.ts +65 -0
  144. package/dist/npcs/codex/watcher.js +356 -0
  145. package/dist/npcs/cursor/defaults.d.ts +9 -0
  146. package/dist/npcs/cursor/defaults.js +11 -0
  147. package/dist/npcs/cursor/hook-handler.d.ts +8 -0
  148. package/dist/npcs/cursor/hook-handler.js +60 -0
  149. package/dist/npcs/cursor/index.d.ts +18 -0
  150. package/dist/npcs/cursor/index.js +57 -0
  151. package/dist/npcs/cursor/normalize.d.ts +6 -0
  152. package/dist/npcs/cursor/normalize.js +49 -0
  153. package/dist/npcs/cursor/setup.d.ts +29 -0
  154. package/dist/npcs/cursor/setup.js +90 -0
  155. package/dist/npcs/gemini/defaults.d.ts +9 -0
  156. package/dist/npcs/gemini/defaults.js +11 -0
  157. package/dist/npcs/gemini/hook-handler.d.ts +10 -0
  158. package/dist/npcs/gemini/hook-handler.js +87 -0
  159. package/dist/npcs/gemini/index.d.ts +17 -0
  160. package/dist/npcs/gemini/index.js +56 -0
  161. package/dist/npcs/gemini/setup.d.ts +29 -0
  162. package/dist/npcs/gemini/setup.js +90 -0
  163. package/dist/npcs/github-watcher/defaults.d.ts +8 -0
  164. package/dist/npcs/github-watcher/defaults.js +8 -0
  165. package/dist/npcs/github-watcher/format.d.ts +3 -0
  166. package/dist/npcs/github-watcher/format.js +18 -0
  167. package/dist/npcs/github-watcher/github-api.d.ts +55 -0
  168. package/dist/npcs/github-watcher/github-api.js +120 -0
  169. package/dist/npcs/github-watcher/index.d.ts +35 -0
  170. package/dist/npcs/github-watcher/index.js +144 -0
  171. package/dist/npcs/internet-connectivity/checks.d.ts +24 -0
  172. package/dist/npcs/internet-connectivity/checks.js +84 -0
  173. package/dist/npcs/internet-connectivity/defaults.d.ts +20 -0
  174. package/dist/npcs/internet-connectivity/defaults.js +19 -0
  175. package/dist/npcs/internet-connectivity/index.d.ts +55 -0
  176. package/dist/npcs/internet-connectivity/index.js +246 -0
  177. package/dist/npcs/log-watcher/buffer.d.ts +18 -0
  178. package/dist/npcs/log-watcher/buffer.js +34 -0
  179. package/dist/npcs/log-watcher/defaults.d.ts +9 -0
  180. package/dist/npcs/log-watcher/defaults.js +9 -0
  181. package/dist/npcs/log-watcher/index.d.ts +45 -0
  182. package/dist/npcs/log-watcher/index.js +157 -0
  183. package/dist/npcs/log-watcher/tail.d.ts +9 -0
  184. package/dist/npcs/log-watcher/tail.js +78 -0
  185. package/dist/npcs/process-watcher/defaults.d.ts +6 -0
  186. package/dist/npcs/process-watcher/defaults.js +6 -0
  187. package/dist/npcs/process-watcher/index.d.ts +26 -0
  188. package/dist/npcs/process-watcher/index.js +136 -0
  189. package/dist/npcs/process-watcher/process-scanner.d.ts +19 -0
  190. package/dist/npcs/process-watcher/process-scanner.js +110 -0
  191. package/dist/npcs/receptionist/defaults.d.ts +6 -0
  192. package/dist/npcs/receptionist/defaults.js +6 -0
  193. package/dist/npcs/receptionist/index.d.ts +29 -0
  194. package/dist/npcs/receptionist/index.js +116 -0
  195. package/dist/npcs/receptionist/weather.d.ts +37 -0
  196. package/dist/npcs/receptionist/weather.js +121 -0
  197. package/dist/npcs/system-monitor/collectors.d.ts +45 -0
  198. package/dist/npcs/system-monitor/collectors.js +60 -0
  199. package/dist/npcs/system-monitor/defaults.d.ts +7 -0
  200. package/dist/npcs/system-monitor/defaults.js +10 -0
  201. package/dist/npcs/system-monitor/index.d.ts +27 -0
  202. package/dist/npcs/system-monitor/index.js +101 -0
  203. package/dist/npcs/web-page-watcher/basic-mode.d.ts +16 -0
  204. package/dist/npcs/web-page-watcher/basic-mode.js +52 -0
  205. package/dist/npcs/web-page-watcher/capture.d.ts +49 -0
  206. package/dist/npcs/web-page-watcher/capture.js +75 -0
  207. package/dist/npcs/web-page-watcher/defaults.d.ts +19 -0
  208. package/dist/npcs/web-page-watcher/defaults.js +27 -0
  209. package/dist/npcs/web-page-watcher/index.d.ts +42 -0
  210. package/dist/npcs/web-page-watcher/index.js +230 -0
  211. package/dist/npcs/web-page-watcher/pixelate.d.ts +36 -0
  212. package/dist/npcs/web-page-watcher/pixelate.js +172 -0
  213. package/package.json +73 -3
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SystemMonitorNPC = void 0;
4
+ const polling_npc_js_1 = require("../../core/polling-npc.js");
5
+ const thresholds_js_1 = require("../../core/thresholds.js");
6
+ const screen_js_1 = require("../../core/screen.js");
7
+ const collectors_js_1 = require("./collectors.js");
8
+ const defaults_js_1 = require("./defaults.js");
9
+ class SystemMonitorNPC extends polling_npc_js_1.PollingNPC {
10
+ diskPath;
11
+ cpuThreshold;
12
+ memThreshold;
13
+ diskThreshold;
14
+ cpuSampler = (0, collectors_js_1.createCpuSampler)();
15
+ cpuHistory = [];
16
+ memHistory = [];
17
+ constructor(config) {
18
+ super({
19
+ ...config,
20
+ pollInterval: config.pollInterval ?? defaults_js_1.DEFAULTS.pollInterval,
21
+ });
22
+ this.diskPath = config.diskPath ?? defaults_js_1.DEFAULTS.diskPath;
23
+ this.cpuThreshold = config.cpuThreshold ?? defaults_js_1.DEFAULT_THRESHOLDS['cpu'];
24
+ this.memThreshold = config.memThreshold ?? defaults_js_1.DEFAULT_THRESHOLDS['mem'];
25
+ this.diskThreshold = config.diskThreshold ?? defaults_js_1.DEFAULT_THRESHOLDS['disk'];
26
+ }
27
+ get name() {
28
+ return 'system-monitor';
29
+ }
30
+ get defaultDisplayName() {
31
+ return 'System Monitor';
32
+ }
33
+ get archetype() {
34
+ return 'service';
35
+ }
36
+ get heartbeatSeconds() {
37
+ return 30;
38
+ }
39
+ get behavior() {
40
+ return { idle: 'wander' };
41
+ }
42
+ get companion() {
43
+ return {
44
+ screenType: 'dashboard-wall',
45
+ size: { w: 2, h: 2 },
46
+ placement: 'adjacent-wall',
47
+ label: 'System',
48
+ };
49
+ }
50
+ async collect() {
51
+ const cpu = this.cpuSampler.sample();
52
+ const mem = (0, collectors_js_1.collectMemory)();
53
+ const disk = await (0, collectors_js_1.collectDisk)(this.diskPath);
54
+ const cpuStatus = (0, thresholds_js_1.evaluateThreshold)(cpu.percent, this.cpuThreshold);
55
+ const memStatus = (0, thresholds_js_1.evaluateThreshold)(mem.percent, this.memThreshold);
56
+ const diskStatus = (0, thresholds_js_1.evaluateThreshold)(disk.percent, this.diskThreshold);
57
+ const status = (0, thresholds_js_1.worstStatus)([cpuStatus, memStatus, diskStatus]);
58
+ const mood = status === 'critical'
59
+ ? 'stressed'
60
+ : status === 'warning'
61
+ ? 'concerned'
62
+ : 'happy';
63
+ (0, screen_js_1.pushHistory)(this.cpuHistory, cpu.percent);
64
+ (0, screen_js_1.pushHistory)(this.memHistory, mem.percent);
65
+ const screenData = {
66
+ layout: 'multi-metric',
67
+ values: [
68
+ {
69
+ label: 'CPU',
70
+ value: `${cpu.percent}%`,
71
+ color: (0, screen_js_1.statusColor)(cpuStatus),
72
+ sparkline: [...this.cpuHistory],
73
+ },
74
+ {
75
+ label: 'MEM',
76
+ value: `${mem.percent}%`,
77
+ color: (0, screen_js_1.statusColor)(memStatus),
78
+ sparkline: [...this.memHistory],
79
+ },
80
+ { label: 'DISK', value: `${disk.percent}%`, color: (0, screen_js_1.statusColor)(diskStatus) },
81
+ { label: 'LOAD', value: cpu.loadAvg1m.toFixed(2), color: 'gray' },
82
+ ],
83
+ };
84
+ return {
85
+ status,
86
+ mood,
87
+ message: `CPU: ${cpu.percent}% | Mem: ${mem.percent}% | Disk: ${disk.percent}%`,
88
+ screenData,
89
+ // When strained, go find a desk and get to work; otherwise wander.
90
+ behavior: status !== 'healthy' ? { busy: 'seek-desk' } : undefined,
91
+ metrics: {
92
+ cpu_percent: cpu.percent,
93
+ mem_percent: mem.percent,
94
+ mem_used_gb: mem.usedGb,
95
+ mem_total_gb: mem.totalGb,
96
+ disk_percent: disk.percent,
97
+ disk_used_gb: disk.usedGb,
98
+ disk_total_gb: disk.totalGb,
99
+ load_avg_1m: cpu.loadAvg1m,
100
+ },
101
+ };
102
+ }
103
+ }
104
+ exports.SystemMonitorNPC = SystemMonitorNPC;
105
+ //# 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-mode.js.map
@@ -0,0 +1,80 @@
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 node_child_process_1 = require("node:child_process");
8
+ const MAC_PATHS = [
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
+ '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
14
+ ];
15
+ const LINUX_BINARIES = [
16
+ 'google-chrome',
17
+ 'google-chrome-stable',
18
+ 'chromium',
19
+ 'chromium-browser',
20
+ 'brave-browser',
21
+ ];
22
+ function which(bin) {
23
+ try {
24
+ const out = (0, node_child_process_1.execFileSync)('which', [bin], { encoding: 'utf-8' }).trim();
25
+ return out || null;
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
31
+ /**
32
+ * Find a usable Chrome/Chromium executable: explicit path → CHROME_PATH env →
33
+ * platform-specific known locations (mac app bundles; `which` on linux).
34
+ */
35
+ function findChrome(explicit) {
36
+ const envPath = process.env['CHROME_PATH'];
37
+ if (explicit && (0, node_fs_1.existsSync)(explicit))
38
+ return explicit;
39
+ if (envPath && (0, node_fs_1.existsSync)(envPath))
40
+ return envPath;
41
+ if (process.platform === 'darwin') {
42
+ for (const p of MAC_PATHS)
43
+ if ((0, node_fs_1.existsSync)(p))
44
+ return p;
45
+ return null;
46
+ }
47
+ if (process.platform === 'linux') {
48
+ for (const bin of LINUX_BINARIES) {
49
+ const resolved = which(bin);
50
+ if (resolved && (0, node_fs_1.existsSync)(resolved))
51
+ return resolved;
52
+ }
53
+ return null;
54
+ }
55
+ return null;
56
+ }
57
+ /**
58
+ * Load the optional `puppeteer-core` peer dependency without a static import,
59
+ * so the package builds and runs when it isn't installed. Returns null if the
60
+ * module is absent.
61
+ */
62
+ async function loadPuppeteer() {
63
+ try {
64
+ const dynImport = new Function('m', 'return import(m)');
65
+ const mod = await dynImport('puppeteer-core');
66
+ return (mod.default ?? mod);
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ }
72
+ /** True if the browser reports a live connection. */
73
+ function browserConnected(browser) {
74
+ if (typeof browser.connected === 'boolean')
75
+ return browser.connected;
76
+ if (typeof browser.isConnected === 'function')
77
+ return browser.isConnected();
78
+ return true;
79
+ }
80
+ //# sourceMappingURL=capture.js.map
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULTS = void 0;
4
+ exports.parseDimensions = parseDimensions;
5
+ exports.DEFAULTS = {
6
+ pollInterval: 60000, // 60s
7
+ minPollInterval: 15000, // floor
8
+ navTimeoutMs: 15000,
9
+ maxColors: 32,
10
+ /** Polls to keep the 'changed' sub-status active after a page change. */
11
+ changedPolls: 2,
12
+ // Default TV resolution (4:3, matches the 4×3 web-view companion). The client
13
+ // reads WxH from the pixel-image `fmt` string, so any size is self-consistent.
14
+ screenW: 160,
15
+ screenH: 120,
16
+ // deviceScaleFactor is 1/8, so the viewport defaults to screen × 8 → Chrome
17
+ // rasterizes the screen size natively (overridable via --viewport).
18
+ scaleDivisor: 8,
19
+ waitUntil: 'networkidle2',
20
+ };
21
+ /** Parse a "WxH" string into a {width, height} pair, or null if malformed. */
22
+ function parseDimensions(s) {
23
+ const m = s
24
+ .trim()
25
+ .toLowerCase()
26
+ .match(/^(\d+)x(\d+)$/);
27
+ if (!m)
28
+ return null;
29
+ return { width: parseInt(m[1], 10), height: parseInt(m[2], 10) };
30
+ }
31
+ //# sourceMappingURL=defaults.js.map
@@ -0,0 +1,234 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WebPageWatcherNPC = void 0;
4
+ const polling_npc_js_1 = require("../../core/polling-npc.js");
5
+ const defaults_js_1 = require("./defaults.js");
6
+ const pixelate_js_1 = require("./pixelate.js");
7
+ const capture_js_1 = require("./capture.js");
8
+ const basic_mode_js_1 = require("./basic-mode.js");
9
+ class WebPageWatcherNPC extends polling_npc_js_1.PollingNPC {
10
+ url;
11
+ mode;
12
+ chromePath;
13
+ viewportW;
14
+ viewportH;
15
+ screenW;
16
+ screenH;
17
+ waitUntil;
18
+ puppeteer = null;
19
+ browser = null;
20
+ browserUnavailable = false;
21
+ chromeHintShown = false;
22
+ lastHash = 0;
23
+ changedPollsLeft = 0;
24
+ constructor(config) {
25
+ super({
26
+ ...config,
27
+ // Floor the poll interval — screenshots are expensive.
28
+ pollInterval: Math.max(config.pollInterval ?? defaults_js_1.DEFAULTS.pollInterval, defaults_js_1.DEFAULTS.minPollInterval),
29
+ });
30
+ this.url = config.url;
31
+ this.mode = config.mode ?? 'auto';
32
+ this.chromePath = config.chromePath;
33
+ this.screenW = config.screenWidth ?? defaults_js_1.DEFAULTS.screenW;
34
+ this.screenH = config.screenHeight ?? defaults_js_1.DEFAULTS.screenH;
35
+ // Derive the viewport from the screen size for native downscaling unless the
36
+ // user overrides it (in which case pngToPixelImage box-filters to screen).
37
+ this.viewportW = config.viewportWidth ?? this.screenW * defaults_js_1.DEFAULTS.scaleDivisor;
38
+ this.viewportH = config.viewportHeight ?? this.screenH * defaults_js_1.DEFAULTS.scaleDivisor;
39
+ this.waitUntil = config.waitUntil ?? defaults_js_1.DEFAULTS.waitUntil;
40
+ }
41
+ get name() {
42
+ return 'web-page-watcher';
43
+ }
44
+ get defaultDisplayName() {
45
+ return (0, basic_mode_js_1.hostnameOf)(this.url);
46
+ }
47
+ get archetype() {
48
+ return 'custom';
49
+ }
50
+ get companion() {
51
+ return {
52
+ screenType: 'web-view',
53
+ size: { w: 4, h: 3 },
54
+ placement: 'adjacent-wall',
55
+ label: (0, basic_mode_js_1.hostnameOf)(this.url),
56
+ };
57
+ }
58
+ async collect() {
59
+ if (this.mode === 'basic')
60
+ return this.collectBasic();
61
+ const browser = await this.ensureBrowser();
62
+ if (!browser) {
63
+ // 'auto' silently degrades; explicit 'screenshot' warns once.
64
+ this.maybeShowChromeHint();
65
+ return this.collectBasic();
66
+ }
67
+ return this.collectScreenshot(browser);
68
+ }
69
+ async onStop() {
70
+ await super.onStop();
71
+ if (this.browser) {
72
+ try {
73
+ await this.browser.close();
74
+ }
75
+ catch {
76
+ // best effort
77
+ }
78
+ this.browser = null;
79
+ }
80
+ }
81
+ // --- Screenshot mode ---
82
+ async collectScreenshot(browser) {
83
+ const page = await browser.newPage();
84
+ try {
85
+ // deviceScaleFactor 0.125 makes Chrome rasterize the viewport down to the
86
+ // screen size natively (e.g. 1280×800 → 160×100); pngToPixelImage
87
+ // box-filters as a fallback if it doesn't.
88
+ await page.setViewport({
89
+ width: this.viewportW,
90
+ height: this.viewportH,
91
+ deviceScaleFactor: 0.125,
92
+ });
93
+ let httpStatus = 0;
94
+ let timedOut = false;
95
+ const loadStart = Date.now();
96
+ try {
97
+ const resp = await page.goto(this.url, {
98
+ waitUntil: this.waitUntil,
99
+ timeout: defaults_js_1.DEFAULTS.navTimeoutMs,
100
+ });
101
+ httpStatus = resp ? resp.status() : 0;
102
+ }
103
+ catch (err) {
104
+ timedOut = err instanceof Error && /timeout/i.test(err.message);
105
+ }
106
+ const loadMs = Date.now() - loadStart;
107
+ const captureStart = Date.now();
108
+ const shotRaw = await page.screenshot({ type: 'png' });
109
+ const shot = Buffer.isBuffer(shotRaw) ? shotRaw : Buffer.from(shotRaw);
110
+ const { screenData, byteSize, hash } = (0, pixelate_js_1.pngToPixelImage)(shot, defaults_js_1.DEFAULTS.maxColors, this.screenW, this.screenH);
111
+ const captureMs = Date.now() - captureStart;
112
+ const changed = this.lastHash !== 0 && hash !== this.lastHash;
113
+ this.lastHash = hash;
114
+ if (changed)
115
+ this.changedPollsLeft = defaults_js_1.DEFAULTS.changedPolls;
116
+ else if (this.changedPollsLeft > 0)
117
+ this.changedPollsLeft--;
118
+ const status = (0, basic_mode_js_1.statusFromHttp)(httpStatus, timedOut);
119
+ const host = (0, basic_mode_js_1.hostnameOf)(this.url);
120
+ const message = changed
121
+ ? 'Page updated!'
122
+ : httpStatus
123
+ ? `${host} ${httpStatus < 400 ? 'OK' : 'ERR'} (${httpStatus}, ${loadMs}ms)`
124
+ : `${host} unreachable`;
125
+ return {
126
+ status,
127
+ mood: moodFor(status),
128
+ message,
129
+ subStatus: this.changedPollsLeft > 0 ? 'changed' : undefined,
130
+ screenData,
131
+ metrics: {
132
+ status_code: httpStatus,
133
+ load_ms: loadMs,
134
+ capture_ms: captureMs,
135
+ frame_bytes: byteSize,
136
+ },
137
+ };
138
+ }
139
+ finally {
140
+ await page.close().catch(() => undefined);
141
+ }
142
+ }
143
+ async ensureBrowser() {
144
+ if (this.browserUnavailable)
145
+ return null;
146
+ if (this.browser && (0, capture_js_1.browserConnected)(this.browser))
147
+ return this.browser;
148
+ if (this.browser)
149
+ this.browser = null; // disconnected — relaunch
150
+ if (!this.puppeteer) {
151
+ this.puppeteer = await (0, capture_js_1.loadPuppeteer)();
152
+ }
153
+ const chromePath = (0, capture_js_1.findChrome)(this.chromePath);
154
+ if (!this.puppeteer || !chromePath) {
155
+ this.browserUnavailable = true;
156
+ return null;
157
+ }
158
+ try {
159
+ this.browser = await this.puppeteer.launch({
160
+ executablePath: chromePath,
161
+ headless: true,
162
+ args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu', '--hide-scrollbars'],
163
+ });
164
+ this.browser.on('disconnected', () => {
165
+ this.browser = null;
166
+ });
167
+ return this.browser;
168
+ }
169
+ catch (err) {
170
+ this.logger.warn('Failed to launch Chrome', {
171
+ error: err instanceof Error ? err.message : String(err),
172
+ });
173
+ this.browserUnavailable = true;
174
+ return null;
175
+ }
176
+ }
177
+ maybeShowChromeHint() {
178
+ if (this.chromeHintShown)
179
+ return;
180
+ this.chromeHintShown = true;
181
+ this.logger.warn('Chrome/puppeteer-core unavailable — falling back to basic mode. ' +
182
+ 'Install `puppeteer-core` and Google Chrome (or set --chrome-path/CHROME_PATH) for the pixel-art TV.');
183
+ }
184
+ // --- Basic mode ---
185
+ async collectBasic() {
186
+ const loadStart = Date.now();
187
+ const check = await (0, basic_mode_js_1.basicCheck)(this.url);
188
+ const loadMs = Date.now() - loadStart;
189
+ // Basic mode can't render the pixel TV, so it never reports fully healthy —
190
+ // a healthy HTTP result is downgraded to a warning to nudge Chrome setup.
191
+ let status = (0, basic_mode_js_1.statusFromHttp)(check.status, check.timedOut);
192
+ if (status === 'healthy')
193
+ status = 'warning';
194
+ const host = (0, basic_mode_js_1.hostnameOf)(this.url);
195
+ const message = check.title ? `${host}: ${check.title}` : `${host} (${check.status || 'ERR'})`;
196
+ return {
197
+ status,
198
+ mood: moodFor(status),
199
+ message,
200
+ screenData: {
201
+ layout: 'ticker',
202
+ values: [
203
+ { label: 'URL', value: host },
204
+ {
205
+ label: 'HTTP',
206
+ value: check.status ? String(check.status) : 'ERR',
207
+ color: check.status >= 200 && check.status < 400
208
+ ? 'green'
209
+ : check.status >= 500 || check.status === 0
210
+ ? 'red'
211
+ : 'amber',
212
+ },
213
+ { label: 'Title', value: check.title || '(no title)' },
214
+ { label: 'Mode', value: 'basic' },
215
+ ],
216
+ },
217
+ metrics: { status_code: check.status, load_ms: loadMs, capture_ms: 0 },
218
+ };
219
+ }
220
+ }
221
+ exports.WebPageWatcherNPC = WebPageWatcherNPC;
222
+ function moodFor(status) {
223
+ switch (status) {
224
+ case 'healthy':
225
+ return 'happy';
226
+ case 'warning':
227
+ return 'concerned';
228
+ case 'critical':
229
+ return 'stressed';
230
+ default:
231
+ return 'idle';
232
+ }
233
+ }
234
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,181 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TARGET_H = exports.TARGET_W = void 0;
4
+ exports.decodePng = decodePng;
5
+ exports.boxDownscale = boxDownscale;
6
+ exports.quantize = quantize;
7
+ exports.buildPixelImageScreen = buildPixelImageScreen;
8
+ exports.fnv1a = fnv1a;
9
+ exports.pngToPixelImage = pngToPixelImage;
10
+ const pngjs_1 = require("pngjs");
11
+ /** Default target resolution for the web-view TV (4:3 to match a 4×3 companion). */
12
+ exports.TARGET_W = 160;
13
+ exports.TARGET_H = 120;
14
+ /** Decode a PNG buffer to RGBA bytes + dimensions. */
15
+ function decodePng(buf) {
16
+ const png = pngjs_1.PNG.sync.read(buf);
17
+ return { width: png.width, height: png.height, data: png.data };
18
+ }
19
+ /** Area-average (box filter) downscale of RGBA `src` to `tw`×`th`. */
20
+ function boxDownscale(src, sw, sh, tw, th) {
21
+ const out = Buffer.alloc(tw * th * 4);
22
+ for (let ty = 0; ty < th; ty++) {
23
+ const sy0 = Math.floor((ty * sh) / th);
24
+ const sy1 = Math.max(sy0 + 1, Math.floor(((ty + 1) * sh) / th));
25
+ for (let tx = 0; tx < tw; tx++) {
26
+ const sx0 = Math.floor((tx * sw) / tw);
27
+ const sx1 = Math.max(sx0 + 1, Math.floor(((tx + 1) * sw) / tw));
28
+ let r = 0;
29
+ let g = 0;
30
+ let b = 0;
31
+ let n = 0;
32
+ for (let sy = sy0; sy < sy1; sy++) {
33
+ for (let sx = sx0; sx < sx1; sx++) {
34
+ const i = (sy * sw + sx) * 4;
35
+ r += src[i];
36
+ g += src[i + 1];
37
+ b += src[i + 2];
38
+ n++;
39
+ }
40
+ }
41
+ const o = (ty * tw + tx) * 4;
42
+ out[o] = Math.round(r / n);
43
+ out[o + 1] = Math.round(g / n);
44
+ out[o + 2] = Math.round(b / n);
45
+ out[o + 3] = 255;
46
+ }
47
+ }
48
+ return out;
49
+ }
50
+ /**
51
+ * Median-cut quantization of RGBA pixels into at most `maxColors` colors.
52
+ * Returns a flat RGB palette and a per-pixel palette-index array. Each pixel's
53
+ * index is the box it lands in, so no separate nearest-color pass is needed.
54
+ */
55
+ function quantize(rgba, pixelCount, maxColors) {
56
+ const pixels = new Array(pixelCount);
57
+ for (let i = 0; i < pixelCount; i++) {
58
+ const o = i * 4;
59
+ pixels[i] = { r: rgba[o], g: rgba[o + 1], b: rgba[o + 2] };
60
+ }
61
+ const refs = pixels.map((_, i) => i);
62
+ const boxes = [{ start: 0, end: refs.length }];
63
+ const widestChannel = (box) => {
64
+ let rmin = 255;
65
+ let rmax = 0;
66
+ let gmin = 255;
67
+ let gmax = 0;
68
+ let bmin = 255;
69
+ let bmax = 0;
70
+ for (let i = box.start; i < box.end; i++) {
71
+ const p = pixels[refs[i]];
72
+ if (p.r < rmin)
73
+ rmin = p.r;
74
+ if (p.r > rmax)
75
+ rmax = p.r;
76
+ if (p.g < gmin)
77
+ gmin = p.g;
78
+ if (p.g > gmax)
79
+ gmax = p.g;
80
+ if (p.b < bmin)
81
+ bmin = p.b;
82
+ if (p.b > bmax)
83
+ bmax = p.b;
84
+ }
85
+ const rr = rmax - rmin;
86
+ const gr = gmax - gmin;
87
+ const br = bmax - bmin;
88
+ if (rr >= gr && rr >= br)
89
+ return { channel: 'r', range: rr };
90
+ if (gr >= br)
91
+ return { channel: 'g', range: gr };
92
+ return { channel: 'b', range: br };
93
+ };
94
+ while (boxes.length < maxColors) {
95
+ let target = -1;
96
+ let best = 0;
97
+ for (let i = 0; i < boxes.length; i++) {
98
+ const box = boxes[i];
99
+ if (box.end - box.start < 2)
100
+ continue;
101
+ const { range } = widestChannel(box);
102
+ if (range > best) {
103
+ best = range;
104
+ target = i;
105
+ }
106
+ }
107
+ if (target < 0 || best <= 0)
108
+ break;
109
+ const box = boxes[target];
110
+ const { channel } = widestChannel(box);
111
+ const slice = refs
112
+ .slice(box.start, box.end)
113
+ .sort((a, b) => pixels[a][channel] - pixels[b][channel]);
114
+ for (let i = 0; i < slice.length; i++)
115
+ refs[box.start + i] = slice[i];
116
+ const mid = box.start + ((box.end - box.start) >> 1);
117
+ boxes.splice(target, 1, { start: box.start, end: mid }, { start: mid, end: box.end });
118
+ }
119
+ const palette = new Uint8Array(boxes.length * 3);
120
+ const indices = new Uint8Array(pixelCount);
121
+ boxes.forEach((box, bi) => {
122
+ let r = 0;
123
+ let g = 0;
124
+ let b = 0;
125
+ const n = box.end - box.start;
126
+ for (let i = box.start; i < box.end; i++) {
127
+ const ref = refs[i];
128
+ const p = pixels[ref];
129
+ r += p.r;
130
+ g += p.g;
131
+ b += p.b;
132
+ indices[ref] = bi;
133
+ }
134
+ palette[bi * 3] = Math.round(r / Math.max(1, n));
135
+ palette[bi * 3 + 1] = Math.round(g / Math.max(1, n));
136
+ palette[bi * 3 + 2] = Math.round(b / Math.max(1, n));
137
+ });
138
+ return { palette, indices };
139
+ }
140
+ /** Build a pixel-image screenData payload from a palette + index array. */
141
+ function buildPixelImageScreen(width, height, palette, indices) {
142
+ const paletteCount = palette.length / 3;
143
+ return {
144
+ layout: 'pixel-image',
145
+ values: [
146
+ { label: 'fmt', value: `idx8;${width}x${height};p${paletteCount}` },
147
+ { label: 'palette', value: Buffer.from(palette).toString('base64') },
148
+ { label: 'pixels', value: Buffer.from(indices).toString('base64') },
149
+ ],
150
+ };
151
+ }
152
+ /** FNV-1a hash over bytes — cheap change detection for the rendered frame. */
153
+ function fnv1a(bytes) {
154
+ let h = 0x811c9dc5;
155
+ for (let i = 0; i < bytes.length; i++) {
156
+ h ^= bytes[i];
157
+ h = Math.imul(h, 0x01000193);
158
+ }
159
+ return h >>> 0;
160
+ }
161
+ /**
162
+ * Decode a PNG screenshot into a pixel-image screenData payload, downscaling to
163
+ * the target resolution if the source isn't already there (box-filter fallback
164
+ * for when Chrome's fractional deviceScaleFactor doesn't rasterize natively).
165
+ */
166
+ function pngToPixelImage(pngBuffer, maxColors = 32, targetW = exports.TARGET_W, targetH = exports.TARGET_H) {
167
+ const { width, height, data } = decodePng(pngBuffer);
168
+ let rgba = data;
169
+ let w = width;
170
+ let h = height;
171
+ if (width !== targetW || height !== targetH) {
172
+ rgba = boxDownscale(data, width, height, targetW, targetH);
173
+ w = targetW;
174
+ h = targetH;
175
+ }
176
+ const { palette, indices } = quantize(rgba, w * h, maxColors);
177
+ const screenData = buildPixelImageScreen(w, h, palette, indices);
178
+ const byteSize = screenData.values.reduce((sum, v) => sum + v.value.length, 0);
179
+ return { screenData, byteSize, hash: fnv1a(indices) };
180
+ }
181
+ //# sourceMappingURL=pixelate.js.map
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}