opencode-discord-notify 0.3.3 → 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 +48 -27
- 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,15 +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();
|
|
267
285
|
const sentTextPartIds = /* @__PURE__ */ new Set();
|
|
268
286
|
const showToast = async ({ title, message, variant }) => {
|
|
269
287
|
try {
|
|
270
288
|
await client.tui.showToast({
|
|
271
|
-
body: { title, message, variant, duration:
|
|
289
|
+
body: { title, message, variant, duration: TOAST_DURATION_MS }
|
|
272
290
|
});
|
|
273
291
|
} catch {
|
|
274
292
|
}
|
|
@@ -282,7 +300,7 @@ var plugin = async ({ client }) => {
|
|
|
282
300
|
if (!showErrorAlert) return;
|
|
283
301
|
const now = Date.now();
|
|
284
302
|
const last = lastAlertAtByKey.get(key);
|
|
285
|
-
if (last !== void 0 && now - last <
|
|
303
|
+
if (last !== void 0 && now - last < TOAST_COOLDOWN_MS) return;
|
|
286
304
|
lastAlertAtByKey.set(key, now);
|
|
287
305
|
await showToast({ title, message, variant });
|
|
288
306
|
};
|
|
@@ -290,7 +308,7 @@ var plugin = async ({ client }) => {
|
|
|
290
308
|
async function showMissingUrlToastOnce() {
|
|
291
309
|
const now = Date.now();
|
|
292
310
|
const last = lastAlertAtByKey.get(MISSING_URL_KEY);
|
|
293
|
-
if (last !== void 0 && now - last <
|
|
311
|
+
if (last !== void 0 && now - last < TOAST_COOLDOWN_MS) return;
|
|
294
312
|
lastAlertAtByKey.set(MISSING_URL_KEY, now);
|
|
295
313
|
await showToast({
|
|
296
314
|
title: "Discord webhook not configured",
|
|
@@ -318,15 +336,17 @@ var plugin = async ({ client }) => {
|
|
|
318
336
|
}
|
|
319
337
|
function buildThreadName(sessionID) {
|
|
320
338
|
const fromUser = normalizeThreadTitle(firstUserTextBySession.get(sessionID));
|
|
321
|
-
if (fromUser) return fromUser.slice(0,
|
|
339
|
+
if (fromUser) return fromUser.slice(0, DISCORD_THREAD_NAME_MAX_LENGTH);
|
|
322
340
|
const fromSessionTitle = normalizeThreadTitle(
|
|
323
341
|
lastSessionInfo.get(sessionID)?.title
|
|
324
342
|
);
|
|
325
|
-
if (fromSessionTitle)
|
|
343
|
+
if (fromSessionTitle)
|
|
344
|
+
return fromSessionTitle.slice(0, DISCORD_THREAD_NAME_MAX_LENGTH);
|
|
326
345
|
const fromSessionId = normalizeThreadTitle(
|
|
327
346
|
sessionID ? `session ${sessionID}` : ""
|
|
328
347
|
);
|
|
329
|
-
if (fromSessionId)
|
|
348
|
+
if (fromSessionId)
|
|
349
|
+
return fromSessionId.slice(0, DISCORD_THREAD_NAME_MAX_LENGTH);
|
|
330
350
|
return "untitled";
|
|
331
351
|
}
|
|
332
352
|
async function ensureThread(sessionID) {
|
|
@@ -460,7 +480,10 @@ var plugin = async ({ client }) => {
|
|
|
460
480
|
sendParams
|
|
461
481
|
)
|
|
462
482
|
),
|
|
463
|
-
description: truncateText(
|
|
483
|
+
description: truncateText(
|
|
484
|
+
text || "(empty)",
|
|
485
|
+
DISCORD_EMBED_DESCRIPTION_MAX_LENGTH
|
|
486
|
+
)
|
|
464
487
|
};
|
|
465
488
|
enqueueToThread(sessionID, { embeds: [embed] });
|
|
466
489
|
if (role === "user") {
|
|
@@ -469,12 +492,6 @@ var plugin = async ({ client }) => {
|
|
|
469
492
|
await flushPending(sessionID);
|
|
470
493
|
}
|
|
471
494
|
}
|
|
472
|
-
function setIfChanged(map, key, next) {
|
|
473
|
-
const prev = map.get(key);
|
|
474
|
-
if (prev === next) return false;
|
|
475
|
-
map.set(key, next);
|
|
476
|
-
return true;
|
|
477
|
-
}
|
|
478
495
|
return {
|
|
479
496
|
event: async ({ event }) => {
|
|
480
497
|
try {
|
|
@@ -571,7 +588,10 @@ var plugin = async ({ client }) => {
|
|
|
571
588
|
const embed = {
|
|
572
589
|
title: "Session error",
|
|
573
590
|
color: COLORS.error,
|
|
574
|
-
description: errorStr ? errorStr.length >
|
|
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,
|
|
575
595
|
fields: buildFields(
|
|
576
596
|
filterSendFields(
|
|
577
597
|
[["sessionID", sessionID]],
|
|
@@ -669,7 +689,8 @@ plugin.__test__ = {
|
|
|
669
689
|
buildTodoChecklist,
|
|
670
690
|
buildFields,
|
|
671
691
|
toIsoTimestamp,
|
|
672
|
-
postDiscordWebhook
|
|
692
|
+
postDiscordWebhook,
|
|
693
|
+
getTodoStatusMarker
|
|
673
694
|
};
|
|
674
695
|
var index_default = plugin;
|
|
675
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",
|