@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,246 @@
1
+ import { PollingNPC } from '../../core/polling-npc.js';
2
+ import { evaluateThreshold, worstStatus } from '../../core/thresholds.js';
3
+ import { statusColor, pushHistory } from '../../core/screen.js';
4
+ import { numberFlag, stringFlag } from '../../core/config.js';
5
+ import { checkDns, checkHttp, checkLatency } from './checks.js';
6
+ import { DEFAULTS } from './defaults.js';
7
+ /**
8
+ * Derive overall status from live per-check thresholds. All checks failing is
9
+ * critical; any partial failure is at least a warning; slow-but-working checks
10
+ * escalate per their own thresholds.
11
+ */
12
+ export function evaluateConnectivityStatus(sample, thresholds) {
13
+ if (sample.allFailed)
14
+ return 'critical';
15
+ const timeStatus = worstStatus([
16
+ evaluateThreshold(sample.dnsAvg, thresholds.dnsTime),
17
+ evaluateThreshold(sample.httpAvg, thresholds.httpTime),
18
+ evaluateThreshold(sample.latencyAvg, thresholds.latencyTime),
19
+ ]);
20
+ const failStatus = sample.anyFailed ? 'warning' : 'healthy';
21
+ return worstStatus([timeStatus, failStatus]);
22
+ }
23
+ /** Scale a threshold set by a ratio, preserving direction. */
24
+ function scaleThresholds(base, ratio) {
25
+ const scale = (t) => ({
26
+ warning: Math.round(t.warning * ratio),
27
+ critical: Math.round(t.critical * ratio),
28
+ direction: t.direction,
29
+ });
30
+ return {
31
+ dnsTime: scale(base.dnsTime),
32
+ httpTime: scale(base.httpTime),
33
+ latencyTime: scale(base.latencyTime),
34
+ };
35
+ }
36
+ export class InternetConnectivityNPC extends PollingNPC {
37
+ dnsHosts;
38
+ httpUrls;
39
+ latencyHosts;
40
+ httpTimeout;
41
+ thresholds;
42
+ dnsHistory = [];
43
+ httpHistory = [];
44
+ latencyHistory = [];
45
+ constructor(config) {
46
+ super({
47
+ ...config,
48
+ pollInterval: config.pollInterval ?? DEFAULTS.pollInterval,
49
+ });
50
+ this.dnsHosts = config.dnsHosts ?? DEFAULTS.dnsHosts;
51
+ this.httpUrls = config.httpUrls ?? DEFAULTS.httpUrls;
52
+ this.latencyHosts = config.latencyHosts
53
+ ? parseLatencyHosts(config.latencyHosts)
54
+ : DEFAULTS.latencyHosts;
55
+ this.httpTimeout = config.httpTimeout ?? DEFAULTS.httpTimeout;
56
+ // Threshold resolution: explicit thresholds win; else if the legacy
57
+ // --slow-threshold differs from the default, scale the live thresholds by
58
+ // that ratio for back-compat; else use the defaults.
59
+ if (config.thresholds) {
60
+ this.thresholds = config.thresholds;
61
+ }
62
+ else if (config.slowThreshold && config.slowThreshold !== DEFAULTS.slowThreshold) {
63
+ this.thresholds = scaleThresholds(DEFAULTS.thresholds, config.slowThreshold / DEFAULTS.slowThreshold);
64
+ }
65
+ else {
66
+ this.thresholds = DEFAULTS.thresholds;
67
+ }
68
+ }
69
+ get name() {
70
+ return 'internet-connectivity';
71
+ }
72
+ get defaultDisplayName() {
73
+ return 'Internet';
74
+ }
75
+ get archetype() {
76
+ return 'gateway';
77
+ }
78
+ get behavior() {
79
+ return { idle: 'wander' };
80
+ }
81
+ get companion() {
82
+ return {
83
+ screenType: 'dashboard-wall',
84
+ size: { w: 2, h: 2 },
85
+ placement: 'adjacent-wall',
86
+ label: 'Internet',
87
+ };
88
+ }
89
+ async collect() {
90
+ const [dnsResults, httpResults, latencyResults] = await Promise.all([
91
+ checkDns(this.dnsHosts),
92
+ checkHttp(this.httpUrls, this.httpTimeout),
93
+ checkLatency(this.latencyHosts),
94
+ ]);
95
+ const dnsOk = dnsResults.filter((r) => r.ok).length;
96
+ const dnsFail = dnsResults.length - dnsOk;
97
+ const httpOk = httpResults.filter((r) => r.ok).length;
98
+ const httpFail = httpResults.length - httpOk;
99
+ const dnsAvg = avgTime(dnsResults);
100
+ const httpAvg = avgTime(httpResults);
101
+ const latencyAvg = avgTime(latencyResults);
102
+ const isOnline = dnsOk > 0 || httpOk > 0;
103
+ const allFailed = dnsOk === 0 && httpOk === 0;
104
+ const anyFailed = dnsResults.some((r) => !r.ok) ||
105
+ httpResults.some((r) => !r.ok) ||
106
+ latencyResults.some((r) => !r.ok);
107
+ const status = evaluateConnectivityStatus({ allFailed, anyFailed, dnsAvg, httpAvg, latencyAvg }, this.thresholds);
108
+ const mood = statusToMood(status);
109
+ const message = allFailed
110
+ ? this.buildOfflineMessage(dnsResults, httpResults)
111
+ : this.buildOnlineMessage(dnsAvg, httpAvg, latencyAvg);
112
+ pushHistory(this.dnsHistory, round(dnsAvg));
113
+ pushHistory(this.httpHistory, round(httpAvg));
114
+ pushHistory(this.latencyHistory, round(latencyAvg));
115
+ const screenData = {
116
+ layout: 'multi-metric',
117
+ values: [
118
+ {
119
+ label: 'DNS',
120
+ value: `${round(dnsAvg)}ms`,
121
+ color: statusColor(evaluateThreshold(dnsAvg, this.thresholds.dnsTime)),
122
+ sparkline: [...this.dnsHistory],
123
+ },
124
+ {
125
+ label: 'HTTP',
126
+ value: `${round(httpAvg)}ms`,
127
+ color: statusColor(evaluateThreshold(httpAvg, this.thresholds.httpTime)),
128
+ sparkline: [...this.httpHistory],
129
+ },
130
+ {
131
+ label: 'TCP',
132
+ value: `${round(latencyAvg)}ms`,
133
+ color: statusColor(evaluateThreshold(latencyAvg, this.thresholds.latencyTime)),
134
+ sparkline: [...this.latencyHistory],
135
+ },
136
+ { label: 'NET', value: isOnline ? 'ONLINE' : 'OFFLINE', color: isOnline ? 'green' : 'red' },
137
+ ],
138
+ };
139
+ return {
140
+ status,
141
+ mood,
142
+ message,
143
+ subStatus: isOnline ? undefined : 'offline',
144
+ screenData,
145
+ metrics: {
146
+ dns_avg_ms: round(dnsAvg),
147
+ http_avg_ms: round(httpAvg),
148
+ latency_avg_ms: round(latencyAvg),
149
+ dns_ok_count: dnsOk,
150
+ dns_fail_count: dnsFail,
151
+ http_ok_count: httpOk,
152
+ http_fail_count: httpFail,
153
+ is_online: isOnline ? 1 : 0,
154
+ },
155
+ };
156
+ }
157
+ buildOnlineMessage(dnsAvg, httpAvg, latencyAvg) {
158
+ return `Online | DNS: ${round(dnsAvg)}ms | HTTP: ${round(httpAvg)}ms | Latency: ${round(latencyAvg)}ms`;
159
+ }
160
+ buildOfflineMessage(dnsResults, httpResults) {
161
+ const problems = [];
162
+ if (dnsResults.every((r) => !r.ok)) {
163
+ problems.push('DNS resolution failed');
164
+ }
165
+ if (httpResults.every((r) => !r.ok)) {
166
+ problems.push('no HTTP connectivity');
167
+ }
168
+ return `OFFLINE: ${problems.join(', ') || 'all checks failed'}`;
169
+ }
170
+ /**
171
+ * Build config from CLI flags. Merges with any programmatic overrides.
172
+ */
173
+ static configFromFlags(flags) {
174
+ const config = {};
175
+ const dnsHostsRaw = stringFlag(flags, 'dns-hosts');
176
+ if (dnsHostsRaw) {
177
+ config.dnsHosts = dnsHostsRaw.split(',').map((s) => s.trim());
178
+ }
179
+ const httpUrlsRaw = stringFlag(flags, 'http-urls');
180
+ if (httpUrlsRaw) {
181
+ config.httpUrls = httpUrlsRaw.split(',').map((s) => s.trim());
182
+ }
183
+ const latencyHostsRaw = stringFlag(flags, 'latency-hosts');
184
+ if (latencyHostsRaw) {
185
+ config.latencyHosts = latencyHostsRaw.split(',').map((s) => {
186
+ const trimmed = s.trim();
187
+ const colonIndex = trimmed.lastIndexOf(':');
188
+ if (colonIndex !== -1) {
189
+ const host = trimmed.slice(0, colonIndex);
190
+ const port = Number(trimmed.slice(colonIndex + 1));
191
+ if (!Number.isNaN(port))
192
+ return { host, port };
193
+ }
194
+ return { host: trimmed, port: 80 };
195
+ });
196
+ }
197
+ const slowThreshold = numberFlag(flags, 'slow-threshold');
198
+ if (slowThreshold !== undefined)
199
+ config.slowThreshold = slowThreshold;
200
+ const pollInterval = numberFlag(flags, 'poll-interval');
201
+ if (pollInterval !== undefined)
202
+ config.pollInterval = pollInterval;
203
+ return config;
204
+ }
205
+ }
206
+ // ---------- Helpers ----------
207
+ function avgTime(results) {
208
+ if (results.length === 0)
209
+ return 0;
210
+ const sum = results.reduce((acc, r) => acc + r.timeMs, 0);
211
+ return sum / results.length;
212
+ }
213
+ function round(n) {
214
+ return Math.round(n * 100) / 100;
215
+ }
216
+ function statusToMood(status) {
217
+ switch (status) {
218
+ case 'healthy':
219
+ return 'happy';
220
+ case 'warning':
221
+ return 'concerned';
222
+ case 'critical':
223
+ return 'stressed';
224
+ default:
225
+ return 'idle';
226
+ }
227
+ }
228
+ /**
229
+ * Normalise latency hosts from either string[] (CLI) or LatencyHost[].
230
+ * String entries can be "host" or "host:port".
231
+ */
232
+ function parseLatencyHosts(input) {
233
+ return input.map((item) => {
234
+ if (typeof item === 'object')
235
+ return item;
236
+ const colonIndex = item.lastIndexOf(':');
237
+ if (colonIndex !== -1) {
238
+ const host = item.slice(0, colonIndex);
239
+ const port = Number(item.slice(colonIndex + 1));
240
+ if (!Number.isNaN(port))
241
+ return { host, port };
242
+ }
243
+ return { host: item, port: 80 };
244
+ });
245
+ }
246
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,18 @@
1
+ import type { ScreenData } from '../../core/types.js';
2
+ export type LogLineLevel = 'error' | 'warning' | 'info';
3
+ export interface RecentLine {
4
+ text: string;
5
+ level: LogLineLevel;
6
+ }
7
+ /** Classify a raw log line against the error/warning patterns. */
8
+ export declare function classifyLine(line: string, errorRe: RegExp, warningRe: RegExp): LogLineLevel;
9
+ /**
10
+ * Push a line into the bounded ring buffer: each entry is capped at `cap`
11
+ * characters and the buffer keeps only the last `max` entries.
12
+ */
13
+ export declare function pushRecent(buffer: RecentLine[], line: string, level: LogLineLevel, max?: number, cap?: number): RecentLine[];
14
+ /** Build a log-terminal screen payload from the ring buffer, colored per level. */
15
+ export declare function buildLogScreen(buffer: RecentLine[]): ScreenData;
16
+ /** True when enough time has elapsed since the last burst push to emit again. */
17
+ export declare function shouldEmitBurst(lastEmit: number, now: number, windowMs?: number): boolean;
18
+ //# sourceMappingURL=buffer.d.ts.map
@@ -0,0 +1,34 @@
1
+ /** Classify a raw log line against the error/warning patterns. */
2
+ export function classifyLine(line, errorRe, warningRe) {
3
+ if (errorRe.test(line))
4
+ return 'error';
5
+ if (warningRe.test(line))
6
+ return 'warning';
7
+ return 'info';
8
+ }
9
+ /**
10
+ * Push a line into the bounded ring buffer: each entry is capped at `cap`
11
+ * characters and the buffer keeps only the last `max` entries.
12
+ */
13
+ export function pushRecent(buffer, line, level, max = 12, cap = 80) {
14
+ buffer.push({ text: line.slice(0, cap), level });
15
+ if (buffer.length > max)
16
+ buffer.splice(0, buffer.length - max);
17
+ return buffer;
18
+ }
19
+ /** Build a log-terminal screen payload from the ring buffer, colored per level. */
20
+ export function buildLogScreen(buffer) {
21
+ return {
22
+ layout: 'log-terminal',
23
+ values: buffer.map((l) => ({
24
+ label: '',
25
+ value: l.text,
26
+ color: l.level === 'error' ? 'red' : l.level === 'warning' ? 'amber' : 'green',
27
+ })),
28
+ };
29
+ }
30
+ /** True when enough time has elapsed since the last burst push to emit again. */
31
+ export function shouldEmitBurst(lastEmit, now, windowMs = 5000) {
32
+ return now - lastEmit >= windowMs;
33
+ }
34
+ //# sourceMappingURL=buffer.js.map
@@ -0,0 +1,9 @@
1
+ export declare const DEFAULTS: {
2
+ reportInterval: number;
3
+ windowSeconds: number;
4
+ errorPattern: string;
5
+ warningPattern: string;
6
+ errorWarn: number;
7
+ errorCrit: number;
8
+ };
9
+ //# sourceMappingURL=defaults.d.ts.map
@@ -0,0 +1,9 @@
1
+ export const DEFAULTS = {
2
+ reportInterval: 15000, // 15 seconds
3
+ windowSeconds: 300, // 5 minutes
4
+ errorPattern: 'ERROR|FATAL',
5
+ warningPattern: 'WARN',
6
+ errorWarn: 5, // errors in window before warning status
7
+ errorCrit: 20, // errors in window before critical status
8
+ };
9
+ //# sourceMappingURL=defaults.js.map
@@ -0,0 +1,45 @@
1
+ import { BaseNPC } from '../../core/base-npc.js';
2
+ import type { NpcConfig, Archetype, BehaviorHints, CompanionDecl } from '../../core/types.js';
3
+ export interface LogWatcherConfig extends NpcConfig {
4
+ files: string[];
5
+ errorPattern?: string;
6
+ warningPattern?: string;
7
+ windowSeconds?: number;
8
+ errorWarn?: number;
9
+ errorCrit?: number;
10
+ reportInterval?: number;
11
+ }
12
+ export declare class LogWatcherNPC extends BaseNPC {
13
+ private tailers;
14
+ private timer;
15
+ private entries;
16
+ private recentLines;
17
+ private lastBurstPush;
18
+ private errorRegex;
19
+ private warningRegex;
20
+ private windowMs;
21
+ private errorWarn;
22
+ private errorCrit;
23
+ private reportInterval;
24
+ private files;
25
+ constructor(config: LogWatcherConfig);
26
+ get name(): string;
27
+ protected get defaultDisplayName(): string;
28
+ get archetype(): Archetype;
29
+ get heartbeatSeconds(): number;
30
+ get behavior(): BehaviorHints;
31
+ get companion(): CompanionDecl;
32
+ protected onStart(): Promise<void>;
33
+ protected onStop(): Promise<void>;
34
+ private handleLine;
35
+ /** A fresh error triggers an immediate push, debounced to at most once / 5s. */
36
+ private onErrorBurst;
37
+ private pruneWindow;
38
+ private report;
39
+ /**
40
+ * Build and push the current status. If `overrideMessage` is set (burst push),
41
+ * it drives the bubble and the status floors at warning.
42
+ */
43
+ private pushReport;
44
+ }
45
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,157 @@
1
+ import { basename } from 'node:path';
2
+ import { BaseNPC } from '../../core/base-npc.js';
3
+ import { evaluateThreshold, worstStatus } from '../../core/thresholds.js';
4
+ import { truncate } from '../../core/text.js';
5
+ import { createTailer } from './tail.js';
6
+ import { classifyLine, pushRecent, buildLogScreen, shouldEmitBurst } from './buffer.js';
7
+ import { DEFAULTS } from './defaults.js';
8
+ export class LogWatcherNPC extends BaseNPC {
9
+ tailers = [];
10
+ timer = null;
11
+ entries = [];
12
+ recentLines = [];
13
+ lastBurstPush = 0;
14
+ errorRegex;
15
+ warningRegex;
16
+ windowMs;
17
+ errorWarn;
18
+ errorCrit;
19
+ reportInterval;
20
+ files;
21
+ constructor(config) {
22
+ super(config);
23
+ this.files = config.files;
24
+ this.errorRegex = new RegExp(config.errorPattern ?? DEFAULTS.errorPattern);
25
+ this.warningRegex = new RegExp(config.warningPattern ?? DEFAULTS.warningPattern);
26
+ this.windowMs = (config.windowSeconds ?? DEFAULTS.windowSeconds) * 1000;
27
+ this.errorWarn = config.errorWarn ?? DEFAULTS.errorWarn;
28
+ this.errorCrit = config.errorCrit ?? DEFAULTS.errorCrit;
29
+ this.reportInterval = config.reportInterval ?? DEFAULTS.reportInterval;
30
+ }
31
+ get name() {
32
+ return 'log-watcher';
33
+ }
34
+ get defaultDisplayName() {
35
+ const first = this.files[0];
36
+ return first ? `Logs: ${basename(first)}` : 'Logs';
37
+ }
38
+ get archetype() {
39
+ return 'service';
40
+ }
41
+ get heartbeatSeconds() {
42
+ return 30;
43
+ }
44
+ get behavior() {
45
+ return { idle: 'wander' };
46
+ }
47
+ get companion() {
48
+ return {
49
+ screenType: 'log-terminal',
50
+ size: { w: 2, h: 2 },
51
+ placement: 'adjacent-wall',
52
+ label: this.files[0] ? basename(this.files[0]) : 'log',
53
+ };
54
+ }
55
+ async onStart() {
56
+ for (const filePath of this.files) {
57
+ this.logger.info(`Tailing ${filePath}`);
58
+ const tailer = createTailer(filePath, (line) => {
59
+ this.handleLine(line);
60
+ });
61
+ this.tailers.push(tailer);
62
+ }
63
+ // Report immediately, then on interval
64
+ await this.report();
65
+ this.timer = setInterval(() => {
66
+ void this.report();
67
+ }, this.reportInterval);
68
+ }
69
+ async onStop() {
70
+ if (this.timer) {
71
+ clearInterval(this.timer);
72
+ this.timer = null;
73
+ }
74
+ for (const tailer of this.tailers) {
75
+ tailer.close();
76
+ }
77
+ this.tailers = [];
78
+ }
79
+ handleLine(line) {
80
+ const now = Date.now();
81
+ const level = classifyLine(line, this.errorRegex, this.warningRegex);
82
+ // Ring buffer holds ALL lines (matched ones flagged) for the log TV.
83
+ pushRecent(this.recentLines, line, level);
84
+ if (level === 'error') {
85
+ this.entries.push({ timestamp: now, type: 'error', message: line });
86
+ this.onErrorBurst(line);
87
+ }
88
+ else if (level === 'warning') {
89
+ this.entries.push({ timestamp: now, type: 'warning', message: line });
90
+ }
91
+ }
92
+ /** A fresh error triggers an immediate push, debounced to at most once / 5s. */
93
+ onErrorBurst(line) {
94
+ const now = Date.now();
95
+ if (!shouldEmitBurst(this.lastBurstPush, now))
96
+ return;
97
+ this.lastBurstPush = now;
98
+ void this.pushReport(truncate(line, 200), 'error');
99
+ }
100
+ pruneWindow() {
101
+ const cutoff = Date.now() - this.windowMs;
102
+ this.entries = this.entries.filter((e) => e.timestamp >= cutoff);
103
+ }
104
+ async report() {
105
+ if (this.state !== 'running')
106
+ return;
107
+ await this.pushReport();
108
+ }
109
+ /**
110
+ * Build and push the current status. If `overrideMessage` is set (burst push),
111
+ * it drives the bubble and the status floors at warning.
112
+ */
113
+ async pushReport(overrideMessage, subStatus) {
114
+ if (this.state !== 'running')
115
+ return;
116
+ this.pruneWindow();
117
+ const errorCount = this.entries.filter((e) => e.type === 'error').length;
118
+ const warningCount = this.entries.filter((e) => e.type === 'warning').length;
119
+ const windowSeconds = this.windowMs / 1000;
120
+ let status = evaluateThreshold(errorCount, {
121
+ warning: this.errorWarn,
122
+ critical: this.errorCrit,
123
+ direction: 'above',
124
+ });
125
+ if (overrideMessage)
126
+ status = worstStatus([status, 'warning']);
127
+ const mood = status === 'critical' ? 'stressed' : status === 'warning' ? 'concerned' : 'happy';
128
+ const lastError = this.entries.filter((e) => e.type === 'error').at(-1);
129
+ const message = overrideMessage ??
130
+ (errorCount > 0
131
+ ? `${errorCount} error${errorCount !== 1 ? 's' : ''} in last ${windowSeconds / 60}min across ${this.files.length} file${this.files.length !== 1 ? 's' : ''}`
132
+ : `No errors in last ${windowSeconds / 60}min`);
133
+ const metrics = {
134
+ status,
135
+ mood,
136
+ message,
137
+ subStatus,
138
+ screenData: buildLogScreen(this.recentLines),
139
+ metrics: {
140
+ error_count: errorCount,
141
+ warning_count: warningCount,
142
+ window_seconds: windowSeconds,
143
+ files_watched: this.files.length,
144
+ last_error_message: lastError?.message ?? '',
145
+ },
146
+ };
147
+ try {
148
+ await this.pushMetrics(metrics);
149
+ }
150
+ catch (err) {
151
+ this.logger.error('Failed to push metrics', {
152
+ error: err instanceof Error ? err.message : String(err),
153
+ });
154
+ }
155
+ }
156
+ }
157
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,9 @@
1
+ export interface FileTailer {
2
+ close(): void;
3
+ }
4
+ /**
5
+ * Tail a file by watching for changes and reading new lines from the last
6
+ * known byte offset. Handles file rotation (size shrinks -> reset to 0).
7
+ */
8
+ export declare function createTailer(filePath: string, onLine: (line: string) => void): FileTailer;
9
+ //# sourceMappingURL=tail.d.ts.map
@@ -0,0 +1,78 @@
1
+ import { watch, createReadStream, promises as fsp } from 'node:fs';
2
+ import { createInterface } from 'node:readline';
3
+ /**
4
+ * Tail a file by watching for changes and reading new lines from the last
5
+ * known byte offset. Handles file rotation (size shrinks -> reset to 0).
6
+ */
7
+ export function createTailer(filePath, onLine) {
8
+ let offset = 0;
9
+ let reading = false;
10
+ let watcher = null;
11
+ let closed = false;
12
+ async function init() {
13
+ // Start at the end of the file so we only read new lines
14
+ try {
15
+ const stats = await fsp.stat(filePath);
16
+ offset = stats.size;
17
+ }
18
+ catch {
19
+ // File may not exist yet; start at 0
20
+ offset = 0;
21
+ }
22
+ watcher = watch(filePath, () => {
23
+ if (!closed) {
24
+ void readNewLines();
25
+ }
26
+ });
27
+ // Ignore watcher errors (e.g. file deleted then recreated)
28
+ watcher.on('error', () => { });
29
+ }
30
+ async function readNewLines() {
31
+ if (reading || closed)
32
+ return;
33
+ reading = true;
34
+ try {
35
+ const stats = await fsp.stat(filePath);
36
+ // File rotation detected: size smaller than our offset
37
+ if (stats.size < offset) {
38
+ offset = 0;
39
+ }
40
+ // Nothing new to read
41
+ if (stats.size === offset) {
42
+ reading = false;
43
+ return;
44
+ }
45
+ const stream = createReadStream(filePath, {
46
+ start: offset,
47
+ encoding: 'utf-8',
48
+ });
49
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
50
+ let bytesRead = 0;
51
+ for await (const line of rl) {
52
+ if (closed)
53
+ break;
54
+ // Account for line content + newline character
55
+ bytesRead += Buffer.byteLength(line, 'utf-8') + 1;
56
+ onLine(line);
57
+ }
58
+ offset += bytesRead;
59
+ }
60
+ catch {
61
+ // File may have been removed; ignore and wait for next change
62
+ }
63
+ finally {
64
+ reading = false;
65
+ }
66
+ }
67
+ void init();
68
+ return {
69
+ close() {
70
+ closed = true;
71
+ if (watcher) {
72
+ watcher.close();
73
+ watcher = null;
74
+ }
75
+ },
76
+ };
77
+ }
78
+ //# sourceMappingURL=tail.js.map
@@ -0,0 +1,6 @@
1
+ export declare const DEFAULTS: {
2
+ pollInterval: number;
3
+ cpuWarn: number;
4
+ memWarn: number;
5
+ };
6
+ //# sourceMappingURL=defaults.d.ts.map
@@ -0,0 +1,6 @@
1
+ export const DEFAULTS = {
2
+ pollInterval: 15000, // 15 seconds
3
+ cpuWarn: 80, // percent
4
+ memWarn: 500, // MB
5
+ };
6
+ //# sourceMappingURL=defaults.js.map
@@ -0,0 +1,26 @@
1
+ import { PollingNPC } from '../../core/polling-npc.js';
2
+ import type { PollingNpcConfig } from '../../core/polling-npc.js';
3
+ import type { CollectedMetrics, Archetype, CompanionDecl } from '../../core/types.js';
4
+ export interface ProcessWatcherConfig extends PollingNpcConfig {
5
+ /** List of process names to watch */
6
+ processes: string[];
7
+ /** CPU usage percent threshold for warning status (default: 80) */
8
+ cpuWarn?: number;
9
+ /** Memory usage MB threshold for warning status (default: 500) */
10
+ memWarn?: number;
11
+ }
12
+ export declare class ProcessWatcherNPC extends PollingNPC {
13
+ private processes;
14
+ private cpuWarn;
15
+ private memWarn;
16
+ private cpuHistories;
17
+ private previousRunning;
18
+ constructor(config: ProcessWatcherConfig);
19
+ get name(): string;
20
+ protected get defaultDisplayName(): string;
21
+ get archetype(): Archetype;
22
+ get heartbeatSeconds(): number;
23
+ get companion(): CompanionDecl;
24
+ protected collect(): Promise<CollectedMetrics>;
25
+ }
26
+ //# sourceMappingURL=index.d.ts.map