@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.
- package/README.md +55 -17
- package/dist/cjs/cli.js +94 -39
- package/dist/cjs/core/base-npc.js +15 -0
- package/dist/cjs/core/config.js +19 -2
- package/dist/cjs/core/errors.js +23 -0
- package/dist/cjs/core/http.js +121 -0
- package/dist/cjs/core/pixel-art.js +110 -0
- package/dist/cjs/core/polling-npc.js +99 -14
- package/dist/cjs/core/text.js +13 -0
- package/dist/cjs/core/thresholds.js +29 -0
- package/dist/cjs/index.js +21 -2
- package/dist/cjs/npcs/agent-common/session.js +1 -0
- package/dist/cjs/npcs/agent-common/shim.js +8 -2
- package/dist/cjs/npcs/github-watcher/defaults.js +7 -1
- package/dist/cjs/npcs/github-watcher/github-api.js +82 -45
- package/dist/cjs/npcs/github-watcher/index.js +82 -36
- package/dist/cjs/npcs/internet-connectivity/checks.js +70 -67
- package/dist/cjs/npcs/internet-connectivity/defaults.js +5 -2
- package/dist/cjs/npcs/internet-connectivity/index.js +134 -131
- package/dist/cjs/npcs/log-watcher/buffer.js +61 -0
- package/dist/cjs/npcs/log-watcher/index.js +109 -103
- package/dist/cjs/npcs/log-watcher/tail.js +100 -53
- package/dist/cjs/npcs/office-characters/index.js +92 -0
- package/dist/cjs/npcs/process-watcher/index.js +60 -51
- package/dist/cjs/npcs/process-watcher/process-scanner.js +100 -68
- package/dist/cjs/npcs/receptionist/defaults.js +4 -0
- package/dist/cjs/npcs/receptionist/index.js +6 -93
- package/dist/cjs/npcs/receptionist/weather.js +42 -47
- package/dist/cjs/npcs/security/index.js +26 -0
- package/dist/cjs/npcs/system-monitor/collectors.js +66 -17
- package/dist/cjs/npcs/system-monitor/index.js +52 -28
- package/dist/cjs/npcs/web-page-watcher/basic-mode.js +30 -14
- package/dist/cjs/npcs/web-page-watcher/capture.js +23 -1
- package/dist/cjs/npcs/web-page-watcher/defaults.js +44 -2
- package/dist/cjs/npcs/web-page-watcher/index.js +119 -68
- package/dist/cjs/npcs/web-page-watcher/pixelate.js +2 -1
- package/dist/cli.js +95 -40
- package/dist/core/base-npc.d.ts +5 -0
- package/dist/core/base-npc.js +15 -0
- package/dist/core/config.d.ts +5 -0
- package/dist/core/config.js +18 -2
- package/dist/core/errors.d.ts +11 -0
- package/dist/core/errors.js +18 -0
- package/dist/core/http.d.ts +52 -0
- package/dist/core/http.js +111 -0
- package/dist/core/pixel-art.d.ts +38 -0
- package/dist/core/pixel-art.js +105 -0
- package/dist/core/polling-npc.d.ts +22 -1
- package/dist/core/polling-npc.js +98 -13
- package/dist/core/text.d.ts +6 -0
- package/dist/core/text.js +12 -0
- package/dist/core/thresholds.d.ts +9 -1
- package/dist/core/thresholds.js +27 -0
- package/dist/core/types.d.ts +5 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +6 -2
- package/dist/npcs/agent-common/session.js +1 -0
- package/dist/npcs/agent-common/shim.js +8 -2
- package/dist/npcs/github-watcher/defaults.d.ts +6 -0
- package/dist/npcs/github-watcher/defaults.js +7 -1
- package/dist/npcs/github-watcher/github-api.d.ts +21 -7
- package/dist/npcs/github-watcher/github-api.js +80 -45
- package/dist/npcs/github-watcher/index.d.ts +6 -0
- package/dist/npcs/github-watcher/index.js +83 -37
- package/dist/npcs/internet-connectivity/checks.d.ts +15 -18
- package/dist/npcs/internet-connectivity/checks.js +70 -67
- package/dist/npcs/internet-connectivity/defaults.d.ts +3 -0
- package/dist/npcs/internet-connectivity/defaults.js +5 -2
- package/dist/npcs/internet-connectivity/index.d.ts +19 -6
- package/dist/npcs/internet-connectivity/index.js +135 -133
- package/dist/npcs/log-watcher/buffer.d.ts +22 -0
- package/dist/npcs/log-watcher/buffer.js +58 -0
- package/dist/npcs/log-watcher/index.d.ts +22 -21
- package/dist/npcs/log-watcher/index.js +112 -106
- package/dist/npcs/log-watcher/tail.d.ts +23 -3
- package/dist/npcs/log-watcher/tail.js +101 -54
- package/dist/npcs/office-characters/index.d.ts +50 -0
- package/dist/npcs/office-characters/index.js +82 -0
- package/dist/npcs/process-watcher/index.d.ts +1 -1
- package/dist/npcs/process-watcher/index.js +61 -52
- package/dist/npcs/process-watcher/process-scanner.d.ts +31 -1
- package/dist/npcs/process-watcher/process-scanner.js +97 -70
- package/dist/npcs/receptionist/defaults.d.ts +4 -0
- package/dist/npcs/receptionist/defaults.js +4 -0
- package/dist/npcs/receptionist/index.d.ts +3 -12
- package/dist/npcs/receptionist/index.js +6 -93
- package/dist/npcs/receptionist/weather.d.ts +8 -6
- package/dist/npcs/receptionist/weather.js +39 -45
- package/dist/npcs/security/index.d.ts +12 -0
- package/dist/npcs/security/index.js +22 -0
- package/dist/npcs/system-monitor/collectors.d.ts +15 -3
- package/dist/npcs/system-monitor/collectors.js +64 -17
- package/dist/npcs/system-monitor/defaults.d.ts +1 -1
- package/dist/npcs/system-monitor/index.d.ts +2 -1
- package/dist/npcs/system-monitor/index.js +54 -30
- package/dist/npcs/web-page-watcher/basic-mode.d.ts +2 -2
- package/dist/npcs/web-page-watcher/basic-mode.js +30 -14
- package/dist/npcs/web-page-watcher/capture.d.ts +18 -0
- package/dist/npcs/web-page-watcher/capture.js +21 -1
- package/dist/npcs/web-page-watcher/defaults.d.ts +23 -0
- package/dist/npcs/web-page-watcher/defaults.js +41 -1
- package/dist/npcs/web-page-watcher/index.d.ts +18 -1
- package/dist/npcs/web-page-watcher/index.js +121 -70
- package/dist/npcs/web-page-watcher/pixelate.js +2 -1
- package/package.json +2 -2
|
@@ -1,49 +1,87 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.LogWatcherNPC = void 0;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
4
5
|
const node_path_1 = require("node:path");
|
|
5
|
-
const
|
|
6
|
+
const polling_npc_js_1 = require("../../core/polling-npc.js");
|
|
6
7
|
const thresholds_js_1 = require("../../core/thresholds.js");
|
|
7
|
-
const
|
|
8
|
+
const errors_js_1 = require("../../core/errors.js");
|
|
8
9
|
const tail_js_1 = require("./tail.js");
|
|
9
10
|
const buffer_js_1 = require("./buffer.js");
|
|
10
11
|
const defaults_js_1 = require("./defaults.js");
|
|
11
|
-
|
|
12
|
+
/** Lines shown on the log terminal, and their width. */
|
|
13
|
+
const SCREEN_LINES = 12;
|
|
14
|
+
const SCREEN_LINE_CHARS = 80;
|
|
15
|
+
function compilePattern(flag, source) {
|
|
16
|
+
try {
|
|
17
|
+
return new RegExp(source);
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
throw new errors_js_1.NpcConfigError(`${flag}: invalid regular expression (${err instanceof Error ? err.message : String(err)})`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** "45s", "5min", "1.5h" */
|
|
24
|
+
function formatWindow(seconds) {
|
|
25
|
+
if (seconds < 60)
|
|
26
|
+
return `${seconds}s`;
|
|
27
|
+
if (seconds < 3600)
|
|
28
|
+
return `${Math.round((seconds / 60) * 10) / 10}min`;
|
|
29
|
+
return `${Math.round((seconds / 3600) * 10) / 10}h`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Tails log files and reports error bursts. A periodic report (every
|
|
33
|
+
* `reportInterval`) carries the error count over the sliding window and the
|
|
34
|
+
* last lines for the log terminal; a fresh error line also triggers an
|
|
35
|
+
* immediate report, debounced to one per 5s, so a burst shows up at once.
|
|
36
|
+
* Every report is one composed push, never a push per line.
|
|
37
|
+
*/
|
|
38
|
+
class LogWatcherNPC extends polling_npc_js_1.PollingNPC {
|
|
12
39
|
tailers = [];
|
|
13
|
-
|
|
14
|
-
entries = [];
|
|
40
|
+
counter;
|
|
15
41
|
recentLines = [];
|
|
16
|
-
|
|
42
|
+
lastError = null;
|
|
43
|
+
/** The error line that triggered an immediate report, consumed by the next collect(). */
|
|
44
|
+
burstLine = null;
|
|
45
|
+
lastBurstAt = 0;
|
|
17
46
|
errorRegex;
|
|
18
47
|
warningRegex;
|
|
19
48
|
windowMs;
|
|
20
|
-
|
|
21
|
-
errorCrit;
|
|
22
|
-
reportInterval;
|
|
49
|
+
errorThreshold;
|
|
23
50
|
files;
|
|
24
51
|
constructor(config) {
|
|
25
|
-
super(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
this.
|
|
30
|
-
this.
|
|
31
|
-
|
|
32
|
-
|
|
52
|
+
super({
|
|
53
|
+
...config,
|
|
54
|
+
pollInterval: config.reportInterval ?? config.pollInterval ?? defaults_js_1.DEFAULTS.reportInterval,
|
|
55
|
+
});
|
|
56
|
+
this.files = [...new Set(config.files.map((f) => f.trim()).filter(Boolean))].map((f) => (0, node_path_1.resolve)(f));
|
|
57
|
+
if (this.files.length === 0) {
|
|
58
|
+
throw new errors_js_1.NpcConfigError('log-watcher needs at least one file (--files)');
|
|
59
|
+
}
|
|
60
|
+
this.errorRegex = compilePattern('--error-pattern', config.errorPattern ?? defaults_js_1.DEFAULTS.errorPattern);
|
|
61
|
+
this.warningRegex = compilePattern('--warning-pattern', config.warningPattern ?? defaults_js_1.DEFAULTS.warningPattern);
|
|
62
|
+
const windowSeconds = config.windowSeconds ?? defaults_js_1.DEFAULTS.windowSeconds;
|
|
63
|
+
if (!(windowSeconds > 0))
|
|
64
|
+
throw new errors_js_1.NpcConfigError('--window must be a positive number of seconds');
|
|
65
|
+
this.windowMs = windowSeconds * 1000;
|
|
66
|
+
this.counter = new buffer_js_1.WindowCounter(this.windowMs);
|
|
67
|
+
this.errorThreshold = {
|
|
68
|
+
warning: config.errorWarn ?? defaults_js_1.DEFAULTS.errorWarn,
|
|
69
|
+
critical: config.errorCrit ?? defaults_js_1.DEFAULTS.errorCrit,
|
|
70
|
+
direction: 'above',
|
|
71
|
+
};
|
|
72
|
+
const problem = (0, thresholds_js_1.thresholdProblem)('--error-warn/--error-crit', this.errorThreshold);
|
|
73
|
+
if (problem)
|
|
74
|
+
throw new errors_js_1.NpcConfigError(problem);
|
|
33
75
|
}
|
|
34
76
|
get name() {
|
|
35
77
|
return 'log-watcher';
|
|
36
78
|
}
|
|
37
79
|
get defaultDisplayName() {
|
|
38
|
-
|
|
39
|
-
return first ? `Logs: ${(0, node_path_1.basename)(first)}` : 'Logs';
|
|
80
|
+
return `Logs: ${(0, node_path_1.basename)(this.files[0])}`;
|
|
40
81
|
}
|
|
41
82
|
get archetype() {
|
|
42
83
|
return 'service';
|
|
43
84
|
}
|
|
44
|
-
get heartbeatSeconds() {
|
|
45
|
-
return 30;
|
|
46
|
-
}
|
|
47
85
|
get behavior() {
|
|
48
86
|
return { idle: 'wander' };
|
|
49
87
|
}
|
|
@@ -52,109 +90,77 @@ class LogWatcherNPC extends base_npc_js_1.BaseNPC {
|
|
|
52
90
|
screenType: 'log-terminal',
|
|
53
91
|
size: { w: 2, h: 2 },
|
|
54
92
|
placement: 'adjacent-wall',
|
|
55
|
-
label:
|
|
93
|
+
label: (0, node_path_1.basename)(this.files[0]),
|
|
56
94
|
};
|
|
57
95
|
}
|
|
58
96
|
async onStart() {
|
|
59
97
|
for (const filePath of this.files) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
98
|
+
if ((0, node_fs_1.existsSync)(filePath)) {
|
|
99
|
+
this.logger.info(`Tailing ${filePath}`);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
this.logger.warn(`${filePath} does not exist yet; will tail it once it appears`);
|
|
103
|
+
}
|
|
104
|
+
this.tailers.push((0, tail_js_1.createTailer)(filePath, (line) => this.handleLine(line), {
|
|
105
|
+
onError: (err) => this.logger.warn(`Cannot read ${filePath}`, { error: err.message }),
|
|
106
|
+
}));
|
|
65
107
|
}
|
|
66
|
-
|
|
67
|
-
await this.report();
|
|
68
|
-
this.timer = setInterval(() => {
|
|
69
|
-
void this.report();
|
|
70
|
-
}, this.reportInterval);
|
|
108
|
+
await super.onStart();
|
|
71
109
|
}
|
|
72
110
|
async onStop() {
|
|
73
|
-
|
|
74
|
-
clearInterval(this.timer);
|
|
75
|
-
this.timer = null;
|
|
76
|
-
}
|
|
77
|
-
for (const tailer of this.tailers) {
|
|
111
|
+
for (const tailer of this.tailers)
|
|
78
112
|
tailer.close();
|
|
79
|
-
}
|
|
80
113
|
this.tailers = [];
|
|
114
|
+
await super.onStop();
|
|
81
115
|
}
|
|
82
|
-
|
|
83
|
-
|
|
116
|
+
/** Visible for tests: feed one raw line as if a tailer had read it. */
|
|
117
|
+
handleLine(line, now = Date.now()) {
|
|
84
118
|
const level = (0, buffer_js_1.classifyLine)(line, this.errorRegex, this.warningRegex);
|
|
85
|
-
//
|
|
86
|
-
(0, buffer_js_1.pushRecent)(this.recentLines, line, level);
|
|
87
|
-
if (level === '
|
|
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))
|
|
119
|
+
// The ring buffer holds ALL lines (matched ones flagged) for the log TV.
|
|
120
|
+
(0, buffer_js_1.pushRecent)(this.recentLines, (0, buffer_js_1.cleanLogLine)(line, SCREEN_LINE_CHARS), level, SCREEN_LINES);
|
|
121
|
+
if (level === 'info')
|
|
99
122
|
return;
|
|
100
|
-
this.
|
|
101
|
-
|
|
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')
|
|
123
|
+
this.counter.add(level, now);
|
|
124
|
+
if (level !== 'error')
|
|
109
125
|
return;
|
|
110
|
-
|
|
126
|
+
const text = (0, buffer_js_1.cleanLogLine)(line);
|
|
127
|
+
this.lastError = { text, at: now };
|
|
128
|
+
if ((0, buffer_js_1.shouldEmitBurst)(this.lastBurstAt, now)) {
|
|
129
|
+
this.lastBurstAt = now;
|
|
130
|
+
this.burstLine = text;
|
|
131
|
+
this.collectNow();
|
|
132
|
+
}
|
|
111
133
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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)
|
|
134
|
+
async collect() {
|
|
135
|
+
const now = Date.now();
|
|
136
|
+
const { errors, warnings } = this.counter.counts(now);
|
|
137
|
+
const window = formatWindow(this.windowMs / 1000);
|
|
138
|
+
const burst = this.burstLine;
|
|
139
|
+
this.burstLine = null;
|
|
140
|
+
let status = (0, thresholds_js_1.evaluateThreshold)(errors, this.errorThreshold);
|
|
141
|
+
// A burst report drives the bubble with the error itself and floors at warning.
|
|
142
|
+
if (burst)
|
|
129
143
|
status = (0, thresholds_js_1.worstStatus)([status, 'warning']);
|
|
130
|
-
const
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
144
|
+
const fileCount = `${this.files.length} file${this.files.length !== 1 ? 's' : ''}`;
|
|
145
|
+
const message = burst ??
|
|
146
|
+
(errors > 0
|
|
147
|
+
? `${errors} error${errors !== 1 ? 's' : ''} in last ${window} across ${fileCount}`
|
|
148
|
+
: `No errors in last ${window}`);
|
|
149
|
+
const lastErrorInWindow = this.lastError && now - this.lastError.at <= this.windowMs ? this.lastError.text : '';
|
|
150
|
+
return {
|
|
137
151
|
status,
|
|
138
|
-
mood,
|
|
152
|
+
mood: (0, thresholds_js_1.moodForStatus)(status),
|
|
139
153
|
message,
|
|
140
|
-
subStatus,
|
|
154
|
+
subStatus: burst ? 'error' : undefined,
|
|
141
155
|
screenData: (0, buffer_js_1.buildLogScreen)(this.recentLines),
|
|
142
156
|
metrics: {
|
|
143
|
-
error_count:
|
|
144
|
-
warning_count:
|
|
145
|
-
window_seconds:
|
|
157
|
+
error_count: errors,
|
|
158
|
+
warning_count: warnings,
|
|
159
|
+
window_seconds: this.windowMs / 1000,
|
|
146
160
|
files_watched: this.files.length,
|
|
147
|
-
last_error_message:
|
|
161
|
+
last_error_message: lastErrorInWindow,
|
|
148
162
|
},
|
|
149
163
|
};
|
|
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
164
|
}
|
|
159
165
|
}
|
|
160
166
|
exports.LogWatcherNPC = LogWatcherNPC;
|
|
@@ -2,79 +2,126 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createTailer = createTailer;
|
|
4
4
|
const node_fs_1 = require("node:fs");
|
|
5
|
-
const
|
|
5
|
+
const node_string_decoder_1 = require("node:string_decoder");
|
|
6
|
+
const DEFAULTS = {
|
|
7
|
+
pollMs: 1000,
|
|
8
|
+
maxLineChars: 16 * 1024,
|
|
9
|
+
maxReadBytes: 1024 * 1024,
|
|
10
|
+
maxBacklogBytes: 8 * 1024 * 1024,
|
|
11
|
+
};
|
|
12
|
+
function isNotFound(err) {
|
|
13
|
+
return err?.code === 'ENOENT';
|
|
14
|
+
}
|
|
6
15
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
16
|
+
* Follow a growing file like `tail -F`: emit each complete new line, starting
|
|
17
|
+
* from the end of the file as it is now (or from the start of a file created
|
|
18
|
+
* later).
|
|
19
|
+
*
|
|
20
|
+
* Polls with `stat` instead of `fs.watch`: watchers follow the inode, so they
|
|
21
|
+
* go deaf after logrotate's rename-and-recreate, and they throw for files that
|
|
22
|
+
* don't exist yet. Rotation (new inode) and truncation (size shrinks) restart
|
|
23
|
+
* from offset 0. Reads are bounded per poll, a partial last line is held until
|
|
24
|
+
* its newline arrives, over-long lines are cut, and a backlog beyond
|
|
25
|
+
* `maxBacklogBytes` (a runaway logger) is skipped so the watcher stays current.
|
|
9
26
|
*/
|
|
10
|
-
function createTailer(filePath, onLine) {
|
|
27
|
+
function createTailer(filePath, onLine, options = {}) {
|
|
28
|
+
const opts = { ...DEFAULTS, ...options };
|
|
11
29
|
let offset = 0;
|
|
12
|
-
let
|
|
13
|
-
let
|
|
30
|
+
let inode = null;
|
|
31
|
+
let partial = '';
|
|
32
|
+
let decoder = new node_string_decoder_1.StringDecoder('utf8');
|
|
33
|
+
let busy = false;
|
|
14
34
|
let closed = false;
|
|
15
|
-
|
|
16
|
-
|
|
35
|
+
/** False until the first successful stat (or ENOENT) fixes the starting offset. */
|
|
36
|
+
let started = false;
|
|
37
|
+
let lastError = '';
|
|
38
|
+
const reset = () => {
|
|
39
|
+
offset = 0;
|
|
40
|
+
partial = '';
|
|
41
|
+
decoder = new node_string_decoder_1.StringDecoder('utf8');
|
|
42
|
+
};
|
|
43
|
+
const reportError = (err) => {
|
|
44
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
45
|
+
if (error.message === lastError)
|
|
46
|
+
return;
|
|
47
|
+
lastError = error.message;
|
|
48
|
+
opts.onError?.(error);
|
|
49
|
+
};
|
|
50
|
+
const emitText = (text) => {
|
|
51
|
+
const pieces = (partial + text).split('\n');
|
|
52
|
+
partial = pieces.pop().slice(0, opts.maxLineChars);
|
|
53
|
+
for (const piece of pieces) {
|
|
54
|
+
if (closed)
|
|
55
|
+
return;
|
|
56
|
+
onLine(piece.replace(/\r$/, '').slice(0, opts.maxLineChars));
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
async function readNew(size) {
|
|
60
|
+
if (size - offset > opts.maxBacklogBytes) {
|
|
61
|
+
// Too far behind: jump to the newest data rather than lag forever.
|
|
62
|
+
offset = size - opts.maxReadBytes;
|
|
63
|
+
partial = '';
|
|
64
|
+
decoder = new node_string_decoder_1.StringDecoder('utf8');
|
|
65
|
+
}
|
|
66
|
+
const length = Math.min(size - offset, opts.maxReadBytes);
|
|
67
|
+
const handle = await node_fs_1.promises.open(filePath, 'r');
|
|
17
68
|
try {
|
|
18
|
-
const
|
|
19
|
-
|
|
69
|
+
const buf = Buffer.alloc(length);
|
|
70
|
+
const { bytesRead } = await handle.read(buf, 0, length, offset);
|
|
71
|
+
offset += bytesRead;
|
|
72
|
+
emitText(decoder.write(buf.subarray(0, bytesRead)));
|
|
20
73
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
offset = 0;
|
|
74
|
+
finally {
|
|
75
|
+
await handle.close();
|
|
24
76
|
}
|
|
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
77
|
}
|
|
33
|
-
async function
|
|
34
|
-
if (
|
|
78
|
+
async function poll() {
|
|
79
|
+
if (busy || closed)
|
|
35
80
|
return;
|
|
36
|
-
|
|
81
|
+
busy = true;
|
|
37
82
|
try {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
reading = false;
|
|
83
|
+
const st = await node_fs_1.promises.stat(filePath);
|
|
84
|
+
if (!started) {
|
|
85
|
+
// Start at the current end of the file so only new lines are reported.
|
|
86
|
+
started = true;
|
|
87
|
+
inode = st.ino;
|
|
88
|
+
offset = st.size;
|
|
89
|
+
lastError = '';
|
|
46
90
|
return;
|
|
47
91
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
break;
|
|
57
|
-
// Account for line content + newline character
|
|
58
|
-
bytesRead += Buffer.byteLength(line, 'utf-8') + 1;
|
|
59
|
-
onLine(line);
|
|
60
|
-
}
|
|
61
|
-
offset += bytesRead;
|
|
92
|
+
if (inode !== null && st.ino !== inode)
|
|
93
|
+
reset(); // rotated: a new file took the name
|
|
94
|
+
inode = st.ino;
|
|
95
|
+
if (st.size < offset)
|
|
96
|
+
reset(); // truncated in place
|
|
97
|
+
if (st.size > offset)
|
|
98
|
+
await readNew(st.size);
|
|
99
|
+
lastError = '';
|
|
62
100
|
}
|
|
63
|
-
catch {
|
|
64
|
-
|
|
101
|
+
catch (err) {
|
|
102
|
+
if (isNotFound(err)) {
|
|
103
|
+
started = true;
|
|
104
|
+
// Rotated away and not yet recreated (or never created): start the
|
|
105
|
+
// replacement from its first byte.
|
|
106
|
+
if (inode !== null)
|
|
107
|
+
reportError(new Error(`${filePath} disappeared; waiting for it`));
|
|
108
|
+
inode = null;
|
|
109
|
+
reset();
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
reportError(err);
|
|
113
|
+
}
|
|
65
114
|
}
|
|
66
115
|
finally {
|
|
67
|
-
|
|
116
|
+
busy = false;
|
|
68
117
|
}
|
|
69
118
|
}
|
|
70
|
-
void
|
|
119
|
+
const timer = setInterval(() => void poll(), opts.pollMs);
|
|
120
|
+
void poll();
|
|
71
121
|
return {
|
|
72
122
|
close() {
|
|
73
123
|
closed = true;
|
|
74
|
-
|
|
75
|
-
watcher.close();
|
|
76
|
-
watcher = null;
|
|
77
|
-
}
|
|
124
|
+
clearInterval(timer);
|
|
78
125
|
},
|
|
79
126
|
};
|
|
80
127
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OFFICE_CHARACTER_NPCS = exports.OutOfTouchExecutiveNPC = exports.ITTechnicianNPC = exports.PlantCaretakerNPC = exports.OfficeDogNPC = exports.OfficeCatNPC = exports.BaristaNPC = exports.JanitorNPC = void 0;
|
|
4
|
+
const polling_npc_js_1 = require("../../core/polling-npc.js");
|
|
5
|
+
/** Quiet heartbeats; the authoritative server drives routines and conversation. */
|
|
6
|
+
class OfficeCharacterNPC extends polling_npc_js_1.PollingNPC {
|
|
7
|
+
constructor(config) {
|
|
8
|
+
super({ ...config, pollInterval: config.pollInterval ?? 60_000 });
|
|
9
|
+
}
|
|
10
|
+
get archetype() {
|
|
11
|
+
return 'custom';
|
|
12
|
+
}
|
|
13
|
+
get behavior() {
|
|
14
|
+
return { role: this.name, activity: 'idle', idlePose: 'stand' };
|
|
15
|
+
}
|
|
16
|
+
async collect() {
|
|
17
|
+
return { status: 'healthy', mood: 'happy', message: '', metrics: {} };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
class JanitorNPC extends OfficeCharacterNPC {
|
|
21
|
+
get name() {
|
|
22
|
+
return 'janitor';
|
|
23
|
+
}
|
|
24
|
+
get defaultDisplayName() {
|
|
25
|
+
return 'Janitor';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
exports.JanitorNPC = JanitorNPC;
|
|
29
|
+
class BaristaNPC extends OfficeCharacterNPC {
|
|
30
|
+
get name() {
|
|
31
|
+
return 'barista';
|
|
32
|
+
}
|
|
33
|
+
get defaultDisplayName() {
|
|
34
|
+
return 'Barista';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.BaristaNPC = BaristaNPC;
|
|
38
|
+
class OfficeCatNPC extends OfficeCharacterNPC {
|
|
39
|
+
get name() {
|
|
40
|
+
return 'office-cat';
|
|
41
|
+
}
|
|
42
|
+
get defaultDisplayName() {
|
|
43
|
+
return 'Office Cat';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.OfficeCatNPC = OfficeCatNPC;
|
|
47
|
+
class OfficeDogNPC extends OfficeCharacterNPC {
|
|
48
|
+
get name() {
|
|
49
|
+
return 'office-dog';
|
|
50
|
+
}
|
|
51
|
+
get defaultDisplayName() {
|
|
52
|
+
return 'Office Dog';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
exports.OfficeDogNPC = OfficeDogNPC;
|
|
56
|
+
class PlantCaretakerNPC extends OfficeCharacterNPC {
|
|
57
|
+
get name() {
|
|
58
|
+
return 'plant-caretaker';
|
|
59
|
+
}
|
|
60
|
+
get defaultDisplayName() {
|
|
61
|
+
return 'Plant Caretaker';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
exports.PlantCaretakerNPC = PlantCaretakerNPC;
|
|
65
|
+
class ITTechnicianNPC extends OfficeCharacterNPC {
|
|
66
|
+
get name() {
|
|
67
|
+
return 'it-technician';
|
|
68
|
+
}
|
|
69
|
+
get defaultDisplayName() {
|
|
70
|
+
return 'IT Technician';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
exports.ITTechnicianNPC = ITTechnicianNPC;
|
|
74
|
+
class OutOfTouchExecutiveNPC extends OfficeCharacterNPC {
|
|
75
|
+
get name() {
|
|
76
|
+
return 'out-of-touch-executive';
|
|
77
|
+
}
|
|
78
|
+
get defaultDisplayName() {
|
|
79
|
+
return 'Out-of-Touch Executive';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
exports.OutOfTouchExecutiveNPC = OutOfTouchExecutiveNPC;
|
|
83
|
+
exports.OFFICE_CHARACTER_NPCS = {
|
|
84
|
+
janitor: JanitorNPC,
|
|
85
|
+
barista: BaristaNPC,
|
|
86
|
+
'office-cat': OfficeCatNPC,
|
|
87
|
+
'office-dog': OfficeDogNPC,
|
|
88
|
+
'plant-caretaker': PlantCaretakerNPC,
|
|
89
|
+
'it-technician': ITTechnicianNPC,
|
|
90
|
+
'out-of-touch-executive': OutOfTouchExecutiveNPC,
|
|
91
|
+
};
|
|
92
|
+
//# sourceMappingURL=index.js.map
|