@nocville/npc 0.2.1 → 0.2.3

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 (100) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +58 -120
  3. package/dist/cjs/cli.js +55 -34
  4. package/dist/cjs/core/base-npc.js +8 -0
  5. package/dist/cjs/core/config.js +19 -2
  6. package/dist/cjs/core/errors.js +23 -0
  7. package/dist/cjs/core/http.js +121 -0
  8. package/dist/cjs/core/pixel-art.js +110 -0
  9. package/dist/cjs/core/polling-npc.js +99 -14
  10. package/dist/cjs/core/session-filter.js +3 -1
  11. package/dist/cjs/core/text.js +13 -0
  12. package/dist/cjs/core/thresholds.js +29 -0
  13. package/dist/cjs/index.js +10 -2
  14. package/dist/cjs/npcs/agent-common/session.js +1 -0
  15. package/dist/cjs/npcs/github-watcher/defaults.js +7 -1
  16. package/dist/cjs/npcs/github-watcher/github-api.js +82 -45
  17. package/dist/cjs/npcs/github-watcher/index.js +82 -36
  18. package/dist/cjs/npcs/internet-connectivity/checks.js +70 -67
  19. package/dist/cjs/npcs/internet-connectivity/defaults.js +5 -2
  20. package/dist/cjs/npcs/internet-connectivity/index.js +134 -131
  21. package/dist/cjs/npcs/log-watcher/buffer.js +61 -0
  22. package/dist/cjs/npcs/log-watcher/index.js +109 -103
  23. package/dist/cjs/npcs/log-watcher/tail.js +100 -53
  24. package/dist/cjs/npcs/process-watcher/index.js +61 -51
  25. package/dist/cjs/npcs/process-watcher/process-scanner.js +63 -69
  26. package/dist/cjs/npcs/receptionist/defaults.js +4 -0
  27. package/dist/cjs/npcs/receptionist/index.js +45 -19
  28. package/dist/cjs/npcs/receptionist/weather.js +42 -47
  29. package/dist/cjs/npcs/system-monitor/collectors.js +66 -17
  30. package/dist/cjs/npcs/system-monitor/index.js +52 -28
  31. package/dist/cjs/npcs/web-page-watcher/basic-mode.js +30 -14
  32. package/dist/cjs/npcs/web-page-watcher/capture.js +23 -1
  33. package/dist/cjs/npcs/web-page-watcher/defaults.js +44 -2
  34. package/dist/cjs/npcs/web-page-watcher/index.js +119 -68
  35. package/dist/cjs/npcs/web-page-watcher/pixelate.js +2 -1
  36. package/dist/cli.js +56 -35
  37. package/dist/core/base-npc.d.ts +5 -0
  38. package/dist/core/base-npc.js +8 -0
  39. package/dist/core/config.d.ts +5 -0
  40. package/dist/core/config.js +18 -2
  41. package/dist/core/errors.d.ts +11 -0
  42. package/dist/core/errors.js +18 -0
  43. package/dist/core/http.d.ts +52 -0
  44. package/dist/core/http.js +111 -0
  45. package/dist/core/pixel-art.d.ts +38 -0
  46. package/dist/core/pixel-art.js +105 -0
  47. package/dist/core/polling-npc.d.ts +22 -1
  48. package/dist/core/polling-npc.js +98 -13
  49. package/dist/core/session-filter.d.ts +2 -2
  50. package/dist/core/session-filter.js +3 -1
  51. package/dist/core/text.d.ts +6 -0
  52. package/dist/core/text.js +12 -0
  53. package/dist/core/thresholds.d.ts +9 -1
  54. package/dist/core/thresholds.js +27 -0
  55. package/dist/index.d.ts +5 -2
  56. package/dist/index.js +4 -2
  57. package/dist/npcs/agent-common/session.js +1 -0
  58. package/dist/npcs/github-watcher/defaults.d.ts +6 -0
  59. package/dist/npcs/github-watcher/defaults.js +7 -1
  60. package/dist/npcs/github-watcher/github-api.d.ts +21 -7
  61. package/dist/npcs/github-watcher/github-api.js +80 -45
  62. package/dist/npcs/github-watcher/index.d.ts +6 -0
  63. package/dist/npcs/github-watcher/index.js +83 -37
  64. package/dist/npcs/internet-connectivity/checks.d.ts +15 -18
  65. package/dist/npcs/internet-connectivity/checks.js +70 -67
  66. package/dist/npcs/internet-connectivity/defaults.d.ts +3 -0
  67. package/dist/npcs/internet-connectivity/defaults.js +5 -2
  68. package/dist/npcs/internet-connectivity/index.d.ts +19 -6
  69. package/dist/npcs/internet-connectivity/index.js +135 -133
  70. package/dist/npcs/log-watcher/buffer.d.ts +22 -0
  71. package/dist/npcs/log-watcher/buffer.js +58 -0
  72. package/dist/npcs/log-watcher/index.d.ts +22 -21
  73. package/dist/npcs/log-watcher/index.js +112 -106
  74. package/dist/npcs/log-watcher/tail.d.ts +23 -3
  75. package/dist/npcs/log-watcher/tail.js +101 -54
  76. package/dist/npcs/process-watcher/index.d.ts +1 -1
  77. package/dist/npcs/process-watcher/index.js +62 -52
  78. package/dist/npcs/process-watcher/process-scanner.d.ts +29 -1
  79. package/dist/npcs/process-watcher/process-scanner.js +61 -71
  80. package/dist/npcs/receptionist/defaults.d.ts +4 -0
  81. package/dist/npcs/receptionist/defaults.js +4 -0
  82. package/dist/npcs/receptionist/index.d.ts +12 -2
  83. package/dist/npcs/receptionist/index.js +46 -20
  84. package/dist/npcs/receptionist/weather.d.ts +8 -6
  85. package/dist/npcs/receptionist/weather.js +39 -45
  86. package/dist/npcs/system-monitor/collectors.d.ts +15 -3
  87. package/dist/npcs/system-monitor/collectors.js +64 -17
  88. package/dist/npcs/system-monitor/defaults.d.ts +1 -1
  89. package/dist/npcs/system-monitor/index.d.ts +2 -1
  90. package/dist/npcs/system-monitor/index.js +54 -30
  91. package/dist/npcs/web-page-watcher/basic-mode.d.ts +2 -2
  92. package/dist/npcs/web-page-watcher/basic-mode.js +30 -14
  93. package/dist/npcs/web-page-watcher/capture.d.ts +18 -0
  94. package/dist/npcs/web-page-watcher/capture.js +21 -1
  95. package/dist/npcs/web-page-watcher/defaults.d.ts +23 -0
  96. package/dist/npcs/web-page-watcher/defaults.js +41 -1
  97. package/dist/npcs/web-page-watcher/index.d.ts +18 -1
  98. package/dist/npcs/web-page-watcher/index.js +121 -70
  99. package/dist/npcs/web-page-watcher/pixelate.js +2 -1
  100. package/package.json +2 -7
