@nordbyte/nordrelay 0.7.0 → 0.8.1

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 (47) hide show
  1. package/.env.example +35 -0
  2. package/README.md +118 -49
  3. package/dist/activity-events.js +2 -2
  4. package/dist/adapter-conformance.js +61 -0
  5. package/dist/bot.js +18 -31
  6. package/dist/channel-adapter.js +33 -6
  7. package/dist/channel-command-catalog.js +6 -0
  8. package/dist/channel-command-core.js +60 -0
  9. package/dist/channel-command-service.js +20 -4
  10. package/dist/channel-mirror-registry.js +9 -2
  11. package/dist/channel-prompt-engine.js +177 -0
  12. package/dist/channel-turn-lifecycle.js +73 -0
  13. package/dist/config-metadata.js +67 -8
  14. package/dist/config.js +48 -1
  15. package/dist/context-key.js +32 -0
  16. package/dist/discord-bot.js +99 -327
  17. package/dist/index.js +9 -0
  18. package/dist/metrics.js +2 -0
  19. package/dist/peer-client.js +90 -2
  20. package/dist/peer-readiness.js +77 -0
  21. package/dist/peer-runtime-service.js +22 -0
  22. package/dist/peer-server.js +20 -4
  23. package/dist/peer-store.js +17 -2
  24. package/dist/relay-runtime-helpers.js +3 -1
  25. package/dist/relay-runtime.js +7 -0
  26. package/dist/settings-wizard-test.js +216 -0
  27. package/dist/slack-artifacts.js +165 -0
  28. package/dist/slack-bot.js +1461 -0
  29. package/dist/slack-channel-runtime.js +147 -0
  30. package/dist/slack-command-surface.js +46 -0
  31. package/dist/slack-diagnostics.js +116 -0
  32. package/dist/slack-rate-limit.js +139 -0
  33. package/dist/user-management-crypto.js +38 -0
  34. package/dist/user-management-normalize.js +188 -0
  35. package/dist/user-management-types.js +1 -0
  36. package/dist/user-management.js +193 -196
  37. package/dist/web-api-contract.js +8 -0
  38. package/dist/web-dashboard-access-routes.js +62 -0
  39. package/dist/web-dashboard-assets.js +1 -0
  40. package/dist/web-dashboard-pages.js +14 -4
  41. package/dist/web-dashboard-peer-routes.js +32 -11
  42. package/dist/web-dashboard.js +34 -0
  43. package/dist/web-state.js +2 -2
  44. package/dist/webui-assets/dashboard.css +193 -0
  45. package/dist/webui-assets/dashboard.js +546 -145
  46. package/package.json +3 -1
  47. package/plugins/nordrelay/scripts/nordrelay.mjs +105 -11
@@ -12,8 +12,10 @@ import { AuditLogStore } from "./audit-log.js";
12
12
  import { BotPreferencesStore } from "./bot-preferences.js";
13
13
  import { capabilitiesOf, filterActivityEvents, formatLocalDateTime, parseActivityOptions, renderExternalMirrorEvent, renderExternalMirrorStatus, renderPromptFailure, trimLine } from "./bot-rendering.js";
14
14
  import { renderAgentUpdateJobAction, renderAgentUpdateJobsAction, renderAgentUpdateLogAction, renderAgentUpdatePickerAction, renderQueueListAction } from "./channel-actions.js";
15
+ import { createSharedChannelCommandDispatcher } from "./channel-command-core.js";
15
16
  import { ChannelCommandService } from "./channel-command-service.js";
16
17
  import { discordHelpCommandList } from "./channel-command-catalog.js";
18
+ import { createChannelPromptEngine } from "./channel-prompt-engine.js";
17
19
  import { runChannelPeerPrompt } from "./channel-peer-prompt.js";
18
20
  import { deliverChannelAction } from "./channel-runtime.js";
19
21
  import { checkAuthStatus, startLogin as startCodexLogin, startLogout as startCodexLogout } from "./codex-auth.js";
@@ -595,157 +597,41 @@ export function createDiscordBridge(config, registry) {
595
597
  }
596
598
  const busyState = getBusyState(request.contextKey);
597
599
  busyState.processing = true;
