@nocville/npc 0.0.0-stage → 0.2.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 +268 -2
- package/dist/cjs/cli.js +584 -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/session-filter.js +150 -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 +95 -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 +412 -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 +361 -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 +549 -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/session-filter.d.ts +59 -0
- package/dist/core/session-filter.js +143 -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 +41 -0
- package/dist/index.js +26 -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 +192 -0
- package/dist/npcs/agent-common/session.js +390 -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 +65 -0
- package/dist/npcs/codex/watcher.js +356 -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,230 @@
|
|
|
1
|
+
import { PollingNPC } from '../../core/polling-npc.js';
|
|
2
|
+
import { DEFAULTS } from './defaults.js';
|
|
3
|
+
import { pngToPixelImage } from './pixelate.js';
|
|
4
|
+
import { findChrome, loadPuppeteer, browserConnected } from './capture.js';
|
|
5
|
+
import { basicCheck, statusFromHttp, hostnameOf } from './basic-mode.js';
|
|
6
|
+
export class WebPageWatcherNPC extends PollingNPC {
|
|
7
|
+
url;
|
|
8
|
+
mode;
|
|
9
|
+
chromePath;
|
|
10
|
+
viewportW;
|
|
11
|
+
viewportH;
|
|
12
|
+
screenW;
|
|
13
|
+
screenH;
|
|
14
|
+
waitUntil;
|
|
15
|
+
puppeteer = null;
|
|
16
|
+
browser = null;
|
|
17
|
+
browserUnavailable = false;
|
|
18
|
+
chromeHintShown = false;
|
|
19
|
+
lastHash = 0;
|
|
20
|
+
changedPollsLeft = 0;
|
|
21
|
+
constructor(config) {
|
|
22
|
+
super({
|
|
23
|
+
...config,
|
|
24
|
+
// Floor the poll interval — screenshots are expensive.
|
|
25
|
+
pollInterval: Math.max(config.pollInterval ?? DEFAULTS.pollInterval, DEFAULTS.minPollInterval),
|
|
26
|
+
});
|
|
27
|
+
this.url = config.url;
|
|
28
|
+
this.mode = config.mode ?? 'auto';
|
|
29
|
+
this.chromePath = config.chromePath;
|
|
30
|
+
this.screenW = config.screenWidth ?? DEFAULTS.screenW;
|
|
31
|
+
this.screenH = config.screenHeight ?? DEFAULTS.screenH;
|
|
32
|
+
// Derive the viewport from the screen size for native downscaling unless the
|
|
33
|
+
// user overrides it (in which case pngToPixelImage box-filters to screen).
|
|
34
|
+
this.viewportW = config.viewportWidth ?? this.screenW * DEFAULTS.scaleDivisor;
|
|
35
|
+
this.viewportH = config.viewportHeight ?? this.screenH * DEFAULTS.scaleDivisor;
|
|
36
|
+
this.waitUntil = config.waitUntil ?? DEFAULTS.waitUntil;
|
|
37
|
+
}
|
|
38
|
+
get name() {
|
|
39
|
+
return 'web-page-watcher';
|
|
40
|
+
}
|
|
41
|
+
get defaultDisplayName() {
|
|
42
|
+
return hostnameOf(this.url);
|
|
43
|
+
}
|
|
44
|
+
get archetype() {
|
|
45
|
+
return 'custom';
|
|
46
|
+
}
|
|
47
|
+
get companion() {
|
|
48
|
+
return {
|
|
49
|
+
screenType: 'web-view',
|
|
50
|
+
size: { w: 4, h: 3 },
|
|
51
|
+
placement: 'adjacent-wall',
|
|
52
|
+
label: hostnameOf(this.url),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async collect() {
|
|
56
|
+
if (this.mode === 'basic')
|
|
57
|
+
return this.collectBasic();
|
|
58
|
+
const browser = await this.ensureBrowser();
|
|
59
|
+
if (!browser) {
|
|
60
|
+
// 'auto' silently degrades; explicit 'screenshot' warns once.
|
|
61
|
+
this.maybeShowChromeHint();
|
|
62
|
+
return this.collectBasic();
|
|
63
|
+
}
|
|
64
|
+
return this.collectScreenshot(browser);
|
|
65
|
+
}
|
|
66
|
+
async onStop() {
|
|
67
|
+
await super.onStop();
|
|
68
|
+
if (this.browser) {
|
|
69
|
+
try {
|
|
70
|
+
await this.browser.close();
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// best effort
|
|
74
|
+
}
|
|
75
|
+
this.browser = null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// --- Screenshot mode ---
|
|
79
|
+
async collectScreenshot(browser) {
|
|
80
|
+
const page = await browser.newPage();
|
|
81
|
+
try {
|
|
82
|
+
// deviceScaleFactor 0.125 makes Chrome rasterize the viewport down to the
|
|
83
|
+
// screen size natively (e.g. 1280×800 → 160×100); pngToPixelImage
|
|
84
|
+
// box-filters as a fallback if it doesn't.
|
|
85
|
+
await page.setViewport({
|
|
86
|
+
width: this.viewportW,
|
|
87
|
+
height: this.viewportH,
|
|
88
|
+
deviceScaleFactor: 0.125,
|
|
89
|
+
});
|
|
90
|
+
let httpStatus = 0;
|
|
91
|
+
let timedOut = false;
|
|
92
|
+
const loadStart = Date.now();
|
|
93
|
+
try {
|
|
94
|
+
const resp = await page.goto(this.url, {
|
|
95
|
+
waitUntil: this.waitUntil,
|
|
96
|
+
timeout: DEFAULTS.navTimeoutMs,
|
|
97
|
+
});
|
|
98
|
+
httpStatus = resp ? resp.status() : 0;
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
timedOut = err instanceof Error && /timeout/i.test(err.message);
|
|
102
|
+
}
|
|
103
|
+
const loadMs = Date.now() - loadStart;
|
|
104
|
+
const captureStart = Date.now();
|
|
105
|
+
const shotRaw = await page.screenshot({ type: 'png' });
|
|
106
|
+
const shot = Buffer.isBuffer(shotRaw) ? shotRaw : Buffer.from(shotRaw);
|
|
107
|
+
const { screenData, byteSize, hash } = pngToPixelImage(shot, DEFAULTS.maxColors, this.screenW, this.screenH);
|
|
108
|
+
const captureMs = Date.now() - captureStart;
|
|
109
|
+
const changed = this.lastHash !== 0 && hash !== this.lastHash;
|
|
110
|
+
this.lastHash = hash;
|
|
111
|
+
if (changed)
|
|
112
|
+
this.changedPollsLeft = DEFAULTS.changedPolls;
|
|
113
|
+
else if (this.changedPollsLeft > 0)
|
|
114
|
+
this.changedPollsLeft--;
|
|
115
|
+
const status = statusFromHttp(httpStatus, timedOut);
|
|
116
|
+
const host = hostnameOf(this.url);
|
|
117
|
+
const message = changed
|
|
118
|
+
? 'Page updated!'
|
|
119
|
+
: httpStatus
|
|
120
|
+
? `${host} ${httpStatus < 400 ? 'OK' : 'ERR'} (${httpStatus}, ${loadMs}ms)`
|
|
121
|
+
: `${host} unreachable`;
|
|
122
|
+
return {
|
|
123
|
+
status,
|
|
124
|
+
mood: moodFor(status),
|
|
125
|
+
message,
|
|
126
|
+
subStatus: this.changedPollsLeft > 0 ? 'changed' : undefined,
|
|
127
|
+
screenData,
|
|
128
|
+
metrics: {
|
|
129
|
+
status_code: httpStatus,
|
|
130
|
+
load_ms: loadMs,
|
|
131
|
+
capture_ms: captureMs,
|
|
132
|
+
frame_bytes: byteSize,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
await page.close().catch(() => undefined);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async ensureBrowser() {
|
|
141
|
+
if (this.browserUnavailable)
|
|
142
|
+
return null;
|
|
143
|
+
if (this.browser && browserConnected(this.browser))
|
|
144
|
+
return this.browser;
|
|
145
|
+
if (this.browser)
|
|
146
|
+
this.browser = null; // disconnected — relaunch
|
|
147
|
+
if (!this.puppeteer) {
|
|
148
|
+
this.puppeteer = await loadPuppeteer();
|
|
149
|
+
}
|
|
150
|
+
const chromePath = findChrome(this.chromePath);
|
|
151
|
+
if (!this.puppeteer || !chromePath) {
|
|
152
|
+
this.browserUnavailable = true;
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
this.browser = await this.puppeteer.launch({
|
|
157
|
+
executablePath: chromePath,
|
|
158
|
+
headless: true,
|
|
159
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu', '--hide-scrollbars'],
|
|
160
|
+
});
|
|
161
|
+
this.browser.on('disconnected', () => {
|
|
162
|
+
this.browser = null;
|
|
163
|
+
});
|
|
164
|
+
return this.browser;
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
this.logger.warn('Failed to launch Chrome', {
|
|
168
|
+
error: err instanceof Error ? err.message : String(err),
|
|
169
|
+
});
|
|
170
|
+
this.browserUnavailable = true;
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
maybeShowChromeHint() {
|
|
175
|
+
if (this.chromeHintShown)
|
|
176
|
+
return;
|
|
177
|
+
this.chromeHintShown = true;
|
|
178
|
+
this.logger.warn('Chrome/puppeteer-core unavailable — falling back to basic mode. ' +
|
|
179
|
+
'Install `puppeteer-core` and Google Chrome (or set --chrome-path/CHROME_PATH) for the pixel-art TV.');
|
|
180
|
+
}
|
|
181
|
+
// --- Basic mode ---
|
|
182
|
+
async collectBasic() {
|
|
183
|
+
const loadStart = Date.now();
|
|
184
|
+
const check = await basicCheck(this.url);
|
|
185
|
+
const loadMs = Date.now() - loadStart;
|
|
186
|
+
// Basic mode can't render the pixel TV, so it never reports fully healthy —
|
|
187
|
+
// a healthy HTTP result is downgraded to a warning to nudge Chrome setup.
|
|
188
|
+
let status = statusFromHttp(check.status, check.timedOut);
|
|
189
|
+
if (status === 'healthy')
|
|
190
|
+
status = 'warning';
|
|
191
|
+
const host = hostnameOf(this.url);
|
|
192
|
+
const message = check.title ? `${host}: ${check.title}` : `${host} (${check.status || 'ERR'})`;
|
|
193
|
+
return {
|
|
194
|
+
status,
|
|
195
|
+
mood: moodFor(status),
|
|
196
|
+
message,
|
|
197
|
+
screenData: {
|
|
198
|
+
layout: 'ticker',
|
|
199
|
+
values: [
|
|
200
|
+
{ label: 'URL', value: host },
|
|
201
|
+
{
|
|
202
|
+
label: 'HTTP',
|
|
203
|
+
value: check.status ? String(check.status) : 'ERR',
|
|
204
|
+
color: check.status >= 200 && check.status < 400
|
|
205
|
+
? 'green'
|
|
206
|
+
: check.status >= 500 || check.status === 0
|
|
207
|
+
? 'red'
|
|
208
|
+
: 'amber',
|
|
209
|
+
},
|
|
210
|
+
{ label: 'Title', value: check.title || '(no title)' },
|
|
211
|
+
{ label: 'Mode', value: 'basic' },
|
|
212
|
+
],
|
|
213
|
+
},
|
|
214
|
+
metrics: { status_code: check.status, load_ms: loadMs, capture_ms: 0 },
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function moodFor(status) {
|
|
219
|
+
switch (status) {
|
|
220
|
+
case 'healthy':
|
|
221
|
+
return 'happy';
|
|
222
|
+
case 'warning':
|
|
223
|
+
return 'concerned';
|
|
224
|
+
case 'critical':
|
|
225
|
+
return 'stressed';
|
|
226
|
+
default:
|
|
227
|
+
return 'idle';
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ScreenData } from '../../core/types.js';
|
|
2
|
+
/** Default target resolution for the web-view TV (4:3 to match a 4×3 companion). */
|
|
3
|
+
export declare const TARGET_W = 160;
|
|
4
|
+
export declare const TARGET_H = 120;
|
|
5
|
+
/** Decode a PNG buffer to RGBA bytes + dimensions. */
|
|
6
|
+
export declare function decodePng(buf: Buffer): {
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
data: Buffer;
|
|
10
|
+
};
|
|
11
|
+
/** Area-average (box filter) downscale of RGBA `src` to `tw`×`th`. */
|
|
12
|
+
export declare function boxDownscale(src: Buffer, sw: number, sh: number, tw: number, th: number): Buffer;
|
|
13
|
+
/**
|
|
14
|
+
* Median-cut quantization of RGBA pixels into at most `maxColors` colors.
|
|
15
|
+
* Returns a flat RGB palette and a per-pixel palette-index array. Each pixel's
|
|
16
|
+
* index is the box it lands in, so no separate nearest-color pass is needed.
|
|
17
|
+
*/
|
|
18
|
+
export declare function quantize(rgba: Buffer, pixelCount: number, maxColors: number): {
|
|
19
|
+
palette: Uint8Array;
|
|
20
|
+
indices: Uint8Array;
|
|
21
|
+
};
|
|
22
|
+
/** Build a pixel-image screenData payload from a palette + index array. */
|
|
23
|
+
export declare function buildPixelImageScreen(width: number, height: number, palette: Uint8Array, indices: Uint8Array): ScreenData;
|
|
24
|
+
/** FNV-1a hash over bytes — cheap change detection for the rendered frame. */
|
|
25
|
+
export declare function fnv1a(bytes: Uint8Array): number;
|
|
26
|
+
/**
|
|
27
|
+
* Decode a PNG screenshot into a pixel-image screenData payload, downscaling to
|
|
28
|
+
* the target resolution if the source isn't already there (box-filter fallback
|
|
29
|
+
* for when Chrome's fractional deviceScaleFactor doesn't rasterize natively).
|
|
30
|
+
*/
|
|
31
|
+
export declare function pngToPixelImage(pngBuffer: Buffer, maxColors?: number, targetW?: number, targetH?: number): {
|
|
32
|
+
screenData: ScreenData;
|
|
33
|
+
byteSize: number;
|
|
34
|
+
hash: number;
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=pixelate.d.ts.map
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { PNG } from 'pngjs';
|
|
2
|
+
/** Default target resolution for the web-view TV (4:3 to match a 4×3 companion). */
|
|
3
|
+
export const TARGET_W = 160;
|
|
4
|
+
export const TARGET_H = 120;
|
|
5
|
+
/** Decode a PNG buffer to RGBA bytes + dimensions. */
|
|
6
|
+
export function decodePng(buf) {
|
|
7
|
+
const png = PNG.sync.read(buf);
|
|
8
|
+
return { width: png.width, height: png.height, data: png.data };
|
|
9
|
+
}
|
|
10
|
+
/** Area-average (box filter) downscale of RGBA `src` to `tw`×`th`. */
|
|
11
|
+
export function boxDownscale(src, sw, sh, tw, th) {
|
|
12
|
+
const out = Buffer.alloc(tw * th * 4);
|
|
13
|
+
for (let ty = 0; ty < th; ty++) {
|
|
14
|
+
const sy0 = Math.floor((ty * sh) / th);
|
|
15
|
+
const sy1 = Math.max(sy0 + 1, Math.floor(((ty + 1) * sh) / th));
|
|
16
|
+
for (let tx = 0; tx < tw; tx++) {
|
|
17
|
+
const sx0 = Math.floor((tx * sw) / tw);
|
|
18
|
+
const sx1 = Math.max(sx0 + 1, Math.floor(((tx + 1) * sw) / tw));
|
|
19
|
+
let r = 0;
|
|
20
|
+
let g = 0;
|
|
21
|
+
let b = 0;
|
|
22
|
+
let n = 0;
|
|
23
|
+
for (let sy = sy0; sy < sy1; sy++) {
|
|
24
|
+
for (let sx = sx0; sx < sx1; sx++) {
|
|
25
|
+
const i = (sy * sw + sx) * 4;
|
|
26
|
+
r += src[i];
|
|
27
|
+
g += src[i + 1];
|
|
28
|
+
b += src[i + 2];
|
|
29
|
+
n++;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const o = (ty * tw + tx) * 4;
|
|
33
|
+
out[o] = Math.round(r / n);
|
|
34
|
+
out[o + 1] = Math.round(g / n);
|
|
35
|
+
out[o + 2] = Math.round(b / n);
|
|
36
|
+
out[o + 3] = 255;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Median-cut quantization of RGBA pixels into at most `maxColors` colors.
|
|
43
|
+
* Returns a flat RGB palette and a per-pixel palette-index array. Each pixel's
|
|
44
|
+
* index is the box it lands in, so no separate nearest-color pass is needed.
|
|
45
|
+
*/
|
|
46
|
+
export function quantize(rgba, pixelCount, maxColors) {
|
|
47
|
+
const pixels = new Array(pixelCount);
|
|
48
|
+
for (let i = 0; i < pixelCount; i++) {
|
|
49
|
+
const o = i * 4;
|
|
50
|
+
pixels[i] = { r: rgba[o], g: rgba[o + 1], b: rgba[o + 2] };
|
|
51
|
+
}
|
|
52
|
+
const refs = pixels.map((_, i) => i);
|
|
53
|
+
const boxes = [{ start: 0, end: refs.length }];
|
|
54
|
+
const widestChannel = (box) => {
|
|
55
|
+
let rmin = 255;
|
|
56
|
+
let rmax = 0;
|
|
57
|
+
let gmin = 255;
|
|
58
|
+
let gmax = 0;
|
|
59
|
+
let bmin = 255;
|
|
60
|
+
let bmax = 0;
|
|
61
|
+
for (let i = box.start; i < box.end; i++) {
|
|
62
|
+
const p = pixels[refs[i]];
|
|
63
|
+
if (p.r < rmin)
|
|
64
|
+
rmin = p.r;
|
|
65
|
+
if (p.r > rmax)
|
|
66
|
+
rmax = p.r;
|
|
67
|
+
if (p.g < gmin)
|
|
68
|
+
gmin = p.g;
|
|
69
|
+
if (p.g > gmax)
|
|
70
|
+
gmax = p.g;
|
|
71
|
+
if (p.b < bmin)
|
|
72
|
+
bmin = p.b;
|
|
73
|
+
if (p.b > bmax)
|
|
74
|
+
bmax = p.b;
|
|
75
|
+
}
|
|
76
|
+
const rr = rmax - rmin;
|
|
77
|
+
const gr = gmax - gmin;
|
|
78
|
+
const br = bmax - bmin;
|
|
79
|
+
if (rr >= gr && rr >= br)
|
|
80
|
+
return { channel: 'r', range: rr };
|
|
81
|
+
if (gr >= br)
|
|
82
|
+
return { channel: 'g', range: gr };
|
|
83
|
+
return { channel: 'b', range: br };
|
|
84
|
+
};
|
|
85
|
+
while (boxes.length < maxColors) {
|
|
86
|
+
let target = -1;
|
|
87
|
+
let best = 0;
|
|
88
|
+
for (let i = 0; i < boxes.length; i++) {
|
|
89
|
+
const box = boxes[i];
|
|
90
|
+
if (box.end - box.start < 2)
|
|
91
|
+
continue;
|
|
92
|
+
const { range } = widestChannel(box);
|
|
93
|
+
if (range > best) {
|
|
94
|
+
best = range;
|
|
95
|
+
target = i;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (target < 0 || best <= 0)
|
|
99
|
+
break;
|
|
100
|
+
const box = boxes[target];
|
|
101
|
+
const { channel } = widestChannel(box);
|
|
102
|
+
const slice = refs
|
|
103
|
+
.slice(box.start, box.end)
|
|
104
|
+
.sort((a, b) => pixels[a][channel] - pixels[b][channel]);
|
|
105
|
+
for (let i = 0; i < slice.length; i++)
|
|
106
|
+
refs[box.start + i] = slice[i];
|
|
107
|
+
const mid = box.start + ((box.end - box.start) >> 1);
|
|
108
|
+
boxes.splice(target, 1, { start: box.start, end: mid }, { start: mid, end: box.end });
|
|
109
|
+
}
|
|
110
|
+
const palette = new Uint8Array(boxes.length * 3);
|
|
111
|
+
const indices = new Uint8Array(pixelCount);
|
|
112
|
+
boxes.forEach((box, bi) => {
|
|
113
|
+
let r = 0;
|
|
114
|
+
let g = 0;
|
|
115
|
+
let b = 0;
|
|
116
|
+
const n = box.end - box.start;
|
|
117
|
+
for (let i = box.start; i < box.end; i++) {
|
|
118
|
+
const ref = refs[i];
|
|
119
|
+
const p = pixels[ref];
|
|
120
|
+
r += p.r;
|
|
121
|
+
g += p.g;
|
|
122
|
+
b += p.b;
|
|
123
|
+
indices[ref] = bi;
|
|
124
|
+
}
|
|
125
|
+
palette[bi * 3] = Math.round(r / Math.max(1, n));
|
|
126
|
+
palette[bi * 3 + 1] = Math.round(g / Math.max(1, n));
|
|
127
|
+
palette[bi * 3 + 2] = Math.round(b / Math.max(1, n));
|
|
128
|
+
});
|
|
129
|
+
return { palette, indices };
|
|
130
|
+
}
|
|
131
|
+
/** Build a pixel-image screenData payload from a palette + index array. */
|
|
132
|
+
export function buildPixelImageScreen(width, height, palette, indices) {
|
|
133
|
+
const paletteCount = palette.length / 3;
|
|
134
|
+
return {
|
|
135
|
+
layout: 'pixel-image',
|
|
136
|
+
values: [
|
|
137
|
+
{ label: 'fmt', value: `idx8;${width}x${height};p${paletteCount}` },
|
|
138
|
+
{ label: 'palette', value: Buffer.from(palette).toString('base64') },
|
|
139
|
+
{ label: 'pixels', value: Buffer.from(indices).toString('base64') },
|
|
140
|
+
],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/** FNV-1a hash over bytes — cheap change detection for the rendered frame. */
|
|
144
|
+
export function fnv1a(bytes) {
|
|
145
|
+
let h = 0x811c9dc5;
|
|
146
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
147
|
+
h ^= bytes[i];
|
|
148
|
+
h = Math.imul(h, 0x01000193);
|
|
149
|
+
}
|
|
150
|
+
return h >>> 0;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Decode a PNG screenshot into a pixel-image screenData payload, downscaling to
|
|
154
|
+
* the target resolution if the source isn't already there (box-filter fallback
|
|
155
|
+
* for when Chrome's fractional deviceScaleFactor doesn't rasterize natively).
|
|
156
|
+
*/
|
|
157
|
+
export function pngToPixelImage(pngBuffer, maxColors = 32, targetW = TARGET_W, targetH = TARGET_H) {
|
|
158
|
+
const { width, height, data } = decodePng(pngBuffer);
|
|
159
|
+
let rgba = data;
|
|
160
|
+
let w = width;
|
|
161
|
+
let h = height;
|
|
162
|
+
if (width !== targetW || height !== targetH) {
|
|
163
|
+
rgba = boxDownscale(data, width, height, targetW, targetH);
|
|
164
|
+
w = targetW;
|
|
165
|
+
h = targetH;
|
|
166
|
+
}
|
|
167
|
+
const { palette, indices } = quantize(rgba, w * h, maxColors);
|
|
168
|
+
const screenData = buildPixelImageScreen(w, h, palette, indices);
|
|
169
|
+
const byteSize = screenData.values.reduce((sum, v) => sum + v.value.length, 0);
|
|
170
|
+
return { screenData, byteSize, hash: fnv1a(indices) };
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=pixelate.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,76 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocville/npc",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Put your Claude Code and Codex sessions (and your servers) in the Nocville office as live NPCs",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"nocville-npc": "dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"require": "./dist/cjs/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"!dist/**/*.map",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"pngjs": "^7.0.0",
|
|
27
|
+
"@nocville/node": "^0.2.1"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"puppeteer-core": "*"
|
|
31
|
+
},
|
|
32
|
+
"peerDependenciesMeta": {
|
|
33
|
+
"puppeteer-core": {
|
|
34
|
+
"optional": true
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^20.10.0",
|
|
39
|
+
"@types/pngjs": "^6.0.5",
|
|
40
|
+
"typescript": "^5.3.2",
|
|
41
|
+
"vitest": "^3.2.6",
|
|
42
|
+
"puppeteer-core": "^24.0.0"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=20.12.0"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"keywords": [
|
|
51
|
+
"nocville",
|
|
52
|
+
"claude-code",
|
|
53
|
+
"codex",
|
|
54
|
+
"ai-agents",
|
|
55
|
+
"monitoring",
|
|
56
|
+
"npc",
|
|
57
|
+
"status",
|
|
58
|
+
"cli"
|
|
59
|
+
],
|
|
60
|
+
"license": "MIT",
|
|
61
|
+
"homepage": "https://nocville.com",
|
|
62
|
+
"repository": {
|
|
63
|
+
"type": "git",
|
|
64
|
+
"url": "git+https://github.com/marshmansf/nocville.git",
|
|
65
|
+
"directory": "libs/nocville-npc-library"
|
|
66
|
+
},
|
|
67
|
+
"scripts": {
|
|
68
|
+
"build": "tsc && tsc -p tsconfig.cjs.json && node -e \"require('fs').writeFileSync('dist/cjs/package.json', JSON.stringify({ type: 'commonjs' }))\" && node -e \"require('fs').chmodSync('dist/cli.js', 0o755)\"",
|
|
69
|
+
"clean": "rm -rf dist",
|
|
70
|
+
"typecheck": "tsc --noEmit",
|
|
71
|
+
"test": "vitest run",
|
|
72
|
+
"test:watch": "vitest",
|
|
73
|
+
"smoke": "node scripts/smoke.mjs",
|
|
74
|
+
"simulate": "node scripts/simulate.mjs"
|
|
75
|
+
}
|
|
6
76
|
}
|