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