@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,140 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ProcessWatcherNPC = 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 process_scanner_js_1 = require("./process-scanner.js");
8
+ const defaults_js_1 = require("./defaults.js");
9
+ /** Max per-process panels on the companion dashboard. */
10
+ const MAX_PANELS = 8;
11
+ class ProcessWatcherNPC extends polling_npc_js_1.PollingNPC {
12
+ processes;
13
+ cpuWarn;
14
+ memWarn;
15
+ cpuHistories = new Map();
16
+ previousRunning = null;
17
+ constructor(config) {
18
+ if (process.platform === 'win32') {
19
+ throw new Error('process-watcher is not supported on Windows (relies on the POSIX `ps` command). ' +
20
+ 'Run it under WSL/Linux, or use system-monitor instead.');
21
+ }
22
+ super(config);
23
+ this.processes = config.processes;
24
+ this.cpuWarn = config.cpuWarn ?? defaults_js_1.DEFAULTS.cpuWarn;
25
+ this.memWarn = config.memWarn ?? defaults_js_1.DEFAULTS.memWarn;
26
+ }
27
+ get name() {
28
+ return 'process-watcher';
29
+ }
30
+ get defaultDisplayName() {
31
+ return 'Processes';
32
+ }
33
+ get archetype() {
34
+ return 'service';
35
+ }
36
+ get heartbeatSeconds() {
37
+ return 30;
38
+ }
39
+ get companion() {
40
+ return {
41
+ screenType: 'dashboard-wall',
42
+ size: { w: 2, h: 2 },
43
+ placement: 'on-desk',
44
+ label: 'Processes',
45
+ };
46
+ }
47
+ async collect() {
48
+ const infos = await (0, process_scanner_js_1.scanProcesses)(this.processes);
49
+ const statuses = [];
50
+ const messageParts = [];
51
+ const metrics = {};
52
+ const panels = [];
53
+ let totalRunning = 0;
54
+ let totalMissing = 0;
55
+ for (const info of infos) {
56
+ const safeKey = info.name.replace(/[^a-zA-Z0-9_]/g, '_');
57
+ if (!info.running) {
58
+ statuses.push('critical');
59
+ messageParts.push(`${info.name}: NOT FOUND`);
60
+ metrics[`${safeKey}_running`] = 0;
61
+ metrics[`${safeKey}_cpu`] = 0;
62
+ metrics[`${safeKey}_mem_mb`] = 0;
63
+ totalMissing++;
64
+ if (panels.length < MAX_PANELS) {
65
+ panels.push({ label: info.name, value: 'DOWN', color: 'red' });
66
+ }
67
+ continue;
68
+ }
69
+ totalRunning++;
70
+ const cpu = info.cpuPercent ?? 0;
71
+ metrics[`${safeKey}_running`] = 1;
72
+ metrics[`${safeKey}_cpu`] = cpu;
73
+ metrics[`${safeKey}_mem_mb`] = info.memMb ?? 0;
74
+ const history = this.cpuHistories.get(info.name) ?? [];
75
+ (0, screen_js_1.pushHistory)(history, cpu);
76
+ this.cpuHistories.set(info.name, history);
77
+ const cpuHigh = cpu >= this.cpuWarn;
78
+ const memHigh = (info.memMb ?? 0) >= this.memWarn;
79
+ if (cpuHigh || memHigh) {
80
+ statuses.push('warning');
81
+ const warnings = [];
82
+ if (cpuHigh)
83
+ warnings.push(`cpu=${cpu}%`);
84
+ if (memHigh)
85
+ warnings.push(`mem=${info.memMb}MB`);
86
+ messageParts.push(`${info.name}: WARN (${warnings.join(', ')})`);
87
+ }
88
+ else {
89
+ statuses.push('healthy');
90
+ messageParts.push(`${info.name}: OK`);
91
+ }
92
+ if (panels.length < MAX_PANELS) {
93
+ panels.push({
94
+ label: info.name,
95
+ value: `${cpu}%`,
96
+ color: cpuHigh || memHigh ? 'amber' : 'green',
97
+ sparkline: [...history],
98
+ });
99
+ }
100
+ }
101
+ metrics['total_watched'] = this.processes.length;
102
+ metrics['total_running'] = totalRunning;
103
+ metrics['total_missing'] = totalMissing;
104
+ // Transition speech: compare the running set to last poll's. On the first
105
+ // poll we only seed the set (no bogus "is back" for already-running procs).
106
+ const currentRunning = new Set(infos.filter((i) => i.running).map((i) => i.name));
107
+ let transitions = [];
108
+ if (this.previousRunning !== null) {
109
+ transitions = (0, process_scanner_js_1.detectTransitions)(this.previousRunning, currentRunning);
110
+ }
111
+ this.previousRunning = currentRunning;
112
+ const status = (0, thresholds_js_1.worstStatus)(statuses);
113
+ const mood = deriveMood(status);
114
+ // A death message takes over the bubble this cycle so it reads as an alert.
115
+ const message = transitions.length > 0 ? transitions.join(' · ') : messageParts.join(' | ');
116
+ const screenData = { layout: 'multi-metric', values: panels };
117
+ return {
118
+ status,
119
+ mood,
120
+ message,
121
+ subStatus: totalMissing > 0 ? 'process-down' : undefined,
122
+ screenData,
123
+ metrics,
124
+ };
125
+ }
126
+ }
127
+ exports.ProcessWatcherNPC = ProcessWatcherNPC;
128
+ function deriveMood(status) {
129
+ switch (status) {
130
+ case 'healthy':
131
+ return 'happy';
132
+ case 'warning':
133
+ return 'concerned';
134
+ case 'critical':
135
+ return 'stressed';
136
+ default:
137
+ return 'idle';
138
+ }
139
+ }
140
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detectTransitions = detectTransitions;
4
+ exports.scanProcesses = scanProcesses;
5
+ const node_child_process_1 = require("node:child_process");
6
+ const node_util_1 = require("node:util");
7
+ const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
8
+ /**
9
+ * Compare the previous and current running-process sets and return speech
10
+ * strings for any transitions: "<name> died!" when a process disappears and
11
+ * "<name> is back" when one returns.
12
+ */
13
+ function detectTransitions(prev, curr) {
14
+ const messages = [];
15
+ for (const name of prev) {
16
+ if (!curr.has(name))
17
+ messages.push(`${name} died!`);
18
+ }
19
+ for (const name of curr) {
20
+ if (!prev.has(name))
21
+ messages.push(`${name} is back`);
22
+ }
23
+ return messages;
24
+ }
25
+ /**
26
+ * Parse the output of `ps` into structured entries.
27
+ * Expects columns: PID, %CPU, RSS (KB), COMMAND
28
+ */
29
+ function parsePsOutput(stdout) {
30
+ const lines = stdout.trim().split('\n');
31
+ // Skip the header line
32
+ const entries = [];
33
+ for (let i = 1; i < lines.length; i++) {
34
+ const line = lines[i].trim();
35
+ if (!line)
36
+ continue;
37
+ // Fields are whitespace-separated: PID, %CPU, RSS, COMMAND (may contain spaces)
38
+ const parts = line.split(/\s+/);
39
+ if (parts.length < 4)
40
+ continue;
41
+ const pid = parseInt(parts[0], 10);
42
+ const cpu = parseFloat(parts[1]);
43
+ const memKb = parseInt(parts[2], 10);
44
+ // COMMAND is everything from the 4th column onward
45
+ const comm = parts.slice(3).join(' ');
46
+ if (Number.isNaN(pid) || Number.isNaN(cpu) || Number.isNaN(memKb))
47
+ continue;
48
+ entries.push({ pid, cpu, memKb, comm });
49
+ }
50
+ return entries;
51
+ }
52
+ /**
53
+ * Match a process name against a ps COMMAND column.
54
+ * Matches if the basename of the command path equals the search name,
55
+ * or if the command contains the search name.
56
+ */
57
+ function matchesName(comm, searchName) {
58
+ // Get the basename of the command (strip path and arguments)
59
+ const basename = comm.split('/').pop()?.split(/\s/)[0] ?? '';
60
+ if (basename === searchName)
61
+ return true;
62
+ // Also check if the full command string contains the name
63
+ // (handles cases like "python my-script.py" when searching for "python")
64
+ return comm.toLowerCase().includes(searchName.toLowerCase());
65
+ }
66
+ /**
67
+ * Scan for processes by name and return their status and resource usage.
68
+ * Works on macOS and Linux by using `ps` to list all processes.
69
+ */
70
+ async function scanProcesses(names) {
71
+ let psArgs;
72
+ if (process.platform === 'darwin') {
73
+ // macOS: -e selects all processes, -o specifies output columns
74
+ psArgs = ['-eo', 'pid,pcpu,rss,comm'];
75
+ }
76
+ else {
77
+ // Linux: -eo with similar columns
78
+ psArgs = ['-eo', 'pid,pcpu,rss,comm'];
79
+ }
80
+ let entries;
81
+ try {
82
+ const { stdout } = await execFileAsync('ps', psArgs);
83
+ entries = parsePsOutput(stdout);
84
+ }
85
+ catch {
86
+ // If ps fails, report all processes as unknown / not running
87
+ return names.map((name) => ({ name, running: false }));
88
+ }
89
+ const results = [];
90
+ for (const name of names) {
91
+ // Find all matching entries for this process name
92
+ const matching = entries.filter((e) => matchesName(e.comm, name));
93
+ if (matching.length === 0) {
94
+ results.push({ name, running: false });
95
+ continue;
96
+ }
97
+ // Aggregate: take the first matching PID, sum CPU and memory across all instances
98
+ let totalCpu = 0;
99
+ let totalMemKb = 0;
100
+ for (const entry of matching) {
101
+ totalCpu += entry.cpu;
102
+ totalMemKb += entry.memKb;
103
+ }
104
+ results.push({
105
+ name,
106
+ running: true,
107
+ pid: matching[0].pid,
108
+ cpuPercent: Math.round(totalCpu * 100) / 100,
109
+ memMb: Math.round((totalMemKb / 1024) * 100) / 100,
110
+ });
111
+ }
112
+ return results;
113
+ }
114
+ //# sourceMappingURL=process-scanner.js.map
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULTS = void 0;
4
+ exports.DEFAULTS = {
5
+ pollInterval: 60000, // 60s clock tick
6
+ weatherRefreshMs: 15 * 60 * 1000, // refresh weather every 15 minutes
7
+ fetchTimeoutMs: 5000,
8
+ };
9
+ //# sourceMappingURL=defaults.js.map
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ReceptionistNPC = void 0;
4
+ const polling_npc_js_1 = require("../../core/polling-npc.js");
5
+ const defaults_js_1 = require("./defaults.js");
6
+ const weather_js_1 = require("./weather.js");
7
+ class ReceptionistNPC extends polling_npc_js_1.PollingNPC {
8
+ explicitCoords;
9
+ location;
10
+ fahrenheit;
11
+ coords = null;
12
+ geoAttempted = false;
13
+ weather = null;
14
+ lastWeatherFetch = 0;
15
+ constructor(config) {
16
+ super({ ...config, pollInterval: config.pollInterval ?? defaults_js_1.DEFAULTS.pollInterval });
17
+ this.explicitCoords =
18
+ config.lat !== undefined && config.lon !== undefined
19
+ ? { lat: config.lat, lon: config.lon }
20
+ : null;
21
+ this.location = config.location;
22
+ this.fahrenheit = config.fahrenheit ?? false;
23
+ }
24
+ get name() {
25
+ return 'receptionist';
26
+ }
27
+ get defaultDisplayName() {
28
+ return 'Reception';
29
+ }
30
+ get archetype() {
31
+ return 'custom';
32
+ }
33
+ get behavior() {
34
+ return {
35
+ greetPlayers: true,
36
+ patrol: { everyMinutes: [15, 30], stops: 5 },
37
+ idle: 'stay',
38
+ home: { near: 'entrance' },
39
+ };
40
+ }
41
+ get companion() {
42
+ return {
43
+ screenType: 'welcome-sign',
44
+ size: { w: 2, h: 1 },
45
+ placement: 'adjacent-wall',
46
+ label: 'Welcome',
47
+ };
48
+ }
49
+ async collect() {
50
+ await this.refreshWeather();
51
+ const now = new Date();
52
+ const { day, date, time } = (0, weather_js_1.formatClock)(now);
53
+ const values = [
54
+ { label: 'time', value: time },
55
+ { label: 'day', value: day },
56
+ { label: 'date', value: date },
57
+ ];
58
+ let message = `${day} ${time}`;
59
+ const metrics = { weather_ok: this.weather ? 1 : 0 };
60
+ if (this.weather) {
61
+ const temp = this.fahrenheit
62
+ ? `${(0, weather_js_1.toFahrenheit)(this.weather.tempC)}°F`
63
+ : `${this.weather.tempC}°C`;
64
+ message += ` • ${temp} ${this.weather.text}`;
65
+ values.push({ label: 'temp', value: temp }, { label: 'condition', value: this.weather.text }, { label: 'icon', value: this.weather.icon });
66
+ metrics['temp_c'] = this.weather.tempC;
67
+ metrics['weather_code'] = this.weather.code;
68
+ metrics['wind_kph'] = this.weather.windKph;
69
+ }
70
+ // The receptionist is a greeter/clock — it stays healthy even when weather
71
+ // is unavailable (degraded time-only mode).
72
+ return {
73
+ status: 'healthy',
74
+ mood: 'happy',
75
+ message,
76
+ screenData: { layout: 'welcome-sign', values },
77
+ metrics,
78
+ };
79
+ }
80
+ async refreshWeather() {
81
+ if (!this.coords) {
82
+ await this.resolveCoords();
83
+ }
84
+ if (!this.coords)
85
+ return;
86
+ const now = Date.now();
87
+ if (this.weather && now - this.lastWeatherFetch < defaults_js_1.DEFAULTS.weatherRefreshMs)
88
+ return;
89
+ const w = await (0, weather_js_1.fetchWeather)(this.coords, defaults_js_1.DEFAULTS.fetchTimeoutMs);
90
+ if (w) {
91
+ this.weather = w;
92
+ this.lastWeatherFetch = now;
93
+ }
94
+ }
95
+ async resolveCoords() {
96
+ if (this.explicitCoords) {
97
+ this.coords = this.explicitCoords;
98
+ return;
99
+ }
100
+ // Only attempt geocoding/geo-IP once per (failed) resolution to avoid
101
+ // hammering the keyless endpoints every poll.
102
+ if (this.geoAttempted)
103
+ return;
104
+ this.geoAttempted = true;
105
+ if (this.location) {
106
+ this.coords = await (0, weather_js_1.geocodeLocation)(this.location, defaults_js_1.DEFAULTS.fetchTimeoutMs);
107
+ if (this.coords)
108
+ return;
109
+ this.logger.warn(`Could not geocode "${this.location}", falling back to IP geo`);
110
+ }
111
+ this.coords = await (0, weather_js_1.autoGeo)(defaults_js_1.DEFAULTS.fetchTimeoutMs);
112
+ if (!this.coords) {
113
+ this.logger.warn('Weather geo unavailable — running in time-only mode');
114
+ // Allow a later retry.
115
+ this.geoAttempted = false;
116
+ }
117
+ }
118
+ }
119
+ exports.ReceptionistNPC = ReceptionistNPC;
120
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ /**
3
+ * Keyless weather + geolocation via open-meteo, with ipapi.co IP fallback.
4
+ * All network helpers return null on failure so the receptionist can degrade
5
+ * to a time-only mode without erroring.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.describeWmo = describeWmo;
9
+ exports.geocodeLocation = geocodeLocation;
10
+ exports.autoGeo = autoGeo;
11
+ exports.fetchWeather = fetchWeather;
12
+ exports.toFahrenheit = toFahrenheit;
13
+ exports.formatClock = formatClock;
14
+ // WMO weather-interpretation codes → human text + a glyph keyword the
15
+ // welcome-sign renderer understands (matched by /sun|clear|partly|rain|snow|storm|fog/).
16
+ const WMO = {
17
+ 0: { text: 'Clear', icon: 'clear' },
18
+ 1: { text: 'Mainly clear', icon: 'partly' },
19
+ 2: { text: 'Partly cloudy', icon: 'partly' },
20
+ 3: { text: 'Overcast', icon: 'cloud' },
21
+ 45: { text: 'Fog', icon: 'fog' },
22
+ 48: { text: 'Rime fog', icon: 'fog' },
23
+ 51: { text: 'Light drizzle', icon: 'drizzle' },
24
+ 53: { text: 'Drizzle', icon: 'drizzle' },
25
+ 55: { text: 'Heavy drizzle', icon: 'drizzle' },
26
+ 56: { text: 'Freezing drizzle', icon: 'drizzle' },
27
+ 57: { text: 'Freezing drizzle', icon: 'drizzle' },
28
+ 61: { text: 'Light rain', icon: 'rain' },
29
+ 63: { text: 'Rain', icon: 'rain' },
30
+ 65: { text: 'Heavy rain', icon: 'rain' },
31
+ 66: { text: 'Freezing rain', icon: 'rain' },
32
+ 67: { text: 'Freezing rain', icon: 'rain' },
33
+ 71: { text: 'Light snow', icon: 'snow' },
34
+ 73: { text: 'Snow', icon: 'snow' },
35
+ 75: { text: 'Heavy snow', icon: 'snow' },
36
+ 77: { text: 'Snow grains', icon: 'snow' },
37
+ 80: { text: 'Rain showers', icon: 'shower' },
38
+ 81: { text: 'Rain showers', icon: 'shower' },
39
+ 82: { text: 'Violent showers', icon: 'shower' },
40
+ 85: { text: 'Snow showers', icon: 'snow' },
41
+ 86: { text: 'Snow showers', icon: 'snow' },
42
+ 95: { text: 'Thunderstorm', icon: 'storm' },
43
+ 96: { text: 'Thunderstorm', icon: 'storm' },
44
+ 99: { text: 'Thunderstorm', icon: 'storm' },
45
+ };
46
+ /** Map a WMO weather code to display text + a renderer glyph keyword. */
47
+ function describeWmo(code) {
48
+ return WMO[code] ?? { text: 'Unknown', icon: 'cloud' };
49
+ }
50
+ async function fetchJson(url, timeoutMs) {
51
+ const controller = new AbortController();
52
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
53
+ try {
54
+ const res = await fetch(url, {
55
+ signal: controller.signal,
56
+ headers: { 'User-Agent': 'nocville-npc' },
57
+ });
58
+ if (!res.ok)
59
+ return null;
60
+ return (await res.json());
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ finally {
66
+ clearTimeout(timer);
67
+ }
68
+ }
69
+ /** Resolve a place name to coordinates via the open-meteo geocoding API. */
70
+ async function geocodeLocation(name, timeoutMs = 5000) {
71
+ const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(name)}&count=1`;
72
+ const res = await fetchJson(url, timeoutMs);
73
+ const first = res?.['results']?.[0];
74
+ if (!first || typeof first['latitude'] !== 'number')
75
+ return null;
76
+ return {
77
+ lat: first['latitude'],
78
+ lon: first['longitude'],
79
+ label: first['name'] ?? name,
80
+ };
81
+ }
82
+ /** Best-effort IP geolocation via ipapi.co (keyless). */
83
+ async function autoGeo(timeoutMs = 5000) {
84
+ const res = await fetchJson('https://ipapi.co/json/', timeoutMs);
85
+ if (!res || typeof res['latitude'] !== 'number')
86
+ return null;
87
+ return {
88
+ lat: res['latitude'],
89
+ lon: res['longitude'],
90
+ label: res['city'],
91
+ };
92
+ }
93
+ /** Fetch current temperature, weather code, and wind for coordinates. */
94
+ async function fetchWeather(coords, timeoutMs = 5000) {
95
+ const url = `https://api.open-meteo.com/v1/forecast?latitude=${coords.lat}` +
96
+ `&longitude=${coords.lon}&current=temperature_2m,weather_code,wind_speed_10m`;
97
+ const res = await fetchJson(url, timeoutMs);
98
+ const cur = res?.['current'];
99
+ if (!cur || typeof cur['temperature_2m'] !== 'number')
100
+ return null;
101
+ const code = Number(cur['weather_code'] ?? 0);
102
+ const { text, icon } = describeWmo(code);
103
+ return {
104
+ tempC: Math.round(cur['temperature_2m']),
105
+ code,
106
+ text,
107
+ icon,
108
+ windKph: Math.round(Number(cur['wind_speed_10m'] ?? 0)),
109
+ };
110
+ }
111
+ /** Convert Celsius to Fahrenheit, rounded. */
112
+ function toFahrenheit(c) {
113
+ return Math.round((c * 9) / 5 + 32);
114
+ }
115
+ const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
116
+ /** Format a Date into day/date/time parts for the welcome sign. */
117
+ function formatClock(d) {
118
+ const day = DAYS[d.getDay()];
119
+ let h = d.getHours();
120
+ const m = d.getMinutes();
121
+ const ampm = h >= 12 ? 'PM' : 'AM';
122
+ h = h % 12;
123
+ if (h === 0)
124
+ h = 12;
125
+ const time = `${h}:${String(m).padStart(2, '0')} ${ampm}`;
126
+ const date = `${d.getMonth() + 1}/${d.getDate()}`;
127
+ return { day, date, time };
128
+ }
129
+ //# sourceMappingURL=weather.js.map
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createCpuSampler = createCpuSampler;
7
+ exports.collectMemory = collectMemory;
8
+ exports.collectDisk = collectDisk;
9
+ const node_os_1 = __importDefault(require("node:os"));
10
+ const node_fs_1 = require("node:fs");
11
+ function aggregate(cpus) {
12
+ let idle = 0;
13
+ let total = 0;
14
+ for (const cpu of cpus) {
15
+ const t = cpu.times;
16
+ idle += t.idle;
17
+ total += t.user + t.nice + t.sys + t.irq + t.idle;
18
+ }
19
+ return { idle, total };
20
+ }
21
+ /**
22
+ * Stateful CPU sampler that computes usage from the delta between consecutive
23
+ * `os.cpus()` snapshots — no blocking sleep. The first sample reports usage
24
+ * since system boot; subsequent samples report usage since the previous call.
25
+ *
26
+ * `readCpus`/`readLoad` are injectable for testing.
27
+ */
28
+ function createCpuSampler(readCpus = () => node_os_1.default.cpus(), readLoad = () => node_os_1.default.loadavg()[0] ?? 0) {
29
+ let prev = null;
30
+ return {
31
+ sample() {
32
+ const cur = aggregate(readCpus());
33
+ const loadAvg1m = Math.round(readLoad() * 100) / 100;
34
+ const base = prev ?? { idle: 0, total: 0 };
35
+ const idleDiff = cur.idle - base.idle;
36
+ const totalDiff = cur.total - base.total;
37
+ prev = cur;
38
+ const percent = totalDiff > 0 ? Math.round((1 - idleDiff / totalDiff) * 100) : 0;
39
+ return { percent, loadAvg1m };
40
+ },
41
+ };
42
+ }
43
+ /**
44
+ * Collect memory usage from OS.
45
+ */
46
+ function collectMemory() {
47
+ const totalBytes = node_os_1.default.totalmem();
48
+ const freeBytes = node_os_1.default.freemem();
49
+ const usedBytes = totalBytes - freeBytes;
50
+ const totalGb = Math.round((totalBytes / 1024 ** 3) * 100) / 100;
51
+ const usedGb = Math.round((usedBytes / 1024 ** 3) * 100) / 100;
52
+ const percent = Math.round((usedBytes / totalBytes) * 100);
53
+ return { percent, usedGb, totalGb };
54
+ }
55
+ /**
56
+ * Collect disk usage via fs.statfs (Node 18.15+).
57
+ */
58
+ async function collectDisk(path) {
59
+ const stats = await node_fs_1.promises.statfs(path);
60
+ const totalBytes = stats.blocks * stats.bsize;
61
+ const freeBytes = stats.bavail * stats.bsize;
62
+ const usedBytes = totalBytes - freeBytes;
63
+ const totalGb = Math.round((totalBytes / 1024 ** 3) * 100) / 100;
64
+ const usedGb = Math.round((usedBytes / 1024 ** 3) * 100) / 100;
65
+ const percent = totalBytes > 0 ? Math.round((usedBytes / totalBytes) * 100) : 0;
66
+ return { percent, usedGb, totalGb };
67
+ }
68
+ //# sourceMappingURL=collectors.js.map
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_THRESHOLDS = exports.DEFAULTS = void 0;
4
+ exports.DEFAULTS = {
5
+ pollInterval: 15000,
6
+ diskPath: '/',
7
+ };
8
+ exports.DEFAULT_THRESHOLDS = {
9
+ cpu: { warning: 75, critical: 90, direction: 'above' },
10
+ mem: { warning: 75, critical: 90, direction: 'above' },
11
+ disk: { warning: 75, critical: 90, direction: 'above' },
12
+ };
13
+ //# sourceMappingURL=defaults.js.map