backeer-opencode 0.2.1 → 0.2.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 CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  All notable changes to `backeer-opencode` are documented here.
4
4
 
5
+ ## 0.2.2 - 2026-09-01
6
+
7
+ - Show user-facing Backeer delivery and decision results in the OpenCode
8
+ conversation instead of writing them into the TUI input area.
9
+ - Queue conversation feedback until the session is idle and mark it as ignored
10
+ so it cannot trigger another agent response or enter model context.
11
+ - Send technical diagnostics through OpenCode's structured application log API
12
+ instead of stdout.
13
+
5
14
  ## 0.2.1 - 2026-09-01
6
15
 
7
16
  - Add `/backeer doctor` for read-only local configuration, protected token,
package/README.md CHANGED
@@ -150,25 +150,28 @@ Supported options are `origin`, `tokenFile`, `permissions`, `questions`,
150
150
  `todos`, `lifecycle`, and `logLevel`. Use `"lifecycle": false` to disable the
151
151
  entire lifecycle group. The token itself is deliberately not a plugin option.
152
152
 
153
- ## Console feedback
153
+ ## Conversation feedback
154
154
 
155
- The plugin writes compact, single-line status messages instead of raw HTTP logs:
155
+ User-facing status messages appear as compact Backeer entries in the OpenCode
156
+ conversation:
156
157
 
157
158
  ```text
158
- ◆ Backeer Permission request sent
159
- ◆ Backeer Question sent
160
- ◆ Backeer Todo completion sent
161
- ◆ Backeer Task completion sent
162
- ◆ Backeer Retry warning sent
163
- ◆ Backeer Task failure sent
164
- ✓ Backeer Decision applied: Always allow
165
- ! Backeer Connection interrupted — retrying
159
+ Backeer ✓ Permission request sent
160
+ Backeer ✓ Question sent
161
+ Backeer ✓ Todo completion sent
162
+ Backeer ✓ Task completion sent
163
+ Backeer ✓ Decision applied: Always allow
166
164
  ```
167
165
 
168
- TTY output uses restrained status colors and respects `NO_COLOR`. Repeated
169
- connection errors are suppressed for 30 seconds. Normal `info` output omits HTTP
170
- status codes and internal errors; set `BACKEER_LOG_LEVEL=debug` when technical
171
- details are needed. Set it to `error` for failures only or `silent` for no output.
166
+ These entries are marked `noReply` and `ignored`, so they are visible without
167
+ starting another agent turn or entering model context. Feedback generated while
168
+ an agent is active is queued until the session becomes idle; it never writes to
169
+ the user's input field.
170
+
171
+ Technical diagnostics use OpenCode's structured application log API instead of
172
+ stdout. Normal `info` logging omits HTTP details; set
173
+ `BACKEER_LOG_LEVEL=debug` when technical details are needed. Set it to `error`
174
+ for failures only or `silent` to disable structured logs.
172
175
 
173
176
  ## OpenCode command
174
177
 
@@ -263,10 +266,11 @@ npm run publish:check
263
266
 
264
267
  ## Development architecture
265
268
 
266
- The npm plugin runs from the modular source in `src`. Configuration, console
267
- reporting, HTTP transport, SSE consumption, event publishing, permission command
268
- handling, and OpenCode event routing are kept in separate modules. The package
269
- root continues to expose only the default OpenCode plugin entrypoint.
269
+ The npm plugin runs from the modular source in `src`. Configuration, structured
270
+ logging, conversation feedback, HTTP transport, SSE consumption, event
271
+ publishing, permission command handling, and OpenCode event routing are kept in
272
+ separate modules. The package root continues to expose only the default OpenCode
273
+ plugin entrypoint.
270
274
 
271
275
  The legacy side-loaded installer still deploys one dependency-free file. Build
272
276
  that generated artifact before running the installer directly from a source
@@ -33,37 +33,31 @@ function normalizeLogLevel(value) {
33
33
  const level = String(value ?? "info").trim().toLowerCase();
34
34
  return LOG_LEVELS.has(level) ? level : "info";
35
35
  }
