@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.
Files changed (213) hide show
  1. package/README.md +268 -2
  2. package/dist/cjs/cli.js +584 -0
  3. package/dist/cjs/core/api-key.js +69 -0
  4. package/dist/cjs/core/base-npc.js +160 -0
  5. package/dist/cjs/core/config.js +108 -0
  6. package/dist/cjs/core/event-npc.js +14 -0
  7. package/dist/cjs/core/logger.js +39 -0
  8. package/dist/cjs/core/polling-npc.js +65 -0
  9. package/dist/cjs/core/screen.js +28 -0
  10. package/dist/cjs/core/session-filter.js +150 -0
  11. package/dist/cjs/core/text.js +15 -0
  12. package/dist/cjs/core/thresholds.js +48 -0
  13. package/dist/cjs/core/types.js +10 -0
  14. package/dist/cjs/core/user-config.js +92 -0
  15. package/dist/cjs/doctor.js +130 -0
  16. package/dist/cjs/index.js +95 -0
  17. package/dist/cjs/npcs/agent-common/activity.js +291 -0
  18. package/dist/cjs/npcs/agent-common/git-stats.js +71 -0
  19. package/dist/cjs/npcs/agent-common/hook-runner.js +252 -0
  20. package/dist/cjs/npcs/agent-common/hook-settings.js +279 -0
  21. package/dist/cjs/npcs/agent-common/liveness.js +112 -0
  22. package/dist/cjs/npcs/agent-common/lock.js +99 -0
  23. package/dist/cjs/npcs/agent-common/runtime.js +154 -0
  24. package/dist/cjs/npcs/agent-common/session.js +412 -0
  25. package/dist/cjs/npcs/agent-common/shim.js +96 -0
  26. package/dist/cjs/npcs/agent-common/watchdog.js +149 -0
  27. package/dist/cjs/npcs/claude-code/defaults.js +20 -0
  28. package/dist/cjs/npcs/claude-code/hook-handler.js +139 -0
  29. package/dist/cjs/npcs/claude-code/index.js +104 -0
  30. package/dist/cjs/npcs/claude-code/setup.js +133 -0
  31. package/dist/cjs/npcs/claude-code/state-machine.js +130 -0
  32. package/dist/cjs/npcs/codex/defaults.js +32 -0
  33. package/dist/cjs/npcs/codex/hook-handler.js +116 -0
  34. package/dist/cjs/npcs/codex/index.js +92 -0
  35. package/dist/cjs/npcs/codex/rollout.js +246 -0
  36. package/dist/cjs/npcs/codex/setup.js +104 -0
  37. package/dist/cjs/npcs/codex/watcher.js +361 -0
  38. package/dist/cjs/npcs/cursor/defaults.js +15 -0
  39. package/dist/cjs/npcs/cursor/hook-handler.js +65 -0
  40. package/dist/cjs/npcs/cursor/index.js +72 -0
  41. package/dist/cjs/npcs/cursor/normalize.js +54 -0
  42. package/dist/cjs/npcs/cursor/setup.js +98 -0
  43. package/dist/cjs/npcs/gemini/defaults.js +15 -0
  44. package/dist/cjs/npcs/gemini/hook-handler.js +93 -0
  45. package/dist/cjs/npcs/gemini/index.js +69 -0
  46. package/dist/cjs/npcs/gemini/setup.js +98 -0
  47. package/dist/cjs/npcs/github-watcher/defaults.js +11 -0
  48. package/dist/cjs/npcs/github-watcher/format.js +21 -0
  49. package/dist/cjs/npcs/github-watcher/github-api.js +126 -0
  50. package/dist/cjs/npcs/github-watcher/index.js +148 -0
  51. package/dist/cjs/npcs/internet-connectivity/checks.js +92 -0
  52. package/dist/cjs/npcs/internet-connectivity/defaults.js +22 -0
  53. package/dist/cjs/npcs/internet-connectivity/index.js +251 -0
  54. package/dist/cjs/npcs/log-watcher/buffer.js +40 -0
  55. package/dist/cjs/npcs/log-watcher/defaults.js +12 -0
  56. package/dist/cjs/npcs/log-watcher/index.js +161 -0
  57. package/dist/cjs/npcs/log-watcher/tail.js +81 -0
  58. package/dist/cjs/npcs/process-watcher/defaults.js +9 -0
  59. package/dist/cjs/npcs/process-watcher/index.js +140 -0
  60. package/dist/cjs/npcs/process-watcher/process-scanner.js +114 -0
  61. package/dist/cjs/npcs/receptionist/defaults.js +9 -0
  62. package/dist/cjs/npcs/receptionist/index.js +120 -0
  63. package/dist/cjs/npcs/receptionist/weather.js +129 -0
  64. package/dist/cjs/npcs/system-monitor/collectors.js +68 -0
  65. package/dist/cjs/npcs/system-monitor/defaults.js +13 -0
  66. package/dist/cjs/npcs/system-monitor/index.js +105 -0
  67. package/dist/cjs/npcs/web-page-watcher/basic-mode.js +58 -0
  68. package/dist/cjs/npcs/web-page-watcher/capture.js +80 -0
  69. package/dist/cjs/npcs/web-page-watcher/defaults.js +31 -0
  70. package/dist/cjs/npcs/web-page-watcher/index.js +234 -0
  71. package/dist/cjs/npcs/web-page-watcher/pixelate.js +181 -0
  72. package/dist/cjs/package.json +1 -0
  73. package/dist/cli.d.ts +3 -0
  74. package/dist/cli.js +549 -0
  75. package/dist/core/api-key.d.ts +34 -0
  76. package/dist/core/api-key.js +64 -0
  77. package/dist/core/base-npc.d.ts +41 -0
  78. package/dist/core/base-npc.js +156 -0
  79. package/dist/core/config.d.ts +22 -0
  80. package/dist/core/config.js +101 -0
  81. package/dist/core/event-npc.d.ts +8 -0
  82. package/dist/core/event-npc.js +10 -0
  83. package/dist/core/logger.d.ts +11 -0
  84. package/dist/core/logger.js +35 -0
  85. package/dist/core/polling-npc.d.ts +18 -0
  86. package/dist/core/polling-npc.js +61 -0
  87. package/dist/core/screen.d.ts +11 -0
  88. package/dist/core/screen.js +24 -0
  89. package/dist/core/session-filter.d.ts +59 -0
  90. package/dist/core/session-filter.js +143 -0
  91. package/dist/core/text.d.ts +6 -0
  92. package/dist/core/text.js +12 -0
  93. package/dist/core/thresholds.d.ts +10 -0
  94. package/dist/core/thresholds.js +44 -0
  95. package/dist/core/types.d.ts +60 -0
  96. package/dist/core/types.js +7 -0
  97. package/dist/core/user-config.d.ts +43 -0
  98. package/dist/core/user-config.js +85 -0
  99. package/dist/doctor.d.ts +8 -0
  100. package/dist/doctor.js +126 -0
  101. package/dist/index.d.ts +41 -0
  102. package/dist/index.js +26 -0
  103. package/dist/npcs/agent-common/activity.d.ts +42 -0
  104. package/dist/npcs/agent-common/activity.js +280 -0
  105. package/dist/npcs/agent-common/git-stats.d.ts +11 -0
  106. package/dist/npcs/agent-common/git-stats.js +67 -0
  107. package/dist/npcs/agent-common/hook-runner.d.ts +62 -0
  108. package/dist/npcs/agent-common/hook-runner.js +245 -0
  109. package/dist/npcs/agent-common/hook-settings.d.ts +75 -0
  110. package/dist/npcs/agent-common/hook-settings.js +265 -0
  111. package/dist/npcs/agent-common/liveness.d.ts +12 -0
  112. package/dist/npcs/agent-common/liveness.js +105 -0
  113. package/dist/npcs/agent-common/lock.d.ts +6 -0
  114. package/dist/npcs/agent-common/lock.js +95 -0
  115. package/dist/npcs/agent-common/runtime.d.ts +16 -0
  116. package/dist/npcs/agent-common/runtime.js +149 -0
  117. package/dist/npcs/agent-common/session.d.ts +192 -0
  118. package/dist/npcs/agent-common/session.js +390 -0
  119. package/dist/npcs/agent-common/shim.d.ts +4 -0
  120. package/dist/npcs/agent-common/shim.js +90 -0
  121. package/dist/npcs/agent-common/watchdog.d.ts +16 -0
  122. package/dist/npcs/agent-common/watchdog.js +144 -0
  123. package/dist/npcs/claude-code/defaults.d.ts +15 -0
  124. package/dist/npcs/claude-code/defaults.js +17 -0
  125. package/dist/npcs/claude-code/hook-handler.d.ts +40 -0
  126. package/dist/npcs/claude-code/hook-handler.js +131 -0
  127. package/dist/npcs/claude-code/index.d.ts +36 -0
  128. package/dist/npcs/claude-code/index.js +92 -0
  129. package/dist/npcs/claude-code/setup.d.ts +58 -0
  130. package/dist/npcs/claude-code/setup.js +121 -0
  131. package/dist/npcs/claude-code/state-machine.d.ts +28 -0
  132. package/dist/npcs/claude-code/state-machine.js +125 -0
  133. package/dist/npcs/codex/defaults.d.ts +24 -0
  134. package/dist/npcs/codex/defaults.js +28 -0
  135. package/dist/npcs/codex/hook-handler.d.ts +24 -0
  136. package/dist/npcs/codex/hook-handler.js +110 -0
  137. package/dist/npcs/codex/index.d.ts +21 -0
  138. package/dist/npcs/codex/index.js +76 -0
  139. package/dist/npcs/codex/rollout.d.ts +39 -0
  140. package/dist/npcs/codex/rollout.js +242 -0
  141. package/dist/npcs/codex/setup.d.ts +37 -0
  142. package/dist/npcs/codex/setup.js +96 -0
  143. package/dist/npcs/codex/watcher.d.ts +65 -0
  144. package/dist/npcs/codex/watcher.js +356 -0
  145. package/dist/npcs/cursor/defaults.d.ts +9 -0
  146. package/dist/npcs/cursor/defaults.js +11 -0
  147. package/dist/npcs/cursor/hook-handler.d.ts +8 -0
  148. package/dist/npcs/cursor/hook-handler.js +60 -0
  149. package/dist/npcs/cursor/index.d.ts +18 -0
  150. package/dist/npcs/cursor/index.js +57 -0
  151. package/dist/npcs/cursor/normalize.d.ts +6 -0
  152. package/dist/npcs/cursor/normalize.js +49 -0
  153. package/dist/npcs/cursor/setup.d.ts +29 -0
  154. package/dist/npcs/cursor/setup.js +90 -0
  155. package/dist/npcs/gemini/defaults.d.ts +9 -0
  156. package/dist/npcs/gemini/defaults.js +11 -0
  157. package/dist/npcs/gemini/hook-handler.d.ts +10 -0
  158. package/dist/npcs/gemini/hook-handler.js +87 -0
  159. package/dist/npcs/gemini/index.d.ts +17 -0
  160. package/dist/npcs/gemini/index.js +56 -0
  161. package/dist/npcs/gemini/setup.d.ts +29 -0
  162. package/dist/npcs/gemini/setup.js +90 -0
  163. package/dist/npcs/github-watcher/defaults.d.ts +8 -0
  164. package/dist/npcs/github-watcher/defaults.js +8 -0
  165. package/dist/npcs/github-watcher/format.d.ts +3 -0
  166. package/dist/npcs/github-watcher/format.js +18 -0
  167. package/dist/npcs/github-watcher/github-api.d.ts +55 -0
  168. package/dist/npcs/github-watcher/github-api.js +120 -0
  169. package/dist/npcs/github-watcher/index.d.ts +35 -0
  170. package/dist/npcs/github-watcher/index.js +144 -0
  171. package/dist/npcs/internet-connectivity/checks.d.ts +24 -0
  172. package/dist/npcs/internet-connectivity/checks.js +84 -0
  173. package/dist/npcs/internet-connectivity/defaults.d.ts +20 -0
  174. package/dist/npcs/internet-connectivity/defaults.js +19 -0
  175. package/dist/npcs/internet-connectivity/index.d.ts +55 -0
  176. package/dist/npcs/internet-connectivity/index.js +246 -0
  177. package/dist/npcs/log-watcher/buffer.d.ts +18 -0
  178. package/dist/npcs/log-watcher/buffer.js +34 -0
  179. package/dist/npcs/log-watcher/defaults.d.ts +9 -0
  180. package/dist/npcs/log-watcher/defaults.js +9 -0
  181. package/dist/npcs/log-watcher/index.d.ts +45 -0
  182. package/dist/npcs/log-watcher/index.js +157 -0
  183. package/dist/npcs/log-watcher/tail.d.ts +9 -0
  184. package/dist/npcs/log-watcher/tail.js +78 -0
  185. package/dist/npcs/process-watcher/defaults.d.ts +6 -0
  186. package/dist/npcs/process-watcher/defaults.js +6 -0
  187. package/dist/npcs/process-watcher/index.d.ts +26 -0
  188. package/dist/npcs/process-watcher/index.js +136 -0
  189. package/dist/npcs/process-watcher/process-scanner.d.ts +19 -0
  190. package/dist/npcs/process-watcher/process-scanner.js +110 -0
  191. package/dist/npcs/receptionist/defaults.d.ts +6 -0
  192. package/dist/npcs/receptionist/defaults.js +6 -0
  193. package/dist/npcs/receptionist/index.d.ts +29 -0
  194. package/dist/npcs/receptionist/index.js +116 -0
  195. package/dist/npcs/receptionist/weather.d.ts +37 -0
  196. package/dist/npcs/receptionist/weather.js +121 -0
  197. package/dist/npcs/system-monitor/collectors.d.ts +45 -0
  198. package/dist/npcs/system-monitor/collectors.js +60 -0
  199. package/dist/npcs/system-monitor/defaults.d.ts +7 -0
  200. package/dist/npcs/system-monitor/defaults.js +10 -0
  201. package/dist/npcs/system-monitor/index.d.ts +27 -0
  202. package/dist/npcs/system-monitor/index.js +101 -0
  203. package/dist/npcs/web-page-watcher/basic-mode.d.ts +16 -0
  204. package/dist/npcs/web-page-watcher/basic-mode.js +52 -0
  205. package/dist/npcs/web-page-watcher/capture.d.ts +49 -0
  206. package/dist/npcs/web-page-watcher/capture.js +75 -0
  207. package/dist/npcs/web-page-watcher/defaults.d.ts +19 -0
  208. package/dist/npcs/web-page-watcher/defaults.js +27 -0
  209. package/dist/npcs/web-page-watcher/index.d.ts +42 -0
  210. package/dist/npcs/web-page-watcher/index.js +230 -0
  211. package/dist/npcs/web-page-watcher/pixelate.d.ts +36 -0
  212. package/dist/npcs/web-page-watcher/pixelate.js +172 -0
  213. 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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
  }