@@ -1,7 +1,9 @@
1
1
  import { PollingNPC } from '../../core/polling-npc.js';
2
- import { evaluateThreshold, worstStatus } from '../../core/thresholds.js';
2
+ import { evaluateThreshold, worstStatus, moodForStatus } from '../../core/thresholds.js';
3
3
  import { statusColor, pushHistory } from '../../core/screen.js';
4
- import { numberFlag, stringFlag } from '../../core/config.js';
4
+ import { listFlag, numberFlag } from '../../core/config.js';
5
+ import { NpcConfigError } from '../../core/errors.js';
6
+ import { isHttpUrl } from '../../core/http.js';
5
7
  import { checkDns, checkHttp, checkLatency } from './checks.js';
6
8
  import { DEFAULTS } from './defaults.js';
7
9
  /**
@@ -12,13 +14,15 @@ import { DEFAULTS } from './defaults.js';
12
14
  export function evaluateConnectivityStatus(sample, thresholds) {
13
15
  if (sample.allFailed)
14
16
  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]);
17
+ const statuses = [sample.anyFailed ? 'warning' : 'healthy'];
18
+ if (sample.dnsAvg !== null)
19
+ statuses.push(evaluateThreshold(sample.dnsAvg, thresholds.dnsTime));
20
+ if (sample.httpAvg !== null)
21
+ statuses.push(evaluateThreshold(sample.httpAvg, thresholds.httpTime));
22
+ if (sample.latencyAvg !== null) {
23
+ statuses.push(evaluateThreshold(sample.latencyAvg, thresholds.latencyTime));
24
+ }
25
+ return worstStatus(statuses);
22
26
  }
23
27
  /** Scale a threshold set by a ratio, preserving direction. */
24
28
  function scaleThresholds(base, ratio) {
@@ -33,6 +37,30 @@ function scaleThresholds(base, ratio) {
33
37
  latencyTime: scale(base.latencyTime),
34
38
  };
35
39
  }
