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.
Files changed (2) hide show
  1. package/dist/index.js +48 -27
  2. 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: value.length > 1024 ? value.slice(0, 1021) + "..." : 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 === "completed" ? "[\u2713]" : status === "in_progress" ? "[\u25B6]" : "[ ]";
132
- const line = `> ${marker} ${truncateText(content, 200)}`;
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 > maxDescription) {
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 <= maxDescription) {
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 === 429) {
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 * 1e3);
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 429 (rate limited). Waited ${Math.round(
212
- waitMs / 1e3
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 = 1e4;
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: 8e3 }
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 < toastCooldownMs) return;
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 < toastCooldownMs) return;
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, 100);
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) return fromSessionTitle.slice(0, 100);
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) return fromSessionId.slice(0, 100);
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(text || "(empty)", 4096)
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 > 4096 ? errorStr.slice(0, 4093) + "..." : errorStr : void 0,
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.3",
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",