36
- function createConsoleReporter(levelValue) {
36
+ function createOpenCodeReporter(client, levelValue) {
37
37
  const level = normalizeLogLevel(levelValue);
38
- const useColor = Boolean(process.stdout?.isTTY) && !("NO_COLOR" in process.env) && process.env.TERM !== "dumb";
39
38
  const recentlyEmitted = /* @__PURE__ */ new Map();
40
- const decorate = (code, text) => useColor ? `\x1B[${code}m${text}\x1B[0m` : text;
41
- const symbols = {
42
- info: decorate("36", "\u25C6"),
43
- success: decorate("32", "\u25C6"),
44
- applied: decorate("32", "\u2713"),
45
- warning: decorate("33", "!"),
46
- error: decorate("31", "\xD7"),
47
- debug: decorate("2", "\xB7")
48
- };
49
- const brand = decorate("1", "Backeer");
39
+ const method = client?.app?.log;
50
40
  function emit(requiredLevel, kind, message, detail, deduplicate = false) {
51
- if (LOG_RANK[level] < LOG_RANK[requiredLevel]) return;
41
+ if (LOG_RANK[level] < LOG_RANK[requiredLevel] || typeof method !== "function") return;
52
42
  const key = `${kind}:${message}`;
53
43
  const now = Date.now();
54
44
  if (deduplicate) {
55
45
  const previous = recentlyEmitted.get(key);
56
46
  if (previous && now - previous < LOG_DEDUPLICATION_MS) return;
57
- recentlyEmitted.delete(key);
58
- recentlyEmitted.set(key, now);
59
- while (recentlyEmitted.size > 100) {
60
- recentlyEmitted.delete(recentlyEmitted.keys().next().value);
61
- }
47
+ rememberRecent(recentlyEmitted, key, now);
48
+ }
49
+ const body = {
50
+ service: "backeer-opencode",
51
+ level: kind === "warning" ? "warn" : kind === "applied" || kind === "success" ? "info" : kind,
52
+ message: limit(message, 500)
53
+ };
54
+ if (level === "debug" && detail) {
55
+ body.extra = { detail: limit(detail?.message ?? detail, 500) };
62
56
  }
63
- const technical = level === "debug" && detail ? decorate("2", ` \xB7 ${limit(detail, 500)}`) : "";
64
- const line = `${symbols[kind]} ${brand} ${message}${technical}`;
65
- if (kind === "error" || kind === "warning") console.error(line);
66
- else console.log(line);
57
+ setTimeout(() => {
58
+ void Promise.resolve(method.call(client.app, { body })).catch(() => {
59
+ });
60
+ }, 0);
67
61
  }
68
62
  return {
69
63
  info: (message, detail) => emit("info", "info", message, detail),
@@ -74,6 +68,11 @@ function createConsoleReporter(levelValue) {
74
68
  debug: (message, detail) => emit("debug", "debug", message, detail)
75
69
  };
76
70
  }
71
+ function rememberRecent(map, key, value) {
72
+ map.delete(key);
73
+ map.set(key, value);
74
+ while (map.size > 100) map.delete(map.keys().next().value);
75
+ }
77
76
 
78
77
  // src/config.js
79
78
  var DEFAULT_ORIGIN = "https://external-gateway.backeer.com";
@@ -245,7 +244,7 @@ function permissionIdentity(request) {
245
244
  externalId: `opencode:${sessionID}:${requestID}`
246
245
  };
247
246
  }
248
- async function publishPermission(config, reporter, request, directory, identity) {
247
+ async function publishPermission(config, reporter, request, directory, identity, conversation) {
249
248
  const permissionType = request.permission ?? request.action ?? "unknown";
250
249
  const resources = Array.isArray(request.patterns) ? request.patterns.join(", ") : Array.isArray(request.resources) ? request.resources.join(", ") : "";
251
250
  const payload = [
@@ -269,6 +268,7 @@ async function publishPermission(config, reporter, request, directory, identity)
269
268
  direction: "in"
270
269
  });
271
270
  reporter.success("Permission request sent", `HTTP ${result.status}`);
271
+ conversation?.success(identity.sessionID, "Permission request sent");
272
272
  }
273
273
  function questionIdentity(request) {
274
274
  const requestID = request.id ?? request.requestID;
@@ -303,7 +303,7 @@ async function resolveSessionTitle(client, directory, sessionID, reporter) {
303
303
  return "";
304
304
  }
305
305
  }
306
- async function publishQuestion(config, reporter, request, identity, client, directory) {
306
+ async function publishQuestion(config, reporter, request, identity, client, directory, conversation) {
307
307
  const questions = Array.isArray(request.questions) ? request.questions : [];
308
308
  if (questions.length === 0) return;
309
309
  const sessionTitle = await resolveSessionTitle(
@@ -345,8 +345,9 @@ async function publishQuestion(config, reporter, request, identity, client, dire
345
345
  direction: "in"
346
346
  });
347
347
  reporter.success("Question sent", `HTTP ${result.status}`);
348
+ conversation?.success(identity.sessionID, "Question sent");
348
349
  }
349
- async function publishTodoCompletion(config, reporter, request, eventID, client, directory) {
350
+ async function publishTodoCompletion(config, reporter, request, eventID, client, directory, conversation) {
350
351
  const todos = request.todos;
351
352
  const sessionTitle = await resolveSessionTitle(
352
353
  client,
@@ -384,6 +385,7 @@ async function publishTodoCompletion(config, reporter, request, eventID, client,
384
385
  direction: "in"
385
386
  });
386
387
  reporter.success("Todo completion sent", `HTTP ${result.status}`);
388
+ conversation?.success(request.sessionID, "Todo completion sent");
387
389
  }
388
390
 
389
391
  // src/management.js
@@ -402,7 +404,7 @@ import { randomUUID } from "node:crypto";
402
404
  import { statSync } from "node:fs";
403
405
 
404
406
  // src/updates.js
405
- var PLUGIN_VERSION = "0.2.1";
407
+ var PLUGIN_VERSION = "0.2.2";
406
408
  var PACKAGE_NAME = "backeer-opencode";
407
409
  var NPM_REGISTRY_URL = "https://registry.npmjs.org";
408
410
  var CHECK_TIMEOUT_MS = 3e3;
@@ -640,6 +642,111 @@ async function publishTestNotification(config, reporter) {
640
642
  return testID;
641
643
  }
642
644
 
645
+ // src/session-messages.js
646
+ var FEEDBACK_SYMBOLS = {
647
+ info: "\u2022",
648
+ success: "\u2713",
649
+ applied: "\u2713",
650
+ warning: "!",
651
+ error: "\xD7"
652
+ };
653
+ async function sendSessionMessage(client, sessionID, text) {
654
+ if (typeof client?.session?.prompt !== "function") {
655
+ throw new Error("OpenCode session messaging is unavailable");
656
+ }
657
+ await client.session.prompt({
658
+ path: { id: sessionID },
659
+ body: {
660
+ noReply: true,
661
+ parts: [{ type: "text", text, ignored: true }]
662
+ }
663
+ });
664
+ }
665
+ function formatBackeerFeedback(kind, message) {
666
+ const symbol = FEEDBACK_SYMBOLS[kind] ?? FEEDBACK_SYMBOLS.info;
667
+ return `> **Backeer** ${symbol} ${limit(message, 300)}`;
668
+ }
669
+ function createConversationFeedback({ client, reporter }) {
670
+ const sessions = /* @__PURE__ */ new Map();
671
+ let disposed = false;
672
+ function sessionState(sessionID) {
673
+ let state = sessions.get(sessionID);
674
+ if (!state) {
675
+ state = { idle: false, flushing: false, queue: [], queued: /* @__PURE__ */ new Set() };
676
+ rememberBounded(sessions, sessionID, state);
677
+ }
678
+ return state;
679
+ }
680
+ async function flush(sessionID, state) {
681
+ if (disposed || state.flushing || !state.idle || state.queue.length === 0) return;
682
+ state.flushing = true;
683
+ try {
684
+ while (!disposed && state.idle && state.queue.length > 0) {
685
+ const item = state.queue[0];
686
+ try {
687
+ await sendSessionMessage(
688
+ client,
689
+ sessionID,
690
+ formatBackeerFeedback(item.kind, item.message)
691
+ );
692
+ state.queue.shift();
693
+ state.queued.delete(item.key);
694
+ } catch (error) {
695
+ state.idle = false;
696
+ reporter.debug("Conversation feedback could not be displayed", error);
697
+ }
698
+ }
699
+ } finally {
700
+ state.flushing = false;
701
+ }
702
+ }
703
+ function enqueue(kind, sessionID, message) {
704
+ if (disposed || typeof sessionID !== "string" || !sessionID || !message) return;
705
+ const state = sessionState(sessionID);
706
+ const key = `${sessionID}:${kind}:${message}`;
707
+ if (state.queued.has(key)) return;
708
+ state.queue.push({ kind, message: limit(message, 300), key });
709
+ state.queued.add(key);
710
+ while (state.queue.length > 20) {
711
+ const removed = state.queue.shift();
712
+ state.queued.delete(removed.key);
713
+ }
714
+ if (state.idle) void flush(sessionID, state);
715
+ }
716
+ function setIdle(sessionID, idle) {
717
+ if (typeof sessionID !== "string" || !sessionID) return;
718
+ const state = sessionState(sessionID);
719
+ state.idle = idle;
720
+ if (idle) void flush(sessionID, state);
721
+ }
722
+ return {
723
+ info: (sessionID, message) => enqueue("info", sessionID, message),
724
+ success: (sessionID, message) => enqueue("success", sessionID, message),
725
+ applied: (sessionID, message) => enqueue("applied", sessionID, message),
726
+ warning: (sessionID, message) => enqueue("warning", sessionID, message),
727
+ error: (sessionID, message) => enqueue("error", sessionID, message),
728
+ handle(event) {
729
+ const sessionID = event?.properties?.sessionID;
730
+ if (typeof sessionID !== "string") return;
731
+ if (event.type === "session.status") {
732
+ const status = event.properties?.status?.type;
733
+ if (status === "idle") setIdle(sessionID, true);
734
+ else if (status === "busy" || status === "retry") setIdle(sessionID, false);
735
+ } else if (event.type === "session.idle") {
736
+ setIdle(sessionID, true);
737
+ } else if (event.type === "permission.asked" || event.type === "question.asked" || event.type === "question.v2.asked" || event.type === "todo.updated") {
738
+ setIdle(sessionID, false);
739
+ } else if (event.type === "session.deleted") {
740
+ sessions.delete(sessionID);
741
+ }
742
+ },
743
+ dispose() {
744
+ disposed = true;
745
+ sessions.clear();
746
+ }
747
+ };
748
+ }
749
+
643
750
  // src/management.js
644
751
  var COMMAND_DESCRIPTION = "Check, test, update, or manage Backeer: /backeer [status|doctor|test|update-check|update|token|token-file PATH]";
645
752
  function tokenFileState(path) {
@@ -766,18 +873,6 @@ function tokenInstructions() {
766
873
  "`token-file` activates the new token immediately; no OpenCode restart is required."
767
874
  ].join("\n");
768
875
  }
769
- async function sendCommandMessage(client, sessionID, text) {
770
- if (typeof client?.session?.prompt !== "function") {
771
- throw new Error("OpenCode session messaging is unavailable");
772
- }
773
- await client.session.prompt({
774
- path: { id: sessionID },
775
- body: {
776
- noReply: true,
777
- parts: [{ type: "text", text, ignored: true }]
778
- }
779
- });
780
- }
781
876
  function registerBackeerCommand(opencodeConfig) {
782
877
  opencodeConfig.command ??= {};
783
878
  opencodeConfig.command.backeer = {
@@ -859,7 +954,7 @@ Backeer is already current at version ${update.currentVersion}.` : "## Backeer u
859
954
  "Use `/backeer token` for secure token-change instructions."
860
955
  ].join("\n");
861
956
  }
862
- await sendCommandMessage(client, input.sessionID, message);
957
+ await sendSessionMessage(client, input.sessionID, message);
863
958
  };
864
959
  }
865
960
 
@@ -900,7 +995,7 @@ async function resolveSessionTitle2(client, directory, sessionID, reporter) {
900
995
  return "";
901
996
  }
902
997
  }
903
- async function publish(config, reporter, event, feedback) {
998
+ async function publish(config, reporter, conversation, sessionID, event, feedback) {
904
999
  const result = await postJsonWithRetry(config, config.eventsEndpoint, {
905
1000
  type: "application",
906
1001
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -909,6 +1004,7 @@ async function publish(config, reporter, event, feedback) {
909
1004
  ...event
910
1005
  });
911
1006
  reporter.success(feedback, `HTTP ${result.status}`);
1007
+ conversation?.success(sessionID, feedback);
912
1008
  }
913
1009
  function secondsLabel(durationMs) {
914
1010
  const seconds = Math.max(0, Math.round(durationMs / 1e3));
@@ -924,7 +1020,8 @@ async function publishSessionCompleted({
924
1020
  directory,
925
1021
  sessionID,
926
1022
  messageID,
927
- durationMs
1023
+ durationMs,
1024
+ conversation
928
1025
  }) {
929
1026
  const sessionTitle = await resolveSessionTitle2(
930
1027
  client,
@@ -934,7 +1031,7 @@ async function publishSessionCompleted({
934
1031
  );
935
1032
  const duration = secondsLabel(durationMs);
936
1033
  const location = sessionTitle ? ` \u201C${sessionTitle}\u201D` : "";
937
- await publish(config, reporter, {
1034
+ await publish(config, reporter, conversation, sessionID, {
938
1035
  severity: "info",
939
1036
  title: "OpenCode task completed",
940
1037
  body: `OpenCode finished${location} in ${duration}.`,
@@ -954,7 +1051,8 @@ async function publishSessionRetry({
954
1051
  sessionID,
955
1052
  correlation,
956
1053
  attempt,
957
- next
1054
+ next,
1055
+ conversation
958
1056
  }) {
959
1057
  const sessionTitle = await resolveSessionTitle2(
960
1058
  client,
@@ -963,7 +1061,7 @@ async function publishSessionRetry({
963
1061
  reporter
964
1062
  );
965
1063
  const retryAt = Number.isFinite(next) ? new Date(next).toISOString() : "";
966
- await publish(config, reporter, {
1064
+ await publish(config, reporter, conversation, sessionID, {
967
1065
  severity: "warning",
968
1066
  title: "OpenCode is retrying",
969
1067
  body: `OpenCode reached retry attempt ${attempt}. The operation is still running.`,
@@ -982,7 +1080,8 @@ async function publishSessionError({
982
1080
  directory,
983
1081
  sessionID,
984
1082
  correlation,
985
- error
1083
+ error,
1084
+ conversation
986
1085
  }) {
987
1086
  const sessionTitle = await resolveSessionTitle2(
988
1087
  client,
@@ -993,7 +1092,7 @@ async function publishSessionError({
993
1092
  const details = safeErrorDetails(error);
994
1093
  const location = sessionTitle ? ` in \u201C${sessionTitle}\u201D` : "";
995
1094
  const body = details.message ? `OpenCode failed${location}: ${details.message}` : `OpenCode failed${location}.`;
996
- await publish(config, reporter, {
1095
+ await publish(config, reporter, conversation, sessionID, {
997
1096
  // Backeer/iOS currently support info, warning and critical severities.
998
1097
  severity: "critical",
999
1098
  title: "OpenCode task failed",
@@ -1010,7 +1109,7 @@ async function publishSessionError({
1010
1109
  }
1011
1110
  async function publishOpenCodeUpdate({ config, reporter, version }) {
1012
1111
  const safeVersion = limit(version, 100);
1013
- await publish(config, reporter, {
1112
+ await publish(config, reporter, void 0, void 0, {
1014
1113
  severity: "info",
1015
1114
  title: "OpenCode update available",
1016
1115
  body: `OpenCode ${safeVersion} is available.`,
@@ -1038,6 +1137,7 @@ function createLifecycleController({
1038
1137
  reporter,
1039
1138
  client,
1040
1139
  directory,
1140
+ conversation,
1041
1141
  now = () => Date.now(),
1042
1142
  publishers = {}
1043
1143
  }) {
@@ -1063,8 +1163,11 @@ function createLifecycleController({
1063
1163
  }
1064
1164
  return state;
1065
1165
  }
1066
- function dispatch(label, operation) {
1067
- void Promise.resolve(operation).catch((error) => reporter.error(label, error));
1166
+ function dispatch(label, operation, sessionID) {
1167
+ void Promise.resolve(operation).catch((error) => {
1168
+ reporter.error(label, error);
1169
+ conversation?.error(sessionID, label);
1170
+ });
1068
1171
  }
1069
1172
  function beginCycle(state) {
1070
1173
  if (state.busy) return;
@@ -1092,8 +1195,10 @@ function createLifecycleController({
1092
1195
  directory,
1093
1196
  sessionID,
1094
1197
  messageID: limit(assistant.id, 200),
1095
- durationMs
1096
- })
1198
+ durationMs,
1199
+ conversation
1200
+ }),
1201
+ sessionID
1097
1202
  );
1098
1203
  }
1099
1204
  function handleMessage(event) {
@@ -1138,8 +1243,10 @@ function createLifecycleController({
1138
1243
  sessionID,
1139
1244
  correlation: limit(correlation, 200),
1140
1245
  attempt: status.attempt,
1141
- next: status.next
1142
- })
1246
+ next: status.next,
1247
+ conversation
1248
+ }),
1249
+ sessionID
1143
1250
  );
1144
1251
  }
1145
1252
  function handleError(event) {
@@ -1156,8 +1263,10 @@ function createLifecycleController({
1156
1263
  directory,
1157
1264
  sessionID,
1158
1265
  correlation: errorCorrelation(event, state),
1159
- error: event.properties?.error
1160
- })
1266
+ error: event.properties?.error,
1267
+ conversation
1268
+ }),
1269
+ sessionID
1161
1270
  );
1162
1271
  }
1163
1272
  function handleWaiting(event, waiting) {
@@ -1343,7 +1452,8 @@ function createPermissionController({
1343
1452
  config,
1344
1453
  reporter,
1345
1454
  client,
1346
- directory
1455
+ directory,
1456
+ conversation
1347
1457
  }) {
1348
1458
  const pendingByExternalID = /* @__PURE__ */ new Map();
1349
1459
  const externalIDByRequestID = /* @__PURE__ */ new Map();
@@ -1404,6 +1514,7 @@ function createPermissionController({
1404
1514
  };
1405
1515
  if (stale) removePendingByRequestID(pending.requestID);
1406
1516
  reporter.error("Decision could not be applied", error);
1517
+ conversation?.error(pending.sessionID, "Decision could not be applied");
1407
1518
  } finally {
1408
1519
  processingCommands.delete(command.id);
1409
1520
  }
@@ -1418,12 +1529,19 @@ function createPermissionController({
1418
1529
  reporter.applied(
1419
1530
  `Decision applied: ${DECISION_LABELS[acknowledgement.decision]}`
1420
1531
  );
1532
+ conversation?.applied(
1533
+ pending.sessionID,
1534
+ `Decision applied: ${DECISION_LABELS[acknowledgement.decision]}`
1535
+ );
1421
1536
  } else if (acknowledgement.status === "expired") {
1422
1537
  reporter.warning("Decision expired before it could be applied");
1538
+ conversation?.warning(pending.sessionID, "Decision expired before it could be applied");
1423
1539
  } else if (acknowledgement.status === "stale") {
1424
1540
  reporter.warning("Permission was already answered locally");
1541
+ conversation?.warning(pending.sessionID, "Permission was already answered locally");
1425
1542
  } else if (acknowledgement.status === "rejected") {
1426
1543
  reporter.warning("Decision was rejected by validation");
1544
+ conversation?.warning(pending.sessionID, "Decision was rejected by validation");
1427
1545
  }
1428
1546
  await acknowledgeCommand(config, command.id, acknowledgement).catch((error) => {
1429
1547
  reporter.warning("Decision receipt could not be sent", error);
@@ -1486,7 +1604,8 @@ function createPermissionController({
1486
1604
 
1487
1605
  // src/plugin.js
1488
1606
  async function BackeerPlugin({ client, directory }, options = {}) {
1489
- const bootstrapReporter = createConsoleReporter(
1607
+ const bootstrapReporter = createOpenCodeReporter(
1608
+ client,
1490
1609
  options.logLevel ?? process.env.BACKEER_LOG_LEVEL
1491
1610
  );
1492
1611
  let config;
@@ -1496,18 +1615,21 @@ async function BackeerPlugin({ client, directory }, options = {}) {
1496
1615
  bootstrapReporter.error("Plugin disabled \u2014 invalid configuration", error);
1497
1616
  return {};
1498
1617
  }
1499
- const reporter = createConsoleReporter(config.logLevel);
1618
+ const reporter = createOpenCodeReporter(client, config.logLevel);
1619
+ const conversation = createConversationFeedback({ client, reporter });
1500
1620
  const permissionController = createPermissionController({
1501
1621
  config,
1502
1622
  reporter,
1503
1623
  client,
1504
- directory
1624
+ directory,
1625
+ conversation
1505
1626
  });
1506
1627
  const lifecycleController = createLifecycleController({
1507
1628
  config,
1508
1629
  reporter,
1509
1630
  client,
1510
- directory
1631
+ directory,
1632
+ conversation
1511
1633
  });
1512
1634
  const todoCompletionBySessionID = /* @__PURE__ */ new Map();
1513
1635
  const publishedQuestions = /* @__PURE__ */ new Map();
@@ -1538,8 +1660,12 @@ async function BackeerPlugin({ client, directory }, options = {}) {
1538
1660
  reporter,
1539
1661
  event.properties,
1540
1662
  directory,
1541
- identity
1542
- ).catch((error) => reporter.error("Permission request could not be sent", error));
1663
+ identity,
1664
+ conversation
1665
+ ).catch((error) => {
1666
+ reporter.error("Permission request could not be sent", error);
1667
+ conversation.error(identity.sessionID, "Permission request could not be sent");
1668
+ });
1543
1669
  }
1544
1670
  async function handleTodoUpdated(event) {
1545
1671
  const { sessionID, todos } = event.properties;
@@ -1557,8 +1683,12 @@ async function BackeerPlugin({ client, directory }, options = {}) {
1557
1683
  event.properties,
1558
1684
  event.id,
1559
1685
  client,
1560
- directory
1561
- ).catch((error) => reporter.error("Todo completion could not be sent", error));
1686
+ directory,
1687
+ conversation
1688
+ ).catch((error) => {
1689
+ reporter.error("Todo completion could not be sent", error);
1690
+ conversation.error(sessionID, "Todo completion could not be sent");
1691
+ });
1562
1692
  }
1563
1693
  async function handleQuestionAsked(event) {
1564
1694
  const identity = questionIdentity(event.properties);
@@ -1574,8 +1704,12 @@ async function BackeerPlugin({ client, directory }, options = {}) {
1574
1704
  event.properties,
1575
1705
  identity,
1576
1706
  client,
1577
- directory
1578
- ).catch((error) => reporter.error("Question could not be sent", error));
1707
+ directory,
1708
+ conversation
1709
+ ).catch((error) => {
1710
+ reporter.error("Question could not be sent", error);
1711
+ conversation.error(identity.sessionID, "Question could not be sent");
1712
+ });
1579
1713
  }
1580
1714
  return {
1581
1715
  config: async (opencodeConfig) => {
@@ -1585,6 +1719,7 @@ async function BackeerPlugin({ client, directory }, options = {}) {
1585
1719
  "chat.message": handleUpdateNotice,
1586
1720
  event: async ({ event }) => {
1587
1721
  if (!config.token) return;
1722
+ conversation.handle(event);
1588
1723
  lifecycleController.handle(event);
1589
1724
  if (config.permissions && event.type === "permission.asked") {
1590
1725
  await handlePermissionAsked(event);
@@ -1609,6 +1744,7 @@ async function BackeerPlugin({ client, directory }, options = {}) {
1609
1744
  lifecycleController.dispose();
1610
1745
  todoCompletionBySessionID.clear();
1611
1746
  publishedQuestions.clear();
1747
+ conversation.dispose();
1612
1748
  }
1613
1749
  };
1614
1750
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backeer-opencode",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Backeer lifecycle notifications and remote permission decisions for OpenCode",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -21,6 +21,7 @@ export async function publishPermission(
21
21
  request,
22
22
  directory,
23
23
  identity,
24
+ conversation,
24
25
  ) {
25
26
  const permissionType = request.permission ?? request.action ?? "unknown"
26
27
  const resources = Array.isArray(request.patterns)
@@ -53,6 +54,7 @@ export async function publishPermission(
53
54
  direction: "in",
54
55
  })
55
56
  reporter.success("Permission request sent", `HTTP ${result.status}`)
57
+ conversation?.success(identity.sessionID, "Permission request sent")
56
58
  }
57
59
 
58
60
  export function questionIdentity(request) {
@@ -104,6 +106,7 @@ export async function publishQuestion(
104
106
  identity,
105
107
  client,
106
108
  directory,
109
+ conversation,
107
110
  ) {
108
111
  const questions = Array.isArray(request.questions) ? request.questions : []
109
112
  if (questions.length === 0) return
@@ -153,6 +156,7 @@ export async function publishQuestion(
153
156
  direction: "in",
154
157
  })
155
158
  reporter.success("Question sent", `HTTP ${result.status}`)
159
+ conversation?.success(identity.sessionID, "Question sent")
156
160
  }
157
161
 
158
162
  export async function publishTodoCompletion(
@@ -162,6 +166,7 @@ export async function publishTodoCompletion(
162
166
  eventID,
163
167
  client,
164
168
  directory,
169
+ conversation,
165
170
  ) {
166
171
  const todos = request.todos
167
172
  const sessionTitle = await resolveSessionTitle(
@@ -211,4 +216,5 @@ export async function publishTodoCompletion(
211
216
  direction: "in",
212
217
  })
213
218
  reporter.success("Todo completion sent", `HTTP ${result.status}`)
219
+ conversation?.success(request.sessionID, "Todo completion sent")
214
220
  }
@@ -20,6 +20,7 @@ export function createLifecycleController({
20
20
  reporter,
21
21
  client,
22
22
  directory,
23
+ conversation,
23
24
  now = () => Date.now(),
24
25
  publishers = {},
25
26
  }) {
@@ -47,8 +48,11 @@ export function createLifecycleController({
47
48
  return state
48
49
  }
49
50
 
50
- function dispatch(label, operation) {
51
- void Promise.resolve(operation).catch((error) => reporter.error(label, error))
51
+ function dispatch(label, operation, sessionID) {
52
+ void Promise.resolve(operation).catch((error) => {
53
+ reporter.error(label, error)
54
+ conversation?.error(sessionID, label)
55
+ })
52
56
  }
53
57
 
54
58
  function beginCycle(state) {
@@ -83,7 +87,9 @@ export function createLifecycleController({
83
87
  sessionID,
84
88
  messageID: limit(assistant.id, 200),
85
89
  durationMs,
90
+ conversation,
86
91
  }),
92
+ sessionID,
87
93
  )
88
94
  }
89
95
 
@@ -135,7 +141,9 @@ export function createLifecycleController({
135
141
  correlation: limit(correlation, 200),
136
142
  attempt: status.attempt,
137
143
  next: status.next,
144
+ conversation,
138
145
  }),
146
+ sessionID,
139
147
  )
140
148
  }
141
149
 
@@ -157,7 +165,9 @@ export function createLifecycleController({
157
165
  sessionID,
158
166
  correlation: errorCorrelation(event, state),
159
167
  error: event.properties?.error,
168
+ conversation,
160
169
  }),
170
+ sessionID,
161
171
  )
162
172
  }
163
173
 
@@ -53,7 +53,7 @@ async function resolveSessionTitle(client, directory, sessionID, reporter) {
53
53
  }
54
54
  }
55
55
 
56
- async function publish(config, reporter, event, feedback) {
56
+ async function publish(config, reporter, conversation, sessionID, event, feedback) {
57
57
  const result = await postJsonWithRetry(config, config.eventsEndpoint, {
58
58
  type: "application",
59
59
  timestamp: new Date().toISOString(),
@@ -62,6 +62,7 @@ async function publish(config, reporter, event, feedback) {
62
62
  ...event,
63
63
  })
64
64
  reporter.success(feedback, `HTTP ${result.status}`)
65
+ conversation?.success(sessionID, feedback)
65
66
  }
66
67
 
67
68
  function secondsLabel(durationMs) {
@@ -80,6 +81,7 @@ export async function publishSessionCompleted({
80
81
  sessionID,
81
82
  messageID,
82
83
  durationMs,
84
+ conversation,
83
85
  }) {
84
86
  const sessionTitle = await resolveSessionTitle(
85
87
  client,
@@ -89,7 +91,7 @@ export async function publishSessionCompleted({
89
91
  )
90
92
  const duration = secondsLabel(durationMs)
91
93
  const location = sessionTitle ? ` “${sessionTitle}”` : ""
92
- await publish(config, reporter, {
94
+ await publish(config, reporter, conversation, sessionID, {
93
95
  severity: "info",
94
96
  title: "OpenCode task completed",
95
97
  body: `OpenCode finished${location} in ${duration}.`,
@@ -111,6 +113,7 @@ export async function publishSessionRetry({
111
113
  correlation,
112
114
  attempt,
113
115
  next,
116
+ conversation,
114
117
  }) {
115
118
  const sessionTitle = await resolveSessionTitle(
116
119
  client,
@@ -119,7 +122,7 @@ export async function publishSessionRetry({
119
122
  reporter,
120
123
  )
121
124
  const retryAt = Number.isFinite(next) ? new Date(next).toISOString() : ""
122
- await publish(config, reporter, {
125
+ await publish(config, reporter, conversation, sessionID, {
123
126
  severity: "warning",
124
127
  title: "OpenCode is retrying",
125
128
  body: `OpenCode reached retry attempt ${attempt}. The operation is still running.`,
@@ -140,6 +143,7 @@ export async function publishSessionError({
140
143
  sessionID,
141
144
  correlation,
142
145
  error,
146
+ conversation,
143
147
  }) {
144
148
  const sessionTitle = await resolveSessionTitle(
145
149
  client,
@@ -152,7 +156,7 @@ export async function publishSessionError({
152
156
  const body = details.message
153
157
  ? `OpenCode failed${location}: ${details.message}`
154
158
  : `OpenCode failed${location}.`
155
- await publish(config, reporter, {
159
+ await publish(config, reporter, conversation, sessionID, {
156
160
  // Backeer/iOS currently support info, warning and critical severities.
157
161
  severity: "critical",
158
162
  title: "OpenCode task failed",
@@ -170,7 +174,7 @@ export async function publishSessionError({
170
174
 
171
175
  export async function publishOpenCodeUpdate({ config, reporter, version }) {
172
176
  const safeVersion = limit(version, 100)
173
- await publish(config, reporter, {
177
+ await publish(config, reporter, undefined, undefined, {
174
178
  severity: "info",
175
179
  title: "OpenCode update available",
176
180
  body: `OpenCode ${safeVersion} is available.`,
package/src/management.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  import { dirname, resolve } from "node:path"
10
10
 
11
11
  import { buildDoctorReport, publishTestNotification } from "./diagnostics.js"
12
+ import { sendSessionMessage } from "./session-messages.js"
12
13
  import {
13
14
  formatAgentUpdateInstruction,
14
15
  PLUGIN_VERSION,
@@ -156,19 +157,6 @@ function tokenInstructions() {
156
157
  ].join("\n")
157
158
  }
158
159
 
159
- async function sendCommandMessage(client, sessionID, text) {
160
- if (typeof client?.session?.prompt !== "function") {
161
- throw new Error("OpenCode session messaging is unavailable")
162
- }
163
- await client.session.prompt({
164
- path: { id: sessionID },
165
- body: {
166
- noReply: true,
167
- parts: [{ type: "text", text, ignored: true }],
168
- },
169
- })
170
- }
171
-
172
160
  export function registerBackeerCommand(opencodeConfig) {
173
161
  opencodeConfig.command ??= {}
174
162
  opencodeConfig.command.backeer = {
@@ -262,6 +250,6 @@ export function createBackeerCommandHandler({
262
250
  ].join("\n")
263
251
  }
264
252
 
265
- await sendCommandMessage(client, input.sessionID, message)
253
+ await sendSessionMessage(client, input.sessionID, message)
266
254
  }
267
255
  }
@@ -35,6 +35,7 @@ export function createPermissionController({
35
35
  reporter,
36
36
  client,
37
37
  directory,
38
+ conversation,
38
39
  }) {
39
40
  const pendingByExternalID = new Map()
40
41
  const externalIDByRequestID = new Map()
@@ -104,6 +105,7 @@ export function createPermissionController({
104
105
  }
105
106
  if (stale) removePendingByRequestID(pending.requestID)
106
107
  reporter.error("Decision could not be applied", error)
108
+ conversation?.error(pending.sessionID, "Decision could not be applied")
107
109
  } finally {
108
110
  processingCommands.delete(command.id)
109
111
  }
@@ -120,12 +122,19 @@ export function createPermissionController({
120
122
  reporter.applied(
121
123
  `Decision applied: ${DECISION_LABELS[acknowledgement.decision]}`,
122
124
  )
125
+ conversation?.applied(
126
+ pending.sessionID,
127
+ `Decision applied: ${DECISION_LABELS[acknowledgement.decision]}`,
128
+ )
123
129
  } else if (acknowledgement.status === "expired") {
124
130
  reporter.warning("Decision expired before it could be applied")
131
+ conversation?.warning(pending.sessionID, "Decision expired before it could be applied")
125
132
  } else if (acknowledgement.status === "stale") {
126
133
  reporter.warning("Permission was already answered locally")
134
+ conversation?.warning(pending.sessionID, "Permission was already answered locally")
127
135
  } else if (acknowledgement.status === "rejected") {
128
136
  reporter.warning("Decision was rejected by validation")
137
+ conversation?.warning(pending.sessionID, "Decision was rejected by validation")
129
138
  }
130
139
 
131
140
  await acknowledgeCommand(config, command.id, acknowledgement).catch((error) => {
package/src/plugin.js CHANGED
@@ -12,7 +12,8 @@ import {
12
12
  } from "./management.js"
13
13
  import { createLifecycleController } from "./lifecycle-controller.js"
14
14
  import { createPermissionController } from "./permission-controller.js"
15
- import { createConsoleReporter } from "./reporter.js"
15
+ import { createOpenCodeReporter } from "./reporter.js"
16
+ import { createConversationFeedback } from "./session-messages.js"
16
17
  import {
17
18
  createUpdateChecker,
18
19
  createUpdateNoticeHook,
@@ -28,7 +29,8 @@ import { rememberBounded } from "./utilities.js"
28
29
  * protected file so it cannot accidentally be committed in opencode.json.
29
30
  */
30
31
  export default async function BackeerPlugin({ client, directory }, options = {}) {
31
- const bootstrapReporter = createConsoleReporter(
32
+ const bootstrapReporter = createOpenCodeReporter(
33
+ client,
32
34
  options.logLevel ?? process.env.BACKEER_LOG_LEVEL,
33
35
  )
34
36
  let config
@@ -39,19 +41,22 @@ export default async function BackeerPlugin({ client, directory }, options = {})
39
41
  return {}
40
42
  }
41
43
 
42
- const reporter = createConsoleReporter(config.logLevel)
44
+ const reporter = createOpenCodeReporter(client, config.logLevel)
45
+ const conversation = createConversationFeedback({ client, reporter })
43
46
 
44
47
  const permissionController = createPermissionController({
45
48
  config,
46
49
  reporter,
47
50
  client,
48
51
  directory,
52
+ conversation,
49
53
  })
50
54
  const lifecycleController = createLifecycleController({
51
55
  config,
52
56
  reporter,
53
57
  client,
54
58
  directory,
59
+ conversation,
55
60
  })
56
61
  const todoCompletionBySessionID = new Map()
57
62
  const publishedQuestions = new Map()
@@ -86,7 +91,11 @@ export default async function BackeerPlugin({ client, directory }, options = {})
86
91
  event.properties,
87
92
  directory,
88
93
  identity,
89
- ).catch((error) => reporter.error("Permission request could not be sent", error))
94
+ conversation,
95
+ ).catch((error) => {
96
+ reporter.error("Permission request could not be sent", error)
97
+ conversation.error(identity.sessionID, "Permission request could not be sent")
98
+ })
90
99
  }
91
100
 
92
101
  async function handleTodoUpdated(event) {
@@ -109,7 +118,11 @@ export default async function BackeerPlugin({ client, directory }, options = {})
109
118
  event.id,
110
119
  client,
111
120
  directory,
112
- ).catch((error) => reporter.error("Todo completion could not be sent", error))
121
+ conversation,
122
+ ).catch((error) => {
123
+ reporter.error("Todo completion could not be sent", error)
124
+ conversation.error(sessionID, "Todo completion could not be sent")
125
+ })
113
126
  }
114
127
 
115
128
  async function handleQuestionAsked(event) {
@@ -128,7 +141,11 @@ export default async function BackeerPlugin({ client, directory }, options = {})
128
141
  identity,
129
142
  client,
130
143
  directory,
131
- ).catch((error) => reporter.error("Question could not be sent", error))
144
+ conversation,
145
+ ).catch((error) => {
146
+ reporter.error("Question could not be sent", error)
147
+ conversation.error(identity.sessionID, "Question could not be sent")
148
+ })
132
149
  }
133
150
 
134
151
  return {
@@ -143,6 +160,7 @@ export default async function BackeerPlugin({ client, directory }, options = {})
143
160
  event: async ({ event }) => {
144
161
  if (!config.token) return
145
162
 
163
+ conversation.handle(event)
146
164
  lifecycleController.handle(event)
147
165
 
148
166
  if (config.permissions && event.type === "permission.asked") {
@@ -175,6 +193,7 @@ export default async function BackeerPlugin({ client, directory }, options = {})
175
193
  lifecycleController.dispose()
176
194
  todoCompletionBySessionID.clear()
177
195
  publishedQuestions.clear()
196
+ conversation.dispose()
178
197
  },
179
198
  }
180
199
  }
package/src/reporter.js CHANGED
@@ -58,3 +58,53 @@ export function createConsoleReporter(levelValue) {
58
58
  debug: (message, detail) => emit("debug", "debug", message, detail),
59
59
  }
60
60
  }
61
+
62
+ export function createOpenCodeReporter(client, levelValue) {
63
+ const level = normalizeLogLevel(levelValue)
64
+ const recentlyEmitted = new Map()
65
+ const method = client?.app?.log
66
+
67
+ function emit(requiredLevel, kind, message, detail, deduplicate = false) {
68
+ if (LOG_RANK[level] < LOG_RANK[requiredLevel] || typeof method !== "function") return
69
+
70
+ const key = `${kind}:${message}`
71
+ const now = Date.now()
72
+ if (deduplicate) {
73
+ const previous = recentlyEmitted.get(key)
74
+ if (previous && now - previous < LOG_DEDUPLICATION_MS) return
75
+ rememberRecent(recentlyEmitted, key, now)
76
+ }
77
+
78
+ const body = {
79
+ service: "backeer-opencode",
80
+ level: kind === "warning" ? "warn" : kind === "applied" || kind === "success"
81
+ ? "info"
82
+ : kind,
83
+ message: limit(message, 500),
84
+ }
85
+ if (level === "debug" && detail) {
86
+ body.extra = { detail: limit(detail?.message ?? detail, 500) }
87
+ }
88
+
89
+ // Logging during plugin initialization can deadlock older OpenCode builds.
90
+ // Defer it and never fall back to stdout, because stdout corrupts the TUI prompt.
91
+ setTimeout(() => {
92
+ void Promise.resolve(method.call(client.app, { body })).catch(() => {})
93
+ }, 0)
94
+ }
95
+
96
+ return {
97
+ info: (message, detail) => emit("info", "info", message, detail),
98
+ success: (message, detail) => emit("info", "success", message, detail),
99
+ applied: (message, detail) => emit("info", "applied", message, detail),
100
+ warning: (message, detail) => emit("error", "warning", message, detail, true),
101
+ error: (message, detail) => emit("error", "error", message, detail, true),
102
+ debug: (message, detail) => emit("debug", "debug", message, detail),
103
+ }
104
+ }
105
+
106
+ function rememberRecent(map, key, value) {
107
+ map.delete(key)
108
+ map.set(key, value)
109
+ while (map.size > 100) map.delete(map.keys().next().value)
110
+ }
@@ -0,0 +1,122 @@
1
+ import { limit, rememberBounded } from "./utilities.js"
2
+
3
+ const FEEDBACK_SYMBOLS = {
4
+ info: "•",
5
+ success: "✓",
6
+ applied: "✓",
7
+ warning: "!",
8
+ error: "×",
9
+ }
10
+
11
+ export async function sendSessionMessage(client, sessionID, text) {
12
+ if (typeof client?.session?.prompt !== "function") {
13
+ throw new Error("OpenCode session messaging is unavailable")
14
+ }
15
+ await client.session.prompt({
16
+ path: { id: sessionID },
17
+ body: {
18
+ noReply: true,
19
+ parts: [{ type: "text", text, ignored: true }],
20
+ },
21
+ })
22
+ }
23
+
24
+ export function formatBackeerFeedback(kind, message) {
25
+ const symbol = FEEDBACK_SYMBOLS[kind] ?? FEEDBACK_SYMBOLS.info
26
+ return `> **Backeer** ${symbol} ${limit(message, 300)}`
27
+ }
28
+
29
+ export function createConversationFeedback({ client, reporter }) {
30
+ const sessions = new Map()
31
+ let disposed = false
32
+
33
+ function sessionState(sessionID) {
34
+ let state = sessions.get(sessionID)
35
+ if (!state) {
36
+ state = { idle: false, flushing: false, queue: [], queued: new Set() }
37
+ rememberBounded(sessions, sessionID, state)
38
+ }
39
+ return state
40
+ }
41
+
42
+ async function flush(sessionID, state) {
43
+ if (disposed || state.flushing || !state.idle || state.queue.length === 0) return
44
+ state.flushing = true
45
+ try {
46
+ while (!disposed && state.idle && state.queue.length > 0) {
47
+ const item = state.queue[0]
48
+ try {
49
+ await sendSessionMessage(
50
+ client,
51
+ sessionID,
52
+ formatBackeerFeedback(item.kind, item.message),
53
+ )
54
+ state.queue.shift()
55
+ state.queued.delete(item.key)
56
+ } catch (error) {
57
+ state.idle = false
58
+ reporter.debug("Conversation feedback could not be displayed", error)
59
+ }
60
+ }
61
+ } finally {
62
+ state.flushing = false
63
+ }
64
+ }
65
+
66
+ function enqueue(kind, sessionID, message) {
67
+ if (disposed || typeof sessionID !== "string" || !sessionID || !message) return
68
+ const state = sessionState(sessionID)
69
+ const key = `${sessionID}:${kind}:${message}`
70
+ if (state.queued.has(key)) return
71
+
72
+ state.queue.push({ kind, message: limit(message, 300), key })
73
+ state.queued.add(key)
74
+ while (state.queue.length > 20) {
75
+ const removed = state.queue.shift()
76
+ state.queued.delete(removed.key)
77
+ }
78
+ if (state.idle) void flush(sessionID, state)
79
+ }
80
+
81
+ function setIdle(sessionID, idle) {
82
+ if (typeof sessionID !== "string" || !sessionID) return
83
+ const state = sessionState(sessionID)
84
+ state.idle = idle
85
+ if (idle) void flush(sessionID, state)
86
+ }
87
+
88
+ return {
89
+ info: (sessionID, message) => enqueue("info", sessionID, message),
90
+ success: (sessionID, message) => enqueue("success", sessionID, message),
91
+ applied: (sessionID, message) => enqueue("applied", sessionID, message),
92
+ warning: (sessionID, message) => enqueue("warning", sessionID, message),
93
+ error: (sessionID, message) => enqueue("error", sessionID, message),
94
+
95
+ handle(event) {
96
+ const sessionID = event?.properties?.sessionID
97
+ if (typeof sessionID !== "string") return
98
+
99
+ if (event.type === "session.status") {
100
+ const status = event.properties?.status?.type
101
+ if (status === "idle") setIdle(sessionID, true)
102
+ else if (status === "busy" || status === "retry") setIdle(sessionID, false)
103
+ } else if (event.type === "session.idle") {
104
+ setIdle(sessionID, true)
105
+ } else if (
106
+ event.type === "permission.asked" ||
107
+ event.type === "question.asked" ||
108
+ event.type === "question.v2.asked" ||
109
+ event.type === "todo.updated"
110
+ ) {
111
+ setIdle(sessionID, false)
112
+ } else if (event.type === "session.deleted") {
113
+ sessions.delete(sessionID)
114
+ }
115
+ },
116
+
117
+ dispose() {
118
+ disposed = true
119
+ sessions.clear()
120
+ },
121
+ }
122
+ }
package/src/updates.js CHANGED
@@ -1,4 +1,4 @@
1
- export const PLUGIN_VERSION = "0.2.1"
1
+ export const PLUGIN_VERSION = "0.2.2"
2
2
 
3
3
  const PACKAGE_NAME = "backeer-opencode"
4
4
  const NPM_REGISTRY_URL = "https://registry.npmjs.org"