opencode-discord-notify 0.3.2 → 0.4.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/dist/index.js +208 -111
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
+
var DISCORD_FIELD_VALUE_MAX_LENGTH = 1024;
|
|
3
|
+
var DISCORD_EMBED_DESCRIPTION_MAX_LENGTH = 4096;
|
|
4
|
+
var DISCORD_THREAD_NAME_MAX_LENGTH = 100;
|
|
5
|
+
var ELLIPSIS = "...";
|
|
6
|
+
var ELLIPSIS_LENGTH = 3;
|
|
7
|
+
var TOAST_DURATION_MS = 8e3;
|
|
8
|
+
var TOAST_COOLDOWN_MS = 3e4;
|
|
9
|
+
var TODO_ITEM_DISPLAY_MAX_LENGTH = 200;
|
|
10
|
+
var HTTP_STATUS_TOO_MANY_REQUESTS = 429;
|
|
11
|
+
var MS_PER_SECOND = 1e3;
|
|
12
|
+
var DEFAULT_RATE_LIMIT_WAIT_MS = 1e4;
|
|
2
13
|
var SEND_PARAM_KEYS = [
|
|
3
14
|
"sessionID",
|
|
4
15
|
"permissionID",
|
|
@@ -37,9 +48,13 @@ function buildFields(fields, inline = false) {
|
|
|
37
48
|
for (const [name, rawValue] of fields) {
|
|
38
49
|
const value = safeString(rawValue);
|
|
39
50
|
if (!value) continue;
|
|
51
|
+
const truncatedValue = value.length > DISCORD_FIELD_VALUE_MAX_LENGTH ? value.slice(
|
|
52
|
+
0,
|
|
53
|
+
DISCORD_FIELD_VALUE_MAX_LENGTH - ELLIPSIS_LENGTH
|
|
54
|
+
) + ELLIPSIS : value;
|
|
40
55
|
result.push({
|
|
41
56
|
name,
|
|
42
|
-
value:
|
|
57
|
+
value: truncatedValue,
|
|
43
58
|
inline
|
|
44
59
|
});
|
|
45
60
|
}
|
|
@@ -117,8 +132,12 @@ function buildMention(mention, nameForLog) {
|
|
|
117
132
|
function normalizeTodoContent(value) {
|
|
118
133
|
return safeString(value).replace(/\s+/g, " ").trim();
|
|
119
134
|
}
|
|
135
|
+
function getTodoStatusMarker(status) {
|
|
136
|
+
if (status === "completed") return "[\u2713]";
|
|
137
|
+
if (status === "in_progress") return "[\u25B6]";
|
|
138
|
+
return "[ ]";
|
|
139
|
+
}
|
|
120
140
|
function buildTodoChecklist(todos) {
|
|
121
|
-
const maxDescription = 4096;
|
|
122
141
|
const items = Array.isArray(todos) ? todos : [];
|
|
123
142
|
let matchCount = 0;
|
|
124
143
|
let description = "";
|
|
@@ -128,10 +147,10 @@ function buildTodoChecklist(todos) {
|
|
|
128
147
|
if (status === "cancelled") continue;
|
|
129
148
|
const content = normalizeTodoContent(item?.content);
|
|
130
149
|
if (!content) continue;
|
|
131
|
-
const marker = status
|
|
132
|
-
const line = `> ${marker} ${truncateText(content,
|
|
150
|
+
const marker = getTodoStatusMarker(status);
|
|
151
|
+
const line = `> ${marker} ${truncateText(content, TODO_ITEM_DISPLAY_MAX_LENGTH)}`;
|
|
133
152
|
const nextChunk = (description ? "\n" : "") + line;
|
|
134
|
-
if (description.length + nextChunk.length >
|
|
153
|
+
if (description.length + nextChunk.length > DISCORD_EMBED_DESCRIPTION_MAX_LENGTH) {
|
|
135
154
|
truncated = true;
|
|
136
155
|
break;
|
|
137
156
|
}
|
|
@@ -143,7 +162,7 @@ function buildTodoChecklist(todos) {
|
|
|
143
162
|
}
|
|
144
163
|
if (truncated || matchCount < items.length) {
|
|
145
164
|
const moreLine = `${description ? "\n" : ""}> ...and more`;
|
|
146
|
-
if (description.length + moreLine.length <=
|
|
165
|
+
if (description.length + moreLine.length <= DISCORD_EMBED_DESCRIPTION_MAX_LENGTH) {
|
|
147
166
|
description += moreLine;
|
|
148
167
|
}
|
|
149
168
|
}
|
|
@@ -197,10 +216,10 @@ async function postDiscordWebhook(input, deps) {
|
|
|
197
216
|
return void 0;
|
|
198
217
|
return { id: messageId, channel_id: channelId };
|
|
199
218
|
}
|
|
200
|
-
if (response.status ===
|
|
219
|
+
if (response.status === HTTP_STATUS_TOO_MANY_REQUESTS) {
|
|
201
220
|
const text2 = await response.text().catch(() => "");
|
|
202
221
|
const retryAfterSeconds = parseRetryAfterFromText(text2) ?? parseRetryAfterFromHeader(response.headers);
|
|
203
|
-
const waitMs = retryAfterSeconds === void 0 ? deps.waitOnRateLimitMs : Math.ceil(retryAfterSeconds *
|
|
222
|
+
const waitMs = retryAfterSeconds === void 0 ? deps.waitOnRateLimitMs : Math.ceil(retryAfterSeconds * MS_PER_SECOND);
|
|
204
223
|
await sleepImpl(waitMs);
|
|
205
224
|
const retryResponse = await doRequest();
|
|
206
225
|
if (!retryResponse.ok) {
|
|
@@ -208,8 +227,8 @@ async function postDiscordWebhook(input, deps) {
|
|
|
208
227
|
await deps.maybeAlertError({
|
|
209
228
|
key: `discord_webhook_error:${retryResponse.status}`,
|
|
210
229
|
title: "Discord webhook rate-limited",
|
|
211
|
-
message: `Discord webhook returned
|
|
212
|
-
waitMs /
|
|
230
|
+
message: `Discord webhook returned ${HTTP_STATUS_TOO_MANY_REQUESTS} (rate limited). Waited ${Math.round(
|
|
231
|
+
waitMs / MS_PER_SECOND
|
|
213
232
|
)}s and retried, but it still failed.`,
|
|
214
233
|
variant: "warning"
|
|
215
234
|
});
|
|
@@ -260,14 +279,14 @@ var plugin = async ({ client }) => {
|
|
|
260
279
|
const excludeInputContext = excludeInputContextRaw !== "0";
|
|
261
280
|
const showErrorAlertRaw = (getEnv("DISCORD_WEBHOOK_SHOW_ERROR_ALERT") ?? "1").trim();
|
|
262
281
|
const showErrorAlert = showErrorAlertRaw !== "0";
|
|
263
|
-
const waitOnRateLimitMs =
|
|
264
|
-
const toastCooldownMs = 3e4;
|
|
282
|
+
const waitOnRateLimitMs = DEFAULT_RATE_LIMIT_WAIT_MS;
|
|
265
283
|
const sendParams = parseSendParams(getEnv("DISCORD_SEND_PARAMS"));
|
|
266
284
|
const lastAlertAtByKey = /* @__PURE__ */ new Map();
|
|
285
|
+
const sentTextPartIds = /* @__PURE__ */ new Set();
|
|
267
286
|
const showToast = async ({ title, message, variant }) => {
|
|
268
287
|
try {
|
|
269
288
|
await client.tui.showToast({
|
|
270
|
-
body: { title, message, variant, duration:
|
|
289
|
+
body: { title, message, variant, duration: TOAST_DURATION_MS }
|
|
271
290
|
});
|
|
272
291
|
} catch {
|
|
273
292
|
}
|
|
@@ -281,7 +300,7 @@ var plugin = async ({ client }) => {
|
|
|
281
300
|
if (!showErrorAlert) return;
|
|
282
301
|
const now = Date.now();
|
|
283
302
|
const last = lastAlertAtByKey.get(key);
|
|
284
|
-
if (last !== void 0 && now - last <
|
|
303
|
+
if (last !== void 0 && now - last < TOAST_COOLDOWN_MS) return;
|
|
285
304
|
lastAlertAtByKey.set(key, now);
|
|
286
305
|
await showToast({ title, message, variant });
|
|
287
306
|
};
|
|
@@ -289,7 +308,7 @@ var plugin = async ({ client }) => {
|
|
|
289
308
|
async function showMissingUrlToastOnce() {
|
|
290
309
|
const now = Date.now();
|
|
291
310
|
const last = lastAlertAtByKey.get(MISSING_URL_KEY);
|
|
292
|
-
if (last !== void 0 && now - last <
|
|
311
|
+
if (last !== void 0 && now - last < TOAST_COOLDOWN_MS) return;
|
|
293
312
|
lastAlertAtByKey.set(MISSING_URL_KEY, now);
|
|
294
313
|
await showToast({
|
|
295
314
|
title: "Discord webhook not configured",
|
|
@@ -317,15 +336,17 @@ var plugin = async ({ client }) => {
|
|
|
317
336
|
}
|
|
318
337
|
function buildThreadName(sessionID) {
|
|
319
338
|
const fromUser = normalizeThreadTitle(firstUserTextBySession.get(sessionID));
|
|
320
|
-
if (fromUser) return fromUser.slice(0,
|
|
339
|
+
if (fromUser) return fromUser.slice(0, DISCORD_THREAD_NAME_MAX_LENGTH);
|
|
321
340
|
const fromSessionTitle = normalizeThreadTitle(
|
|
322
341
|
lastSessionInfo.get(sessionID)?.title
|
|
323
342
|
);
|
|
324
|
-
if (fromSessionTitle)
|
|
343
|
+
if (fromSessionTitle)
|
|
344
|
+
return fromSessionTitle.slice(0, DISCORD_THREAD_NAME_MAX_LENGTH);
|
|
325
345
|
const fromSessionId = normalizeThreadTitle(
|
|
326
346
|
sessionID ? `session ${sessionID}` : ""
|
|
327
347
|
);
|
|
328
|
-
if (fromSessionId)
|
|
348
|
+
if (fromSessionId)
|
|
349
|
+
return fromSessionId.slice(0, DISCORD_THREAD_NAME_MAX_LENGTH);
|
|
329
350
|
return "untitled";
|
|
330
351
|
}
|
|
331
352
|
async function ensureThread(sessionID) {
|
|
@@ -424,11 +445,52 @@ var plugin = async ({ client }) => {
|
|
|
424
445
|
function buildPermissionMention() {
|
|
425
446
|
return buildMention(permissionMention, "DISCORD_WEBHOOK_PERMISSION_MENTION");
|
|
426
447
|
}
|
|
427
|
-
function
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
448
|
+
function escapeDiscordMention(text) {
|
|
449
|
+
return text.replace(/^@/g, "@\u200B");
|
|
450
|
+
}
|
|
451
|
+
async function handleTextPart(input) {
|
|
452
|
+
const { part, role, sessionID, messageID, replay } = input;
|
|
453
|
+
const partID = part?.id;
|
|
454
|
+
if (!partID) return;
|
|
455
|
+
if (!replay && role === "assistant" && !part?.time?.end) return;
|
|
456
|
+
if (sentTextPartIds.has(partID)) return;
|
|
457
|
+
sentTextPartIds.add(partID);
|
|
458
|
+
const text = escapeDiscordMention(safeString(part?.text));
|
|
459
|
+
if (role === "user" && excludeInputContext && isInputContextText(text)) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (role === "user" && text.trim() === "" || text.trim() === "(empty)") {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (role === "user" && !firstUserTextBySession.has(sessionID)) {
|
|
466
|
+
const normalized = normalizeThreadTitle(text);
|
|
467
|
+
if (normalized) firstUserTextBySession.set(sessionID, normalized);
|
|
468
|
+
}
|
|
469
|
+
const embed = {
|
|
470
|
+
title: getTextPartEmbedTitle(role),
|
|
471
|
+
color: COLORS.info,
|
|
472
|
+
fields: buildFields(
|
|
473
|
+
filterSendFields(
|
|
474
|
+
[
|
|
475
|
+
["sessionID", sessionID],
|
|
476
|
+
["messageID", messageID],
|
|
477
|
+
["partID", partID],
|
|
478
|
+
["role", role]
|
|
479
|
+
],
|
|
480
|
+
sendParams
|
|
481
|
+
)
|
|
482
|
+
),
|
|
483
|
+
description: truncateText(
|
|
484
|
+
text || "(empty)",
|
|
485
|
+
DISCORD_EMBED_DESCRIPTION_MAX_LENGTH
|
|
486
|
+
)
|
|
487
|
+
};
|
|
488
|
+
enqueueToThread(sessionID, { embeds: [embed] });
|
|
489
|
+
if (role === "user") {
|
|
490
|
+
await flushPending(sessionID);
|
|
491
|
+
} else if (shouldFlush(sessionID)) {
|
|
492
|
+
await flushPending(sessionID);
|
|
493
|
+
}
|
|
432
494
|
}
|
|
433
495
|
return {
|
|
434
496
|
event: async ({ event }) => {
|
|
@@ -465,7 +527,105 @@ var plugin = async ({ client }) => {
|
|
|
465
527
|
};
|
|
466
528
|
lastSessionInfo.set(sessionID, { title, shareUrl });
|
|
467
529
|
enqueueToThread(sessionID, { embeds: [embed] });
|
|
468
|
-
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
case "permission.updated": {
|
|
533
|
+
const p = event.properties;
|
|
534
|
+
const sessionID = p?.sessionID;
|
|
535
|
+
if (!sessionID) return;
|
|
536
|
+
const embed = {
|
|
537
|
+
title: "Permission required",
|
|
538
|
+
description: p?.title,
|
|
539
|
+
color: COLORS.warning,
|
|
540
|
+
timestamp: toIsoTimestamp(p?.time?.created),
|
|
541
|
+
fields: buildFields(
|
|
542
|
+
filterSendFields(
|
|
543
|
+
[
|
|
544
|
+
["sessionID", sessionID],
|
|
545
|
+
["permissionID", p?.id],
|
|
546
|
+
["type", p?.type],
|
|
547
|
+
["pattern", p?.pattern],
|
|
548
|
+
["messageID", p?.messageID],
|
|
549
|
+
["callID", p?.callID]
|
|
550
|
+
],
|
|
551
|
+
sendParams
|
|
552
|
+
)
|
|
553
|
+
)
|
|
554
|
+
};
|
|
555
|
+
const mention = buildPermissionMention();
|
|
556
|
+
enqueueToThread(sessionID, {
|
|
557
|
+
content: mention ? `${mention.content}` : void 0,
|
|
558
|
+
allowed_mentions: mention?.allowed_mentions,
|
|
559
|
+
embeds: [embed]
|
|
560
|
+
});
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
case "session.idle": {
|
|
564
|
+
const sessionID = event.properties?.sessionID;
|
|
565
|
+
if (!sessionID) return;
|
|
566
|
+
const embed = {
|
|
567
|
+
title: "Session completed",
|
|
568
|
+
color: COLORS.success,
|
|
569
|
+
fields: buildFields(
|
|
570
|
+
filterSendFields(
|
|
571
|
+
[["sessionID", sessionID]],
|
|
572
|
+
withForcedSendParams(sendParams, ["sessionID"])
|
|
573
|
+
)
|
|
574
|
+
)
|
|
575
|
+
};
|
|
576
|
+
const mention = buildCompleteMention();
|
|
577
|
+
enqueueToThread(sessionID, {
|
|
578
|
+
content: mention ? `${mention.content}` : void 0,
|
|
579
|
+
allowed_mentions: mention?.allowed_mentions,
|
|
580
|
+
embeds: [embed]
|
|
581
|
+
});
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
case "session.error": {
|
|
585
|
+
const p = event.properties;
|
|
586
|
+
const sessionID = p?.sessionID;
|
|
587
|
+
const errorStr = safeString(p?.error);
|
|
588
|
+
const embed = {
|
|
589
|
+
title: "Session error",
|
|
590
|
+
color: COLORS.error,
|
|
591
|
+
description: errorStr ? errorStr.length > DISCORD_EMBED_DESCRIPTION_MAX_LENGTH ? errorStr.slice(
|
|
592
|
+
0,
|
|
593
|
+
DISCORD_EMBED_DESCRIPTION_MAX_LENGTH - ELLIPSIS_LENGTH
|
|
594
|
+
) + ELLIPSIS : errorStr : void 0,
|
|
595
|
+
fields: buildFields(
|
|
596
|
+
filterSendFields(
|
|
597
|
+
[["sessionID", sessionID]],
|
|
598
|
+
withForcedSendParams(sendParams, [
|
|
599
|
+
"sessionID",
|
|
600
|
+
"projectID",
|
|
601
|
+
"directory"
|
|
602
|
+
])
|
|
603
|
+
)
|
|
604
|
+
)
|
|
605
|
+
};
|
|
606
|
+
if (!sessionID) return;
|
|
607
|
+
const mention = buildCompleteMention();
|
|
608
|
+
enqueueToThread(sessionID, {
|
|
609
|
+
content: mention ? `$Session error` : void 0,
|
|
610
|
+
allowed_mentions: mention?.allowed_mentions,
|
|
611
|
+
embeds: [embed]
|
|
612
|
+
});
|
|
613
|
+
await flushPending(sessionID);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
case "todo.updated": {
|
|
617
|
+
const p = event.properties;
|
|
618
|
+
const sessionID = p?.sessionID;
|
|
619
|
+
if (!sessionID) return;
|
|
620
|
+
const embed = {
|
|
621
|
+
title: "Todo updated",
|
|
622
|
+
color: COLORS.info,
|
|
623
|
+
fields: buildFields(
|
|
624
|
+
filterSendFields([["sessionID", sessionID]], sendParams)
|
|
625
|
+
),
|
|
626
|
+
description: buildTodoChecklist(p?.todos)
|
|
627
|
+
};
|
|
628
|
+
enqueueToThread(sessionID, { embeds: [embed] });
|
|
469
629
|
return;
|
|
470
630
|
}
|
|
471
631
|
case "message.updated": {
|
|
@@ -473,52 +633,22 @@ var plugin = async ({ client }) => {
|
|
|
473
633
|
const messageID = info?.id;
|
|
474
634
|
const role = info?.role;
|
|
475
635
|
if (!messageID) return;
|
|
476
|
-
if (role
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
});
|
|
493
|
-
setIfChanged(/* @__PURE__ */ new Map(), partID, snapshot);
|
|
494
|
-
continue;
|
|
495
|
-
}
|
|
496
|
-
if (role === "user" && !firstUserTextBySession.has(sessionID)) {
|
|
497
|
-
const normalized = normalizeThreadTitle(text);
|
|
498
|
-
if (normalized)
|
|
499
|
-
firstUserTextBySession.set(sessionID, normalized);
|
|
500
|
-
}
|
|
501
|
-
const embed = {
|
|
502
|
-
title: getTextPartEmbedTitle(role),
|
|
503
|
-
color: COLORS.info,
|
|
504
|
-
fields: buildFields(
|
|
505
|
-
filterSendFields(
|
|
506
|
-
[
|
|
507
|
-
["sessionID", sessionID],
|
|
508
|
-
["messageID", messageID],
|
|
509
|
-
["partID", partID],
|
|
510
|
-
["role", role]
|
|
511
|
-
],
|
|
512
|
-
sendParams
|
|
513
|
-
)
|
|
514
|
-
),
|
|
515
|
-
description: truncateText(text || "(empty)", 4096)
|
|
516
|
-
};
|
|
517
|
-
enqueueToThread(sessionID, { embeds: [embed] });
|
|
518
|
-
if (role === "user") await flushPending(sessionID);
|
|
519
|
-
else if (shouldFlush(sessionID)) await flushPending(sessionID);
|
|
520
|
-
}
|
|
521
|
-
}
|
|
636
|
+
if (role !== "user" && role !== "assistant") return;
|
|
637
|
+
messageRoleById.set(messageID, role);
|
|
638
|
+
const pendingParts = pendingTextPartsByMessageId.get(messageID);
|
|
639
|
+
if (!pendingParts?.length) return;
|
|
640
|
+
pendingTextPartsByMessageId.delete(messageID);
|
|
641
|
+
for (const part of pendingParts) {
|
|
642
|
+
const sessionID = part?.sessionID;
|
|
643
|
+
const partID = part?.id;
|
|
644
|
+
if (!sessionID || !partID || part?.type !== "text") continue;
|
|
645
|
+
await handleTextPart({
|
|
646
|
+
part,
|
|
647
|
+
role,
|
|
648
|
+
sessionID,
|
|
649
|
+
messageID,
|
|
650
|
+
replay: true
|
|
651
|
+
});
|
|
522
652
|
}
|
|
523
653
|
return;
|
|
524
654
|
}
|
|
@@ -538,46 +668,12 @@ var plugin = async ({ client }) => {
|
|
|
538
668
|
pendingTextPartsByMessageId.set(messageID, list);
|
|
539
669
|
return;
|
|
540
670
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
role,
|
|
548
|
-
skipped: "input_context"
|
|
549
|
-
});
|
|
550
|
-
setIfChanged(/* @__PURE__ */ new Map(), partID, snapshot);
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
if (role === "user" && !firstUserTextBySession.has(sessionID)) {
|
|
554
|
-
const normalized = normalizeThreadTitle(text);
|
|
555
|
-
if (normalized)
|
|
556
|
-
firstUserTextBySession.set(sessionID, normalized);
|
|
557
|
-
}
|
|
558
|
-
const embed = {
|
|
559
|
-
title: getTextPartEmbedTitle(role),
|
|
560
|
-
color: COLORS.info,
|
|
561
|
-
fields: buildFields(
|
|
562
|
-
filterSendFields(
|
|
563
|
-
[
|
|
564
|
-
["sessionID", sessionID],
|
|
565
|
-
["messageID", messageID],
|
|
566
|
-
["partID", partID],
|
|
567
|
-
["role", role]
|
|
568
|
-
],
|
|
569
|
-
sendParams
|
|
570
|
-
)
|
|
571
|
-
),
|
|
572
|
-
description: truncateText(text || "(empty)", 4096)
|
|
573
|
-
};
|
|
574
|
-
enqueueToThread(sessionID, { embeds: [embed] });
|
|
575
|
-
if (role === "user") {
|
|
576
|
-
await flushPending(sessionID);
|
|
577
|
-
} else if (shouldFlush(sessionID)) {
|
|
578
|
-
await flushPending(sessionID);
|
|
579
|
-
}
|
|
580
|
-
}
|
|
671
|
+
await handleTextPart({
|
|
672
|
+
part,
|
|
673
|
+
role,
|
|
674
|
+
sessionID,
|
|
675
|
+
messageID
|
|
676
|
+
});
|
|
581
677
|
return;
|
|
582
678
|
}
|
|
583
679
|
default:
|
|
@@ -593,7 +689,8 @@ plugin.__test__ = {
|
|
|
593
689
|
buildTodoChecklist,
|
|
594
690
|
buildFields,
|
|
595
691
|
toIsoTimestamp,
|
|
596
|
-
postDiscordWebhook
|
|
692
|
+
postDiscordWebhook,
|
|
693
|
+
getTodoStatusMarker
|
|
597
694
|
};
|
|
598
695
|
var index_default = plugin;
|
|
599
696
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-discord-notify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "A plugin that posts OpenCode events to a Discord webhook.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@opencode-ai/plugin": "1.0.194",
|
|
38
38
|
"@types/node": "^20.19.27",
|
|
39
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
39
40
|
"prettier": "^3.7.4",
|
|
40
41
|
"prettier-plugin-organize-imports": "^4.3.0",
|
|
41
42
|
"tsup": "^8.5.0",
|