@2018kguo/jumpseat 0.0.0-stage → 0.1.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.
package/README.md CHANGED
@@ -1,3 +1,21 @@
1
- # Temporary Holding Version
1
+ # @2018kguo/jumpseat
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The browser client for [jumpseat](https://github.com/2018kguo/jumpseat): watch a
4
+ computer-use agent's Linux desktop live (H.264 over one websocket, decoded with
5
+ WebCodecs) and take over when it needs you. No dependencies.
6
+
7
+ ```ts
8
+ import { connect, supportsStream } from "@2018kguo/jumpseat";
9
+
10
+ if (!(await supportsStream())) showFallback(); // no WebCodecs H.264 decoder
11
+ const view = connect(element, `wss://host/stream?token=${token}`, {
12
+ connected: () => {},
13
+ dropped: () => {},
14
+ status: ({ kind, text }) => {}, // "acting" | "waiting" | "handoff" | "idle"
15
+ });
16
+ view.setControlling(true); // the person's keys, pointer and scrolls reach the desktop
17
+ ```
18
+
19
+ The server, the agent API and every option are in
20
+ [the repository](https://github.com/2018kguo/jumpseat#readme) and its
21
+ [reference](https://github.com/2018kguo/jumpseat/blob/main/docs/reference.md).
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The browser end of jumpseat: H.264 frames decoded with
3
+ * WebCodecs onto a canvas, and the user's input sent back while they have
4
+ * taken over. The wire format is in docs/reference.md.
5
+ */
6
+ /** The desktop's size until the server says otherwise. */
7
+ export declare const DESKTOP_WIDTH = 1280;
8
+ export declare const DESKTOP_HEIGHT = 800;
9
+ type Size = {
10
+ height: number;
11
+ width: number;
12
+ };
13
+ /** What the agent is doing on the desktop, as the VM reports it. */
14
+ export type AgentStatus = {
15
+ kind: "idle" | "acting" | "waiting" | "handoff";
16
+ text: string;
17
+ };
18
+ export type DesktopStream = {
19
+ close(): void;
20
+ setControlling(controlling: boolean): void;
21
+ };
22
+ type Rect = {
23
+ height: number;
24
+ left: number;
25
+ top: number;
26
+ width: number;
27
+ };
28
+ /**
29
+ * The desktop pixel under a client point, or null outside the picture. The
30
+ * canvas is letterboxed (object-fit: contain), so the picture is the largest
31
+ * box of the desktop's size centered in the element.
32
+ */
33
+ export declare function desktopPoint(rect: Rect, clientX: number, clientY: number, size?: Size): {
34
+ x: number;
35
+ y: number;
36
+ } | null;
37
+ /**
38
+ * The key the VM gets for a KeyboardEvent.key. Meta is Control there, so
39
+ * Cmd+A, Cmd+L and Cmd+C on a Mac do what they do at home.
40
+ */
41
+ export declare function desktopKey(event: Pick<KeyboardEvent, "altKey" | "code" | "ctrlKey" | "key" | "metaKey">): string;
42
+ /** Whether this browser can decode the stream (WebCodecs H.264). */
43
+ export declare function canDecodeDesktop(): boolean;
44
+ /** The codec the server sends: H.264 High profile (CABAC, no B-frames). */
45
+ export declare const STREAM_CODEC = "avc1.640029";
46
+ /**
47
+ * Whether this browser decodes the stream: WebCodecs exists and has an H.264
48
+ * decoder (some Linux Chromium builds ship without one).
49
+ */
50
+ export declare function supportsStream(): Promise<boolean>;
51
+ export declare function connectDesktopStream(container: HTMLElement, socketUrl: string, on: {
52
+ connected: () => void;
53
+ dropped: () => void;
54
+ status?: (status: AgentStatus) => void;
55
+ /** The browser cannot decode H.264: the stream stops instead of dropping. */
56
+ unsupported?: () => void;
57
+ },
58
+ /**
59
+ * resize: ask the desktop to take the element's size, in CSS pixels, as it
60
+ * changes. The server honors it only with --resize.
61
+ */
62
+ options?: {
63
+ resize?: boolean;
64
+ }): DesktopStream;
65
+ /** connect(container, url, handlers): the same as connectDesktopStream. */
66
+ export declare const connect: typeof connectDesktopStream;
67
+ export {};
@@ -0,0 +1,462 @@
1
+ // jumpseat.ts
2
+ var DESKTOP_WIDTH = 1280;
3
+ var DESKTOP_HEIGHT = 800;
4
+ function desktopPoint(rect, clientX, clientY, size = { height: DESKTOP_HEIGHT, width: DESKTOP_WIDTH }) {
5
+ const scale = Math.min(rect.width / size.width, rect.height / size.height);
6
+ if (scale <= 0) {
7
+ return null;
8
+ }
9
+ const left = rect.left + (rect.width - size.width * scale) / 2;
10
+ const top = rect.top + (rect.height - size.height * scale) / 2;
11
+ const x = Math.floor((clientX - left) / scale);
12
+ const y = Math.floor((clientY - top) / scale);
13
+ return x >= 0 && y >= 0 && x < size.width && y < size.height ? { x, y } : null;
14
+ }
15
+ function desktopKey(event) {
16
+ if (event.key === "Meta") {
17
+ return "Control";
18
+ }
19
+ if ((event.ctrlKey || event.metaKey) && !event.altKey && event.key.length === 1 && event.key.charCodeAt(0) > 127) {
20
+ const latin = /^(?:Key([A-Z])|Digit([0-9]))$/.exec(event.code);
21
+ if (latin) {
22
+ return (latin[1] ?? latin[2] ?? "").toLowerCase();
23
+ }
24
+ }
25
+ return event.key;
26
+ }
27
+ var STATUS_KINDS = new Set(["idle", "acting", "waiting", "handoff"]);
28
+ function parseStreamText(raw) {
29
+ let value;
30
+ try {
31
+ value = JSON.parse(raw);
32
+ } catch {
33
+ return null;
34
+ }
35
+ if (typeof value !== "object" || value === null) {
36
+ return null;
37
+ }
38
+ const m = value;
39
+ const num = (v) => typeof v === "number" && Number.isFinite(v);
40
+ const str = (v) => typeof v === "string";
41
+ if (m.t === "p" && num(m.x) && num(m.y)) {
42
+ return { t: "p", x: m.x, y: m.y };
43
+ }
44
+ if (m.t === "c" && str(m.text)) {
45
+ return { t: "c", text: m.text };
46
+ }
47
+ if (m.t === "status" && STATUS_KINDS.has(m.kind) && str(m.text)) {
48
+ return { t: "status", kind: m.kind, text: m.text };
49
+ }
50
+ if (m.t === "size" && num(m.w) && num(m.h) && m.w > 0 && m.h > 0) {
51
+ return { t: "size", w: m.w, h: m.h };
52
+ }
53
+ if (m.t === "cursor" && str(m.png) && num(m.x) && num(m.y)) {
54
+ return { t: "cursor", png: m.png, x: m.x, y: m.y };
55
+ }
56
+ return null;
57
+ }
58
+ var HEARTBEAT_MS = 5000;
59
+ var FIRST_FRAME_MS = 20000;
60
+ var X_BUTTONS = { 0: 1, 1: 2, 2: 3 };
61
+ var POINTER_ARROW = `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='20'%3E%3Cpath d='M1 1v15l4-4 3 7 3-1-3-7h6z' fill='white' stroke='black'/%3E%3C/svg%3E")`;
62
+ function canDecodeDesktop() {
63
+ return typeof window !== "undefined" && "VideoDecoder" in window;
64
+ }
65
+ var STREAM_CODEC = "avc1.640029";
66
+ async function supportsStream() {
67
+ if (!canDecodeDesktop()) {
68
+ return false;
69
+ }
70
+ try {
71
+ return (await VideoDecoder.isConfigSupported({ codec: STREAM_CODEC })).supported === true;
72
+ } catch {
73
+ return false;
74
+ }
75
+ }
76
+ function connectDesktopStream(container, socketUrl, on, options = {}) {
77
+ const canvas = document.createElement("canvas");
78
+ const size = { height: DESKTOP_HEIGHT, width: DESKTOP_WIDTH };
79
+ canvas.width = size.width;
80
+ canvas.height = size.height;
81
+ canvas.tabIndex = 0;
82
+ canvas.style.cssText = "width:100%;height:100%;object-fit:contain;outline:none;display:block";
83
+ const pointer = document.createElement("div");
84
+ pointer.style.cssText = `position:absolute;width:16px;height:20px;pointer-events:none;display:none;background:${POINTER_ARROW} no-repeat;background-size:contain`;
85
+ let cursor = { css: "default", height: 20, width: 16, x: 0, y: 0 };
86
+ const label = document.createElement("span");
87
+ label.textContent = "Agent";
88
+ label.style.cssText = "position:absolute;left:100%;top:100%;padding:1px 6px;border-radius:9999px;background:#4f46e5;color:#fff;font:600 11px/16px system-ui,sans-serif;white-space:nowrap";
89
+ pointer.append(label);
90
+ container.style.position = "relative";
91
+ container.append(canvas, pointer);
92
+ const context = canvas.getContext("2d");
93
+ let closed = false;
94
+ let controlling = false;
95
+ let configured = false;
96
+ let connected = false;
97
+ let copiedAt = 0;
98
+ const decoder = new VideoDecoder({
99
+ output: (frame) => {
100
+ if (canvas.width !== frame.displayWidth || canvas.height !== frame.displayHeight) {
101
+ canvas.width = frame.displayWidth;
102
+ canvas.height = frame.displayHeight;
103
+ }
104
+ context?.drawImage(frame, 0, 0);
105
+ sendAlways({ t: "a", ts: Math.round(frame.timestamp / 1000) });
106
+ frame.close();
107
+ },
108
+ error: () => drop()
109
+ });
110
+ const socket = new WebSocket(socketUrl);
111
+ socket.binaryType = "arraybuffer";
112
+ async function checkDecoderSupport() {
113
+ if (!await supportsStream() && !closed) {
114
+ closed = true;
115
+ socket.close();
116
+ (on.unsupported ?? on.dropped)();
117
+ }
118
+ }
119
+ function drop() {
120
+ if (!closed) {
121
+ closed = true;
122
+ clearInterval(heartbeat);
123
+ clearTimeout(firstFrame);
124
+ resizer?.disconnect();
125
+ socket.close();
126
+ on.dropped();
127
+ }
128
+ }
129
+ const sendAlways = (message) => {
130
+ if (socket.readyState === WebSocket.OPEN) {
131
+ socket.send(JSON.stringify(message));
132
+ }
133
+ };
134
+ const send = (message) => {
135
+ if (controlling) {
136
+ sendAlways(message);
137
+ }
138
+ };
139
+ const heartbeat = setInterval(() => sendAlways({ t: "h" }), HEARTBEAT_MS);
140
+ let firstFrame = setTimeout(drop, FIRST_FRAME_MS);
141
+ socket.addEventListener("message", (event) => {
142
+ if (typeof event.data === "string") {
143
+ const message = parseStreamText(event.data);
144
+ if (!message) {
145
+ return;
146
+ }
147
+ if (message.t === "size") {
148
+ size.width = message.w;
149
+ size.height = message.h;
150
+ configured = false;
151
+ } else if (message.t === "cursor") {
152
+ const url = `url(data:image/png;base64,${message.png})`;
153
+ cursor = {
154
+ css: `${url} ${message.x} ${message.y}, default`,
155
+ height: cursor.height,
156
+ width: cursor.width,
157
+ x: message.x,
158
+ y: message.y
159
+ };
160
+ pointer.style.background = `${url} no-repeat`;
161
+ pointer.style.backgroundSize = "contain";
162
+ const image = new Image;
163
+ image.addEventListener("load", () => {
164
+ cursor.width = image.width;
165
+ cursor.height = image.height;
166
+ });
167
+ image.src = `data:image/png;base64,${message.png}`;
168
+ if (controlling) {
169
+ canvas.style.cursor = cursor.css;
170
+ }
171
+ } else if (message.t === "status") {
172
+ on.status?.({ kind: message.kind, text: message.text });
173
+ } else if (message.t === "p") {
174
+ const rect = canvas.getBoundingClientRect();
175
+ const scale = Math.min(rect.width / size.width, rect.height / size.height);
176
+ pointer.style.left = `${(rect.width - size.width * scale) / 2 + (message.x - cursor.x) * scale}px`;
177
+ pointer.style.top = `${(rect.height - size.height * scale) / 2 + (message.y - cursor.y) * scale}px`;
178
+ pointer.style.width = `${cursor.width * scale}px`;
179
+ pointer.style.height = `${cursor.height * scale}px`;
180
+ pointer.style.display = controlling ? "none" : "block";
181
+ } else if (controlling && performance.now() - copiedAt < 2000) {
182
+ navigator.clipboard?.writeText(message.text).catch(() => {});
183
+ }
184
+ return;
185
+ }
186
+ if (!(event.data instanceof ArrayBuffer)) {
187
+ return;
188
+ }
189
+ const data = new Uint8Array(event.data);
190
+ const key = data[0] === 1;
191
+ const stamp = Number(new DataView(event.data).getBigUint64(1));
192
+ if (!configured) {
193
+ if (!key) {
194
+ return;
195
+ }
196
+ checkDecoderSupport();
197
+ decoder.configure({
198
+ codec: STREAM_CODEC,
199
+ optimizeForLatency: true
200
+ });
201
+ configured = true;
202
+ }
203
+ decoder.decode(new EncodedVideoChunk({
204
+ data: data.subarray(9),
205
+ timestamp: stamp * 1000,
206
+ type: key ? "key" : "delta"
207
+ }));
208
+ if (!connected) {
209
+ connected = true;
210
+ clearTimeout(firstFrame);
211
+ on.connected();
212
+ }
213
+ });
214
+ socket.addEventListener("close", drop);
215
+ let resizeTimer;
216
+ const askSize = () => {
217
+ const { width, height } = container.getBoundingClientRect();
218
+ sendAlways({ t: "z", w: Math.round(width), h: Math.round(height) });
219
+ };
220
+ const resizer = options.resize ? new ResizeObserver(() => {
221
+ clearTimeout(resizeTimer);
222
+ resizeTimer = setTimeout(askSize, 200);
223
+ }) : null;
224
+ resizer?.observe(container);
225
+ if (resizer) {
226
+ socket.addEventListener("open", askSize);
227
+ }
228
+ socket.addEventListener("error", drop);
229
+ const at = (event) => desktopPoint(canvas.getBoundingClientRect(), event.clientX, event.clientY, size);
230
+ canvas.addEventListener("pointermove", (event) => {
231
+ if (event.pointerType === "touch") {
232
+ return;
233
+ }
234
+ const point = at(event);
235
+ if (point) {
236
+ send({ t: "m", ...point });
237
+ }
238
+ });
239
+ const onButton = (down) => (event) => {
240
+ const button = X_BUTTONS[event.button];
241
+ if (!controlling || button === undefined || event.pointerType === "touch") {
242
+ return;
243
+ }
244
+ const rect = canvas.getBoundingClientRect();
245
+ const point = at(event) ?? (down ? null : desktopPoint(rect, Math.min(Math.max(event.clientX, rect.left), rect.right - 1), Math.min(Math.max(event.clientY, rect.top), rect.bottom - 1), size));
246
+ if (!point) {
247
+ return;
248
+ }
249
+ if (down) {
250
+ canvas.focus();
251
+ canvas.setPointerCapture(event.pointerId);
252
+ }
253
+ send({ t: "b", b: button, down, ...point });
254
+ event.preventDefault();
255
+ };
256
+ canvas.addEventListener("pointerdown", onButton(true));
257
+ canvas.addEventListener("pointerup", onButton(false));
258
+ attachTouch(canvas, {
259
+ at,
260
+ controlling: () => controlling,
261
+ send
262
+ });
263
+ canvas.addEventListener("wheel", (event) => {
264
+ if (!controlling) {
265
+ return;
266
+ }
267
+ const unit = (event.deltaMode === 1 ? 40 : event.deltaMode === 2 ? 800 : 1) * desktopPerCssPixel(canvas);
268
+ send({ t: "w", sx: event.deltaX * unit, sy: event.deltaY * unit });
269
+ event.preventDefault();
270
+ }, { passive: false });
271
+ canvas.addEventListener("contextmenu", (event) => {
272
+ if (controlling) {
273
+ event.preventDefault();
274
+ }
275
+ });
276
+ const onKey = (down) => (event) => {
277
+ if (!controlling) {
278
+ return;
279
+ }
280
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "v") {
281
+ return;
282
+ }
283
+ if (event.repeat) {
284
+ event.preventDefault();
285
+ return;
286
+ }
287
+ if (down && event.key === "AltGraph") {
288
+ send({ t: "k", key: "Control", down: false });
289
+ }
290
+ if (down && (event.metaKey || event.ctrlKey) && /^[cx]$/i.test(event.key)) {
291
+ copiedAt = performance.now();
292
+ }
293
+ send({ t: "k", key: desktopKey(event), down });
294
+ if (!down && event.key === "Meta") {
295
+ send({ t: "r" });
296
+ }
297
+ event.preventDefault();
298
+ };
299
+ canvas.addEventListener("keydown", onKey(true));
300
+ canvas.addEventListener("keyup", onKey(false));
301
+ canvas.addEventListener("blur", () => send({ t: "r" }));
302
+ const onPaste = (event) => {
303
+ const text = event.clipboardData?.getData("text/plain");
304
+ if (!controlling || document.activeElement !== canvas || !text || text.length > 64 * 1024) {
305
+ return;
306
+ }
307
+ send({ t: "c", text });
308
+ for (const [key, down] of [
309
+ ["Control", true],
310
+ ["v", true],
311
+ ["v", false],
312
+ ["Control", false]
313
+ ]) {
314
+ send({ t: "k", key, down });
315
+ }
316
+ event.preventDefault();
317
+ };
318
+ document.addEventListener("paste", onPaste);
319
+ const onVisibility = () => {
320
+ sendAlways({ t: "v", hidden: document.hidden });
321
+ clearTimeout(firstFrame);
322
+ if (!document.hidden && !connected && !closed) {
323
+ firstFrame = setTimeout(drop, FIRST_FRAME_MS);
324
+ }
325
+ };
326
+ document.addEventListener("visibilitychange", onVisibility);
327
+ socket.addEventListener("open", () => {
328
+ if (document.hidden) {
329
+ onVisibility();
330
+ }
331
+ });
332
+ return {
333
+ close() {
334
+ closed = true;
335
+ clearInterval(heartbeat);
336
+ clearTimeout(firstFrame);
337
+ resizer?.disconnect();
338
+ socket.close();
339
+ if (decoder.state !== "closed") {
340
+ decoder.close();
341
+ }
342
+ document.removeEventListener("paste", onPaste);
343
+ document.removeEventListener("visibilitychange", onVisibility);
344
+ canvas.remove();
345
+ pointer.remove();
346
+ },
347
+ setControlling(next) {
348
+ if (!next) {
349
+ send({ t: "r" });
350
+ }
351
+ controlling = next;
352
+ pointer.style.display = "none";
353
+ canvas.style.cursor = next ? cursor.css : "default";
354
+ if (next) {
355
+ canvas.focus();
356
+ }
357
+ }
358
+ };
359
+ }
360
+ var LONG_PRESS_MS = 600;
361
+ var TAP_SLOP_PX = 10;
362
+ function attachTouch(canvas, view) {
363
+ canvas.style.touchAction = "none";
364
+ const fingers = new Map;
365
+ let start = null;
366
+ let pressed = false;
367
+ let longPress;
368
+ const reset = () => {
369
+ clearTimeout(longPress);
370
+ start = null;
371
+ };
372
+ canvas.addEventListener("pointerdown", (event) => {
373
+ if (event.pointerType !== "touch" || !view.controlling()) {
374
+ return;
375
+ }
376
+ event.preventDefault();
377
+ fingers.set(event.pointerId, { x: event.clientX, y: event.clientY });
378
+ if (fingers.size > 1) {
379
+ if (pressed && start) {
380
+ view.send({ t: "b", b: 1, down: false, x: start.x, y: start.y });
381
+ pressed = false;
382
+ }
383
+ reset();
384
+ return;
385
+ }
386
+ const point = view.at(event);
387
+ if (!point) {
388
+ return;
389
+ }
390
+ canvas.focus();
391
+ start = { clientX: event.clientX, clientY: event.clientY, ...point };
392
+ longPress = setTimeout(() => {
393
+ if (start && !pressed) {
394
+ view.send({ t: "b", b: 3, down: true, x: start.x, y: start.y });
395
+ view.send({ t: "b", b: 3, down: false, x: start.x, y: start.y });
396
+ start = null;
397
+ }
398
+ }, LONG_PRESS_MS);
399
+ });
400
+ canvas.addEventListener("pointermove", (event) => {
401
+ const last = fingers.get(event.pointerId);
402
+ if (event.pointerType !== "touch" || !last || !view.controlling()) {
403
+ return;
404
+ }
405
+ fingers.set(event.pointerId, { x: event.clientX, y: event.clientY });
406
+ if (fingers.size > 1) {
407
+ const unit = desktopPerCssPixel(canvas);
408
+ view.send({
409
+ t: "w",
410
+ sx: (last.x - event.clientX) * unit,
411
+ sy: (last.y - event.clientY) * unit
412
+ });
413
+ return;
414
+ }
415
+ if (!start) {
416
+ return;
417
+ }
418
+ const point = view.at(event);
419
+ if (!pressed && Math.hypot(event.clientX - start.clientX, event.clientY - start.clientY) > TAP_SLOP_PX) {
420
+ clearTimeout(longPress);
421
+ view.send({ t: "b", b: 1, down: true, x: start.x, y: start.y });
422
+ pressed = true;
423
+ }
424
+ if (pressed && point) {
425
+ view.send({ t: "m", ...point });
426
+ }
427
+ });
428
+ const lift = (event) => {
429
+ if (event.pointerType !== "touch" || !fingers.delete(event.pointerId)) {
430
+ return;
431
+ }
432
+ if (start && fingers.size === 0) {
433
+ const point = view.at(event) ?? start;
434
+ if (!pressed && event.type === "pointerup") {
435
+ view.send({ t: "b", b: 1, down: true, x: start.x, y: start.y });
436
+ view.send({ t: "b", b: 1, down: false, ...point });
437
+ } else if (pressed) {
438
+ view.send({ t: "b", b: 1, down: false, ...point });
439
+ }
440
+ }
441
+ pressed = false;
442
+ reset();
443
+ };
444
+ canvas.addEventListener("pointerup", lift);
445
+ canvas.addEventListener("pointercancel", lift);
446
+ }
447
+ function desktopPerCssPixel(canvas) {
448
+ const rect = canvas.getBoundingClientRect();
449
+ return 1 / Math.min(rect.width / canvas.width, rect.height / canvas.height);
450
+ }
451
+ var connect = connectDesktopStream;
452
+ export {
453
+ DESKTOP_HEIGHT,
454
+ DESKTOP_WIDTH,
455
+ STREAM_CODEC,
456
+ canDecodeDesktop,
457
+ connect,
458
+ connectDesktopStream,
459
+ desktopKey,
460
+ desktopPoint,
461
+ supportsStream
462
+ };
package/package.json CHANGED
@@ -1,6 +1,39 @@
1
1
  {
2
2
  "name": "@2018kguo/jumpseat",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Watch and take over a computer-use agent's desktop: the browser client for jumpseat (H.264 over a websocket, WebCodecs).",
5
+ "keywords": [
6
+ "computer-use",
7
+ "ai-agents",
8
+ "remote-desktop",
9
+ "webcodecs",
10
+ "h264",
11
+ "websocket",
12
+ "human-in-the-loop"
13
+ ],
14
+ "homepage": "https://github.com/2018kguo/jumpseat#readme",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/2018kguo/jumpseat.git",
18
+ "directory": "client"
19
+ },
20
+ "license": "MIT",
21
+ "type": "module",
22
+ "main": "dist/jumpseat.js",
23
+ "types": "dist/jumpseat.d.ts",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/jumpseat.d.ts",
27
+ "default": "./dist/jumpseat.js"
28
+ }
29
+ },
30
+ "sideEffects": false,
31
+ "files": [
32
+ "dist",
33
+ "README.md"
34
+ ],
35
+ "scripts": {
36
+ "build": "bun build jumpseat.ts --format=esm --target=browser --outfile dist/jumpseat.js && bunx tsc jumpseat.ts --declaration --emitDeclarationOnly --target es2022 --lib es2022,dom --outDir dist",
37
+ "build:demo": "bun build jumpseat.ts --format=esm --target=browser --outfile ../server/web/jumpseat.js"
38
+ }
39
+ }