lightman-agent 1.0.5 → 1.0.6

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 (115) hide show
  1. package/agent.config.template.json +30 -30
  2. package/package.json +52 -52
  3. package/public/assets/index-CcBNCz6h.css +1 -1
  4. package/public/assets/index-D9QHMG8k.js +1 -0
  5. package/public/assets/index-H-8HDl46.js +1 -1
  6. package/public/assets/index-YodeiCia.css +1 -0
  7. package/public/assets/index-legacy-DWtNM8y7.js +41 -0
  8. package/public/assets/museum-map-CwVDA2z1.svg +4182 -0
  9. package/public/assets/polyfills-legacy-DyVYWHbW.js +4 -0
  10. package/public/index.html +7 -2
  11. package/public/templates/custom08/elements/back-button.svg +20 -0
  12. package/public/templates/custom08/elements/base-map-background.svg +37 -0
  13. package/public/templates/custom08/elements/base-map.svg +1191 -0
  14. package/public/templates/custom08/elements/gallery-1-2-3-info-panel.svg +236 -0
  15. package/public/templates/custom08/elements/gallery-4-5-6-7-info-panel.svg +266 -0
  16. package/public/templates/custom08/elements/gallery-8-9-info-panel.svg +274 -0
  17. package/public/templates/custom08/elements/gallery-labels/_nav-map-styles.css +554 -0
  18. package/public/templates/custom08/elements/gallery-labels/_styles.css +556 -0
  19. package/public/templates/custom08/elements/gallery-labels/gallery-1.svg +35 -0
  20. package/public/templates/custom08/elements/gallery-labels/gallery-2.svg +34 -0
  21. package/public/templates/custom08/elements/gallery-labels/gallery-3.svg +34 -0
  22. package/public/templates/custom08/elements/gallery-labels/gallery-4.svg +37 -0
  23. package/public/templates/custom08/elements/gallery-labels/gallery-5.svg +34 -0
  24. package/public/templates/custom08/elements/gallery-labels/gallery-6.svg +34 -0
  25. package/public/templates/custom08/elements/gallery-labels/gallery-7.svg +34 -0
  26. package/public/templates/custom08/elements/gallery-labels/gallery-8.svg +37 -0
  27. package/public/templates/custom08/elements/gallery-labels/gallery-9.svg +34 -0
  28. package/public/templates/custom08/elements/hand-hint.png +0 -0
  29. package/public/templates/custom08/elements/idle-screen-bg.svg +5 -0
  30. package/public/templates/custom08/elements/idle-screen-map.svg +627 -0
  31. package/public/templates/custom08/elements/idle-screen-text.svg +350 -0
  32. package/public/templates/custom08/elements/key-map-1.svg +986 -0
  33. package/public/templates/custom08/elements/key-map-2.svg +1018 -0
  34. package/public/templates/custom08/elements/key-map-3.svg +1019 -0
  35. package/public/templates/custom08/elements/key-map-combined.svg +1001 -0
  36. package/public/templates/custom08/elements/map-highlight-marker.svg +11 -0
  37. package/public/templates/custom08/elements/map-pin-marker.svg +15 -0
  38. package/public/templates/custom08/elements/map-teardrop-star-marker.svg +13 -0
  39. package/public/templates/custom08/elements/nav-circle-galleries-1-3.svg +21 -0
  40. package/public/templates/custom08/elements/nav-circle-galleries-4-7.svg +24 -0
  41. package/public/templates/custom08/elements/nav-circle-galleries-8-9.svg +20 -0
  42. package/public/templates/custom08/elements/section1-map.svg +1435 -0
  43. package/public/templates/custom08/elements/section2-map.svg +1724 -0
  44. package/public/templates/custom08/elements/section3-map.svg +1295 -0
  45. package/public/templates/custom08/fonts/CabinetGrotesk-Variable.ttf +0 -0
  46. package/public/templates/custom08/images/highlights/Screenshot_2026-03-05_at_7.23.12_PM.png +0 -0
  47. package/public/templates/custom08/images/highlights/Screenshot_2026-03-05_at_7.23.56_PM.png +0 -0
  48. package/public/templates/custom08/images/highlights/Screenshot_2026-03-05_at_7.24.24_PM.png +0 -0
  49. package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.31.58_PM.jpg +0 -0
  50. package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.32.11_PM.jpg +0 -0
  51. package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.32.36_PM.jpg +0 -0
  52. package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.32.48_PM.jpg +0 -0
  53. package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.32.59_PM.jpg +0 -0
  54. package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.33.15_PM.jpg +0 -0
  55. package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.33.27_PM.jpg +0 -0
  56. package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.33.34_PM.jpg +0 -0
  57. package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.33.42_PM.jpg +0 -0
  58. package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.33.50_PM.jpg +0 -0
  59. package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.33.58_PM.jpg +0 -0
  60. package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.34.04_PM.jpg +0 -0
  61. package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.34.11_PM.jpg +0 -0
  62. package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.34.20_PM.jpg +0 -0
  63. package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.34.57_PM.jpg +0 -0
  64. package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.35.03_PM.jpg +0 -0
  65. package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.35.16_PM.jpg +0 -0
  66. package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.35.23_PM.jpg +0 -0
  67. package/public/templates/custom08/images/highlights/prologue-highlight.png +0 -0
  68. package/scripts/guardian.ps1 +75 -75
  69. package/scripts/install-linux.sh +134 -134
  70. package/scripts/install-rpi.sh +117 -117
  71. package/scripts/install-windows.ps1 +505 -505
  72. package/scripts/launch-kiosk.vbs +101 -101
  73. package/scripts/lightman-agent.logrotate +12 -12
  74. package/scripts/lightman-agent.service +38 -38
  75. package/scripts/lightman-shell.bat +107 -107
  76. package/scripts/reinstall-windows.ps1 +26 -26
  77. package/scripts/restore-desktop.ps1 +32 -32
  78. package/scripts/setup.ps1 +116 -116
  79. package/scripts/setup.sh +115 -115
  80. package/scripts/sync-display.mjs +20 -0
  81. package/scripts/uninstall-linux.sh +50 -50
  82. package/scripts/uninstall-windows.ps1 +54 -54
  83. package/src/commands/display.ts +177 -177
  84. package/src/commands/kiosk.ts +113 -113
  85. package/src/commands/maintenance.ts +106 -106
  86. package/src/commands/network.ts +129 -129
  87. package/src/commands/power.ts +163 -163
  88. package/src/commands/rpi.ts +45 -45
  89. package/src/commands/screenshot.ts +166 -166
  90. package/src/commands/serial.ts +17 -17
  91. package/src/commands/update.ts +124 -124
  92. package/src/index.ts +652 -652
  93. package/src/lib/config.ts +69 -69
  94. package/src/lib/identity.ts +40 -40
  95. package/src/lib/logger.ts +137 -137
  96. package/src/lib/platform.ts +10 -10
  97. package/src/lib/rpi.ts +180 -180
  98. package/src/lib/screens.ts +128 -128
  99. package/src/lib/types.ts +176 -176
  100. package/src/services/commands.ts +107 -107
  101. package/src/services/health.ts +161 -161
  102. package/src/services/kiosk.ts +384 -384
  103. package/src/services/localEvents.ts +60 -60
  104. package/src/services/logForwarder.ts +72 -72
  105. package/src/services/multiScreenKiosk.ts +324 -324
  106. package/src/services/oscBridge.ts +186 -186
  107. package/src/services/powerScheduler.ts +260 -260
  108. package/src/services/provisioning.ts +120 -120
  109. package/src/services/serialBridge.ts +230 -230
  110. package/src/services/serviceLauncher.ts +183 -183
  111. package/src/services/staticServer.ts +226 -226
  112. package/src/services/updater.ts +249 -249
  113. package/src/services/watchdog.ts +310 -310
  114. package/src/services/websocket.ts +152 -152
  115. package/tsconfig.json +28 -28
