backeer-opencode 0.1.1 → 0.1.2
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/CHANGELOG.md +8 -0
- package/README.md +20 -2
- package/package.json +1 -1
- package/src/index.js +147 -34
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `backeer-opencode` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.1.2 - 2026-09-01
|
|
6
|
+
|
|
7
|
+
- Replace raw HTTP log lines with compact Backeer console status messages.
|
|
8
|
+
- Add restrained TTY colors with `NO_COLOR` support.
|
|
9
|
+
- Suppress repeated connection errors for 30 seconds.
|
|
10
|
+
- Add `BACKEER_LOG_LEVEL` and the `logLevel` plugin option.
|
|
11
|
+
- Keep technical HTTP and error details in debug mode only.
|
|
12
|
+
|
|
5
13
|
## 0.1.1 - 2026-09-01
|
|
6
14
|
|
|
7
15
|
- Add `backeer-opencode configure` for hidden interactive token entry.
|
package/README.md
CHANGED
|
@@ -109,6 +109,7 @@ environment variables are supported:
|
|
|
109
109
|
| `BACKEER_NOTIFY_PERMISSIONS` | Set to `false` to disable permission notifications and remote replies |
|
|
110
110
|
| `BACKEER_NOTIFY_QUESTIONS` | Set to `false` to disable question notifications |
|
|
111
111
|
| `BACKEER_NOTIFY_TODOS` | Set to `false` to disable todo-completion notifications |
|
|
112
|
+
| `BACKEER_LOG_LEVEL` | `info` (default), `error`, `debug`, or `silent` |
|
|
112
113
|
|
|
113
114
|
For npm installations, non-secret options can also be supplied through the
|
|
114
115
|
OpenCode plugin tuple syntax:
|
|
@@ -128,8 +129,25 @@ OpenCode plugin tuple syntax:
|
|
|
128
129
|
}
|
|
129
130
|
```
|
|
130
131
|
|
|
131
|
-
Supported options are `origin`, `tokenFile`, `permissions`, `questions`,
|
|
132
|
-
`todos`. The token itself is deliberately not a plugin option.
|
|
132
|
+
Supported options are `origin`, `tokenFile`, `permissions`, `questions`,
|
|
133
|
+
`todos`, and `logLevel`. The token itself is deliberately not a plugin option.
|
|
134
|
+
|
|
135
|
+
## Console feedback
|
|
136
|
+
|
|
137
|
+
The plugin writes compact, single-line status messages instead of raw HTTP logs:
|
|
138
|
+
|
|
139
|
+
```text
|
|
140
|
+
◆ Backeer Permission request sent
|
|
141
|
+
◆ Backeer Question sent
|
|
142
|
+
◆ Backeer Todo completion sent
|
|
143
|
+
✓ Backeer Decision applied: Always allow
|
|
144
|
+
! Backeer Connection interrupted — retrying
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
TTY output uses restrained status colors and respects `NO_COLOR`. Repeated
|
|
148
|
+
connection errors are suppressed for 30 seconds. Normal `info` output omits HTTP
|
|
149
|
+
status codes and internal errors; set `BACKEER_LOG_LEVEL=debug` when technical
|
|
150
|
+
details are needed. Set it to `error` for failures only or `silent` for no output.
|
|
133
151
|
|
|
134
152
|
## Events and decisions
|
|
135
153
|
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -8,6 +8,14 @@ const RETRY_DELAYS_MS = [0, 500, 1500]
|
|
|
8
8
|
const MAX_COMPLETED_COMMANDS = 500
|
|
9
9
|
const MAX_TRACKED_ITEMS = 500
|
|
10
10
|
const ALLOWED_DECISIONS = new Set(["once", "always", "reject"])
|
|
11
|
+
const DECISION_LABELS = {
|
|
12
|
+
once: "Allow once",
|
|
13
|
+
always: "Always allow",
|
|
14
|
+
reject: "Reject",
|
|
15
|
+
}
|
|
16
|
+
const LOG_LEVELS = new Set(["silent", "error", "info", "debug"])
|
|
17
|
+
const LOG_RANK = { silent: 0, error: 1, info: 2, debug: 3 }
|
|
18
|
+
const LOG_DEDUPLICATION_MS = 30_000
|
|
11
19
|
|
|
12
20
|
class HttpError extends Error {
|
|
13
21
|
constructor(message, status, responseBody = "") {
|
|
@@ -21,6 +29,61 @@ class HttpError extends Error {
|
|
|
21
29
|
const limit = (value, length = 1000) =>
|
|
22
30
|
String(value ?? "").slice(0, length)
|
|
23
31
|
|
|
32
|
+
function normalizeLogLevel(value) {
|
|
33
|
+
const level = String(value ?? "info").trim().toLowerCase()
|
|
34
|
+
return LOG_LEVELS.has(level) ? level : "info"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createConsoleReporter(levelValue) {
|
|
38
|
+
const level = normalizeLogLevel(levelValue)
|
|
39
|
+
const useColor = Boolean(process.stdout?.isTTY) &&
|
|
40
|
+
!("NO_COLOR" in process.env) && process.env.TERM !== "dumb"
|
|
41
|
+
const recentlyEmitted = new Map()
|
|
42
|
+
|
|
43
|
+
const decorate = (code, text) => useColor ? `\u001b[${code}m${text}\u001b[0m` : text
|
|
44
|
+
const symbols = {
|
|
45
|
+
info: decorate("36", "◆"),
|
|
46
|
+
success: decorate("32", "◆"),
|
|
47
|
+
applied: decorate("32", "✓"),
|
|
48
|
+
warning: decorate("33", "!"),
|
|
49
|
+
error: decorate("31", "×"),
|
|
50
|
+
debug: decorate("2", "·"),
|
|
51
|
+
}
|
|
52
|
+
const brand = decorate("1", "Backeer")
|
|
53
|
+
|
|
54
|
+
function emit(requiredLevel, kind, message, detail, deduplicate = false) {
|
|
55
|
+
if (LOG_RANK[level] < LOG_RANK[requiredLevel]) return
|
|
56
|
+
|
|
57
|
+
const key = `${kind}:${message}`
|
|
58
|
+
const now = Date.now()
|
|
59
|
+
if (deduplicate) {
|
|
60
|
+
const previous = recentlyEmitted.get(key)
|
|
61
|
+
if (previous && now - previous < LOG_DEDUPLICATION_MS) return
|
|
62
|
+
recentlyEmitted.delete(key)
|
|
63
|
+
recentlyEmitted.set(key, now)
|
|
64
|
+
while (recentlyEmitted.size > 100) {
|
|
65
|
+
recentlyEmitted.delete(recentlyEmitted.keys().next().value)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const technical = level === "debug" && detail
|
|
70
|
+
? decorate("2", ` · ${limit(detail, 500)}`)
|
|
71
|
+
: ""
|
|
72
|
+
const line = `${symbols[kind]} ${brand} ${message}${technical}`
|
|
73
|
+
if (kind === "error" || kind === "warning") console.error(line)
|
|
74
|
+
else console.log(line)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
info: (message, detail) => emit("info", "info", message, detail),
|
|
79
|
+
success: (message, detail) => emit("info", "success", message, detail),
|
|
80
|
+
applied: (message, detail) => emit("info", "applied", message, detail),
|
|
81
|
+
warning: (message, detail) => emit("error", "warning", message, detail, true),
|
|
82
|
+
error: (message, detail) => emit("error", "error", message, detail, true),
|
|
83
|
+
debug: (message, detail) => emit("debug", "debug", message, detail),
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
24
87
|
function defaultConfigDirectory() {
|
|
25
88
|
const base = process.env.XDG_CONFIG_HOME?.trim()
|
|
26
89
|
return base ? join(base, "opencode") : join(homedir(), ".config", "opencode")
|
|
@@ -69,6 +132,9 @@ function loadConfig(options = {}) {
|
|
|
69
132
|
options.questions ?? process.env.BACKEER_NOTIFY_QUESTIONS,
|
|
70
133
|
),
|
|
71
134
|
todos: enabled(options.todos ?? process.env.BACKEER_NOTIFY_TODOS),
|
|
135
|
+
logLevel: normalizeLogLevel(
|
|
136
|
+
options.logLevel ?? process.env.BACKEER_LOG_LEVEL,
|
|
137
|
+
),
|
|
72
138
|
}
|
|
73
139
|
}
|
|
74
140
|
|
|
@@ -159,7 +225,7 @@ function permissionIdentity(request) {
|
|
|
159
225
|
}
|
|
160
226
|
}
|
|
161
227
|
|
|
162
|
-
async function publishPermission(config, request, directory, identity) {
|
|
228
|
+
async function publishPermission(config, reporter, request, directory, identity) {
|
|
163
229
|
const permissionType = request.permission ?? request.action ?? "unknown"
|
|
164
230
|
const resources = Array.isArray(request.patterns)
|
|
165
231
|
? request.patterns.join(", ")
|
|
@@ -190,7 +256,7 @@ async function publishPermission(config, request, directory, identity) {
|
|
|
190
256
|
notify: true,
|
|
191
257
|
direction: "in",
|
|
192
258
|
})
|
|
193
|
-
|
|
259
|
+
reporter.success("Permission request sent", `HTTP ${result.status}`)
|
|
194
260
|
}
|
|
195
261
|
|
|
196
262
|
function questionIdentity(request) {
|
|
@@ -219,7 +285,7 @@ function formatQuestionOptions(options) {
|
|
|
219
285
|
.join(" | ")
|
|
220
286
|
}
|
|
221
287
|
|
|
222
|
-
async function resolveSessionTitle(client, directory, sessionID) {
|
|
288
|
+
async function resolveSessionTitle(client, directory, sessionID, reporter) {
|
|
223
289
|
if (typeof client?.session?.get !== "function") return ""
|
|
224
290
|
|
|
225
291
|
try {
|
|
@@ -230,12 +296,19 @@ async function resolveSessionTitle(client, directory, sessionID) {
|
|
|
230
296
|
})
|
|
231
297
|
return limit((result?.data ?? result)?.title)
|
|
232
298
|
} catch (error) {
|
|
233
|
-
|
|
299
|
+
reporter.debug("Session title unavailable", error)
|
|
234
300
|
return ""
|
|
235
301
|
}
|
|
236
302
|
}
|
|
237
303
|
|
|
238
|
-
async function publishQuestion(
|
|
304
|
+
async function publishQuestion(
|
|
305
|
+
config,
|
|
306
|
+
reporter,
|
|
307
|
+
request,
|
|
308
|
+
identity,
|
|
309
|
+
client,
|
|
310
|
+
directory,
|
|
311
|
+
) {
|
|
239
312
|
const questions = Array.isArray(request.questions) ? request.questions : []
|
|
240
313
|
if (questions.length === 0) return
|
|
241
314
|
|
|
@@ -243,6 +316,7 @@ async function publishQuestion(config, request, identity, client, directory) {
|
|
|
243
316
|
client,
|
|
244
317
|
directory,
|
|
245
318
|
identity.sessionID,
|
|
319
|
+
reporter,
|
|
246
320
|
)
|
|
247
321
|
const payload = [
|
|
248
322
|
{ key: "OpenCode session", value: sessionTitle },
|
|
@@ -282,7 +356,7 @@ async function publishQuestion(config, request, identity, client, directory) {
|
|
|
282
356
|
notify: true,
|
|
283
357
|
direction: "in",
|
|
284
358
|
})
|
|
285
|
-
|
|
359
|
+
reporter.success("Question sent", `HTTP ${result.status}`)
|
|
286
360
|
}
|
|
287
361
|
|
|
288
362
|
function stableHash(value) {
|
|
@@ -294,12 +368,20 @@ function stableHash(value) {
|
|
|
294
368
|
return (hash >>> 0).toString(16).padStart(8, "0")
|
|
295
369
|
}
|
|
296
370
|
|
|
297
|
-
async function publishTodoCompletion(
|
|
371
|
+
async function publishTodoCompletion(
|
|
372
|
+
config,
|
|
373
|
+
reporter,
|
|
374
|
+
request,
|
|
375
|
+
eventID,
|
|
376
|
+
client,
|
|
377
|
+
directory,
|
|
378
|
+
) {
|
|
298
379
|
const todos = request.todos
|
|
299
380
|
const sessionTitle = await resolveSessionTitle(
|
|
300
381
|
client,
|
|
301
382
|
directory,
|
|
302
383
|
request.sessionID,
|
|
384
|
+
reporter,
|
|
303
385
|
)
|
|
304
386
|
const correlation =
|
|
305
387
|
typeof eventID === "string" && eventID.length > 0
|
|
@@ -341,7 +423,7 @@ async function publishTodoCompletion(config, request, eventID, client, directory
|
|
|
341
423
|
notify: true,
|
|
342
424
|
direction: "in",
|
|
343
425
|
})
|
|
344
|
-
|
|
426
|
+
reporter.success("Todo completion sent", `HTTP ${result.status}`)
|
|
345
427
|
}
|
|
346
428
|
|
|
347
429
|
function parseSseFrame(frame) {
|
|
@@ -379,7 +461,7 @@ function nextSseFrame(buffer) {
|
|
|
379
461
|
return { frame: buffer.slice(0, lfIndex), rest: buffer.slice(lfIndex + 2) }
|
|
380
462
|
}
|
|
381
463
|
|
|
382
|
-
async function consumeSse(response, state, onCommand, signal) {
|
|
464
|
+
async function consumeSse(response, state, onCommand, signal, reporter) {
|
|
383
465
|
if (!response.body) throw new Error("Backeer command stream has no body")
|
|
384
466
|
|
|
385
467
|
const reader = response.body.getReader()
|
|
@@ -405,7 +487,7 @@ async function consumeSse(response, state, onCommand, signal) {
|
|
|
405
487
|
await onCommand(JSON.parse(message.data))
|
|
406
488
|
} catch (error) {
|
|
407
489
|
if (error instanceof SyntaxError) {
|
|
408
|
-
|
|
490
|
+
reporter.error("Invalid command received", "SSE data is not valid JSON")
|
|
409
491
|
continue
|
|
410
492
|
}
|
|
411
493
|
throw error
|
|
@@ -417,7 +499,11 @@ async function consumeSse(response, state, onCommand, signal) {
|
|
|
417
499
|
}
|
|
418
500
|
}
|
|
419
501
|
|
|
420
|
-
async function runCommandStream(
|
|
502
|
+
async function runCommandStream(
|
|
503
|
+
config,
|
|
504
|
+
reporter,
|
|
505
|
+
{ signal, hasPending, onCommand },
|
|
506
|
+
) {
|
|
421
507
|
const state = { lastEventID: "" }
|
|
422
508
|
let reconnectDelay = 1000
|
|
423
509
|
let lastLoggedError = ""
|
|
@@ -448,14 +534,21 @@ async function runCommandStream(config, { signal, hasPending, onCommand }) {
|
|
|
448
534
|
|
|
449
535
|
reconnectDelay = 1000
|
|
450
536
|
lastLoggedError = ""
|
|
451
|
-
await consumeSse(response, state, onCommand, signal)
|
|
537
|
+
await consumeSse(response, state, onCommand, signal, reporter)
|
|
452
538
|
} catch (error) {
|
|
453
539
|
if (signal.aborted || isAbortError(error)) return
|
|
454
540
|
const message = error instanceof HttpError
|
|
455
541
|
? `${error.message}: ${error.responseBody}`
|
|
456
542
|
: limit(error, 500)
|
|
457
543
|
if (message !== lastLoggedError) {
|
|
458
|
-
|
|
544
|
+
const authenticationFailure = error instanceof HttpError &&
|
|
545
|
+
[401, 403].includes(error.status)
|
|
546
|
+
reporter.error(
|
|
547
|
+
authenticationFailure
|
|
548
|
+
? "Authentication failed — run: npx backeer-opencode@latest configure"
|
|
549
|
+
: "Connection interrupted — retrying",
|
|
550
|
+
message,
|
|
551
|
+
)
|
|
459
552
|
lastLoggedError = message
|
|
460
553
|
}
|
|
461
554
|
if (error instanceof HttpError && [401, 403, 404].includes(error.status)) {
|
|
@@ -505,17 +598,23 @@ function rememberBounded(map, key, value) {
|
|
|
505
598
|
* protected file so it cannot accidentally be committed in opencode.json.
|
|
506
599
|
*/
|
|
507
600
|
export default async function BackeerPlugin({ client, directory }, options = {}) {
|
|
601
|
+
const bootstrapReporter = createConsoleReporter(
|
|
602
|
+
options.logLevel ?? process.env.BACKEER_LOG_LEVEL,
|
|
603
|
+
)
|
|
508
604
|
let config
|
|
509
605
|
try {
|
|
510
606
|
config = loadConfig(options)
|
|
511
607
|
} catch (error) {
|
|
512
|
-
|
|
608
|
+
bootstrapReporter.error("Plugin disabled — invalid configuration", error)
|
|
513
609
|
return {}
|
|
514
610
|
}
|
|
515
611
|
|
|
612
|
+
const reporter = createConsoleReporter(config.logLevel)
|
|
613
|
+
|
|
516
614
|
if (!config.token) {
|
|
517
|
-
|
|
518
|
-
|
|
615
|
+
reporter.error(
|
|
616
|
+
"Plugin disabled — run: npx backeer-opencode@latest configure",
|
|
617
|
+
`Token not found at ${config.tokenFile}`,
|
|
519
618
|
)
|
|
520
619
|
return {}
|
|
521
620
|
}
|
|
@@ -548,9 +647,8 @@ export default async function BackeerPlugin({ client, directory }, options = {})
|
|
|
548
647
|
|
|
549
648
|
const previousAcknowledgement = completedCommands.get(command.id)
|
|
550
649
|
if (previousAcknowledgement) {
|
|
551
|
-
await acknowledgeCommand(config, command.id, previousAcknowledgement)
|
|
552
|
-
(error) =>
|
|
553
|
-
)
|
|
650
|
+
await acknowledgeCommand(config, command.id, previousAcknowledgement)
|
|
651
|
+
.catch((error) => reporter.warning("Decision receipt retry failed", error))
|
|
554
652
|
return
|
|
555
653
|
}
|
|
556
654
|
if (processingCommands.has(command.id)) return
|
|
@@ -590,7 +688,7 @@ export default async function BackeerPlugin({ client, directory }, options = {})
|
|
|
590
688
|
errorCode: stale ? "permission_not_found" : "opencode_reply_failed",
|
|
591
689
|
}
|
|
592
690
|
if (stale) removePendingByRequestID(pending.requestID)
|
|
593
|
-
|
|
691
|
+
reporter.error("Decision could not be applied", error)
|
|
594
692
|
} finally {
|
|
595
693
|
processingCommands.delete(command.id)
|
|
596
694
|
}
|
|
@@ -602,8 +700,21 @@ export default async function BackeerPlugin({ client, directory }, options = {})
|
|
|
602
700
|
processedAt: new Date().toISOString(),
|
|
603
701
|
}
|
|
604
702
|
rememberBounded(completedCommands, command.id, acknowledgement)
|
|
703
|
+
|
|
704
|
+
if (acknowledgement.status === "applied") {
|
|
705
|
+
reporter.applied(
|
|
706
|
+
`Decision applied: ${DECISION_LABELS[acknowledgement.decision]}`,
|
|
707
|
+
)
|
|
708
|
+
} else if (acknowledgement.status === "expired") {
|
|
709
|
+
reporter.warning("Decision expired before it could be applied")
|
|
710
|
+
} else if (acknowledgement.status === "stale") {
|
|
711
|
+
reporter.warning("Permission was already answered locally")
|
|
712
|
+
} else if (acknowledgement.status === "rejected") {
|
|
713
|
+
reporter.warning("Decision was rejected by validation")
|
|
714
|
+
}
|
|
715
|
+
|
|
605
716
|
await acknowledgeCommand(config, command.id, acknowledgement).catch((error) => {
|
|
606
|
-
|
|
717
|
+
reporter.warning("Decision receipt could not be sent", error)
|
|
607
718
|
})
|
|
608
719
|
stopCommandStreamIfIdle()
|
|
609
720
|
}
|
|
@@ -614,7 +725,7 @@ export default async function BackeerPlugin({ client, directory }, options = {})
|
|
|
614
725
|
const controller = new AbortController()
|
|
615
726
|
const current = {
|
|
616
727
|
controller,
|
|
617
|
-
task: runCommandStream(config, {
|
|
728
|
+
task: runCommandStream(config, reporter, {
|
|
618
729
|
signal: controller.signal,
|
|
619
730
|
hasPending: () => pendingByExternalID.size > 0,
|
|
620
731
|
onCommand: handleCommand,
|
|
@@ -632,7 +743,7 @@ export default async function BackeerPlugin({ client, directory }, options = {})
|
|
|
632
743
|
if (config.permissions && event.type === "permission.asked") {
|
|
633
744
|
const identity = permissionIdentity(event.properties)
|
|
634
745
|
if (!identity) {
|
|
635
|
-
|
|
746
|
+
reporter.error("Permission event is missing correlation IDs")
|
|
636
747
|
return
|
|
637
748
|
}
|
|
638
749
|
|
|
@@ -642,9 +753,13 @@ export default async function BackeerPlugin({ client, directory }, options = {})
|
|
|
642
753
|
|
|
643
754
|
if (publishedPermissions.has(identity.externalId)) return
|
|
644
755
|
rememberBounded(publishedPermissions, identity.externalId, true)
|
|
645
|
-
void publishPermission(
|
|
646
|
-
|
|
647
|
-
|
|
756
|
+
void publishPermission(
|
|
757
|
+
config,
|
|
758
|
+
reporter,
|
|
759
|
+
event.properties,
|
|
760
|
+
directory,
|
|
761
|
+
identity,
|
|
762
|
+
).catch((error) => reporter.error("Permission request could not be sent", error))
|
|
648
763
|
return
|
|
649
764
|
}
|
|
650
765
|
|
|
@@ -657,7 +772,7 @@ export default async function BackeerPlugin({ client, directory }, options = {})
|
|
|
657
772
|
if (config.todos && event.type === "todo.updated") {
|
|
658
773
|
const { sessionID, todos } = event.properties
|
|
659
774
|
if (typeof sessionID !== "string" || !Array.isArray(todos)) {
|
|
660
|
-
|
|
775
|
+
reporter.error("Todo event is missing a valid session or list")
|
|
661
776
|
return
|
|
662
777
|
}
|
|
663
778
|
|
|
@@ -669,13 +784,12 @@ export default async function BackeerPlugin({ client, directory }, options = {})
|
|
|
669
784
|
|
|
670
785
|
void publishTodoCompletion(
|
|
671
786
|
config,
|
|
787
|
+
reporter,
|
|
672
788
|
event.properties,
|
|
673
789
|
event.id,
|
|
674
790
|
client,
|
|
675
791
|
directory,
|
|
676
|
-
).catch(
|
|
677
|
-
(error) => console.error(`[backeer] todo webhook failed: ${limit(error, 500)}`),
|
|
678
|
-
)
|
|
792
|
+
).catch((error) => reporter.error("Todo completion could not be sent", error))
|
|
679
793
|
return
|
|
680
794
|
}
|
|
681
795
|
|
|
@@ -685,7 +799,7 @@ export default async function BackeerPlugin({ client, directory }, options = {})
|
|
|
685
799
|
) {
|
|
686
800
|
const identity = questionIdentity(event.properties)
|
|
687
801
|
if (!identity) {
|
|
688
|
-
|
|
802
|
+
reporter.error("Question event is missing correlation IDs")
|
|
689
803
|
return
|
|
690
804
|
}
|
|
691
805
|
if (publishedQuestions.has(identity.externalId)) return
|
|
@@ -693,13 +807,12 @@ export default async function BackeerPlugin({ client, directory }, options = {})
|
|
|
693
807
|
|
|
694
808
|
void publishQuestion(
|
|
695
809
|
config,
|
|
810
|
+
reporter,
|
|
696
811
|
event.properties,
|
|
697
812
|
identity,
|
|
698
813
|
client,
|
|
699
814
|
directory,
|
|
700
|
-
).catch(
|
|
701
|
-
(error) => console.error(`[backeer] question webhook failed: ${limit(error, 500)}`),
|
|
702
|
-
)
|
|
815
|
+
).catch((error) => reporter.error("Question could not be sent", error))
|
|
703
816
|
}
|
|
704
817
|
},
|
|
705
818
|
dispose: async () => {
|