40
+ /**
41
+ * Parse a latency target: "host", "host:port", "[ipv6]:port" or a bare IPv6
42
+ * address. Throws NpcConfigError on an out-of-range port.
43
+ */
44
+ export function parseLatencyHost(input, defaultPort = DEFAULTS.latencyPort) {
45
+ const s = input.trim();
46
+ const toPort = (raw) => {
47
+ const port = Number(raw);
48
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
49
+ throw new NpcConfigError(`Invalid port in latency host "${s}"`);
50
+ }
51
+ return port;
52
+ };
53
+ const bracketed = s.match(/^\[([^\]]+)\](?::(\d*))?$/);
54
+ if (bracketed) {
55
+ return { host: bracketed[1], port: bracketed[2] ? toPort(bracketed[2]) : defaultPort };
56
+ }
57
+ const colons = s.split(':').length - 1;
58
+ if (colons === 1) {
59
+ const [host, port] = s.split(':');
60
+ return { host, port: toPort(port) };
61
+ }
62
+ return { host: s, port: defaultPort }; // plain host, or a bare IPv6 address
63
+ }
36
64
  export class InternetConnectivityNPC extends PollingNPC {
37
65
  dnsHosts;
38
66
  httpUrls;
@@ -49,10 +77,16 @@ export class InternetConnectivityNPC extends PollingNPC {
49
77
  });
50
78
  this.dnsHosts = config.dnsHosts ?? DEFAULTS.dnsHosts;
51
79
  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;
80
+ this.latencyHosts = (config.latencyHosts ?? DEFAULTS.latencyHosts).map((h) => typeof h === 'string' ? parseLatencyHost(h) : h);
81
+ // Every check must finish well inside the poll interval.
82
+ this.httpTimeout = Math.min(Math.max(500, config.httpTimeout ?? DEFAULTS.httpTimeout), this.pollInterval / 2);
83
+ const badUrl = this.httpUrls.find((u) => !isHttpUrl(u));
84
+ if (badUrl !== undefined) {
85
+ throw new NpcConfigError(`--http-urls: "${badUrl}" is not an http(s) URL`);
86
+ }
87
+ if (this.dnsHosts.length === 0 && this.httpUrls.length === 0) {
88
+ throw new NpcConfigError('internet-connectivity needs at least one DNS host or HTTP URL');
89
+ }
56
90
  // Threshold resolution: explicit thresholds win; else if the legacy
57
91
  // --slow-threshold differs from the default, scale the live thresholds by
58
92
  // that ratio for back-compat; else use the defaults.
@@ -88,115 +122,91 @@ export class InternetConnectivityNPC extends PollingNPC {
88
122
  }
89
123
  async collect() {
90
124
  const [dnsResults, httpResults, latencyResults] = await Promise.all([
91
- checkDns(this.dnsHosts),
125
+ checkDns(this.dnsHosts, this.httpTimeout),
92
126
  checkHttp(this.httpUrls, this.httpTimeout),
93
- checkLatency(this.latencyHosts),
127
+ checkLatency(this.latencyHosts, this.httpTimeout),
94
128
  ]);
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);
129
+ const dnsOk = countOk(dnsResults);
130
+ const httpOk = countOk(httpResults);
131
+ const latencyOk = countOk(latencyResults);
132
+ const dnsAvg = avgOkTime(dnsResults);
133
+ const httpAvg = avgOkTime(httpResults);
134
+ const latencyAvg = avgOkTime(latencyResults);
102
135
  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
