@nocville/npc 0.0.0-stage → 0.2.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.
- package/README.md +243 -2
- package/dist/cjs/cli.js +533 -0
- package/dist/cjs/core/api-key.js +69 -0
- package/dist/cjs/core/base-npc.js +160 -0
- package/dist/cjs/core/config.js +108 -0
- package/dist/cjs/core/event-npc.js +14 -0
- package/dist/cjs/core/logger.js +39 -0
- package/dist/cjs/core/polling-npc.js +65 -0
- package/dist/cjs/core/screen.js +28 -0
- package/dist/cjs/core/text.js +15 -0
- package/dist/cjs/core/thresholds.js +48 -0
- package/dist/cjs/core/types.js +10 -0
- package/dist/cjs/core/user-config.js +92 -0
- package/dist/cjs/doctor.js +130 -0
- package/dist/cjs/index.js +90 -0
- package/dist/cjs/npcs/agent-common/activity.js +291 -0
- package/dist/cjs/npcs/agent-common/git-stats.js +71 -0
- package/dist/cjs/npcs/agent-common/hook-runner.js +252 -0
- package/dist/cjs/npcs/agent-common/hook-settings.js +279 -0
- package/dist/cjs/npcs/agent-common/liveness.js +112 -0
- package/dist/cjs/npcs/agent-common/lock.js +99 -0
- package/dist/cjs/npcs/agent-common/runtime.js +154 -0
- package/dist/cjs/npcs/agent-common/session.js +390 -0
- package/dist/cjs/npcs/agent-common/shim.js +96 -0
- package/dist/cjs/npcs/agent-common/watchdog.js +149 -0
- package/dist/cjs/npcs/claude-code/defaults.js +20 -0
- package/dist/cjs/npcs/claude-code/hook-handler.js +139 -0
- package/dist/cjs/npcs/claude-code/index.js +104 -0
- package/dist/cjs/npcs/claude-code/setup.js +133 -0
- package/dist/cjs/npcs/claude-code/state-machine.js +130 -0
- package/dist/cjs/npcs/codex/defaults.js +32 -0
- package/dist/cjs/npcs/codex/hook-handler.js +116 -0
- package/dist/cjs/npcs/codex/index.js +92 -0
- package/dist/cjs/npcs/codex/rollout.js +246 -0
- package/dist/cjs/npcs/codex/setup.js +104 -0
- package/dist/cjs/npcs/codex/watcher.js +346 -0
- package/dist/cjs/npcs/cursor/defaults.js +15 -0
- package/dist/cjs/npcs/cursor/hook-handler.js +65 -0
- package/dist/cjs/npcs/cursor/index.js +72 -0
- package/dist/cjs/npcs/cursor/normalize.js +54 -0
- package/dist/cjs/npcs/cursor/setup.js +98 -0
- package/dist/cjs/npcs/gemini/defaults.js +15 -0
- package/dist/cjs/npcs/gemini/hook-handler.js +93 -0
- package/dist/cjs/npcs/gemini/index.js +69 -0
- package/dist/cjs/npcs/gemini/setup.js +98 -0
- package/dist/cjs/npcs/github-watcher/defaults.js +11 -0
- package/dist/cjs/npcs/github-watcher/format.js +21 -0
- package/dist/cjs/npcs/github-watcher/github-api.js +126 -0
- package/dist/cjs/npcs/github-watcher/index.js +148 -0
- package/dist/cjs/npcs/internet-connectivity/checks.js +92 -0
- package/dist/cjs/npcs/internet-connectivity/defaults.js +22 -0
- package/dist/cjs/npcs/internet-connectivity/index.js +251 -0
- package/dist/cjs/npcs/log-watcher/buffer.js +40 -0
- package/dist/cjs/npcs/log-watcher/defaults.js +12 -0
- package/dist/cjs/npcs/log-watcher/index.js +161 -0
- package/dist/cjs/npcs/log-watcher/tail.js +81 -0
- package/dist/cjs/npcs/process-watcher/defaults.js +9 -0
- package/dist/cjs/npcs/process-watcher/index.js +140 -0
- package/dist/cjs/npcs/process-watcher/process-scanner.js +114 -0
- package/dist/cjs/npcs/receptionist/defaults.js +9 -0
- package/dist/cjs/npcs/receptionist/index.js +120 -0
- package/dist/cjs/npcs/receptionist/weather.js +129 -0
- package/dist/cjs/npcs/system-monitor/collectors.js +68 -0
- package/dist/cjs/npcs/system-monitor/defaults.js +13 -0
- package/dist/cjs/npcs/system-monitor/index.js +105 -0
- package/dist/cjs/npcs/web-page-watcher/basic-mode.js +58 -0
- package/dist/cjs/npcs/web-page-watcher/capture.js +80 -0
- package/dist/cjs/npcs/web-page-watcher/defaults.js +31 -0
- package/dist/cjs/npcs/web-page-watcher/index.js +234 -0
- package/dist/cjs/npcs/web-page-watcher/pixelate.js +181 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +498 -0
- package/dist/core/api-key.d.ts +34 -0
- package/dist/core/api-key.js +64 -0
- package/dist/core/base-npc.d.ts +41 -0
- package/dist/core/base-npc.js +156 -0
- package/dist/core/config.d.ts +22 -0
- package/dist/core/config.js +101 -0
- package/dist/core/event-npc.d.ts +8 -0
- package/dist/core/event-npc.js +10 -0
- package/dist/core/logger.d.ts +11 -0
- package/dist/core/logger.js +35 -0
- package/dist/core/polling-npc.d.ts +18 -0
- package/dist/core/polling-npc.js +61 -0
- package/dist/core/screen.d.ts +11 -0
- package/dist/core/screen.js +24 -0
- package/dist/core/text.d.ts +6 -0
- package/dist/core/text.js +12 -0
- package/dist/core/thresholds.d.ts +10 -0
- package/dist/core/thresholds.js +44 -0
- package/dist/core/types.d.ts +60 -0
- package/dist/core/types.js +7 -0
- package/dist/core/user-config.d.ts +43 -0
- package/dist/core/user-config.js +85 -0
- package/dist/doctor.d.ts +8 -0
- package/dist/doctor.js +126 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +25 -0
- package/dist/npcs/agent-common/activity.d.ts +42 -0
- package/dist/npcs/agent-common/activity.js +280 -0
- package/dist/npcs/agent-common/git-stats.d.ts +11 -0
- package/dist/npcs/agent-common/git-stats.js +67 -0
- package/dist/npcs/agent-common/hook-runner.d.ts +62 -0
- package/dist/npcs/agent-common/hook-runner.js +245 -0
- package/dist/npcs/agent-common/hook-settings.d.ts +75 -0
- package/dist/npcs/agent-common/hook-settings.js +265 -0
- package/dist/npcs/agent-common/liveness.d.ts +12 -0
- package/dist/npcs/agent-common/liveness.js +105 -0
- package/dist/npcs/agent-common/lock.d.ts +6 -0
- package/dist/npcs/agent-common/lock.js +95 -0
- package/dist/npcs/agent-common/runtime.d.ts +16 -0
- package/dist/npcs/agent-common/runtime.js +149 -0
- package/dist/npcs/agent-common/session.d.ts +184 -0
- package/dist/npcs/agent-common/session.js +368 -0
- package/dist/npcs/agent-common/shim.d.ts +4 -0
- package/dist/npcs/agent-common/shim.js +90 -0
- package/dist/npcs/agent-common/watchdog.d.ts +16 -0
- package/dist/npcs/agent-common/watchdog.js +144 -0
- package/dist/npcs/claude-code/defaults.d.ts +15 -0
- package/dist/npcs/claude-code/defaults.js +17 -0
- package/dist/npcs/claude-code/hook-handler.d.ts +40 -0
- package/dist/npcs/claude-code/hook-handler.js +131 -0
- package/dist/npcs/claude-code/index.d.ts +36 -0
- package/dist/npcs/claude-code/index.js +92 -0
- package/dist/npcs/claude-code/setup.d.ts +58 -0
- package/dist/npcs/claude-code/setup.js +121 -0
- package/dist/npcs/claude-code/state-machine.d.ts +28 -0
- package/dist/npcs/claude-code/state-machine.js +125 -0
- package/dist/npcs/codex/defaults.d.ts +24 -0
- package/dist/npcs/codex/defaults.js +28 -0
- package/dist/npcs/codex/hook-handler.d.ts +24 -0
- package/dist/npcs/codex/hook-handler.js +110 -0
- package/dist/npcs/codex/index.d.ts +21 -0
- package/dist/npcs/codex/index.js +76 -0
- package/dist/npcs/codex/rollout.d.ts +39 -0
- package/dist/npcs/codex/rollout.js +242 -0
- package/dist/npcs/codex/setup.d.ts +37 -0
- package/dist/npcs/codex/setup.js +96 -0
- package/dist/npcs/codex/watcher.d.ts +62 -0
- package/dist/npcs/codex/watcher.js +341 -0
- package/dist/npcs/cursor/defaults.d.ts +9 -0
- package/dist/npcs/cursor/defaults.js +11 -0
- package/dist/npcs/cursor/hook-handler.d.ts +8 -0
- package/dist/npcs/cursor/hook-handler.js +60 -0
- package/dist/npcs/cursor/index.d.ts +18 -0
- package/dist/npcs/cursor/index.js +57 -0
- package/dist/npcs/cursor/normalize.d.ts +6 -0
- package/dist/npcs/cursor/normalize.js +49 -0
- package/dist/npcs/cursor/setup.d.ts +29 -0
- package/dist/npcs/cursor/setup.js +90 -0
- package/dist/npcs/gemini/defaults.d.ts +9 -0
- package/dist/npcs/gemini/defaults.js +11 -0
- package/dist/npcs/gemini/hook-handler.d.ts +10 -0
- package/dist/npcs/gemini/hook-handler.js +87 -0
- package/dist/npcs/gemini/index.d.ts +17 -0
- package/dist/npcs/gemini/index.js +56 -0
- package/dist/npcs/gemini/setup.d.ts +29 -0
- package/dist/npcs/gemini/setup.js +90 -0
- package/dist/npcs/github-watcher/defaults.d.ts +8 -0
- package/dist/npcs/github-watcher/defaults.js +8 -0
- package/dist/npcs/github-watcher/format.d.ts +3 -0
- package/dist/npcs/github-watcher/format.js +18 -0
- package/dist/npcs/github-watcher/github-api.d.ts +55 -0
- package/dist/npcs/github-watcher/github-api.js +120 -0
- package/dist/npcs/github-watcher/index.d.ts +35 -0
- package/dist/npcs/github-watcher/index.js +144 -0
- package/dist/npcs/internet-connectivity/checks.d.ts +24 -0
- package/dist/npcs/internet-connectivity/checks.js +84 -0
- package/dist/npcs/internet-connectivity/defaults.d.ts +20 -0
- package/dist/npcs/internet-connectivity/defaults.js +19 -0
- package/dist/npcs/internet-connectivity/index.d.ts +55 -0
- package/dist/npcs/internet-connectivity/index.js +246 -0
- package/dist/npcs/log-watcher/buffer.d.ts +18 -0
- package/dist/npcs/log-watcher/buffer.js +34 -0
- package/dist/npcs/log-watcher/defaults.d.ts +9 -0
- package/dist/npcs/log-watcher/defaults.js +9 -0
- package/dist/npcs/log-watcher/index.d.ts +45 -0
- package/dist/npcs/log-watcher/index.js +157 -0
- package/dist/npcs/log-watcher/tail.d.ts +9 -0
- package/dist/npcs/log-watcher/tail.js +78 -0
- package/dist/npcs/process-watcher/defaults.d.ts +6 -0
- package/dist/npcs/process-watcher/defaults.js +6 -0
- package/dist/npcs/process-watcher/index.d.ts +26 -0
- package/dist/npcs/process-watcher/index.js +136 -0
- package/dist/npcs/process-watcher/process-scanner.d.ts +19 -0
- package/dist/npcs/process-watcher/process-scanner.js +110 -0
- package/dist/npcs/receptionist/defaults.d.ts +6 -0
- package/dist/npcs/receptionist/defaults.js +6 -0
- package/dist/npcs/receptionist/index.d.ts +29 -0
- package/dist/npcs/receptionist/index.js +116 -0
- package/dist/npcs/receptionist/weather.d.ts +37 -0
- package/dist/npcs/receptionist/weather.js +121 -0
- package/dist/npcs/system-monitor/collectors.d.ts +45 -0
- package/dist/npcs/system-monitor/collectors.js +60 -0
- package/dist/npcs/system-monitor/defaults.d.ts +7 -0
- package/dist/npcs/system-monitor/defaults.js +10 -0
- package/dist/npcs/system-monitor/index.d.ts +27 -0
- package/dist/npcs/system-monitor/index.js +101 -0
- package/dist/npcs/web-page-watcher/basic-mode.d.ts +16 -0
- package/dist/npcs/web-page-watcher/basic-mode.js +52 -0
- package/dist/npcs/web-page-watcher/capture.d.ts +49 -0
- package/dist/npcs/web-page-watcher/capture.js +75 -0
- package/dist/npcs/web-page-watcher/defaults.d.ts +19 -0
- package/dist/npcs/web-page-watcher/defaults.js +27 -0
- package/dist/npcs/web-page-watcher/index.d.ts +42 -0
- package/dist/npcs/web-page-watcher/index.js +230 -0
- package/dist/npcs/web-page-watcher/pixelate.d.ts +36 -0
- package/dist/npcs/web-page-watcher/pixelate.js +172 -0
- package/package.json +73 -3
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LogWatcherNPC = void 0;
|
|
4
|
+
const node_path_1 = require("node:path");
|
|
5
|
+
const base_npc_js_1 = require("../../core/base-npc.js");
|
|
6
|
+
const thresholds_js_1 = require("../../core/thresholds.js");
|
|
7
|
+
const text_js_1 = require("../../core/text.js");
|
|
8
|
+
const tail_js_1 = require("./tail.js");
|
|
9
|
+
const buffer_js_1 = require("./buffer.js");
|
|
10
|
+
const defaults_js_1 = require("./defaults.js");
|
|
11
|
+
class LogWatcherNPC extends base_npc_js_1.BaseNPC {
|
|
12
|
+
tailers = [];
|
|
13
|
+
timer = null;
|
|
14
|
+
entries = [];
|
|
15
|
+
recentLines = [];
|
|
16
|
+
lastBurstPush = 0;
|
|
17
|
+
errorRegex;
|
|
18
|
+
warningRegex;
|
|
19
|
+
windowMs;
|
|
20
|
+
errorWarn;
|
|
21
|
+
errorCrit;
|
|
22
|
+
reportInterval;
|
|
23
|
+
files;
|
|
24
|
+
constructor(config) {
|
|
25
|
+
super(config);
|
|
26
|
+
this.files = config.files;
|
|
27
|
+
this.errorRegex = new RegExp(config.errorPattern ?? defaults_js_1.DEFAULTS.errorPattern);
|
|
28
|
+
this.warningRegex = new RegExp(config.warningPattern ?? defaults_js_1.DEFAULTS.warningPattern);
|
|
29
|
+
this.windowMs = (config.windowSeconds ?? defaults_js_1.DEFAULTS.windowSeconds) * 1000;
|
|
30
|
+
this.errorWarn = config.errorWarn ?? defaults_js_1.DEFAULTS.errorWarn;
|
|
31
|
+
this.errorCrit = config.errorCrit ?? defaults_js_1.DEFAULTS.errorCrit;
|
|
32
|
+
this.reportInterval = config.reportInterval ?? defaults_js_1.DEFAULTS.reportInterval;
|
|
33
|
+
}
|
|
34
|
+
get name() {
|
|
35
|
+
return 'log-watcher';
|
|
36
|
+
}
|
|
37
|
+
get defaultDisplayName() {
|
|
38
|
+
const first = this.files[0];
|
|
39
|
+
return first ? `Logs: ${(0, node_path_1.basename)(first)}` : 'Logs';
|
|
40
|
+
}
|
|
41
|
+
get archetype() {
|
|
42
|
+
return 'service';
|
|
43
|
+
}
|
|
44
|
+
get heartbeatSeconds() {
|
|
45
|
+
return 30;
|
|
46
|
+
}
|
|
47
|
+
get behavior() {
|
|
48
|
+
return { idle: 'wander' };
|
|
49
|
+
}
|
|
50
|
+
get companion() {
|
|
51
|
+
return {
|
|
52
|
+
screenType: 'log-terminal',
|
|
53
|
+
size: { w: 2, h: 2 },
|
|
54
|
+
placement: 'adjacent-wall',
|
|
55
|
+
label: this.files[0] ? (0, node_path_1.basename)(this.files[0]) : 'log',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async onStart() {
|
|
59
|
+
for (const filePath of this.files) {
|
|
60
|
+
this.logger.info(`Tailing ${filePath}`);
|
|
61
|
+
const tailer = (0, tail_js_1.createTailer)(filePath, (line) => {
|
|
62
|
+
this.handleLine(line);
|
|
63
|
+
});
|
|
64
|
+
this.tailers.push(tailer);
|
|
65
|
+
}
|
|
66
|
+
// Report immediately, then on interval
|
|
67
|
+
await this.report();
|
|
68
|
+
this.timer = setInterval(() => {
|
|
69
|
+
void this.report();
|
|
70
|
+
}, this.reportInterval);
|
|
71
|
+
}
|
|
72
|
+
async onStop() {
|
|
73
|
+
if (this.timer) {
|
|
74
|
+
clearInterval(this.timer);
|
|
75
|
+
this.timer = null;
|
|
76
|
+
}
|
|
77
|
+
for (const tailer of this.tailers) {
|
|
78
|
+
tailer.close();
|
|
79
|
+
}
|
|
80
|
+
this.tailers = [];
|
|
81
|
+
}
|
|
82
|
+
handleLine(line) {
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
const level = (0, buffer_js_1.classifyLine)(line, this.errorRegex, this.warningRegex);
|
|
85
|
+
// Ring buffer holds ALL lines (matched ones flagged) for the log TV.
|
|
86
|
+
(0, buffer_js_1.pushRecent)(this.recentLines, line, level);
|
|
87
|
+
if (level === 'error') {
|
|
88
|
+
this.entries.push({ timestamp: now, type: 'error', message: line });
|
|
89
|
+
this.onErrorBurst(line);
|
|
90
|
+
}
|
|
91
|
+
else if (level === 'warning') {
|
|
92
|
+
this.entries.push({ timestamp: now, type: 'warning', message: line });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** A fresh error triggers an immediate push, debounced to at most once / 5s. */
|
|
96
|
+
onErrorBurst(line) {
|
|
97
|
+
const now = Date.now();
|
|
98
|
+
if (!(0, buffer_js_1.shouldEmitBurst)(this.lastBurstPush, now))
|
|
99
|
+
return;
|
|
100
|
+
this.lastBurstPush = now;
|
|
101
|
+
void this.pushReport((0, text_js_1.truncate)(line, 200), 'error');
|
|
102
|
+
}
|
|
103
|
+
pruneWindow() {
|
|
104
|
+
const cutoff = Date.now() - this.windowMs;
|
|
105
|
+
this.entries = this.entries.filter((e) => e.timestamp >= cutoff);
|
|
106
|
+
}
|
|
107
|
+
async report() {
|
|
108
|
+
if (this.state !== 'running')
|
|
109
|
+
return;
|
|
110
|
+
await this.pushReport();
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Build and push the current status. If `overrideMessage` is set (burst push),
|
|
114
|
+
* it drives the bubble and the status floors at warning.
|
|
115
|
+
*/
|
|
116
|
+
async pushReport(overrideMessage, subStatus) {
|
|
117
|
+
if (this.state !== 'running')
|
|
118
|
+
return;
|
|
119
|
+
this.pruneWindow();
|
|
120
|
+
const errorCount = this.entries.filter((e) => e.type === 'error').length;
|
|
121
|
+
const warningCount = this.entries.filter((e) => e.type === 'warning').length;
|
|
122
|
+
const windowSeconds = this.windowMs / 1000;
|
|
123
|
+
let status = (0, thresholds_js_1.evaluateThreshold)(errorCount, {
|
|
124
|
+
warning: this.errorWarn,
|
|
125
|
+
critical: this.errorCrit,
|
|
126
|
+
direction: 'above',
|
|
127
|
+
});
|
|
128
|
+
if (overrideMessage)
|
|
129
|
+
status = (0, thresholds_js_1.worstStatus)([status, 'warning']);
|
|
130
|
+
const mood = status === 'critical' ? 'stressed' : status === 'warning' ? 'concerned' : 'happy';
|
|
131
|
+
const lastError = this.entries.filter((e) => e.type === 'error').at(-1);
|
|
132
|
+
const message = overrideMessage ??
|
|
133
|
+
(errorCount > 0
|
|
134
|
+
? `${errorCount} error${errorCount !== 1 ? 's' : ''} in last ${windowSeconds / 60}min across ${this.files.length} file${this.files.length !== 1 ? 's' : ''}`
|
|
135
|
+
: `No errors in last ${windowSeconds / 60}min`);
|
|
136
|
+
const metrics = {
|
|
137
|
+
status,
|
|
138
|
+
mood,
|
|
139
|
+
message,
|
|
140
|
+
subStatus,
|
|
141
|
+
screenData: (0, buffer_js_1.buildLogScreen)(this.recentLines),
|
|
142
|
+
metrics: {
|
|
143
|
+
error_count: errorCount,
|
|
144
|
+
warning_count: warningCount,
|
|
145
|
+
window_seconds: windowSeconds,
|
|
146
|
+
files_watched: this.files.length,
|
|
147
|
+
last_error_message: lastError?.message ?? '',
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
try {
|
|
151
|
+
await this.pushMetrics(metrics);
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
this.logger.error('Failed to push metrics', {
|
|
155
|
+
error: err instanceof Error ? err.message : String(err),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
exports.LogWatcherNPC = LogWatcherNPC;
|
|
161
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createTailer = createTailer;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const node_readline_1 = require("node:readline");
|
|
6
|
+
/**
|
|
7
|
+
* Tail a file by watching for changes and reading new lines from the last
|
|
8
|
+
* known byte offset. Handles file rotation (size shrinks -> reset to 0).
|
|
9
|
+
*/
|
|
10
|
+
function createTailer(filePath, onLine) {
|
|
11
|
+
let offset = 0;
|
|
12
|
+
let reading = false;
|
|
13
|
+
let watcher = null;
|
|
14
|
+
let closed = false;
|
|
15
|
+
async function init() {
|
|
16
|
+
// Start at the end of the file so we only read new lines
|
|
17
|
+
try {
|
|
18
|
+
const stats = await node_fs_1.promises.stat(filePath);
|
|
19
|
+
offset = stats.size;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// File may not exist yet; start at 0
|
|
23
|
+
offset = 0;
|
|
24
|
+
}
|
|
25
|
+
watcher = (0, node_fs_1.watch)(filePath, () => {
|
|
26
|
+
if (!closed) {
|
|
27
|
+
void readNewLines();
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
// Ignore watcher errors (e.g. file deleted then recreated)
|
|
31
|
+
watcher.on('error', () => { });
|
|
32
|
+
}
|
|
33
|
+
async function readNewLines() {
|
|
34
|
+
if (reading || closed)
|
|
35
|
+
return;
|
|
36
|
+
reading = true;
|
|
37
|
+
try {
|
|
38
|
+
const stats = await node_fs_1.promises.stat(filePath);
|
|
39
|
+
// File rotation detected: size smaller than our offset
|
|
40
|
+
if (stats.size < offset) {
|
|
41
|
+
offset = 0;
|
|
42
|
+
}
|
|
43
|
+
// Nothing new to read
|
|
44
|
+
if (stats.size === offset) {
|
|
45
|
+
reading = false;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const stream = (0, node_fs_1.createReadStream)(filePath, {
|
|
49
|
+
start: offset,
|
|
50
|
+
encoding: 'utf-8',
|
|
51
|
+
});
|
|
52
|
+
const rl = (0, node_readline_1.createInterface)({ input: stream, crlfDelay: Infinity });
|
|
53
|
+
let bytesRead = 0;
|
|
54
|
+
for await (const line of rl) {
|
|
55
|
+
if (closed)
|
|
56
|
+
break;
|
|
57
|
+
// Account for line content + newline character
|
|
58
|
+
bytesRead += Buffer.byteLength(line, 'utf-8') + 1;
|
|
59
|
+
onLine(line);
|
|
60
|
+
}
|
|
61
|
+
offset += bytesRead;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// File may have been removed; ignore and wait for next change
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
reading = false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
void init();
|
|
71
|
+
return {
|
|
72
|
+
close() {
|
|
73
|
+
closed = true;
|
|
74
|
+
if (watcher) {
|
|
75
|
+
watcher.close();
|
|
76
|
+
watcher = null;
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=tail.js.map
|
|
@@ -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
|