diff --git a/docs/channels/discord.md b/docs/channels/discord.md index e377ee984..71f34f407 100644 --- a/docs/channels/discord.md +++ b/docs/channels/discord.md @@ -368,10 +368,10 @@ sender as `Member (PK:System)` to avoid accidental Discord pings. discord: { pluralkit: { enabled: true, - token: "pk_live_..." // optional; required for private systems - } - } - } + token: "pk_live_...", // optional; required for private systems + }, + }, + }, } ``` diff --git a/src/agents/bash-tools.exec.path.test.ts b/src/agents/bash-tools.exec.path.test.ts index 05ed50b70..4b2e7d964 100644 --- a/src/agents/bash-tools.exec.path.test.ts +++ b/src/agents/bash-tools.exec.path.test.ts @@ -53,6 +53,12 @@ const normalizeText = (value?: string) => .replace(/\r/g, "\n") .trim(); +const normalizePathEntries = (value?: string) => + normalizeText(value) + .split(/[:\s]+/) + .map((entry) => entry.trim()) + .filter(Boolean); + describe("exec PATH login shell merge", () => { const originalPath = process.env.PATH; @@ -74,9 +80,9 @@ describe("exec PATH login shell merge", () => { const tool = createExecTool({ host: "gateway", security: "full", ask: "off" }); const result = await tool.execute("call1", { command: "echo $PATH" }); - const text = normalizeText(result.content.find((c) => c.type === "text")?.text); + const entries = normalizePathEntries(result.content.find((c) => c.type === "text")?.text); - expect(text).toBe("/custom/bin:/opt/bin:/usr/bin"); + expect(entries).toEqual(["/custom/bin", "/opt/bin", "/usr/bin"]); expect(shellPathMock).toHaveBeenCalledTimes(1); }); @@ -96,9 +102,9 @@ describe("exec PATH login shell merge", () => { command: "echo $PATH", env: { PATH: "/explicit/bin" }, }); - const text = normalizeText(result.content.find((c) => c.type === "text")?.text); + const entries = normalizePathEntries(result.content.find((c) => c.type === "text")?.text); - expect(text).toBe("/explicit/bin"); + expect(entries).toEqual(["/explicit/bin"]); expect(shellPathMock).not.toHaveBeenCalled(); }); }); diff --git a/src/auto-reply/reply/commands-core.ts b/src/auto-reply/reply/commands-core.ts index 4c20d8f66..4559eae36 100644 --- a/src/auto-reply/reply/commands-core.ts +++ b/src/auto-reply/reply/commands-core.ts @@ -33,32 +33,35 @@ import { handleSubagentsCommand } from "./commands-subagents.js"; import { handleTtsCommands } from "./commands-tts.js"; import { routeReply } from "./route-reply.js"; -const HANDLERS: CommandHandler[] = [ - // Plugin commands are processed first, before built-in commands - handlePluginCommand, - handleBashCommand, - handleActivationCommand, - handleSendPolicyCommand, - handleUsageCommand, - handleRestartCommand, - handleTtsCommands, - handleHelpCommand, - handleCommandsListCommand, - handleStatusCommand, - handleAllowlistCommand, - handleApproveCommand, - handleContextCommand, - handleWhoamiCommand, - handleSubagentsCommand, - handleConfigCommand, - handleDebugCommand, - handleModelsCommand, - handleStopCommand, - handleCompactCommand, - handleAbortTrigger, -]; +let HANDLERS: CommandHandler[] | null = null; export async function handleCommands(params: HandleCommandsParams): Promise { + if (HANDLERS === null) { + HANDLERS = [ + // Plugin commands are processed first, before built-in commands + handlePluginCommand, + handleBashCommand, + handleActivationCommand, + handleSendPolicyCommand, + handleUsageCommand, + handleRestartCommand, + handleTtsCommands, + handleHelpCommand, + handleCommandsListCommand, + handleStatusCommand, + handleAllowlistCommand, + handleApproveCommand, + handleContextCommand, + handleWhoamiCommand, + handleSubagentsCommand, + handleConfigCommand, + handleDebugCommand, + handleModelsCommand, + handleStopCommand, + handleCompactCommand, + handleAbortTrigger, + ]; + } const resetMatch = params.command.commandBodyNormalized.match(/^\/(new|reset)(?:\s|$)/); const resetRequested = Boolean(resetMatch); if (resetRequested && !params.command.isAuthorizedSender) { diff --git a/src/config/types.discord.ts b/src/config/types.discord.ts index d2ca453be..283ab9e33 100644 --- a/src/config/types.discord.ts +++ b/src/config/types.discord.ts @@ -1,3 +1,4 @@ +import type { DiscordPluralKitConfig } from "../discord/pluralkit.js"; import type { BlockStreamingCoalesceConfig, DmPolicy, @@ -6,7 +7,6 @@ import type { OutboundRetryConfig, ReplyToMode, } from "./types.base.js"; -import type { DiscordPluralKitConfig } from "../discord/pluralkit.js"; import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js"; import type { DmConfig, ProviderCommandsConfig } from "./types.messages.js"; import type { GroupToolPolicyBySenderConfig, GroupToolPolicyConfig } from "./types.tools.js"; diff --git a/src/discord/monitor/message-handler.inbound-contract.test.ts b/src/discord/monitor/message-handler.inbound-contract.test.ts index cceab2ea3..784bff3bd 100644 --- a/src/discord/monitor/message-handler.inbound-contract.test.ts +++ b/src/discord/monitor/message-handler.inbound-contract.test.ts @@ -40,6 +40,7 @@ describe("discord processDiscordMessage inbound contract", () => { historyLimit: 0, mediaMaxBytes: 1024, textLimit: 4000, + sender: { label: "user" }, replyToMode: "off", ackReactionScope: "direct", groupPolicy: "open", diff --git a/src/discord/monitor/message-handler.preflight.ts b/src/discord/monitor/message-handler.preflight.ts index befdc7d42..16148f046 100644 --- a/src/discord/monitor/message-handler.preflight.ts +++ b/src/discord/monitor/message-handler.preflight.ts @@ -1,5 +1,8 @@ import { ChannelType, MessageType, type User } from "@buape/carbon"; - +import type { + DiscordMessagePreflightContext, + DiscordMessagePreflightParams, +} from "./message-handler.preflight.types.js"; import { hasControlCommand } from "../../auto-reply/command-detection.js"; import { shouldHandleTextCommands } from "../../auto-reply/commands-registry.js"; import { @@ -43,10 +46,6 @@ import { resolveDiscordSystemLocation, resolveTimestampMs, } from "./format.js"; -import type { - DiscordMessagePreflightContext, - DiscordMessagePreflightParams, -} from "./message-handler.preflight.types.js"; import { resolveDiscordChannelInfo, resolveDiscordMessageText } from "./message-utils.js"; import { resolveDiscordSenderIdentity, resolveDiscordWebhookId } from "./sender-identity.js"; import { resolveDiscordSystemEvent } from "./system-events.js"; @@ -55,7 +54,6 @@ import { resolveDiscordThreadChannel, resolveDiscordThreadParentInfo } from "./t export type { DiscordMessagePreflightContext, DiscordMessagePreflightParams, - DiscordSenderIdentity, } from "./message-handler.preflight.types.js"; export async function preflightDiscordMessage( diff --git a/src/discord/monitor/message-handler.process.test.ts b/src/discord/monitor/message-handler.process.test.ts index 534eede87..bffee48e0 100644 --- a/src/discord/monitor/message-handler.process.test.ts +++ b/src/discord/monitor/message-handler.process.test.ts @@ -26,7 +26,7 @@ vi.mock("../../auto-reply/reply/reply-dispatcher.js", () => ({ })), })); -import { processDiscordMessage } from "./message-handler.process.js"; +const { processDiscordMessage } = await import("./message-handler.process.js"); async function createBaseContext(overrides: Record = {}) { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-discord-")); @@ -102,6 +102,7 @@ describe("processDiscordMessage ack reactions", () => { const ctx = await createBaseContext({ shouldRequireMention: false, effectiveWasMentioned: false, + sender: { label: "user" }, }); await processDiscordMessage(ctx as any); @@ -113,6 +114,7 @@ describe("processDiscordMessage ack reactions", () => { const ctx = await createBaseContext({ shouldRequireMention: true, effectiveWasMentioned: true, + sender: { label: "user" }, }); await processDiscordMessage(ctx as any); diff --git a/src/discord/monitor/native-command.ts b/src/discord/monitor/native-command.ts index 332dcbf0b..f8b2a4f35 100644 --- a/src/discord/monitor/native-command.ts +++ b/src/discord/monitor/native-command.ts @@ -50,8 +50,8 @@ import { resolveDiscordGuildEntry, resolveDiscordUserAllowed, } from "./allow-list.js"; -import { resolveDiscordSenderIdentity } from "./sender-identity.js"; import { resolveDiscordChannelInfo } from "./message-utils.js"; +import { resolveDiscordSenderIdentity } from "./sender-identity.js"; import { resolveDiscordThreadParentInfo } from "./threading.js"; type DiscordConfig = NonNullable["discord"]; diff --git a/src/discord/monitor/sender-identity.ts b/src/discord/monitor/sender-identity.ts index ed3b1683b..aea870fdf 100644 --- a/src/discord/monitor/sender-identity.ts +++ b/src/discord/monitor/sender-identity.ts @@ -1,8 +1,6 @@ import type { User } from "@buape/carbon"; - -import { formatDiscordUserTag } from "./format.js"; -import type { DiscordMessageEvent } from "./listeners.js"; import type { PluralKitMessageInfo } from "../pluralkit.js"; +import { formatDiscordUserTag } from "./format.js"; export type DiscordSenderIdentity = { id: string; @@ -30,7 +28,7 @@ export function resolveDiscordWebhookId(message: DiscordWebhookMessageLike): str export function resolveDiscordSenderIdentity(params: { author: User; - member?: DiscordMessageEvent["member"] | null; + member?: any; pluralkitInfo?: PluralKitMessageInfo | null; }): DiscordSenderIdentity { const pkInfo = params.pluralkitInfo ?? null; @@ -75,7 +73,7 @@ export function resolveDiscordSenderIdentity(params: { export function resolveDiscordSenderLabel(params: { author: User; - member?: DiscordMessageEvent["member"] | null; + member?: any; pluralkitInfo?: PluralKitMessageInfo | null; }): string { return resolveDiscordSenderIdentity(params).label; diff --git a/src/slack/monitor.threading.missing-thread-ts.test.ts b/src/slack/monitor.threading.missing-thread-ts.test.ts index 31b95b0de..57e0d7ff0 100644 --- a/src/slack/monitor.threading.missing-thread-ts.test.ts +++ b/src/slack/monitor.threading.missing-thread-ts.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; -import { monitorSlackProvider } from "./monitor.js"; + +const { monitorSlackProvider } = await import("./monitor.js"); const sendMock = vi.fn(); const replyMock = vi.fn(); diff --git a/src/slack/monitor.tool-result.forces-thread-replies-replytoid-is-set.test.ts b/src/slack/monitor.tool-result.forces-thread-replies-replytoid-is-set.test.ts index 1906c7478..803e4eaff 100644 --- a/src/slack/monitor.tool-result.forces-thread-replies-replytoid-is-set.test.ts +++ b/src/slack/monitor.tool-result.forces-thread-replies-replytoid-is-set.test.ts @@ -1,6 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; -import { monitorSlackProvider } from "./monitor.js"; import { defaultSlackTestConfig, flush, @@ -11,6 +10,8 @@ import { waitForSlackEvent, } from "./monitor.test-helpers.js"; +const { monitorSlackProvider } = await import("./monitor.js"); + const slackTestState = getSlackTestState(); const { sendMock, replyMock, reactMock, upsertPairingRequestMock } = slackTestState; diff --git a/src/slack/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts b/src/slack/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts index eae8fad0e..d17684c31 100644 --- a/src/slack/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts +++ b/src/slack/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts @@ -2,7 +2,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { HISTORY_CONTEXT_MARKER } from "../auto-reply/reply/history.js"; import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; import { CURRENT_MESSAGE_MARKER } from "../auto-reply/reply/mentions.js"; -import { monitorSlackProvider } from "./monitor.js"; import { defaultSlackTestConfig, flush, @@ -13,6 +12,8 @@ import { waitForSlackEvent, } from "./monitor.test-helpers.js"; +const { monitorSlackProvider } = await import("./monitor.js"); + const slackTestState = getSlackTestState(); const { sendMock, replyMock } = slackTestState; diff --git a/src/slack/monitor.tool-result.threads-top-level-replies-replytomode-is-all.test.ts b/src/slack/monitor.tool-result.threads-top-level-replies-replytomode-is-all.test.ts index c0143355c..15a570ec4 100644 --- a/src/slack/monitor.tool-result.threads-top-level-replies-replytomode-is-all.test.ts +++ b/src/slack/monitor.tool-result.threads-top-level-replies-replytomode-is-all.test.ts @@ -1,6 +1,5 @@ import { beforeEach, describe, expect, it } from "vitest"; import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; -import { monitorSlackProvider } from "./monitor.js"; import { defaultSlackTestConfig, flush, @@ -11,6 +10,8 @@ import { waitForSlackEvent, } from "./monitor.test-helpers.js"; +const { monitorSlackProvider } = await import("./monitor.js"); + const slackTestState = getSlackTestState(); const { sendMock, replyMock } = slackTestState;