- };
136
+ const anyFailed = dnsOk < dnsResults.length || httpOk < httpResults.length || latencyOk < latencyResults.length;
137
+ const status = evaluateConnectivityStatus({ allFailed: !isOnline, anyFailed, dnsAvg, httpAvg, latencyAvg }, this.thresholds);
138
+ const message = isOnline
139
+ ? `Online | DNS: ${fmtMs(dnsAvg)} | HTTP: ${fmtMs(httpAvg)} | Latency: ${fmtMs(latencyAvg)}` +
140
+ (anyFailed ? ` | ${failureSummary(dnsResults, httpResults, latencyResults)}` : '')
141
+ : offlineMessage(dnsResults, httpResults);
139
142
  return {
140
143
  status,
141
- mood,
144
+ mood: moodForStatus(status),
142
145
  message,
143
146
  subStatus: isOnline ? undefined : 'offline',
144
- screenData,
147
+ screenData: {
148
+ layout: 'multi-metric',
149
+ values: [
150
+ this.panel('DNS', dnsResults, dnsAvg, this.dnsHistory, this.thresholds.dnsTime),
151
+ this.panel('HTTP', httpResults, httpAvg, this.httpHistory, this.thresholds.httpTime),
152
+ this.panel('TCP', latencyResults, latencyAvg, this.latencyHistory, this.thresholds.latencyTime),
153
+ {
154
+ label: 'NET',
155
+ value: isOnline ? 'ONLINE' : 'OFFLINE',
156
+ color: isOnline ? 'green' : 'red',
157
+ },
158
+ ],
159
+ },
145
160
  metrics: {
146
- dns_avg_ms: round(dnsAvg),
147
- http_avg_ms: round(httpAvg),
148
- latency_avg_ms: round(latencyAvg),
161
+ dns_avg_ms: round(dnsAvg ?? 0),
162
+ http_avg_ms: round(httpAvg ?? 0),
163
+ latency_avg_ms: round(latencyAvg ?? 0),
149
164
  dns_ok_count: dnsOk,
150
- dns_fail_count: dnsFail,
165
+ dns_fail_count: dnsResults.length - dnsOk,
151
166
  http_ok_count: httpOk,
152
- http_fail_count: httpFail,
167
+ http_fail_count: httpResults.length - httpOk,
168
+ latency_ok_count: latencyOk,
169
+ latency_fail_count: latencyResults.length - latencyOk,
153
170
  is_online: isOnline ? 1 : 0,
154
171
  },
155
172
  };
156
173
  }
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'}`;
174
+ /**
175
+ * One dashboard panel. A category whose checks all failed shows FAIL in red
176
+ * (and a 0 sparkline sample) rather than a misleading latency; a partial
177
+ * failure is amber.
178
+ */
179
+ panel(label, results, avg, history, threshold) {
180
+ if (results.length === 0)
181
+ return { label, value: 'off', color: 'gray' };
182
+ pushHistory(history, round(avg ?? 0));
183
+ const sparkline = [...history];
184
+ if (avg === null)
185
+ return { label, value: 'FAIL', color: 'red', sparkline };
186
+ const color = countOk(results) < results.length ? 'amber' : statusColor(evaluateThreshold(avg, threshold));
187
+ return { label, value: fmtMs(avg), color, sparkline };
169
188
  }
170
189
  /**
171
- * Build config from CLI flags. Merges with any programmatic overrides.
190
+ * Build config from CLI flags. List flags are comma-separated; latency
191
+ * hosts accept "host", "host:port" or "[ipv6]:port".
172
192
  */
173
193
  static configFromFlags(flags) {
174
194
  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
- }
195
+ const dnsHosts = listFlag(flags, 'dns-hosts');
196
+ if (dnsHosts)
197
+ config.dnsHosts = dnsHosts;
198
+ const httpUrls = listFlag(flags, 'http-urls');
199
+ if (httpUrls)
200
+ config.httpUrls = httpUrls;
201
+ const latencyHosts = listFlag(flags, 'latency-hosts');
202
+ if (latencyHosts)
203
+ config.latencyHosts = latencyHosts.map((h) => parseLatencyHost(h));
197
204
  const slowThreshold = numberFlag(flags, 'slow-threshold');
198
205
  if (slowThreshold !== undefined)
199
206
  config.slowThreshold = slowThreshold;
207
+ const timeout = numberFlag(flags, 'timeout');
208
+ if (timeout !== undefined)
209
+ config.httpTimeout = timeout;
200
210
  const pollInterval = numberFlag(flags, 'poll-interval');
201
211
  if (pollInterval !== undefined)
202
212
  config.pollInterval = pollInterval;
@@ -204,43 +214,35 @@ export class InternetConnectivityNPC extends PollingNPC {
204
214
  }
205
215
  }
206
216
  // ---------- 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;
217
+ function countOk(results) {
218
+ return results.filter((r) => r.ok).length;
219
+ }
220
+ /** Mean time of the successful checks only — a 5s timeout is a failure, not a latency. */
221
+ function avgOkTime(results) {
222
+ const ok = results.filter((r) => r.ok);
223
+ if (ok.length === 0)
224
+ return null;
225
+ return ok.reduce((acc, r) => acc + r.timeMs, 0) / ok.length;
212
226
  }
