replicas-computer 0.2.3 → 0.2.4

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/dist/index.js CHANGED
@@ -18,7 +18,7 @@ import { dirname, isAbsolute, resolve } from "path";
18
18
  var DESKTOP_STREAM_PORT = 6080;
19
19
  function getDesktopViewerUrl(publicUrl) {
20
20
  const url = new URL(`${publicUrl.replace(/\/$/, "")}/`);
21
- url.searchParams.set("v", "14");
21
+ url.searchParams.set("v", "15");
22
22
  return url.toString();
23
23
  }
24
24
  var DESKTOP_VIEWER_WIDTH = 1920;
@@ -2921,7 +2921,7 @@ async function computerStatusCommand() {
2921
2921
  }
2922
2922
 
2923
2923
  // src/version.ts
2924
- var COMPUTER_USE_VERSION = "0.2.3";
2924
+ var COMPUTER_USE_VERSION = "0.2.4";
2925
2925
  var COMPUTER_USE_PROTOCOL_VERSION = 1;
2926
2926
 
2927
2927
  // src/program.ts
@@ -3357,7 +3357,16 @@ var RtpClock = class {
3357
3357
  base;
3358
3358
  last;
3359
3359
  wraps = 0;
3360
+ origin = 0;
3361
+ emitted = 0;
3362
+ /**
3363
+ * A fresh encoder starts RTP timestamps from a new random base, so the
3364
+ * timeline continues from the last value emitted rather than jumping
3365
+ * backwards — decoders and the viewer's media element both require
3366
+ * monotonic presentation timestamps.
3367
+ */
3360
3368
  reset() {
3369
+ this.origin = this.emitted;
3361
3370
  this.base = void 0;
3362
3371
  this.last = void 0;
3363
3372
  this.wraps = 0;
@@ -3367,7 +3376,8 @@ var RtpClock = class {
3367
3376
  this.last = timestamp;
3368
3377
  const extended = timestamp + this.wraps * 4294967296;
3369
3378
  this.base ??= extended;
3370
- return Math.round((extended - this.base) * 1e6 / this.rate);
3379
+ this.emitted = this.origin + Math.round((extended - this.base) * 1e6 / this.rate);
3380
+ return this.emitted;
3371
3381
  }
3372
3382
  };
3373
3383
  var startCode = new Uint8Array([0, 0, 0, 1]);
@@ -3462,8 +3472,21 @@ import WebSocket2, { WebSocketServer } from "ws";
3462
3472
  var width = Number.parseInt(process.env.REPLICAS_DESKTOP_WIDTH || String(DESKTOP_VIEWER_WIDTH), 10);
3463
3473
  var height = Number.parseInt(process.env.REPLICAS_DESKTOP_HEIGHT || String(DESKTOP_VIEWER_HEIGHT), 10);
3464
3474
  var fps = Number.parseInt(process.env.REPLICAS_DESKTOP_STREAM_FPS || "60", 10);
3465
- var keyframeInterval = fps;
3466
3475
  var port = Number.parseInt(process.env.REPLICAS_DESKTOP_STREAM_PORT || String(DESKTOP_STREAM_PORT), 10);
3476
+ var qualityLadder = [
3477
+ { fps, bitrate: process.env.REPLICAS_DESKTOP_STREAM_VIDEO_BITRATE || "12M", crf: 18 },
3478
+ { fps: Math.max(1, Math.round(fps / 2)), bitrate: "6M", crf: 20 },
3479
+ { fps: Math.max(1, Math.round(fps / 2)), bitrate: "3M", crf: 23 },
3480
+ { fps: Math.max(1, Math.round(fps / 4)), bitrate: "1200k", crf: 26 }
3481
+ ];
3482
+ var keyframeMs = 250;
3483
+ var probeIntervalMs = 250;
3484
+ var targetBacklogMs = 120;
3485
+ var congestedBacklogMs = 250;
3486
+ var congestedDecodeQueue = 3;
3487
+ var probeTimeoutMs = 2e3;
3488
+ var fallbackBufferedBytes = 512 * 1024;
3489
+ var maximumBufferedBytes = 4 * 1024 * 1024;
3467
3490
  var display = process.env.REPLICAS_DESKTOP_DISPLAY?.replace(/^([^:])/, ":$1") || ":99";
3468
3491
  var videoCodec = "avc1.42E02A";
3469
3492
  var config = {
@@ -3508,7 +3531,7 @@ function validInput(value) {
3508
3531
  if (!value || typeof value !== "object" || !("type" in value) || typeof value.type !== "string") return false;
3509
3532
  if (value.type === "release") return true;
3510
3533
  if (value.type === "stats") return "videoFps" in value && typeof value.videoFps === "number" && Number.isFinite(value.videoFps) && "audioPacketsPerSecond" in value && typeof value.audioPacketsPerSecond === "number" && Number.isFinite(value.audioPacketsPerSecond) && "decodeQueue" in value && typeof value.decodeQueue === "number" && Number.isFinite(value.decodeQueue) && "rttMs" in value && typeof value.rttMs === "number" && Number.isFinite(value.rttMs);
3511
- if (value.type === "ping") return "sentAt" in value && typeof value.sentAt === "number";
3534
+ if (value.type === "ping" || value.type === "pong") return "sentAt" in value && typeof value.sentAt === "number";
3512
3535
  if (value.type === "key") return "code" in value && typeof value.code === "string" && value.code.length < 40 && "down" in value && typeof value.down === "boolean";
3513
3536
  if (!("x" in value) || !("y" in value) || typeof value.x !== "number" || typeof value.y !== "number") return false;
3514
3537
  if (value.type === "pointer") return true;
@@ -3524,23 +3547,30 @@ async function runMediaServer() {
3524
3547
  const videoAssembler = new H264RtpAssembler();
3525
3548
  const audioClock = new RtpClock(48e3);
3526
3549
  const clients = /* @__PURE__ */ new Map();
3527
- let encoder;
3528
- let encoderRestart;
3529
3550
  let encoderStop;
3551
+ let rung = 0;
3552
+ let descents = 0;
3553
+ let congestedSeconds = 0;
3554
+ let comfortableSeconds = 0;
3530
3555
  let lastVideoAt = 0;
3531
3556
  let lastAudioAt = 0;
3532
3557
  let videoFrames = 0;
3533
3558
  let audioPackets = 0;
3534
3559
  let videoBytes = 0;
3535
3560
  let audioBytes = 0;
3561
+ const queueMs = (state) => {
3562
+ const outstanding = state.probeSentAt ? Date.now() - state.probeSentAt : 0;
3563
+ if (!state.probeAnswered && outstanding > probeTimeoutMs) return 0;
3564
+ return Math.max(state.probeAckedMs, outstanding);
3565
+ };
3536
3566
  const broadcast = (packet, keyframe = false) => {
3537
3567
  for (const [client, state] of clients) {
3538
3568
  if (client.readyState !== WebSocket2.OPEN) continue;
3539
- if (client.bufferedAmount > 4 * 1024 * 1024) {
3569
+ if (client.bufferedAmount > maximumBufferedBytes) {
3540
3570
  client.close(1013, "stream client is too slow");
3541
3571
  continue;
3542
3572
  }
3543
- if (!keyframe && (state.waitingForKeyframe || client.bufferedAmount > 512 * 1024)) {
3573
+ if (!keyframe && (state.waitingForKeyframe || queueMs(state) > targetBacklogMs || client.bufferedAmount > fallbackBufferedBytes)) {
3544
3574
  state.waitingForKeyframe = true;
3545
3575
  continue;
3546
3576
  }
@@ -3567,14 +3597,77 @@ async function runMediaServer() {
3567
3597
  audioBytes += rtp.payload.byteLength;
3568
3598
  broadcast(mediaPacket(3 /* Audio */, audioClock.microseconds(rtp.timestamp), rtp.payload));
3569
3599
  });
3570
- const startEncoder = () => {
3571
- clearTimeout(encoderStop);
3572
- if (encoder && encoder.exitCode === null) return;
3573
- clearTimeout(encoderRestart);
3600
+ const superviseEncoder = (label, prepare, buildArgs) => {
3601
+ let child;
3602
+ let retiring;
3603
+ let restart;
3604
+ let intent = 0;
3605
+ const alive = (process2) => Boolean(process2 && process2.exitCode === null && process2.signalCode === null);
3606
+ const start = () => {
3607
+ clearTimeout(encoderStop);
3608
+ if (alive(child)) return;
3609
+ if (retiring) return;
3610
+ clearTimeout(restart);
3611
+ prepare();
3612
+ const spawned = spawn4("ffmpeg", buildArgs(), { env: { ...process.env, DISPLAY: display }, stdio: ["ignore", "ignore", "pipe"] });
3613
+ spawned.stderr?.on("data", (chunk) => process.stderr.write(`[desktop-stream] ${chunk}`));
3614
+ child = spawned;
3615
+ spawned.once("exit", (code, signal) => {
3616
+ if (child !== spawned) return;
3617
+ child = void 0;
3618
+ if (clients.size === 0) return;
3619
+ process.stderr.write(`[desktop-stream] ${label} encoder exited (${signal || code}); restarting
3620
+ `);
3621
+ restart = setTimeout(start, 500);
3622
+ });
3623
+ };
3624
+ return {
3625
+ start,
3626
+ running: () => alive(child),
3627
+ stop: () => {
3628
+ intent += 1;
3629
+ clearTimeout(restart);
3630
+ child?.kill("SIGTERM");
3631
+ retiring?.kill("SIGKILL");
3632
+ child = void 0;
3633
+ retiring = void 0;
3634
+ },
3635
+ replace: () => {
3636
+ const issued = intent += 1;
3637
+ clearTimeout(restart);
3638
+ const previous = child ?? retiring;
3639
+ child = void 0;
3640
+ retiring = void 0;
3641
+ if (!alive(previous)) {
3642
+ start();
3643
+ return;
3644
+ }
3645
+ retiring = previous;
3646
+ let settled = false;
3647
+ const resume = () => {
3648
+ if (settled) return;
3649
+ settled = true;
3650
+ clearTimeout(forceExit);
3651
+ clearTimeout(abandonWait);
3652
+ if (intent !== issued) return;
3653
+ retiring = void 0;
3654
+ if (clients.size === 0) return;
3655
+ start();
3656
+ };
3657
+ const forceExit = setTimeout(() => previous.kill("SIGKILL"), 2e3);
3658
+ const abandonWait = setTimeout(resume, 4e3);
3659
+ previous.once("exit", resume);
3660
+ previous.kill("SIGTERM");
3661
+ }
3662
+ };
3663
+ };
3664
+ const videoEncoder = superviseEncoder("video", () => {
3574
3665
  videoAssembler.reset();
3575
- audioClock.reset();
3576
- const videoBitrate = process.env.REPLICAS_DESKTOP_STREAM_VIDEO_BITRATE || "12M";
3577
- encoder = spawn4("ffmpeg", [
3666
+ for (const state of clients.values()) state.waitingForKeyframe = true;
3667
+ }, () => {
3668
+ const quality = qualityLadder[rung];
3669
+ const keyframeInterval = Math.max(1, Math.round(quality.fps * keyframeMs / 1e3));
3670
+ return [
3578
3671
  "-hide_banner",
3579
3672
  "-loglevel",
3580
3673
  "warning",
@@ -3582,16 +3675,10 @@ async function runMediaServer() {
3582
3675
  "nobuffer",
3583
3676
  "-flags",
3584
3677
  "low_delay",
3585
- "-thread_queue_size",
3586
- "1024",
3587
- "-f",
3588
- "pulse",
3589
- "-fragment_size",
3590
- "2048",
3591
- "-use_wallclock_as_timestamps",
3592
- "1",
3593
- "-i",
3594
- process.env.REPLICAS_DESKTOP_STREAM_AUDIO_SOURCE || "default",
3678
+ "-probesize",
3679
+ "32",
3680
+ "-analyzeduration",
3681
+ "0",
3595
3682
  "-thread_queue_size",
3596
3683
  "1024",
3597
3684
  "-f",
@@ -3599,15 +3686,13 @@ async function runMediaServer() {
3599
3686
  "-draw_mouse",
3600
3687
  "0",
3601
3688
  "-framerate",
3602
- String(fps),
3689
+ String(quality.fps),
3603
3690
  "-video_size",
3604
3691
  `${width}x${height}`,
3605
3692
  "-use_wallclock_as_timestamps",
3606
3693
  "1",
3607
3694
  "-i",
3608
3695
  `${display}.0`,
3609
- "-map",
3610
- "1:v:0",
3611
3696
  "-an",
3612
3697
  "-c:v",
3613
3698
  "libx264",
@@ -3630,9 +3715,9 @@ async function runMediaServer() {
3630
3715
  "-sc_threshold",
3631
3716
  "0",
3632
3717
  "-crf",
3633
- "18",
3718
+ String(quality.crf),
3634
3719
  "-maxrate",
3635
- videoBitrate,
3720
+ quality.bitrate,
3636
3721
  "-bufsize",
3637
3722
  "1M",
3638
3723
  "-x264-params",
@@ -3645,57 +3730,111 @@ async function runMediaServer() {
3645
3730
  "rtp",
3646
3731
  "-payload_type",
3647
3732
  "96",
3648
- `rtp://127.0.0.1:${videoPort}?pkt_size=1200&connect=1&rtcpport=${videoPort}`,
3649
- "-map",
3650
- "0:a:0",
3651
- "-vn",
3652
- "-af",
3653
- "aresample=async=1:first_pts=0",
3654
- "-c:a",
3655
- "libopus",
3656
- "-application",
3657
- "lowdelay",
3658
- "-frame_duration",
3659
- "20",
3660
- "-b:a",
3661
- "128k",
3662
- "-ar",
3663
- "48000",
3664
- "-ac",
3665
- "2",
3666
- "-flush_packets",
3667
- "1",
3668
- "-f",
3669
- "rtp",
3670
- "-payload_type",
3671
- "111",
3672
- `rtp://127.0.0.1:${audioPort}?pkt_size=1200&connect=1&rtcpport=${audioPort}`
3673
- ], { env: { ...process.env, DISPLAY: display }, stdio: ["ignore", "ignore", "pipe"] });
3674
- encoder.stderr?.on("data", (chunk) => process.stderr.write(`[desktop-stream] ${chunk}`));
3675
- encoder.once("exit", (code, signal) => {
3676
- encoder = void 0;
3677
- if (clients.size > 0) {
3678
- process.stderr.write(`[desktop-stream] encoder exited (${signal || code}); restarting
3733
+ `rtp://127.0.0.1:${videoPort}?pkt_size=1200&connect=1&rtcpport=${videoPort}`
3734
+ ];
3735
+ });
3736
+ const audioEncoder = superviseEncoder("audio", () => audioClock.reset(), () => [
3737
+ "-hide_banner",
3738
+ "-loglevel",
3739
+ "warning",
3740
+ "-fflags",
3741
+ "nobuffer",
3742
+ "-flags",
3743
+ "low_delay",
3744
+ "-probesize",
3745
+ "32",
3746
+ "-analyzeduration",
3747
+ "0",
3748
+ "-thread_queue_size",
3749
+ "1024",
3750
+ "-f",
3751
+ "pulse",
3752
+ "-fragment_size",
3753
+ "2048",
3754
+ "-use_wallclock_as_timestamps",
3755
+ "1",
3756
+ "-i",
3757
+ process.env.REPLICAS_DESKTOP_STREAM_AUDIO_SOURCE || "default",
3758
+ "-vn",
3759
+ "-af",
3760
+ "aresample=async=1:first_pts=0",
3761
+ "-c:a",
3762
+ "libopus",
3763
+ "-application",
3764
+ "lowdelay",
3765
+ "-frame_duration",
3766
+ "20",
3767
+ "-b:a",
3768
+ "128k",
3769
+ "-ar",
3770
+ "48000",
3771
+ "-ac",
3772
+ "2",
3773
+ "-flush_packets",
3774
+ "1",
3775
+ "-f",
3776
+ "rtp",
3777
+ "-payload_type",
3778
+ "111",
3779
+ `rtp://127.0.0.1:${audioPort}?pkt_size=1200&connect=1&rtcpport=${audioPort}`
3780
+ ]);
3781
+ const startEncoders = () => {
3782
+ videoEncoder.start();
3783
+ audioEncoder.start();
3784
+ };
3785
+ const setRung = (next) => {
3786
+ if (next === rung) return;
3787
+ if (next > rung) descents += 1;
3788
+ rung = next;
3789
+ congestedSeconds = 0;
3790
+ comfortableSeconds = 0;
3791
+ process.stderr.write(`[desktop-stream] quality rung ${rung} (${qualityLadder[rung].fps} fps, ${qualityLadder[rung].bitrate})
3679
3792
  `);
3680
- encoderRestart = setTimeout(startEncoder, 500);
3681
- }
3682
- });
3793
+ videoEncoder.replace();
3683
3794
  };
3684
- const stopEncoderSoon = () => {
3795
+ const stopEncodersSoon = () => {
3685
3796
  clearTimeout(encoderStop);
3686
3797
  encoderStop = setTimeout(() => {
3687
3798
  if (clients.size > 0) return;
3688
- encoder?.kill("SIGTERM");
3689
- encoder = void 0;
3799
+ videoEncoder.stop();
3800
+ audioEncoder.stop();
3690
3801
  }, 5e3);
3691
3802
  };
3803
+ const probe = setInterval(() => {
3804
+ for (const [client, state] of clients) {
3805
+ if (client.readyState !== WebSocket2.OPEN || state.probeSentAt) continue;
3806
+ state.probeSentAt = Date.now();
3807
+ client.send(JSON.stringify({ type: "ping", sentAt: state.probeSentAt }));
3808
+ }
3809
+ }, probeIntervalMs);
3810
+ probe.unref();
3811
+ const control = setInterval(() => {
3812
+ const states = [...clients.values()];
3813
+ if (states.length === 0) return;
3814
+ const struggling = states.filter((state) => queueMs(state) > congestedBacklogMs || (state.stats?.decodeQueue ?? 0) > congestedDecodeQueue).length;
3815
+ if (struggling === states.length) {
3816
+ comfortableSeconds = 0;
3817
+ congestedSeconds += 1;
3818
+ if (congestedSeconds >= 2) setRung(Math.min(qualityLadder.length - 1, rung + 1));
3819
+ return;
3820
+ }
3821
+ congestedSeconds = 0;
3822
+ if (struggling > 0) {
3823
+ comfortableSeconds = 0;
3824
+ return;
3825
+ }
3826
+ comfortableSeconds += 1;
3827
+ if (comfortableSeconds >= Math.min(300, 15 * 2 ** descents)) setRung(Math.max(0, rung - 1));
3828
+ }, 1e3);
3829
+ control.unref();
3692
3830
  const httpServer = createServer2((request, response) => {
3693
3831
  const url = new URL(request.url || "/", "http://localhost");
3694
3832
  if (url.pathname === "/health") {
3695
3833
  response.setHeader("content-type", "application/json");
3696
3834
  response.end(JSON.stringify({
3697
3835
  clients: clients.size,
3698
- encoder: Boolean(encoder && encoder.exitCode === null),
3836
+ encoder: videoEncoder.running(),
3837
+ audioEncoder: audioEncoder.running(),
3699
3838
  lastVideoFrameMsAgo: lastVideoAt ? Date.now() - lastVideoAt : null,
3700
3839
  lastAudioPacketMsAgo: lastAudioAt ? Date.now() - lastAudioAt : null,
3701
3840
  videoFrames,
@@ -3703,9 +3842,11 @@ async function runMediaServer() {
3703
3842
  videoBytes,
3704
3843
  audioBytes,
3705
3844
  clientStats: [...clients.values()].flatMap((client) => client.stats ? [client.stats] : []),
3845
+ clientQueueMs: [...clients.values()].map((client) => Math.round(queueMs(client))),
3846
+ qualityRung: rung,
3706
3847
  width,
3707
3848
  height,
3708
- fps
3849
+ fps: qualityLadder[rung].fps
3709
3850
  }));
3710
3851
  return;
3711
3852
  }
@@ -3729,9 +3870,15 @@ async function runMediaServer() {
3729
3870
  webSocketServer.handleUpgrade(request, socket, head, (client) => webSocketServer.emit("connection", client, request));
3730
3871
  });
3731
3872
  webSocketServer.on("connection", (client) => {
3732
- clients.set(client, { waitingForKeyframe: true, input: new InputBridge(display, width, height) });
3873
+ clients.set(client, {
3874
+ waitingForKeyframe: true,
3875
+ input: new InputBridge(display, width, height),
3876
+ probeSentAt: 0,
3877
+ probeAckedMs: 0,
3878
+ probeAnswered: false
3879
+ });
3733
3880
  client.send(JSON.stringify(config));
3734
- startEncoder();
3881
+ startEncoders();
3735
3882
  client.on("message", (raw, binary) => {
3736
3883
  if (binary) return;
3737
3884
  let message;
@@ -3742,21 +3889,32 @@ async function runMediaServer() {
3742
3889
  }
3743
3890
  if (!validInput(message)) return;
3744
3891
  if (message.type === "ping") client.send(JSON.stringify({ type: "pong", sentAt: message.sentAt }));
3745
- else if (message.type === "stats") {
3892
+ else if (message.type === "pong") {
3893
+ const state = clients.get(client);
3894
+ if (!state || message.sentAt !== state.probeSentAt) return;
3895
+ state.probeAckedMs = Date.now() - state.probeSentAt;
3896
+ state.probeSentAt = 0;
3897
+ state.probeAnswered = true;
3898
+ } else if (message.type === "stats") {
3746
3899
  const state = clients.get(client);
3747
3900
  if (!state) return;
3748
3901
  state.stats = {
3749
- videoFps: message.videoFps,
3750
- audioPacketsPerSecond: message.audioPacketsPerSecond,
3751
- decodeQueue: message.decodeQueue,
3752
- rttMs: message.rttMs
3902
+ videoFps: clamp(message.videoFps, 0, 1e3),
3903
+ audioPacketsPerSecond: clamp(message.audioPacketsPerSecond, 0, 1e3),
3904
+ decodeQueue: clamp(message.decodeQueue, 0, 1e3),
3905
+ rttMs: clamp(message.rttMs, 0, 6e5)
3753
3906
  };
3754
3907
  } else clients.get(client)?.input.handle(message);
3755
3908
  });
3756
3909
  client.once("close", () => {
3757
3910
  clients.get(client)?.input.close();
3758
3911
  clients.delete(client);
3759
- if (clients.size === 0) stopEncoderSoon();
3912
+ if (clients.size > 0) return;
3913
+ rung = 0;
3914
+ descents = 0;
3915
+ congestedSeconds = 0;
3916
+ comfortableSeconds = 0;
3917
+ stopEncodersSoon();
3760
3918
  });
3761
3919
  });
3762
3920
  await new Promise((resolveServer, reject) => {
@@ -3772,13 +3930,15 @@ async function runMediaServer() {
3772
3930
  const close = () => {
3773
3931
  if (closing) return;
3774
3932
  closing = true;
3775
- clearTimeout(encoderRestart);
3933
+ clearInterval(probe);
3934
+ clearInterval(control);
3776
3935
  clearTimeout(encoderStop);
3777
3936
  for (const [client, state] of clients) {
3778
3937
  state.input.close();
3779
3938
  client.close(1001, "server shutting down");
3780
3939
  }
3781
- encoder?.kill("SIGTERM");
3940
+ videoEncoder.stop();
3941
+ audioEncoder.stop();
3782
3942
  videoSocket.close();
3783
3943
  audioSocket.close();
3784
3944
  webSocketServer.close();
@@ -166,7 +166,7 @@ var handleBinary = (data) => {
166
166
  }
167
167
  if (!videoDecoder || !activeVideoConfig || videoDecoder.state !== "configured") return;
168
168
  const key = kind === 1 /* VideoKey */;
169
- if (videoDecoder.decodeQueueSize > 12) {
169
+ if (videoDecoder.decodeQueueSize > 3) {
170
170
  videoDecoder.reset();
171
171
  videoDecoder.configure(activeVideoConfig);
172
172
  waitingForKeyframe = true;
@@ -205,6 +205,10 @@ var connect = () => {
205
205
  rtt = performance.now() - message.sentAt;
206
206
  return;
207
207
  }
208
+ if (message.type === "ping") {
209
+ send({ type: "pong", sentAt: message.sentAt });
210
+ return;
211
+ }
208
212
  if (message.type !== "config" || message.protocolVersion !== STREAM_PROTOCOL_VERSION) return;
209
213
  config = message;
210
214
  fitScreen();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-computer",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Replicas workspace computer-use runtime",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",