obsbot-mcp 0.7.0 → 0.9.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 (56) hide show
  1. package/README.md +111 -20
  2. package/dist/build-info.json +5 -0
  3. package/dist/device/manager.d.ts +8 -0
  4. package/dist/device/manager.js +37 -4
  5. package/dist/device/manager.js.map +1 -1
  6. package/dist/geometry/aim.d.ts +24 -13
  7. package/dist/geometry/aim.js +26 -13
  8. package/dist/geometry/aim.js.map +1 -1
  9. package/dist/ipc/build-id.d.ts +25 -0
  10. package/dist/ipc/build-id.js +60 -0
  11. package/dist/ipc/build-id.js.map +1 -0
  12. package/dist/ipc/client.d.ts +36 -0
  13. package/dist/ipc/client.js +106 -4
  14. package/dist/ipc/client.js.map +1 -1
  15. package/dist/ipc/coordinator.d.ts +91 -12
  16. package/dist/ipc/coordinator.js +293 -57
  17. package/dist/ipc/coordinator.js.map +1 -1
  18. package/dist/ipc/gate.d.ts +14 -0
  19. package/dist/ipc/gate.js +45 -0
  20. package/dist/ipc/gate.js.map +1 -0
  21. package/dist/ipc/owner.d.ts +27 -1
  22. package/dist/ipc/owner.js +67 -4
  23. package/dist/ipc/owner.js.map +1 -1
  24. package/dist/ipc/protocol.d.ts +30 -0
  25. package/dist/ipc/protocol.js +40 -0
  26. package/dist/ipc/protocol.js.map +1 -1
  27. package/dist/ipc/rendezvous.d.ts +37 -11
  28. package/dist/ipc/rendezvous.js +143 -30
  29. package/dist/ipc/rendezvous.js.map +1 -1
  30. package/dist/mcp/server.js +40 -12
  31. package/dist/mcp/server.js.map +1 -1
  32. package/dist/tail2/api.d.ts +381 -0
  33. package/dist/tail2/api.js +547 -0
  34. package/dist/tail2/api.js.map +1 -0
  35. package/dist/tail2/mdns.d.ts +48 -0
  36. package/dist/tail2/mdns.js +216 -0
  37. package/dist/tail2/mdns.js.map +1 -0
  38. package/dist/tail2/move.d.ts +41 -0
  39. package/dist/tail2/move.js +79 -0
  40. package/dist/tail2/move.js.map +1 -0
  41. package/dist/tail2/registry.d.ts +100 -0
  42. package/dist/tail2/registry.js +200 -0
  43. package/dist/tail2/registry.js.map +1 -0
  44. package/dist/tail2/snapshot.d.ts +54 -0
  45. package/dist/tail2/snapshot.js +157 -0
  46. package/dist/tail2/snapshot.js.map +1 -0
  47. package/dist/tail2/tools.d.ts +7 -0
  48. package/dist/tail2/tools.js +772 -0
  49. package/dist/tail2/tools.js.map +1 -0
  50. package/dist/version.d.ts +1 -1
  51. package/dist/version.js +1 -1
  52. package/native/prebuilt/darwin-arm64/obsbot-helper +0 -0
  53. package/native/prebuilt/darwin-x64/obsbot-helper +0 -0
  54. package/native/prebuilt/linux-x64/obsbot-helper +0 -0
  55. package/native/prebuilt/win32-x64/obsbot-helper.exe +0 -0
  56. package/package.json +4 -1