213
227
  function round(n) {
214
228
  return Math.round(n * 100) / 100;
215
229
  }
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
- }
230
+ function fmtMs(n) {
231
+ return n === null ? 'FAIL' : `${Math.round(n)}ms`;
227
232
  }
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
- });
233
+ /** "failing: 8.8.8.8:443, dns.example" — names the targets, never error text. */
234
+ function failureSummary(...groups) {
235
+ const failed = groups.flat().filter((r) => !r.ok);
236
+ return `failing: ${failed.map((r) => r.target).join(', ')}`;
237
+ }
238
+ function offlineMessage(dnsResults, httpResults) {
239
+ const problems = [];
240
+ if (dnsResults.length > 0 && dnsResults.every((r) => !r.ok)) {
241
+ problems.push('DNS resolution failed');
242
+ }
243
+ if (httpResults.length > 0 && httpResults.every((r) => !r.ok)) {
244
+ problems.push('no HTTP connectivity');
245
+ }
246
+ return `OFFLINE: ${problems.join(', ') || 'all checks failed'}`;
245
247
  }
246
248
  //# sourceMappingURL=index.js.map
@@ -6,6 +6,13 @@ export interface RecentLine {
6
6
  }
7
7
  /** Classify a raw log line against the error/warning patterns. */
8
8
  export declare function classifyLine(line: string, errorRe: RegExp, warningRe: RegExp): LogLineLevel;
9
+ /**
10
+ * Prepare a log line for display to the whole team: strip terminal escapes,
11
+ * redact anything secret-looking, drop control characters and truncate.
12
+ * Log lines routinely contain tokens and passwords; only redacted text ever
13
+ * leaves the machine.
14
+ */
15
+ export declare function cleanLogLine(line: string, max?: number): string;
9
16
  /**
10
17
  * Push a line into the bounded ring buffer: each entry is capped at `cap`
11
18
  * characters and the buffer keeps only the last `max` entries.
@@ -15,4 +22,19 @@ export declare function pushRecent(buffer: RecentLine[], line: string, level: Lo
15
22
  export declare function buildLogScreen(buffer: RecentLine[]): ScreenData;
16
23
  /** True when enough time has elapsed since the last burst push to emit again. */
17
24
  export declare function shouldEmitBurst(lastEmit: number, now: number, windowMs?: number): boolean;
25
+ /**
26
+ * Sliding-window error/warning counter in one-second buckets: memory is
27
+ * bounded by the window length, not by how many lines a runaway logger writes.
28
+ */
29
+ export declare class WindowCounter {
30
+ private readonly windowMs;
31
+ private buckets;
32
+ constructor(windowMs: number);
33
+ add(level: 'error' | 'warning', now?: number): void;
34
+ counts(now?: number): {
35
+ errors: number;
36
+ warnings: number;
37
+ };
38
+ private prune;
39
+ }
18
40
  //# sourceMappingURL=buffer.d.ts.map
@@ -1,3 +1,5 @@
1
+ import { sanitizeText } from '../../core/text.js';
2
+ import { redactSecrets } from '../agent-common/activity.js';
1
3
  /** Classify a raw log line against the error/warning patterns. */
