@nocville/npc 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +55 -17
  2. package/dist/cjs/cli.js +94 -39
  3. package/dist/cjs/core/base-npc.js +15 -0
  4. package/dist/cjs/core/config.js +19 -2
  5. package/dist/cjs/core/errors.js +23 -0
  6. package/dist/cjs/core/http.js +121 -0
  7. package/dist/cjs/core/pixel-art.js +110 -0
  8. package/dist/cjs/core/polling-npc.js +99 -14
  9. package/dist/cjs/core/text.js +13 -0
  10. package/dist/cjs/core/thresholds.js +29 -0
  11. package/dist/cjs/index.js +21 -2
  12. package/dist/cjs/npcs/agent-common/session.js +1 -0
  13. package/dist/cjs/npcs/agent-common/shim.js +8 -2
  14. package/dist/cjs/npcs/github-watcher/defaults.js +7 -1
  15. package/dist/cjs/npcs/github-watcher/github-api.js +82 -45
  16. package/dist/cjs/npcs/github-watcher/index.js +82 -36
  17. package/dist/cjs/npcs/internet-connectivity/checks.js +70 -67
  18. package/dist/cjs/npcs/internet-connectivity/defaults.js +5 -2
  19. package/dist/cjs/npcs/internet-connectivity/index.js +134 -131
  20. package/dist/cjs/npcs/log-watcher/buffer.js +61 -0
  21. package/dist/cjs/npcs/log-watcher/index.js +109 -103
  22. package/dist/cjs/npcs/log-watcher/tail.js +100 -53
  23. package/dist/cjs/npcs/office-characters/index.js +92 -0
  24. package/dist/cjs/npcs/process-watcher/index.js +60 -51
  25. package/dist/cjs/npcs/process-watcher/process-scanner.js +100 -68
  26. package/dist/cjs/npcs/receptionist/defaults.js +4 -0
  27. package/dist/cjs/npcs/receptionist/index.js +6 -93
  28. package/dist/cjs/npcs/receptionist/weather.js +42 -47
  29. package/dist/cjs/npcs/security/index.js +26 -0
  30. package/dist/cjs/npcs/system-monitor/collectors.js +66 -17
  31. package/dist/cjs/npcs/system-monitor/index.js +52 -28
  32. package/dist/cjs/npcs/web-page-watcher/basic-mode.js +30 -14
  33. package/dist/cjs/npcs/web-page-watcher/capture.js +23 -1
  34. package/dist/cjs/npcs/web-page-watcher/defaults.js +44 -2
  35. package/dist/cjs/npcs/web-page-watcher/index.js +119 -68
  36. package/dist/cjs/npcs/web-page-watcher/pixelate.js +2 -1
  37. package/dist/cli.js +95 -40
  38. package/dist/core/base-npc.d.ts +5 -0
  39. package/dist/core/base-npc.js +15 -0
  40. package/dist/core/config.d.ts +5 -0
  41. package/dist/core/config.js +18 -2
  42. package/dist/core/errors.d.ts +11 -0
  43. package/dist/core/errors.js +18 -0
  44. package/dist/core/http.d.ts +52 -0
  45. package/dist/core/http.js +111 -0
  46. package/dist/core/pixel-art.d.ts +38 -0
  47. package/dist/core/pixel-art.js +105 -0
  48. package/dist/core/polling-npc.d.ts +22 -1
  49. package/dist/core/polling-npc.js +98 -13
  50. package/dist/core/text.d.ts +6 -0
  51. package/dist/core/text.js +12 -0
  52. package/dist/core/thresholds.d.ts +9 -1
  53. package/dist/core/thresholds.js +27 -0
  54. package/dist/core/types.d.ts +5 -0
  55. package/dist/index.d.ts +7 -2
  56. package/dist/index.js +6 -2
  57. package/dist/npcs/agent-common/session.js +1 -0
  58. package/dist/npcs/agent-common/shim.js +8 -2
  59. package/dist/npcs/github-watcher/defaults.d.ts +6 -0
  60. package/dist/npcs/github-watcher/defaults.js +7 -1
  61. package/dist/npcs/github-watcher/github-api.d.ts +21 -7
  62. package/dist/npcs/github-watcher/github-api.js +80 -45
  63. package/dist/npcs/github-watcher/index.d.ts +6 -0
  64. package/dist/npcs/github-watcher/index.js +83 -37
  65. package/dist/npcs/internet-connectivity/checks.d.ts +15 -18
  66. package/dist/npcs/internet-connectivity/checks.js +70 -67
  67. package/dist/npcs/internet-connectivity/defaults.d.ts +3 -0
  68. package/dist/npcs/internet-connectivity/defaults.js +5 -2
  69. package/dist/npcs/internet-connectivity/index.d.ts +19 -6
  70. package/dist/npcs/internet-connectivity/index.js +135 -133
  71. package/dist/npcs/log-watcher/buffer.d.ts +22 -0
  72. package/dist/npcs/log-watcher/buffer.js +58 -0
  73. package/dist/npcs/log-watcher/index.d.ts +22 -21
  74. package/dist/npcs/log-watcher/index.js +112 -106
  75. package/dist/npcs/log-watcher/tail.d.ts +23 -3
  76. package/dist/npcs/log-watcher/tail.js +101 -54
  77. package/dist/npcs/office-characters/index.d.ts +50 -0
  78. package/dist/npcs/office-characters/index.js +82 -0
  79. package/dist/npcs/process-watcher/index.d.ts +1 -1
  80. package/dist/npcs/process-watcher/index.js +61 -52
  81. package/dist/npcs/process-watcher/process-scanner.d.ts +31 -1
  82. package/dist/npcs/process-watcher/process-scanner.js +97 -70
  83. package/dist/npcs/receptionist/defaults.d.ts +4 -0
  84. package/dist/npcs/receptionist/defaults.js +4 -0
  85. package/dist/npcs/receptionist/index.d.ts +3 -12
  86. package/dist/npcs/receptionist/index.js +6 -93
  87. package/dist/npcs/receptionist/weather.d.ts +8 -6
  88. package/dist/npcs/receptionist/weather.js +39 -45
  89. package/dist/npcs/security/index.d.ts +12 -0
  90. package/dist/npcs/security/index.js +22 -0
  91. package/dist/npcs/system-monitor/collectors.d.ts +15 -3
  92. package/dist/npcs/system-monitor/collectors.js +64 -17
  93. package/dist/npcs/system-monitor/defaults.d.ts +1 -1
  94. package/dist/npcs/system-monitor/index.d.ts +2 -1
  95. package/dist/npcs/system-monitor/index.js +54 -30
  96. package/dist/npcs/web-page-watcher/basic-mode.d.ts +2 -2
  97. package/dist/npcs/web-page-watcher/basic-mode.js +30 -14
  98. package/dist/npcs/web-page-watcher/capture.d.ts +18 -0
  99. package/dist/npcs/web-page-watcher/capture.js +21 -1
  100. package/dist/npcs/web-page-watcher/defaults.d.ts +23 -0
  101. package/dist/npcs/web-page-watcher/defaults.js +41 -1
  102. package/dist/npcs/web-page-watcher/index.d.ts +18 -1
  103. package/dist/npcs/web-page-watcher/index.js +121 -70
  104. package/dist/npcs/web-page-watcher/pixelate.js +2 -1
  105. package/package.json +2 -2
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.GitHubWatcherNPC = void 0;
4
4
  const polling_npc_js_1 = require("../../core/polling-npc.js");
