@nocville/npc 0.2.3 → 0.3.1
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 +36 -15
- package/dist/cjs/cli.js +44 -8
- package/dist/cjs/core/base-npc.js +7 -0
- package/dist/cjs/core/polling-npc.js +8 -0
- package/dist/cjs/index.js +13 -1
- package/dist/cjs/npcs/agent-common/shim.js +8 -2
- package/dist/cjs/npcs/github-watcher/github.js +109 -0
- package/dist/cjs/npcs/office-characters/index.js +102 -0
- package/dist/cjs/npcs/process-watcher/index.js +0 -1
- package/dist/cjs/npcs/process-watcher/process-scanner.js +42 -4
- package/dist/cjs/npcs/receptionist/index.js +6 -119
- package/dist/cjs/npcs/security/index.js +26 -0
- package/dist/cjs/npcs/web-page-watcher/basic.js +58 -0
- package/dist/cjs/npcs/web-page-watcher/capture.js +26 -0
- package/dist/cjs/npcs/web-page-watcher/chrome.js +62 -0
- package/dist/cli.js +44 -8
- package/dist/core/base-npc.js +7 -0
- package/dist/core/polling-npc.d.ts +3 -0
- package/dist/core/polling-npc.js +8 -0
- package/dist/core/types.d.ts +5 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/npcs/agent-common/shim.js +8 -2
- package/dist/npcs/github-watcher/github.d.ts +39 -0
- package/dist/npcs/github-watcher/github.js +103 -0
- package/dist/npcs/office-characters/index.d.ts +55 -0
- package/dist/npcs/office-characters/index.js +91 -0
- package/dist/npcs/process-watcher/index.js +1 -2
- package/dist/npcs/process-watcher/process-scanner.d.ts +3 -1
- package/dist/npcs/process-watcher/process-scanner.js +41 -4
- package/dist/npcs/receptionist/index.d.ts +2 -21
- package/dist/npcs/receptionist/index.js +6 -119
- package/dist/npcs/security/index.d.ts +12 -0
- package/dist/npcs/security/index.js +22 -0
- package/dist/npcs/web-page-watcher/basic.d.ts +16 -0
- package/dist/npcs/web-page-watcher/basic.js +52 -0
- package/dist/npcs/web-page-watcher/capture.js +26 -0
- package/dist/npcs/web-page-watcher/chrome.d.ts +46 -0
- package/dist/npcs/web-page-watcher/chrome.js +57 -0
- package/package.json +2 -2
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { PollingNPC } from '../../core/polling-npc.js';
|
|
2
|
+
import type { PollingNpcConfig } from '../../core/polling-npc.js';
|
|
3
|
+
import type { Archetype, BehaviorHints, CollectedMetrics } from '../../core/types.js';
|
|
4
|
+
/** Quiet heartbeats; the authoritative server drives routines and conversation. */
|
|
5
|
+
declare abstract class OfficeCharacterNPC extends PollingNPC {
|
|
6
|
+
constructor(config: PollingNpcConfig);
|
|
7
|
+
abstract get name(): NonNullable<BehaviorHints['role']>;
|
|
8
|
+
get archetype(): Archetype;
|
|
9
|
+
get behavior(): BehaviorHints;
|
|
10
|
+
protected collect(): Promise<CollectedMetrics>;
|
|
11
|
+
}
|
|
12
|
+
export declare class JanitorNPC extends OfficeCharacterNPC {
|
|
13
|
+
get name(): 'janitor';
|
|
14
|
+
protected get defaultDisplayName(): string;
|
|
15
|
+
}
|
|
16
|
+
export declare class BaristaNPC extends OfficeCharacterNPC {
|
|
17
|
+
get name(): 'barista';
|
|
18
|
+
protected get defaultDisplayName(): string;
|
|
19
|
+
}
|
|
20
|
+
export declare class OfficeCatNPC extends OfficeCharacterNPC {
|
|
21
|
+
get name(): 'office-cat';
|
|
22
|
+
protected get defaultDisplayName(): string;
|
|
23
|
+
}
|
|
24
|
+
export declare class OfficeDogNPC extends OfficeCharacterNPC {
|
|
25
|
+
get name(): 'office-dog';
|
|
26
|
+
protected get defaultDisplayName(): string;
|
|
27
|
+
}
|
|
28
|
+
export declare class PlantCaretakerNPC extends OfficeCharacterNPC {
|
|
29
|
+
get name(): 'plant-caretaker';
|
|
30
|
+
protected get defaultDisplayName(): string;
|
|
31
|
+
}
|
|
32
|
+
export declare class ITTechnicianNPC extends OfficeCharacterNPC {
|
|
33
|
+
get name(): 'it-technician';
|
|
34
|
+
protected get defaultDisplayName(): string;
|
|
35
|
+
}
|
|
36
|
+
export declare class OutOfTouchExecutiveNPC extends OfficeCharacterNPC {
|
|
37
|
+
get name(): 'out-of-touch-executive';
|
|
38
|
+
protected get defaultDisplayName(): string;
|
|
39
|
+
}
|
|
40
|
+
export declare class SlightlyInappropriateHRNPC extends OfficeCharacterNPC {
|
|
41
|
+
get name(): 'slightly-inappropriate-hr';
|
|
42
|
+
protected get defaultDisplayName(): string;
|
|
43
|
+
}
|
|
44
|
+
export declare const OFFICE_CHARACTER_NPCS: {
|
|
45
|
+
readonly janitor: typeof JanitorNPC;
|
|
46
|
+
readonly barista: typeof BaristaNPC;
|
|
47
|
+
readonly 'office-cat': typeof OfficeCatNPC;
|
|
48
|
+
readonly 'office-dog': typeof OfficeDogNPC;
|
|
49
|
+
readonly 'plant-caretaker': typeof PlantCaretakerNPC;
|
|
50
|
+
readonly 'it-technician': typeof ITTechnicianNPC;
|
|
51
|
+
readonly 'out-of-touch-executive': typeof OutOfTouchExecutiveNPC;
|
|
52
|
+
readonly 'slightly-inappropriate-hr': typeof SlightlyInappropriateHRNPC;
|
|
53
|
+
};
|
|
54
|
+
export {};
|
|
55
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { PollingNPC } from '../../core/polling-npc.js';
|
|
2
|
+
/** Quiet heartbeats; the authoritative server drives routines and conversation. */
|
|
3
|
+
class OfficeCharacterNPC extends PollingNPC {
|
|
4
|
+
constructor(config) {
|
|
5
|
+
super({ ...config, pollInterval: config.pollInterval ?? 60_000 });
|
|
6
|
+
}
|
|
7
|
+
get archetype() {
|
|
8
|
+
return 'custom';
|
|
9
|
+
}
|
|
10
|
+
get behavior() {
|
|
11
|
+
return { role: this.name, activity: 'idle', idlePose: 'stand' };
|
|
12
|
+
}
|
|
13
|
+
async collect() {
|
|
14
|
+
return { status: 'healthy', mood: 'happy', message: '', metrics: {} };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class JanitorNPC extends OfficeCharacterNPC {
|
|
18
|
+
get name() {
|
|
19
|
+
return 'janitor';
|
|
20
|
+
}
|
|
21
|
+
get defaultDisplayName() {
|
|
22
|
+
return 'Janitor';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export class BaristaNPC extends OfficeCharacterNPC {
|
|
26
|
+
get name() {
|
|
27
|
+
return 'barista';
|
|
28
|
+
}
|
|
29
|
+
get defaultDisplayName() {
|
|
30
|
+
return 'Barista';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export class OfficeCatNPC extends OfficeCharacterNPC {
|
|
34
|
+
get name() {
|
|
35
|
+
return 'office-cat';
|
|
36
|
+
}
|
|
37
|
+
get defaultDisplayName() {
|
|
38
|
+
return 'Office Cat';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export class OfficeDogNPC extends OfficeCharacterNPC {
|
|
42
|
+
get name() {
|
|
43
|
+
return 'office-dog';
|
|
44
|
+
}
|
|
45
|
+
get defaultDisplayName() {
|
|
46
|
+
return 'Office Dog';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export class PlantCaretakerNPC extends OfficeCharacterNPC {
|
|
50
|
+
get name() {
|
|
51
|
+
return 'plant-caretaker';
|
|
52
|
+
}
|
|
53
|
+
get defaultDisplayName() {
|
|
54
|
+
return 'Plant Caretaker';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export class ITTechnicianNPC extends OfficeCharacterNPC {
|
|
58
|
+
get name() {
|
|
59
|
+
return 'it-technician';
|
|
60
|
+
}
|
|
61
|
+
get defaultDisplayName() {
|
|
62
|
+
return 'IT Technician';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export class OutOfTouchExecutiveNPC extends OfficeCharacterNPC {
|
|
66
|
+
get name() {
|
|
67
|
+
return 'out-of-touch-executive';
|
|
68
|
+
}
|
|
69
|
+
get defaultDisplayName() {
|
|
70
|
+
return 'Out-of-Touch Executive';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export class SlightlyInappropriateHRNPC extends OfficeCharacterNPC {
|
|
74
|
+
get name() {
|
|
75
|
+
return 'slightly-inappropriate-hr';
|
|
76
|
+
}
|
|
77
|
+
get defaultDisplayName() {
|
|
78
|
+
return 'Slightly Inappropriate HR';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export const OFFICE_CHARACTER_NPCS = {
|
|
82
|
+
janitor: JanitorNPC,
|
|
83
|
+
barista: BaristaNPC,
|
|
84
|
+
'office-cat': OfficeCatNPC,
|
|
85
|
+
'office-dog': OfficeDogNPC,
|
|
86
|
+
'plant-caretaker': PlantCaretakerNPC,
|
|
87
|
+
'it-technician': ITTechnicianNPC,
|
|
88
|
+
'out-of-touch-executive': OutOfTouchExecutiveNPC,
|
|
89
|
+
'slightly-inappropriate-hr': SlightlyInappropriateHRNPC,
|
|
90
|
+
};
|
|
91
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { PollingNPC } from '../../core/polling-npc.js';
|
|
2
2
|
import { evaluateThreshold, worstStatus, moodForStatus } from '../../core/thresholds.js';
|
|
3
3
|
import { statusColor, pushHistory } from '../../core/screen.js';
|
|
4
|
-
import { NpcConfigError
|
|
4
|
+
import { NpcConfigError } from '../../core/errors.js';
|
|
5
5
|
import { scanProcesses, detectTransitions } from './process-scanner.js';
|
|
6
6
|
import { DEFAULTS } from './defaults.js';
|
|
7
7
|
/** Max per-process panels on the companion dashboard. */
|
|
@@ -13,7 +13,6 @@ export class ProcessWatcherNPC extends PollingNPC {
|
|
|
13
13
|
cpuHistories = new Map();
|
|
14
14
|
previousRunning = null;
|
|
15
15
|
constructor(config) {
|
|
16
|
-
assertPosix('process-watcher', 'It relies on the POSIX `ps` command; run it under WSL/Linux, or use system-monitor instead.');
|
|
17
16
|
super(config);
|
|
18
17
|
this.processes = [...new Set(config.processes.map((p) => p.trim()).filter(Boolean))];
|
|
19
18
|
if (this.processes.length === 0) {
|
|
@@ -37,11 +37,13 @@ export declare function parsePsOutput(stdout: string): PsEntry[];
|
|
|
37
37
|
export declare function matchesName(comm: string, searchName: string): boolean;
|
|
38
38
|
/** List every process. Throws if `ps` fails — callers must not read that as "all dead". */
|
|
39
39
|
export declare function listProcesses(): Promise<PsEntry[]>;
|
|
40
|
+
/** Windows instance suffixes (#1, #2) distinguish copies of the same executable. */
|
|
41
|
+
export declare function parseWindowsProcesses(stdout: string, cores?: number): PsEntry[];
|
|
40
42
|
/** Summarise each watched name against a process list (CPU and memory summed per name). */
|
|
41
43
|
export declare function matchProcesses(names: string[], entries: PsEntry[]): ProcessInfo[];
|
|
42
44
|
/**
|
|
43
45
|
* Scan for processes by name and return their status and resource usage.
|
|
44
|
-
*
|
|
46
|
+
* Uses `ps` on macOS/Linux and CIM on Windows. Throws when the process list is unavailable.
|
|
45
47
|
*/
|
|
46
48
|
export declare function scanProcesses(names: string[]): Promise<ProcessInfo[]>;
|
|
47
49
|
//# sourceMappingURL=process-scanner.d.ts.map
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
|
+
import { cpus } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
3
5
|
const execFileAsync = promisify(execFile);
|
|
4
6
|
/** Bounds on the `ps` call: a wedged or enormous process table can't stall or flood us. */
|
|
5
7
|
const PS_TIMEOUT_MS = 5000;
|
|
@@ -52,10 +54,10 @@ export function parsePsOutput(stdout) {
|
|
|
52
54
|
* Unlike a substring match, `sh` does not match `bash` or `ssh-agent`.
|
|
53
55
|
*/
|
|
54
56
|
export function matchesName(comm, searchName) {
|
|
55
|
-
if (searchName
|
|
57
|
+
if (/[\\/]/.test(searchName))
|
|
56
58
|
return comm === searchName;
|
|
57
|
-
const base = (comm.split(
|
|
58
|
-
const name = searchName.toLowerCase();
|
|
59
|
+
const base = (comm.split(/[\\/]/).pop() ?? comm).toLowerCase().replace(/\.exe$/, '');
|
|
60
|
+
const name = searchName.toLowerCase().replace(/\.exe$/, '');
|
|
59
61
|
if (base === name)
|
|
60
62
|
return true;
|
|
61
63
|
if (name.length > LINUX_COMM_LEN && base === name.slice(0, LINUX_COMM_LEN))
|
|
@@ -64,6 +66,18 @@ export function matchesName(comm, searchName) {
|
|
|
64
66
|
}
|
|
65
67
|
/** List every process. Throws if `ps` fails — callers must not read that as "all dead". */
|
|
66
68
|
export async function listProcesses() {
|
|
69
|
+
if (process.platform === 'win32') {
|
|
70
|
+
// Fixed command, no user strings interpolated. Performance counters report a
|
|
71
|
+
// sampling interval (Get-Process.CPU is lifetime CPU seconds, not a percent).
|
|
72
|
+
const executable = join(process.env['SystemRoot'] || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
73
|
+
const { stdout } = await execFileAsync(executable, [
|
|
74
|
+
'-NoProfile',
|
|
75
|
+
'-NonInteractive',
|
|
76
|
+
'-Command',
|
|
77
|
+
'$ErrorActionPreference="Stop"; Get-CimInstance Win32_PerfFormattedData_PerfProc_Process | Select-Object IDProcess,Name,PercentProcessorTime,WorkingSetPrivate | ConvertTo-Json -Compress',
|
|
78
|
+
], { timeout: PS_TIMEOUT_MS, maxBuffer: PS_MAX_BUFFER, windowsHide: true });
|
|
79
|
+
return parseWindowsProcesses(stdout, cpus().length);
|
|
80
|
+
}
|
|
67
81
|
const { stdout } = await execFileAsync('ps', ['-eo', 'pid=,pcpu=,rss=,comm='], {
|
|
68
82
|
timeout: PS_TIMEOUT_MS,
|
|
69
83
|
maxBuffer: PS_MAX_BUFFER,
|
|
@@ -72,6 +86,29 @@ export async function listProcesses() {
|
|
|
72
86
|
});
|
|
73
87
|
return parsePsOutput(stdout);
|
|
74
88
|
}
|
|
89
|
+
/** Windows instance suffixes (#1, #2) distinguish copies of the same executable. */
|
|
90
|
+
export function parseWindowsProcesses(stdout, cores = 1) {
|
|
91
|
+
const value = JSON.parse(stdout.replace(/^\uFEFF/, ''));
|
|
92
|
+
const rows = Array.isArray(value) ? value : value ? [value] : [];
|
|
93
|
+
return rows.flatMap((row) => {
|
|
94
|
+
const pid = Number(row.IDProcess);
|
|
95
|
+
const cpu = Number(row.PercentProcessorTime);
|
|
96
|
+
const bytes = Number(row.WorkingSetPrivate);
|
|
97
|
+
if (!(pid > 0) ||
|
|
98
|
+
!Number.isFinite(cpu) ||
|
|
99
|
+
!Number.isFinite(bytes) ||
|
|
100
|
+
typeof row.Name !== 'string')
|
|
101
|
+
return [];
|
|
102
|
+
return [
|
|
103
|
+
{
|
|
104
|
+
pid,
|
|
105
|
+
cpu: cpu / Math.max(1, cores),
|
|
106
|
+
memKb: bytes / 1024,
|
|
107
|
+
comm: row.Name.replace(/#\d+$/, ''),
|
|
108
|
+
},
|
|
109
|
+
];
|
|
110
|
+
});
|
|
111
|
+
}
|
|
75
112
|
/** Summarise each watched name against a process list (CPU and memory summed per name). */
|
|
76
113
|
export function matchProcesses(names, entries) {
|
|
77
114
|
return names.map((name) => {
|
|
@@ -92,7 +129,7 @@ export function matchProcesses(names, entries) {
|
|
|
92
129
|
}
|
|
93
130
|
/**
|
|
94
131
|
* Scan for processes by name and return their status and resource usage.
|
|
95
|
-
*
|
|
132
|
+
* Uses `ps` on macOS/Linux and CIM on Windows. Throws when the process list is unavailable.
|
|
96
133
|
*/
|
|
97
134
|
export async function scanProcesses(names) {
|
|
98
135
|
return matchProcesses(names, await listProcesses());
|
|
@@ -1,39 +1,20 @@
|
|
|
1
1
|
import { PollingNPC } from '../../core/polling-npc.js';
|
|
2
2
|
import type { PollingNpcConfig } from '../../core/polling-npc.js';
|
|
3
|
-
import type { CollectedMetrics, Archetype, BehaviorHints
|
|
3
|
+
import type { CollectedMetrics, Archetype, BehaviorHints } from '../../core/types.js';
|
|
4
4
|
export interface ReceptionistConfig extends PollingNpcConfig {
|
|
5
|
+
/** @deprecated Reception no longer uses weather or geolocation. Accepted for compatibility. */
|
|
5
6
|
lat?: number;
|
|
6
7
|
lon?: number;
|
|
7
8
|
location?: string;
|
|
8
|
-
/** Display temperature in Fahrenheit instead of Celsius. */
|
|
9
9
|
fahrenheit?: boolean;
|
|
10
|
-
/**
|
|
11
|
-
* Fall back to IP geolocation (ipapi.co) when no --lat/--lon or --location
|
|
12
|
-
* is given. Default true; false keeps the receptionist in time-only mode
|
|
13
|
-
* rather than sending this machine's IP to a third party.
|
|
14
|
-
*/
|
|
15
10
|
ipGeo?: boolean;
|
|
16
11
|
}
|
|
17
12
|
export declare class ReceptionistNPC extends PollingNPC {
|
|
18
|
-
private explicitCoords;
|
|
19
|
-
private location?;
|
|
20
|
-
private fahrenheit;
|
|
21
|
-
private ipGeo;
|
|
22
|
-
private coords;
|
|
23
|
-
/** Earliest time to retry geocoding / IP geo after a failure. */
|
|
24
|
-
private nextGeoAttempt;
|
|
25
|
-
private weather;
|
|
26
|
-
private weatherFetchedAt;
|
|
27
|
-
/** Earliest time to retry the weather after a failure. */
|
|
28
|
-
private nextWeatherAttempt;
|
|
29
13
|
constructor(config: ReceptionistConfig);
|
|
30
14
|
get name(): string;
|
|
31
15
|
protected get defaultDisplayName(): string;
|
|
32
16
|
get archetype(): Archetype;
|
|
33
17
|
get behavior(): BehaviorHints;
|
|
34
|
-
get companion(): CompanionDecl;
|
|
35
18
|
protected collect(): Promise<CollectedMetrics>;
|
|
36
|
-
private refreshWeather;
|
|
37
|
-
private resolveCoords;
|
|
38
19
|
}
|
|
39
20
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,34 +1,7 @@
|
|
|
1
1
|
import { PollingNPC } from '../../core/polling-npc.js';
|
|
2
|
-
import { NpcConfigError } from '../../core/errors.js';
|
|
3
|
-
import { DEFAULTS } from './defaults.js';
|
|
4
|
-
import { fetchWeather, geocodeLocation, autoGeo, formatClock, toFahrenheit, validCoords, } from './weather.js';
|
|
5
2
|
export class ReceptionistNPC extends PollingNPC {
|
|
6
|
-
explicitCoords;
|
|
7
|
-
location;
|
|
8
|
-
fahrenheit;
|
|
9
|
-
ipGeo;
|
|
10
|
-
coords = null;
|
|
11
|
-
/** Earliest time to retry geocoding / IP geo after a failure. */
|
|
12
|
-
nextGeoAttempt = 0;
|
|
13
|
-
weather = null;
|
|
14
|
-
weatherFetchedAt = 0;
|
|
15
|
-
/** Earliest time to retry the weather after a failure. */
|
|
16
|
-
nextWeatherAttempt = 0;
|
|
17
3
|
constructor(config) {
|
|
18
|
-
super({ ...config, pollInterval: config.pollInterval ??
|
|
19
|
-
if ((config.lat === undefined) !== (config.lon === undefined)) {
|
|
20
|
-
throw new NpcConfigError('--lat and --lon must be given together');
|
|
21
|
-
}
|
|
22
|
-
if (config.lat !== undefined && !validCoords(config.lat, config.lon)) {
|
|
23
|
-
throw new NpcConfigError('--lat must be within ±90 and --lon within ±180');
|
|
24
|
-
}
|
|
25
|
-
this.explicitCoords =
|
|
26
|
-
config.lat !== undefined && config.lon !== undefined
|
|
27
|
-
? { lat: config.lat, lon: config.lon }
|
|
28
|
-
: null;
|
|
29
|
-
this.location = config.location?.trim() || undefined;
|
|
30
|
-
this.fahrenheit = config.fahrenheit ?? false;
|
|
31
|
-
this.ipGeo = config.ipGeo ?? true;
|
|
4
|
+
super({ ...config, pollInterval: config.pollInterval ?? 60_000 });
|
|
32
5
|
}
|
|
33
6
|
get name() {
|
|
34
7
|
return 'receptionist';
|
|
@@ -41,102 +14,16 @@ export class ReceptionistNPC extends PollingNPC {
|
|
|
41
14
|
}
|
|
42
15
|
get behavior() {
|
|
43
16
|
return {
|
|
17
|
+
role: 'receptionist',
|
|
18
|
+
activity: 'idle',
|
|
44
19
|
greetPlayers: true,
|
|
45
|
-
patrol: { everyMinutes: [15, 30], stops: 5 },
|
|
46
20
|
idle: 'stay',
|
|
47
|
-
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
get companion() {
|
|
51
|
-
return {
|
|
52
|
-
screenType: 'welcome-sign',
|
|
53
|
-
size: { w: 2, h: 1 },
|
|
54
|
-
placement: 'adjacent-wall',
|
|
55
|
-
label: 'Welcome',
|
|
21
|
+
idlePose: 'sit',
|
|
56
22
|
};
|
|
57
23
|
}
|
|
58
24
|
async collect() {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const { day, date, time } = formatClock(now);
|
|
62
|
-
const values = [
|
|
63
|
-
{ label: 'time', value: time },
|
|
64
|
-
{ label: 'day', value: day },
|
|
65
|
-
{ label: 'date', value: date },
|
|
66
|
-
];
|
|
67
|
-
let message = `${day} ${time}`;
|
|
68
|
-
const metrics = { weather_ok: this.weather ? 1 : 0 };
|
|
69
|
-
if (this.weather) {
|
|
70
|
-
const temp = this.fahrenheit
|
|
71
|
-
? `${toFahrenheit(this.weather.tempC)}°F`
|
|
72
|
-
: `${this.weather.tempC}°C`;
|
|
73
|
-
message += ` • ${temp} ${this.weather.text}`;
|
|
74
|
-
values.push({ label: 'temp', value: temp }, { label: 'condition', value: this.weather.text }, { label: 'icon', value: this.weather.icon });
|
|
75
|
-
metrics['temp_c'] = this.weather.tempC;
|
|
76
|
-
metrics['weather_code'] = this.weather.code;
|
|
77
|
-
metrics['wind_kph'] = this.weather.windKph;
|
|
78
|
-
}
|
|
79
|
-
// The receptionist is a greeter/clock — it stays healthy even when weather
|
|
80
|
-
// is unavailable (degraded time-only mode).
|
|
81
|
-
return {
|
|
82
|
-
status: 'healthy',
|
|
83
|
-
mood: 'happy',
|
|
84
|
-
message,
|
|
85
|
-
screenData: { layout: 'welcome-sign', values },
|
|
86
|
-
metrics,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
async refreshWeather() {
|
|
90
|
-
const now = Date.now();
|
|
91
|
-
if (!this.coords)
|
|
92
|
-
await this.resolveCoords(now);
|
|
93
|
-
// Don't show a reading that has gone stale through repeated failures.
|
|
94
|
-
if (this.weather && now - this.weatherFetchedAt > DEFAULTS.weatherMaxAgeMs) {
|
|
95
|
-
this.weather = null;
|
|
96
|
-
}
|
|
97
|
-
if (!this.coords || now < this.nextWeatherAttempt)
|
|
98
|
-
return;
|
|
99
|
-
if (this.weather && now - this.weatherFetchedAt < DEFAULTS.weatherRefreshMs)
|
|
100
|
-
return;
|
|
101
|
-
const w = await fetchWeather(this.coords, DEFAULTS.fetchTimeoutMs);
|
|
102
|
-
if (w) {
|
|
103
|
-
this.weather = w;
|
|
104
|
-
this.weatherFetchedAt = now;
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
this.nextWeatherAttempt = now + DEFAULTS.retryBackoffMs;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
async resolveCoords(now) {
|
|
111
|
-
if (this.explicitCoords) {
|
|
112
|
-
this.coords = this.explicitCoords;
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
// Back off after a failure rather than hitting the keyless endpoints every tick.
|
|
116
|
-
if (now < this.nextGeoAttempt)
|
|
117
|
-
return;
|
|
118
|
-
this.nextGeoAttempt = now + DEFAULTS.retryBackoffMs;
|
|
119
|
-
if (this.location) {
|
|
120
|
-
this.coords = await geocodeLocation(this.location, DEFAULTS.fetchTimeoutMs);
|
|
121
|
-
if (this.coords)
|
|
122
|
-
return;
|
|
123
|
-
this.logger.warn(`Could not geocode "${this.location}"` + (this.ipGeo ? ', falling back to IP geo' : ''));
|
|
124
|
-
}
|
|
125
|
-
if (!this.ipGeo) {
|
|
126
|
-
if (!this.location) {
|
|
127
|
-
this.nextGeoAttempt = Infinity; // nothing to resolve, ever
|
|
128
|
-
this.logger.info('No location set and IP geolocation disabled: time-only mode');
|
|
129
|
-
}
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
this.coords = await autoGeo(DEFAULTS.fetchTimeoutMs);
|
|
133
|
-
if (this.coords) {
|
|
134
|
-
this.logger.info("Weather location estimated from this machine's IP via ipapi.co " +
|
|
135
|
-
'(set --location or --lat/--lon, or pass --no-ip-geo to disable)');
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
this.logger.warn('Weather geo unavailable — running in time-only mode for now');
|
|
139
|
-
}
|
|
25
|
+
// The server handles conversation and movement. Heartbeats stay quiet.
|
|
26
|
+
return { status: 'healthy', mood: 'happy', message: '', metrics: {} };
|
|
140
27
|
}
|
|
141
28
|
}
|
|
142
29
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { PollingNPC } from '../../core/polling-npc.js';
|
|
2
|
+
import type { PollingNpcConfig } from '../../core/polling-npc.js';
|
|
3
|
+
import type { CollectedMetrics, Archetype, BehaviorHints } from '../../core/types.js';
|
|
4
|
+
export declare class SecurityNPC extends PollingNPC {
|
|
5
|
+
constructor(config: PollingNpcConfig);
|
|
6
|
+
get name(): string;
|
|
7
|
+
protected get defaultDisplayName(): string;
|
|
8
|
+
get archetype(): Archetype;
|
|
9
|
+
get behavior(): BehaviorHints;
|
|
10
|
+
protected collect(): Promise<CollectedMetrics>;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { PollingNPC } from '../../core/polling-npc.js';
|
|
2
|
+
export class SecurityNPC extends PollingNPC {
|
|
3
|
+
constructor(config) {
|
|
4
|
+
super({ ...config, pollInterval: config.pollInterval ?? 60_000 });
|
|
5
|
+
}
|
|
6
|
+
get name() {
|
|
7
|
+
return 'security';
|
|
8
|
+
}
|
|
9
|
+
get defaultDisplayName() {
|
|
10
|
+
return 'Security';
|
|
11
|
+
}
|
|
12
|
+
get archetype() {
|
|
13
|
+
return 'custom';
|
|
14
|
+
}
|
|
15
|
+
get behavior() {
|
|
16
|
+
return { role: 'security', activity: 'idle', idlePose: 'stand' };
|
|
17
|
+
}
|
|
18
|
+
async collect() {
|
|
19
|
+
return { status: 'healthy', mood: 'happy', message: '', metrics: {} };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Status } from '../../core/types.js';
|
|
2
|
+
export interface BasicCheck {
|
|
3
|
+
status: number;
|
|
4
|
+
title: string;
|
|
5
|
+
ok: boolean;
|
|
6
|
+
timedOut: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** Extract the <title> text from an HTML string (empty if none). */
|
|
9
|
+
export declare function extractTitle(html: string): string;
|
|
10
|
+
/** Map an HTTP status to NPC status: 2xx/3xx ok, 4xx/timeout warn, 5xx/refused crit. */
|
|
11
|
+
export declare function statusFromHttp(code: number, timedOut?: boolean): Status;
|
|
12
|
+
/** Fetch a page and extract HTTP status + <title> (basic, no browser). */
|
|
13
|
+
export declare function basicCheck(url: string, timeoutMs?: number): Promise<BasicCheck>;
|
|
14
|
+
/** Best-effort hostname for a URL, falling back to the raw string. */
|
|
15
|
+
export declare function hostnameOf(url: string): string;
|
|
16
|
+
//# sourceMappingURL=basic.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Extract the <title> text from an HTML string (empty if none). */
|
|
2
|
+
export function extractTitle(html) {
|
|
3
|
+
const m = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
4
|
+
return m ? m[1].replace(/\s+/g, ' ').trim().slice(0, 80) : '';
|
|
5
|
+
}
|
|
6
|
+
/** Map an HTTP status to NPC status: 2xx/3xx ok, 4xx/timeout warn, 5xx/refused crit. */
|
|
7
|
+
export function statusFromHttp(code, timedOut = false) {
|
|
8
|
+
if (code >= 200 && code < 400)
|
|
9
|
+
return 'healthy';
|
|
10
|
+
if (code >= 400 && code < 500)
|
|
11
|
+
return 'warning';
|
|
12
|
+
if (code >= 500)
|
|
13
|
+
return 'critical';
|
|
14
|
+
return timedOut ? 'warning' : 'critical'; // 0 = network error
|
|
15
|
+
}
|
|
16
|
+
/** Fetch a page and extract HTTP status + <title> (basic, no browser). */
|
|
17
|
+
export async function basicCheck(url, timeoutMs = 8000) {
|
|
18
|
+
const controller = new AbortController();
|
|
19
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
20
|
+
try {
|
|
21
|
+
const res = await fetch(url, {
|
|
22
|
+
signal: controller.signal,
|
|
23
|
+
headers: { 'User-Agent': 'nocville-npc' },
|
|
24
|
+
redirect: 'follow',
|
|
25
|
+
});
|
|
26
|
+
let title = '';
|
|
27
|
+
try {
|
|
28
|
+
title = extractTitle(await res.text());
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// body unavailable; leave title blank
|
|
32
|
+
}
|
|
33
|
+
return { status: res.status, title, ok: res.ok, timedOut: false };
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
const timedOut = err instanceof Error && err.name === 'AbortError';
|
|
37
|
+
return { status: 0, title: '', ok: false, timedOut };
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Best-effort hostname for a URL, falling back to the raw string. */
|
|
44
|
+
export function hostnameOf(url) {
|
|
45
|
+
try {
|
|
46
|
+
return new URL(url).hostname;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return url.slice(0, 40);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=basic.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
1
2
|
import { existsSync } from 'node:fs';
|
|
2
3
|
import { execFileSync } from 'node:child_process';
|
|
3
4
|
const MAC_PATHS = [
|
|
@@ -43,6 +44,25 @@ export function findChrome(explicit) {
|
|
|
43
44
|
return p;
|
|
44
45
|
return null;
|
|
45
46
|
}
|
|
47
|
+
if (process.platform === 'win32') {
|
|
48
|
+
const roots = [
|
|
49
|
+
process.env['LOCALAPPDATA'],
|
|
50
|
+
process.env['PROGRAMFILES'],
|
|
51
|
+
process.env['PROGRAMFILES(X86)'],
|
|
52
|
+
].filter((p) => !!p);
|
|
53
|
+
for (const root of roots)
|
|
54
|
+
for (const relative of [
|
|
55
|
+
['Google', 'Chrome', 'Application', 'chrome.exe'],
|
|
56
|
+
['Microsoft', 'Edge', 'Application', 'msedge.exe'],
|
|
57
|
+
['Chromium', 'Application', 'chrome.exe'],
|
|
58
|
+
['BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'],
|
|
59
|
+
]) {
|
|
60
|
+
const path = join(root, ...relative);
|
|
61
|
+
if (existsSync(path))
|
|
62
|
+
return path;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
46
66
|
if (process.platform === 'linux') {
|
|
47
67
|
for (const bin of LINUX_BINARIES) {
|
|
48
68
|
const resolved = which(bin);
|
|
@@ -76,6 +96,12 @@ export function runningAsRoot() {
|
|
|
76
96
|
*/
|
|
77
97
|
export async function loadPuppeteer() {
|
|
78
98
|
try {
|
|
99
|
+
// Prefer CommonJS in bundled runtimes so Electron resolves through its
|
|
100
|
+
// ASAR loader. Native ESM installations use the optional dynamic import below.
|
|
101
|
+
if (typeof require === 'function') {
|
|
102
|
+
const mod = require('puppeteer-core');
|
|
103
|
+
return mod.default ?? mod;
|
|
104
|
+
}
|
|
79
105
|
const dynImport = new Function('m', 'return import(m)');
|
|
80
106
|
const mod = await dynImport('puppeteer-core');
|
|
81
107
|
return (mod.default ?? mod);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface PuppeteerViewport {
|
|
2
|
+
width: number;
|
|
3
|
+
height: number;
|
|
4
|
+
deviceScaleFactor?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface PuppeteerResponse {
|
|
7
|
+
status(): number;
|
|
8
|
+
}
|
|
9
|
+
export interface PuppeteerPage {
|
|
10
|
+
setViewport(v: PuppeteerViewport): Promise<void>;
|
|
11
|
+
goto(url: string, opts?: {
|
|
12
|
+
waitUntil?: string;
|
|
13
|
+
timeout?: number;
|
|
14
|
+
}): Promise<PuppeteerResponse | null>;
|
|
15
|
+
screenshot(opts: {
|
|
16
|
+
type?: string;
|
|
17
|
+
fullPage?: boolean;
|
|
18
|
+
}): Promise<Buffer | Uint8Array>;
|
|
19
|
+
close(): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
export interface PuppeteerBrowser {
|
|
22
|
+
newPage(): Promise<PuppeteerPage>;
|
|
23
|
+
close(): Promise<void>;
|
|
24
|
+
/** puppeteer >= 20 exposes `connected`; older versions expose isConnected(). */
|
|
25
|
+
connected?: boolean;
|
|
26
|
+
isConnected?(): boolean;
|
|
27
|
+
on(event: string, cb: () => void): void;
|
|
28
|
+
}
|
|
29
|
+
export interface PuppeteerModule {
|
|
30
|
+
launch(opts: {
|
|
31
|
+
executablePath?: string;
|
|
32
|
+
headless?: boolean | 'new';
|
|
33
|
+
args?: string[];
|
|
34
|
+
}): Promise<PuppeteerBrowser>;
|
|
35
|
+
}
|
|
36
|
+
/** Find a usable Chrome/Chromium executable: explicit path → env → known paths. */
|
|
37
|
+
export declare function findChrome(explicit?: string): string | null;
|
|
38
|
+
/**
|
|
39
|
+
* Load the optional `puppeteer-core` peer dependency without a static import,
|
|
40
|
+
* so the package builds and runs when it isn't installed. Returns null if the
|
|
41
|
+
* module is absent.
|
|
42
|
+
*/
|
|
43
|
+
export declare function loadPuppeteer(): Promise<PuppeteerModule | null>;
|
|
44
|
+
/** True if the browser reports a live connection. */
|
|
45
|
+
export declare function browserConnected(browser: PuppeteerBrowser): boolean;
|
|
46
|
+
//# sourceMappingURL=chrome.d.ts.map
|