@@ -1,45 +1,45 @@
1
- import type { CommandHandler } from '../lib/types.js';
2
- import type { Logger } from '../lib/logger.js';
3
- import { isRaspberryPi, getRpiInfo, getGpuTemp, getThrottled, isSdCardReadOnly, startWatchdog, stopWatchdog } from '../lib/rpi.js';
4
-
5
- export function registerRpiCommands(
6
- register: (command: string, handler: CommandHandler) => void,
7
- logger: Logger
8
- ): void {
9
- // rpi:info — returns model, serial, revision, gpuTemp, throttled, sdCardReadOnly
10
- register('rpi:info', async () => {
11
- if (!isRaspberryPi()) {
12
- throw new Error('Not a Raspberry Pi');
13
- }
14
- const info = getRpiInfo();
15
- return {
16
- ...info,
17
- gpuTemp: getGpuTemp(),
18
- throttled: getThrottled(),
19
- sdCardReadOnly: isSdCardReadOnly(),
20
- };
21
- });
22
-
23
- // rpi:watchdog-start — start hardware watchdog
24
- register('rpi:watchdog-start', async () => {
25
- if (!isRaspberryPi()) {
26
- throw new Error('Not a Raspberry Pi');
27
- }
28
- const started = startWatchdog();
29
- if (!started) {
30
- throw new Error('Watchdog device not available. Ensure /dev/watchdog exists and agent has permissions.');
31
- }
32
- logger.info('Hardware watchdog started');
33
- return { started: true };
34
- });
35
-
36
- // rpi:watchdog-stop — stop hardware watchdog gracefully
37
- register('rpi:watchdog-stop', async () => {
38
- if (!isRaspberryPi()) {
39
- throw new Error('Not a Raspberry Pi');
40
- }
41
- stopWatchdog();
42
- logger.info('Hardware watchdog stopped');
43
- return { stopped: true };
44
- });
45
- }
1
+ import type { CommandHandler } from '../lib/types.js';
2
+ import type { Logger } from '../lib/logger.js';
3
+ import { isRaspberryPi, getRpiInfo, getGpuTemp, getThrottled, isSdCardReadOnly, startWatchdog, stopWatchdog } from '../lib/rpi.js';
4
+
5
+ export function registerRpiCommands(
6
+ register: (command: string, handler: CommandHandler) => void,
7
+ logger: Logger
8
+ ): void {
9
+ // rpi:info — returns model, serial, revision, gpuTemp, throttled, sdCardReadOnly
10
+ register('rpi:info', async () => {
11
+ if (!isRaspberryPi()) {
12
+ throw new Error('Not a Raspberry Pi');
13
+ }
14
+ const info = getRpiInfo();
15
+ return {
16
+ ...info,
17
+ gpuTemp: getGpuTemp(),
18
+ throttled: getThrottled(),
19
+ sdCardReadOnly: isSdCardReadOnly(),
20
+ };
21
+ });
22
+
23
+ // rpi:watchdog-start — start hardware watchdog
24
+ register('rpi:watchdog-start', async () => {
25
+ if (!isRaspberryPi()) {
26
+ throw new Error('Not a Raspberry Pi');
27
+ }
28
+ const started = startWatchdog();
29
+ if (!started) {
30
+ throw new Error('Watchdog device not available. Ensure /dev/watchdog exists and agent has permissions.');
31
+ }
32
+ logger.info('Hardware watchdog started');
33
+ return { started: true };
34
+ });
35
+
36
+ // rpi:watchdog-stop — stop hardware watchdog gracefully
37
+ register('rpi:watchdog-stop', async () => {
38
+ if (!isRaspberryPi()) {
39
+ throw new Error('Not a Raspberry Pi');
40
+ }
41
+ stopWatchdog();
42
+ logger.info('Hardware watchdog stopped');
43
+ return { stopped: true };
44
+ });
45
+ }
@@ -1,166 +1,166 @@
1
- import { execFileSync } from 'child_process';
2
- import * as fs from 'fs';
3
- import * as path from 'path';
4
- import { z } from 'zod';
5
- import type { CommandHandler } from '../lib/types.js';
6
- import type { Logger } from '../lib/logger.js';
7
- import { getPlatform } from '../lib/platform.js';
8
-
9
- const ALLOWED_CAPTURE_TOOLS = ['scrot', 'import', 'screencapture'] as const;
10
-
11
- // --- Zod Schema ---
12
- const ScreenshotArgsSchema = z.object({
13
- serverUrl: z.string().url().optional(),
14
- deviceId: z.string().uuid().optional(),
15
- apiKey: z.string().min(1).optional(),
16
- quality: z.number().int().min(1).max(100).optional(),
17
- });
18
-
19
- export function registerScreenshotCommands(
20
- register: (command: string, handler: CommandHandler) => void,
21
- logger: Logger
22
- ): void {
23
- register('kiosk:screenshot', async (args) => {
24
- const parsed = ScreenshotArgsSchema.safeParse(args ?? {});
25
- if (!parsed.success) {
26
- throw new Error(`Invalid screenshot args: ${parsed.error.issues.map((i) => i.message).join(', ')}`);
27
- }
28
-
29
- const { serverUrl, deviceId, apiKey, quality: parsedQuality } = parsed.data;
30
- const quality = parsedQuality ?? 75;
31
-
32
- const tmpFile = `/tmp/lightman-screenshot-${Date.now()}.jpg`;
33
-
34
- try {
35
- // --- Capture screenshot ---
36
- const { tool, toolArgs } = resolveCapture(quality, tmpFile);
37
- logger.info(`Capturing screenshot: ${tool} ${toolArgs.join(' ')}`);
38
-
39
- execFileSync(tool, toolArgs, { timeout: 10_000, stdio: 'pipe' });
40
-
41
- if (!fs.existsSync(tmpFile)) {
42
- throw new Error('Screenshot file was not created');
43
- }
44
-
45
- const buffer = await fs.promises.readFile(tmpFile);
46
- logger.info(`Screenshot captured: ${buffer.length} bytes`);
47
-
48
- // --- Upload if server info provided ---
49
- if (serverUrl && deviceId && apiKey) {
50
- try {
51
- const uploaded = await uploadScreenshot(
52
- buffer,
53
- tmpFile,
54
- serverUrl,
55
- deviceId,
56
- apiKey,
57
- logger
58
- );
59
- return { captured: true, size: buffer.length, uploaded };
60
- } catch (uploadErr) {
61
- const errMsg =
62
- uploadErr instanceof Error ? uploadErr.message : String(uploadErr);
63
- logger.error('Screenshot upload failed:', errMsg);
64
- return { captured: true, size: buffer.length, uploaded: false, error: errMsg };
65
- }
66
- }
67
-
68
- return { captured: true, size: buffer.length, uploaded: false };
69
- } catch (err) {
70
- const errMsg = err instanceof Error ? err.message : String(err);
71
- logger.error('Screenshot capture failed:', errMsg);
72
- throw new Error(`Screenshot capture failed: ${errMsg}`);
73
- } finally {
74
- // Clean up temp file
75
- try {
76
- if (fs.existsSync(tmpFile)) {
77
- await fs.promises.unlink(tmpFile);
78
- }
79
- } catch {
80
- // Ignore cleanup errors
81
- }
82
- }
83
- });
84
- }
85
-
86
- /**
87
- * Determine the capture tool and arguments based on platform.
88
- * No user-controlled command strings — only allowlisted tools with safe args.
89
- */
90
- function resolveCapture(
91
- quality: number,
92
- tmpFile: string
93
- ): { tool: string; toolArgs: string[] } {
94
- const platform = getPlatform();
95
-
96
- if (platform === 'linux') {
97
- if (commandExists('scrot')) {
98
- return { tool: 'scrot', toolArgs: ['-q', String(quality), tmpFile] };
99
- }
100
- if (commandExists('import')) {
101
- return { tool: 'import', toolArgs: ['-window', 'root', '-quality', String(quality), tmpFile] };
102
- }
103
- throw new Error(
104
- 'No screenshot tool available. Install scrot or ImageMagick (import).'
105
- );
106
- }
107
-
108
- if (platform === 'darwin') {
109
- return { tool: 'screencapture', toolArgs: ['-x', '-t', 'jpg', tmpFile] };
110
- }
111
-
112
- throw new Error(`Screenshot capture not supported on platform: ${platform}`);
113
- }
114
-
115
- /**
116
- * Check if a command exists on the system using execFileSync (no shell injection).
117
- */
118
- function commandExists(cmd: string): boolean {
119
- if (!ALLOWED_CAPTURE_TOOLS.includes(cmd as typeof ALLOWED_CAPTURE_TOOLS[number])) {
120
- return false;
121
- }
122
- try {
123
- execFileSync('which', [cmd], { stdio: 'pipe' });
124
- return true;
125
- } catch {
126
- return false;
127
- }
128
- }
129
-
130
- /**
131
- * Upload screenshot to server via multipart/form-data.
132
- */
133
- async function uploadScreenshot(
134
- buffer: Buffer,
135
- filePath: string,
136
- serverUrl: string,
137
- deviceId: string,
138
- apiKey: string,
139
- logger: Logger
140
- ): Promise<boolean> {
141
- const endpoint = `/api/devices/${deviceId}/screenshot`;
142
- const url = `${serverUrl}${endpoint}`;
143
- const filename = path.basename(filePath);
144
-
145
- logger.info(`Uploading screenshot to ${url}`);
146
-
147
- const blob = new Blob([buffer], { type: 'image/jpeg' });
148
- const formData = new FormData();
149
- formData.append('screenshot', blob, filename);
150
-
151
- const response = await fetch(url, {
152
- method: 'POST',
153
- headers: {
154
- 'x-api-key': apiKey,
155
- },
156
- body: formData,
157
- });
158
-
159
- if (!response.ok) {
160
- const body = await response.text().catch(() => '');
161
- throw new Error(`Upload failed: HTTP ${response.status} ${body}`);
162
- }
163
-
164
- logger.info('Screenshot uploaded successfully');
165
- return true;
166
- }
1
+ import { execFileSync } from 'child_process';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { z } from 'zod';
5
+ import type { CommandHandler } from '../lib/types.js';
6
+ import type { Logger } from '../lib/logger.js';
7
+ import { getPlatform } from '../lib/platform.js';
8
+
9
+ const ALLOWED_CAPTURE_TOOLS = ['scrot', 'import', 'screencapture'] as const;
10
+
11
+ // --- Zod Schema ---
12
+ const ScreenshotArgsSchema = z.object({
13
+ serverUrl: z.string().url().optional(),
14
+ deviceId: z.string().uuid().optional(),
15
+ apiKey: z.string().min(1).optional(),
16
+ quality: z.number().int().min(1).max(100).optional(),
17
+ });
18
+
19
+ export function registerScreenshotCommands(
20
+ register: (command: string, handler: CommandHandler) => void,
21
+ logger: Logger
22
+ ): void {
23
+ register('kiosk:screenshot', async (args) => {
24
+ const parsed = ScreenshotArgsSchema.safeParse(args ?? {});
25
+ if (!parsed.success) {
26
+ throw new Error(`Invalid screenshot args: ${parsed.error.issues.map((i) => i.message).join(', ')}`);
27
+ }
28
+
29
+ const { serverUrl, deviceId, apiKey, quality: parsedQuality } = parsed.data;
30
+ const quality = parsedQuality ?? 75;
31
+
32
+ const tmpFile = `/tmp/lightman-screenshot-${Date.now()}.jpg`;
33
+
34
+ try {
35
+ // --- Capture screenshot ---
36
+ const { tool, toolArgs } = resolveCapture(quality, tmpFile);
37
+ logger.info(`Capturing screenshot: ${tool} ${toolArgs.join(' ')}`);
38
+
39
+ execFileSync(tool, toolArgs, { timeout: 10_000, stdio: 'pipe' });
40
+
41
+ if (!fs.existsSync(tmpFile)) {
42
+ throw new Error('Screenshot file was not created');
43
+ }
44
+
45
+ const buffer = await fs.promises.readFile(tmpFile);
46
+ logger.info(`Screenshot captured: ${buffer.length} bytes`);
47
+
48
+ // --- Upload if server info provided ---
49
+ if (serverUrl && deviceId && apiKey) {
50
+ try {
51
+ const uploaded = await uploadScreenshot(
52
+ buffer,
53
+ tmpFile,
54
+ serverUrl,
55
+ deviceId,
56
+ apiKey,
57
+ logger
58
+ );
59
+ return { captured: true, size: buffer.length, uploaded };
60
+ } catch (uploadErr) {
61
+ const errMsg =
62
+ uploadErr instanceof Error ? uploadErr.message : String(uploadErr);
63
+ logger.error('Screenshot upload failed:', errMsg);
64
+ return { captured: true, size: buffer.length, uploaded: false, error: errMsg };
65
+ }
66
+ }
67
+
68
+ return { captured: true, size: buffer.length, uploaded: false };
69
+ } catch (err) {
70
+ const errMsg = err instanceof Error ? err.message : String(err);
71
+ logger.error('Screenshot capture failed:', errMsg);
72
+ throw new Error(`Screenshot capture failed: ${errMsg}`);
73
+ } finally {
74
+ // Clean up temp file
75
+ try {
76
+ if (fs.existsSync(tmpFile)) {
77
+ await fs.promises.unlink(tmpFile);
78
+ }
79
+ } catch {
80
+ // Ignore cleanup errors
81
+ }
82
+ }
83
+ });
84
+ }
85
+
86
+ /**
87
+ * Determine the capture tool and arguments based on platform.
88
+ * No user-controlled command strings — only allowlisted tools with safe args.
89
+ */
90
+ function resolveCapture(
91
+ quality: number,
92
+ tmpFile: string
93
+ ): { tool: string; toolArgs: string[] } {
94
+ const platform = getPlatform();
95
+
96
+ if (platform === 'linux') {
97
+ if (commandExists('scrot')) {
98
+ return { tool: 'scrot', toolArgs: ['-q', String(quality), tmpFile] };
99
+ }
100
+ if (commandExists('import')) {
101
+ return { tool: 'import', toolArgs: ['-window', 'root', '-quality', String(quality), tmpFile] };
102
+ }
103
+ throw new Error(
104
+ 'No screenshot tool available. Install scrot or ImageMagick (import).'
105
+ );
106
+ }
107
+
108
+ if (platform === 'darwin') {
109
+ return { tool: 'screencapture', toolArgs: ['-x', '-t', 'jpg', tmpFile] };
110
+ }
111
+
112
+ throw new Error(`Screenshot capture not supported on platform: ${platform}`);
113
+ }
114
+
115
+ /**
116
+ * Check if a command exists on the system using execFileSync (no shell injection).
117
+ */
118
+ function commandExists(cmd: string): boolean {
119
+ if (!ALLOWED_CAPTURE_TOOLS.includes(cmd as typeof ALLOWED_CAPTURE_TOOLS[number])) {
120
+ return false;
121
+ }
122
+ try {
123
+ execFileSync('which', [cmd], { stdio: 'pipe' });
124
+ return true;
125
+ } catch {
126
+ return false;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Upload screenshot to server via multipart/form-data.
132
+ */
133
+ async function uploadScreenshot(
134
+ buffer: Buffer,
135
+ filePath: string,
136
+ serverUrl: string,
137
+ deviceId: string,
138
+ apiKey: string,
139
+ logger: Logger
140
+ ): Promise<boolean> {
141
+ const endpoint = `/api/devices/${deviceId}/screenshot`;
142
+ const url = `${serverUrl}${endpoint}`;
143
+ const filename = path.basename(filePath);
144
+
145
+ logger.info(`Uploading screenshot to ${url}`);
146
+
147
+ const blob = new Blob([buffer], { type: 'image/jpeg' });
148
+ const formData = new FormData();
149
+ formData.append('screenshot', blob, filename);
150
+
151
+ const response = await fetch(url, {
152
+ method: 'POST',
153
+ headers: {
154
+ 'x-api-key': apiKey,
155
+ },
156
+ body: formData,
157
+ });
158
+
159
+ if (!response.ok) {
160
+ const body = await response.text().catch(() => '');
161
+ throw new Error(`Upload failed: HTTP ${response.status} ${body}`);
162
+ }
163
+
164
+ logger.info('Screenshot uploaded successfully');
165
+ return true;
166
+ }
@@ -1,17 +1,17 @@
1
- import type { CommandHandler } from '../lib/types.js';
2
- import type { Logger } from '../lib/logger.js';
3
-
4
- // --- Register Serial Commands ---
5
-
6
- export function registerSerialCommands(
7
- register: (command: string, handler: CommandHandler) => void,
8
- logger: Logger
9
- ): void {
10
- // serial:close — placeholder
11
- register('serial:close', async (args) => {
12
- const port = args?.port as string;
13
- if (!port) throw new Error('Port path is required');
14
- logger.info(`serial:close requested for ${port}`);
15
- return { status: 'acknowledged', port };
16
- });
17
- }
1
+ import type { CommandHandler } from '../lib/types.js';
2
+ import type { Logger } from '../lib/logger.js';
3
+
4
+ // --- Register Serial Commands ---
5
+
6
+ export function registerSerialCommands(
7
+ register: (command: string, handler: CommandHandler) => void,
8
+ logger: Logger
9
+ ): void {
10
+ // serial:close — placeholder
11
+ register('serial:close', async (args) => {
12
+ const port = args?.port as string;
13
+ if (!port) throw new Error('Port path is required');
14
+ logger.info(`serial:close requested for ${port}`);
15
+ return { status: 'acknowledged', port };
16
+ });
17
+ }