@@ -0,0 +1,381 @@
1
+ /**
2
+ * HTTP/WS client for the OBSBOT Tail 2 control API.
3
+ *
4
+ * The Tail 2's network control plane is a lighttpd REST API plus a WebSocket
5
+ * status push (see TAIL2-PROTOCOL.md). Its USB-C UVC mode has a control
6
+ * surface of its own (§11) that this client does not use.
7
+ * Everything here is plain HTTP and therefore platform-independent: the same
8
+ * code runs on Windows, Linux and macOS with no native helper, which is the
9
+ * whole reason this module exists as a separate family from the Tiny 2's
10
+ * helper-process transports.
11
+ *
12
+ * Identity is the camera's MAC (what OBSBOT Center keys on); device_name and
13
+ * any of its IPs are accepted as selectors one level up, in the registry.
14
+ */
15
+ /** `GET /camera/sdk/device_info` — the probe target and identity source. */
16
+ export interface Tail2DeviceInfo {
17
+ device_name: string;
18
+ wired_ip: string;
19
+ wireless_ip: string;
20
+ mac: string;
21
+ }
22
+ /**
23
+ * One push from `ws://<host>/ws/`. The camera sends the FULL status block
24
+ * ~1 Hz with no subscription message. Fields the tools rely on are typed;
25
+ * the block carries more (and firmware may add some) — unknown fields pass
26
+ * through untouched via the index signature.
27
+ *
28
+ * MEASURED 2026-09-26 against firmware 7.2.13.1; see TAIL2-PROTOCOL.md §4.
29
+ */
30
+ export interface Tail2Status {
31
+ [key: string]: unknown;
32
+ power_on?: boolean;
33
+ usb_mode?: number;
34
+ rec?: boolean;
35
+ rec_time?: number;
36
+ switch_portrait?: boolean;
37
+ ai_mode?: string;
38
+ zoom_ratio?: number;
39
+ roll_bias?: number;
40
+ focus_mode?: string;
41
+ auto_focus_mode?: string;
42
+ ndi?: {
43
+ enable?: boolean;
44
+ };
45
+ rtsp?: {
46
+ enable?: boolean;
47
+ };
48
+ srt?: {
49
+ enable?: boolean;
50
+ };
51
+ rtmp?: {
52
+ enable?: boolean;
53
+ };
54
+ preset_info?: Array<{
55
+ id: number;
56
+ pitch: number;
57
+ yaw: number;
58
+ roll: number;
59
+ ratio: number;
60
+ name: string;
61
+ }>;
62
+ device_status?: Record<string, unknown>;
63
+ }
64
+ /** A saved gimbal pose: degrees + zoom ratio. Names are base64 on the wire only. */
65
+ export interface Preset {
66
+ id: number;
67
+ pitch: number;
68
+ yaw: number;
69
+ roll: number;
70
+ ratio: number;
71
+ name: string;
72
+ }
73
+ /** A live gimbal pose in degrees, plus the zoom ratio the pose read carries. */
74
+ export interface Pose {
75
+ yaw: number;
76
+ pitch: number;
77
+ roll: number;
78
+ ratio: number;
79
+ }
80
+ export declare class Tail2HttpError extends Error {
81
+ readonly status: number;
82
+ readonly path: string;
83
+ readonly body: string;
84
+ constructor(status: number, path: string, body: string);
85
+ }
86
+ /**
87
+ * `{"code":200,"err_idx":0}` means ACKNOWLEDGED, not applied — MEASURED
88
+ * 2026-09-26: a rollbias write returned 200 while the portrait motor was in
89
+ * motion and never landed (TAIL2-PROTOCOL.md §8.1). This is the same failure
90
+ * class as the Tiny 2's post-replug vendor mailbox, so every setter here
91
+ * verifies by readback before reporting success, with a bounded retry ladder
92
+ * sized for the actuator involved (the zoom motor and the portrait rotation
93
+ * take seconds, not milliseconds).
94
+ */
95
+ export interface VerifyOpts {
96
+ attempts?: number;
97
+ delayMs?: number;
98
+ }
99
+ export declare class Tail2Api {
100
+ readonly baseUrl: string;
101
+ private readonly timeoutMs;
102
+ private readonly wsUrl;
103
+ constructor(opts: {
104
+ baseUrl: string;
105
+ timeoutMs?: number;
106
+ });
107
+ private req;
108
+ /**
109
+ * PUT/POST with ack checking. `body` may be an object (JSON.stringify'd)
110
+ * or a pre-built JSON string — evbias needs a float literal like `0.0`
111
+ * that stringifying a number cannot produce (measured 2026-09-30: JSON
112
+ * integers 0 and -1 get 400 "Invalid value type"; 0.0 and -1.0 apply).
113
+ */
114
+ private send;
115
+ /**
116
+ * Read `read()` until `match()` holds, on a bounded ladder. Returns
117
+ * `settled:false` (NOT a throw) when the ladder runs out — the command was
118
+ * sent and acknowledged, so the caller decides whether that is failure.
119
+ * Mirrors `obsbot_zoom_uvc`'s `settled` contract.
120
+ */
121
+ verified<T>(read: () => Promise<T>, match: (v: T) => boolean, opts?: VerifyOpts): Promise<{
122
+ settled: boolean;
123
+ value: T;
124
+ }>;
125
+ info(): Promise<Tail2DeviceInfo>;
126
+ /** One-shot status: connect, take the first push, close. No persistent socket. */
127
+ status(): Promise<Tail2Status>;
128
+ ranges(): Promise<Record<string, unknown>>;
129
+ networkConfig(): Promise<Record<string, unknown>>;
130
+ imageState(): Promise<Record<string, unknown>>;
131
+ zoomGet(): Promise<{
132
+ ratio: number;
133
+ }>;
134
+ /**
135
+ * Absolute zoom ratio on the Tail 2's own 1.0–12.0 scale (`GET range`).
136
+ * `speed` is REQUIRED by the firmware (a ratio-only PUT is a 400) — both
137
+ * fields MEASURED 2026-09-26.
138
+ */
139
+ zoomSet(ratio: number, speed: number, verify?: VerifyOpts): Promise<{
140
+ settled: boolean;
141
+ ratio: number;
142
+ }>;
143
+ /** Gimbal recenter (`POST ptz/reset`). Open-loop: the Tail 2 reports no live pose. */
144
+ recenter(): Promise<void>;
145
+ gimbalSpeed(yaw: number, pitch: number, roll: number): Promise<void>;
146
+ gimbalStop(): Promise<void>;
147
+ gimbalInvertGet(): Promise<{
148
+ enable: boolean;
149
+ }>;
150
+ /** Reverse (invert) control directions. Readback-verified like every write. */
151
+ gimbalInvertSet(enable: boolean, verify?: VerifyOpts): Promise<{
152
+ settled: boolean;
153
+ enable: boolean;
154
+ }>;
155
+ /** Marker name for pose-probe scratch slots, so crashed probes are reusable. */
156
+ static readonly POSE_PROBE_NAME = "pose-probe";
157
+ readPose(verify?: VerifyOpts): Promise<Pose>;
158
+ /**
159
+ * Motorized 90° barrel rotation. Verified against the WS push rather than a
160
+ * REST readback (none exists) — and the readback MUST be tolerant of the
161
+ * motor taking ~1.5s, plus the swallowed-write hazard during motion.
162
+ */
163
+ portraitSet(enable: boolean, verify?: VerifyOpts): Promise<{
164
+ settled: boolean;
165
+ switch_portrait: boolean;
166
+ }>;
167
+ rollBiasGet(): Promise<{
168
+ angle: number;
169
+ }>;
170
+ /** Roll trim in degrees. MEASURED write + readback 2026-09-26. */
171
+ rollBiasSet(angle: number, verify?: VerifyOpts): Promise<{
172
+ settled: boolean;
173
+ angle: number;
174
+ }>;
175
+ aiModeGet(): Promise<{
176
+ mode: string;
177
+ }>;
178
+ /**
179
+ * `mode` strings are the camera's own enum (bundle-verified, TAIL2-PROTOCOL
180
+ * §3): none | humanTrackingSingleMode | humanTrackingGroupMode |
181
+ * animalTrackingNormal | animalTrackingCloseUp | objectTracking |
182
+ * objectTrackingNormal | objectTrackingCloseUp.
183
+ */
184
+ aiModeSet(mode: string, verify?: VerifyOpts): Promise<{
185
+ settled: boolean;
186
+ mode: string;
187
+ }>;
188
+ trackSpeedGet(): Promise<{
189
+ speed: string;
190
+ }>;
191
+ /**
192
+ * The Tail 2's own six-speed enum (bundle-verified): superLazy | lazy |
193
+ * slow | fast | crazy | customized. NOT the Tiny 2's standard/sport pair.
194
+ */
195
+ trackSpeedSet(speed: string, verify?: VerifyOpts): Promise<{
196
+ settled: boolean;
197
+ speed: string;
198
+ }>;
199
+ recordGet(): Promise<{
200
+ recording: "on" | "off";
201
+ }>;
202
+ recordSet(on: boolean, verify?: VerifyOpts): Promise<{
203
+ settled: boolean;
204
+ recording: "on" | "off";
205
+ }>;
206
+ /** Still-photo trigger. Needs storage; the ack says the camera accepted it. */
207
+ captureTrigger(): Promise<void>;
208
+ focusModeGet(): Promise<{
209
+ mode: "afc" | "afs" | "mf";
210
+ }>;
211
+ focusModeSet(mode: "afc" | "afs" | "mf", verify?: VerifyOpts): Promise<{
212
+ settled: boolean;
213
+ mode: "afc" | "afs" | "mf";
214
+ }>;
215
+ focusPositionGet(): Promise<{
216
+ position: number;
217
+ }>;
218
+ focusPositionSet(position: number, verify?: VerifyOpts): Promise<{
219
+ settled: boolean;
220
+ position: number;
221
+ }>;
222
+ focusWindowGet(): Promise<{
223
+ x: number;
224
+ y: number;
225
+ }>;
226
+ /** Move the focus window AND start a point focus (per the vendor doc). AFC/AFS only. */
227
+ focusWindowSet(x: number, y: number, verify?: VerifyOpts): Promise<{
228
+ settled: boolean;
229
+ x: number;
230
+ y: number;
231
+ }>;
232
+ /**
233
+ * Tap-to-track (MEASURED 2026-09-30, undocumented in the Tail 2's own doc):
234
+ * POST a normalized coordinate and the camera engages tracking on the
235
+ * subject there — from mode none it armed humanTrackingSingleMode on its
236
+ * own. Fire-and-ack; which mode engages depends on what (if anything) is
237
+ * at the coordinate, so the caller reads ai mode back when it matters.
238
+ */
239
+ targetSelect(x: number, y: number): Promise<void>;
240
+ exposureModeGet(): Promise<{
241
+ mode: "manual" | "auto";
242
+ }>;
243
+ exposureModeSet(mode: "manual" | "auto", verify?: VerifyOpts): Promise<{
244
+ settled: boolean;
245
+ mode: "manual" | "auto";
246
+ }>;
247
+ exposureAutoModeGet(): Promise<{
248
+ mode: "global" | "face";
249
+ }>;
250
+ exposureAutoModeSet(mode: "global" | "face", verify?: VerifyOpts): Promise<{
251
+ settled: boolean;
252
+ mode: "global" | "face";
253
+ }>;
254
+ exposureEvbiasGet(): Promise<{
255
+ evbias: number;
256
+ }>;
257
+ exposureEvbiasSet(evbias: number, verify?: VerifyOpts): Promise<{
258
+ settled: boolean;
259
+ evbias: number;
260
+ }>;
261
+ exposureIsoGet(): Promise<{
262
+ iso: number;
263
+ }>;
264
+ exposureIsoSet(iso: number, verify?: VerifyOpts): Promise<{
265
+ settled: boolean;
266
+ iso: number;
267
+ }>;
268
+ exposureShutterGet(): Promise<{
269
+ shutter: string;
270
+ }>;
271
+ exposureShutterSet(shutter: string, verify?: VerifyOpts): Promise<{
272
+ settled: boolean;
273
+ shutter: string;
274
+ }>;
275
+ styleGet(): Promise<{
276
+ mode: string;
277
+ brightness: number;
278
+ contrast: number;
279
+ hue: number;
280
+ saturation: number;
281
+ sharpness: number;
282
+ }>;
283
+ styleSet(control: "brightness" | "contrast" | "hue" | "saturation" | "sharpness", value: number, verify?: VerifyOpts): Promise<{
284
+ settled: boolean;
285
+ value: number;
286
+ mode: string;
287
+ }>;
288
+ styleModeSet(mode: "standard" | "outdoor" | "pastel" | "manual", verify?: VerifyOpts): Promise<{
289
+ settled: boolean;
290
+ mode: string;
291
+ }>;
292
+ hdrGet(): Promise<{
293
+ control: "on" | "off";
294
+ }>;
295
+ hdrSet(on: boolean, verify?: VerifyOpts): Promise<{
296
+ settled: boolean;
297
+ control: "on" | "off";
298
+ }>;
299
+ wbConfigGet(): Promise<{
300
+ mode: string;
301
+ temperature: number;
302
+ }>;
303
+ wbConfigSet(mode: "auto" | "daylight" | "fluorescent" | "tungsten" | "cloudy" | "manual", temperature: number | undefined, verify?: VerifyOpts): Promise<{
304
+ settled: boolean;
305
+ mode: string;
306
+ temperature: number;
307
+ }>;
308
+ streamControlGet(): Promise<{
309
+ control: "ndi" | "rtsp" | "srt" | "off";
310
+ }>;
311
+ streamControlSet(control: "ndi" | "rtsp" | "srt" | "off", verify?: VerifyOpts): Promise<{
312
+ settled: boolean;
313
+ control: "ndi" | "rtsp" | "srt" | "off";
314
+ }>;
315
+ onlyMeGet(): Promise<{
316
+ enable: boolean;
317
+ }>;
318
+ onlyMeSet(enable: boolean, verify?: VerifyOpts): Promise<{
319
+ settled: boolean;
320
+ enable: boolean;
321
+ }>;
322
+ audioVolumeGet(): Promise<{
323
+ volume: number;
324
+ }>;
325
+ audioVolumeSet(volume: number, verify?: VerifyOpts): Promise<{
326
+ settled: boolean;
327
+ volume: number;
328
+ }>;
329
+ audioMuteGet(): Promise<{
330
+ enable: boolean;
331
+ }>;
332
+ audioMuteSet(enable: boolean, verify?: VerifyOpts): Promise<{
333
+ settled: boolean;
334
+ enable: boolean;
335
+ }>;
336
+ /**
337
+ * A saved pose: angles in DEGREES plus zoom ratio — the same units the
338
+ * tools speak. `name` is base64 on the wire; encode/decode lives here so
339
+ * callers never see it raw.
340
+ */
341
+ presetsGet(): Promise<Preset[]>;
342
+ /**
343
+ * `PUT ptz/preset {"operation": ..., "id": ..., "name": base64}` — one
344
+ * endpoint, four operations (grammar decoded from the camera's own web
345
+ * bundle and hardware-verified 2026-09-26; see TAIL2-PROTOCOL.md §3).
346
+ * IDs are 0-based (0–2); the tools expose 1-based slots for consistency
347
+ * with the Tiny 2's preset tools.
348
+ *
349
+ * `set` saves the CURRENT live pose — there is NO explicit-pose write
350
+ * anywhere in this API (measured), and `set` on an occupied slot
351
+ * OVERWRITES it (unlike the Tiny 2's create-once slots).
352
+ */
353
+ private presetOp;
354
+ /**
355
+ * Save the current live pose into slot `id`. The preset LIST lags the
356
+ * write by up to ~1s (measured), so verification is a ladder, and the
357
+ * pose itself can't be verified at all — the camera reports no live pose,
358
+ * so "the slot exists afterwards" is the honest success criterion.
359
+ */
360
+ presetSave(id: number, name: string, verify?: VerifyOpts): Promise<{
361
+ settled: boolean;
362
+ presets: Preset[];
363
+ }>;
364
+ /**
365
+ * Recall slot `id`. Physical arrival is verifiable through exactly one
366
+ * observable: the preset's zoom ratio landing in the live zoom_ratio (the
367
+ * WS push carries it). The gimbal axes themselves are open-loop.
368
+ */
369
+ presetRecall(id: number, verify?: VerifyOpts): Promise<{
370
+ settled: boolean;
371
+ zoom: number;
372
+ }>;
373
+ presetDelete(id: number, verify?: VerifyOpts): Promise<{
374
+ settled: boolean;
375
+ presets: Preset[];
376
+ }>;
377
+ presetRename(id: number, name: string, verify?: VerifyOpts): Promise<{
378
+ settled: boolean;
379
+ presets: Preset[];
380
+ }>;
381
+ }