@sesamespace/sesame 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 +266 -39
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
let pluginRuntime = null;
|
|
17
17
|
// Connection state per account
|
|
18
18
|
const connections = new Map();
|
|
19
|
+
// Streaming state: tracks channels where streaming already delivered the message
|
|
20
|
+
// Key: channelId, Value: { messageId, text } — outbound.sendText should edit instead of send
|
|
21
|
+
const streamDelivered = new Map();
|
|
19
22
|
function getLogger() {
|
|
20
23
|
return (pluginRuntime?.logging?.getChildLogger?.({ plugin: "sesame" }) ?? console);
|
|
21
24
|
}
|
|
@@ -146,6 +149,30 @@ const sesameChannelPlugin = {
|
|
|
146
149
|
if (!account?.apiKey)
|
|
147
150
|
throw new Error("Sesame not configured");
|
|
148
151
|
const channelId = to?.startsWith("sesame:") ? to.slice(7) : to;
|
|
152
|
+
const log = getLogger();
|
|
153
|
+
// Check if streaming already delivered this message
|
|
154
|
+
const delivered = streamDelivered.get(channelId);
|
|
155
|
+
if (delivered) {
|
|
156
|
+
streamDelivered.delete(channelId);
|
|
157
|
+
// Do a final edit to ensure the complete text is correct, then return
|
|
158
|
+
log.info?.(`[sesame] outbound.sendText: streaming already sent to ${channelId}, doing final edit`);
|
|
159
|
+
const editRes = await fetch(`${account.apiUrl}/api/v1/channels/${channelId}/messages/${delivered.messageId}`, {
|
|
160
|
+
method: "PATCH",
|
|
161
|
+
headers: {
|
|
162
|
+
"Content-Type": "application/json",
|
|
163
|
+
Authorization: `Bearer ${account.apiKey}`,
|
|
164
|
+
},
|
|
165
|
+
body: JSON.stringify({ content: text }),
|
|
166
|
+
});
|
|
167
|
+
if (!editRes.ok) {
|
|
168
|
+
log.warn?.(`[sesame] Final stream edit in sendText failed (${editRes.status})`);
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
channel: "sesame",
|
|
172
|
+
messageId: delivered.messageId,
|
|
173
|
+
chatId: channelId,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
149
176
|
const response = await fetch(`${account.apiUrl}/api/v1/channels/${channelId}/messages`, {
|
|
150
177
|
method: "POST",
|
|
151
178
|
headers: {
|
|
@@ -304,6 +331,90 @@ const sesameChannelPlugin = {
|
|
|
304
331
|
},
|
|
305
332
|
},
|
|
306
333
|
};
|
|
334
|
+
// ── Wake Context (cold start optimization) ──
|
|
335
|
+
async function fetchWakeContext(account, agentId, ctx) {
|
|
336
|
+
const log = getLogger();
|
|
337
|
+
try {
|
|
338
|
+
const response = await fetch(`${account.apiUrl}/api/v1/agents/${agentId}/wake`, { headers: { Authorization: `Bearer ${account.apiKey}` } });
|
|
339
|
+
if (!response.ok) {
|
|
340
|
+
log.warn?.(`[sesame] Wake endpoint returned ${response.status}`);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const result = await response.json();
|
|
344
|
+
if (!result.ok)
|
|
345
|
+
return;
|
|
346
|
+
const data = result.data;
|
|
347
|
+
const lines = ["[Sesame Platform Context]"];
|
|
348
|
+
// Focused task
|
|
349
|
+
if (data.focusedTask) {
|
|
350
|
+
const t = data.focusedTask;
|
|
351
|
+
lines.push(`\nFocused Task: T-${t.taskNumber} — ${t.title} (${t.status})`);
|
|
352
|
+
if (t.context?.notes)
|
|
353
|
+
lines.push(`Notes: ${t.context.notes}`);
|
|
354
|
+
if (t.context?.background)
|
|
355
|
+
lines.push(`Background: ${t.context.background}`);
|
|
356
|
+
}
|
|
357
|
+
// Active tasks summary
|
|
358
|
+
const active = data.tasks?.active ?? [];
|
|
359
|
+
const blocked = data.tasks?.blocked ?? [];
|
|
360
|
+
const todo = data.tasks?.todo ?? [];
|
|
361
|
+
if (active.length + blocked.length + todo.length > 0) {
|
|
362
|
+
lines.push(`\nTasks: ${active.length} active, ${blocked.length} blocked, ${todo.length} todo`);
|
|
363
|
+
for (const t of active.slice(0, 5)) {
|
|
364
|
+
lines.push(` [active] T-${t.taskNumber}: ${t.title}`);
|
|
365
|
+
}
|
|
366
|
+
for (const t of blocked.slice(0, 3)) {
|
|
367
|
+
lines.push(` [blocked] T-${t.taskNumber}: ${t.title}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// Unread channels
|
|
371
|
+
const unread = data.channels?.unread ?? [];
|
|
372
|
+
if (unread.length > 0) {
|
|
373
|
+
lines.push(`\nUnread: ${unread.length} channel${unread.length > 1 ? "s" : ""} with unread messages`);
|
|
374
|
+
}
|
|
375
|
+
// Upcoming schedule
|
|
376
|
+
const upcoming = data.schedule?.upcoming ?? [];
|
|
377
|
+
if (upcoming.length > 0) {
|
|
378
|
+
lines.push(`\nSchedule: ${upcoming.length} upcoming event${upcoming.length > 1 ? "s" : ""}`);
|
|
379
|
+
for (const e of upcoming.slice(0, 3)) {
|
|
380
|
+
lines.push(` ${e.title} — ${e.nextOccurrenceAt ?? "soon"}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// Pinned memories
|
|
384
|
+
const pinned = data.memory?.pinned ?? [];
|
|
385
|
+
if (pinned.length > 0) {
|
|
386
|
+
lines.push(`\nPinned Memories:`);
|
|
387
|
+
for (const m of pinned.slice(0, 10)) {
|
|
388
|
+
lines.push(` [${m.category}/${m.key}] ${m.content.slice(0, 200)}`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// Session state
|
|
392
|
+
if (data.state?.state) {
|
|
393
|
+
lines.push(`\nSession State: ${JSON.stringify(data.state.state).slice(0, 500)}`);
|
|
394
|
+
}
|
|
395
|
+
// Inject as system context via wake event
|
|
396
|
+
if (lines.length > 1) {
|
|
397
|
+
const contextText = lines.join("\n");
|
|
398
|
+
log.info?.(`[sesame] Injecting wake context (${contextText.length} chars)`);
|
|
399
|
+
// Use the OpenClaw plugin API to inject a wake event
|
|
400
|
+
try {
|
|
401
|
+
const runtime = pluginRuntime;
|
|
402
|
+
if (runtime?.events?.emit) {
|
|
403
|
+
runtime.events.emit("wake", {
|
|
404
|
+
text: contextText,
|
|
405
|
+
source: "sesame-wake",
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
catch (e) {
|
|
410
|
+
log.warn?.(`[sesame] Could not inject wake event: ${e}`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
catch (err) {
|
|
415
|
+
log.warn?.(`[sesame] Wake context error: ${err}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
307
418
|
// ── WebSocket connection ──
|
|
308
419
|
async function connect(account, ctx) {
|
|
309
420
|
const log = ctx.log ?? getLogger();
|
|
@@ -411,15 +522,33 @@ function handleEvent(event, account, state, ctx) {
|
|
|
411
522
|
}, event.heartbeatIntervalMs ?? 30000);
|
|
412
523
|
log.info?.("[sesame] Authenticated successfully");
|
|
413
524
|
state.ws?.send(JSON.stringify({ type: "replay", cursors: {} }));
|
|
525
|
+
// Call wake endpoint for cold start context (fire and forget)
|
|
526
|
+
if (state.agentId) {
|
|
527
|
+
fetchWakeContext(account, state.agentId, ctx).catch((err) => log.warn?.(`[sesame] Wake context fetch failed: ${err}`));
|
|
528
|
+
}
|
|
414
529
|
break;
|
|
415
530
|
case "message": {
|
|
416
531
|
const msg = event.message ?? event.data;
|
|
417
|
-
//
|
|
532
|
+
// For voice messages: check if transcript is available.
|
|
533
|
+
// If not, check plaintext (API may have set it during sync transcription).
|
|
534
|
+
// Only skip if neither is available — wait for voice.transcribed event.
|
|
418
535
|
if (msg?.kind === "voice") {
|
|
419
536
|
const meta = msg.metadata ?? {};
|
|
420
537
|
const transcript = meta.transcript;
|
|
421
|
-
|
|
422
|
-
|
|
538
|
+
const plaintext = msg.plaintext;
|
|
539
|
+
const hasTranscript = transcript || (plaintext && plaintext !== "(voice note)");
|
|
540
|
+
if (!hasTranscript) {
|
|
541
|
+
log.info?.(`[sesame] Voice message ${msg.id} — no transcript yet, waiting for voice.transcribed (30s fallback)`);
|
|
542
|
+
// Track pending voice messages for fallback delivery
|
|
543
|
+
const pendingKey = `voice:${msg.id}`;
|
|
544
|
+
state.sentMessageIds.add(pendingKey); // reuse set to track pending
|
|
545
|
+
setTimeout(() => {
|
|
546
|
+
if (state.sentMessageIds.has(pendingKey)) {
|
|
547
|
+
state.sentMessageIds.delete(pendingKey);
|
|
548
|
+
log.warn?.(`[sesame] Voice message ${msg.id} — transcription timeout, delivering without transcript`);
|
|
549
|
+
handleMessage(msg, account, state, ctx);
|
|
550
|
+
}
|
|
551
|
+
}, 30000);
|
|
423
552
|
break;
|
|
424
553
|
}
|
|
425
554
|
}
|
|
@@ -437,6 +566,9 @@ function handleEvent(event, account, state, ctx) {
|
|
|
437
566
|
log.warn?.("[sesame] voice.transcribed event missing fields");
|
|
438
567
|
break;
|
|
439
568
|
}
|
|
569
|
+
// Clear pending fallback so we don't double-deliver
|
|
570
|
+
const pendingKey = `voice:${messageId}`;
|
|
571
|
+
state.sentMessageIds.delete(pendingKey);
|
|
440
572
|
log.info?.(`[sesame] Voice transcribed for ${messageId}: "${transcript.slice(0, 80)}"`);
|
|
441
573
|
// Build a synthetic message object so handleMessage can process it
|
|
442
574
|
handleMessage({
|
|
@@ -484,8 +616,21 @@ async function handleMessage(message, account, state, ctx) {
|
|
|
484
616
|
// For voice messages, prefer transcript from metadata over content
|
|
485
617
|
const msgMeta = message.metadata ?? {};
|
|
486
618
|
const voiceTranscript = message.kind === "voice" ? msgMeta.transcript : undefined;
|
|
487
|
-
|
|
488
|
-
|
|
619
|
+
// Prefer plaintext over content — plaintext includes attachment info block
|
|
620
|
+
// appended by the API (download URLs, file names, sizes)
|
|
621
|
+
const rawText = voiceTranscript ?? message.plaintext ?? message.content ?? message.body ?? message.text ?? "";
|
|
622
|
+
// If plaintext didn't include attachments but metadata does, build the block ourselves
|
|
623
|
+
let bodyText = message.kind === "voice" && rawText ? `(voice note)\n${rawText}` : rawText;
|
|
624
|
+
const attachments = msgMeta.attachments;
|
|
625
|
+
if (attachments?.length && !bodyText.includes("[Attachments]")) {
|
|
626
|
+
const lines = attachments.map((a) => {
|
|
627
|
+
const sizeKB = Math.round((a.size ?? 0) / 1024);
|
|
628
|
+
const sizeStr = sizeKB >= 1024 ? `${(sizeKB / 1024).toFixed(1)} MB` : `${sizeKB} KB`;
|
|
629
|
+
const dl = a.downloadUrl ? `\n Download: ${a.downloadUrl}` : "";
|
|
630
|
+
return `- ${a.fileName ?? "file"} (${a.contentType ?? "unknown"}, ${sizeStr})${dl}`;
|
|
631
|
+
});
|
|
632
|
+
bodyText = bodyText + `\n\n[Attachments]\n${lines.join("\n")}`;
|
|
633
|
+
}
|
|
489
634
|
const channelId = message.channelId;
|
|
490
635
|
const messageId = message.id;
|
|
491
636
|
log.info?.(`[sesame] Message from ${message.senderId} in ${channelId}: "${bodyText.slice(0, 100)}"`);
|
|
@@ -610,12 +755,23 @@ async function handleMessage(message, account, state, ctx) {
|
|
|
610
755
|
sessionKey: inboundCtx.SessionKey ?? sesameSessionKey,
|
|
611
756
|
ctx: inboundCtx,
|
|
612
757
|
});
|
|
613
|
-
//
|
|
758
|
+
// ── Streaming reply: send initial message, then edit in place ──
|
|
759
|
+
const sesameStreamMode = cfg.channels?.sesame?.streamMode ?? "buffer";
|
|
614
760
|
const replyBuffer = [];
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
761
|
+
let streamMessageId = null;
|
|
762
|
+
let streamSending = false; // lock to prevent concurrent first-message sends
|
|
763
|
+
let streamEditTimer = null;
|
|
764
|
+
let streamPendingText = "";
|
|
765
|
+
const STREAM_EDIT_DEBOUNCE_MS = 300; // don't edit more than ~3x/sec
|
|
766
|
+
const trackSentMessage = (sentId) => {
|
|
767
|
+
state.sentMessageIds.add(sentId);
|
|
768
|
+
if (state.sentMessageIds.size > 1000) {
|
|
769
|
+
const first = state.sentMessageIds.values().next().value;
|
|
770
|
+
if (first)
|
|
771
|
+
state.sentMessageIds.delete(first);
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
const sendNewMessage = async (text) => {
|
|
619
775
|
// Outbound circuit breaker: max 5 messages per 10s window
|
|
620
776
|
const cbNow = Date.now();
|
|
621
777
|
if (cbNow - state.outboundWindowStart > 10000) {
|
|
@@ -623,45 +779,78 @@ async function handleMessage(message, account, state, ctx) {
|
|
|
623
779
|
state.outboundWindowStart = cbNow;
|
|
624
780
|
}
|
|
625
781
|
if (state.recentOutboundCount >= 5) {
|
|
626
|
-
log.error?.(`[sesame] Circuit breaker: suppressing outbound message
|
|
627
|
-
return;
|
|
782
|
+
log.error?.(`[sesame] Circuit breaker: suppressing outbound message`);
|
|
783
|
+
return null;
|
|
628
784
|
}
|
|
629
|
-
log.info?.(`[sesame] Flushing buffered reply (${fullReply.length} chars): "${fullReply.slice(0, 100)}"`);
|
|
630
785
|
const res = await fetch(`${account.apiUrl}/api/v1/channels/${channelId}/messages`, {
|
|
631
786
|
method: "POST",
|
|
632
787
|
headers: {
|
|
633
788
|
"Content-Type": "application/json",
|
|
634
789
|
Authorization: `Bearer ${account.apiKey}`,
|
|
635
790
|
},
|
|
636
|
-
body: JSON.stringify({
|
|
637
|
-
content: fullReply,
|
|
638
|
-
kind: "text",
|
|
639
|
-
intent: "chat",
|
|
640
|
-
}),
|
|
791
|
+
body: JSON.stringify({ content: text, kind: "text", intent: "chat" }),
|
|
641
792
|
});
|
|
642
793
|
if (!res.ok) {
|
|
643
794
|
const err = await res.text().catch(() => "");
|
|
644
795
|
log.error?.(`[sesame] Send failed (${res.status}): ${err.slice(0, 200)}`);
|
|
796
|
+
return null;
|
|
645
797
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
798
|
+
state.recentOutboundCount++;
|
|
799
|
+
const resData = (await res.json().catch(() => ({})));
|
|
800
|
+
const sentId = resData.data?.id ?? resData.id ?? null;
|
|
801
|
+
if (sentId)
|
|
802
|
+
trackSentMessage(sentId);
|
|
803
|
+
return sentId;
|
|
804
|
+
};
|
|
805
|
+
const editMessage = async (msgId, text) => {
|
|
806
|
+
const res = await fetch(`${account.apiUrl}/api/v1/channels/${channelId}/messages/${msgId}`, {
|
|
807
|
+
method: "PATCH",
|
|
808
|
+
headers: {
|
|
809
|
+
"Content-Type": "application/json",
|
|
810
|
+
Authorization: `Bearer ${account.apiKey}`,
|
|
811
|
+
},
|
|
812
|
+
body: JSON.stringify({ content: text }),
|
|
813
|
+
});
|
|
814
|
+
if (!res.ok) {
|
|
815
|
+
log.warn?.(`[sesame] Edit failed (${res.status})`);
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
const scheduleStreamEdit = () => {
|
|
819
|
+
if (streamEditTimer)
|
|
820
|
+
return; // already scheduled
|
|
821
|
+
streamEditTimer = setTimeout(async () => {
|
|
822
|
+
streamEditTimer = null;
|
|
823
|
+
if (streamMessageId && streamPendingText) {
|
|
824
|
+
await editMessage(streamMessageId, streamPendingText);
|
|
661
825
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
826
|
+
}, STREAM_EDIT_DEBOUNCE_MS);
|
|
827
|
+
};
|
|
828
|
+
const flushBuffer = async () => {
|
|
829
|
+
const fullReply = replyBuffer.join("\n\n").trim();
|
|
830
|
+
// If nothing in buffer but we streamed via onPartialReply, do final edit with last known text
|
|
831
|
+
if (!fullReply && streamMessageId && streamPendingText) {
|
|
832
|
+
clearTimeout(streamEditTimer);
|
|
833
|
+
streamEditTimer = null;
|
|
834
|
+
await editMessage(streamMessageId, streamPendingText);
|
|
835
|
+
streamDelivered.set(channelId, { messageId: streamMessageId, text: streamPendingText });
|
|
836
|
+
log.info?.(`[sesame] Final stream edit from partial (${streamPendingText.length} chars), marked as delivered`);
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
if (!fullReply)
|
|
840
|
+
return;
|
|
841
|
+
if (streamMessageId) {
|
|
842
|
+
// Final edit with complete text
|
|
843
|
+
clearTimeout(streamEditTimer);
|
|
844
|
+
streamEditTimer = null;
|
|
845
|
+
await editMessage(streamMessageId, fullReply);
|
|
846
|
+
// Signal to outbound.sendText that this channel was already handled
|
|
847
|
+
streamDelivered.set(channelId, { messageId: streamMessageId, text: fullReply });
|
|
848
|
+
log.info?.(`[sesame] Final stream edit (${fullReply.length} chars), marked as delivered`);
|
|
849
|
+
}
|
|
850
|
+
else {
|
|
851
|
+
// No streaming happened — don't send here, let outbound.sendText handle it
|
|
852
|
+
// (avoids double-send when OpenClaw calls both deliver + outbound.sendText)
|
|
853
|
+
log.info?.(`[sesame] Buffer mode: deferring to outbound.sendText (${fullReply.length} chars)`);
|
|
665
854
|
}
|
|
666
855
|
};
|
|
667
856
|
const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({
|
|
@@ -670,18 +859,56 @@ async function handleMessage(message, account, state, ctx) {
|
|
|
670
859
|
onIdle: () => clearInterval(typingInterval),
|
|
671
860
|
deliver: async (payload) => {
|
|
672
861
|
const replyText = payload.text ?? payload.body ?? payload.content ?? "";
|
|
673
|
-
if (replyText)
|
|
674
|
-
|
|
862
|
+
if (!replyText)
|
|
863
|
+
return;
|
|
864
|
+
replyBuffer.push(replyText);
|
|
865
|
+
const fullSoFar = replyBuffer.join("\n\n").trim();
|
|
866
|
+
if (sesameStreamMode === "partial" && fullSoFar.length > 0) {
|
|
867
|
+
streamPendingText = fullSoFar;
|
|
868
|
+
if (!streamMessageId && !streamSending) {
|
|
869
|
+
// First chunk — send initial message (with lock to prevent race)
|
|
870
|
+
streamSending = true;
|
|
871
|
+
streamMessageId = await sendNewMessage(fullSoFar);
|
|
872
|
+
streamSending = false;
|
|
873
|
+
log.info?.(`[sesame] Stream started, msgId=${streamMessageId}`);
|
|
874
|
+
}
|
|
875
|
+
else if (streamMessageId) {
|
|
876
|
+
// Subsequent chunks — debounced edit
|
|
877
|
+
scheduleStreamEdit();
|
|
878
|
+
}
|
|
879
|
+
// If streamSending is true, another deliver call is already creating the message — skip
|
|
880
|
+
}
|
|
675
881
|
},
|
|
676
882
|
onError: (err) => {
|
|
677
883
|
log.error?.(`[sesame] Reply failed: ${String(err)}`);
|
|
678
884
|
},
|
|
679
885
|
});
|
|
886
|
+
// Add onPartialReply for real-time streaming (token-by-token updates)
|
|
887
|
+
const streamingReplyOptions = { ...replyOptions };
|
|
888
|
+
if (sesameStreamMode === "partial") {
|
|
889
|
+
streamingReplyOptions.onPartialReply = async (payload) => {
|
|
890
|
+
const text = payload.text;
|
|
891
|
+
if (!text)
|
|
892
|
+
return;
|
|
893
|
+
streamPendingText = text;
|
|
894
|
+
if (!streamMessageId && !streamSending) {
|
|
895
|
+
streamSending = true;
|
|
896
|
+
streamMessageId = await sendNewMessage(text);
|
|
897
|
+
streamSending = false;
|
|
898
|
+
if (streamMessageId) {
|
|
899
|
+
log.info?.(`[sesame] Stream started, msgId=${streamMessageId}`);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
else if (streamMessageId) {
|
|
903
|
+
scheduleStreamEdit();
|
|
904
|
+
}
|
|
905
|
+
};
|
|
906
|
+
}
|
|
680
907
|
await core.channel.reply.dispatchReplyFromConfig({
|
|
681
908
|
ctx: inboundCtx,
|
|
682
909
|
cfg,
|
|
683
910
|
dispatcher,
|
|
684
|
-
replyOptions,
|
|
911
|
+
replyOptions: streamingReplyOptions,
|
|
685
912
|
});
|
|
686
913
|
typingStopped = true;
|
|
687
914
|
clearInterval(typingInterval);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sesamespace/sesame",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Sesame channel plugin for OpenClaw — connect your AI agent to the Sesame messaging platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"ws": "^8.18.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@types/node": "^25.
|
|
59
|
+
"@types/node": "^25.5.0",
|
|
60
60
|
"typescript": "^5.9.3"
|
|
61
61
|
}
|
|
62
62
|
}
|