@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,45 @@
|
|
|
1
|
+
import { BaseNPC } from '../../core/base-npc.js';
|
|
2
|
+
import type { NpcConfig, Archetype, BehaviorHints, CompanionDecl } from '../../core/types.js';
|
|
3
|
+
export interface LogWatcherConfig extends NpcConfig {
|
|
4
|
+
files: string[];
|
|
5
|
+
errorPattern?: string;
|
|
6
|
+
warningPattern?: string;
|
|
7
|
+
windowSeconds?: number;
|
|
8
|
+
errorWarn?: number;
|
|
9
|
+
errorCrit?: number;
|
|
10
|
+
reportInterval?: number;
|
|
11
|
+
}
|
|
12
|
+
export declare class LogWatcherNPC extends BaseNPC {
|
|
13
|
+
private tailers;
|
|
14
|
+
private timer;
|
|
15
|
+
private entries;
|
|
16
|
+
private recentLines;
|
|
17
|
+
private lastBurstPush;
|
|
18
|
+
private errorRegex;
|
|
19
|
+
private warningRegex;
|
|
20
|
+
private windowMs;
|
|
21
|
+
private errorWarn;
|
|
22
|
+
private errorCrit;
|
|
23
|
+
private reportInterval;
|
|
24
|
+
private files;
|
|
25
|
+
constructor(config: LogWatcherConfig);
|
|
26
|
+
get name(): string;
|
|
27
|
+
protected get defaultDisplayName(): string;
|
|
28
|
+
get archetype(): Archetype;
|
|
29
|
+
get heartbeatSeconds(): number;
|
|
30
|
+
get behavior(): BehaviorHints;
|
|
31
|
+
get companion(): CompanionDecl;
|
|
32
|
+
protected onStart(): Promise<void>;
|
|
33
|
+
protected onStop(): Promise<void>;
|
|
34
|
+
private handleLine;
|
|
35
|
+
/** A fresh error triggers an immediate push, debounced to at most once / 5s. */
|
|
36
|
+
private onErrorBurst;
|
|
37
|
+
private pruneWindow;
|
|
38
|
+
private report;
|
|
39
|
+
/**
|
|
40
|
+
* Build and push the current status. If `overrideMessage` is set (burst push),
|
|
41
|
+
* it drives the bubble and the status floors at warning.
|
|
42
|
+
*/
|
|
43
|
+
private pushReport;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
2
|
+
import { BaseNPC } from '../../core/base-npc.js';
|
|
3
|
+
import { evaluateThreshold, worstStatus } from '../../core/thresholds.js';
|
|
4
|
+
import { truncate } from '../../core/text.js';
|
|
5
|
+
import { createTailer } from './tail.js';
|
|
6
|
+
import { classifyLine, pushRecent, buildLogScreen, shouldEmitBurst } from './buffer.js';
|
|
7
|
+
import { DEFAULTS } from './defaults.js';
|
|
8
|
+
export class LogWatcherNPC extends BaseNPC {
|
|
9
|
+
tailers = [];
|
|
10
|
+
timer = null;
|
|
11
|
+
entries = [];
|
|
12
|
+
recentLines = [];
|
|
13
|
+
lastBurstPush = 0;
|
|
14
|
+
errorRegex;
|
|
15
|
+
warningRegex;
|
|
16
|
+
windowMs;
|
|
17
|
+
errorWarn;
|
|
18
|
+
errorCrit;
|
|
19
|
+
reportInterval;
|
|
20
|
+
files;
|
|
21
|
+
constructor(config) {
|
|
22
|
+
super(config);
|
|
23
|
+
this.files = config.files;
|
|
24
|
+
this.errorRegex = new RegExp(config.errorPattern ?? DEFAULTS.errorPattern);
|
|
25
|
+
this.warningRegex = new RegExp(config.warningPattern ?? DEFAULTS.warningPattern);
|
|
26
|
+
this.windowMs = (config.windowSeconds ?? DEFAULTS.windowSeconds) * 1000;
|
|
27
|
+
this.errorWarn = config.errorWarn ?? DEFAULTS.errorWarn;
|
|
28
|
+
this.errorCrit = config.errorCrit ?? DEFAULTS.errorCrit;
|
|
29
|
+
this.reportInterval = config.reportInterval ?? DEFAULTS.reportInterval;
|
|
30
|
+
}
|
|
31
|
+
get name() {
|
|
32
|
+
return 'log-watcher';
|
|
33
|
+
}
|
|
34
|
+
get defaultDisplayName() {
|
|
35
|
+
const first = this.files[0];
|
|
36
|
+
return first ? `Logs: ${basename(first)}` : 'Logs';
|
|
37
|
+
}
|
|
38
|
+
get archetype() {
|
|
39
|
+
return 'service';
|
|
40
|
+
}
|
|
41
|
+
get heartbeatSeconds() {
|
|
42
|
+
return 30;
|
|
43
|
+
}
|
|
44
|
+
get behavior() {
|
|
45
|
+
return { idle: 'wander' };
|
|
46
|
+
}
|
|
47
|
+
get companion() {
|
|
48
|
+
return {
|
|
49
|
+
screenType: 'log-terminal',
|
|
50
|
+
size: { w: 2, h: 2 },
|
|
51
|
+
placement: 'adjacent-wall',
|
|
52
|
+
label: this.files[0] ? basename(this.files[0]) : 'log',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async onStart() {
|
|
56
|
+
for (const filePath of this.files) {
|
|
57
|
+
this.logger.info(`Tailing ${filePath}`);
|
|
58
|
+
const tailer = createTailer(filePath, (line) => {
|
|
59
|
+
this.handleLine(line);
|
|
60
|
+
});
|
|
61
|
+
this.tailers.push(tailer);
|
|
62
|
+
}
|
|
63
|
+
// Report immediately, then on interval
|
|
64
|
+
await this.report();
|
|
65
|
+
this.timer = setInterval(() => {
|
|
66
|
+
void this.report();
|
|
67
|
+
}, this.reportInterval);
|
|
68
|
+
}
|
|
69
|
+
async onStop() {
|
|
70
|
+
if (this.timer) {
|
|
71
|
+
clearInterval(this.timer);
|
|
72
|
+
this.timer = null;
|
|
73
|
+
}
|
|
74
|
+
for (const tailer of this.tailers) {
|
|
75
|
+
tailer.close();
|
|
76
|
+
}
|
|
77
|
+
this.tailers = [];
|
|
78
|
+
}
|
|
79
|
+
handleLine(line) {
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
const level = classifyLine(line, this.errorRegex, this.warningRegex);
|
|
82
|
+
// Ring buffer holds ALL lines (matched ones flagged) for the log TV.
|
|
83
|
+
pushRecent(this.recentLines, line, level);
|
|
84
|
+
if (level === 'error') {
|
|
85
|
+
this.entries.push({ timestamp: now, type: 'error', message: line });
|
|
86
|
+
this.onErrorBurst(line);
|
|
87
|
+
}
|
|
88
|
+
else if (level === 'warning') {
|
|
89
|
+
this.entries.push({ timestamp: now, type: 'warning', message: line });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** A fresh error triggers an immediate push, debounced to at most once / 5s. */
|
|
93
|
+
onErrorBurst(line) {
|
|
94
|
+
const now = Date.now();
|
|
95
|
+
if (!shouldEmitBurst(this.lastBurstPush, now))
|
|
96
|
+
return;
|
|
97
|
+
this.lastBurstPush = now;
|
|
98
|
+
void this.pushReport(truncate(line, 200), 'error');
|
|
99
|
+
}
|
|
100
|
+
pruneWindow() {
|
|
101
|
+
const cutoff = Date.now() - this.windowMs;
|
|
102
|
+
this.entries = this.entries.filter((e) => e.timestamp >= cutoff);
|
|
103
|
+
}
|
|
104
|
+
async report() {
|
|
105
|
+
if (this.state !== 'running')
|
|
106
|
+
return;
|
|
107
|
+
await this.pushReport();
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Build and push the current status. If `overrideMessage` is set (burst push),
|
|
111
|
+
* it drives the bubble and the status floors at warning.
|
|
112
|
+
*/
|
|
113
|
+
async pushReport(overrideMessage, subStatus) {
|
|
114
|
+
if (this.state !== 'running')
|
|
115
|
+
return;
|
|
116
|
+
this.pruneWindow();
|
|
117
|
+
const errorCount = this.entries.filter((e) => e.type === 'error').length;
|
|
118
|
+
const warningCount = this.entries.filter((e) => e.type === 'warning').length;
|
|
119
|
+
const windowSeconds = this.windowMs / 1000;
|
|
120
|
+
let status = evaluateThreshold(errorCount, {
|
|
121
|
+
warning: this.errorWarn,
|
|
122
|
+
critical: this.errorCrit,
|
|
123
|
+
direction: 'above',
|
|
124
|
+
});
|
|
125
|
+
if (overrideMessage)
|
|
126
|
+
status = worstStatus([status, 'warning']);
|
|
127
|
+
const mood = status === 'critical' ? 'stressed' : status === 'warning' ? 'concerned' : 'happy';
|
|
128
|
+
const lastError = this.entries.filter((e) => e.type === 'error').at(-1);
|
|
129
|
+
const message = overrideMessage ??
|
|
130
|
+
(errorCount > 0
|
|
131
|
+
? `${errorCount} error${errorCount !== 1 ? 's' : ''} in last ${windowSeconds / 60}min across ${this.files.length} file${this.files.length !== 1 ? 's' : ''}`
|
|
132
|
+
: `No errors in last ${windowSeconds / 60}min`);
|
|
133
|
+
const metrics = {
|
|
134
|
+
status,
|
|
135
|
+
mood,
|
|
136
|
+
message,
|
|
137
|
+
subStatus,
|
|
138
|
+
screenData: buildLogScreen(this.recentLines),
|
|
139
|
+
metrics: {
|
|
140
|
+
error_count: errorCount,
|
|
141
|
+
warning_count: warningCount,
|
|
142
|
+
window_seconds: windowSeconds,
|
|
143
|
+
files_watched: this.files.length,
|
|
144
|
+
last_error_message: lastError?.message ?? '',
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
try {
|
|
148
|
+
await this.pushMetrics(metrics);
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
this.logger.error('Failed to push metrics', {
|
|
152
|
+
error: err instanceof Error ? err.message : String(err),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface FileTailer {
|
|
2
|
+
close(): void;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Tail a file by watching for changes and reading new lines from the last
|
|
6
|
+
* known byte offset. Handles file rotation (size shrinks -> reset to 0).
|
|
7
|
+
*/
|
|
8
|
+
export declare function createTailer(filePath: string, onLine: (line: string) => void): FileTailer;
|
|
9
|
+
//# sourceMappingURL=tail.d.ts.map
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { watch, createReadStream, promises as fsp } from 'node:fs';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
/**
|
|
4
|
+
* Tail a file by watching for changes and reading new lines from the last
|
|
5
|
+
* known byte offset. Handles file rotation (size shrinks -> reset to 0).
|
|
6
|
+
*/
|
|
7
|
+
export function createTailer(filePath, onLine) {
|
|
8
|
+
let offset = 0;
|
|
9
|
+
let reading = false;
|
|
10
|
+
let watcher = null;
|
|
11
|
+
let closed = false;
|
|
12
|
+
async function init() {
|
|
13
|
+
// Start at the end of the file so we only read new lines
|
|
14
|
+
try {
|
|
15
|
+
const stats = await fsp.stat(filePath);
|
|
16
|
+
offset = stats.size;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// File may not exist yet; start at 0
|
|
20
|
+
offset = 0;
|
|
21
|
+
}
|
|
22
|
+
watcher = watch(filePath, () => {
|
|
23
|
+
if (!closed) {
|
|
24
|
+
void readNewLines();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
// Ignore watcher errors (e.g. file deleted then recreated)
|
|
28
|
+
watcher.on('error', () => { });
|
|
29
|
+
}
|
|
30
|
+
async function readNewLines() {
|
|
31
|
+
if (reading || closed)
|
|
32
|
+
return;
|
|
33
|
+
reading = true;
|
|
34
|
+
try {
|
|
35
|
+
const stats = await fsp.stat(filePath);
|
|
36
|
+
// File rotation detected: size smaller than our offset
|
|
37
|
+
if (stats.size < offset) {
|
|
38
|
+
offset = 0;
|
|
39
|
+
}
|
|
40
|
+
// Nothing new to read
|
|
41
|
+
if (stats.size === offset) {
|
|
42
|
+
reading = false;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const stream = createReadStream(filePath, {
|
|
46
|
+
start: offset,
|
|
47
|
+
encoding: 'utf-8',
|
|
48
|
+
});
|
|
49
|
+
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
50
|
+
let bytesRead = 0;
|
|
51
|
+
for await (const line of rl) {
|
|
52
|
+
if (closed)
|
|
53
|
+
break;
|
|
54
|
+
// Account for line content + newline character
|
|
55
|
+
bytesRead += Buffer.byteLength(line, 'utf-8') + 1;
|
|
56
|
+
onLine(line);
|
|
57
|
+
}
|
|
58
|
+
offset += bytesRead;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// File may have been removed; ignore and wait for next change
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
reading = false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
void init();
|
|
68
|
+
return {
|
|
69
|
+
close() {
|
|
70
|
+
closed = true;
|
|
71
|
+
if (watcher) {
|
|
72
|
+
watcher.close();
|
|
73
|
+
watcher = null;
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=tail.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { PollingNPC } from '../../core/polling-npc.js';
|
|
2
|
+
import type { PollingNpcConfig } from '../../core/polling-npc.js';
|
|
3
|
+
import type { CollectedMetrics, Archetype, CompanionDecl } from '../../core/types.js';
|
|
4
|
+
export interface ProcessWatcherConfig extends PollingNpcConfig {
|
|
5
|
+
/** List of process names to watch */
|
|
6
|
+
processes: string[];
|
|
7
|
+
/** CPU usage percent threshold for warning status (default: 80) */
|
|
8
|
+
cpuWarn?: number;
|
|
9
|
+
/** Memory usage MB threshold for warning status (default: 500) */
|
|
10
|
+
memWarn?: number;
|
|
11
|
+
}
|
|
12
|
+
export declare class ProcessWatcherNPC extends PollingNPC {
|
|
13
|
+
private processes;
|
|
14
|
+
private cpuWarn;
|
|
15
|
+
private memWarn;
|
|
16
|
+
private cpuHistories;
|
|
17
|
+
private previousRunning;
|
|
18
|
+
constructor(config: ProcessWatcherConfig);
|
|
19
|
+
get name(): string;
|
|
20
|
+
protected get defaultDisplayName(): string;
|
|
21
|
+
get archetype(): Archetype;
|
|
22
|
+
get heartbeatSeconds(): number;
|
|
23
|
+
get companion(): CompanionDecl;
|
|
24
|
+
protected collect(): Promise<CollectedMetrics>;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { PollingNPC } from '../../core/polling-npc.js';
|
|
2
|
+
import { worstStatus } from '../../core/thresholds.js';
|
|
3
|
+
import { pushHistory } from '../../core/screen.js';
|
|
4
|
+
import { scanProcesses, detectTransitions } from './process-scanner.js';
|
|
5
|
+
import { DEFAULTS } from './defaults.js';
|
|
6
|
+
/** Max per-process panels on the companion dashboard. */
|
|
7
|
+
const MAX_PANELS = 8;
|
|
8
|
+
export class ProcessWatcherNPC extends PollingNPC {
|
|
9
|
+
processes;
|
|
10
|
+
cpuWarn;
|
|
11
|
+
memWarn;
|
|
12
|
+
cpuHistories = new Map();
|
|
13
|
+
previousRunning = null;
|
|
14
|
+
constructor(config) {
|
|
15
|
+
if (process.platform === 'win32') {
|
|
16
|
+
throw new Error('process-watcher is not supported on Windows (relies on the POSIX `ps` command). ' +
|
|
17
|
+
'Run it under WSL/Linux, or use system-monitor instead.');
|
|
18
|
+
}
|
|
19
|
+
super(config);
|
|
20
|
+
this.processes = config.processes;
|
|
21
|
+
this.cpuWarn = config.cpuWarn ?? DEFAULTS.cpuWarn;
|
|
22
|
+
this.memWarn = config.memWarn ?? DEFAULTS.memWarn;
|
|
23
|
+
}
|
|
24
|
+
get name() {
|
|
25
|
+
return 'process-watcher';
|
|
26
|
+
}
|
|
27
|
+
get defaultDisplayName() {
|
|
28
|
+
return 'Processes';
|
|
29
|
+
}
|
|
30
|
+
get archetype() {
|
|
31
|
+
return 'service';
|
|
32
|
+
}
|
|
33
|
+
get heartbeatSeconds() {
|
|
34
|
+
return 30;
|
|
35
|
+
}
|
|
36
|
+
get companion() {
|
|
37
|
+
return {
|
|
38
|
+
screenType: 'dashboard-wall',
|
|
39
|
+
size: { w: 2, h: 2 },
|
|
40
|
+
placement: 'on-desk',
|
|
41
|
+
label: 'Processes',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async collect() {
|
|
45
|
+
const infos = await scanProcesses(this.processes);
|
|
46
|
+
const statuses = [];
|
|
47
|
+
const messageParts = [];
|
|
48
|
+
const metrics = {};
|
|
49
|
+
const panels = [];
|
|
50
|
+
let totalRunning = 0;
|
|
51
|
+
let totalMissing = 0;
|
|
52
|
+
for (const info of infos) {
|
|
53
|
+
const safeKey = info.name.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
54
|
+
if (!info.running) {
|
|
55
|
+
statuses.push('critical');
|
|
56
|
+
messageParts.push(`${info.name}: NOT FOUND`);
|
|
57
|
+
metrics[`${safeKey}_running`] = 0;
|
|
58
|
+
metrics[`${safeKey}_cpu`] = 0;
|
|
59
|
+
metrics[`${safeKey}_mem_mb`] = 0;
|
|
60
|
+
totalMissing++;
|
|
61
|
+
if (panels.length < MAX_PANELS) {
|
|
62
|
+
panels.push({ label: info.name, value: 'DOWN', color: 'red' });
|
|
63
|
+
}
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
totalRunning++;
|
|
67
|
+
const cpu = info.cpuPercent ?? 0;
|
|
68
|
+
metrics[`${safeKey}_running`] = 1;
|
|
69
|
+
metrics[`${safeKey}_cpu`] = cpu;
|
|
70
|
+
metrics[`${safeKey}_mem_mb`] = info.memMb ?? 0;
|
|
71
|
+
const history = this.cpuHistories.get(info.name) ?? [];
|
|
72
|
+
pushHistory(history, cpu);
|
|
73
|
+
this.cpuHistories.set(info.name, history);
|
|
74
|
+
const cpuHigh = cpu >= this.cpuWarn;
|
|
75
|
+
const memHigh = (info.memMb ?? 0) >= this.memWarn;
|
|
76
|
+
if (cpuHigh || memHigh) {
|
|
77
|
+
statuses.push('warning');
|
|
78
|
+
const warnings = [];
|
|
79
|
+
if (cpuHigh)
|
|
80
|
+
warnings.push(`cpu=${cpu}%`);
|
|
81
|
+
if (memHigh)
|
|
82
|
+
warnings.push(`mem=${info.memMb}MB`);
|
|
83
|
+
messageParts.push(`${info.name}: WARN (${warnings.join(', ')})`);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
statuses.push('healthy');
|
|
87
|
+
messageParts.push(`${info.name}: OK`);
|
|
88
|
+
}
|
|
89
|
+
if (panels.length < MAX_PANELS) {
|
|
90
|
+
panels.push({
|
|
91
|
+
label: info.name,
|
|
92
|
+
value: `${cpu}%`,
|
|
93
|
+
color: cpuHigh || memHigh ? 'amber' : 'green',
|
|
94
|
+
sparkline: [...history],
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
metrics['total_watched'] = this.processes.length;
|
|
99
|
+
metrics['total_running'] = totalRunning;
|
|
100
|
+
metrics['total_missing'] = totalMissing;
|
|
101
|
+
// Transition speech: compare the running set to last poll's. On the first
|
|
102
|
+
// poll we only seed the set (no bogus "is back" for already-running procs).
|
|
103
|
+
const currentRunning = new Set(infos.filter((i) => i.running).map((i) => i.name));
|
|
104
|
+
let transitions = [];
|
|
105
|
+
if (this.previousRunning !== null) {
|
|
106
|
+
transitions = detectTransitions(this.previousRunning, currentRunning);
|
|
107
|
+
}
|
|
108
|
+
this.previousRunning = currentRunning;
|
|
109
|
+
const status = worstStatus(statuses);
|
|
110
|
+
const mood = deriveMood(status);
|
|
111
|
+
// A death message takes over the bubble this cycle so it reads as an alert.
|
|
112
|
+
const message = transitions.length > 0 ? transitions.join(' · ') : messageParts.join(' | ');
|
|
113
|
+
const screenData = { layout: 'multi-metric', values: panels };
|
|
114
|
+
return {
|
|
115
|
+
status,
|
|
116
|
+
mood,
|
|
117
|
+
message,
|
|
118
|
+
subStatus: totalMissing > 0 ? 'process-down' : undefined,
|
|
119
|
+
screenData,
|
|
120
|
+
metrics,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function deriveMood(status) {
|
|
125
|
+
switch (status) {
|
|
126
|
+
case 'healthy':
|
|
127
|
+
return 'happy';
|
|
128
|
+
case 'warning':
|
|
129
|
+
return 'concerned';
|
|
130
|
+
case 'critical':
|
|
131
|
+
return 'stressed';
|
|
132
|
+
default:
|
|
133
|
+
return 'idle';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface ProcessInfo {
|
|
2
|
+
name: string;
|
|
3
|
+
running: boolean;
|
|
4
|
+
pid?: number;
|
|
5
|
+
cpuPercent?: number;
|
|
6
|
+
memMb?: number;
|
|
7
|
+
}
|
|
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
|
+
export declare function detectTransitions(prev: Set<string>, curr: Set<string>): string[];
|
|
14
|
+
/**
|
|
15
|
+
* Scan for processes by name and return their status and resource usage.
|
|
16
|
+
* Works on macOS and Linux by using `ps` to list all processes.
|
|
17
|
+
*/
|
|
18
|
+
export declare function scanProcesses(names: string[]): Promise<ProcessInfo[]>;
|
|
19
|
+
//# sourceMappingURL=process-scanner.d.ts.map
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
/**
|
|
5
|
+
* Compare the previous and current running-process sets and return speech
|
|
6
|
+
* strings for any transitions: "<name> died!" when a process disappears and
|
|
7
|
+
* "<name> is back" when one returns.
|
|
8
|
+
*/
|
|
9
|
+
export function detectTransitions(prev, curr) {
|
|
10
|
+
const messages = [];
|
|
11
|
+
for (const name of prev) {
|
|
12
|
+
if (!curr.has(name))
|
|
13
|
+
messages.push(`${name} died!`);
|
|
14
|
+
}
|
|
15
|
+
for (const name of curr) {
|
|
16
|
+
if (!prev.has(name))
|
|
17
|
+
messages.push(`${name} is back`);
|
|
18
|
+
}
|
|
19
|
+
return messages;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Parse the output of `ps` into structured entries.
|
|
23
|
+
* Expects columns: PID, %CPU, RSS (KB), COMMAND
|
|
24
|
+
*/
|
|
25
|
+
function parsePsOutput(stdout) {
|
|
26
|
+
const lines = stdout.trim().split('\n');
|
|
27
|
+
// Skip the header line
|
|
28
|
+
const entries = [];
|
|
29
|
+
for (let i = 1; i < lines.length; i++) {
|
|
30
|
+
const line = lines[i].trim();
|
|
31
|
+
if (!line)
|
|
32
|
+
continue;
|
|
33
|
+
// Fields are whitespace-separated: PID, %CPU, RSS, COMMAND (may contain spaces)
|
|
34
|
+
const parts = line.split(/\s+/);
|
|
35
|
+
if (parts.length < 4)
|
|
36
|
+
continue;
|
|
37
|
+
const pid = parseInt(parts[0], 10);
|
|
38
|
+
const cpu = parseFloat(parts[1]);
|
|
39
|
+
const memKb = parseInt(parts[2], 10);
|
|
40
|
+
// COMMAND is everything from the 4th column onward
|
|
41
|
+
const comm = parts.slice(3).join(' ');
|
|
42
|
+
if (Number.isNaN(pid) || Number.isNaN(cpu) || Number.isNaN(memKb))
|
|
43
|
+
continue;
|
|
44
|
+
entries.push({ pid, cpu, memKb, comm });
|
|
45
|
+
}
|
|
46
|
+
return entries;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Match a process name against a ps COMMAND column.
|
|
50
|
+
* Matches if the basename of the command path equals the search name,
|
|
51
|
+
* or if the command contains the search name.
|
|
52
|
+
*/
|
|
53
|
+
function matchesName(comm, searchName) {
|
|
54
|
+
// Get the basename of the command (strip path and arguments)
|
|
55
|
+
const basename = comm.split('/').pop()?.split(/\s/)[0] ?? '';
|
|
56
|
+
if (basename === searchName)
|
|
57
|
+
return true;
|
|
58
|
+
// Also check if the full command string contains the name
|
|
59
|
+
// (handles cases like "python my-script.py" when searching for "python")
|
|
60
|
+
return comm.toLowerCase().includes(searchName.toLowerCase());
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Scan for processes by name and return their status and resource usage.
|
|
64
|
+
* Works on macOS and Linux by using `ps` to list all processes.
|
|
65
|
+
*/
|
|
66
|
+
export async function scanProcesses(names) {
|
|
67
|
+
let psArgs;
|
|
68
|
+
if (process.platform === 'darwin') {
|
|
69
|
+
// macOS: -e selects all processes, -o specifies output columns
|
|
70
|
+
psArgs = ['-eo', 'pid,pcpu,rss,comm'];
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
// Linux: -eo with similar columns
|
|
74
|
+
psArgs = ['-eo', 'pid,pcpu,rss,comm'];
|
|
75
|
+
}
|
|
76
|
+
let entries;
|
|
77
|
+
try {
|
|
78
|
+
const { stdout } = await execFileAsync('ps', psArgs);
|
|
79
|
+
entries = parsePsOutput(stdout);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// If ps fails, report all processes as unknown / not running
|
|
83
|
+
return names.map((name) => ({ name, running: false }));
|
|
84
|
+
}
|
|
85
|
+
const results = [];
|
|
86
|
+
for (const name of names) {
|
|
87
|
+
// Find all matching entries for this process name
|
|
88
|
+
const matching = entries.filter((e) => matchesName(e.comm, name));
|
|
89
|
+
if (matching.length === 0) {
|
|
90
|
+
results.push({ name, running: false });
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
// Aggregate: take the first matching PID, sum CPU and memory across all instances
|
|
94
|
+
let totalCpu = 0;
|
|
95
|
+
let totalMemKb = 0;
|
|
96
|
+
for (const entry of matching) {
|
|
97
|
+
totalCpu += entry.cpu;
|
|
98
|
+
totalMemKb += entry.memKb;
|
|
99
|
+
}
|
|
100
|
+
results.push({
|
|
101
|
+
name,
|
|
102
|
+
running: true,
|
|
103
|
+
pid: matching[0].pid,
|
|
104
|
+
cpuPercent: Math.round(totalCpu * 100) / 100,
|
|
105
|
+
memMb: Math.round((totalMemKb / 1024) * 100) / 100,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return results;
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=process-scanner.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
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 ReceptionistConfig extends PollingNpcConfig {
|
|
5
|
+
lat?: number;
|
|
6
|
+
lon?: number;
|
|
7
|
+
location?: string;
|
|
8
|
+
/** Display temperature in Fahrenheit instead of Celsius. */
|
|
9
|
+
fahrenheit?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare class ReceptionistNPC extends PollingNPC {
|
|
12
|
+
private explicitCoords;
|
|
13
|
+
private location?;
|
|
14
|
+
private fahrenheit;
|
|
15
|
+
private coords;
|
|
16
|
+
private geoAttempted;
|
|
17
|
+
private weather;
|
|
18
|
+
private lastWeatherFetch;
|
|
19
|
+
constructor(config: ReceptionistConfig);
|
|
20
|
+
get name(): string;
|
|
21
|
+
protected get defaultDisplayName(): string;
|
|
22
|
+
get archetype(): Archetype;
|
|
23
|
+
get behavior(): BehaviorHints;
|
|
24
|
+
get companion(): CompanionDecl;
|
|
25
|
+
protected collect(): Promise<CollectedMetrics>;
|
|
26
|
+
private refreshWeather;
|
|
27
|
+
private resolveCoords;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=index.d.ts.map
|