2
4
  export function classifyLine(line, errorRe, warningRe) {
3
5
  if (errorRe.test(line))
@@ -6,6 +8,23 @@ export function classifyLine(line, errorRe, warningRe) {
6
8
  return 'warning';
7
9
  return 'info';
8
10
  }
11
+ // ANSI CSI/OSC escape sequences (colours, cursor moves, hyperlinks).
12
+ // eslint-disable-next-line no-control-regex
13
+ const ANSI = /\u001b(?:\[[0-?]*[ -/]*[@-~]|\][^\u0007\u001b]*(?:\u0007|\u001b\\))/g;
14
+ // `password=…`, `"token": "…"`, `Authorization: …` style pairs common in logs
15
+ // (redactSecrets covers CLI flags, bearer tokens, URL credentials and keys).
16
+ const SECRET_PAIR = /\b([\w-]*(?:password|passwd|pwd|secret|token|api[_-]?key|access[_-]?key|authorization|cookie|session)[\w-]*)(["']?\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,;&]+)/gi;
17
+ /**
18
+ * Prepare a log line for display to the whole team: strip terminal escapes,
19
+ * redact anything secret-looking, drop control characters and truncate.
20
+ * Log lines routinely contain tokens and passwords; only redacted text ever
21
+ * leaves the machine.
22
+ */
23
+ export function cleanLogLine(line, max = 200) {
24
+ const plain = line.replace(ANSI, '');
25
+ const redacted = redactSecrets(plain).replace(SECRET_PAIR, (_m, key, sep) => `${key}${sep}***`);
26
+ return sanitizeText(redacted, max);
27
+ }
9
28
  /**
10
29
  * Push a line into the bounded ring buffer: each entry is capped at `cap`
11
30
  * characters and the buffer keeps only the last `max` entries.
@@ -31,4 +50,43 @@ export function buildLogScreen(buffer) {
31
50
  export function shouldEmitBurst(lastEmit, now, windowMs = 5000) {
32
51
  return now - lastEmit >= windowMs;
33
52
  }
53
+ /**
54
+ * Sliding-window error/warning counter in one-second buckets: memory is
55
+ * bounded by the window length, not by how many lines a runaway logger writes.
56
+ */
57
+ export class WindowCounter {
58
+ windowMs;
59
+ buckets = new Map();
60
+ constructor(windowMs) {
61
+ this.windowMs = windowMs;
62
+ }
63
+ add(level, now = Date.now()) {
64
+ const key = Math.floor(now / 1000);
65
+ const bucket = this.buckets.get(key) ?? { errors: 0, warnings: 0 };
66
+ if (level === 'error')
67
+ bucket.errors++;
68
+ else
69
+ bucket.warnings++;
70
+ this.buckets.set(key, bucket);
71
+ if (this.buckets.size > this.windowMs / 1000 + 2)
72
+ this.prune(now);
73
+ }
74
+ counts(now = Date.now()) {
75
+ this.prune(now);
76
+ let errors = 0;
77
+ let warnings = 0;
78
+ for (const b of this.buckets.values()) {
79
+ errors += b.errors;
80
+ warnings += b.warnings;
81
+ }
82
+ return { errors, warnings };
83
+ }
84
+ prune(now) {
85
+ const oldest = Math.floor((now - this.windowMs) / 1000);
86
+ for (const key of this.buckets.keys()) {
87
+ if (key < oldest)
88
+ this.buckets.delete(key);
89
+ }
90
+ }
91
+ }
34
92
  //# sourceMappingURL=buffer.js.map
@@ -1,45 +1,46 @@
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 {
1
+ import { PollingNPC } from '../../core/polling-npc.js';
2
+ import type { PollingNpcConfig } from '../../core/polling-npc.js';
3
+ import type { CollectedMetrics, Archetype, BehaviorHints, CompanionDecl } from '../../core/types.js';
4
+ export interface LogWatcherConfig extends PollingNpcConfig {
4
5
  files: string[];
5
6
  errorPattern?: string;
6
7
  warningPattern?: string;
7
8
  windowSeconds?: number;
8
9
  errorWarn?: number;
9
10
  errorCrit?: number;
11
+ /** Report interval in ms (alias of pollInterval, kept for compatibility). */
10
12
  reportInterval?: number;
11
13
  }
12
- export declare class LogWatcherNPC extends BaseNPC {
14
+ /**
15
+ * Tails log files and reports error bursts. A periodic report (every
16
+ * `reportInterval`) carries the error count over the sliding window and the
17
+ * last lines for the log terminal; a fresh error line also triggers an
18
+ * immediate report, debounced to one per 5s, so a burst shows up at once.
19
+ * Every report is one composed push, never a push per line.
20
+ */
21
+ export declare class LogWatcherNPC extends PollingNPC {
13
22
  private tailers;
14
- private timer;
15
- private entries;
23
+ private counter;
16
24
  private recentLines;
17
- private lastBurstPush;
25
+ private lastError;
26
+ /** The error line that triggered an immediate report, consumed by the next collect(). */
27
+ private burstLine;
28
+ private lastBurstAt;
18
29
  private errorRegex;
19
30
  private warningRegex;
20
31
  private windowMs;
21
- private errorWarn;
22
- private errorCrit;
23
- private reportInterval;
32
+ private errorThreshold;
24
33
  private files;
25
34
  constructor(config: LogWatcherConfig);
26
35
  get name(): string;
27
36
  protected get defaultDisplayName(): string;
28
37
  get archetype(): Archetype;
29
- get heartbeatSeconds(): number;
30
38
  get behavior(): BehaviorHints;
31
39
  get companion(): CompanionDecl;
32
40
  protected onStart(): Promise<void>;
33
41
  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;
42
+ /** Visible for tests: feed one raw line as if a tailer had read it. */
43
+ protected handleLine(line: string, now?: number): void;
44
+ protected collect(): Promise<CollectedMetrics>;
44
45
  }
45
46
  //# sourceMappingURL=index.d.ts.map