598
- const typing = setInterval(() => {
599
- void runtime.sendTyping(request.context).catch(() => { });
600
- }, TYPING_INTERVAL_MS);
601
- void runtime.sendTyping(request.context).catch(() => { });
602
- let accumulatedText = "";
603
- let responseMessageId;
604
- let planMessageId;
605
- let flushTimer;
606
- let lastEditAt = 0;
607
- let running = true;
608
- let finalized = false;
609
- const toolCounts = new Map();
610
- const toolVerbosity = config.toolVerbosity;
611
- const startedAt = Date.now();
612
- const turnId = randomUUID().slice(0, 12);
613
- const progress = {
614
- status: "running",
600
+ const engine = createChannelPromptEngine({
601
+ runtime,
602
+ context: request.context,
603
+ contextKey: request.contextKey,
615
604
  promptDescription: envelope.description,
616
- startedAt,
617
- updatedAt: startedAt,
618
- toolCounts,
619
- textCharacters: 0,
620
- };
605
+ abortAction: `discord_abort:${request.contextKey}`,
606
+ trimMessage: trimDiscordMessage,
607
+ splitMessage: splitDiscordMessage,
608
+ editDebounceMs: EDIT_DEBOUNCE_MS,
609
+ typingIntervalMs: TYPING_INTERVAL_MS,
610
+ toolVerbosity: config.toolVerbosity,
611
+ logPrefix: "Discord",
612
+ onResponseMessage: (messageId) => responseOwners.set(messageId, request.contextKey),
613
+ onToolStart: (toolName) => appendActivity(request, {
614
+ status: "running",
615
+ type: "tool_started",
616
+ prompt: envelope.description,
617
+ detail: toolName,
618
+ threadId: session.getInfo().threadId,
619
+ workspace: session.getInfo().workspace,
620
+ agentId: session.getInfo().agentId,
621
+ }),
622
+ onToolEnd: (isError) => appendActivity(request, {
623
+ status: isError ? "failed" : "completed",
624
+ type: isError ? "tool_failed" : "tool_completed",
625
+ prompt: envelope.description,
626
+ detail: "tool",
627
+ threadId: session.getInfo().threadId,
628
+ workspace: session.getInfo().workspace,
629
+ agentId: session.getInfo().agentId,
630
+ }),
631
+ });
632
+ const progress = engine.progress;
621
633
  turnProgress.set(request.contextKey, progress);