5
5
  const text_js_1 = require("../../core/text.js");
6
+ const thresholds_js_1 = require("../../core/thresholds.js");
7
+ const errors_js_1 = require("../../core/errors.js");
6
8
  const defaults_js_1 = require("./defaults.js");
7
9
  const github_api_js_1 = require("./github-api.js");
8
10
  const format_js_1 = require("./format.js");
@@ -15,12 +17,24 @@ class GitHubWatcherNPC extends polling_npc_js_1.PollingNPC {
15
17
  lastSeenSha = null;
16
18
  commits = [];
17
19
  rateRemaining = null;
20
+ /** While rate-limited, don't call GitHub again before this time (epoch ms). */
21
+ rateLimitedUntil = 0;
22
+ consecutiveFailures = 0;
18
23
  newCommitPollsLeft = 0;
19
24
  constructor(config) {
20
- super({ ...config, pollInterval: config.pollInterval ?? defaults_js_1.DEFAULTS.pollInterval });
25
+ super({
26
+ ...config,
27
+ // Unauthenticated requests get 60/hour and 304s still count against
28
+ // them, so a 60s poll (plus the branch lookup) would hit the limit.
29
+ pollInterval: config.pollInterval ??
30
+ (config.token ? defaults_js_1.DEFAULTS.pollInterval : defaults_js_1.DEFAULTS.unauthenticatedPollInterval),
31
+ });
32
+ if (!(0, github_api_js_1.isValidRepo)(config.repo)) {
33
+ throw new errors_js_1.NpcConfigError(`--repo must look like owner/name (got "${(0, text_js_1.truncate)(config.repo, 80)}")`);
34
+ }
21
35
  this.repo = config.repo;
22
- this.token = config.token;
23
- this.branch = config.branch;
36
+ this.token = config.token || undefined;
37
+ this.branch = config.branch?.trim() || undefined;
24
38
  this.fetchFn = config.fetchFn;
25
39
  }
26
40
  get name() {
@@ -42,7 +56,8 @@ class GitHubWatcherNPC extends polling_npc_js_1.PollingNPC {
42
56
  };
43
57
  }
44
58
  async onStart() {
45
- // Resolve the default branch once if the user didn't pin one.
59
+ // Resolve the default branch once if the user didn't pin one (it only
60
+ // labels the board; the commits API already defaults to that branch).
46
61
  if (!this.branch) {
47
62
  const def = await (0, github_api_js_1.fetchDefaultBranch)(this.repo, {
48
63
  token: this.token,
@@ -57,6 +72,8 @@ class GitHubWatcherNPC extends polling_npc_js_1.PollingNPC {
57
72
  await super.onStart();
58
73
  }
59
74
  async collect() {
75
+ if (Date.now() < this.rateLimitedUntil)
76
+ return this.rateLimited();
60
77
  const result = await (0, github_api_js_1.fetchCommits)(this.repo, {
61
78
  token: this.token,
62
79
  etag: this.etag,
@@ -67,39 +84,68 @@ class GitHubWatcherNPC extends polling_npc_js_1.PollingNPC {
67
84
  });
68
85
  if (result.rateRemaining !== null)
69
86
  this.rateRemaining = result.rateRemaining;
70
- if (result.status === 403) {
71
- return this.report('warning', 'concerned', 'GitHub API rate-limited (403)', 'rate-limited');
87
+ const failure = this.classifyFailure(result);
88
+ if (failure)
89
+ return failure;
90
+ this.consecutiveFailures = 0;
91
+ if (result.notModified)
92
+ return this.reportIdle();
93
+ if (result.status === 409)
94
+ return this.report('healthy', 'Repository is empty');
95
+ this.etag = result.etag ?? this.etag;
96
+ if (result.commits.length === 0)
97
+ return this.reportIdle();
98
+ const fresh = (0, github_api_js_1.newCommitsSince)(result.commits, this.lastSeenSha);
99
+ this.commits = result.commits;
100
+ const seeded = this.lastSeenSha !== null;
101
+ this.lastSeenSha = result.commits[0].sha;
102
+ if (!seeded || fresh.length === 0)
103
+ return this.reportIdle();
104
+ this.newCommitPollsLeft = defaults_js_1.DEFAULTS.newCommitPolls;
105
+ return this.reportNewCommits(fresh);
106
+ }
107
+ /** Map an error response to a report, or null when the fetch succeeded. */
108
+ classifyFailure(result) {
109
+ const { status } = result;
110
+ if (status === 304 || status === 409 || (status >= 200 && status < 300))
111
+ return null;
112
+ if (status === 429 || (status === 403 && result.rateRemaining === 0)) {
113
+ this.rateLimitedUntil = result.rateResetAt ?? Date.now() + 15 * 60_000;
114
+ if (!this.token) {
115
+ this.logger.warn('GitHub rate limit reached; set GITHUB_TOKEN for a higher limit');
116
+ }
117
+ return this.rateLimited();
72
118
  }
73
- if (result.status === 404) {
74
- return this.report('warning', 'concerned', `Repo not found: ${this.repo}`, 'not-found');
119
+ if (status === 401) {
120
+ return this.report('warning', 'GitHub token rejected (401)', 'auth-failed');
75
121
  }
76
- if (result.status === 0 && this.commits.length === 0) {
77
- return this.report('warning', 'concerned', 'GitHub API unreachable', 'unreachable');
122
+ if (status === 403) {
123
+ return this.report('warning', `Access to ${this.repo} denied (403)`, 'forbidden');
78
124
  }
79
- if (result.notModified) {
80
- return this.reportIdle();
125
+ if (status === 404) {
126
+ const hint = this.token ? '' : ' (private repos need GITHUB_TOKEN)';
127
+ return this.report('warning', `Repo not found: ${this.repo}${hint}`, 'not-found');
81
128
  }
82
- if (result.commits.length > 0) {
83
- const fresh = (0, github_api_js_1.newCommitsSince)(result.commits, this.lastSeenSha);
84
- this.commits = result.commits;
85
- this.etag = result.etag ?? this.etag;
86
- if (this.lastSeenSha === null) {
87
- this.lastSeenSha = result.commits[0].sha;
88
- return this.reportIdle();
89
- }
90
- if (fresh.length > 0) {
91
- this.lastSeenSha = result.commits[0].sha;
92
- this.newCommitPollsLeft = defaults_js_1.DEFAULTS.newCommitPolls;
93
- return this.reportNewCommits(fresh);
94
- }
129
+ // Network error, timeout or 5xx: ride out blips while there's a board to show.
130
+ this.consecutiveFailures++;
131
+ if (this.commits.length > 0 && this.consecutiveFailures < defaults_js_1.DEFAULTS.failuresBeforeWarning) {
132
+ return this.reportIdle();
95
133
  }
96
- return this.reportIdle();
134
+ const what = status === 0 ? 'unreachable' : `error (HTTP ${status})`;
135
+ return this.report('warning', `GitHub API ${what}`, 'unreachable');
136
+ }
137
+ rateLimited() {
138
+ const resumes = new Date(this.rateLimitedUntil).toLocaleTimeString([], {
139
+ hour: '2-digit',
140
+ minute: '2-digit',
141
+ });
142
+ return this.report('warning', `GitHub API rate-limited until ${resumes}`, 'rate-limited');
97
143
  }
98
144
  reportNewCommits(fresh) {
99
145
  const newest = fresh[0];
100
146
  const latest = `${newest.author}: ${(0, text_js_1.truncate)(newest.message, 120)} (${newest.sha.slice(0, 7)})`;
101
147
  const message = fresh.length > 1 ? `${fresh.length} new commits — latest: ${latest}` : latest;
102
- return this.report('healthy', 'happy', message, 'new-commit');
148
+ return this.report('healthy', message, 'new-commit');
103
149
  }
104
150
  reportIdle() {
105
151
  if (this.newCommitPollsLeft > 0)
@@ -109,16 +155,16 @@ class GitHubWatcherNPC extends polling_npc_js_1.PollingNPC {
109
155
  const message = newest
110
156
  ? `Last: ${(0, text_js_1.truncate)(newest.message, 80)} (${(0, format_js_1.relativeTime)(newest.date)})`
111
157
  : `Watching ${this.repo}`;
112
- return this.report('healthy', 'happy', message, subStatus);
158
+ return this.report('healthy', message, subStatus);
113
159
  }
114
- report(status, mood, message, subStatus) {
115
- const newest = this.commits[0];
116
- const secondsSinceLast = newest && newest.date
117
- ? Math.max(0, Math.round((Date.now() - Date.parse(newest.date)) / 1000))
160
+ report(status, message, subStatus) {
161
+ const newestAt = Date.parse(this.commits[0]?.date ?? '');
162
+ const secondsSinceLast = Number.isFinite(newestAt)
163
+ ? Math.max(0, Math.round((Date.now() - newestAt) / 1000))
118
164
  : 0;
119
165
  return {
120
166
  status,
121
- mood,
167
+ mood: (0, thresholds_js_1.moodForStatus)(status),
122
168
  message,
123
169
  subStatus,
124
170
  screenData: this.buildBoard(),
@@ -132,9 +178,9 @@ class GitHubWatcherNPC extends polling_npc_js_1.PollingNPC {
132
178
  buildBoard() {
133
179
  const now = Date.now();
134
180
  const header = this.branch ? `${this.repo}@${this.branch}` : this.repo;
135
- const values = [{ label: 'repo', value: header }];
136
- for (const c of this.commits.slice(0, 8)) {
137
- const recent = c.date && now - Date.parse(c.date) < 3600_000;
181
+ const values = [{ label: 'repo', value: (0, text_js_1.truncate)(header, 60) }];
182
+ for (const c of this.commits.slice(0, defaults_js_1.DEFAULTS.boardRows)) {
183
+ const recent = now - Date.parse(c.date) < 3600_000;
138
184
  values.push({
139
185
  label: c.sha.slice(0, 7),
140
186
  value: (0, text_js_1.truncate)(`${c.author}: ${c.message}`, 60),
@@ -8,85 +8,88 @@ exports.checkHttp = checkHttp;
8
8
  exports.checkLatency = checkLatency;
9
9
  const node_dns_1 = require("node:dns");
10
10
  const node_net_1 = __importDefault(require("node:net"));
11
- async function checkDns(hosts) {
12
- return Promise.all(hosts.map(resolveSingleDns));
11
+ const http_js_1 = require("../../core/http.js");
12
+ function errorText(err) {
13
+ return err instanceof Error ? err.message : String(err);
13
14
  }
14
- async function resolveSingleDns(host) {
15
- const start = performance.now();
16
- try {
17
- await node_dns_1.promises.resolve(host);
18
- return { host, ok: true, timeMs: performance.now() - start };
19
- }
20
- catch (err) {
21
- return {
22
- host,
23
- ok: false,
24
- timeMs: performance.now() - start,
25
- error: err instanceof Error ? err.message : String(err),
26
- };
27
- }
28
- }
29
- async function checkHttp(urls, timeout) {
30
- return Promise.all(urls.map((url) => fetchSingleUrl(url, timeout)));
31
- }
32
- async function fetchSingleUrl(url, timeout) {
33
- const controller = new AbortController();
34
- const timer = setTimeout(() => controller.abort(), timeout);
35
- const start = performance.now();
15
+ // ---------- DNS ----------
16
+ /**
17
+ * Resolve each host's A records through a dedicated resolver with a bounded
18
+ * timeout (the default resolver retries for tens of seconds when DNS is down).
19
+ */
20
+ async function checkDns(hosts, timeoutMs) {
21
+ const resolver = new node_dns_1.promises.Resolver({
22
+ timeout: Math.max(250, Math.floor(timeoutMs / 2)),
23
+ tries: 2,
24
+ });
25
+ let timer;
26
+ const deadline = new Promise((resolve) => {
27
+ timer = setTimeout(() => resolve('timeout'), timeoutMs);
28
+ });
36
29
  try {
37
- const response = await fetch(url, { signal: controller.signal });
38
- // Consume body to ensure connection is completed
39
- await response.text();
40
- return {
41
- url,
42
- ok: response.ok,
43
- timeMs: performance.now() - start,
44
- statusCode: response.status,
45
- };
46
- }
47
- catch (err) {
48
- return {
49
- url,
50
- ok: false,
51
- timeMs: performance.now() - start,
52
- error: err instanceof Error ? err.message : String(err),
53
- };
30
+ return await Promise.all(hosts.map(async (host) => {
31
+ const start = performance.now();
32
+ try {
33
+ const outcome = await Promise.race([resolver.resolve4(host), deadline]);
34
+ if (outcome === 'timeout')
35
+ return { target: host, ok: false, timeMs: 0, error: 'timeout' };
36
+ return { target: host, ok: true, timeMs: performance.now() - start };
37
+ }
38
+ catch (err) {
39
+ return { target: host, ok: false, timeMs: 0, error: errorText(err) };
40
+ }
41
+ }));
54
42
  }
55
43
  finally {
56
44
  clearTimeout(timer);
45
+ resolver.cancel();
57
46
  }
58
47
  }
59
- async function checkLatency(hosts) {
60
- return Promise.all(hosts.map(measureTcpConnect));
48
+ // ---------- HTTP ----------
49
+ /**
50
+ * Time to response headers for each URL. The body is discarded unread, so a
51
+ * large or endless response costs nothing. Only 2xx counts as reachable.
52
+ */
53
+ async function checkHttp(urls, timeoutMs) {
54
+ return Promise.all(urls.map(async (url) => {
55
+ const target = (0, http_js_1.displayUrl)(url); // never echo credentials or query tokens
56
+ const start = performance.now();
57
+ try {
58
+ const res = await fetch(url, {
59
+ signal: AbortSignal.timeout(timeoutMs),
60
+ headers: { 'User-Agent': http_js_1.NPC_USER_AGENT },
61
+ });
62
+ const timeMs = performance.now() - start;
63
+ await (0, http_js_1.discardBody)(res);
64
+ return res.ok
65
+ ? { target, ok: true, timeMs }
66
+ : { target, ok: false, timeMs: 0, error: `HTTP ${res.status}` };
67
+ }
68
+ catch (err) {
69
+ return { target, ok: false, timeMs: 0, error: errorText(err) };
70
+ }
71
+ }));
72
+ }
73
+ // ---------- Latency (TCP connect) ----------
74
+ /** Time to establish a TCP connection to each host:port. */
75
+ async function checkLatency(hosts, timeoutMs) {
76
+ return Promise.all(hosts.map((h) => measureTcpConnect(h, timeoutMs)));
61
77
  }
62
- function measureTcpConnect(target) {
78
+ function measureTcpConnect(target, timeoutMs) {
79
+ const label = target.host.includes(':')
80
+ ? `[${target.host}]:${target.port}`
81
+ : `${target.host}:${target.port}`;
63
82
  return new Promise((resolve) => {
64
83
  const start = performance.now();
65
84
  const socket = node_net_1.default.connect({ host: target.host, port: target.port });
66
- const timeout = setTimeout(() => {
85
+ const finish = (result) => {
86
+ clearTimeout(timer);
67
87
  socket.destroy();
68
- resolve({
69
- host: target.host,
70
- ok: false,
71
- timeMs: performance.now() - start,
72
- error: 'Connection timed out',
73
- });
74
- }, 5000);
75
- socket.on('connect', () => {
76
- clearTimeout(timeout);
77
- const timeMs = performance.now() - start;
78
- socket.destroy();
79
- resolve({ host: target.host, ok: true, timeMs });
80
- });
81
- socket.on('error', (err) => {
82
- clearTimeout(timeout);
83
- resolve({
84
- host: target.host,
85
- ok: false,
86
- timeMs: performance.now() - start,
87
- error: err.message,
88
- });
89
- });
88
+ resolve(result);
89
+ };
90
+ const timer = setTimeout(() => finish({ target: label, ok: false, timeMs: 0, error: 'Connection timed out' }), timeoutMs);
91
+ socket.once('connect', () => finish({ target: label, ok: true, timeMs: performance.now() - start }));
92
+ socket.once('error', (err) => finish({ target: label, ok: false, timeMs: 0, error: err.message }));
90
93
  });
91
94
  }
92
95
  //# sourceMappingURL=checks.js.map
@@ -7,10 +7,13 @@ exports.DEFAULTS = {
7
7
  // gstatic generate_204 is a stable, low-latency 204 endpoint used by many
8
8
  // OSes for connectivity checks (replaces the flaky httpbin.org default).
9
9
  httpUrls: ['https://1.1.1.1/cdn-cgi/trace', 'https://www.gstatic.com/generate_204'],
10
+ // 443, not 80: Google's 8.8.8.8 doesn't listen on port 80, so that default
11
+ // timed out on every poll and pinned the NPC at warning.
10
12
  latencyHosts: [
11
- { host: '8.8.8.8', port: 80 },
12
- { host: '1.1.1.1', port: 80 },
13
+ { host: '8.8.8.8', port: 443 },
14
+ { host: '1.1.1.1', port: 443 },
13
15
  ],
16
+ latencyPort: 80,
14
17
  slowThreshold: 1000, // ms
15
18
  httpTimeout: 5000, // ms
16
19
  thresholds: {