622
- const scheduleFlush = () => {
623
- if (flushTimer || !running) {
624
- return;
625
- }
626
- const delay = Math.max(0, EDIT_DEBOUNCE_MS - (Date.now() - lastEditAt));
627
- flushTimer = setTimeout(() => {
628
- flushTimer = undefined;
629
- void flushResponse().catch((error) => console.error("Failed to edit Discord response:", error));
630
- }, delay);
631
- };
632
- const ensureResponse = async () => {
633
- if (responseMessageId)
634
- return;
635
- const preview = trimDiscordMessage(accumulatedText || "Working...");
636
- const sent = await runtime.sendMessage(request.context, {
637
- text: preview,
638
- fallbackText: preview,
639
- buttons: [[{ label: "Abort", action: `discord_abort:${request.contextKey}` }]],
640
- });
641
- responseMessageId = sent.messageId;
642
- responseOwners.set(responseMessageId, request.contextKey);
643
- lastEditAt = Date.now();
644
- };
645
- const flushResponse = async (force = false) => {
646
- if (!accumulatedText.trim())
647
- return;
648
- await ensureResponse();
649
- if (!responseMessageId)
650
- return;
651
- const now = Date.now();
652
- if (!force && now - lastEditAt < EDIT_DEBOUNCE_MS)
653
- return;
654
- await runtime.editMessage(request.context, responseMessageId, {
655
- text: trimDiscordMessage(accumulatedText),
656
- fallbackText: trimDiscordMessage(accumulatedText),
657
- buttons: [[{ label: "Abort", action: `discord_abort:${request.contextKey}` }]],
658
- });
659
- lastEditAt = Date.now();
660
- };
661
- const finalize = async () => {
662
- if (finalized) {
663
- return;
664
- }
665
- finalized = true;
666
- running = false;
667
- clearInterval(typing);
668
- if (flushTimer) {
669
- clearTimeout(flushTimer);
670
- flushTimer = undefined;
671
- }
672
- const finalText = accumulatedText.trim() || "Done.";
673
- const chunks = splitDiscordMessage(finalText);
674
- if (responseMessageId) {
675
- const [first, ...rest] = chunks;
676
- await runtime.editMessage(request.context, responseMessageId, { text: first ?? "Done.", fallbackText: first ?? "Done." });
677
- for (const chunk of rest) {
678
- await runtime.sendMessage(request.context, { text: chunk, fallbackText: chunk });
679
- }
680
- }
681
- else {
682
- for (const chunk of chunks) {
683
- await runtime.sendMessage(request.context, { text: chunk, fallbackText: chunk });
684
- }
685
- }
686
- };
687
- const callbacks = {
688
- onTextDelta: (delta) => {
689
- accumulatedText += delta;
690
- progress.textCharacters = accumulatedText.length;
691
- progress.updatedAt = Date.now();
692
- void ensureResponse().then(() => scheduleFlush()).catch((error) => console.error("Failed to send Discord response:", error));
693
- },
694
- onToolStart: (toolName) => {
695
- toolCounts.set(toolName, (toolCounts.get(toolName) ?? 0) + 1);
696
- progress.currentTool = toolName;
697
- progress.lastTool = toolName;
698
- progress.updatedAt = Date.now();
699
- appendActivity(request, {
700
- status: "running",
701
- type: "tool_started",
702
- prompt: envelope.description,
703
- detail: toolName,
704
- threadId: session.getInfo().threadId,
705
- workspace: session.getInfo().workspace,
706
- agentId: session.getInfo().agentId,
707
- });
708
- if (toolVerbosity === "all") {
709
- void runtime.sendMessage(request.context, { text: `Tool started: ${toolName}`, fallbackText: `Tool started: ${toolName}` }).catch(() => { });
710
- }
711
- },
712
- onToolUpdate: () => { },
713
- onToolEnd: (_toolCallId, isError) => {
714
- progress.currentTool = undefined;
715
- progress.updatedAt = Date.now();
716
- appendActivity(request, {
717
- status: isError ? "failed" : "completed",
718
- type: isError ? "tool_failed" : "tool_completed",
719
- prompt: envelope.description,
720
- detail: "tool",
721
- threadId: session.getInfo().threadId,
722
- workspace: session.getInfo().workspace,
723
- agentId: session.getInfo().agentId,
724
- });
725
- },
726
- onTodoUpdate: (items) => {
727
- progress.updatedAt = Date.now();
728
- const text = [
729
- "Plan:",
730
- ...items.map((item) => `${item.completed ? "[x]" : "[ ]"} ${item.text}`),
731
- ].join("\n");
732
- if (!planMessageId) {
733
- void runtime.sendMessage(request.context, { text, fallbackText: text }).then((result) => {
734
- planMessageId = result.messageId;
735
- }).catch(() => { });
736
- }
737
- else {
738
- void runtime.editMessage(request.context, planMessageId, { text, fallbackText: text }).catch(() => { });
739
- }
740
- },
741
- onTurnComplete: () => { },
742
- onAgentEnd: () => {
743
- progress.status = "completed";
744
- progress.completedAt = Date.now();
745
- progress.updatedAt = progress.completedAt;
746
- void finalize().catch((error) => console.error("Failed to finalize Discord response:", error));
747
- },
748
- };
634
+ engine.start();
749
635
  try {
750
636
  const info = session.getInfo();
751
637
  if ((info.capabilities ?? capabilitiesOf(info)).auth) {
@@ -777,15 +663,15 @@ export function createDiscordBridge(config, registry) {
777
663
  workspace: currentInfo.workspace,
778
664
  description: envelope.description,
779
665
  });
780
- await session.prompt(envelope.input, callbacks);
666
+ await session.prompt(envelope.input, engine.callbacks);
781
667
  updateSession(request, session);
782
668
  progress.status = "completed";
783
669
  progress.completedAt = Date.now();
784
670
  progress.updatedAt = progress.completedAt;
785
- await finalize();
786
- await artifactService.persistWorkspaceArtifactsForTurn(session.getInfo().workspace, turnId, new Date(startedAt));
671
+ await engine.finalize();
672
+ await artifactService.persistWorkspaceArtifactsForTurn(session.getInfo().workspace, engine.turnId, new Date(engine.startedAt));
787
673
  if (config.discordAutoSendArtifacts) {
788
- await sendRecentDiscordArtifacts(artifactDeps, request, session, new Date(startedAt), turnId);
674
+ await sendRecentDiscordArtifacts(artifactDeps, request, session, new Date(engine.startedAt), engine.turnId);
789
675
  }
790
676
  appendActivity(request, {
791
677
  status: "completed",
@@ -794,7 +680,7 @@ export function createDiscordBridge(config, registry) {
794
680
  threadId: session.getInfo().threadId,
795
681
  workspace: session.getInfo().workspace,
796
682
  agentId: session.getInfo().agentId,
797
- durationMs: Date.now() - startedAt,
683
+ durationMs: Date.now() - engine.startedAt,
798
684
  });
799
685
  audit(request, {
800
686
  action: "prompt_completed",
@@ -810,13 +696,8 @@ export function createDiscordBridge(config, registry) {
810
696
  progress.completedAt = Date.now();
811
697
  progress.updatedAt = progress.completedAt;
812
698
  progress.error = friendlyErrorText(error);
813
- const errorText = renderPromptFailure(accumulatedText, error);
814
- if (responseMessageId) {
815
- await runtime.editMessage(request.context, responseMessageId, { text: trimDiscordMessage(errorText), fallbackText: trimDiscordMessage(errorText) }).catch(() => { });
816
- }
817
- else {
818
- await reply(request, errorText).catch(() => { });
819
- }
699
+ const errorText = renderPromptFailure(engine.accumulatedText(), error);
700
+ await engine.fail(errorText);
820
701
  appendActivity(request, {
821
702
  status: "failed",
822
703
  type: "prompt_failed",
@@ -825,7 +706,7 @@ export function createDiscordBridge(config, registry) {
825
706
  threadId: session.getInfo().threadId,
826
707
  workspace: session.getInfo().workspace,
827
708
  agentId: session.getInfo().agentId,
828
- durationMs: Date.now() - startedAt,
709
+ durationMs: Date.now() - engine.startedAt,
829
710
  });
830
711
  audit(request, {
831
712
  action: "prompt_failed",
@@ -838,8 +719,7 @@ export function createDiscordBridge(config, registry) {
838
719
  });
839
720
  }
840
721
  finally {
841
- running = false;
842
- clearInterval(typing);
722
+ engine.stop();
843
723
  busyState.processing = false;
844
724
  await drainQueue(request).catch((error) => console.error("Failed to drain Discord queue:", error));
845
725
  }
@@ -916,6 +796,58 @@ export function createDiscordBridge(config, registry) {
916
796
  if (state)
917
797
  state.artifactsDeliveredForTurnId = turnId;
918
798
  };
799
+ const commandDispatcher = createSharedChannelCommandDispatcher({
800
+ transport: "discord",
801
+ bindings: [
802
+ { names: ["start", "help"], handler: (request) => commandHelp(request) },
803
+ { names: ["channels"], handler: (request) => deliverChannelAction(runtime, request.context, commandService.renderChannels()).then(() => { }) },
804
+ { names: ["peers"], handler: (request) => deliverChannelAction(runtime, request.context, commandService.renderPeers()).then(() => { }) },
805
+ { names: ["target"], handler: (request, argument) => deliverChannelAction(runtime, request.context, commandService.renderTargetPreference({ source: "discord", contextKey: request.contextKey, argument, preferencesStore })).then(() => { }) },
806
+ { names: ["agents"], handler: (request) => deliverChannelAction(runtime, request.context, commandService.renderAgents()).then(() => { }) },
807
+ { names: ["agent"], handler: (request, argument) => commandAgent(request, argument) },
808
+ { names: ["auth"], handler: (request) => commandAuth(request) },
809
+ { names: ["login"], handler: (request) => commandLogin(request) },
810
+ { names: ["logout"], handler: (request) => commandLogout(request) },
811
+ { names: ["session"], handler: (request) => commandSession(request) },
812
+ { names: ["sessions"], handler: (request, argument) => commandSessions(request, argument) },
813
+ { names: ["new"], handler: (request, argument) => commandNew(request, argument) },
814
+ { names: ["switch", "attach"], handler: (request, argument) => commandSwitch(request, argument) },
815
+ { names: ["model"], handler: (request, argument) => commandModel(request, argument) },
816
+ { names: ["reasoning", "effort"], handler: (request, argument) => commandReasoning(request, argument) },
817
+ { names: ["fast"], handler: (request, argument) => commandFast(request, argument) },
818
+ { names: ["launch", "launch_profiles", "launch-profiles"], handler: (request, argument) => commandLaunch(request, argument) },
819
+ { names: ["queue"], handler: (request, argument) => commandQueue(request, argument) },
820
+ { names: ["clearqueue"], handler: (request) => { promptStore.clear(request.contextKey); return reply(request, "Queue cleared."); } },
821
+ { names: ["cancel"], handler: (request, argument) => commandQueue(request, `cancel ${argument}`) },
822
+ { names: ["abort", "stop"], handler: (request) => commandAbort(request) },
823
+ { names: ["retry"], handler: (request) => commandRetry(request) },
824
+ { names: ["sync"], handler: (request) => commandSync(request) },
825
+ { names: ["tasks", "progress"], handler: (request) => commandProgress(request) },
826
+ { names: ["activity"], handler: (request, argument) => commandActivity(request, argument) },
827
+ { names: ["audit"], handler: (request, argument) => commandAudit(request, argument) },
828
+ { names: ["artifacts"], handler: (request, argument) => commandArtifacts(request, argument) },
829
+ { names: ["logs"], handler: (request, argument) => commandLogs(request, argument) },
830
+ { names: ["version", "health", "status"], handler: (request) => commandVersion(request) },
831
+ { names: ["diagnostics", "support"], handler: (request) => commandDiagnostics(request) },
832
+ { names: ["restart"], handler: (request) => commandRestart(request) },
833
+ { names: ["update"], handler: (request, argument) => commandUpdate(request, argument) },
834
+ { names: ["lock"], handler: (request) => commandLock(request) },
835
+ { names: ["unlock"], handler: (request) => { lockStore.clear(request.contextKey); return reply(request, "Session unlocked."); } },
836
+ { names: ["locks"], handler: (request) => reply(request, lockStore.list().map((lock) => `${lock.contextKey}: ${lock.ownerLabel || lock.ownerUserId}`).join("\n") || "No active locks.") },
837
+ { names: ["mirror"], handler: (request, argument) => commandMirror(request, argument) },
838
+ { names: ["notify"], handler: (request, argument) => commandNotify(request, argument) },
839
+ { names: ["voice"], handler: (request, argument) => commandVoice(request, argument) },
840
+ { names: ["workspaces"], handler: (request) => commandWorkspaces(request) },
841
+ { names: ["pin"], handler: (request, argument) => commandPin(request, argument) },
842
+ { names: ["unpin"], handler: (request, argument) => commandUnpin(request, argument) },
843
+ { names: ["pinned"], handler: (request) => commandPinned(request) },
844
+ { names: ["handback"], handler: (request) => commandHandback(request) },
845
+ { names: ["register_channel"], handler: (request) => commandRegisterChannel(request) },
846
+ { names: ["link"], handler: (request, argument) => commandLink(request, argument) },
847
+ { names: ["whoami"], handler: (request) => reply(request, request.authUser ? `${request.authUser.user.displayName} <${request.authUser.user.email}>\nGroups: ${request.authUser.groups.map((group) => group.name).join(", ")}` : "Not linked.") },
848
+ { names: ["prompt"], handler: (request, argument) => handlePrompt(request, argument) },
849
+ ],
850
+ });
919
851
  const handleCommand = async (request, command, argument) => {
920
852
  const normalized = command.toLowerCase();
921
853
  const permission = requiredPermissionForDiscordCommand(normalized, argument);
@@ -923,169 +855,9 @@ export function createDiscordBridge(config, registry) {
923
855
  return;
924
856
  }
925
857
  audit(request, { action: "command", status: "ok", description: `/${normalized} ${argument}`.trim() });
926
- switch (normalized) {
927
- case "start":
928
- case "help":
929
- await commandHelp(request);
930
- return;
931
- case "channels":
932
- await deliverChannelAction(runtime, request.context, commandService.renderChannels());
933
- return;
934
- case "peers":
935
- await deliverChannelAction(runtime, request.context, commandService.renderPeers());
936
- return;
937
- case "target":
938
- await deliverChannelAction(runtime, request.context, commandService.renderTargetPreference({
939
- source: "discord",
940
- contextKey: request.contextKey,
941
- argument,
942
- preferencesStore,
943
- }));
944
- return;
945
- case "agents":
946
- await deliverChannelAction(runtime, request.context, commandService.renderAgents());
947
- return;
948
- case "agent":
949
- await commandAgent(request, argument);
950
- return;
951
- case "auth":
952
- await commandAuth(request);
953
- return;
954
- case "login":
955
- await commandLogin(request);
956
- return;
957
- case "logout":
958
- await commandLogout(request);
959
- return;
960
- case "session":
961
- await commandSession(request);
962
- return;
963
- case "sessions":
964
- await commandSessions(request, argument);
965
- return;
966
- case "new":
967
- await commandNew(request, argument);
968
- return;
969
- case "switch":
970
- case "attach":
971
- await commandSwitch(request, argument);
972
- return;
973
- case "model":
974
- await commandModel(request, argument);
975
- return;
976
- case "reasoning":
977
- case "effort":
978
- await commandReasoning(request, argument);
979
- return;
980
- case "fast":
981
- await commandFast(request, argument);
982
- return;
983
- case "launch":
984
- case "launch_profiles":
985
- case "launch-profiles":
986
- await commandLaunch(request, argument);
987
- return;
988
- case "queue":
989
- await commandQueue(request, argument);
990
- return;
991
- case "clearqueue":
992
- promptStore.clear(request.contextKey);
993
- await reply(request, "Queue cleared.");
994
- return;
995
- case "cancel":
996
- await commandQueue(request, `cancel ${argument}`);
997
- return;
998
- case "abort":
999
- case "stop":
1000
- await commandAbort(request);
1001
- return;
1002
- case "retry":
1003
- await commandRetry(request);
1004
- return;
1005
- case "sync":
1006
- await commandSync(request);
1007
- return;
1008
- case "tasks":
1009
- case "progress":
1010
- await commandProgress(request);
1011
- return;
1012
- case "activity":
1013
- await commandActivity(request, argument);
1014
- return;
1015
- case "audit":
1016
- await commandAudit(request, argument);
1017
- return;
1018
- case "artifacts":
1019
- await commandArtifacts(request, argument);
1020
- return;
1021
- case "logs":
1022
- await commandLogs(request, argument);
1023
- return;
1024
- case "version":
1025
- case "health":
1026
- case "status":
1027
- await commandVersion(request);
1028
- return;
1029
- case "diagnostics":
1030
- await commandDiagnostics(request);
1031
- return;
1032
- case "support":
1033
- await commandDiagnostics(request);
1034
- return;
1035
- case "restart":
1036
- await commandRestart(request);
1037
- return;
1038
- case "update":
1039
- await commandUpdate(request, argument);
1040
- return;
1041
- case "lock":
1042
- await commandLock(request);
1043
- return;
1044
- case "unlock":
1045
- lockStore.clear(request.contextKey);
1046
- await reply(request, "Session unlocked.");
1047
- return;
1048
- case "locks":
1049
- await reply(request, lockStore.list().map((lock) => `${lock.contextKey}: ${lock.ownerLabel || lock.ownerUserId}`).join("\n") || "No active locks.");
1050
- return;
1051
- case "mirror":
1052
- await commandMirror(request, argument);
1053
- return;
1054
- case "notify":
1055
- await commandNotify(request, argument);
1056
- return;
1057
- case "voice":
1058
- await commandVoice(request, argument);
1059
- return;
1060
- case "workspaces":
1061
- await commandWorkspaces(request);
1062
- return;
1063
- case "pin":
1064
- await commandPin(request, argument);
1065
- return;
1066
- case "unpin":
1067
- await commandUnpin(request, argument);
1068
- return;
1069
- case "pinned":
1070
- await commandPinned(request);
1071
- return;
1072
- case "handback":
1073
- await commandHandback(request);
1074
- return;
1075
- case "register_channel":
1076
- await commandRegisterChannel(request);
1077
- return;
1078
- case "link":
1079
- await commandLink(request, argument);
1080
- return;
1081
- case "whoami":
1082
- await reply(request, request.authUser ? `${request.authUser.user.displayName} <${request.authUser.user.email}>\nGroups: ${request.authUser.groups.map((group) => group.name).join(", ")}` : "Not linked.");
1083
- return;
1084
- case "prompt":
1085
- await handlePrompt(request, argument);
1086
- return;
1087
- default:
1088
- await reply(request, `Unknown command: /${normalized}`);
858
+ const result = await commandDispatcher.dispatch(request, normalized, argument);
859
+ if (!result.matched) {
860
+ await reply(request, `Unknown command: /${normalized}`);
1089
861
  }
1090
862
  };
1091
863
  const commandHelp = async (request) => {
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { webhookCallback } from "grammy";
5
5
  import { agentLabel } from "./agent.js";
6
6
  import { createBot, registerCommands } from "./bot.js";
7
7
  import { createDiscordBridge } from "./discord-bot.js";
8
+ import { createSlackBridge } from "./slack-bot.js";
8
9
  import { checkAuthStatus } from "./codex-auth.js";
9
10
  import { describeCodexCli, resolveCodexCli } from "./codex-cli.js";
10
11
  import { checkClaudeCodeAuthStatus } from "./claude-code-auth.js";
@@ -27,6 +28,7 @@ import { UserStore } from "./user-management.js";
27
28
  let registry;
28
29
  let bot;
29
30
  let discordBridge;
31
+ let slackBridge;
30
32
  let webhookServer;
31
33
  let peerServer;
32
34
  let peerRuntime;
@@ -43,6 +45,8 @@ try {
43
45
  }
44
46
  discordBridge = createDiscordBridge(config, registry);
45
47
  await discordBridge?.start();
48
+ slackBridge = createSlackBridge(config, registry);
49
+ await slackBridge?.start();
46
50
  if (config.peerEnabled) {
47
51
  peerRuntime = new RelayRuntime(config);
48
52
  peerServer = await startPeerServer({ config, runtime: peerRuntime });
@@ -94,6 +98,7 @@ try {
94
98
  console.log("Session mode: per chat context");
95
99
  console.log(`Telegram: ${config.telegramEnabled ? config.telegramTransport : "disabled"}`);
96
100
  console.log(`Discord: ${config.discordEnabled ? "enabled" : "disabled"}`);
101
+ console.log(`Slack: ${config.slackEnabled ? (config.slackSocketMode ? "socket-mode" : `http:${config.slackPort}`) : "disabled"}`);
97
102
  console.log(`Peers: ${peerServer ? peerServer.url : "disabled"}`);
98
103
  await writeConnectorState({
99
104
  status: "ready",
@@ -111,6 +116,7 @@ try {
111
116
  openClawGateway: config.openClawGatewayUrl,
112
117
  telegramTransport: config.telegramTransport,
113
118
  discordEnabled: config.discordEnabled,
119
+ slackEnabled: config.slackEnabled,
114
120
  peerEnabled: config.peerEnabled,
115
121
  peerUrl: peerServer?.url,
116
122
  peerTlsFingerprint: peerServer?.tlsFingerprint,
@@ -164,6 +170,9 @@ const shutdown = (signal) => {
164
170
  void discordBridge?.stop().catch((error) => {
165
171
  console.warn("Failed to stop Discord bridge:", error instanceof Error ? error.message : String(error));
166
172
  });
173
+ void slackBridge?.stop().catch((error) => {
174
+ console.warn("Failed to stop Slack bridge:", error instanceof Error ? error.message : String(error));
175
+ });
167
176
  webhookServer?.close();
168
177
  void peerServer?.close().catch((error) => {
169
178
  console.warn("Failed to stop peer server:", error instanceof Error ? error.message : String(error));
package/dist/metrics.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { monitorEventLoopDelay } from "node:perf_hooks";
2
2
  import { getDiscordRateLimitMetrics } from "./discord-rate-limit.js";
3
+ import { getSlackRateLimitMetrics } from "./slack-rate-limit.js";
3
4
  import { getTelegramRateLimitMetrics } from "./telegram-rate-limit.js";
4
5
  const startedAt = Date.now();
5
6
  const eventLoopDelay = monitorEventLoopDelay({ resolution: 20 });
@@ -35,6 +36,7 @@ export function buildRuntimeMetrics(input) {
35
36
  adapters: {
36
37
  telegram: getTelegramRateLimitMetrics(),
37
38
  discord: getDiscordRateLimitMetrics(),
39
+ slack: getSlackRateLimitMetrics(),
38
40
  },
39
41
  };
40
42
  }