mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-07 22:09:57 +00:00
refactor!: rename chat providers to channels
This commit is contained in:
17
src/agents/channel-tools.ts
Normal file
17
src/agents/channel-tools.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { listChannelPlugins } from "../channels/plugins/index.js";
|
||||
import type { ChannelAgentTool } from "../channels/plugins/types.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
|
||||
export function listChannelAgentTools(params: {
|
||||
cfg?: ClawdbotConfig;
|
||||
}): ChannelAgentTool[] {
|
||||
// Channel docking: aggregate channel-owned tools (login, etc.).
|
||||
const tools: ChannelAgentTool[] = [];
|
||||
for (const plugin of listChannelPlugins()) {
|
||||
const entry = plugin.agentTools;
|
||||
if (!entry) continue;
|
||||
const resolved = typeof entry === "function" ? entry(params) : entry;
|
||||
if (Array.isArray(resolved)) tools.push(...resolved);
|
||||
}
|
||||
return tools;
|
||||
}
|
||||
@@ -73,14 +73,14 @@ describe("sessions tools", () => {
|
||||
kind: "direct",
|
||||
sessionId: "s-main",
|
||||
updatedAt: 10,
|
||||
lastProvider: "whatsapp",
|
||||
lastChannel: "whatsapp",
|
||||
},
|
||||
{
|
||||
key: "discord:group:dev",
|
||||
kind: "group",
|
||||
sessionId: "s-group",
|
||||
updatedAt: 11,
|
||||
provider: "discord",
|
||||
channel: "discord",
|
||||
displayName: "discord:g-dev",
|
||||
},
|
||||
{
|
||||
@@ -120,7 +120,7 @@ describe("sessions tools", () => {
|
||||
};
|
||||
expect(details.sessions).toHaveLength(3);
|
||||
const main = details.sessions?.find((s) => s.key === "main");
|
||||
expect(main?.provider).toBe("whatsapp");
|
||||
expect(main?.channel).toBe("whatsapp");
|
||||
expect(main?.messages?.length).toBe(1);
|
||||
expect(main?.messages?.[0]?.role).toBe("assistant");
|
||||
|
||||
@@ -233,7 +233,7 @@ describe("sessions tools", () => {
|
||||
|
||||
const tool = createClawdbotTools({
|
||||
agentSessionKey: requesterKey,
|
||||
agentProvider: "discord",
|
||||
agentChannel: "discord",
|
||||
}).find((candidate) => candidate.name === "sessions_send");
|
||||
expect(tool).toBeDefined();
|
||||
if (!tool) throw new Error("missing sessions_send tool");
|
||||
@@ -275,7 +275,7 @@ describe("sessions tools", () => {
|
||||
for (const call of agentCalls) {
|
||||
expect(call.params).toMatchObject({
|
||||
lane: "nested",
|
||||
provider: "webchat",
|
||||
channel: "webchat",
|
||||
});
|
||||
}
|
||||
expect(
|
||||
@@ -321,7 +321,7 @@ describe("sessions tools", () => {
|
||||
const replyByRunId = new Map<string, string>();
|
||||
const requesterKey = "discord:group:req";
|
||||
const targetKey = "discord:group:target";
|
||||
let sendParams: { to?: string; provider?: string; message?: string } = {};
|
||||
let sendParams: { to?: string; channel?: string; message?: string } = {};
|
||||
callGatewayMock.mockImplementation(async (opts: unknown) => {
|
||||
const request = opts as { method?: string; params?: unknown };
|
||||
calls.push(request);
|
||||
@@ -371,11 +371,11 @@ describe("sessions tools", () => {
|
||||
}
|
||||
if (request.method === "send") {
|
||||
const params = request.params as
|
||||
| { to?: string; provider?: string; message?: string }
|
||||
| { to?: string; channel?: string; message?: string }
|
||||
| undefined;
|
||||
sendParams = {
|
||||
to: params?.to,
|
||||
provider: params?.provider,
|
||||
channel: params?.channel,
|
||||
message: params?.message,
|
||||
};
|
||||
return { messageId: "m-announce" };
|
||||
@@ -385,7 +385,7 @@ describe("sessions tools", () => {
|
||||
|
||||
const tool = createClawdbotTools({
|
||||
agentSessionKey: requesterKey,
|
||||
agentProvider: "discord",
|
||||
agentChannel: "discord",
|
||||
}).find((candidate) => candidate.name === "sessions_send");
|
||||
expect(tool).toBeDefined();
|
||||
if (!tool) throw new Error("missing sessions_send tool");
|
||||
@@ -407,7 +407,7 @@ describe("sessions tools", () => {
|
||||
for (const call of agentCalls) {
|
||||
expect(call.params).toMatchObject({
|
||||
lane: "nested",
|
||||
provider: "webchat",
|
||||
channel: "webchat",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -423,7 +423,7 @@ describe("sessions tools", () => {
|
||||
expect(replySteps).toHaveLength(2);
|
||||
expect(sendParams).toMatchObject({
|
||||
to: "channel:target",
|
||||
provider: "discord",
|
||||
channel: "discord",
|
||||
message: "announce now",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,12 +37,12 @@ describe("subagents", () => {
|
||||
};
|
||||
});
|
||||
|
||||
it("sessions_spawn announces back to the requester group provider", async () => {
|
||||
it("sessions_spawn announces back to the requester group channel", async () => {
|
||||
resetSubagentRegistryForTests();
|
||||
callGatewayMock.mockReset();
|
||||
const calls: Array<{ method?: string; params?: unknown }> = [];
|
||||
let agentCallCount = 0;
|
||||
let sendParams: { to?: string; provider?: string; message?: string } = {};
|
||||
let sendParams: { to?: string; channel?: string; message?: string } = {};
|
||||
let deletedKey: string | undefined;
|
||||
let childRunId: string | undefined;
|
||||
let childSessionKey: string | undefined;
|
||||
@@ -58,7 +58,7 @@ describe("subagents", () => {
|
||||
const params = request.params as {
|
||||
message?: string;
|
||||
sessionKey?: string;
|
||||
provider?: string;
|
||||
channel?: string;
|
||||
timeout?: number;
|
||||
};
|
||||
const message = params?.message ?? "";
|
||||
@@ -69,7 +69,7 @@ describe("subagents", () => {
|
||||
childRunId = runId;
|
||||
childSessionKey = sessionKey;
|
||||
sessionLastAssistantText.set(sessionKey, "result");
|
||||
expect(params?.provider).toBe("discord");
|
||||
expect(params?.channel).toBe("discord");
|
||||
expect(params?.timeout).toBe(1);
|
||||
}
|
||||
return {
|
||||
@@ -96,11 +96,11 @@ describe("subagents", () => {
|
||||
}
|
||||
if (request.method === "send") {
|
||||
const params = request.params as
|
||||
| { to?: string; provider?: string; message?: string }
|
||||
| { to?: string; channel?: string; message?: string }
|
||||
| undefined;
|
||||
sendParams = {
|
||||
to: params?.to,
|
||||
provider: params?.provider,
|
||||
channel: params?.channel,
|
||||
message: params?.message,
|
||||
};
|
||||
return { messageId: "m-announce" };
|
||||
@@ -115,7 +115,7 @@ describe("subagents", () => {
|
||||
|
||||
const tool = createClawdbotTools({
|
||||
agentSessionKey: "discord:group:req",
|
||||
agentProvider: "discord",
|
||||
agentChannel: "discord",
|
||||
}).find((candidate) => candidate.name === "sessions_spawn");
|
||||
if (!tool) throw new Error("missing sessions_spawn tool");
|
||||
|
||||
@@ -153,22 +153,22 @@ describe("subagents", () => {
|
||||
lane?: string;
|
||||
deliver?: boolean;
|
||||
sessionKey?: string;
|
||||
provider?: string;
|
||||
channel?: string;
|
||||
}
|
||||
| undefined;
|
||||
expect(first?.lane).toBe("subagent");
|
||||
expect(first?.deliver).toBe(false);
|
||||
expect(first?.provider).toBe("discord");
|
||||
expect(first?.channel).toBe("discord");
|
||||
expect(first?.sessionKey?.startsWith("agent:main:subagent:")).toBe(true);
|
||||
expect(childSessionKey?.startsWith("agent:main:subagent:")).toBe(true);
|
||||
const second = agentCalls[1]?.params as
|
||||
| { provider?: string; deliver?: boolean; lane?: string }
|
||||
| { channel?: string; deliver?: boolean; lane?: string }
|
||||
| undefined;
|
||||
expect(second?.lane).toBe("nested");
|
||||
expect(second?.deliver).toBe(false);
|
||||
expect(second?.provider).toBe("webchat");
|
||||
expect(second?.channel).toBe("webchat");
|
||||
|
||||
expect(sendParams.provider).toBe("discord");
|
||||
expect(sendParams.channel).toBe("discord");
|
||||
expect(sendParams.to).toBe("channel:req");
|
||||
expect(sendParams.message ?? "").toContain("announce now");
|
||||
expect(sendParams.message ?? "").toContain("Stats:");
|
||||
@@ -180,7 +180,7 @@ describe("subagents", () => {
|
||||
callGatewayMock.mockReset();
|
||||
const calls: Array<{ method?: string; params?: unknown }> = [];
|
||||
let agentCallCount = 0;
|
||||
let sendParams: { to?: string; provider?: string; message?: string } = {};
|
||||
let sendParams: { to?: string; channel?: string; message?: string } = {};
|
||||
let deletedKey: string | undefined;
|
||||
let childRunId: string | undefined;
|
||||
let childSessionKey: string | undefined;
|
||||
@@ -196,7 +196,7 @@ describe("subagents", () => {
|
||||
const params = request.params as {
|
||||
message?: string;
|
||||
sessionKey?: string;
|
||||
provider?: string;
|
||||
channel?: string;
|
||||
timeout?: number;
|
||||
};
|
||||
const message = params?.message ?? "";
|
||||
@@ -207,7 +207,7 @@ describe("subagents", () => {
|
||||
childRunId = runId;
|
||||
childSessionKey = sessionKey;
|
||||
sessionLastAssistantText.set(sessionKey, "result");
|
||||
expect(params?.provider).toBe("discord");
|
||||
expect(params?.channel).toBe("discord");
|
||||
expect(params?.timeout).toBe(1);
|
||||
}
|
||||
return {
|
||||
@@ -238,11 +238,11 @@ describe("subagents", () => {
|
||||
}
|
||||
if (request.method === "send") {
|
||||
const params = request.params as
|
||||
| { to?: string; provider?: string; message?: string }
|
||||
| { to?: string; channel?: string; message?: string }
|
||||
| undefined;
|
||||
sendParams = {
|
||||
to: params?.to,
|
||||
provider: params?.provider,
|
||||
channel: params?.channel,
|
||||
message: params?.message,
|
||||
};
|
||||
return { messageId: "m-announce" };
|
||||
@@ -257,7 +257,7 @@ describe("subagents", () => {
|
||||
|
||||
const tool = createClawdbotTools({
|
||||
agentSessionKey: "discord:group:req",
|
||||
agentProvider: "discord",
|
||||
agentChannel: "discord",
|
||||
}).find((candidate) => candidate.name === "sessions_spawn");
|
||||
if (!tool) throw new Error("missing sessions_spawn tool");
|
||||
|
||||
@@ -282,13 +282,13 @@ describe("subagents", () => {
|
||||
const agentCalls = calls.filter((call) => call.method === "agent");
|
||||
expect(agentCalls).toHaveLength(2);
|
||||
const second = agentCalls[1]?.params as
|
||||
| { provider?: string; deliver?: boolean; lane?: string }
|
||||
| { channel?: string; deliver?: boolean; lane?: string }
|
||||
| undefined;
|
||||
expect(second?.lane).toBe("nested");
|
||||
expect(second?.deliver).toBe(false);
|
||||
expect(second?.provider).toBe("webchat");
|
||||
expect(second?.channel).toBe("webchat");
|
||||
|
||||
expect(sendParams.provider).toBe("discord");
|
||||
expect(sendParams.channel).toBe("discord");
|
||||
expect(sendParams.to).toBe("channel:req");
|
||||
expect(sendParams.message ?? "").toContain("announce now");
|
||||
expect(sendParams.message ?? "").toContain("Stats:");
|
||||
@@ -300,7 +300,7 @@ describe("subagents", () => {
|
||||
callGatewayMock.mockReset();
|
||||
const calls: Array<{ method?: string; params?: unknown }> = [];
|
||||
let agentCallCount = 0;
|
||||
let sendParams: { to?: string; provider?: string; message?: string } = {};
|
||||
let sendParams: { to?: string; channel?: string; message?: string } = {};
|
||||
let childRunId: string | undefined;
|
||||
let childSessionKey: string | undefined;
|
||||
const waitCalls: Array<{ runId?: string; timeoutMs?: number }> = [];
|
||||
@@ -314,7 +314,7 @@ describe("subagents", () => {
|
||||
sessions: [
|
||||
{
|
||||
key: "main",
|
||||
lastProvider: "whatsapp",
|
||||
lastChannel: "whatsapp",
|
||||
lastTo: "+123",
|
||||
},
|
||||
],
|
||||
@@ -360,11 +360,11 @@ describe("subagents", () => {
|
||||
}
|
||||
if (request.method === "send") {
|
||||
const params = request.params as
|
||||
| { to?: string; provider?: string; message?: string }
|
||||
| { to?: string; channel?: string; message?: string }
|
||||
| undefined;
|
||||
sendParams = {
|
||||
to: params?.to,
|
||||
provider: params?.provider,
|
||||
channel: params?.channel,
|
||||
message: params?.message,
|
||||
};
|
||||
return { messageId: "m1" };
|
||||
@@ -377,7 +377,7 @@ describe("subagents", () => {
|
||||
|
||||
const tool = createClawdbotTools({
|
||||
agentSessionKey: "main",
|
||||
agentProvider: "whatsapp",
|
||||
agentChannel: "whatsapp",
|
||||
}).find((candidate) => candidate.name === "sessions_spawn");
|
||||
if (!tool) throw new Error("missing sessions_spawn tool");
|
||||
|
||||
@@ -407,7 +407,7 @@ describe("subagents", () => {
|
||||
|
||||
const childWait = waitCalls.find((call) => call.runId === childRunId);
|
||||
expect(childWait?.timeoutMs).toBe(1000);
|
||||
expect(sendParams.provider).toBe("whatsapp");
|
||||
expect(sendParams.channel).toBe("whatsapp");
|
||||
expect(sendParams.to).toBe("+123");
|
||||
expect(sendParams.message ?? "").toContain("hello from sub");
|
||||
expect(sendParams.message ?? "").toContain("Stats:");
|
||||
@@ -420,7 +420,7 @@ describe("subagents", () => {
|
||||
|
||||
const tool = createClawdbotTools({
|
||||
agentSessionKey: "main",
|
||||
agentProvider: "whatsapp",
|
||||
agentChannel: "whatsapp",
|
||||
}).find((candidate) => candidate.name === "sessions_spawn");
|
||||
if (!tool) throw new Error("missing sessions_spawn tool");
|
||||
|
||||
@@ -470,7 +470,7 @@ describe("subagents", () => {
|
||||
|
||||
const tool = createClawdbotTools({
|
||||
agentSessionKey: "main",
|
||||
agentProvider: "whatsapp",
|
||||
agentChannel: "whatsapp",
|
||||
}).find((candidate) => candidate.name === "sessions_spawn");
|
||||
if (!tool) throw new Error("missing sessions_spawn tool");
|
||||
|
||||
@@ -522,7 +522,7 @@ describe("subagents", () => {
|
||||
|
||||
const tool = createClawdbotTools({
|
||||
agentSessionKey: "main",
|
||||
agentProvider: "whatsapp",
|
||||
agentChannel: "whatsapp",
|
||||
}).find((candidate) => candidate.name === "sessions_spawn");
|
||||
if (!tool) throw new Error("missing sessions_spawn tool");
|
||||
|
||||
@@ -574,7 +574,7 @@ describe("subagents", () => {
|
||||
|
||||
const tool = createClawdbotTools({
|
||||
agentSessionKey: "main",
|
||||
agentProvider: "whatsapp",
|
||||
agentChannel: "whatsapp",
|
||||
}).find((candidate) => candidate.name === "sessions_spawn");
|
||||
if (!tool) throw new Error("missing sessions_spawn tool");
|
||||
|
||||
@@ -612,7 +612,7 @@ describe("subagents", () => {
|
||||
|
||||
const tool = createClawdbotTools({
|
||||
agentSessionKey: "main",
|
||||
agentProvider: "whatsapp",
|
||||
agentChannel: "whatsapp",
|
||||
}).find((candidate) => candidate.name === "sessions_spawn");
|
||||
if (!tool) throw new Error("missing sessions_spawn tool");
|
||||
|
||||
@@ -710,7 +710,7 @@ describe("subagents", () => {
|
||||
|
||||
const tool = createClawdbotTools({
|
||||
agentSessionKey: "agent:main:main",
|
||||
agentProvider: "discord",
|
||||
agentChannel: "discord",
|
||||
}).find((candidate) => candidate.name === "sessions_spawn");
|
||||
if (!tool) throw new Error("missing sessions_spawn tool");
|
||||
|
||||
@@ -754,7 +754,7 @@ describe("subagents", () => {
|
||||
|
||||
const tool = createClawdbotTools({
|
||||
agentSessionKey: "agent:research:main",
|
||||
agentProvider: "discord",
|
||||
agentChannel: "discord",
|
||||
}).find((candidate) => candidate.name === "sessions_spawn");
|
||||
if (!tool) throw new Error("missing sessions_spawn tool");
|
||||
|
||||
@@ -804,7 +804,7 @@ describe("subagents", () => {
|
||||
|
||||
const tool = createClawdbotTools({
|
||||
agentSessionKey: "main",
|
||||
agentProvider: "whatsapp",
|
||||
agentChannel: "whatsapp",
|
||||
}).find((candidate) => candidate.name === "sessions_spawn");
|
||||
if (!tool) throw new Error("missing sessions_spawn tool");
|
||||
|
||||
@@ -840,7 +840,7 @@ describe("subagents", () => {
|
||||
|
||||
const tool = createClawdbotTools({
|
||||
agentSessionKey: "main",
|
||||
agentProvider: "whatsapp",
|
||||
agentChannel: "whatsapp",
|
||||
}).find((candidate) => candidate.name === "sessions_spawn");
|
||||
if (!tool) throw new Error("missing sessions_spawn tool");
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { resolvePluginTools } from "../plugins/tools.js";
|
||||
import type { GatewayMessageProvider } from "../utils/message-provider.js";
|
||||
import type { GatewayMessageChannel } from "../utils/message-channel.js";
|
||||
import { resolveSessionAgentId } from "./agent-scope.js";
|
||||
import { createAgentsListTool } from "./tools/agents-list-tool.js";
|
||||
import { createBrowserTool } from "./tools/browser-tool.js";
|
||||
@@ -28,7 +28,7 @@ export function createClawdbotTools(options?: {
|
||||
allowedControlHosts?: string[];
|
||||
allowedControlPorts?: number[];
|
||||
agentSessionKey?: string;
|
||||
agentProvider?: GatewayMessageProvider;
|
||||
agentChannel?: GatewayMessageChannel;
|
||||
agentAccountId?: string;
|
||||
agentDir?: string;
|
||||
sandboxRoot?: string;
|
||||
@@ -93,12 +93,12 @@ export function createClawdbotTools(options?: {
|
||||
}),
|
||||
createSessionsSendTool({
|
||||
agentSessionKey: options?.agentSessionKey,
|
||||
agentProvider: options?.agentProvider,
|
||||
agentChannel: options?.agentChannel,
|
||||
sandboxed: options?.sandboxed,
|
||||
}),
|
||||
createSessionsSpawnTool({
|
||||
agentSessionKey: options?.agentSessionKey,
|
||||
agentProvider: options?.agentProvider,
|
||||
agentChannel: options?.agentChannel,
|
||||
sandboxed: options?.sandboxed,
|
||||
}),
|
||||
createSessionStatusTool({
|
||||
@@ -121,7 +121,7 @@ export function createClawdbotTools(options?: {
|
||||
config: options?.config,
|
||||
}),
|
||||
sessionKey: options?.agentSessionKey,
|
||||
messageProvider: options?.agentProvider,
|
||||
messageChannel: options?.agentChannel,
|
||||
agentAccountId: options?.agentAccountId,
|
||||
sandboxed: options?.sandboxed,
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
getProviderPlugin,
|
||||
normalizeProviderId,
|
||||
} from "../providers/plugins/index.js";
|
||||
getChannelPlugin,
|
||||
normalizeChannelId,
|
||||
} from "../channels/plugins/index.js";
|
||||
|
||||
export type MessagingToolSend = {
|
||||
tool: string;
|
||||
@@ -15,8 +15,8 @@ const CORE_MESSAGING_TOOLS = new Set(["sessions_send", "message"]);
|
||||
// Provider docking: any plugin with `actions` opts into messaging tool handling.
|
||||
export function isMessagingTool(toolName: string): boolean {
|
||||
if (CORE_MESSAGING_TOOLS.has(toolName)) return true;
|
||||
const providerId = normalizeProviderId(toolName);
|
||||
return Boolean(providerId && getProviderPlugin(providerId)?.actions);
|
||||
const providerId = normalizeChannelId(toolName);
|
||||
return Boolean(providerId && getChannelPlugin(providerId)?.actions);
|
||||
}
|
||||
|
||||
export function isMessagingToolSendAction(
|
||||
@@ -28,9 +28,9 @@ export function isMessagingToolSendAction(
|
||||
if (toolName === "message") {
|
||||
return action === "send" || action === "thread-reply";
|
||||
}
|
||||
const providerId = normalizeProviderId(toolName);
|
||||
const providerId = normalizeChannelId(toolName);
|
||||
if (!providerId) return false;
|
||||
const plugin = getProviderPlugin(providerId);
|
||||
const plugin = getChannelPlugin(providerId);
|
||||
if (!plugin?.actions?.extractToolSend) return false;
|
||||
return Boolean(plugin.actions.extractToolSend({ args })?.to);
|
||||
}
|
||||
@@ -40,8 +40,8 @@ export function normalizeTargetForProvider(
|
||||
raw?: string,
|
||||
): string | undefined {
|
||||
if (!raw) return undefined;
|
||||
const providerId = normalizeProviderId(provider);
|
||||
const plugin = providerId ? getProviderPlugin(providerId) : undefined;
|
||||
const providerId = normalizeChannelId(provider);
|
||||
const plugin = providerId ? getChannelPlugin(providerId) : undefined;
|
||||
const normalized =
|
||||
plugin?.messaging?.normalizeTarget?.(raw) ??
|
||||
(raw.trim().toLowerCase() || undefined);
|
||||
|
||||
@@ -478,17 +478,23 @@ describe("getDmHistoryLimitFromSessionKey", () => {
|
||||
});
|
||||
|
||||
it("returns dmHistoryLimit for telegram provider", () => {
|
||||
const config = { telegram: { dmHistoryLimit: 15 } } as ClawdbotConfig;
|
||||
const config = {
|
||||
channels: { telegram: { dmHistoryLimit: 15 } },
|
||||
} as ClawdbotConfig;
|
||||
expect(getDmHistoryLimitFromSessionKey("telegram:dm:123", config)).toBe(15);
|
||||
});
|
||||
|
||||
it("returns dmHistoryLimit for whatsapp provider", () => {
|
||||
const config = { whatsapp: { dmHistoryLimit: 20 } } as ClawdbotConfig;
|
||||
const config = {
|
||||
channels: { whatsapp: { dmHistoryLimit: 20 } },
|
||||
} as ClawdbotConfig;
|
||||
expect(getDmHistoryLimitFromSessionKey("whatsapp:dm:123", config)).toBe(20);
|
||||
});
|
||||
|
||||
it("returns dmHistoryLimit for agent-prefixed session keys", () => {
|
||||
const config = { telegram: { dmHistoryLimit: 10 } } as ClawdbotConfig;
|
||||
const config = {
|
||||
channels: { telegram: { dmHistoryLimit: 10 } },
|
||||
} as ClawdbotConfig;
|
||||
expect(
|
||||
getDmHistoryLimitFromSessionKey("agent:main:telegram:dm:123", config),
|
||||
).toBe(10);
|
||||
@@ -496,8 +502,10 @@ describe("getDmHistoryLimitFromSessionKey", () => {
|
||||
|
||||
it("returns undefined for non-dm session kinds", () => {
|
||||
const config = {
|
||||
slack: { dmHistoryLimit: 10 },
|
||||
telegram: { dmHistoryLimit: 15 },
|
||||
channels: {
|
||||
telegram: { dmHistoryLimit: 15 },
|
||||
slack: { dmHistoryLimit: 10 },
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
expect(
|
||||
getDmHistoryLimitFromSessionKey("agent:beta:slack:channel:C1", config),
|
||||
@@ -508,14 +516,16 @@ describe("getDmHistoryLimitFromSessionKey", () => {
|
||||
});
|
||||
|
||||
it("returns undefined for unknown provider", () => {
|
||||
const config = { telegram: { dmHistoryLimit: 15 } } as ClawdbotConfig;
|
||||
const config = {
|
||||
channels: { telegram: { dmHistoryLimit: 15 } },
|
||||
} as ClawdbotConfig;
|
||||
expect(
|
||||
getDmHistoryLimitFromSessionKey("unknown:dm:123", config),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined when provider config has no dmHistoryLimit", () => {
|
||||
const config = { telegram: {} } as ClawdbotConfig;
|
||||
const config = { channels: { telegram: {} } } as ClawdbotConfig;
|
||||
expect(
|
||||
getDmHistoryLimitFromSessionKey("telegram:dm:123", config),
|
||||
).toBeUndefined();
|
||||
@@ -533,7 +543,9 @@ describe("getDmHistoryLimitFromSessionKey", () => {
|
||||
] as const;
|
||||
|
||||
for (const provider of providers) {
|
||||
const config = { [provider]: { dmHistoryLimit: 5 } } as ClawdbotConfig;
|
||||
const config = {
|
||||
channels: { [provider]: { dmHistoryLimit: 5 } },
|
||||
} as ClawdbotConfig;
|
||||
expect(
|
||||
getDmHistoryLimitFromSessionKey(`${provider}:dm:123`, config),
|
||||
).toBe(5);
|
||||
@@ -554,9 +566,11 @@ describe("getDmHistoryLimitFromSessionKey", () => {
|
||||
for (const provider of providers) {
|
||||
// Test per-DM override takes precedence
|
||||
const configWithOverride = {
|
||||
[provider]: {
|
||||
dmHistoryLimit: 20,
|
||||
dms: { user123: { historyLimit: 7 } },
|
||||
channels: {
|
||||
[provider]: {
|
||||
dmHistoryLimit: 20,
|
||||
dms: { user123: { historyLimit: 7 } },
|
||||
},
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
expect(
|
||||
@@ -586,9 +600,11 @@ describe("getDmHistoryLimitFromSessionKey", () => {
|
||||
|
||||
it("returns per-DM override when set", () => {
|
||||
const config = {
|
||||
telegram: {
|
||||
dmHistoryLimit: 15,
|
||||
dms: { "123": { historyLimit: 5 } },
|
||||
channels: {
|
||||
telegram: {
|
||||
dmHistoryLimit: 15,
|
||||
dms: { "123": { historyLimit: 5 } },
|
||||
},
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
expect(getDmHistoryLimitFromSessionKey("telegram:dm:123", config)).toBe(5);
|
||||
@@ -596,9 +612,11 @@ describe("getDmHistoryLimitFromSessionKey", () => {
|
||||
|
||||
it("falls back to provider default when per-DM not set", () => {
|
||||
const config = {
|
||||
telegram: {
|
||||
dmHistoryLimit: 15,
|
||||
dms: { "456": { historyLimit: 5 } },
|
||||
channels: {
|
||||
telegram: {
|
||||
dmHistoryLimit: 15,
|
||||
dms: { "456": { historyLimit: 5 } },
|
||||
},
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
expect(getDmHistoryLimitFromSessionKey("telegram:dm:123", config)).toBe(15);
|
||||
@@ -606,9 +624,11 @@ describe("getDmHistoryLimitFromSessionKey", () => {
|
||||
|
||||
it("returns per-DM override for agent-prefixed keys", () => {
|
||||
const config = {
|
||||
telegram: {
|
||||
dmHistoryLimit: 20,
|
||||
dms: { "789": { historyLimit: 3 } },
|
||||
channels: {
|
||||
telegram: {
|
||||
dmHistoryLimit: 20,
|
||||
dms: { "789": { historyLimit: 3 } },
|
||||
},
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
expect(
|
||||
@@ -618,9 +638,11 @@ describe("getDmHistoryLimitFromSessionKey", () => {
|
||||
|
||||
it("handles userId with colons (e.g., email)", () => {
|
||||
const config = {
|
||||
msteams: {
|
||||
dmHistoryLimit: 10,
|
||||
dms: { "user@example.com": { historyLimit: 7 } },
|
||||
channels: {
|
||||
msteams: {
|
||||
dmHistoryLimit: 10,
|
||||
dms: { "user@example.com": { historyLimit: 7 } },
|
||||
},
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
expect(
|
||||
@@ -630,8 +652,10 @@ describe("getDmHistoryLimitFromSessionKey", () => {
|
||||
|
||||
it("returns undefined when per-DM historyLimit is not set", () => {
|
||||
const config = {
|
||||
telegram: {
|
||||
dms: { "123": {} },
|
||||
channels: {
|
||||
telegram: {
|
||||
dms: { "123": {} },
|
||||
},
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
expect(
|
||||
@@ -641,9 +665,11 @@ describe("getDmHistoryLimitFromSessionKey", () => {
|
||||
|
||||
it("returns 0 when per-DM historyLimit is explicitly 0 (unlimited)", () => {
|
||||
const config = {
|
||||
telegram: {
|
||||
dmHistoryLimit: 15,
|
||||
dms: { "123": { historyLimit: 0 } },
|
||||
channels: {
|
||||
telegram: {
|
||||
dmHistoryLimit: 15,
|
||||
dms: { "123": { historyLimit: 0 } },
|
||||
},
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
expect(getDmHistoryLimitFromSessionKey("telegram:dm:123", config)).toBe(0);
|
||||
|
||||
@@ -33,8 +33,8 @@ import type {
|
||||
} from "../auto-reply/thinking.js";
|
||||
import { formatToolAggregate } from "../auto-reply/tool-meta.js";
|
||||
import { isCacheEnabled, resolveCacheTtlMs } from "../config/cache-utils.js";
|
||||
import { resolveChannelCapabilities } from "../config/channel-capabilities.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { resolveProviderCapabilities } from "../config/provider-capabilities.js";
|
||||
import { getMachineDisplayName } from "../infra/machine-name.js";
|
||||
import { registerUnhandledRejectionHandler } from "../infra/unhandled-rejections.js";
|
||||
import { createSubsystemLogger } from "../logging.js";
|
||||
@@ -42,7 +42,7 @@ import {
|
||||
type enqueueCommand,
|
||||
enqueueCommandInLane,
|
||||
} from "../process/command-queue.js";
|
||||
import { normalizeMessageProvider } from "../utils/message-provider.js";
|
||||
import { normalizeMessageChannel } from "../utils/message-channel.js";
|
||||
import { isReasoningTagProvider } from "../utils/provider-utils.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { resolveClawdbotAgentDir } from "./agent-paths.js";
|
||||
@@ -690,19 +690,19 @@ export function getDmHistoryLimitFromSessionKey(
|
||||
// Map provider to config key
|
||||
switch (provider) {
|
||||
case "telegram":
|
||||
return getLimit(config.telegram);
|
||||
return getLimit(config.channels?.telegram);
|
||||
case "whatsapp":
|
||||
return getLimit(config.whatsapp);
|
||||
return getLimit(config.channels?.whatsapp);
|
||||
case "discord":
|
||||
return getLimit(config.discord);
|
||||
return getLimit(config.channels?.discord);
|
||||
case "slack":
|
||||
return getLimit(config.slack);
|
||||
return getLimit(config.channels?.slack);
|
||||
case "signal":
|
||||
return getLimit(config.signal);
|
||||
return getLimit(config.channels?.signal);
|
||||
case "imessage":
|
||||
return getLimit(config.imessage);
|
||||
return getLimit(config.channels?.imessage);
|
||||
case "msteams":
|
||||
return getLimit(config.msteams);
|
||||
return getLimit(config.channels?.msteams);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
@@ -1125,6 +1125,7 @@ function resolveModel(
|
||||
export async function compactEmbeddedPiSession(params: {
|
||||
sessionId: string;
|
||||
sessionKey?: string;
|
||||
messageChannel?: string;
|
||||
messageProvider?: string;
|
||||
agentAccountId?: string;
|
||||
sessionFile: string;
|
||||
@@ -1258,7 +1259,7 @@ export async function compactEmbeddedPiSession(params: {
|
||||
elevated: params.bashElevated,
|
||||
},
|
||||
sandbox,
|
||||
messageProvider: params.messageProvider,
|
||||
messageProvider: params.messageChannel ?? params.messageProvider,
|
||||
agentAccountId: params.agentAccountId,
|
||||
sessionKey: params.sessionKey ?? params.sessionId,
|
||||
agentDir,
|
||||
@@ -1272,13 +1273,13 @@ export async function compactEmbeddedPiSession(params: {
|
||||
});
|
||||
logToolSchemasForGoogle({ tools, provider });
|
||||
const machineName = await getMachineDisplayName();
|
||||
const runtimeProvider = normalizeMessageProvider(
|
||||
params.messageProvider,
|
||||
const runtimeChannel = normalizeMessageChannel(
|
||||
params.messageChannel ?? params.messageProvider,
|
||||
);
|
||||
const runtimeCapabilities = runtimeProvider
|
||||
? (resolveProviderCapabilities({
|
||||
const runtimeCapabilities = runtimeChannel
|
||||
? (resolveChannelCapabilities({
|
||||
cfg: params.config,
|
||||
provider: runtimeProvider,
|
||||
channel: runtimeChannel,
|
||||
accountId: params.agentAccountId,
|
||||
}) ?? [])
|
||||
: undefined;
|
||||
@@ -1288,7 +1289,7 @@ export async function compactEmbeddedPiSession(params: {
|
||||
arch: os.arch(),
|
||||
node: process.version,
|
||||
model: `${provider}/${modelId}`,
|
||||
provider: runtimeProvider,
|
||||
channel: runtimeChannel,
|
||||
capabilities: runtimeCapabilities,
|
||||
};
|
||||
const sandboxInfo = buildEmbeddedSandboxInfo(
|
||||
@@ -1443,6 +1444,7 @@ export async function compactEmbeddedPiSession(params: {
|
||||
export async function runEmbeddedPiAgent(params: {
|
||||
sessionId: string;
|
||||
sessionKey?: string;
|
||||
messageChannel?: string;
|
||||
messageProvider?: string;
|
||||
agentAccountId?: string;
|
||||
/** Current channel ID for auto-threading (Slack). */
|
||||
@@ -1639,7 +1641,7 @@ export async function runEmbeddedPiAgent(params: {
|
||||
attemptedThinking.add(thinkLevel);
|
||||
|
||||
log.debug(
|
||||
`embedded run start: runId=${params.runId} sessionId=${params.sessionId} provider=${provider} model=${modelId} thinking=${thinkLevel} messageProvider=${params.messageProvider ?? "unknown"}`,
|
||||
`embedded run start: runId=${params.runId} sessionId=${params.sessionId} provider=${provider} model=${modelId} thinking=${thinkLevel} messageChannel=${params.messageChannel ?? params.messageProvider ?? "unknown"}`,
|
||||
);
|
||||
|
||||
await fs.mkdir(resolvedWorkspace, { recursive: true });
|
||||
@@ -1698,7 +1700,7 @@ export async function runEmbeddedPiAgent(params: {
|
||||
elevated: params.bashElevated,
|
||||
},
|
||||
sandbox,
|
||||
messageProvider: params.messageProvider,
|
||||
messageProvider: params.messageChannel ?? params.messageProvider,
|
||||
agentAccountId: params.agentAccountId,
|
||||
sessionKey: params.sessionKey ?? params.sessionId,
|
||||
agentDir,
|
||||
|
||||
@@ -6,13 +6,13 @@ import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
||||
import { parseReplyDirectives } from "../auto-reply/reply/reply-directives.js";
|
||||
import type { ReasoningLevel } from "../auto-reply/thinking.js";
|
||||
import { formatToolAggregate } from "../auto-reply/tool-meta.js";
|
||||
import {
|
||||
getChannelPlugin,
|
||||
normalizeChannelId,
|
||||
} from "../channels/plugins/index.js";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import { emitAgentEvent } from "../infra/agent-events.js";
|
||||
import { createSubsystemLogger } from "../logging.js";
|
||||
import {
|
||||
getProviderPlugin,
|
||||
normalizeProviderId,
|
||||
} from "../providers/plugins/index.js";
|
||||
import { truncateUtf16Safe } from "../utils.js";
|
||||
import type { BlockReplyChunking } from "./pi-embedded-block-chunker.js";
|
||||
import { EmbeddedBlockChunker } from "./pi-embedded-block-chunker.js";
|
||||
@@ -124,15 +124,15 @@ function extractMessagingToolSend(
|
||||
if (!toRaw) return undefined;
|
||||
const providerRaw =
|
||||
typeof args.provider === "string" ? args.provider.trim() : "";
|
||||
const providerId = providerRaw ? normalizeProviderId(providerRaw) : null;
|
||||
const providerId = providerRaw ? normalizeChannelId(providerRaw) : null;
|
||||
const provider =
|
||||
providerId ?? (providerRaw ? providerRaw.toLowerCase() : "message");
|
||||
const to = normalizeTargetForProvider(provider, toRaw);
|
||||
return to ? { tool: toolName, provider, accountId, to } : undefined;
|
||||
}
|
||||
const providerId = normalizeProviderId(toolName);
|
||||
const providerId = normalizeChannelId(toolName);
|
||||
if (!providerId) return undefined;
|
||||
const plugin = getProviderPlugin(providerId);
|
||||
const plugin = getChannelPlugin(providerId);
|
||||
const extracted = plugin?.actions?.extractToolSend?.({ args });
|
||||
if (!extracted?.to) return undefined;
|
||||
const to = normalizeTargetForProvider(providerId, extracted.to);
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { detectMime } from "../media/mime.js";
|
||||
import { isSubagentSessionKey } from "../routing/session-key.js";
|
||||
import { resolveGatewayMessageProvider } from "../utils/message-provider.js";
|
||||
import { resolveGatewayMessageChannel } from "../utils/message-channel.js";
|
||||
import {
|
||||
resolveAgentConfig,
|
||||
resolveAgentIdFromSessionKey,
|
||||
@@ -21,9 +21,9 @@ import {
|
||||
type ExecToolDefaults,
|
||||
type ProcessToolDefaults,
|
||||
} from "./bash-tools.js";
|
||||
import { listChannelAgentTools } from "./channel-tools.js";
|
||||
import { createClawdbotTools } from "./clawdbot-tools.js";
|
||||
import type { ModelAuthMode } from "./model-auth.js";
|
||||
import { listProviderAgentTools } from "./provider-tools.js";
|
||||
import type { SandboxContext, SandboxToolPolicy } from "./sandbox.js";
|
||||
import { assertSandboxPath } from "./sandbox-paths.js";
|
||||
import { cleanSchemaForGemini } from "./schema/clean-for-gemini.js";
|
||||
@@ -807,8 +807,8 @@ export function createClawdbotCodingTools(options?: {
|
||||
execTool as unknown as AnyAgentTool,
|
||||
bashTool,
|
||||
processTool as unknown as AnyAgentTool,
|
||||
// Provider docking: include provider-defined agent tools (login, etc.).
|
||||
...listProviderAgentTools({ cfg: options?.config }),
|
||||
// Channel docking: include channel-defined agent tools (login, etc.).
|
||||
...listChannelAgentTools({ cfg: options?.config }),
|
||||
...createClawdbotTools({
|
||||
browserControlUrl: sandbox?.browser?.controlUrl,
|
||||
allowHostBrowserControl: sandbox ? sandbox.browserAllowHostControl : true,
|
||||
@@ -816,7 +816,7 @@ export function createClawdbotCodingTools(options?: {
|
||||
allowedControlHosts: sandbox?.browserAllowedControlHosts,
|
||||
allowedControlPorts: sandbox?.browserAllowedControlPorts,
|
||||
agentSessionKey: options?.sessionKey,
|
||||
agentProvider: resolveGatewayMessageProvider(options?.messageProvider),
|
||||
agentChannel: resolveGatewayMessageChannel(options?.messageProvider),
|
||||
agentAccountId: options?.agentAccountId,
|
||||
agentDir: options?.agentDir,
|
||||
sandboxRoot,
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { listProviderPlugins } from "../providers/plugins/index.js";
|
||||
import type { ProviderAgentTool } from "../providers/plugins/types.js";
|
||||
|
||||
export function listProviderAgentTools(params: {
|
||||
cfg?: ClawdbotConfig;
|
||||
}): ProviderAgentTool[] {
|
||||
// Provider docking: aggregate provider-owned tools (login, etc.).
|
||||
const tools: ProviderAgentTool[] = [];
|
||||
for (const plugin of listProviderPlugins()) {
|
||||
const entry = plugin.agentTools;
|
||||
if (!entry) continue;
|
||||
const resolved = typeof entry === "function" ? entry(params) : entry;
|
||||
if (Array.isArray(resolved)) tools.push(...resolved);
|
||||
}
|
||||
return tools;
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
resolveProfile,
|
||||
} from "../browser/config.js";
|
||||
import { DEFAULT_CLAWD_BROWSER_COLOR } from "../browser/constants.js";
|
||||
import { CHANNEL_IDS } from "../channels/registry.js";
|
||||
import {
|
||||
type ClawdbotConfig,
|
||||
loadConfig,
|
||||
@@ -23,7 +24,6 @@ import {
|
||||
canonicalizeMainSessionAlias,
|
||||
resolveAgentMainSessionKey,
|
||||
} from "../config/sessions.js";
|
||||
import { PROVIDER_IDS } from "../providers/registry.js";
|
||||
import { normalizeAgentId } from "../routing/session-key.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
@@ -188,7 +188,7 @@ const DEFAULT_TOOL_DENY = [
|
||||
"nodes",
|
||||
"cron",
|
||||
"gateway",
|
||||
...PROVIDER_IDS,
|
||||
...CHANNEL_IDS,
|
||||
];
|
||||
export const DEFAULT_SANDBOX_BROWSER_IMAGE =
|
||||
"clawdbot-sandbox-browser:bookworm-slim";
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
resolveStorePath,
|
||||
} from "../config/sessions.js";
|
||||
import { callGateway } from "../gateway/call.js";
|
||||
import { INTERNAL_MESSAGE_PROVIDER } from "../utils/message-provider.js";
|
||||
import { INTERNAL_MESSAGE_CHANNEL } from "../utils/message-channel.js";
|
||||
import { AGENT_LANE_NESTED } from "./lanes.js";
|
||||
import { readLatestAssistantReply, runAgentStep } from "./tools/agent-step.js";
|
||||
import { resolveAnnounceTarget } from "./tools/sessions-announce-target.js";
|
||||
@@ -139,7 +139,7 @@ async function buildSubagentStatsLine(params: {
|
||||
|
||||
export function buildSubagentSystemPrompt(params: {
|
||||
requesterSessionKey?: string;
|
||||
requesterProvider?: string;
|
||||
requesterChannel?: string;
|
||||
childSessionKey: string;
|
||||
label?: string;
|
||||
task?: string;
|
||||
@@ -182,8 +182,8 @@ export function buildSubagentSystemPrompt(params: {
|
||||
params.requesterSessionKey
|
||||
? `- Requester session: ${params.requesterSessionKey}.`
|
||||
: undefined,
|
||||
params.requesterProvider
|
||||
? `- Requester provider: ${params.requesterProvider}.`
|
||||
params.requesterChannel
|
||||
? `- Requester channel: ${params.requesterChannel}.`
|
||||
: undefined,
|
||||
`- Your session: ${params.childSessionKey}.`,
|
||||
"",
|
||||
@@ -195,7 +195,7 @@ export function buildSubagentSystemPrompt(params: {
|
||||
|
||||
function buildSubagentAnnouncePrompt(params: {
|
||||
requesterSessionKey?: string;
|
||||
requesterProvider?: string;
|
||||
requesterChannel?: string;
|
||||
announceChannel: string;
|
||||
task: string;
|
||||
subagentReply?: string;
|
||||
@@ -205,10 +205,10 @@ function buildSubagentAnnouncePrompt(params: {
|
||||
params.requesterSessionKey
|
||||
? `Requester session: ${params.requesterSessionKey}.`
|
||||
: undefined,
|
||||
params.requesterProvider
|
||||
? `Requester provider: ${params.requesterProvider}.`
|
||||
params.requesterChannel
|
||||
? `Requester channel: ${params.requesterChannel}.`
|
||||
: undefined,
|
||||
`Post target provider: ${params.announceChannel}.`,
|
||||
`Post target channel: ${params.announceChannel}.`,
|
||||
`Original task: ${params.task}`,
|
||||
params.subagentReply
|
||||
? `Sub-agent result: ${params.subagentReply}`
|
||||
@@ -226,7 +226,7 @@ export async function runSubagentAnnounceFlow(params: {
|
||||
childSessionKey: string;
|
||||
childRunId: string;
|
||||
requesterSessionKey: string;
|
||||
requesterProvider?: string;
|
||||
requesterChannel?: string;
|
||||
requesterDisplayKey: string;
|
||||
task: string;
|
||||
timeoutMs: number;
|
||||
@@ -269,8 +269,8 @@ export async function runSubagentAnnounceFlow(params: {
|
||||
|
||||
const announcePrompt = buildSubagentAnnouncePrompt({
|
||||
requesterSessionKey: params.requesterSessionKey,
|
||||
requesterProvider: params.requesterProvider,
|
||||
announceChannel: announceTarget.provider,
|
||||
requesterChannel: params.requesterChannel,
|
||||
announceChannel: announceTarget.channel,
|
||||
task: params.task,
|
||||
subagentReply: reply,
|
||||
});
|
||||
@@ -280,7 +280,7 @@ export async function runSubagentAnnounceFlow(params: {
|
||||
message: "Sub-agent announce step.",
|
||||
extraSystemPrompt: announcePrompt,
|
||||
timeoutMs: params.timeoutMs,
|
||||
provider: INTERNAL_MESSAGE_PROVIDER,
|
||||
channel: INTERNAL_MESSAGE_CHANNEL,
|
||||
lane: AGENT_LANE_NESTED,
|
||||
});
|
||||
|
||||
@@ -305,7 +305,7 @@ export async function runSubagentAnnounceFlow(params: {
|
||||
params: {
|
||||
to: announceTarget.to,
|
||||
message,
|
||||
provider: announceTarget.provider,
|
||||
channel: announceTarget.channel,
|
||||
accountId: announceTarget.accountId,
|
||||
idempotencyKey: crypto.randomUUID(),
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@ export type SubagentRunRecord = {
|
||||
runId: string;
|
||||
childSessionKey: string;
|
||||
requesterSessionKey: string;
|
||||
requesterProvider?: string;
|
||||
requesterChannel?: string;
|
||||
requesterDisplayKey: string;
|
||||
task: string;
|
||||
cleanup: "delete" | "keep";
|
||||
@@ -105,7 +105,7 @@ function ensureListener() {
|
||||
childSessionKey: entry.childSessionKey,
|
||||
childRunId: entry.runId,
|
||||
requesterSessionKey: entry.requesterSessionKey,
|
||||
requesterProvider: entry.requesterProvider,
|
||||
requesterChannel: entry.requesterChannel,
|
||||
requesterDisplayKey: entry.requesterDisplayKey,
|
||||
task: entry.task,
|
||||
timeoutMs: 30_000,
|
||||
@@ -133,7 +133,7 @@ export function registerSubagentRun(params: {
|
||||
runId: string;
|
||||
childSessionKey: string;
|
||||
requesterSessionKey: string;
|
||||
requesterProvider?: string;
|
||||
requesterChannel?: string;
|
||||
requesterDisplayKey: string;
|
||||
task: string;
|
||||
cleanup: "delete" | "keep";
|
||||
@@ -152,7 +152,7 @@ export function registerSubagentRun(params: {
|
||||
runId: params.runId,
|
||||
childSessionKey: params.childSessionKey,
|
||||
requesterSessionKey: params.requesterSessionKey,
|
||||
requesterProvider: params.requesterProvider,
|
||||
requesterChannel: params.requesterChannel,
|
||||
requesterDisplayKey: params.requesterDisplayKey,
|
||||
task: params.task,
|
||||
cleanup: params.cleanup,
|
||||
@@ -191,7 +191,7 @@ async function waitForSubagentCompletion(runId: string, waitTimeoutMs: number) {
|
||||
childSessionKey: entry.childSessionKey,
|
||||
childRunId: entry.runId,
|
||||
requesterSessionKey: entry.requesterSessionKey,
|
||||
requesterProvider: entry.requesterProvider,
|
||||
requesterChannel: entry.requesterChannel,
|
||||
requesterDisplayKey: entry.requesterDisplayKey,
|
||||
task: entry.task,
|
||||
timeoutMs: 30_000,
|
||||
|
||||
@@ -153,7 +153,7 @@ describe("buildAgentSystemPrompt", () => {
|
||||
toolNames: ["message"],
|
||||
});
|
||||
|
||||
expect(prompt).toContain("message: Send messages and provider actions");
|
||||
expect(prompt).toContain("message: Send messages and channel actions");
|
||||
expect(prompt).toContain("### message tool");
|
||||
});
|
||||
|
||||
@@ -161,12 +161,12 @@ describe("buildAgentSystemPrompt", () => {
|
||||
const prompt = buildAgentSystemPrompt({
|
||||
workspaceDir: "/tmp/clawd",
|
||||
runtimeInfo: {
|
||||
provider: "telegram",
|
||||
channel: "telegram",
|
||||
capabilities: ["inlineButtons"],
|
||||
},
|
||||
});
|
||||
|
||||
expect(prompt).toContain("provider=telegram");
|
||||
expect(prompt).toContain("channel=telegram");
|
||||
expect(prompt).toContain("capabilities=inlineButtons");
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { ReasoningLevel, ThinkLevel } from "../auto-reply/thinking.js";
|
||||
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
|
||||
import { PROVIDER_IDS } from "../providers/registry.js";
|
||||
import { CHANNEL_IDS } from "../channels/registry.js";
|
||||
import type { EmbeddedContextFile } from "./pi-embedded-helpers.js";
|
||||
|
||||
const MESSAGE_PROVIDER_OPTIONS = PROVIDER_IDS.join("|");
|
||||
const MESSAGE_CHANNEL_OPTIONS = CHANNEL_IDS.join("|");
|
||||
|
||||
export function buildAgentSystemPrompt(params: {
|
||||
workspaceDir: string;
|
||||
@@ -26,7 +26,7 @@ export function buildAgentSystemPrompt(params: {
|
||||
arch?: string;
|
||||
node?: string;
|
||||
model?: string;
|
||||
provider?: string;
|
||||
channel?: string;
|
||||
capabilities?: string[];
|
||||
};
|
||||
sandboxInfo?: {
|
||||
@@ -56,12 +56,12 @@ export function buildAgentSystemPrompt(params: {
|
||||
ls: "List directory contents",
|
||||
exec: "Run shell commands",
|
||||
process: "Manage background exec sessions",
|
||||
// Provider docking: add provider login tools here when a provider needs interactive linking.
|
||||
// Channel docking: add login tools here when a channel needs interactive linking.
|
||||
browser: "Control web browser",
|
||||
canvas: "Present/eval/snapshot the Canvas",
|
||||
nodes: "List/describe/notify/camera/screen on paired nodes",
|
||||
cron: "Manage cron jobs and wake events (use for reminders)",
|
||||
message: "Send messages and provider actions",
|
||||
message: "Send messages and channel actions",
|
||||
gateway:
|
||||
"Restart, apply config, or run updates on the running Clawdbot process",
|
||||
agents_list: "List agent ids allowed for sessions_spawn",
|
||||
@@ -166,7 +166,7 @@ export function buildAgentSystemPrompt(params: {
|
||||
? `Heartbeat prompt: ${heartbeatPrompt}`
|
||||
: "Heartbeat prompt: (configured)";
|
||||
const runtimeInfo = params.runtimeInfo;
|
||||
const runtimeProvider = runtimeInfo?.provider?.trim().toLowerCase();
|
||||
const runtimeChannel = runtimeInfo?.channel?.trim().toLowerCase();
|
||||
const runtimeCapabilities = (runtimeInfo?.capabilities ?? [])
|
||||
.map((cap) => String(cap).trim())
|
||||
.filter(Boolean);
|
||||
@@ -322,23 +322,23 @@ export function buildAgentSystemPrompt(params: {
|
||||
"- [[reply_to_current]] replies to the triggering message.",
|
||||
"- [[reply_to:<id>]] replies to a specific message id when you have it.",
|
||||
"Whitespace inside the tag is allowed (e.g. [[ reply_to_current ]] / [[ reply_to: 123 ]]).",
|
||||
"Tags are stripped before sending; support depends on the current provider config.",
|
||||
"Tags are stripped before sending; support depends on the current channel config.",
|
||||
"",
|
||||
"## Messaging",
|
||||
"- Reply in current session → automatically routes to the source provider (Signal, Telegram, etc.)",
|
||||
"- Reply in current session → automatically routes to the source channel (Signal, Telegram, etc.)",
|
||||
"- Cross-session messaging → use sessions_send(sessionKey, message)",
|
||||
"- Never use exec/curl for provider messaging; Clawdbot handles all routing internally.",
|
||||
availableTools.has("message")
|
||||
? [
|
||||
"",
|
||||
"### message tool",
|
||||
"- Use `message` for proactive sends + provider actions (polls, reactions, etc.).",
|
||||
"- Use `message` for proactive sends + channel actions (polls, reactions, etc.).",
|
||||
"- For `action=send`, include `to` and `message`.",
|
||||
`- If multiple providers are configured, pass \`provider\` (${MESSAGE_PROVIDER_OPTIONS}).`,
|
||||
`- If multiple channels are configured, pass \`channel\` (${MESSAGE_CHANNEL_OPTIONS}).`,
|
||||
inlineButtonsEnabled
|
||||
? "- Inline buttons supported. Use `action=send` with `buttons=[[{text,callback_data}]]` (callback_data routes back as a user message)."
|
||||
: runtimeProvider
|
||||
? `- Inline buttons not enabled for ${runtimeProvider}. If you need them, ask to add "inlineButtons" to ${runtimeProvider}.capabilities or ${runtimeProvider}.accounts.<id>.capabilities.`
|
||||
: runtimeChannel
|
||||
? `- Inline buttons not enabled for ${runtimeChannel}. If you need them, ask to add "inlineButtons" to ${runtimeChannel}.capabilities or ${runtimeChannel}.accounts.<id>.capabilities.`
|
||||
: "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
@@ -397,8 +397,8 @@ export function buildAgentSystemPrompt(params: {
|
||||
: "",
|
||||
runtimeInfo?.node ? `node=${runtimeInfo.node}` : "",
|
||||
runtimeInfo?.model ? `model=${runtimeInfo.model}` : "",
|
||||
runtimeProvider ? `provider=${runtimeProvider}` : "",
|
||||
runtimeProvider
|
||||
runtimeChannel ? `channel=${runtimeChannel}` : "",
|
||||
runtimeChannel
|
||||
? `capabilities=${
|
||||
runtimeCapabilities.length > 0
|
||||
? runtimeCapabilities.join(",")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import { callGateway } from "../../gateway/call.js";
|
||||
import { INTERNAL_MESSAGE_PROVIDER } from "../../utils/message-provider.js";
|
||||
import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
|
||||
import { AGENT_LANE_NESTED } from "../lanes.js";
|
||||
import { extractAssistantText, stripToolMessages } from "./sessions-helpers.js";
|
||||
|
||||
@@ -25,7 +25,7 @@ export async function runAgentStep(params: {
|
||||
message: string;
|
||||
extraSystemPrompt: string;
|
||||
timeoutMs: number;
|
||||
provider?: string;
|
||||
channel?: string;
|
||||
lane?: string;
|
||||
}): Promise<string | undefined> {
|
||||
const stepIdem = crypto.randomUUID();
|
||||
@@ -36,7 +36,7 @@ export async function runAgentStep(params: {
|
||||
sessionKey: params.sessionKey,
|
||||
idempotencyKey: stepIdem,
|
||||
deliver: false,
|
||||
provider: params.provider ?? INTERNAL_MESSAGE_PROVIDER,
|
||||
channel: params.channel ?? INTERNAL_MESSAGE_CHANNEL,
|
||||
lane: params.lane ?? AGENT_LANE_NESTED,
|
||||
extraSystemPrompt: params.extraSystemPrompt,
|
||||
},
|
||||
|
||||
@@ -56,7 +56,7 @@ export async function handleDiscordAction(
|
||||
cfg: ClawdbotConfig,
|
||||
): Promise<AgentToolResult<unknown>> {
|
||||
const action = readStringParam(params, "action", { required: true });
|
||||
const isActionEnabled = createActionGate(cfg.discord?.actions);
|
||||
const isActionEnabled = createActionGate(cfg.channels?.discord?.actions);
|
||||
|
||||
if (messagingActions.has(action)) {
|
||||
return await handleDiscordMessagingAction(action, params, isActionEnabled);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { callGateway } from "../../gateway/call.js";
|
||||
import {
|
||||
GATEWAY_CLIENT_MODES,
|
||||
GATEWAY_CLIENT_NAMES,
|
||||
} from "../../utils/message-provider.js";
|
||||
} from "../../utils/message-channel.js";
|
||||
|
||||
export const DEFAULT_GATEWAY_URL = "ws://127.0.0.1:18789";
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
|
||||
import {
|
||||
listChannelMessageActions,
|
||||
supportsChannelMessageButtons,
|
||||
} from "../../channels/plugins/message-actions.js";
|
||||
import {
|
||||
CHANNEL_MESSAGE_ACTION_NAMES,
|
||||
type ChannelMessageActionName,
|
||||
} from "../../channels/plugins/types.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { loadConfig } from "../../config/config.js";
|
||||
import {
|
||||
@@ -7,23 +14,15 @@ import {
|
||||
GATEWAY_CLIENT_MODES,
|
||||
} from "../../gateway/protocol/client-info.js";
|
||||
import { runMessageAction } from "../../infra/outbound/message-action-runner.js";
|
||||
import {
|
||||
listProviderMessageActions,
|
||||
supportsProviderMessageButtons,
|
||||
} from "../../providers/plugins/message-actions.js";
|
||||
import {
|
||||
PROVIDER_MESSAGE_ACTION_NAMES,
|
||||
type ProviderMessageActionName,
|
||||
} from "../../providers/plugins/types.js";
|
||||
import { normalizeAccountId } from "../../routing/session-key.js";
|
||||
import { stringEnum } from "../schema/typebox.js";
|
||||
import type { AnyAgentTool } from "./common.js";
|
||||
import { jsonResult, readNumberParam, readStringParam } from "./common.js";
|
||||
|
||||
const AllMessageActions = PROVIDER_MESSAGE_ACTION_NAMES;
|
||||
const AllMessageActions = CHANNEL_MESSAGE_ACTION_NAMES;
|
||||
|
||||
const MessageToolCommonSchema = {
|
||||
provider: Type.Optional(Type.String()),
|
||||
channel: Type.Optional(Type.String()),
|
||||
to: Type.Optional(Type.String()),
|
||||
message: Type.Optional(Type.String()),
|
||||
media: Type.Optional(Type.String()),
|
||||
@@ -131,8 +130,8 @@ type MessageToolOptions = {
|
||||
};
|
||||
|
||||
function buildMessageToolSchema(cfg: ClawdbotConfig) {
|
||||
const actions = listProviderMessageActions(cfg);
|
||||
const includeButtons = supportsProviderMessageButtons(cfg);
|
||||
const actions = listChannelMessageActions(cfg);
|
||||
const includeButtons = supportsChannelMessageButtons(cfg);
|
||||
return buildMessageToolSchemaFromActions(
|
||||
actions.length > 0 ? actions : ["send"],
|
||||
{ includeButtons },
|
||||
@@ -155,14 +154,14 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
|
||||
label: "Message",
|
||||
name: "message",
|
||||
description:
|
||||
"Send messages and provider actions (polls, reactions, pins, threads, etc.) via configured provider plugins.",
|
||||
"Send messages and channel actions (polls, reactions, pins, threads, etc.) via configured channel plugins.",
|
||||
parameters: schema,
|
||||
execute: async (_toolCallId, args) => {
|
||||
const params = args as Record<string, unknown>;
|
||||
const cfg = options?.config ?? loadConfig();
|
||||
const action = readStringParam(params, "action", {
|
||||
required: true,
|
||||
}) as ProviderMessageActionName;
|
||||
}) as ChannelMessageActionName;
|
||||
const accountId = readStringParam(params, "accountId") ?? agentAccountId;
|
||||
|
||||
const gateway = {
|
||||
|
||||
@@ -322,8 +322,8 @@ export function createSessionStatusTool(opts?: {
|
||||
|
||||
const queueSettings = resolveQueueSettings({
|
||||
cfg,
|
||||
provider:
|
||||
resolved.entry.provider ?? resolved.entry.lastProvider ?? "unknown",
|
||||
channel:
|
||||
resolved.entry.channel ?? resolved.entry.lastChannel ?? "unknown",
|
||||
sessionEntry: resolved.entry,
|
||||
});
|
||||
const queueKey = resolved.key ?? resolved.entry.sessionId;
|
||||
|
||||
@@ -17,7 +17,7 @@ describe("resolveAnnounceTarget", () => {
|
||||
sessionKey: "agent:main:discord:group:dev",
|
||||
displayKey: "agent:main:discord:group:dev",
|
||||
});
|
||||
expect(target).toEqual({ provider: "discord", to: "channel:dev" });
|
||||
expect(target).toEqual({ channel: "discord", to: "channel:dev" });
|
||||
expect(callGatewayMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ describe("resolveAnnounceTarget", () => {
|
||||
sessions: [
|
||||
{
|
||||
key: "agent:main:whatsapp:group:123@g.us",
|
||||
lastProvider: "whatsapp",
|
||||
lastChannel: "whatsapp",
|
||||
lastTo: "123@g.us",
|
||||
lastAccountId: "work",
|
||||
},
|
||||
@@ -38,7 +38,7 @@ describe("resolveAnnounceTarget", () => {
|
||||
displayKey: "agent:main:whatsapp:group:123@g.us",
|
||||
});
|
||||
expect(target).toEqual({
|
||||
provider: "whatsapp",
|
||||
channel: "whatsapp",
|
||||
to: "123@g.us",
|
||||
accountId: "work",
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { callGateway } from "../../gateway/call.js";
|
||||
import {
|
||||
getProviderPlugin,
|
||||
normalizeProviderId,
|
||||
} from "../../providers/plugins/index.js";
|
||||
getChannelPlugin,
|
||||
normalizeChannelId,
|
||||
} from "../../channels/plugins/index.js";
|
||||
import { callGateway } from "../../gateway/call.js";
|
||||
import type { AnnounceTarget } from "./sessions-send-helpers.js";
|
||||
import { resolveAnnounceTargetFromKey } from "./sessions-send-helpers.js";
|
||||
|
||||
@@ -15,8 +15,8 @@ export async function resolveAnnounceTarget(params: {
|
||||
const fallback = parsed ?? parsedDisplay ?? null;
|
||||
|
||||
if (fallback) {
|
||||
const normalized = normalizeProviderId(fallback.provider);
|
||||
const plugin = normalized ? getProviderPlugin(normalized) : null;
|
||||
const normalized = normalizeChannelId(fallback.channel);
|
||||
const plugin = normalized ? getChannelPlugin(normalized) : null;
|
||||
if (!plugin?.meta?.preferSessionLookupForAnnounceTarget) {
|
||||
return fallback;
|
||||
}
|
||||
@@ -35,14 +35,14 @@ export async function resolveAnnounceTarget(params: {
|
||||
const match =
|
||||
sessions.find((entry) => entry?.key === params.sessionKey) ??
|
||||
sessions.find((entry) => entry?.key === params.displayKey);
|
||||
const provider =
|
||||
typeof match?.lastProvider === "string" ? match.lastProvider : undefined;
|
||||
const channel =
|
||||
typeof match?.lastChannel === "string" ? match.lastChannel : undefined;
|
||||
const to = typeof match?.lastTo === "string" ? match.lastTo : undefined;
|
||||
const accountId =
|
||||
typeof match?.lastAccountId === "string"
|
||||
? match.lastAccountId
|
||||
: undefined;
|
||||
if (provider && to) return { provider, to, accountId };
|
||||
if (channel && to) return { channel, to, accountId };
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
@@ -56,11 +56,11 @@ export function classifySessionKind(params: {
|
||||
return "other";
|
||||
}
|
||||
|
||||
export function deriveProvider(params: {
|
||||
export function deriveChannel(params: {
|
||||
key: string;
|
||||
kind: SessionKind;
|
||||
provider?: string | null;
|
||||
lastProvider?: string | null;
|
||||
channel?: string | null;
|
||||
lastChannel?: string | null;
|
||||
}): string {
|
||||
if (
|
||||
params.kind === "cron" ||
|
||||
@@ -68,10 +68,10 @@ export function deriveProvider(params: {
|
||||
params.kind === "node"
|
||||
)
|
||||
return "internal";
|
||||
const provider = normalizeKey(params.provider ?? undefined);
|
||||
if (provider) return provider;
|
||||
const lastProvider = normalizeKey(params.lastProvider ?? undefined);
|
||||
if (lastProvider) return lastProvider;
|
||||
const channel = normalizeKey(params.channel ?? undefined);
|
||||
if (channel) return channel;
|
||||
const lastChannel = normalizeKey(params.lastChannel ?? undefined);
|
||||
if (lastChannel) return lastChannel;
|
||||
const parts = params.key.split(":").filter(Boolean);
|
||||
if (parts.length >= 3 && (parts[1] === "group" || parts[1] === "channel")) {
|
||||
return parts[0];
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { AnyAgentTool } from "./common.js";
|
||||
import { jsonResult, readStringArrayParam } from "./common.js";
|
||||
import {
|
||||
classifySessionKind,
|
||||
deriveProvider,
|
||||
deriveChannel,
|
||||
resolveDisplaySessionKey,
|
||||
resolveInternalSessionKey,
|
||||
resolveMainSessionAlias,
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
type SessionListRow = {
|
||||
key: string;
|
||||
kind: SessionKind;
|
||||
provider: string;
|
||||
channel: string;
|
||||
label?: string;
|
||||
displayName?: string;
|
||||
updatedAt?: number | null;
|
||||
@@ -37,7 +37,7 @@ type SessionListRow = {
|
||||
systemSent?: boolean;
|
||||
abortedLastRun?: boolean;
|
||||
sendPolicy?: string;
|
||||
lastProvider?: string;
|
||||
lastChannel?: string;
|
||||
lastTo?: string;
|
||||
lastAccountId?: string;
|
||||
transcriptPath?: string;
|
||||
@@ -178,21 +178,19 @@ export function createSessionsListTool(opts?: {
|
||||
mainKey,
|
||||
});
|
||||
|
||||
const entryProvider =
|
||||
typeof entry.provider === "string" ? entry.provider : undefined;
|
||||
const lastProvider =
|
||||
typeof entry.lastProvider === "string"
|
||||
? entry.lastProvider
|
||||
: undefined;
|
||||
const entryChannel =
|
||||
typeof entry.channel === "string" ? entry.channel : undefined;
|
||||
const lastChannel =
|
||||
typeof entry.lastChannel === "string" ? entry.lastChannel : undefined;
|
||||
const lastAccountId =
|
||||
typeof entry.lastAccountId === "string"
|
||||
? entry.lastAccountId
|
||||
: undefined;
|
||||
const derivedProvider = deriveProvider({
|
||||
const derivedChannel = deriveChannel({
|
||||
key,
|
||||
kind,
|
||||
provider: entryProvider,
|
||||
lastProvider,
|
||||
channel: entryChannel,
|
||||
lastChannel,
|
||||
});
|
||||
|
||||
const sessionId =
|
||||
@@ -205,7 +203,7 @@ export function createSessionsListTool(opts?: {
|
||||
const row: SessionListRow = {
|
||||
key: displayKey,
|
||||
kind,
|
||||
provider: derivedProvider,
|
||||
channel: derivedChannel,
|
||||
label: typeof entry.label === "string" ? entry.label : undefined,
|
||||
displayName:
|
||||
typeof entry.displayName === "string"
|
||||
@@ -241,7 +239,7 @@ export function createSessionsListTool(opts?: {
|
||||
: undefined,
|
||||
sendPolicy:
|
||||
typeof entry.sendPolicy === "string" ? entry.sendPolicy : undefined,
|
||||
lastProvider,
|
||||
lastChannel,
|
||||
lastTo: typeof entry.lastTo === "string" ? entry.lastTo : undefined,
|
||||
lastAccountId,
|
||||
transcriptPath,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import {
|
||||
getProviderPlugin,
|
||||
normalizeProviderId,
|
||||
} from "../../providers/plugins/index.js";
|
||||
getChannelPlugin,
|
||||
normalizeChannelId,
|
||||
} from "../../channels/plugins/index.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
|
||||
const ANNOUNCE_SKIP_TOKEN = "ANNOUNCE_SKIP";
|
||||
const REPLY_SKIP_TOKEN = "REPLY_SKIP";
|
||||
@@ -10,7 +10,7 @@ const DEFAULT_PING_PONG_TURNS = 5;
|
||||
const MAX_PING_PONG_TURNS = 5;
|
||||
|
||||
export type AnnounceTarget = {
|
||||
provider: string;
|
||||
channel: string;
|
||||
to: string;
|
||||
accountId?: string;
|
||||
};
|
||||
@@ -24,29 +24,29 @@ export function resolveAnnounceTargetFromKey(
|
||||
? rawParts.slice(2)
|
||||
: rawParts;
|
||||
if (parts.length < 3) return null;
|
||||
const [providerRaw, kind, ...rest] = parts;
|
||||
const [channelRaw, kind, ...rest] = parts;
|
||||
if (kind !== "group" && kind !== "channel") return null;
|
||||
const id = rest.join(":").trim();
|
||||
if (!id) return null;
|
||||
if (!providerRaw) return null;
|
||||
const normalizedProvider = normalizeProviderId(providerRaw);
|
||||
const provider = normalizedProvider ?? providerRaw.toLowerCase();
|
||||
const kindTarget = normalizedProvider
|
||||
if (!channelRaw) return null;
|
||||
const normalizedChannel = normalizeChannelId(channelRaw);
|
||||
const channel = normalizedChannel ?? channelRaw.toLowerCase();
|
||||
const kindTarget = normalizedChannel
|
||||
? kind === "channel"
|
||||
? `channel:${id}`
|
||||
: `group:${id}`
|
||||
: id;
|
||||
const normalized = normalizedProvider
|
||||
? getProviderPlugin(normalizedProvider)?.messaging?.normalizeTarget?.(
|
||||
const normalized = normalizedChannel
|
||||
? getChannelPlugin(normalizedChannel)?.messaging?.normalizeTarget?.(
|
||||
kindTarget,
|
||||
)
|
||||
: undefined;
|
||||
return { provider, to: normalized ?? kindTarget };
|
||||
return { channel, to: normalized ?? kindTarget };
|
||||
}
|
||||
|
||||
export function buildAgentToAgentMessageContext(params: {
|
||||
requesterSessionKey?: string;
|
||||
requesterProvider?: string;
|
||||
requesterChannel?: string;
|
||||
targetSessionKey: string;
|
||||
}) {
|
||||
const lines = [
|
||||
@@ -54,8 +54,8 @@ export function buildAgentToAgentMessageContext(params: {
|
||||
params.requesterSessionKey
|
||||
? `Agent 1 (requester) session: ${params.requesterSessionKey}.`
|
||||
: undefined,
|
||||
params.requesterProvider
|
||||
? `Agent 1 (requester) provider: ${params.requesterProvider}.`
|
||||
params.requesterChannel
|
||||
? `Agent 1 (requester) channel: ${params.requesterChannel}.`
|
||||
: undefined,
|
||||
`Agent 2 (target) session: ${params.targetSessionKey}.`,
|
||||
].filter(Boolean);
|
||||
@@ -64,9 +64,9 @@ export function buildAgentToAgentMessageContext(params: {
|
||||
|
||||
export function buildAgentToAgentReplyContext(params: {
|
||||
requesterSessionKey?: string;
|
||||
requesterProvider?: string;
|
||||
requesterChannel?: string;
|
||||
targetSessionKey: string;
|
||||
targetProvider?: string;
|
||||
targetChannel?: string;
|
||||
currentRole: "requester" | "target";
|
||||
turn: number;
|
||||
maxTurns: number;
|
||||
@@ -82,12 +82,12 @@ export function buildAgentToAgentReplyContext(params: {
|
||||
params.requesterSessionKey
|
||||
? `Agent 1 (requester) session: ${params.requesterSessionKey}.`
|
||||
: undefined,
|
||||
params.requesterProvider
|
||||
? `Agent 1 (requester) provider: ${params.requesterProvider}.`
|
||||
params.requesterChannel
|
||||
? `Agent 1 (requester) channel: ${params.requesterChannel}.`
|
||||
: undefined,
|
||||
`Agent 2 (target) session: ${params.targetSessionKey}.`,
|
||||
params.targetProvider
|
||||
? `Agent 2 (target) provider: ${params.targetProvider}.`
|
||||
params.targetChannel
|
||||
? `Agent 2 (target) channel: ${params.targetChannel}.`
|
||||
: undefined,
|
||||
`If you want to stop the ping-pong, reply exactly "${REPLY_SKIP_TOKEN}".`,
|
||||
].filter(Boolean);
|
||||
@@ -96,9 +96,9 @@ export function buildAgentToAgentReplyContext(params: {
|
||||
|
||||
export function buildAgentToAgentAnnounceContext(params: {
|
||||
requesterSessionKey?: string;
|
||||
requesterProvider?: string;
|
||||
requesterChannel?: string;
|
||||
targetSessionKey: string;
|
||||
targetProvider?: string;
|
||||
targetChannel?: string;
|
||||
originalMessage: string;
|
||||
roundOneReply?: string;
|
||||
latestReply?: string;
|
||||
@@ -108,12 +108,12 @@ export function buildAgentToAgentAnnounceContext(params: {
|
||||
params.requesterSessionKey
|
||||
? `Agent 1 (requester) session: ${params.requesterSessionKey}.`
|
||||
: undefined,
|
||||
params.requesterProvider
|
||||
? `Agent 1 (requester) provider: ${params.requesterProvider}.`
|
||||
params.requesterChannel
|
||||
? `Agent 1 (requester) channel: ${params.requesterChannel}.`
|
||||
: undefined,
|
||||
`Agent 2 (target) session: ${params.targetSessionKey}.`,
|
||||
params.targetProvider
|
||||
? `Agent 2 (target) provider: ${params.targetProvider}.`
|
||||
params.targetChannel
|
||||
? `Agent 2 (target) channel: ${params.targetChannel}.`
|
||||
: undefined,
|
||||
`Original request: ${params.originalMessage}`,
|
||||
params.roundOneReply
|
||||
@@ -123,7 +123,7 @@ export function buildAgentToAgentAnnounceContext(params: {
|
||||
? `Latest reply: ${params.latestReply}`
|
||||
: "Latest reply: (not available).",
|
||||
`If you want to remain silent, reply exactly "${ANNOUNCE_SKIP_TOKEN}".`,
|
||||
"Any other reply will be posted to the target provider.",
|
||||
"Any other reply will be posted to the target channel.",
|
||||
"After this reply, the agent-to-agent conversation is over.",
|
||||
].filter(Boolean);
|
||||
return lines.join("\n");
|
||||
|
||||
@@ -28,7 +28,7 @@ describe("sessions_send gating", () => {
|
||||
it("blocks cross-agent sends when tools.agentToAgent.enabled is false", async () => {
|
||||
const tool = createSessionsSendTool({
|
||||
agentSessionKey: "agent:main:main",
|
||||
agentProvider: "whatsapp",
|
||||
agentChannel: "whatsapp",
|
||||
});
|
||||
|
||||
const result = await tool.execute("call1", {
|
||||
|
||||
@@ -13,9 +13,9 @@ import {
|
||||
} from "../../routing/session-key.js";
|
||||
import { SESSION_LABEL_MAX_LENGTH } from "../../sessions/session-label.js";
|
||||
import {
|
||||
type GatewayMessageProvider,
|
||||
INTERNAL_MESSAGE_PROVIDER,
|
||||
} from "../../utils/message-provider.js";
|
||||
type GatewayMessageChannel,
|
||||
INTERNAL_MESSAGE_CHANNEL,
|
||||
} from "../../utils/message-channel.js";
|
||||
import { AGENT_LANE_NESTED } from "../lanes.js";
|
||||
import { readLatestAssistantReply, runAgentStep } from "./agent-step.js";
|
||||
import type { AnyAgentTool } from "./common.js";
|
||||
@@ -51,7 +51,7 @@ const SessionsSendToolSchema = Type.Object({
|
||||
|
||||
export function createSessionsSendTool(opts?: {
|
||||
agentSessionKey?: string;
|
||||
agentProvider?: GatewayMessageProvider;
|
||||
agentChannel?: GatewayMessageChannel;
|
||||
sandboxed?: boolean;
|
||||
}): AnyAgentTool {
|
||||
return {
|
||||
@@ -297,7 +297,7 @@ export function createSessionsSendTool(opts?: {
|
||||
|
||||
const agentMessageContext = buildAgentToAgentMessageContext({
|
||||
requesterSessionKey: opts?.agentSessionKey,
|
||||
requesterProvider: opts?.agentProvider,
|
||||
requesterChannel: opts?.agentChannel,
|
||||
targetSessionKey: displayKey,
|
||||
});
|
||||
const sendParams = {
|
||||
@@ -305,12 +305,12 @@ export function createSessionsSendTool(opts?: {
|
||||
sessionKey: resolvedKey,
|
||||
idempotencyKey,
|
||||
deliver: false,
|
||||
provider: INTERNAL_MESSAGE_PROVIDER,
|
||||
channel: INTERNAL_MESSAGE_CHANNEL,
|
||||
lane: AGENT_LANE_NESTED,
|
||||
extraSystemPrompt: agentMessageContext,
|
||||
};
|
||||
const requesterSessionKey = opts?.agentSessionKey;
|
||||
const requesterProvider = opts?.agentProvider;
|
||||
const requesterChannel = opts?.agentChannel;
|
||||
const maxPingPongTurns = resolvePingPongTurns(cfg);
|
||||
const delivery = { status: "pending", mode: "announce" as const };
|
||||
|
||||
@@ -344,7 +344,7 @@ export function createSessionsSendTool(opts?: {
|
||||
sessionKey: resolvedKey,
|
||||
displayKey,
|
||||
});
|
||||
const targetProvider = announceTarget?.provider ?? "unknown";
|
||||
const targetChannel = announceTarget?.channel ?? "unknown";
|
||||
if (
|
||||
maxPingPongTurns > 0 &&
|
||||
requesterSessionKey &&
|
||||
@@ -360,9 +360,9 @@ export function createSessionsSendTool(opts?: {
|
||||
: "target";
|
||||
const replyPrompt = buildAgentToAgentReplyContext({
|
||||
requesterSessionKey,
|
||||
requesterProvider,
|
||||
requesterChannel,
|
||||
targetSessionKey: displayKey,
|
||||
targetProvider,
|
||||
targetChannel,
|
||||
currentRole,
|
||||
turn,
|
||||
maxTurns: maxPingPongTurns,
|
||||
@@ -386,9 +386,9 @@ export function createSessionsSendTool(opts?: {
|
||||
}
|
||||
const announcePrompt = buildAgentToAgentAnnounceContext({
|
||||
requesterSessionKey,
|
||||
requesterProvider,
|
||||
requesterChannel,
|
||||
targetSessionKey: displayKey,
|
||||
targetProvider,
|
||||
targetChannel,
|
||||
originalMessage: message,
|
||||
roundOneReply: primaryReply,
|
||||
latestReply,
|
||||
@@ -412,7 +412,7 @@ export function createSessionsSendTool(opts?: {
|
||||
params: {
|
||||
to: announceTarget.to,
|
||||
message: announceReply.trim(),
|
||||
provider: announceTarget.provider,
|
||||
channel: announceTarget.channel,
|
||||
accountId: announceTarget.accountId,
|
||||
idempotencyKey: crypto.randomUUID(),
|
||||
},
|
||||
@@ -421,7 +421,7 @@ export function createSessionsSendTool(opts?: {
|
||||
} catch (err) {
|
||||
log.warn("sessions_send announce delivery failed", {
|
||||
runId: runContextId,
|
||||
provider: announceTarget.provider,
|
||||
channel: announceTarget.channel,
|
||||
to: announceTarget.to,
|
||||
error: formatErrorMessage(err),
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
normalizeAgentId,
|
||||
parseAgentSessionKey,
|
||||
} from "../../routing/session-key.js";
|
||||
import type { GatewayMessageProvider } from "../../utils/message-provider.js";
|
||||
import type { GatewayMessageChannel } from "../../utils/message-channel.js";
|
||||
import { resolveAgentConfig } from "../agent-scope.js";
|
||||
import { AGENT_LANE_SUBAGENT } from "../lanes.js";
|
||||
import { optionalStringEnum } from "../schema/typebox.js";
|
||||
@@ -47,7 +47,7 @@ function normalizeModelSelection(value: unknown): string | undefined {
|
||||
|
||||
export function createSessionsSpawnTool(opts?: {
|
||||
agentSessionKey?: string;
|
||||
agentProvider?: GatewayMessageProvider;
|
||||
agentChannel?: GatewayMessageChannel;
|
||||
sandboxed?: boolean;
|
||||
}): AnyAgentTool {
|
||||
return {
|
||||
@@ -174,7 +174,7 @@ export function createSessionsSpawnTool(opts?: {
|
||||
}
|
||||
const childSystemPrompt = buildSubagentSystemPrompt({
|
||||
requesterSessionKey,
|
||||
requesterProvider: opts?.agentProvider,
|
||||
requesterChannel: opts?.agentChannel,
|
||||
childSessionKey,
|
||||
label: label || undefined,
|
||||
task,
|
||||
@@ -188,7 +188,7 @@ export function createSessionsSpawnTool(opts?: {
|
||||
params: {
|
||||
message: task,
|
||||
sessionKey: childSessionKey,
|
||||
provider: opts?.agentProvider,
|
||||
channel: opts?.agentChannel,
|
||||
idempotencyKey: childIdem,
|
||||
deliver: false,
|
||||
lane: AGENT_LANE_SUBAGENT,
|
||||
@@ -221,7 +221,7 @@ export function createSessionsSpawnTool(opts?: {
|
||||
runId: childRunId,
|
||||
childSessionKey,
|
||||
requesterSessionKey: requesterInternalKey,
|
||||
requesterProvider: opts?.agentProvider,
|
||||
requesterChannel: opts?.agentChannel,
|
||||
requesterDisplayKey,
|
||||
task,
|
||||
cleanup,
|
||||
|
||||
@@ -36,7 +36,7 @@ vi.mock("../../slack/actions.js", () => ({
|
||||
|
||||
describe("handleSlackAction", () => {
|
||||
it("adds reactions", async () => {
|
||||
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
|
||||
await handleSlackAction(
|
||||
{
|
||||
action: "react",
|
||||
@@ -50,7 +50,7 @@ describe("handleSlackAction", () => {
|
||||
});
|
||||
|
||||
it("removes reactions on empty emoji", async () => {
|
||||
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
|
||||
await handleSlackAction(
|
||||
{
|
||||
action: "react",
|
||||
@@ -64,7 +64,7 @@ describe("handleSlackAction", () => {
|
||||
});
|
||||
|
||||
it("removes reactions when remove flag set", async () => {
|
||||
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
|
||||
await handleSlackAction(
|
||||
{
|
||||
action: "react",
|
||||
@@ -79,7 +79,7 @@ describe("handleSlackAction", () => {
|
||||
});
|
||||
|
||||
it("rejects removes without emoji", async () => {
|
||||
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
|
||||
await expect(
|
||||
handleSlackAction(
|
||||
{
|
||||
@@ -96,7 +96,7 @@ describe("handleSlackAction", () => {
|
||||
|
||||
it("respects reaction gating", async () => {
|
||||
const cfg = {
|
||||
slack: { botToken: "tok", actions: { reactions: false } },
|
||||
channels: { slack: { botToken: "tok", actions: { reactions: false } } },
|
||||
} as ClawdbotConfig;
|
||||
await expect(
|
||||
handleSlackAction(
|
||||
@@ -112,7 +112,7 @@ describe("handleSlackAction", () => {
|
||||
});
|
||||
|
||||
it("passes threadTs to sendSlackMessage for thread replies", async () => {
|
||||
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
|
||||
await handleSlackAction(
|
||||
{
|
||||
action: "sendMessage",
|
||||
@@ -133,7 +133,7 @@ describe("handleSlackAction", () => {
|
||||
});
|
||||
|
||||
it("auto-injects threadTs from context when replyToMode=all", async () => {
|
||||
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
|
||||
sendSlackMessage.mockClear();
|
||||
await handleSlackAction(
|
||||
{
|
||||
@@ -159,7 +159,7 @@ describe("handleSlackAction", () => {
|
||||
});
|
||||
|
||||
it("replyToMode=first threads first message then stops", async () => {
|
||||
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
|
||||
sendSlackMessage.mockClear();
|
||||
const hasRepliedRef = { value: false };
|
||||
const context = {
|
||||
@@ -198,7 +198,7 @@ describe("handleSlackAction", () => {
|
||||
});
|
||||
|
||||
it("replyToMode=first marks hasRepliedRef even when threadTs is explicit", async () => {
|
||||
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
|
||||
sendSlackMessage.mockClear();
|
||||
const hasRepliedRef = { value: false };
|
||||
const context = {
|
||||
@@ -244,7 +244,7 @@ describe("handleSlackAction", () => {
|
||||
});
|
||||
|
||||
it("replyToMode=first without hasRepliedRef does not thread", async () => {
|
||||
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
|
||||
sendSlackMessage.mockClear();
|
||||
await handleSlackAction(
|
||||
{ action: "sendMessage", to: "channel:C123", content: "No ref" },
|
||||
@@ -263,7 +263,7 @@ describe("handleSlackAction", () => {
|
||||
});
|
||||
|
||||
it("does not auto-inject threadTs when replyToMode=off", async () => {
|
||||
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
|
||||
sendSlackMessage.mockClear();
|
||||
await handleSlackAction(
|
||||
{
|
||||
@@ -285,7 +285,7 @@ describe("handleSlackAction", () => {
|
||||
});
|
||||
|
||||
it("does not auto-inject threadTs when sending to different channel", async () => {
|
||||
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
|
||||
sendSlackMessage.mockClear();
|
||||
await handleSlackAction(
|
||||
{
|
||||
@@ -311,7 +311,7 @@ describe("handleSlackAction", () => {
|
||||
});
|
||||
|
||||
it("explicit threadTs overrides context threadTs", async () => {
|
||||
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
|
||||
sendSlackMessage.mockClear();
|
||||
await handleSlackAction(
|
||||
{
|
||||
@@ -338,7 +338,7 @@ describe("handleSlackAction", () => {
|
||||
});
|
||||
|
||||
it("handles channel target without prefix when replyToMode=all", async () => {
|
||||
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
|
||||
sendSlackMessage.mockClear();
|
||||
await handleSlackAction(
|
||||
{
|
||||
|
||||
@@ -93,7 +93,7 @@ export async function handleSlackAction(
|
||||
const accountId = readStringParam(params, "accountId");
|
||||
const accountOpts = accountId ? { accountId } : undefined;
|
||||
const account = resolveSlackAccount({ cfg, accountId });
|
||||
const actionConfig = account.actions ?? cfg.slack?.actions;
|
||||
const actionConfig = account.actions ?? cfg.channels?.slack?.actions;
|
||||
const isActionEnabled = createActionGate(actionConfig);
|
||||
|
||||
if (reactionsActions.has(action)) {
|
||||
|
||||
@@ -34,7 +34,9 @@ describe("handleTelegramAction", () => {
|
||||
});
|
||||
|
||||
it("adds reactions", async () => {
|
||||
const cfg = { telegram: { botToken: "tok" } } as ClawdbotConfig;
|
||||
const cfg = {
|
||||
channels: { telegram: { botToken: "tok" } },
|
||||
} as ClawdbotConfig;
|
||||
await handleTelegramAction(
|
||||
{
|
||||
action: "react",
|
||||
@@ -53,7 +55,9 @@ describe("handleTelegramAction", () => {
|
||||
});
|
||||
|
||||
it("removes reactions on empty emoji", async () => {
|
||||
const cfg = { telegram: { botToken: "tok" } } as ClawdbotConfig;
|
||||
const cfg = {
|
||||
channels: { telegram: { botToken: "tok" } },
|
||||
} as ClawdbotConfig;
|
||||
await handleTelegramAction(
|
||||
{
|
||||
action: "react",
|
||||
@@ -72,7 +76,9 @@ describe("handleTelegramAction", () => {
|
||||
});
|
||||
|
||||
it("removes reactions when remove flag set", async () => {
|
||||
const cfg = { telegram: { botToken: "tok" } } as ClawdbotConfig;
|
||||
const cfg = {
|
||||
channels: { telegram: { botToken: "tok" } },
|
||||
} as ClawdbotConfig;
|
||||
await handleTelegramAction(
|
||||
{
|
||||
action: "react",
|
||||
@@ -93,7 +99,9 @@ describe("handleTelegramAction", () => {
|
||||
|
||||
it("respects reaction gating", async () => {
|
||||
const cfg = {
|
||||
telegram: { botToken: "tok", actions: { reactions: false } },
|
||||
channels: {
|
||||
telegram: { botToken: "tok", actions: { reactions: false } },
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
await expect(
|
||||
handleTelegramAction(
|
||||
@@ -109,7 +117,9 @@ describe("handleTelegramAction", () => {
|
||||
});
|
||||
|
||||
it("sends a text message", async () => {
|
||||
const cfg = { telegram: { botToken: "tok" } } as ClawdbotConfig;
|
||||
const cfg = {
|
||||
channels: { telegram: { botToken: "tok" } },
|
||||
} as ClawdbotConfig;
|
||||
const result = await handleTelegramAction(
|
||||
{
|
||||
action: "sendMessage",
|
||||
@@ -130,7 +140,9 @@ describe("handleTelegramAction", () => {
|
||||
});
|
||||
|
||||
it("sends a message with media", async () => {
|
||||
const cfg = { telegram: { botToken: "tok" } } as ClawdbotConfig;
|
||||
const cfg = {
|
||||
channels: { telegram: { botToken: "tok" } },
|
||||
} as ClawdbotConfig;
|
||||
await handleTelegramAction(
|
||||
{
|
||||
action: "sendMessage",
|
||||
@@ -152,7 +164,9 @@ describe("handleTelegramAction", () => {
|
||||
|
||||
it("respects sendMessage gating", async () => {
|
||||
const cfg = {
|
||||
telegram: { botToken: "tok", actions: { sendMessage: false } },
|
||||
channels: {
|
||||
telegram: { botToken: "tok", actions: { sendMessage: false } },
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
await expect(
|
||||
handleTelegramAction(
|
||||
@@ -182,7 +196,9 @@ describe("handleTelegramAction", () => {
|
||||
});
|
||||
|
||||
it("requires inlineButtons capability when buttons are provided", async () => {
|
||||
const cfg = { telegram: { botToken: "tok" } } as ClawdbotConfig;
|
||||
const cfg = {
|
||||
channels: { telegram: { botToken: "tok" } },
|
||||
} as ClawdbotConfig;
|
||||
await expect(
|
||||
handleTelegramAction(
|
||||
{
|
||||
@@ -198,7 +214,9 @@ describe("handleTelegramAction", () => {
|
||||
|
||||
it("sends messages with inline keyboard buttons when enabled", async () => {
|
||||
const cfg = {
|
||||
telegram: { botToken: "tok", capabilities: ["inlineButtons"] },
|
||||
channels: {
|
||||
telegram: { botToken: "tok", capabilities: ["inlineButtons"] },
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
await handleTelegramAction(
|
||||
{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
|
||||
import { resolveChannelCapabilities } from "../../config/channel-capabilities.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { resolveProviderCapabilities } from "../../config/provider-capabilities.js";
|
||||
import {
|
||||
reactMessageTelegram,
|
||||
sendMessageTelegram,
|
||||
@@ -26,9 +25,9 @@ function hasInlineButtonsCapability(params: {
|
||||
accountId?: string | undefined;
|
||||
}): boolean {
|
||||
const caps =
|
||||
resolveProviderCapabilities({
|
||||
resolveChannelCapabilities({
|
||||
cfg: params.cfg,
|
||||
provider: "telegram",
|
||||
channel: "telegram",
|
||||
accountId: params.accountId,
|
||||
}) ?? [];
|
||||
return caps.some((cap) => cap.toLowerCase() === "inlinebuttons");
|
||||
@@ -84,7 +83,7 @@ export async function handleTelegramAction(
|
||||
): Promise<AgentToolResult<unknown>> {
|
||||
const action = readStringParam(params, "action", { required: true });
|
||||
const accountId = readStringParam(params, "accountId");
|
||||
const isActionEnabled = createActionGate(cfg.telegram?.actions);
|
||||
const isActionEnabled = createActionGate(cfg.channels?.telegram?.actions);
|
||||
|
||||
if (action === "react") {
|
||||
if (!isActionEnabled("reactions")) {
|
||||
@@ -103,7 +102,7 @@ export async function handleTelegramAction(
|
||||
const token = resolveTelegramToken(cfg, { accountId }).token;
|
||||
if (!token) {
|
||||
throw new Error(
|
||||
"Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or telegram.botToken.",
|
||||
"Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or channels.telegram.botToken.",
|
||||
);
|
||||
}
|
||||
await reactMessageTelegram(chatId ?? "", messageId ?? 0, emoji ?? "", {
|
||||
@@ -130,7 +129,7 @@ export async function handleTelegramAction(
|
||||
!hasInlineButtonsCapability({ cfg, accountId: accountId ?? undefined })
|
||||
) {
|
||||
throw new Error(
|
||||
'Telegram inline buttons requested but not enabled. Add "inlineButtons" to telegram.capabilities (or telegram.accounts.<id>.capabilities).',
|
||||
'Telegram inline buttons requested but not enabled. Add "inlineButtons" to channels.telegram.capabilities (or channels.telegram.accounts.<id>.capabilities).',
|
||||
);
|
||||
}
|
||||
// Optional threading parameters for forum topics and reply chains
|
||||
@@ -143,7 +142,7 @@ export async function handleTelegramAction(
|
||||
const token = resolveTelegramToken(cfg, { accountId }).token;
|
||||
if (!token) {
|
||||
throw new Error(
|
||||
"Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or telegram.botToken.",
|
||||
"Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or channels.telegram.botToken.",
|
||||
);
|
||||
}
|
||||
const result = await sendMessageTelegram(to, content, {
|
||||
|
||||
@@ -10,7 +10,7 @@ vi.mock("../../web/outbound.js", () => ({
|
||||
}));
|
||||
|
||||
const enabledConfig = {
|
||||
whatsapp: { actions: { reactions: true } },
|
||||
channels: { whatsapp: { actions: { reactions: true } } },
|
||||
} as ClawdbotConfig;
|
||||
|
||||
describe("handleWhatsAppAction", () => {
|
||||
@@ -112,7 +112,7 @@ describe("handleWhatsAppAction", () => {
|
||||
|
||||
it("respects reaction gating", async () => {
|
||||
const cfg = {
|
||||
whatsapp: { actions: { reactions: false } },
|
||||
channels: { whatsapp: { actions: { reactions: false } } },
|
||||
} as ClawdbotConfig;
|
||||
await expect(
|
||||
handleWhatsAppAction(
|
||||
|
||||
@@ -14,7 +14,7 @@ export async function handleWhatsAppAction(
|
||||
cfg: ClawdbotConfig,
|
||||
): Promise<AgentToolResult<unknown>> {
|
||||
const action = readStringParam(params, "action", { required: true });
|
||||
const isActionEnabled = createActionGate(cfg.whatsapp?.actions);
|
||||
const isActionEnabled = createActionGate(cfg.channels?.whatsapp?.actions);
|
||||
|
||||
if (action === "react") {
|
||||
if (!isActionEnabled("reactions")) {
|
||||
|
||||
@@ -126,18 +126,20 @@ describe("resolveTextChunkLimit", () => {
|
||||
});
|
||||
|
||||
it("supports provider overrides", () => {
|
||||
const cfg = { telegram: { textChunkLimit: 1234 } };
|
||||
const cfg = { channels: { telegram: { textChunkLimit: 1234 } } };
|
||||
expect(resolveTextChunkLimit(cfg, "whatsapp")).toBe(4000);
|
||||
expect(resolveTextChunkLimit(cfg, "telegram")).toBe(1234);
|
||||
});
|
||||
|
||||
it("prefers account overrides when provided", () => {
|
||||
const cfg = {
|
||||
telegram: {
|
||||
textChunkLimit: 2000,
|
||||
accounts: {
|
||||
default: { textChunkLimit: 1234 },
|
||||
primary: { textChunkLimit: 777 },
|
||||
channels: {
|
||||
telegram: {
|
||||
textChunkLimit: 2000,
|
||||
accounts: {
|
||||
default: { textChunkLimit: 1234 },
|
||||
primary: { textChunkLimit: 777 },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -147,8 +149,10 @@ describe("resolveTextChunkLimit", () => {
|
||||
|
||||
it("uses the matching provider override", () => {
|
||||
const cfg = {
|
||||
discord: { textChunkLimit: 111 },
|
||||
slack: { textChunkLimit: 222 },
|
||||
channels: {
|
||||
discord: { textChunkLimit: 111 },
|
||||
slack: { textChunkLimit: 222 },
|
||||
},
|
||||
};
|
||||
expect(resolveTextChunkLimit(cfg, "discord")).toBe(111);
|
||||
expect(resolveTextChunkLimit(cfg, "slack")).toBe(222);
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
// unintentionally breaking on newlines. Using [\s\S] keeps newlines inside
|
||||
// the chunk so messages are only split when they truly exceed the limit.
|
||||
|
||||
import type { ChannelId } from "../channels/plugins/types.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import {
|
||||
findFenceSpanAt,
|
||||
isSafeFenceBreak,
|
||||
parseFenceSpans,
|
||||
} from "../markdown/fences.js";
|
||||
import type { ProviderId } from "../providers/plugins/types.js";
|
||||
import { normalizeAccountId } from "../routing/session-key.js";
|
||||
import { INTERNAL_MESSAGE_PROVIDER } from "../utils/message-provider.js";
|
||||
import { INTERNAL_MESSAGE_CHANNEL } from "../utils/message-channel.js";
|
||||
|
||||
export type TextChunkProvider = ProviderId | typeof INTERNAL_MESSAGE_PROVIDER;
|
||||
export type TextChunkProvider = ChannelId | typeof INTERNAL_MESSAGE_CHANNEL;
|
||||
|
||||
const DEFAULT_CHUNK_LIMIT = 4000;
|
||||
|
||||
@@ -55,10 +55,12 @@ export function resolveTextChunkLimit(
|
||||
? opts.fallbackLimit
|
||||
: DEFAULT_CHUNK_LIMIT;
|
||||
const providerOverride = (() => {
|
||||
if (!provider || provider === INTERNAL_MESSAGE_PROVIDER) return undefined;
|
||||
const providerConfig = (cfg as Record<string, unknown> | undefined)?.[
|
||||
provider
|
||||
] as ProviderChunkConfig | undefined;
|
||||
if (!provider || provider === INTERNAL_MESSAGE_CHANNEL) return undefined;
|
||||
const channelsConfig = cfg?.channels as Record<string, unknown> | undefined;
|
||||
const providerConfig = (channelsConfig?.[provider] ??
|
||||
(cfg as Record<string, unknown> | undefined)?.[provider]) as
|
||||
| ProviderChunkConfig
|
||||
| undefined;
|
||||
return resolveChunkLimitForProvider(providerConfig, accountId);
|
||||
})();
|
||||
if (typeof providerOverride === "number" && providerOverride > 0) {
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { MsgContext } from "./templating.js";
|
||||
describe("resolveCommandAuthorization", () => {
|
||||
it("falls back from empty SenderId to SenderE164", () => {
|
||||
const cfg = {
|
||||
whatsapp: { allowFrom: ["+123"] },
|
||||
channels: { whatsapp: { allowFrom: ["+123"] } },
|
||||
} as ClawdbotConfig;
|
||||
|
||||
const ctx = {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { ChannelDock } from "../channels/dock.js";
|
||||
import { getChannelDock, listChannelDocks } from "../channels/dock.js";
|
||||
import type { ChannelId } from "../channels/plugins/types.js";
|
||||
import { normalizeChannelId } from "../channels/registry.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import type { ProviderDock } from "../providers/dock.js";
|
||||
import { getProviderDock, listProviderDocks } from "../providers/dock.js";
|
||||
import type { ProviderId } from "../providers/plugins/types.js";
|
||||
import { normalizeProviderId } from "../providers/registry.js";
|
||||
import type { MsgContext } from "./templating.js";
|
||||
|
||||
export type CommandAuthorization = {
|
||||
providerId?: ProviderId;
|
||||
providerId?: ChannelId;
|
||||
ownerList: string[];
|
||||
senderId?: string;
|
||||
isAuthorizedSender: boolean;
|
||||
@@ -17,20 +17,20 @@ export type CommandAuthorization = {
|
||||
function resolveProviderFromContext(
|
||||
ctx: MsgContext,
|
||||
cfg: ClawdbotConfig,
|
||||
): ProviderId | undefined {
|
||||
): ChannelId | undefined {
|
||||
const direct =
|
||||
normalizeProviderId(ctx.Provider) ??
|
||||
normalizeProviderId(ctx.Surface) ??
|
||||
normalizeProviderId(ctx.OriginatingChannel);
|
||||
normalizeChannelId(ctx.Provider) ??
|
||||
normalizeChannelId(ctx.Surface) ??
|
||||
normalizeChannelId(ctx.OriginatingChannel);
|
||||
if (direct) return direct;
|
||||
const candidates = [ctx.From, ctx.To]
|
||||
.filter((value): value is string => Boolean(value?.trim()))
|
||||
.flatMap((value) => value.split(":").map((part) => part.trim()));
|
||||
for (const candidate of candidates) {
|
||||
const normalized = normalizeProviderId(candidate);
|
||||
const normalized = normalizeChannelId(candidate);
|
||||
if (normalized) return normalized;
|
||||
}
|
||||
const configured = listProviderDocks()
|
||||
const configured = listChannelDocks()
|
||||
.map((dock) => {
|
||||
if (!dock.config?.resolveAllowFrom) return null;
|
||||
const allowFrom = dock.config.resolveAllowFrom({
|
||||
@@ -40,13 +40,13 @@ function resolveProviderFromContext(
|
||||
if (!Array.isArray(allowFrom) || allowFrom.length === 0) return null;
|
||||
return dock.id;
|
||||
})
|
||||
.filter((value): value is ProviderId => Boolean(value));
|
||||
.filter((value): value is ChannelId => Boolean(value));
|
||||
if (configured.length === 1) return configured[0];
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function formatAllowFromList(params: {
|
||||
dock?: ProviderDock;
|
||||
dock?: ChannelDock;
|
||||
cfg: ClawdbotConfig;
|
||||
accountId?: string | null;
|
||||
allowFrom: Array<string | number>;
|
||||
@@ -66,7 +66,7 @@ export function resolveCommandAuthorization(params: {
|
||||
}): CommandAuthorization {
|
||||
const { ctx, cfg, commandAuthorized } = params;
|
||||
const providerId = resolveProviderFromContext(ctx, cfg);
|
||||
const dock = providerId ? getProviderDock(providerId) : undefined;
|
||||
const dock = providerId ? getChannelDock(providerId) : undefined;
|
||||
const from = (ctx.From ?? "").trim();
|
||||
const to = (ctx.To ?? "").trim();
|
||||
const allowFromRaw = dock?.config?.resolveAllowFrom
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { listChannelDocks } from "../channels/dock.js";
|
||||
import type { ClawdbotConfig } from "../config/types.js";
|
||||
import { listProviderDocks } from "../providers/dock.js";
|
||||
|
||||
export type CommandScope = "text" | "native" | "both";
|
||||
|
||||
@@ -276,7 +276,7 @@ let cachedNativeCommandSurfaces: Set<string> | null = null;
|
||||
const getNativeCommandSurfaces = (): Set<string> => {
|
||||
if (!cachedNativeCommandSurfaces) {
|
||||
cachedNativeCommandSurfaces = new Set(
|
||||
listProviderDocks()
|
||||
listChannelDocks()
|
||||
.filter((dock) => dock.capabilities.nativeCommands)
|
||||
.map((dock) => dock.id),
|
||||
);
|
||||
|
||||
@@ -3,13 +3,13 @@ import { describe, expect, it } from "vitest";
|
||||
import { formatAgentEnvelope } from "./envelope.js";
|
||||
|
||||
describe("formatAgentEnvelope", () => {
|
||||
it("includes provider, from, ip, host, and timestamp", () => {
|
||||
it("includes channel, from, ip, host, and timestamp", () => {
|
||||
const originalTz = process.env.TZ;
|
||||
process.env.TZ = "UTC";
|
||||
|
||||
const ts = Date.UTC(2025, 0, 2, 3, 4); // 2025-01-02T03:04:00Z
|
||||
const body = formatAgentEnvelope({
|
||||
provider: "WebChat",
|
||||
channel: "WebChat",
|
||||
from: "user1",
|
||||
host: "mac-mini",
|
||||
ip: "10.0.0.5",
|
||||
@@ -30,7 +30,7 @@ describe("formatAgentEnvelope", () => {
|
||||
|
||||
const ts = Date.UTC(2025, 0, 2, 3, 4); // 2025-01-02T03:04:00Z
|
||||
const body = formatAgentEnvelope({
|
||||
provider: "WebChat",
|
||||
channel: "WebChat",
|
||||
timestamp: ts,
|
||||
body: "hello",
|
||||
});
|
||||
@@ -41,7 +41,7 @@ describe("formatAgentEnvelope", () => {
|
||||
});
|
||||
|
||||
it("handles missing optional fields", () => {
|
||||
const body = formatAgentEnvelope({ provider: "Telegram", body: "hi" });
|
||||
const body = formatAgentEnvelope({ channel: "Telegram", body: "hi" });
|
||||
expect(body).toBe("[Telegram] hi");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export type AgentEnvelopeParams = {
|
||||
provider: string;
|
||||
channel: string;
|
||||
from?: string;
|
||||
timestamp?: number | Date;
|
||||
host?: string;
|
||||
@@ -24,8 +24,8 @@ function formatTimestamp(ts?: number | Date): string | undefined {
|
||||
}
|
||||
|
||||
export function formatAgentEnvelope(params: AgentEnvelopeParams): string {
|
||||
const provider = params.provider?.trim() || "Provider";
|
||||
const parts: string[] = [provider];
|
||||
const channel = params.channel?.trim() || "Channel";
|
||||
const parts: string[] = [channel];
|
||||
if (params.from?.trim()) parts.push(params.from.trim());
|
||||
if (params.host?.trim()) parts.push(params.host.trim());
|
||||
if (params.ip?.trim()) parts.push(params.ip.trim());
|
||||
@@ -36,13 +36,13 @@ export function formatAgentEnvelope(params: AgentEnvelopeParams): string {
|
||||
}
|
||||
|
||||
export function formatThreadStarterEnvelope(params: {
|
||||
provider: string;
|
||||
channel: string;
|
||||
author?: string;
|
||||
timestamp?: number | Date;
|
||||
body: string;
|
||||
}): string {
|
||||
return formatAgentEnvelope({
|
||||
provider: params.provider,
|
||||
channel: params.channel,
|
||||
from: params.author,
|
||||
timestamp: params.timestamp,
|
||||
body: params.body,
|
||||
|
||||
@@ -97,7 +97,7 @@ describe("block streaming", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
},
|
||||
);
|
||||
@@ -156,7 +156,7 @@ describe("block streaming", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
telegram: { allowFrom: ["*"] },
|
||||
channels: { telegram: { allowFrom: ["*"] } },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
},
|
||||
);
|
||||
@@ -205,7 +205,7 @@ describe("block streaming", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
},
|
||||
);
|
||||
@@ -263,7 +263,7 @@ describe("block streaming", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
telegram: { allowFrom: ["*"] },
|
||||
channels: { telegram: { allowFrom: ["*"] } },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
},
|
||||
);
|
||||
@@ -305,7 +305,7 @@ describe("block streaming", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
telegram: { allowFrom: ["*"], streamMode: "block" },
|
||||
channels: { telegram: { allowFrom: ["*"], streamMode: "block" } },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
},
|
||||
);
|
||||
|
||||
@@ -181,7 +181,7 @@ describe("directive behavior", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
},
|
||||
);
|
||||
@@ -210,7 +210,7 @@ describe("directive behavior", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
},
|
||||
);
|
||||
@@ -250,7 +250,7 @@ describe("directive behavior", () => {
|
||||
drop: "summarize",
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
},
|
||||
);
|
||||
@@ -383,7 +383,7 @@ describe("directive behavior", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
},
|
||||
);
|
||||
@@ -419,7 +419,7 @@ describe("directive behavior", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
},
|
||||
);
|
||||
@@ -459,7 +459,7 @@ describe("directive behavior", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
},
|
||||
);
|
||||
@@ -494,9 +494,7 @@ describe("directive behavior", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
},
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
},
|
||||
);
|
||||
@@ -541,9 +539,7 @@ describe("directive behavior", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
},
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: storePath },
|
||||
},
|
||||
);
|
||||
@@ -589,7 +585,7 @@ describe("directive behavior", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: storePath },
|
||||
},
|
||||
);
|
||||
@@ -613,7 +609,7 @@ describe("directive behavior", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: storePath },
|
||||
},
|
||||
);
|
||||
@@ -803,7 +799,7 @@ describe("directive behavior", () => {
|
||||
allowFrom: { whatsapp: ["+1222"] },
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["+1222"] },
|
||||
channels: { whatsapp: { allowFrom: ["+1222"] } },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
},
|
||||
);
|
||||
@@ -842,7 +838,7 @@ describe("directive behavior", () => {
|
||||
allowFrom: { whatsapp: ["+1222"] },
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["+1222"] },
|
||||
channels: { whatsapp: { allowFrom: ["+1222"] } },
|
||||
session: { store: storePath },
|
||||
},
|
||||
);
|
||||
@@ -894,7 +890,7 @@ describe("directive behavior", () => {
|
||||
allowFrom: { whatsapp: ["+1222"] },
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["+1222"] },
|
||||
channels: { whatsapp: { allowFrom: ["+1222"] } },
|
||||
session: { store: storePath },
|
||||
},
|
||||
);
|
||||
@@ -937,7 +933,7 @@ describe("directive behavior", () => {
|
||||
allowFrom: { whatsapp: ["+1222"] },
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["+1222"] },
|
||||
channels: { whatsapp: { allowFrom: ["+1222"] } },
|
||||
session: { store: storePath },
|
||||
},
|
||||
);
|
||||
@@ -964,7 +960,7 @@ describe("directive behavior", () => {
|
||||
allowFrom: { whatsapp: ["+1222"] },
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["+1222"] },
|
||||
channels: { whatsapp: { allowFrom: ["+1222"] } },
|
||||
session: { store: storePath },
|
||||
},
|
||||
);
|
||||
@@ -993,7 +989,7 @@ describe("directive behavior", () => {
|
||||
allowFrom: { whatsapp: ["+1222"] },
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["+1222"] },
|
||||
channels: { whatsapp: { allowFrom: ["+1222"] } },
|
||||
session: { store: storePath },
|
||||
} as const;
|
||||
|
||||
@@ -1079,7 +1075,7 @@ describe("directive behavior", () => {
|
||||
allowFrom: { whatsapp: ["+1222"] },
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["+1222"] },
|
||||
channels: { whatsapp: { allowFrom: ["+1222"] } },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
},
|
||||
);
|
||||
@@ -1126,7 +1122,7 @@ describe("directive behavior", () => {
|
||||
allowFrom: { whatsapp: ["+1222", "+1333"] },
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["+1222", "+1333"] },
|
||||
channels: { whatsapp: { allowFrom: ["+1222", "+1333"] } },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
},
|
||||
);
|
||||
@@ -1173,7 +1169,7 @@ describe("directive behavior", () => {
|
||||
allowFrom: { whatsapp: ["+1222", "+1333"] },
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["+1222", "+1333"] },
|
||||
channels: { whatsapp: { allowFrom: ["+1222", "+1333"] } },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
},
|
||||
);
|
||||
@@ -1210,7 +1206,7 @@ describe("directive behavior", () => {
|
||||
allowFrom: { whatsapp: ["+1222"] },
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["+1222"] },
|
||||
channels: { whatsapp: { allowFrom: ["+1222"] } },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
},
|
||||
);
|
||||
@@ -1247,7 +1243,7 @@ describe("directive behavior", () => {
|
||||
allowFrom: { whatsapp: ["+1222"] },
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["+1222"] },
|
||||
channels: { whatsapp: { allowFrom: ["+1222"] } },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
},
|
||||
);
|
||||
@@ -1283,7 +1279,7 @@ describe("directive behavior", () => {
|
||||
allowFrom: { whatsapp: ["+1222"] },
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["+1222"] },
|
||||
channels: { whatsapp: { allowFrom: ["+1222"] } },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
},
|
||||
);
|
||||
@@ -1321,7 +1317,7 @@ describe("directive behavior", () => {
|
||||
allowFrom: { whatsapp: ["+1222"] },
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["+1222"] },
|
||||
channels: { whatsapp: { allowFrom: ["+1222"] } },
|
||||
session: { store: storePath },
|
||||
},
|
||||
);
|
||||
@@ -1375,7 +1371,7 @@ describe("directive behavior", () => {
|
||||
allowFrom: { whatsapp: ["+1222"] },
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["+1222"] },
|
||||
channels: { whatsapp: { allowFrom: ["+1222"] } },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
},
|
||||
);
|
||||
@@ -1401,7 +1397,7 @@ describe("directive behavior", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: storePath },
|
||||
},
|
||||
);
|
||||
@@ -1434,7 +1430,7 @@ describe("directive behavior", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: storePath },
|
||||
},
|
||||
);
|
||||
@@ -1469,7 +1465,7 @@ describe("directive behavior", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: storePath },
|
||||
},
|
||||
);
|
||||
@@ -1484,7 +1480,7 @@ describe("directive behavior", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: storePath },
|
||||
},
|
||||
);
|
||||
@@ -1545,9 +1541,7 @@ describe("directive behavior", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
},
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: storePath },
|
||||
},
|
||||
);
|
||||
@@ -1608,9 +1602,7 @@ describe("directive behavior", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
},
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: storePath },
|
||||
},
|
||||
);
|
||||
@@ -1625,9 +1617,7 @@ describe("directive behavior", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
},
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: storePath },
|
||||
},
|
||||
);
|
||||
@@ -2283,7 +2273,7 @@ describe("directive behavior", () => {
|
||||
},
|
||||
},
|
||||
tools: { elevated: { allowFrom: { whatsapp: ["*"] } } },
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: storePath },
|
||||
},
|
||||
);
|
||||
@@ -2313,7 +2303,7 @@ describe("directive behavior", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: storePath },
|
||||
},
|
||||
);
|
||||
@@ -2352,9 +2342,7 @@ describe("directive behavior", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
},
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: storePath },
|
||||
},
|
||||
);
|
||||
@@ -2403,9 +2391,7 @@ describe("directive behavior", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
},
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: storePath },
|
||||
},
|
||||
);
|
||||
@@ -2448,9 +2434,7 @@ describe("directive behavior", () => {
|
||||
allowFrom: { whatsapp: ["+1004"] },
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
},
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: storePath },
|
||||
},
|
||||
);
|
||||
|
||||
@@ -60,8 +60,10 @@ function makeCfg(home: string) {
|
||||
workspace: join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
},
|
||||
},
|
||||
session: { store: join(home, "sessions.json") },
|
||||
};
|
||||
|
||||
@@ -50,7 +50,7 @@ function makeCfg(home: string) {
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ function makeCfg(home: string, queue?: Record<string, unknown>) {
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
messages: queue ? { queue } : undefined,
|
||||
};
|
||||
|
||||
@@ -67,7 +67,7 @@ describe("RawBody directive parsing", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
},
|
||||
);
|
||||
@@ -103,7 +103,7 @@ describe("RawBody directive parsing", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
},
|
||||
);
|
||||
@@ -136,7 +136,7 @@ describe("RawBody directive parsing", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
},
|
||||
);
|
||||
@@ -173,7 +173,7 @@ describe("RawBody directive parsing", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["+1222"] },
|
||||
channels: { whatsapp: { allowFrom: ["+1222"] } },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
},
|
||||
);
|
||||
@@ -220,7 +220,7 @@ describe("RawBody directive parsing", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
},
|
||||
);
|
||||
|
||||
@@ -95,8 +95,10 @@ function makeCfg(home: string) {
|
||||
workspace: join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
},
|
||||
},
|
||||
session: { store: join(home, "sessions.json") },
|
||||
};
|
||||
@@ -856,8 +858,10 @@ describe("trigger handling", () => {
|
||||
workspace: join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["+1000"],
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["+1000"],
|
||||
},
|
||||
},
|
||||
session: { store: join(home, "sessions.json") },
|
||||
};
|
||||
@@ -886,8 +890,10 @@ describe("trigger handling", () => {
|
||||
workspace: join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["+1000"],
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["+1000"],
|
||||
},
|
||||
},
|
||||
session: { store: join(home, "sessions.json") },
|
||||
};
|
||||
@@ -923,8 +929,10 @@ describe("trigger handling", () => {
|
||||
workspace: join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["+1000"],
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["+1000"],
|
||||
},
|
||||
},
|
||||
session: { store: join(home, "sessions.json") },
|
||||
};
|
||||
@@ -965,8 +973,10 @@ describe("trigger handling", () => {
|
||||
workspace: join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["+1000"],
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["+1000"],
|
||||
},
|
||||
},
|
||||
session: { store: join(home, "sessions.json") },
|
||||
};
|
||||
@@ -1017,8 +1027,10 @@ describe("trigger handling", () => {
|
||||
workspace: join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["+1000"],
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["+1000"],
|
||||
},
|
||||
},
|
||||
session: { store: join(home, "sessions.json") },
|
||||
};
|
||||
@@ -1060,8 +1072,10 @@ describe("trigger handling", () => {
|
||||
allowFrom: { whatsapp: ["+1000"] },
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["+1000"],
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["+1000"],
|
||||
},
|
||||
},
|
||||
session: { store: join(home, "sessions.json") },
|
||||
};
|
||||
@@ -1104,8 +1118,10 @@ describe("trigger handling", () => {
|
||||
allowFrom: { whatsapp: ["+1000"] },
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["+1000"],
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["+1000"],
|
||||
},
|
||||
},
|
||||
session: { store: join(home, "sessions.json") },
|
||||
};
|
||||
@@ -1154,9 +1170,11 @@ describe("trigger handling", () => {
|
||||
allowFrom: { whatsapp: ["+1000"] },
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["+1000"],
|
||||
groups: { "*": { requireMention: false } },
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["+1000"],
|
||||
groups: { "*": { requireMention: false } },
|
||||
},
|
||||
},
|
||||
session: { store: join(home, "sessions.json") },
|
||||
};
|
||||
@@ -1201,9 +1219,11 @@ describe("trigger handling", () => {
|
||||
allowFrom: { whatsapp: ["+1000"] },
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["+1000"],
|
||||
groups: { "*": { requireMention: false } },
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["+1000"],
|
||||
groups: { "*": { requireMention: false } },
|
||||
},
|
||||
},
|
||||
session: { store: join(home, "sessions.json") },
|
||||
};
|
||||
@@ -1245,9 +1265,11 @@ describe("trigger handling", () => {
|
||||
allowFrom: { whatsapp: ["+1000"] },
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["+1000"],
|
||||
groups: { "*": { requireMention: true } },
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["+1000"],
|
||||
groups: { "*": { requireMention: true } },
|
||||
},
|
||||
},
|
||||
session: { store: join(home, "sessions.json") },
|
||||
};
|
||||
@@ -1293,8 +1315,10 @@ describe("trigger handling", () => {
|
||||
allowFrom: { whatsapp: ["+1000"] },
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["+1000"],
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["+1000"],
|
||||
},
|
||||
},
|
||||
session: { store: join(home, "sessions.json") },
|
||||
};
|
||||
@@ -1343,8 +1367,10 @@ describe("trigger handling", () => {
|
||||
allowFrom: { whatsapp: ["+1000"] },
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["+1000"],
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["+1000"],
|
||||
},
|
||||
},
|
||||
session: { store: join(home, "sessions.json") },
|
||||
};
|
||||
@@ -1648,9 +1674,11 @@ describe("trigger handling", () => {
|
||||
workspace: join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
groups: { "*": { requireMention: false } },
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
groups: { "*": { requireMention: false } },
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
groupChat: {},
|
||||
@@ -1694,8 +1722,10 @@ describe("trigger handling", () => {
|
||||
workspace: join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
},
|
||||
},
|
||||
session: {
|
||||
store: join(tmpdir(), `clawdbot-session-test-${Date.now()}.json`),
|
||||
@@ -1735,8 +1765,10 @@ describe("trigger handling", () => {
|
||||
workspace: join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
},
|
||||
},
|
||||
session: {
|
||||
store: join(tmpdir(), `clawdbot-session-test-${Date.now()}.json`),
|
||||
@@ -1769,8 +1801,10 @@ describe("trigger handling", () => {
|
||||
workspace: join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["+1999"],
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["+1999"],
|
||||
},
|
||||
},
|
||||
session: {
|
||||
store: join(tmpdir(), `clawdbot-session-test-${Date.now()}.json`),
|
||||
@@ -1798,8 +1832,10 @@ describe("trigger handling", () => {
|
||||
workspace: join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["+1999"],
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["+1999"],
|
||||
},
|
||||
},
|
||||
session: {
|
||||
store: join(tmpdir(), `clawdbot-session-test-${Date.now()}.json`),
|
||||
@@ -1841,8 +1877,10 @@ describe("trigger handling", () => {
|
||||
workspace: join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
},
|
||||
},
|
||||
session: {
|
||||
store: storePath,
|
||||
@@ -1956,8 +1994,10 @@ describe("trigger handling", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
},
|
||||
},
|
||||
session: {
|
||||
store: join(home, "sessions.json"),
|
||||
|
||||
@@ -24,6 +24,11 @@ import {
|
||||
DEFAULT_AGENT_WORKSPACE_DIR,
|
||||
ensureAgentWorkspace,
|
||||
} from "../agents/workspace.js";
|
||||
import { getChannelDock } from "../channels/dock.js";
|
||||
import {
|
||||
CHAT_CHANNEL_ORDER,
|
||||
normalizeChannelId,
|
||||
} from "../channels/registry.js";
|
||||
import {
|
||||
type AgentElevatedAllowFromConfig,
|
||||
type ClawdbotConfig,
|
||||
@@ -35,14 +40,9 @@ import {
|
||||
} from "../config/sessions.js";
|
||||
import { logVerbose } from "../globals.js";
|
||||
import { clearCommandLane, getQueueSize } from "../process/command-queue.js";
|
||||
import { getProviderDock } from "../providers/dock.js";
|
||||
import {
|
||||
CHAT_PROVIDER_ORDER,
|
||||
normalizeProviderId,
|
||||
} from "../providers/registry.js";
|
||||
import { normalizeMainKey } from "../routing/session-key.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { INTERNAL_MESSAGE_PROVIDER } from "../utils/message-provider.js";
|
||||
import { INTERNAL_MESSAGE_CHANNEL } from "../utils/message-channel.js";
|
||||
import { isReasoningTagProvider } from "../utils/provider-utils.js";
|
||||
import { resolveCommandAuthorization } from "./command-auth.js";
|
||||
import { hasControlCommand } from "./command-detection.js";
|
||||
@@ -141,8 +141,8 @@ function slugAllowToken(value?: string) {
|
||||
}
|
||||
|
||||
const SENDER_PREFIXES = [
|
||||
...CHAT_PROVIDER_ORDER,
|
||||
INTERNAL_MESSAGE_PROVIDER,
|
||||
...CHAT_CHANNEL_ORDER,
|
||||
INTERNAL_MESSAGE_CHANNEL,
|
||||
"user",
|
||||
"group",
|
||||
"channel",
|
||||
@@ -287,9 +287,9 @@ function resolveElevatedPermissions(params: {
|
||||
return { enabled, allowed: false, failures };
|
||||
}
|
||||
|
||||
const normalizedProvider = normalizeProviderId(params.provider);
|
||||
const normalizedProvider = normalizeChannelId(params.provider);
|
||||
const dockFallbackAllowFrom = normalizedProvider
|
||||
? getProviderDock(normalizedProvider)?.elevated?.allowFromFallback?.({
|
||||
? getChannelDock(normalizedProvider)?.elevated?.allowFromFallback?.({
|
||||
cfg: params.cfg,
|
||||
accountId: params.ctx.AccountId,
|
||||
})
|
||||
@@ -1017,10 +1017,8 @@ export async function getReplyFromConfig(
|
||||
}
|
||||
|
||||
const isEmptyConfig = Object.keys(cfg).length === 0;
|
||||
const skipWhenConfigEmpty = command.providerId
|
||||
? Boolean(
|
||||
getProviderDock(command.providerId)?.commands?.skipWhenConfigEmpty,
|
||||
)
|
||||
const skipWhenConfigEmpty = command.channelId
|
||||
? Boolean(getChannelDock(command.channelId)?.commands?.skipWhenConfigEmpty)
|
||||
: false;
|
||||
if (
|
||||
skipWhenConfigEmpty &&
|
||||
@@ -1255,7 +1253,7 @@ export async function getReplyFromConfig(
|
||||
: queueBodyBase;
|
||||
const resolvedQueue = resolveQueueSettings({
|
||||
cfg,
|
||||
provider: sessionCtx.Provider,
|
||||
channel: sessionCtx.Provider,
|
||||
sessionEntry,
|
||||
inlineMode: perMessageQueueMode,
|
||||
inlineOptions: perMessageQueueOptions,
|
||||
|
||||
@@ -21,6 +21,9 @@ import {
|
||||
resolveSandboxRuntimeStatus,
|
||||
} from "../../agents/sandbox.js";
|
||||
import { hasNonzeroUsage, type NormalizedUsage } from "../../agents/usage.js";
|
||||
import { getChannelDock } from "../../channels/dock.js";
|
||||
import type { ChannelThreadingToolContext } from "../../channels/plugins/types.js";
|
||||
import { normalizeChannelId } from "../../channels/registry.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import {
|
||||
loadSessionStore,
|
||||
@@ -37,9 +40,6 @@ import {
|
||||
registerAgentRunContext,
|
||||
} from "../../infra/agent-events.js";
|
||||
import { isAudioFileName } from "../../media/mime.js";
|
||||
import { getProviderDock } from "../../providers/dock.js";
|
||||
import type { ProviderThreadingToolContext } from "../../providers/plugins/types.js";
|
||||
import { normalizeProviderId } from "../../providers/registry.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { isReasoningTagProvider } from "../../utils/provider-utils.js";
|
||||
import {
|
||||
@@ -96,19 +96,19 @@ function buildThreadingToolContext(params: {
|
||||
sessionCtx: TemplateContext;
|
||||
config: ClawdbotConfig | undefined;
|
||||
hasRepliedRef: { value: boolean } | undefined;
|
||||
}): ProviderThreadingToolContext {
|
||||
}): ChannelThreadingToolContext {
|
||||
const { sessionCtx, config, hasRepliedRef } = params;
|
||||
if (!config) return {};
|
||||
const provider = normalizeProviderId(sessionCtx.Provider);
|
||||
const provider = normalizeChannelId(sessionCtx.Provider);
|
||||
if (!provider) return {};
|
||||
const dock = getProviderDock(provider);
|
||||
const dock = getChannelDock(provider);
|
||||
if (!dock?.threading?.buildToolContext) return {};
|
||||
return (
|
||||
dock.threading.buildToolContext({
|
||||
cfg: config,
|
||||
accountId: sessionCtx.AccountId,
|
||||
context: {
|
||||
Provider: sessionCtx.Provider,
|
||||
Channel: sessionCtx.Provider,
|
||||
To: sessionCtx.To,
|
||||
ReplyToId: sessionCtx.ReplyToId,
|
||||
ThreadLabel: sessionCtx.ThreadLabel,
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { getChannelDock } from "../../channels/dock.js";
|
||||
import { CHANNEL_IDS, normalizeChannelId } from "../../channels/registry.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import type { BlockStreamingCoalesceConfig } from "../../config/types.js";
|
||||
import { getProviderDock } from "../../providers/dock.js";
|
||||
import { normalizeProviderId, PROVIDER_IDS } from "../../providers/registry.js";
|
||||
import { normalizeAccountId } from "../../routing/session-key.js";
|
||||
import { INTERNAL_MESSAGE_PROVIDER } from "../../utils/message-provider.js";
|
||||
import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
|
||||
import { resolveTextChunkLimit, type TextChunkProvider } from "../chunk.js";
|
||||
|
||||
const DEFAULT_BLOCK_STREAM_MIN = 800;
|
||||
const DEFAULT_BLOCK_STREAM_MAX = 1200;
|
||||
const DEFAULT_BLOCK_STREAM_COALESCE_IDLE_MS = 1000;
|
||||
const BLOCK_CHUNK_PROVIDERS = new Set<TextChunkProvider>([
|
||||
...PROVIDER_IDS,
|
||||
INTERNAL_MESSAGE_PROVIDER,
|
||||
...CHANNEL_IDS,
|
||||
INTERNAL_MESSAGE_CHANNEL,
|
||||
]);
|
||||
|
||||
function normalizeChunkProvider(
|
||||
@@ -64,9 +64,9 @@ export function resolveBlockStreamingChunking(
|
||||
breakPreference: "paragraph" | "newline" | "sentence";
|
||||
} {
|
||||
const providerKey = normalizeChunkProvider(provider);
|
||||
const providerId = providerKey ? normalizeProviderId(providerKey) : null;
|
||||
const providerId = providerKey ? normalizeChannelId(providerKey) : null;
|
||||
const providerChunkLimit = providerId
|
||||
? getProviderDock(providerId)?.outbound?.textChunkLimit
|
||||
? getChannelDock(providerId)?.outbound?.textChunkLimit
|
||||
: undefined;
|
||||
const textLimit = resolveTextChunkLimit(cfg, providerKey, accountId, {
|
||||
fallbackLimit: providerChunkLimit,
|
||||
@@ -102,15 +102,15 @@ export function resolveBlockStreamingCoalescing(
|
||||
},
|
||||
): BlockStreamingCoalescing | undefined {
|
||||
const providerKey = normalizeChunkProvider(provider);
|
||||
const providerId = providerKey ? normalizeProviderId(providerKey) : null;
|
||||
const providerId = providerKey ? normalizeChannelId(providerKey) : null;
|
||||
const providerChunkLimit = providerId
|
||||
? getProviderDock(providerId)?.outbound?.textChunkLimit
|
||||
? getChannelDock(providerId)?.outbound?.textChunkLimit
|
||||
: undefined;
|
||||
const textLimit = resolveTextChunkLimit(cfg, providerKey, accountId, {
|
||||
fallbackLimit: providerChunkLimit,
|
||||
});
|
||||
const providerDefaults = providerId
|
||||
? getProviderDock(providerId)?.streaming?.blockStreamingCoalesceDefaults
|
||||
? getChannelDock(providerId)?.streaming?.blockStreamingCoalesceDefaults
|
||||
: undefined;
|
||||
const providerCfg = resolveProviderBlockStreamingCoalesce({
|
||||
cfg,
|
||||
|
||||
@@ -83,7 +83,7 @@ describe("handleCommands gating", () => {
|
||||
it("blocks /config when disabled", async () => {
|
||||
const cfg = {
|
||||
commands: { config: false, debug: false, text: true },
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
} as ClawdbotConfig;
|
||||
const params = buildParams("/config show", cfg);
|
||||
const result = await handleCommands(params);
|
||||
@@ -94,7 +94,7 @@ describe("handleCommands gating", () => {
|
||||
it("blocks /debug when disabled", async () => {
|
||||
const cfg = {
|
||||
commands: { config: false, debug: false, text: true },
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
} as ClawdbotConfig;
|
||||
const params = buildParams("/debug show", cfg);
|
||||
const result = await handleCommands(params);
|
||||
@@ -133,7 +133,7 @@ describe("handleCommands identity", () => {
|
||||
it("returns sender details for /whoami", async () => {
|
||||
const cfg = {
|
||||
commands: { text: true },
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
} as ClawdbotConfig;
|
||||
const params = buildParams("/whoami", cfg, {
|
||||
SenderId: "12345",
|
||||
@@ -142,7 +142,7 @@ describe("handleCommands identity", () => {
|
||||
});
|
||||
const result = await handleCommands(params);
|
||||
expect(result.shouldContinue).toBe(false);
|
||||
expect(result.reply?.text).toContain("Provider: whatsapp");
|
||||
expect(result.reply?.text).toContain("Channel: whatsapp");
|
||||
expect(result.reply?.text).toContain("User id: 12345");
|
||||
expect(result.reply?.text).toContain("Username: @TestUser");
|
||||
expect(result.reply?.text).toContain("AllowFrom: 12345");
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
isEmbeddedPiRunActive,
|
||||
waitForEmbeddedPiRunEnd,
|
||||
} from "../../agents/pi-embedded.js";
|
||||
import type { ChannelId } from "../../channels/plugins/types.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import {
|
||||
readConfigFileSnapshot,
|
||||
@@ -54,7 +55,6 @@ import {
|
||||
triggerClawdbotRestart,
|
||||
} from "../../infra/restart.js";
|
||||
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
||||
import type { ProviderId } from "../../providers/plugins/types.js";
|
||||
import { parseAgentSessionKey } from "../../routing/session-key.js";
|
||||
import { resolveSendPolicy } from "../../sessions/send-policy.js";
|
||||
import { resolveCommandAuthorization } from "../command-auth.js";
|
||||
@@ -108,8 +108,8 @@ function resolveSessionEntryForKey(
|
||||
|
||||
export type CommandContext = {
|
||||
surface: string;
|
||||
provider: string;
|
||||
providerId?: ProviderId;
|
||||
channel: string;
|
||||
channelId?: ChannelId;
|
||||
ownerList: string[];
|
||||
isAuthorizedSender: boolean;
|
||||
senderId?: string;
|
||||
@@ -189,7 +189,7 @@ export async function buildStatusReply(params: {
|
||||
}
|
||||
const queueSettings = resolveQueueSettings({
|
||||
cfg,
|
||||
provider: command.provider,
|
||||
channel: command.channel,
|
||||
sessionEntry,
|
||||
});
|
||||
const queueKey = sessionKey ?? sessionEntry?.sessionId;
|
||||
@@ -347,7 +347,7 @@ export function buildCommandContext(params: {
|
||||
commandAuthorized: params.commandAuthorized,
|
||||
});
|
||||
const surface = (ctx.Surface ?? ctx.Provider ?? "").trim().toLowerCase();
|
||||
const provider = (ctx.Provider ?? surface).trim().toLowerCase();
|
||||
const channel = (ctx.Provider ?? surface).trim().toLowerCase();
|
||||
const abortKey =
|
||||
sessionKey ?? (auth.from || undefined) ?? (auth.to || undefined);
|
||||
const rawBodyNormalized = triggerBodyNormalized;
|
||||
@@ -359,8 +359,8 @@ export function buildCommandContext(params: {
|
||||
|
||||
return {
|
||||
surface,
|
||||
provider,
|
||||
providerId: auth.providerId,
|
||||
channel,
|
||||
channelId: auth.providerId,
|
||||
ownerList: auth.ownerList,
|
||||
isAuthorizedSender: auth.isAuthorizedSender,
|
||||
senderId: auth.senderId,
|
||||
@@ -677,7 +677,7 @@ export async function handleCommands(params: {
|
||||
}
|
||||
const senderId = ctx.SenderId ?? "";
|
||||
const senderUsername = ctx.SenderUsername ?? "";
|
||||
const lines = ["🧭 Identity", `Provider: ${command.provider}`];
|
||||
const lines = ["🧭 Identity", `Channel: ${command.channel}`];
|
||||
if (senderId) lines.push(`User id: ${senderId}`);
|
||||
if (senderUsername) {
|
||||
const handle = senderUsername.startsWith("@")
|
||||
@@ -980,7 +980,7 @@ export async function handleCommands(params: {
|
||||
const result = await compactEmbeddedPiSession({
|
||||
sessionId,
|
||||
sessionKey,
|
||||
messageProvider: command.provider,
|
||||
messageChannel: command.channel,
|
||||
sessionFile: resolveSessionFilePath(sessionId, sessionEntry),
|
||||
workspaceDir,
|
||||
config: cfg,
|
||||
@@ -1056,7 +1056,7 @@ export async function handleCommands(params: {
|
||||
cfg,
|
||||
entry: sessionEntry,
|
||||
sessionKey,
|
||||
provider: sessionEntry?.provider ?? command.provider,
|
||||
channel: sessionEntry?.channel ?? command.channel,
|
||||
chatType: sessionEntry?.chatType,
|
||||
});
|
||||
if (sendPolicy === "deny") {
|
||||
|
||||
@@ -1150,7 +1150,7 @@ export async function handleDirectiveOnly(params: {
|
||||
) {
|
||||
const settings = resolveQueueSettings({
|
||||
cfg: params.cfg,
|
||||
provider,
|
||||
channel: provider,
|
||||
sessionEntry,
|
||||
});
|
||||
const debounceLabel =
|
||||
|
||||
@@ -7,12 +7,14 @@ import { resolveGroupRequireMention } from "./groups.js";
|
||||
describe("resolveGroupRequireMention", () => {
|
||||
it("respects Discord guild/channel requireMention settings", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
discord: {
|
||||
guilds: {
|
||||
"145": {
|
||||
requireMention: false,
|
||||
channels: {
|
||||
general: { allow: true },
|
||||
channels: {
|
||||
discord: {
|
||||
guilds: {
|
||||
"145": {
|
||||
requireMention: false,
|
||||
channels: {
|
||||
general: { allow: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -25,7 +27,7 @@ describe("resolveGroupRequireMention", () => {
|
||||
GroupSpace: "145",
|
||||
};
|
||||
const groupResolution: GroupKeyResolution = {
|
||||
provider: "discord",
|
||||
channel: "discord",
|
||||
id: "123",
|
||||
chatType: "group",
|
||||
};
|
||||
@@ -37,9 +39,11 @@ describe("resolveGroupRequireMention", () => {
|
||||
|
||||
it("respects Slack channel requireMention settings", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
slack: {
|
||||
channels: {
|
||||
C123: { requireMention: false },
|
||||
channels: {
|
||||
slack: {
|
||||
channels: {
|
||||
C123: { requireMention: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -49,7 +53,7 @@ describe("resolveGroupRequireMention", () => {
|
||||
GroupSubject: "#general",
|
||||
};
|
||||
const groupResolution: GroupKeyResolution = {
|
||||
provider: "slack",
|
||||
channel: "slack",
|
||||
id: "C123",
|
||||
chatType: "group",
|
||||
};
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { getChannelDock } from "../../channels/dock.js";
|
||||
import {
|
||||
getChatChannelMeta,
|
||||
normalizeChannelId,
|
||||
} from "../../channels/registry.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import type {
|
||||
GroupKeyResolution,
|
||||
SessionEntry,
|
||||
} from "../../config/sessions.js";
|
||||
import { getProviderDock } from "../../providers/dock.js";
|
||||
import {
|
||||
getChatProviderMeta,
|
||||
normalizeProviderId,
|
||||
} from "../../providers/registry.js";
|
||||
import { isInternalMessageProvider } from "../../utils/message-provider.js";
|
||||
import { isInternalMessageChannel } from "../../utils/message-channel.js";
|
||||
import { normalizeGroupActivation } from "../group-activation.js";
|
||||
import type { TemplateContext } from "../templating.js";
|
||||
|
||||
@@ -18,14 +18,14 @@ export function resolveGroupRequireMention(params: {
|
||||
groupResolution?: GroupKeyResolution;
|
||||
}): boolean {
|
||||
const { cfg, ctx, groupResolution } = params;
|
||||
const rawProvider = groupResolution?.provider ?? ctx.Provider?.trim();
|
||||
const provider = normalizeProviderId(rawProvider);
|
||||
if (!provider) return true;
|
||||
const rawChannel = groupResolution?.channel ?? ctx.Provider?.trim();
|
||||
const channel = normalizeChannelId(rawChannel);
|
||||
if (!channel) return true;
|
||||
const groupId = groupResolution?.id ?? ctx.From?.replace(/^group:/, "");
|
||||
const groupRoom = ctx.GroupRoom?.trim() ?? ctx.GroupSubject?.trim();
|
||||
const groupSpace = ctx.GroupSpace?.trim();
|
||||
const requireMention = getProviderDock(
|
||||
provider,
|
||||
const requireMention = getChannelDock(
|
||||
channel,
|
||||
)?.groups?.resolveRequireMention?.({
|
||||
cfg,
|
||||
groupId,
|
||||
@@ -57,11 +57,11 @@ export function buildGroupIntro(params: {
|
||||
const members = params.sessionCtx.GroupMembers?.trim();
|
||||
const rawProvider = params.sessionCtx.Provider?.trim();
|
||||
const providerKey = rawProvider?.toLowerCase() ?? "";
|
||||
const providerId = normalizeProviderId(rawProvider);
|
||||
const providerId = normalizeChannelId(rawProvider);
|
||||
const providerLabel = (() => {
|
||||
if (!providerKey) return "chat";
|
||||
if (isInternalMessageProvider(providerKey)) return "WebChat";
|
||||
if (providerId) return getChatProviderMeta(providerId).label;
|
||||
if (isInternalMessageChannel(providerKey)) return "WebChat";
|
||||
if (providerId) return getChatChannelMeta(providerId).label;
|
||||
return `${providerKey.at(0)?.toUpperCase() ?? ""}${providerKey.slice(1)}`;
|
||||
})();
|
||||
const subjectLine = subject
|
||||
@@ -76,7 +76,7 @@ export function buildGroupIntro(params: {
|
||||
const groupRoom = params.sessionCtx.GroupRoom?.trim() ?? subject;
|
||||
const groupSpace = params.sessionCtx.GroupSpace?.trim();
|
||||
const providerIdsLine = providerId
|
||||
? getProviderDock(providerId)?.groups?.resolveGroupIntroHint?.({
|
||||
? getChannelDock(providerId)?.groups?.resolveGroupIntroHint?.({
|
||||
cfg: params.cfg,
|
||||
groupId,
|
||||
groupRoom,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { resolveAgentConfig } from "../../agents/agent-scope.js";
|
||||
import { getChannelDock } from "../../channels/dock.js";
|
||||
import { normalizeChannelId } from "../../channels/registry.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { getProviderDock } from "../../providers/dock.js";
|
||||
import { normalizeProviderId } from "../../providers/registry.js";
|
||||
import type { MsgContext } from "../templating.js";
|
||||
|
||||
function escapeRegExp(text: string): string {
|
||||
@@ -114,9 +114,9 @@ export function stripMentions(
|
||||
agentId?: string,
|
||||
): string {
|
||||
let result = text;
|
||||
const providerId = ctx.Provider ? normalizeProviderId(ctx.Provider) : null;
|
||||
const providerId = ctx.Provider ? normalizeChannelId(ctx.Provider) : null;
|
||||
const providerMentions = providerId
|
||||
? getProviderDock(providerId)?.mentions
|
||||
? getChannelDock(providerId)?.mentions
|
||||
: undefined;
|
||||
const patterns = normalizeMentionPatterns([
|
||||
...resolveMentionPatterns(cfg, agentId),
|
||||
|
||||
@@ -577,28 +577,28 @@ export function scheduleFollowupDrain(
|
||||
}
|
||||
})();
|
||||
}
|
||||
function defaultQueueModeForProvider(_provider?: string): QueueMode {
|
||||
function defaultQueueModeForChannel(_channel?: string): QueueMode {
|
||||
return "collect";
|
||||
}
|
||||
export function resolveQueueSettings(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
provider?: string;
|
||||
channel?: string;
|
||||
sessionEntry?: SessionEntry;
|
||||
inlineMode?: QueueMode;
|
||||
inlineOptions?: Partial<QueueSettings>;
|
||||
}): QueueSettings {
|
||||
const providerKey = params.provider?.trim().toLowerCase();
|
||||
const channelKey = params.channel?.trim().toLowerCase();
|
||||
const queueCfg = params.cfg.messages?.queue;
|
||||
const providerModeRaw =
|
||||
providerKey && queueCfg?.byProvider
|
||||
? (queueCfg.byProvider as Record<string, string | undefined>)[providerKey]
|
||||
channelKey && queueCfg?.byChannel
|
||||
? (queueCfg.byChannel as Record<string, string | undefined>)[channelKey]
|
||||
: undefined;
|
||||
const resolvedMode =
|
||||
params.inlineMode ??
|
||||
normalizeQueueMode(params.sessionEntry?.queueMode) ??
|
||||
normalizeQueueMode(providerModeRaw) ??
|
||||
normalizeQueueMode(queueCfg?.mode) ??
|
||||
defaultQueueModeForProvider(providerKey);
|
||||
defaultQueueModeForChannel(channelKey);
|
||||
const debounceRaw =
|
||||
params.inlineOptions?.debounceMs ??
|
||||
params.sessionEntry?.queueDebounceMs ??
|
||||
|
||||
@@ -24,9 +24,11 @@ describe("resolveReplyToMode", () => {
|
||||
|
||||
it("uses configured value when present", () => {
|
||||
const cfg = {
|
||||
telegram: { replyToMode: "all" },
|
||||
discord: { replyToMode: "first" },
|
||||
slack: { replyToMode: "all" },
|
||||
channels: {
|
||||
telegram: { replyToMode: "all" },
|
||||
discord: { replyToMode: "first" },
|
||||
slack: { replyToMode: "all" },
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
expect(resolveReplyToMode(cfg, "telegram")).toBe("all");
|
||||
expect(resolveReplyToMode(cfg, "discord")).toBe("first");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getChannelDock } from "../../channels/dock.js";
|
||||
import { normalizeChannelId } from "../../channels/registry.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import type { ReplyToMode } from "../../config/types.js";
|
||||
import { getProviderDock } from "../../providers/dock.js";
|
||||
import { normalizeProviderId } from "../../providers/registry.js";
|
||||
import type { OriginatingChannelType } from "../templating.js";
|
||||
import type { ReplyPayload } from "../types.js";
|
||||
|
||||
@@ -10,9 +10,9 @@ export function resolveReplyToMode(
|
||||
channel?: OriginatingChannelType,
|
||||
accountId?: string | null,
|
||||
): ReplyToMode {
|
||||
const provider = normalizeProviderId(channel);
|
||||
const provider = normalizeChannelId(channel);
|
||||
if (!provider) return "all";
|
||||
const resolved = getProviderDock(provider)?.threading?.resolveReplyToMode?.({
|
||||
const resolved = getChannelDock(provider)?.threading?.resolveReplyToMode?.({
|
||||
cfg,
|
||||
accountId,
|
||||
});
|
||||
@@ -43,9 +43,9 @@ export function createReplyToModeFilterForChannel(
|
||||
mode: ReplyToMode,
|
||||
channel?: OriginatingChannelType,
|
||||
) {
|
||||
const provider = normalizeProviderId(channel);
|
||||
const provider = normalizeChannelId(channel);
|
||||
const allowTagsWhenOff = provider
|
||||
? Boolean(getProviderDock(provider)?.threading?.allowTagsWhenOff)
|
||||
? Boolean(getChannelDock(provider)?.threading?.allowTagsWhenOff)
|
||||
: false;
|
||||
return createReplyToModeFilter(mode, {
|
||||
allowTagsWhenOff,
|
||||
|
||||
@@ -226,8 +226,10 @@ describe("routeReply", () => {
|
||||
it("routes MS Teams via proactive sender", async () => {
|
||||
mocks.sendMessageMSTeams.mockClear();
|
||||
const cfg = {
|
||||
msteams: {
|
||||
enabled: true,
|
||||
channels: {
|
||||
msteams: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
} as unknown as ClawdbotConfig;
|
||||
await routeReply({
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
|
||||
import { resolveSessionAgentId } from "../../agents/agent-scope.js";
|
||||
import { resolveEffectiveMessagesConfig } from "../../agents/identity.js";
|
||||
import { normalizeChannelId } from "../../channels/registry.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { normalizeProviderId } from "../../providers/registry.js";
|
||||
import { INTERNAL_MESSAGE_PROVIDER } from "../../utils/message-provider.js";
|
||||
import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
|
||||
import type { OriginatingChannelType } from "../templating.js";
|
||||
import type { ReplyPayload } from "../types.js";
|
||||
import { normalizeReplyPayload } from "./normalize-reply.js";
|
||||
@@ -88,15 +88,15 @@ export async function routeReply(
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
if (channel === INTERNAL_MESSAGE_PROVIDER) {
|
||||
if (channel === INTERNAL_MESSAGE_CHANNEL) {
|
||||
return {
|
||||
ok: false,
|
||||
error: "Webchat routing not supported for queued replies",
|
||||
};
|
||||
}
|
||||
|
||||
const provider = normalizeProviderId(channel) ?? null;
|
||||
if (!provider) {
|
||||
const channelId = normalizeChannelId(channel) ?? null;
|
||||
if (!channelId) {
|
||||
return { ok: false, error: `Unknown channel: ${String(channel)}` };
|
||||
}
|
||||
if (abortSignal?.aborted) {
|
||||
@@ -111,7 +111,7 @@ export async function routeReply(
|
||||
);
|
||||
const results = await deliverOutboundPayloads({
|
||||
cfg,
|
||||
provider,
|
||||
channel: channelId,
|
||||
to,
|
||||
accountId: accountId ?? undefined,
|
||||
payloads: [normalized],
|
||||
@@ -138,10 +138,7 @@ export async function routeReply(
|
||||
*/
|
||||
export function isRoutableChannel(
|
||||
channel: OriginatingChannelType | undefined,
|
||||
): channel is Exclude<
|
||||
OriginatingChannelType,
|
||||
typeof INTERNAL_MESSAGE_PROVIDER
|
||||
> {
|
||||
if (!channel || channel === INTERNAL_MESSAGE_PROVIDER) return false;
|
||||
return normalizeProviderId(channel) !== null;
|
||||
): channel is Exclude<OriginatingChannelType, typeof INTERNAL_MESSAGE_CHANNEL> {
|
||||
if (!channel || channel === INTERNAL_MESSAGE_CHANNEL) return false;
|
||||
return normalizeChannelId(channel) !== null;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import crypto from "node:crypto";
|
||||
import { buildWorkspaceSkillSnapshot } from "../../agents/skills.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { type SessionEntry, saveSessionStore } from "../../config/sessions.js";
|
||||
import { buildProviderSummary } from "../../infra/provider-summary.js";
|
||||
import { drainSystemEventEntries } from "../../infra/system-events.js";
|
||||
import { buildChannelSummary } from "../../infra/channel-summary.js";
|
||||
|
||||
export async function prependSystemEvents(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
@@ -48,7 +48,7 @@ export async function prependSystemEvents(params: {
|
||||
.filter((v): v is string => Boolean(v)),
|
||||
);
|
||||
if (params.isMainSession && params.isNewSession) {
|
||||
const summary = await buildProviderSummary(params.cfg);
|
||||
const summary = await buildChannelSummary(params.cfg);
|
||||
if (summary.length > 0) systemLines.unshift(...summary);
|
||||
}
|
||||
if (systemLines.length === 0) return params.prefixedBodyBase;
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
SessionManager,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
import { resolveSessionAgentId } from "../../agents/agent-scope.js";
|
||||
import { getChannelDock } from "../../channels/dock.js";
|
||||
import { normalizeChannelId } from "../../channels/registry.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import {
|
||||
buildGroupDisplayName,
|
||||
@@ -23,8 +25,6 @@ import {
|
||||
type SessionScope,
|
||||
saveSessionStore,
|
||||
} from "../../config/sessions.js";
|
||||
import { getProviderDock } from "../../providers/dock.js";
|
||||
import { normalizeProviderId } from "../../providers/registry.js";
|
||||
import { normalizeMainKey } from "../../routing/session-key.js";
|
||||
import { resolveCommandAuthorization } from "../command-auth.js";
|
||||
import type { MsgContext, TemplateContext } from "../templating.js";
|
||||
@@ -228,20 +228,20 @@ export async function initSessionState(params: {
|
||||
queueDrop: baseEntry?.queueDrop,
|
||||
displayName: baseEntry?.displayName,
|
||||
chatType: baseEntry?.chatType,
|
||||
provider: baseEntry?.provider,
|
||||
channel: baseEntry?.channel,
|
||||
subject: baseEntry?.subject,
|
||||
room: baseEntry?.room,
|
||||
space: baseEntry?.space,
|
||||
};
|
||||
if (groupResolution?.provider) {
|
||||
const provider = groupResolution.provider;
|
||||
if (groupResolution?.channel) {
|
||||
const channel = groupResolution.channel;
|
||||
const subject = ctx.GroupSubject?.trim();
|
||||
const space = ctx.GroupSpace?.trim();
|
||||
const explicitRoom = ctx.GroupRoom?.trim();
|
||||
const normalizedProvider = normalizeProviderId(provider);
|
||||
const normalizedChannel = normalizeChannelId(channel);
|
||||
const isRoomProvider = Boolean(
|
||||
normalizedProvider &&
|
||||
getProviderDock(normalizedProvider)?.capabilities.chatTypes.includes(
|
||||
normalizedChannel &&
|
||||
getChannelDock(normalizedChannel)?.capabilities.chatTypes.includes(
|
||||
"channel",
|
||||
),
|
||||
);
|
||||
@@ -252,12 +252,12 @@ export async function initSessionState(params: {
|
||||
: undefined);
|
||||
const nextSubject = nextRoom ? undefined : subject;
|
||||
sessionEntry.chatType = groupResolution.chatType ?? "group";
|
||||
sessionEntry.provider = provider;
|
||||
sessionEntry.channel = channel;
|
||||
if (nextSubject) sessionEntry.subject = nextSubject;
|
||||
if (nextRoom) sessionEntry.room = nextRoom;
|
||||
if (space) sessionEntry.space = space;
|
||||
sessionEntry.displayName = buildGroupDisplayName({
|
||||
provider: sessionEntry.provider,
|
||||
provider: sessionEntry.channel,
|
||||
subject: sessionEntry.subject,
|
||||
room: sessionEntry.room,
|
||||
space: sessionEntry.space,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ProviderId } from "../providers/plugins/types.js";
|
||||
import type { InternalMessageProvider } from "../utils/message-provider.js";
|
||||
import type { ChannelId } from "../channels/plugins/types.js";
|
||||
import type { InternalMessageChannel } from "../utils/message-channel.js";
|
||||
|
||||
/** Valid provider channels for message routing. */
|
||||
export type OriginatingChannelType = ProviderId | InternalMessageProvider;
|
||||
export type OriginatingChannelType = ChannelId | InternalMessageChannel;
|
||||
|
||||
export type MsgContext = {
|
||||
Body?: string;
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import * as mod from "./provider-web.js";
|
||||
import * as mod from "./channel-web.js";
|
||||
|
||||
describe("provider-web barrel", () => {
|
||||
describe("channel-web barrel", () => {
|
||||
it("exports the expected web helpers", () => {
|
||||
expect(mod.createWaSocket).toBeTypeOf("function");
|
||||
expect(mod.loginWeb).toBeTypeOf("function");
|
||||
expect(mod.monitorWebProvider).toBeTypeOf("function");
|
||||
expect(mod.monitorWebChannel).toBeTypeOf("function");
|
||||
expect(mod.sendMessageWhatsApp).toBeTypeOf("function");
|
||||
expect(mod.monitorWebInbox).toBeTypeOf("function");
|
||||
expect(mod.pickProvider).toBeTypeOf("function");
|
||||
expect(mod.pickWebChannel).toBeTypeOf("function");
|
||||
expect(mod.WA_WEB_AUTH_DIR).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,14 +1,14 @@
|
||||
// Barrel exports for the web provider pieces. Splitting the original 900+ line
|
||||
// module keeps responsibilities small and testable without changing the public API.
|
||||
// Barrel exports for the web channel pieces. Splitting the original 900+ line
|
||||
// module keeps responsibilities small and testable.
|
||||
export {
|
||||
DEFAULT_WEB_MEDIA_BYTES,
|
||||
HEARTBEAT_PROMPT,
|
||||
HEARTBEAT_TOKEN,
|
||||
monitorWebProvider,
|
||||
monitorWebChannel,
|
||||
resolveHeartbeatRecipients,
|
||||
runWebHeartbeatOnce,
|
||||
type WebChannelStatus,
|
||||
type WebMonitorTuning,
|
||||
type WebProviderStatus,
|
||||
} from "./web/auto-reply.js";
|
||||
export {
|
||||
extractMediaPlaceholder,
|
||||
@@ -26,7 +26,7 @@ export {
|
||||
getStatusCode,
|
||||
logoutWeb,
|
||||
logWebSelfId,
|
||||
pickProvider,
|
||||
pickWebChannel,
|
||||
WA_WEB_AUTH_DIR,
|
||||
waitForWaConnection,
|
||||
webAuthExists,
|
||||
@@ -15,25 +15,25 @@ import {
|
||||
resolveWhatsAppGroupRequireMention,
|
||||
} from "./plugins/group-mentions.js";
|
||||
import type {
|
||||
ProviderCapabilities,
|
||||
ProviderCommandAdapter,
|
||||
ProviderElevatedAdapter,
|
||||
ProviderGroupAdapter,
|
||||
ProviderId,
|
||||
ProviderMentionAdapter,
|
||||
ProviderThreadingAdapter,
|
||||
ChannelCapabilities,
|
||||
ChannelCommandAdapter,
|
||||
ChannelElevatedAdapter,
|
||||
ChannelGroupAdapter,
|
||||
ChannelId,
|
||||
ChannelMentionAdapter,
|
||||
ChannelThreadingAdapter,
|
||||
} from "./plugins/types.js";
|
||||
import { CHAT_PROVIDER_ORDER } from "./registry.js";
|
||||
import { CHAT_CHANNEL_ORDER } from "./registry.js";
|
||||
|
||||
export type ProviderDock = {
|
||||
id: ProviderId;
|
||||
capabilities: ProviderCapabilities;
|
||||
commands?: ProviderCommandAdapter;
|
||||
export type ChannelDock = {
|
||||
id: ChannelId;
|
||||
capabilities: ChannelCapabilities;
|
||||
commands?: ChannelCommandAdapter;
|
||||
outbound?: {
|
||||
textChunkLimit?: number;
|
||||
};
|
||||
streaming?: ProviderDockStreaming;
|
||||
elevated?: ProviderElevatedAdapter;
|
||||
streaming?: ChannelDockStreaming;
|
||||
elevated?: ChannelElevatedAdapter;
|
||||
config?: {
|
||||
resolveAllowFrom?: (params: {
|
||||
cfg: ClawdbotConfig;
|
||||
@@ -45,12 +45,12 @@ export type ProviderDock = {
|
||||
allowFrom: Array<string | number>;
|
||||
}) => string[];
|
||||
};
|
||||
groups?: ProviderGroupAdapter;
|
||||
mentions?: ProviderMentionAdapter;
|
||||
threading?: ProviderThreadingAdapter;
|
||||
groups?: ChannelGroupAdapter;
|
||||
mentions?: ChannelMentionAdapter;
|
||||
threading?: ChannelThreadingAdapter;
|
||||
};
|
||||
|
||||
type ProviderDockStreaming = {
|
||||
type ChannelDockStreaming = {
|
||||
blockStreamingCoalesceDefaults?: {
|
||||
minChars?: number;
|
||||
idleMs?: number;
|
||||
@@ -66,17 +66,17 @@ const formatLower = (allowFrom: Array<string | number>) =>
|
||||
const escapeRegExp = (value: string) =>
|
||||
value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
|
||||
// Provider docks: lightweight provider metadata/behavior for shared code paths.
|
||||
// Channel docks: lightweight channel metadata/behavior for shared code paths.
|
||||
//
|
||||
// Rules:
|
||||
// - keep this module *light* (no monitors, probes, puppeteer/web login, etc)
|
||||
// - OK: config readers, allowFrom formatting, mention stripping patterns, threading defaults
|
||||
// - shared code should import from here (and from `src/providers/registry.ts`), not from the plugins registry
|
||||
// - shared code should import from here (and from `src/channels/registry.ts`), not from the plugins registry
|
||||
//
|
||||
// Adding a provider:
|
||||
// Adding a channel:
|
||||
// - add a new entry to `DOCKS`
|
||||
// - keep it cheap; push heavy logic into `src/providers/plugins/<id>.ts` or provider modules
|
||||
const DOCKS: Record<ProviderId, ProviderDock> = {
|
||||
// - keep it cheap; push heavy logic into `src/channels/plugins/<id>.ts` or channel modules
|
||||
const DOCKS: Record<ChannelId, ChannelDock> = {
|
||||
telegram: {
|
||||
id: "telegram",
|
||||
capabilities: {
|
||||
@@ -101,7 +101,8 @@ const DOCKS: Record<ProviderId, ProviderDock> = {
|
||||
resolveRequireMention: resolveTelegramGroupRequireMention,
|
||||
},
|
||||
threading: {
|
||||
resolveReplyToMode: ({ cfg }) => cfg.telegram?.replyToMode ?? "first",
|
||||
resolveReplyToMode: ({ cfg }) =>
|
||||
cfg.channels?.telegram?.replyToMode ?? "first",
|
||||
buildToolContext: ({ context, hasRepliedRef }) => ({
|
||||
currentChannelId: context.To?.trim() || undefined,
|
||||
currentThreadTs: context.ReplyToId,
|
||||
@@ -170,7 +171,7 @@ const DOCKS: Record<ProviderId, ProviderDock> = {
|
||||
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
|
||||
},
|
||||
elevated: {
|
||||
allowFromFallback: ({ cfg }) => cfg.discord?.dm?.allowFrom,
|
||||
allowFromFallback: ({ cfg }) => cfg.channels?.discord?.dm?.allowFrom,
|
||||
},
|
||||
config: {
|
||||
resolveAllowFrom: ({ cfg, accountId }) =>
|
||||
@@ -186,7 +187,8 @@ const DOCKS: Record<ProviderId, ProviderDock> = {
|
||||
stripPatterns: () => ["<@!?\\d+>"],
|
||||
},
|
||||
threading: {
|
||||
resolveReplyToMode: ({ cfg }) => cfg.discord?.replyToMode ?? "off",
|
||||
resolveReplyToMode: ({ cfg }) =>
|
||||
cfg.channels?.discord?.replyToMode ?? "off",
|
||||
buildToolContext: ({ context, hasRepliedRef }) => ({
|
||||
currentChannelId: context.To?.trim() || undefined,
|
||||
currentThreadTs: context.ReplyToId,
|
||||
@@ -308,7 +310,7 @@ const DOCKS: Record<ProviderId, ProviderDock> = {
|
||||
},
|
||||
outbound: { textChunkLimit: 4000 },
|
||||
config: {
|
||||
resolveAllowFrom: ({ cfg }) => cfg.msteams?.allowFrom ?? [],
|
||||
resolveAllowFrom: ({ cfg }) => cfg.channels?.msteams?.allowFrom ?? [],
|
||||
formatAllowFrom: ({ allowFrom }) => formatLower(allowFrom),
|
||||
},
|
||||
threading: {
|
||||
@@ -321,10 +323,10 @@ const DOCKS: Record<ProviderId, ProviderDock> = {
|
||||
},
|
||||
};
|
||||
|
||||
export function listProviderDocks(): ProviderDock[] {
|
||||
return CHAT_PROVIDER_ORDER.map((id) => DOCKS[id]);
|
||||
export function listChannelDocks(): ChannelDock[] {
|
||||
return CHAT_CHANNEL_ORDER.map((id) => DOCKS[id]);
|
||||
}
|
||||
|
||||
export function getProviderDock(id: ProviderId): ProviderDock | undefined {
|
||||
export function getChannelDock(id: ChannelId): ChannelDock | undefined {
|
||||
return DOCKS[id];
|
||||
}
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
import { handleDiscordAction } from "../../../agents/tools/discord-actions.js";
|
||||
import { listEnabledDiscordAccounts } from "../../../discord/accounts.js";
|
||||
import type {
|
||||
ProviderMessageActionAdapter,
|
||||
ProviderMessageActionName,
|
||||
ChannelMessageActionAdapter,
|
||||
ChannelMessageActionName,
|
||||
} from "../types.js";
|
||||
|
||||
const providerId = "discord";
|
||||
@@ -21,14 +21,14 @@ function readParentIdParam(
|
||||
return readStringParam(params, "parentId");
|
||||
}
|
||||
|
||||
export const discordMessageActions: ProviderMessageActionAdapter = {
|
||||
export const discordMessageActions: ChannelMessageActionAdapter = {
|
||||
listActions: ({ cfg }) => {
|
||||
const accounts = listEnabledDiscordAccounts(cfg).filter(
|
||||
(account) => account.tokenSource !== "none",
|
||||
);
|
||||
if (accounts.length === 0) return [];
|
||||
const gate = createActionGate(cfg.discord?.actions);
|
||||
const actions = new Set<ProviderMessageActionName>(["send"]);
|
||||
const gate = createActionGate(cfg.channels?.discord?.actions);
|
||||
const actions = new Set<ChannelMessageActionName>(["send"]);
|
||||
if (gate("polls")) actions.add("poll");
|
||||
if (gate("reactions")) {
|
||||
actions.add("react");
|
||||
@@ -6,19 +6,19 @@ import { handleTelegramAction } from "../../../agents/tools/telegram-actions.js"
|
||||
import type { ClawdbotConfig } from "../../../config/config.js";
|
||||
import { listEnabledTelegramAccounts } from "../../../telegram/accounts.js";
|
||||
import type {
|
||||
ProviderMessageActionAdapter,
|
||||
ProviderMessageActionName,
|
||||
ChannelMessageActionAdapter,
|
||||
ChannelMessageActionName,
|
||||
} from "../types.js";
|
||||
|
||||
const providerId = "telegram";
|
||||
|
||||
function hasTelegramInlineButtons(cfg: ClawdbotConfig): boolean {
|
||||
const caps = new Set<string>();
|
||||
for (const entry of cfg.telegram?.capabilities ?? []) {
|
||||
for (const entry of cfg.channels?.telegram?.capabilities ?? []) {
|
||||
const trimmed = String(entry).trim();
|
||||
if (trimmed) caps.add(trimmed.toLowerCase());
|
||||
}
|
||||
const accounts = cfg.telegram?.accounts;
|
||||
const accounts = cfg.channels?.telegram?.accounts;
|
||||
if (accounts && typeof accounts === "object") {
|
||||
for (const account of Object.values(accounts)) {
|
||||
const accountCaps = (account as { capabilities?: unknown })?.capabilities;
|
||||
@@ -32,14 +32,14 @@ function hasTelegramInlineButtons(cfg: ClawdbotConfig): boolean {
|
||||
return caps.has("inlinebuttons");
|
||||
}
|
||||
|
||||
export const telegramMessageActions: ProviderMessageActionAdapter = {
|
||||
export const telegramMessageActions: ChannelMessageActionAdapter = {
|
||||
listActions: ({ cfg }) => {
|
||||
const accounts = listEnabledTelegramAccounts(cfg).filter(
|
||||
(account) => account.tokenSource !== "none",
|
||||
);
|
||||
if (accounts.length === 0) return [];
|
||||
const gate = createActionGate(cfg.telegram?.actions);
|
||||
const actions = new Set<ProviderMessageActionName>(["send"]);
|
||||
const gate = createActionGate(cfg.channels?.telegram?.actions);
|
||||
const actions = new Set<ChannelMessageActionName>(["send"]);
|
||||
if (gate("reactions")) actions.add("react");
|
||||
return Array.from(actions);
|
||||
},
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import type { ProviderAgentTool } from "../types.js";
|
||||
import type { ChannelAgentTool } from "../types.js";
|
||||
|
||||
export function createWhatsAppLoginTool(): ProviderAgentTool {
|
||||
export function createWhatsAppLoginTool(): ChannelAgentTool {
|
||||
return {
|
||||
label: "WhatsApp Login",
|
||||
name: "whatsapp_login",
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js";
|
||||
|
||||
type ProviderSection = {
|
||||
type ChannelSection = {
|
||||
accounts?: Record<string, Record<string, unknown>>;
|
||||
enabled?: boolean;
|
||||
};
|
||||
@@ -14,9 +14,8 @@ export function setAccountEnabledInConfigSection(params: {
|
||||
allowTopLevel?: boolean;
|
||||
}): ClawdbotConfig {
|
||||
const accountKey = params.accountId || DEFAULT_ACCOUNT_ID;
|
||||
const base = (params.cfg as Record<string, unknown>)[params.sectionKey] as
|
||||
| ProviderSection
|
||||
| undefined;
|
||||
const channels = params.cfg.channels as Record<string, unknown> | undefined;
|
||||
const base = channels?.[params.sectionKey] as ChannelSection | undefined;
|
||||
const hasAccounts = Boolean(base?.accounts);
|
||||
if (
|
||||
params.allowTopLevel &&
|
||||
@@ -25,9 +24,12 @@ export function setAccountEnabledInConfigSection(params: {
|
||||
) {
|
||||
return {
|
||||
...params.cfg,
|
||||
[params.sectionKey]: {
|
||||
...base,
|
||||
enabled: params.enabled,
|
||||
channels: {
|
||||
...params.cfg.channels,
|
||||
[params.sectionKey]: {
|
||||
...base,
|
||||
enabled: params.enabled,
|
||||
},
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
}
|
||||
@@ -39,13 +41,16 @@ export function setAccountEnabledInConfigSection(params: {
|
||||
const existing = baseAccounts[accountKey] ?? {};
|
||||
return {
|
||||
...params.cfg,
|
||||
[params.sectionKey]: {
|
||||
...base,
|
||||
accounts: {
|
||||
...baseAccounts,
|
||||
[accountKey]: {
|
||||
...existing,
|
||||
enabled: params.enabled,
|
||||
channels: {
|
||||
...params.cfg.channels,
|
||||
[params.sectionKey]: {
|
||||
...base,
|
||||
accounts: {
|
||||
...baseAccounts,
|
||||
[accountKey]: {
|
||||
...existing,
|
||||
enabled: params.enabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -59,9 +64,8 @@ export function deleteAccountFromConfigSection(params: {
|
||||
clearBaseFields?: string[];
|
||||
}): ClawdbotConfig {
|
||||
const accountKey = params.accountId || DEFAULT_ACCOUNT_ID;
|
||||
const base = (params.cfg as Record<string, unknown>)[params.sectionKey] as
|
||||
| ProviderSection
|
||||
| undefined;
|
||||
const channels = params.cfg.channels as Record<string, unknown> | undefined;
|
||||
const base = channels?.[params.sectionKey] as ChannelSection | undefined;
|
||||
if (!base) return params.cfg;
|
||||
|
||||
const baseAccounts =
|
||||
@@ -74,9 +78,12 @@ export function deleteAccountFromConfigSection(params: {
|
||||
delete accounts[accountKey];
|
||||
return {
|
||||
...params.cfg,
|
||||
[params.sectionKey]: {
|
||||
...base,
|
||||
accounts: Object.keys(accounts).length ? accounts : undefined,
|
||||
channels: {
|
||||
...params.cfg.channels,
|
||||
[params.sectionKey]: {
|
||||
...base,
|
||||
accounts: Object.keys(accounts).length ? accounts : undefined,
|
||||
},
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
}
|
||||
@@ -89,14 +96,26 @@ export function deleteAccountFromConfigSection(params: {
|
||||
}
|
||||
return {
|
||||
...params.cfg,
|
||||
[params.sectionKey]: {
|
||||
...baseRecord,
|
||||
accounts: Object.keys(baseAccounts).length ? baseAccounts : undefined,
|
||||
channels: {
|
||||
...params.cfg.channels,
|
||||
[params.sectionKey]: {
|
||||
...baseRecord,
|
||||
accounts: Object.keys(baseAccounts).length ? baseAccounts : undefined,
|
||||
},
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
}
|
||||
|
||||
const clone = { ...params.cfg } as Record<string, unknown>;
|
||||
delete clone[params.sectionKey];
|
||||
return clone as ClawdbotConfig;
|
||||
const nextChannels = { ...(params.cfg.channels ?? {}) } as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
delete nextChannels[params.sectionKey];
|
||||
const nextCfg = { ...params.cfg } as ClawdbotConfig;
|
||||
if (Object.keys(nextChannels).length > 0) {
|
||||
nextCfg.channels = nextChannels as ClawdbotConfig["channels"];
|
||||
} else {
|
||||
delete nextCfg.channels;
|
||||
}
|
||||
return nextCfg;
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
} from "../../routing/session-key.js";
|
||||
import { getChatProviderMeta } from "../registry.js";
|
||||
import { getChatChannelMeta } from "../registry.js";
|
||||
import { discordMessageActions } from "./actions/discord.js";
|
||||
import {
|
||||
deleteAccountFromConfigSection,
|
||||
@@ -27,15 +27,15 @@ import { normalizeDiscordMessagingTarget } from "./normalize-target.js";
|
||||
import { discordOnboardingAdapter } from "./onboarding/discord.js";
|
||||
import { PAIRING_APPROVED_MESSAGE } from "./pairing-message.js";
|
||||
import {
|
||||
applyAccountNameToProviderSection,
|
||||
applyAccountNameToChannelSection,
|
||||
migrateBaseNameToDefaultAccount,
|
||||
} from "./setup-helpers.js";
|
||||
import { collectDiscordStatusIssues } from "./status-issues/discord.js";
|
||||
import type { ProviderPlugin } from "./types.js";
|
||||
import type { ChannelPlugin } from "./types.js";
|
||||
|
||||
const meta = getChatProviderMeta("discord");
|
||||
const meta = getChatChannelMeta("discord");
|
||||
|
||||
export const discordPlugin: ProviderPlugin<ResolvedDiscordAccount> = {
|
||||
export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
|
||||
id: "discord",
|
||||
meta: {
|
||||
...meta,
|
||||
@@ -59,7 +59,7 @@ export const discordPlugin: ProviderPlugin<ResolvedDiscordAccount> = {
|
||||
streaming: {
|
||||
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
|
||||
},
|
||||
reload: { configPrefixes: ["discord"] },
|
||||
reload: { configPrefixes: ["channels.discord"] },
|
||||
config: {
|
||||
listAccountIds: (cfg) => listDiscordAccountIds(cfg),
|
||||
resolveAccount: (cfg, accountId) =>
|
||||
@@ -103,11 +103,11 @@ export const discordPlugin: ProviderPlugin<ResolvedDiscordAccount> = {
|
||||
const resolvedAccountId =
|
||||
accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
|
||||
const useAccountPath = Boolean(
|
||||
cfg.discord?.accounts?.[resolvedAccountId],
|
||||
cfg.channels?.discord?.accounts?.[resolvedAccountId],
|
||||
);
|
||||
const allowFromPath = useAccountPath
|
||||
? `discord.accounts.${resolvedAccountId}.dm.`
|
||||
: "discord.dm.";
|
||||
? `channels.discord.accounts.${resolvedAccountId}.dm.`
|
||||
: "channels.discord.dm.";
|
||||
return {
|
||||
policy: account.config.dm?.policy ?? "pairing",
|
||||
allowFrom: account.config.dm?.allowFrom ?? [],
|
||||
@@ -125,11 +125,11 @@ export const discordPlugin: ProviderPlugin<ResolvedDiscordAccount> = {
|
||||
Object.keys(account.config.guilds ?? {}).length > 0;
|
||||
if (channelAllowlistConfigured) {
|
||||
return [
|
||||
`- Discord guilds: groupPolicy="open" allows any channel not explicitly denied to trigger (mention-gated). Set discord.groupPolicy="allowlist" and configure discord.guilds.<id>.channels.`,
|
||||
`- Discord guilds: groupPolicy="open" allows any channel not explicitly denied to trigger (mention-gated). Set channels.discord.groupPolicy="allowlist" and configure channels.discord.guilds.<id>.channels.`,
|
||||
];
|
||||
}
|
||||
return [
|
||||
`- Discord guilds: groupPolicy="open" with no guild/channel allowlist; any channel can trigger (mention-gated). Set discord.groupPolicy="allowlist" and configure discord.guilds.<id>.channels.`,
|
||||
`- Discord guilds: groupPolicy="open" with no guild/channel allowlist; any channel can trigger (mention-gated). Set channels.discord.groupPolicy="allowlist" and configure channels.discord.guilds.<id>.channels.`,
|
||||
];
|
||||
},
|
||||
},
|
||||
@@ -140,7 +140,8 @@ export const discordPlugin: ProviderPlugin<ResolvedDiscordAccount> = {
|
||||
stripPatterns: () => ["<@!?\\d+>"],
|
||||
},
|
||||
threading: {
|
||||
resolveReplyToMode: ({ cfg }) => cfg.discord?.replyToMode ?? "off",
|
||||
resolveReplyToMode: ({ cfg }) =>
|
||||
cfg.channels?.discord?.replyToMode ?? "off",
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: normalizeDiscordMessagingTarget,
|
||||
@@ -149,9 +150,9 @@ export const discordPlugin: ProviderPlugin<ResolvedDiscordAccount> = {
|
||||
setup: {
|
||||
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
||||
applyAccountName: ({ cfg, accountId, name }) =>
|
||||
applyAccountNameToProviderSection({
|
||||
applyAccountNameToChannelSection({
|
||||
cfg,
|
||||
providerKey: "discord",
|
||||
channelKey: "discord",
|
||||
accountId,
|
||||
name,
|
||||
}),
|
||||
@@ -165,9 +166,9 @@ export const discordPlugin: ProviderPlugin<ResolvedDiscordAccount> = {
|
||||
return null;
|
||||
},
|
||||
applyAccountConfig: ({ cfg, accountId, input }) => {
|
||||
const namedConfig = applyAccountNameToProviderSection({
|
||||
const namedConfig = applyAccountNameToChannelSection({
|
||||
cfg,
|
||||
providerKey: "discord",
|
||||
channelKey: "discord",
|
||||
accountId,
|
||||
name: input.name,
|
||||
});
|
||||
@@ -175,30 +176,40 @@ export const discordPlugin: ProviderPlugin<ResolvedDiscordAccount> = {
|
||||
accountId !== DEFAULT_ACCOUNT_ID
|
||||
? migrateBaseNameToDefaultAccount({
|
||||
cfg: namedConfig,
|
||||
providerKey: "discord",
|
||||
channelKey: "discord",
|
||||
})
|
||||
: namedConfig;
|
||||
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||
return {
|
||||
...next,
|
||||
discord: {
|
||||
...next.discord,
|
||||
enabled: true,
|
||||
...(input.useEnv ? {} : input.token ? { token: input.token } : {}),
|
||||
channels: {
|
||||
...next.channels,
|
||||
discord: {
|
||||
...next.channels?.discord,
|
||||
enabled: true,
|
||||
...(input.useEnv
|
||||
? {}
|
||||
: input.token
|
||||
? { token: input.token }
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
...next,
|
||||
discord: {
|
||||
...next.discord,
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...next.discord?.accounts,
|
||||
[accountId]: {
|
||||
...next.discord?.accounts?.[accountId],
|
||||
enabled: true,
|
||||
...(input.token ? { token: input.token } : {}),
|
||||
channels: {
|
||||
...next.channels,
|
||||
discord: {
|
||||
...next.channels?.discord,
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...next.channels?.discord?.accounts,
|
||||
[accountId]: {
|
||||
...next.channels?.discord?.accounts?.[accountId],
|
||||
enabled: true,
|
||||
...(input.token ? { token: input.token } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -229,7 +240,7 @@ export const discordPlugin: ProviderPlugin<ResolvedDiscordAccount> = {
|
||||
replyTo: replyToId ?? undefined,
|
||||
accountId: accountId ?? undefined,
|
||||
});
|
||||
return { provider: "discord", ...result };
|
||||
return { channel: "discord", ...result };
|
||||
},
|
||||
sendMedia: async ({ to, text, mediaUrl, accountId, deps, replyToId }) => {
|
||||
const send = deps?.sendDiscord ?? sendMessageDiscord;
|
||||
@@ -239,7 +250,7 @@ export const discordPlugin: ProviderPlugin<ResolvedDiscordAccount> = {
|
||||
replyTo: replyToId ?? undefined,
|
||||
accountId: accountId ?? undefined,
|
||||
});
|
||||
return { provider: "discord", ...result };
|
||||
return { channel: "discord", ...result };
|
||||
},
|
||||
sendPoll: async ({ to, poll, accountId }) =>
|
||||
await sendPollDiscord(to, poll, {
|
||||
@@ -255,7 +266,7 @@ export const discordPlugin: ProviderPlugin<ResolvedDiscordAccount> = {
|
||||
lastError: null,
|
||||
},
|
||||
collectStatusIssues: collectDiscordStatusIssues,
|
||||
buildProviderSummary: ({ snapshot }) => ({
|
||||
buildChannelSummary: ({ snapshot }) => ({
|
||||
configured: snapshot.configured ?? false,
|
||||
tokenSource: snapshot.tokenSource ?? "none",
|
||||
running: snapshot.running ?? false,
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { resolveProviderGroupRequireMention } from "../../config/group-policy.js";
|
||||
import { resolveChannelGroupRequireMention } from "../../config/group-policy.js";
|
||||
import type { DiscordConfig } from "../../config/types.js";
|
||||
import { resolveSlackAccount } from "../../slack/accounts.js";
|
||||
|
||||
type GroupMentionParams = {
|
||||
@@ -54,8 +55,8 @@ function resolveTelegramRequireMention(params: {
|
||||
}): boolean | undefined {
|
||||
const { cfg, chatId, topicId } = params;
|
||||
if (!chatId) return undefined;
|
||||
const groupConfig = cfg.telegram?.groups?.[chatId];
|
||||
const groupDefault = cfg.telegram?.groups?.["*"];
|
||||
const groupConfig = cfg.channels?.telegram?.groups?.[chatId];
|
||||
const groupDefault = cfg.channels?.telegram?.groups?.["*"];
|
||||
const topicConfig =
|
||||
topicId && groupConfig?.topics ? groupConfig.topics[topicId] : undefined;
|
||||
const defaultTopicConfig =
|
||||
@@ -76,7 +77,7 @@ function resolveTelegramRequireMention(params: {
|
||||
}
|
||||
|
||||
function resolveDiscordGuildEntry(
|
||||
guilds: NonNullable<ClawdbotConfig["discord"]>["guilds"],
|
||||
guilds: DiscordConfig["guilds"],
|
||||
groupSpace?: string | null,
|
||||
) {
|
||||
if (!guilds || Object.keys(guilds).length === 0) return null;
|
||||
@@ -103,9 +104,9 @@ export function resolveTelegramGroupRequireMention(
|
||||
topicId,
|
||||
});
|
||||
if (typeof requireMention === "boolean") return requireMention;
|
||||
return resolveProviderGroupRequireMention({
|
||||
return resolveChannelGroupRequireMention({
|
||||
cfg: params.cfg,
|
||||
provider: "telegram",
|
||||
channel: "telegram",
|
||||
groupId: chatId ?? params.groupId,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
@@ -114,9 +115,9 @@ export function resolveTelegramGroupRequireMention(
|
||||
export function resolveWhatsAppGroupRequireMention(
|
||||
params: GroupMentionParams,
|
||||
): boolean {
|
||||
return resolveProviderGroupRequireMention({
|
||||
return resolveChannelGroupRequireMention({
|
||||
cfg: params.cfg,
|
||||
provider: "whatsapp",
|
||||
channel: "whatsapp",
|
||||
groupId: params.groupId,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
@@ -125,9 +126,9 @@ export function resolveWhatsAppGroupRequireMention(
|
||||
export function resolveIMessageGroupRequireMention(
|
||||
params: GroupMentionParams,
|
||||
): boolean {
|
||||
return resolveProviderGroupRequireMention({
|
||||
return resolveChannelGroupRequireMention({
|
||||
cfg: params.cfg,
|
||||
provider: "imessage",
|
||||
channel: "imessage",
|
||||
groupId: params.groupId,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
@@ -137,7 +138,7 @@ export function resolveDiscordGroupRequireMention(
|
||||
params: GroupMentionParams,
|
||||
): boolean {
|
||||
const guildEntry = resolveDiscordGuildEntry(
|
||||
params.cfg.discord?.guilds,
|
||||
params.cfg.channels?.discord?.guilds,
|
||||
params.groupSpace,
|
||||
);
|
||||
const channelEntries = guildEntry?.channels;
|
||||
22
src/channels/plugins/helpers.ts
Normal file
22
src/channels/plugins/helpers.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js";
|
||||
import type { ChannelPlugin } from "./types.js";
|
||||
|
||||
// Channel docking helper: use this when selecting the default account for a plugin.
|
||||
export function resolveChannelDefaultAccountId<ResolvedAccount>(params: {
|
||||
plugin: ChannelPlugin<ResolvedAccount>;
|
||||
cfg: ClawdbotConfig;
|
||||
accountIds?: string[];
|
||||
}): string {
|
||||
const accountIds =
|
||||
params.accountIds ?? params.plugin.config.listAccountIds(params.cfg);
|
||||
return (
|
||||
params.plugin.config.defaultAccountId?.(params.cfg) ??
|
||||
accountIds[0] ??
|
||||
DEFAULT_ACCOUNT_ID
|
||||
);
|
||||
}
|
||||
|
||||
export function formatPairingApproveHint(channelId: string): string {
|
||||
return `Approve via: clawdbot pairing list ${channelId} / clawdbot pairing approve ${channelId} <code>`;
|
||||
}
|
||||
@@ -11,25 +11,25 @@ import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
} from "../../routing/session-key.js";
|
||||
import { getChatProviderMeta } from "../registry.js";
|
||||
import { getChatChannelMeta } from "../registry.js";
|
||||
import {
|
||||
deleteAccountFromConfigSection,
|
||||
setAccountEnabledInConfigSection,
|
||||
} from "./config-helpers.js";
|
||||
import { resolveIMessageGroupRequireMention } from "./group-mentions.js";
|
||||
import { formatPairingApproveHint } from "./helpers.js";
|
||||
import { resolveProviderMediaMaxBytes } from "./media-limits.js";
|
||||
import { resolveChannelMediaMaxBytes } from "./media-limits.js";
|
||||
import { imessageOnboardingAdapter } from "./onboarding/imessage.js";
|
||||
import { PAIRING_APPROVED_MESSAGE } from "./pairing-message.js";
|
||||
import {
|
||||
applyAccountNameToProviderSection,
|
||||
applyAccountNameToChannelSection,
|
||||
migrateBaseNameToDefaultAccount,
|
||||
} from "./setup-helpers.js";
|
||||
import type { ProviderPlugin } from "./types.js";
|
||||
import type { ChannelPlugin } from "./types.js";
|
||||
|
||||
const meta = getChatProviderMeta("imessage");
|
||||
const meta = getChatChannelMeta("imessage");
|
||||
|
||||
export const imessagePlugin: ProviderPlugin<ResolvedIMessageAccount> = {
|
||||
export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount> = {
|
||||
id: "imessage",
|
||||
meta: {
|
||||
...meta,
|
||||
@@ -46,7 +46,7 @@ export const imessagePlugin: ProviderPlugin<ResolvedIMessageAccount> = {
|
||||
chatTypes: ["direct", "group"],
|
||||
media: true,
|
||||
},
|
||||
reload: { configPrefixes: ["imessage"] },
|
||||
reload: { configPrefixes: ["channels.imessage"] },
|
||||
config: {
|
||||
listAccountIds: (cfg) => listIMessageAccountIds(cfg),
|
||||
resolveAccount: (cfg, accountId) =>
|
||||
@@ -86,11 +86,11 @@ export const imessagePlugin: ProviderPlugin<ResolvedIMessageAccount> = {
|
||||
const resolvedAccountId =
|
||||
accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
|
||||
const useAccountPath = Boolean(
|
||||
cfg.imessage?.accounts?.[resolvedAccountId],
|
||||
cfg.channels?.imessage?.accounts?.[resolvedAccountId],
|
||||
);
|
||||
const basePath = useAccountPath
|
||||
? `imessage.accounts.${resolvedAccountId}.`
|
||||
: "imessage.";
|
||||
? `channels.imessage.accounts.${resolvedAccountId}.`
|
||||
: "channels.imessage.";
|
||||
return {
|
||||
policy: account.config.dmPolicy ?? "pairing",
|
||||
allowFrom: account.config.allowFrom ?? [],
|
||||
@@ -103,7 +103,7 @@ export const imessagePlugin: ProviderPlugin<ResolvedIMessageAccount> = {
|
||||
const groupPolicy = account.config.groupPolicy ?? "allowlist";
|
||||
if (groupPolicy !== "open") return [];
|
||||
return [
|
||||
`- iMessage groups: groupPolicy="open" allows any member to trigger the bot. Set imessage.groupPolicy="allowlist" + imessage.groupAllowFrom to restrict senders.`,
|
||||
`- iMessage groups: groupPolicy="open" allows any member to trigger the bot. Set channels.imessage.groupPolicy="allowlist" + channels.imessage.groupAllowFrom to restrict senders.`,
|
||||
];
|
||||
},
|
||||
},
|
||||
@@ -113,16 +113,16 @@ export const imessagePlugin: ProviderPlugin<ResolvedIMessageAccount> = {
|
||||
setup: {
|
||||
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
||||
applyAccountName: ({ cfg, accountId, name }) =>
|
||||
applyAccountNameToProviderSection({
|
||||
applyAccountNameToChannelSection({
|
||||
cfg,
|
||||
providerKey: "imessage",
|
||||
channelKey: "imessage",
|
||||
accountId,
|
||||
name,
|
||||
}),
|
||||
applyAccountConfig: ({ cfg, accountId, input }) => {
|
||||
const namedConfig = applyAccountNameToProviderSection({
|
||||
const namedConfig = applyAccountNameToChannelSection({
|
||||
cfg,
|
||||
providerKey: "imessage",
|
||||
channelKey: "imessage",
|
||||
accountId,
|
||||
name: input.name,
|
||||
});
|
||||
@@ -130,31 +130,16 @@ export const imessagePlugin: ProviderPlugin<ResolvedIMessageAccount> = {
|
||||
accountId !== DEFAULT_ACCOUNT_ID
|
||||
? migrateBaseNameToDefaultAccount({
|
||||
cfg: namedConfig,
|
||||
providerKey: "imessage",
|
||||
channelKey: "imessage",
|
||||
})
|
||||
: namedConfig;
|
||||
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||
return {
|
||||
...next,
|
||||
imessage: {
|
||||
...next.imessage,
|
||||
enabled: true,
|
||||
...(input.cliPath ? { cliPath: input.cliPath } : {}),
|
||||
...(input.dbPath ? { dbPath: input.dbPath } : {}),
|
||||
...(input.service ? { service: input.service } : {}),
|
||||
...(input.region ? { region: input.region } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
...next,
|
||||
imessage: {
|
||||
...next.imessage,
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...next.imessage?.accounts,
|
||||
[accountId]: {
|
||||
...next.imessage?.accounts?.[accountId],
|
||||
channels: {
|
||||
...next.channels,
|
||||
imessage: {
|
||||
...next.channels?.imessage,
|
||||
enabled: true,
|
||||
...(input.cliPath ? { cliPath: input.cliPath } : {}),
|
||||
...(input.dbPath ? { dbPath: input.dbPath } : {}),
|
||||
@@ -162,6 +147,27 @@ export const imessagePlugin: ProviderPlugin<ResolvedIMessageAccount> = {
|
||||
...(input.region ? { region: input.region } : {}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
...next,
|
||||
channels: {
|
||||
...next.channels,
|
||||
imessage: {
|
||||
...next.channels?.imessage,
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...next.channels?.imessage?.accounts,
|
||||
[accountId]: {
|
||||
...next.channels?.imessage?.accounts?.[accountId],
|
||||
enabled: true,
|
||||
...(input.cliPath ? { cliPath: input.cliPath } : {}),
|
||||
...(input.dbPath ? { dbPath: input.dbPath } : {}),
|
||||
...(input.service ? { service: input.service } : {}),
|
||||
...(input.region ? { region: input.region } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
@@ -184,26 +190,26 @@ export const imessagePlugin: ProviderPlugin<ResolvedIMessageAccount> = {
|
||||
},
|
||||
sendText: async ({ cfg, to, text, accountId, deps }) => {
|
||||
const send = deps?.sendIMessage ?? sendMessageIMessage;
|
||||
const maxBytes = resolveProviderMediaMaxBytes({
|
||||
const maxBytes = resolveChannelMediaMaxBytes({
|
||||
cfg,
|
||||
resolveProviderLimitMb: ({ cfg, accountId }) =>
|
||||
cfg.imessage?.accounts?.[accountId]?.mediaMaxMb ??
|
||||
cfg.imessage?.mediaMaxMb,
|
||||
resolveChannelLimitMb: ({ cfg, accountId }) =>
|
||||
cfg.channels?.imessage?.accounts?.[accountId]?.mediaMaxMb ??
|
||||
cfg.channels?.imessage?.mediaMaxMb,
|
||||
accountId,
|
||||
});
|
||||
const result = await send(to, text, {
|
||||
maxBytes,
|
||||
accountId: accountId ?? undefined,
|
||||
});
|
||||
return { provider: "imessage", ...result };
|
||||
return { channel: "imessage", ...result };
|
||||
},
|
||||
sendMedia: async ({ cfg, to, text, mediaUrl, accountId, deps }) => {
|
||||
const send = deps?.sendIMessage ?? sendMessageIMessage;
|
||||
const maxBytes = resolveProviderMediaMaxBytes({
|
||||
const maxBytes = resolveChannelMediaMaxBytes({
|
||||
cfg,
|
||||
resolveProviderLimitMb: ({ cfg, accountId }) =>
|
||||
cfg.imessage?.accounts?.[accountId]?.mediaMaxMb ??
|
||||
cfg.imessage?.mediaMaxMb,
|
||||
resolveChannelLimitMb: ({ cfg, accountId }) =>
|
||||
cfg.channels?.imessage?.accounts?.[accountId]?.mediaMaxMb ??
|
||||
cfg.channels?.imessage?.mediaMaxMb,
|
||||
accountId,
|
||||
});
|
||||
const result = await send(to, text, {
|
||||
@@ -211,7 +217,7 @@ export const imessagePlugin: ProviderPlugin<ResolvedIMessageAccount> = {
|
||||
maxBytes,
|
||||
accountId: accountId ?? undefined,
|
||||
});
|
||||
return { provider: "imessage", ...result };
|
||||
return { channel: "imessage", ...result };
|
||||
},
|
||||
},
|
||||
status: {
|
||||
@@ -231,14 +237,14 @@ export const imessagePlugin: ProviderPlugin<ResolvedIMessageAccount> = {
|
||||
if (!lastError) return [];
|
||||
return [
|
||||
{
|
||||
provider: "imessage",
|
||||
channel: "imessage",
|
||||
accountId: account.accountId,
|
||||
kind: "runtime",
|
||||
message: `Provider error: ${lastError}`,
|
||||
message: `Channel error: ${lastError}`,
|
||||
},
|
||||
];
|
||||
}),
|
||||
buildProviderSummary: ({ snapshot }) => ({
|
||||
buildChannelSummary: ({ snapshot }) => ({
|
||||
configured: snapshot.configured ?? false,
|
||||
running: snapshot.running ?? false,
|
||||
lastStartAt: snapshot.lastStartAt ?? null,
|
||||
14
src/channels/plugins/index.test.ts
Normal file
14
src/channels/plugins/index.test.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { CHANNEL_IDS } from "../registry.js";
|
||||
import { listChannelPlugins } from "./index.js";
|
||||
|
||||
describe("channel plugin registry", () => {
|
||||
it("stays in sync with channel ids", () => {
|
||||
const pluginIds = listChannelPlugins()
|
||||
.map((plugin) => plugin.id)
|
||||
.slice()
|
||||
.sort();
|
||||
const channelIds = [...CHANNEL_IDS].slice().sort();
|
||||
expect(pluginIds).toEqual(channelIds);
|
||||
});
|
||||
});
|
||||
67
src/channels/plugins/index.ts
Normal file
67
src/channels/plugins/index.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
CHAT_CHANNEL_ORDER,
|
||||
type ChatChannelId,
|
||||
normalizeChatChannelId,
|
||||
} from "../registry.js";
|
||||
import { discordPlugin } from "./discord.js";
|
||||
import { imessagePlugin } from "./imessage.js";
|
||||
import { msteamsPlugin } from "./msteams.js";
|
||||
import { signalPlugin } from "./signal.js";
|
||||
import { slackPlugin } from "./slack.js";
|
||||
import { telegramPlugin } from "./telegram.js";
|
||||
import type { ChannelId, ChannelPlugin } from "./types.js";
|
||||
import { whatsappPlugin } from "./whatsapp.js";
|
||||
|
||||
// Channel plugins registry (runtime).
|
||||
//
|
||||
// This module is intentionally "heavy" (plugins may import channel monitors, web login, etc).
|
||||
// Shared code paths (reply flow, command auth, sandbox explain) should depend on `src/channels/dock.ts`
|
||||
// instead, and only call `getChannelPlugin()` at execution boundaries.
|
||||
//
|
||||
// Adding a channel:
|
||||
// - add `<id>Plugin` import + entry in `resolveChannels()`
|
||||
// - add an entry to `src/channels/dock.ts` for shared behavior (capabilities, allowFrom, threading, …)
|
||||
// - add ids/aliases in `src/channels/registry.ts`
|
||||
function resolveChannels(): ChannelPlugin[] {
|
||||
return [
|
||||
telegramPlugin,
|
||||
whatsappPlugin,
|
||||
discordPlugin,
|
||||
slackPlugin,
|
||||
signalPlugin,
|
||||
imessagePlugin,
|
||||
msteamsPlugin,
|
||||
];
|
||||
}
|
||||
|
||||
export function listChannelPlugins(): ChannelPlugin[] {
|
||||
return resolveChannels().sort((a, b) => {
|
||||
const indexA = CHAT_CHANNEL_ORDER.indexOf(a.id as ChatChannelId);
|
||||
const indexB = CHAT_CHANNEL_ORDER.indexOf(b.id as ChatChannelId);
|
||||
const orderA = a.meta.order ?? (indexA === -1 ? 999 : indexA);
|
||||
const orderB = b.meta.order ?? (indexB === -1 ? 999 : indexB);
|
||||
if (orderA !== orderB) return orderA - orderB;
|
||||
return a.id.localeCompare(b.id);
|
||||
});
|
||||
}
|
||||
|
||||
export function getChannelPlugin(id: ChannelId): ChannelPlugin | undefined {
|
||||
return resolveChannels().find((plugin) => plugin.id === id);
|
||||
}
|
||||
|
||||
export function normalizeChannelId(raw?: string | null): ChannelId | null {
|
||||
// Channel docking: keep input normalization centralized in src/channels/registry.ts
|
||||
// so CLI/API/protocol can rely on stable aliases without plugin init side effects.
|
||||
return normalizeChatChannelId(raw);
|
||||
}
|
||||
|
||||
export {
|
||||
discordPlugin,
|
||||
imessagePlugin,
|
||||
msteamsPlugin,
|
||||
signalPlugin,
|
||||
slackPlugin,
|
||||
telegramPlugin,
|
||||
whatsappPlugin,
|
||||
};
|
||||
export type { ChannelId, ChannelPlugin } from "./types.js";
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { ProviderId, ProviderPlugin } from "./types.js";
|
||||
import type { ChannelId, ChannelPlugin } from "./types.js";
|
||||
|
||||
type PluginLoader = () => Promise<ProviderPlugin>;
|
||||
type PluginLoader = () => Promise<ChannelPlugin>;
|
||||
|
||||
// Provider docking: load *one* plugin on-demand.
|
||||
// Channel docking: load *one* plugin on-demand.
|
||||
//
|
||||
// This avoids importing `src/providers/plugins/index.ts` (intentionally heavy)
|
||||
// This avoids importing `src/channels/plugins/index.ts` (intentionally heavy)
|
||||
// from shared flows like outbound delivery / followup routing.
|
||||
const LOADERS: Record<ProviderId, PluginLoader> = {
|
||||
const LOADERS: Record<ChannelId, PluginLoader> = {
|
||||
telegram: async () => (await import("./telegram.js")).telegramPlugin,
|
||||
whatsapp: async () => (await import("./whatsapp.js")).whatsappPlugin,
|
||||
discord: async () => (await import("./discord.js")).discordPlugin,
|
||||
@@ -16,11 +16,11 @@ const LOADERS: Record<ProviderId, PluginLoader> = {
|
||||
msteams: async () => (await import("./msteams.js")).msteamsPlugin,
|
||||
};
|
||||
|
||||
const cache = new Map<ProviderId, ProviderPlugin>();
|
||||
const cache = new Map<ChannelId, ChannelPlugin>();
|
||||
|
||||
export async function loadProviderPlugin(
|
||||
id: ProviderId,
|
||||
): Promise<ProviderPlugin | undefined> {
|
||||
export async function loadChannelPlugin(
|
||||
id: ChannelId,
|
||||
): Promise<ChannelPlugin | undefined> {
|
||||
const cached = cache.get(id);
|
||||
if (cached) return cached;
|
||||
const loader = LOADERS[id];
|
||||
@@ -3,22 +3,22 @@ import { normalizeAccountId } from "../../routing/session-key.js";
|
||||
|
||||
const MB = 1024 * 1024;
|
||||
|
||||
export function resolveProviderMediaMaxBytes(params: {
|
||||
export function resolveChannelMediaMaxBytes(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
// Provider-specific config lives under different keys; keep this helper generic
|
||||
// so shared plugin helpers don't need provider-id branching.
|
||||
resolveProviderLimitMb: (params: {
|
||||
// Channel-specific config lives under different keys; keep this helper generic
|
||||
// so shared plugin helpers don't need channel-id branching.
|
||||
resolveChannelLimitMb: (params: {
|
||||
cfg: ClawdbotConfig;
|
||||
accountId: string;
|
||||
}) => number | undefined;
|
||||
accountId?: string | null;
|
||||
}): number | undefined {
|
||||
const accountId = normalizeAccountId(params.accountId);
|
||||
const providerLimit = params.resolveProviderLimitMb({
|
||||
const channelLimit = params.resolveChannelLimitMb({
|
||||
cfg: params.cfg,
|
||||
accountId,
|
||||
});
|
||||
if (providerLimit) return providerLimit * MB;
|
||||
if (channelLimit) return channelLimit * MB;
|
||||
if (params.cfg.agents?.defaults?.mediaMaxMb) {
|
||||
return params.cfg.agents.defaults.mediaMaxMb * MB;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export const PROVIDER_MESSAGE_ACTION_NAMES = [
|
||||
export const CHANNEL_MESSAGE_ACTION_NAMES = [
|
||||
"send",
|
||||
"poll",
|
||||
"react",
|
||||
@@ -39,5 +39,5 @@ export const PROVIDER_MESSAGE_ACTION_NAMES = [
|
||||
"ban",
|
||||
] as const;
|
||||
|
||||
export type ProviderMessageActionName =
|
||||
(typeof PROVIDER_MESSAGE_ACTION_NAMES)[number];
|
||||
export type ChannelMessageActionName =
|
||||
(typeof CHANNEL_MESSAGE_ACTION_NAMES)[number];
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { getProviderPlugin, listProviderPlugins } from "./index.js";
|
||||
import { getChannelPlugin, listChannelPlugins } from "./index.js";
|
||||
import type {
|
||||
ProviderMessageActionContext,
|
||||
ProviderMessageActionName,
|
||||
ChannelMessageActionContext,
|
||||
ChannelMessageActionName,
|
||||
} from "./types.js";
|
||||
|
||||
export function listProviderMessageActions(
|
||||
export function listChannelMessageActions(
|
||||
cfg: ClawdbotConfig,
|
||||
): ProviderMessageActionName[] {
|
||||
const actions = new Set<ProviderMessageActionName>(["send"]);
|
||||
for (const plugin of listProviderPlugins()) {
|
||||
): ChannelMessageActionName[] {
|
||||
const actions = new Set<ChannelMessageActionName>(["send"]);
|
||||
for (const plugin of listChannelPlugins()) {
|
||||
const list = plugin.actions?.listActions?.({ cfg });
|
||||
if (!list) continue;
|
||||
for (const action of list) actions.add(action);
|
||||
@@ -19,17 +19,17 @@ export function listProviderMessageActions(
|
||||
return Array.from(actions);
|
||||
}
|
||||
|
||||
export function supportsProviderMessageButtons(cfg: ClawdbotConfig): boolean {
|
||||
for (const plugin of listProviderPlugins()) {
|
||||
export function supportsChannelMessageButtons(cfg: ClawdbotConfig): boolean {
|
||||
for (const plugin of listChannelPlugins()) {
|
||||
if (plugin.actions?.supportsButtons?.({ cfg })) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function dispatchProviderMessageAction(
|
||||
ctx: ProviderMessageActionContext,
|
||||
export async function dispatchChannelMessageAction(
|
||||
ctx: ChannelMessageActionContext,
|
||||
): Promise<AgentToolResult<unknown> | null> {
|
||||
const plugin = getProviderPlugin(ctx.provider);
|
||||
const plugin = getChannelPlugin(ctx.channel);
|
||||
if (!plugin?.actions?.handleAction) return null;
|
||||
if (
|
||||
plugin.actions.supportsAction &&
|
||||
@@ -1,11 +1,12 @@
|
||||
import { chunkMarkdownText } from "../../auto-reply/chunk.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { createMSTeamsPollStoreFs } from "../../msteams/polls.js";
|
||||
import { sendMessageMSTeams, sendPollMSTeams } from "../../msteams/send.js";
|
||||
import { resolveMSTeamsCredentials } from "../../msteams/token.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js";
|
||||
import { msteamsOnboardingAdapter } from "./onboarding/msteams.js";
|
||||
import { PAIRING_APPROVED_MESSAGE } from "./pairing-message.js";
|
||||
import type { ProviderMessageActionName, ProviderPlugin } from "./types.js";
|
||||
import type { ChannelMessageActionName, ChannelPlugin } from "./types.js";
|
||||
|
||||
type ResolvedMSTeamsAccount = {
|
||||
accountId: string;
|
||||
@@ -22,7 +23,7 @@ const meta = {
|
||||
blurb: "bot via Microsoft Teams.",
|
||||
} as const;
|
||||
|
||||
export const msteamsPlugin: ProviderPlugin<ResolvedMSTeamsAccount> = {
|
||||
export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
|
||||
id: "msteams",
|
||||
meta: {
|
||||
...meta,
|
||||
@@ -45,35 +46,44 @@ export const msteamsPlugin: ProviderPlugin<ResolvedMSTeamsAccount> = {
|
||||
threads: true,
|
||||
media: true,
|
||||
},
|
||||
reload: { configPrefixes: ["msteams"] },
|
||||
reload: { configPrefixes: ["channels.msteams"] },
|
||||
config: {
|
||||
listAccountIds: () => [DEFAULT_ACCOUNT_ID],
|
||||
resolveAccount: (cfg) => ({
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
enabled: cfg.msteams?.enabled !== false,
|
||||
configured: Boolean(resolveMSTeamsCredentials(cfg.msteams)),
|
||||
enabled: cfg.channels?.msteams?.enabled !== false,
|
||||
configured: Boolean(resolveMSTeamsCredentials(cfg.channels?.msteams)),
|
||||
}),
|
||||
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
||||
setAccountEnabled: ({ cfg, enabled }) => ({
|
||||
...cfg,
|
||||
msteams: {
|
||||
...cfg.msteams,
|
||||
enabled,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
msteams: {
|
||||
...cfg.channels?.msteams,
|
||||
enabled,
|
||||
},
|
||||
},
|
||||
}),
|
||||
deleteAccount: ({ cfg }) => {
|
||||
const next = { ...cfg } as Record<string, unknown>;
|
||||
delete next.msteams;
|
||||
return next as typeof cfg;
|
||||
const next = { ...cfg } as ClawdbotConfig;
|
||||
const nextChannels = { ...(cfg.channels ?? {}) };
|
||||
delete nextChannels.msteams;
|
||||
if (Object.keys(nextChannels).length > 0) {
|
||||
next.channels = nextChannels;
|
||||
} else {
|
||||
delete next.channels;
|
||||
}
|
||||
return next;
|
||||
},
|
||||
isConfigured: (_account, cfg) =>
|
||||
Boolean(resolveMSTeamsCredentials(cfg.msteams)),
|
||||
Boolean(resolveMSTeamsCredentials(cfg.channels?.msteams)),
|
||||
describeAccount: (account) => ({
|
||||
accountId: account.accountId,
|
||||
enabled: account.enabled,
|
||||
configured: account.configured,
|
||||
}),
|
||||
resolveAllowFrom: ({ cfg }) => cfg.msteams?.allowFrom ?? [],
|
||||
resolveAllowFrom: ({ cfg }) => cfg.channels?.msteams?.allowFrom ?? [],
|
||||
formatAllowFrom: ({ allowFrom }) =>
|
||||
allowFrom
|
||||
.map((entry) => String(entry).trim())
|
||||
@@ -82,10 +92,10 @@ export const msteamsPlugin: ProviderPlugin<ResolvedMSTeamsAccount> = {
|
||||
},
|
||||
security: {
|
||||
collectWarnings: ({ cfg }) => {
|
||||
const groupPolicy = cfg.msteams?.groupPolicy ?? "allowlist";
|
||||
const groupPolicy = cfg.channels?.msteams?.groupPolicy ?? "allowlist";
|
||||
if (groupPolicy !== "open") return [];
|
||||
return [
|
||||
`- MS Teams groups: groupPolicy="open" allows any member to trigger (mention-gated). Set msteams.groupPolicy="allowlist" + msteams.groupAllowFrom to restrict senders.`,
|
||||
`- MS Teams groups: groupPolicy="open" allows any member to trigger (mention-gated). Set channels.msteams.groupPolicy="allowlist" + channels.msteams.groupAllowFrom to restrict senders.`,
|
||||
];
|
||||
},
|
||||
},
|
||||
@@ -93,19 +103,22 @@ export const msteamsPlugin: ProviderPlugin<ResolvedMSTeamsAccount> = {
|
||||
resolveAccountId: () => DEFAULT_ACCOUNT_ID,
|
||||
applyAccountConfig: ({ cfg }) => ({
|
||||
...cfg,
|
||||
msteams: {
|
||||
...cfg.msteams,
|
||||
enabled: true,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
msteams: {
|
||||
...cfg.channels?.msteams,
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
actions: {
|
||||
listActions: ({ cfg }) => {
|
||||
const enabled =
|
||||
cfg.msteams?.enabled !== false &&
|
||||
Boolean(resolveMSTeamsCredentials(cfg.msteams));
|
||||
cfg.channels?.msteams?.enabled !== false &&
|
||||
Boolean(resolveMSTeamsCredentials(cfg.channels?.msteams));
|
||||
if (!enabled) return [];
|
||||
return ["poll"] satisfies ProviderMessageActionName[];
|
||||
return ["poll"] satisfies ChannelMessageActionName[];
|
||||
},
|
||||
},
|
||||
outbound: {
|
||||
@@ -130,7 +143,7 @@ export const msteamsPlugin: ProviderPlugin<ResolvedMSTeamsAccount> = {
|
||||
deps?.sendMSTeams ??
|
||||
((to, text) => sendMessageMSTeams({ cfg, to, text }));
|
||||
const result = await send(to, text);
|
||||
return { provider: "msteams", ...result };
|
||||
return { channel: "msteams", ...result };
|
||||
},
|
||||
sendMedia: async ({ cfg, to, text, mediaUrl, deps }) => {
|
||||
const send =
|
||||
@@ -138,7 +151,7 @@ export const msteamsPlugin: ProviderPlugin<ResolvedMSTeamsAccount> = {
|
||||
((to, text, opts) =>
|
||||
sendMessageMSTeams({ cfg, to, text, mediaUrl: opts?.mediaUrl }));
|
||||
const result = await send(to, text, { mediaUrl });
|
||||
return { provider: "msteams", ...result };
|
||||
return { channel: "msteams", ...result };
|
||||
},
|
||||
sendPoll: async ({ cfg, to, poll }) => {
|
||||
const maxSelections = poll.maxSelections ?? 1;
|
||||
@@ -172,7 +185,7 @@ export const msteamsPlugin: ProviderPlugin<ResolvedMSTeamsAccount> = {
|
||||
lastError: null,
|
||||
port: null,
|
||||
},
|
||||
buildProviderSummary: ({ snapshot }) => ({
|
||||
buildChannelSummary: ({ snapshot }) => ({
|
||||
configured: snapshot.configured ?? false,
|
||||
running: snapshot.running ?? false,
|
||||
lastStartAt: snapshot.lastStartAt ?? null,
|
||||
@@ -196,7 +209,7 @@ export const msteamsPlugin: ProviderPlugin<ResolvedMSTeamsAccount> = {
|
||||
gateway: {
|
||||
startAccount: async (ctx) => {
|
||||
const { monitorMSTeamsProvider } = await import("../../msteams/index.js");
|
||||
const port = ctx.cfg.msteams?.webhook?.port ?? 3978;
|
||||
const port = ctx.cfg.channels?.msteams?.webhook?.port ?? 3978;
|
||||
ctx.setStatus({ accountId: ctx.accountId, port });
|
||||
ctx.log?.info(`starting provider (port ${port})`);
|
||||
return monitorMSTeamsProvider({
|
||||
@@ -2,23 +2,23 @@ import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import type { DmPolicy } from "../../config/types.js";
|
||||
import type { RuntimeEnv } from "../../runtime.js";
|
||||
import type { WizardPrompter } from "../../wizard/prompts.js";
|
||||
import type { ChatProviderId } from "../registry.js";
|
||||
import type { ChatChannelId } from "../registry.js";
|
||||
|
||||
export type SetupProvidersOptions = {
|
||||
export type SetupChannelsOptions = {
|
||||
allowDisable?: boolean;
|
||||
allowSignalInstall?: boolean;
|
||||
onSelection?: (selection: ChatProviderId[]) => void;
|
||||
accountIds?: Partial<Record<ChatProviderId, string>>;
|
||||
onAccountId?: (provider: ChatProviderId, accountId: string) => void;
|
||||
onSelection?: (selection: ChatChannelId[]) => void;
|
||||
accountIds?: Partial<Record<ChatChannelId, string>>;
|
||||
onAccountId?: (channel: ChatChannelId, accountId: string) => void;
|
||||
promptAccountIds?: boolean;
|
||||
whatsappAccountId?: string;
|
||||
promptWhatsAppAccountId?: boolean;
|
||||
onWhatsAppAccountId?: (accountId: string) => void;
|
||||
forceAllowFromProviders?: ChatProviderId[];
|
||||
forceAllowFromChannels?: ChatChannelId[];
|
||||
skipDmPolicyPrompt?: boolean;
|
||||
skipConfirm?: boolean;
|
||||
quickstartDefaults?: boolean;
|
||||
initialSelection?: ChatProviderId[];
|
||||
initialSelection?: ChatChannelId[];
|
||||
};
|
||||
|
||||
export type PromptAccountIdParams = {
|
||||
@@ -34,56 +34,56 @@ export type PromptAccountId = (
|
||||
params: PromptAccountIdParams,
|
||||
) => Promise<string>;
|
||||
|
||||
export type ProviderOnboardingStatus = {
|
||||
provider: ChatProviderId;
|
||||
export type ChannelOnboardingStatus = {
|
||||
channel: ChatChannelId;
|
||||
configured: boolean;
|
||||
statusLines: string[];
|
||||
selectionHint?: string;
|
||||
quickstartScore?: number;
|
||||
};
|
||||
|
||||
export type ProviderOnboardingStatusContext = {
|
||||
export type ChannelOnboardingStatusContext = {
|
||||
cfg: ClawdbotConfig;
|
||||
options?: SetupProvidersOptions;
|
||||
accountOverrides: Partial<Record<ChatProviderId, string>>;
|
||||
options?: SetupChannelsOptions;
|
||||
accountOverrides: Partial<Record<ChatChannelId, string>>;
|
||||
};
|
||||
|
||||
export type ProviderOnboardingConfigureContext = {
|
||||
export type ChannelOnboardingConfigureContext = {
|
||||
cfg: ClawdbotConfig;
|
||||
runtime: RuntimeEnv;
|
||||
prompter: WizardPrompter;
|
||||
options?: SetupProvidersOptions;
|
||||
accountOverrides: Partial<Record<ChatProviderId, string>>;
|
||||
options?: SetupChannelsOptions;
|
||||
accountOverrides: Partial<Record<ChatChannelId, string>>;
|
||||
shouldPromptAccountIds: boolean;
|
||||
forceAllowFrom: boolean;
|
||||
};
|
||||
|
||||
export type ProviderOnboardingResult = {
|
||||
export type ChannelOnboardingResult = {
|
||||
cfg: ClawdbotConfig;
|
||||
accountId?: string;
|
||||
};
|
||||
|
||||
export type ProviderOnboardingDmPolicy = {
|
||||
export type ChannelOnboardingDmPolicy = {
|
||||
label: string;
|
||||
provider: ChatProviderId;
|
||||
channel: ChatChannelId;
|
||||
policyKey: string;
|
||||
allowFromKey: string;
|
||||
getCurrent: (cfg: ClawdbotConfig) => DmPolicy;
|
||||
setPolicy: (cfg: ClawdbotConfig, policy: DmPolicy) => ClawdbotConfig;
|
||||
};
|
||||
|
||||
export type ProviderOnboardingAdapter = {
|
||||
provider: ChatProviderId;
|
||||
export type ChannelOnboardingAdapter = {
|
||||
channel: ChatChannelId;
|
||||
getStatus: (
|
||||
ctx: ProviderOnboardingStatusContext,
|
||||
) => Promise<ProviderOnboardingStatus>;
|
||||
ctx: ChannelOnboardingStatusContext,
|
||||
) => Promise<ChannelOnboardingStatus>;
|
||||
configure: (
|
||||
ctx: ProviderOnboardingConfigureContext,
|
||||
) => Promise<ProviderOnboardingResult>;
|
||||
dmPolicy?: ProviderOnboardingDmPolicy;
|
||||
ctx: ChannelOnboardingConfigureContext,
|
||||
) => Promise<ChannelOnboardingResult>;
|
||||
dmPolicy?: ChannelOnboardingDmPolicy;
|
||||
onAccountRecorded?: (
|
||||
accountId: string,
|
||||
options?: SetupProvidersOptions,
|
||||
options?: SetupChannelsOptions,
|
||||
) => void;
|
||||
disable?: (cfg: ClawdbotConfig) => ClawdbotConfig;
|
||||
};
|
||||
@@ -12,27 +12,30 @@ import {
|
||||
import { formatDocsLink } from "../../../terminal/links.js";
|
||||
import type { WizardPrompter } from "../../../wizard/prompts.js";
|
||||
import type {
|
||||
ProviderOnboardingAdapter,
|
||||
ProviderOnboardingDmPolicy,
|
||||
ChannelOnboardingAdapter,
|
||||
ChannelOnboardingDmPolicy,
|
||||
} from "../onboarding-types.js";
|
||||
import { addWildcardAllowFrom, promptAccountId } from "./helpers.js";
|
||||
|
||||
const provider = "discord" as const;
|
||||
const channel = "discord" as const;
|
||||
|
||||
function setDiscordDmPolicy(cfg: ClawdbotConfig, dmPolicy: DmPolicy) {
|
||||
const allowFrom =
|
||||
dmPolicy === "open"
|
||||
? addWildcardAllowFrom(cfg.discord?.dm?.allowFrom)
|
||||
? addWildcardAllowFrom(cfg.channels?.discord?.dm?.allowFrom)
|
||||
: undefined;
|
||||
return {
|
||||
...cfg,
|
||||
discord: {
|
||||
...cfg.discord,
|
||||
dm: {
|
||||
...cfg.discord?.dm,
|
||||
enabled: cfg.discord?.dm?.enabled ?? true,
|
||||
policy: dmPolicy,
|
||||
...(allowFrom ? { allowFrom } : {}),
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
discord: {
|
||||
...cfg.channels?.discord,
|
||||
dm: {
|
||||
...cfg.channels?.discord?.dm,
|
||||
enabled: cfg.channels?.discord?.dm?.enabled ?? true,
|
||||
policy: dmPolicy,
|
||||
...(allowFrom ? { allowFrom } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -51,23 +54,23 @@ async function noteDiscordTokenHelp(prompter: WizardPrompter): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
const dmPolicy: ProviderOnboardingDmPolicy = {
|
||||
const dmPolicy: ChannelOnboardingDmPolicy = {
|
||||
label: "Discord",
|
||||
provider,
|
||||
policyKey: "discord.dm.policy",
|
||||
allowFromKey: "discord.dm.allowFrom",
|
||||
getCurrent: (cfg) => cfg.discord?.dm?.policy ?? "pairing",
|
||||
channel,
|
||||
policyKey: "channels.discord.dm.policy",
|
||||
allowFromKey: "channels.discord.dm.allowFrom",
|
||||
getCurrent: (cfg) => cfg.channels?.discord?.dm?.policy ?? "pairing",
|
||||
setPolicy: (cfg, policy) => setDiscordDmPolicy(cfg, policy),
|
||||
};
|
||||
|
||||
export const discordOnboardingAdapter: ProviderOnboardingAdapter = {
|
||||
provider,
|
||||
export const discordOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
channel,
|
||||
getStatus: async ({ cfg }) => {
|
||||
const configured = listDiscordAccountIds(cfg).some((accountId) =>
|
||||
Boolean(resolveDiscordAccount({ cfg, accountId }).token),
|
||||
);
|
||||
return {
|
||||
provider,
|
||||
channel,
|
||||
configured,
|
||||
statusLines: [`Discord: ${configured ? "configured" : "needs token"}`],
|
||||
selectionHint: configured ? "configured" : "needs token",
|
||||
@@ -119,9 +122,9 @@ export const discordOnboardingAdapter: ProviderOnboardingAdapter = {
|
||||
if (keepEnv) {
|
||||
next = {
|
||||
...next,
|
||||
discord: {
|
||||
...next.discord,
|
||||
enabled: true,
|
||||
channels: {
|
||||
...next.channels,
|
||||
discord: { ...next.channels?.discord, enabled: true },
|
||||
},
|
||||
};
|
||||
} else {
|
||||
@@ -158,25 +161,28 @@ export const discordOnboardingAdapter: ProviderOnboardingAdapter = {
|
||||
if (discordAccountId === DEFAULT_ACCOUNT_ID) {
|
||||
next = {
|
||||
...next,
|
||||
discord: {
|
||||
...next.discord,
|
||||
enabled: true,
|
||||
token,
|
||||
channels: {
|
||||
...next.channels,
|
||||
discord: { ...next.channels?.discord, enabled: true, token },
|
||||
},
|
||||
};
|
||||
} else {
|
||||
next = {
|
||||
...next,
|
||||
discord: {
|
||||
...next.discord,
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...next.discord?.accounts,
|
||||
[discordAccountId]: {
|
||||
...next.discord?.accounts?.[discordAccountId],
|
||||
enabled:
|
||||
next.discord?.accounts?.[discordAccountId]?.enabled ?? true,
|
||||
token,
|
||||
channels: {
|
||||
...next.channels,
|
||||
discord: {
|
||||
...next.channels?.discord,
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...next.channels?.discord?.accounts,
|
||||
[discordAccountId]: {
|
||||
...next.channels?.discord?.accounts?.[discordAccountId],
|
||||
enabled:
|
||||
next.channels?.discord?.accounts?.[discordAccountId]
|
||||
?.enabled ?? true,
|
||||
token,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -189,6 +195,9 @@ export const discordOnboardingAdapter: ProviderOnboardingAdapter = {
|
||||
dmPolicy,
|
||||
disable: (cfg) => ({
|
||||
...cfg,
|
||||
discord: { ...cfg.discord, enabled: false },
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
discord: { ...cfg.channels?.discord, enabled: false },
|
||||
},
|
||||
}),
|
||||
};
|
||||
@@ -12,39 +12,42 @@ import {
|
||||
} from "../../../routing/session-key.js";
|
||||
import { formatDocsLink } from "../../../terminal/links.js";
|
||||
import type {
|
||||
ProviderOnboardingAdapter,
|
||||
ProviderOnboardingDmPolicy,
|
||||
ChannelOnboardingAdapter,
|
||||
ChannelOnboardingDmPolicy,
|
||||
} from "../onboarding-types.js";
|
||||
import { addWildcardAllowFrom, promptAccountId } from "./helpers.js";
|
||||
|
||||
const provider = "imessage" as const;
|
||||
const channel = "imessage" as const;
|
||||
|
||||
function setIMessageDmPolicy(cfg: ClawdbotConfig, dmPolicy: DmPolicy) {
|
||||
const allowFrom =
|
||||
dmPolicy === "open"
|
||||
? addWildcardAllowFrom(cfg.imessage?.allowFrom)
|
||||
? addWildcardAllowFrom(cfg.channels?.imessage?.allowFrom)
|
||||
: undefined;
|
||||
return {
|
||||
...cfg,
|
||||
imessage: {
|
||||
...cfg.imessage,
|
||||
dmPolicy,
|
||||
...(allowFrom ? { allowFrom } : {}),
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
imessage: {
|
||||
...cfg.channels?.imessage,
|
||||
dmPolicy,
|
||||
...(allowFrom ? { allowFrom } : {}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const dmPolicy: ProviderOnboardingDmPolicy = {
|
||||
const dmPolicy: ChannelOnboardingDmPolicy = {
|
||||
label: "iMessage",
|
||||
provider,
|
||||
policyKey: "imessage.dmPolicy",
|
||||
allowFromKey: "imessage.allowFrom",
|
||||
getCurrent: (cfg) => cfg.imessage?.dmPolicy ?? "pairing",
|
||||
channel,
|
||||
policyKey: "channels.imessage.dmPolicy",
|
||||
allowFromKey: "channels.imessage.allowFrom",
|
||||
getCurrent: (cfg) => cfg.channels?.imessage?.dmPolicy ?? "pairing",
|
||||
setPolicy: (cfg, policy) => setIMessageDmPolicy(cfg, policy),
|
||||
};
|
||||
|
||||
export const imessageOnboardingAdapter: ProviderOnboardingAdapter = {
|
||||
provider,
|
||||
export const imessageOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
channel,
|
||||
getStatus: async ({ cfg }) => {
|
||||
const configured = listIMessageAccountIds(cfg).some((accountId) => {
|
||||
const account = resolveIMessageAccount({ cfg, accountId });
|
||||
@@ -56,10 +59,10 @@ export const imessageOnboardingAdapter: ProviderOnboardingAdapter = {
|
||||
account.config.region,
|
||||
);
|
||||
});
|
||||
const imessageCliPath = cfg.imessage?.cliPath ?? "imsg";
|
||||
const imessageCliPath = cfg.channels?.imessage?.cliPath ?? "imsg";
|
||||
const imessageCliDetected = await detectBinary(imessageCliPath);
|
||||
return {
|
||||
provider,
|
||||
channel,
|
||||
configured,
|
||||
statusLines: [
|
||||
`iMessage: ${configured ? "configured" : "needs setup"}`,
|
||||
@@ -117,25 +120,32 @@ export const imessageOnboardingAdapter: ProviderOnboardingAdapter = {
|
||||
if (imessageAccountId === DEFAULT_ACCOUNT_ID) {
|
||||
next = {
|
||||
...next,
|
||||
imessage: {
|
||||
...next.imessage,
|
||||
enabled: true,
|
||||
cliPath: resolvedCliPath,
|
||||
channels: {
|
||||
...next.channels,
|
||||
imessage: {
|
||||
...next.channels?.imessage,
|
||||
enabled: true,
|
||||
cliPath: resolvedCliPath,
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
next = {
|
||||
...next,
|
||||
imessage: {
|
||||
...next.imessage,
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...next.imessage?.accounts,
|
||||
[imessageAccountId]: {
|
||||
...next.imessage?.accounts?.[imessageAccountId],
|
||||
enabled:
|
||||
next.imessage?.accounts?.[imessageAccountId]?.enabled ?? true,
|
||||
cliPath: resolvedCliPath,
|
||||
channels: {
|
||||
...next.channels,
|
||||
imessage: {
|
||||
...next.channels?.imessage,
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...next.channels?.imessage?.accounts,
|
||||
[imessageAccountId]: {
|
||||
...next.channels?.imessage?.accounts?.[imessageAccountId],
|
||||
enabled:
|
||||
next.channels?.imessage?.accounts?.[imessageAccountId]
|
||||
?.enabled ?? true,
|
||||
cliPath: resolvedCliPath,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -159,6 +169,9 @@ export const imessageOnboardingAdapter: ProviderOnboardingAdapter = {
|
||||
dmPolicy,
|
||||
disable: (cfg) => ({
|
||||
...cfg,
|
||||
imessage: { ...cfg.imessage, enabled: false },
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
imessage: { ...cfg.channels?.imessage, enabled: false },
|
||||
},
|
||||
}),
|
||||
};
|
||||
@@ -5,26 +5,29 @@ import { DEFAULT_ACCOUNT_ID } from "../../../routing/session-key.js";
|
||||
import { formatDocsLink } from "../../../terminal/links.js";
|
||||
import type { WizardPrompter } from "../../../wizard/prompts.js";
|
||||
import type {
|
||||
ProviderOnboardingAdapter,
|
||||
ProviderOnboardingDmPolicy,
|
||||
ChannelOnboardingAdapter,
|
||||
ChannelOnboardingDmPolicy,
|
||||
} from "../onboarding-types.js";
|
||||
import { addWildcardAllowFrom } from "./helpers.js";
|
||||
|
||||
const provider = "msteams" as const;
|
||||
const channel = "msteams" as const;
|
||||
|
||||
function setMSTeamsDmPolicy(cfg: ClawdbotConfig, dmPolicy: DmPolicy) {
|
||||
const allowFrom =
|
||||
dmPolicy === "open"
|
||||
? addWildcardAllowFrom(cfg.msteams?.allowFrom)?.map((entry) =>
|
||||
? addWildcardAllowFrom(cfg.channels?.msteams?.allowFrom)?.map((entry) =>
|
||||
String(entry),
|
||||
)
|
||||
: undefined;
|
||||
return {
|
||||
...cfg,
|
||||
msteams: {
|
||||
...cfg.msteams,
|
||||
dmPolicy,
|
||||
...(allowFrom ? { allowFrom } : {}),
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
msteams: {
|
||||
...cfg.channels?.msteams,
|
||||
dmPolicy,
|
||||
...(allowFrom ? { allowFrom } : {}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -44,21 +47,23 @@ async function noteMSTeamsCredentialHelp(
|
||||
);
|
||||
}
|
||||
|
||||
const dmPolicy: ProviderOnboardingDmPolicy = {
|
||||
const dmPolicy: ChannelOnboardingDmPolicy = {
|
||||
label: "MS Teams",
|
||||
provider,
|
||||
policyKey: "msteams.dmPolicy",
|
||||
allowFromKey: "msteams.allowFrom",
|
||||
getCurrent: (cfg) => cfg.msteams?.dmPolicy ?? "pairing",
|
||||
channel,
|
||||
policyKey: "channels.msteams.dmPolicy",
|
||||
allowFromKey: "channels.msteams.allowFrom",
|
||||
getCurrent: (cfg) => cfg.channels?.msteams?.dmPolicy ?? "pairing",
|
||||
setPolicy: (cfg, policy) => setMSTeamsDmPolicy(cfg, policy),
|
||||
};
|
||||
|
||||
export const msteamsOnboardingAdapter: ProviderOnboardingAdapter = {
|
||||
provider,
|
||||
export const msteamsOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
channel,
|
||||
getStatus: async ({ cfg }) => {
|
||||
const configured = Boolean(resolveMSTeamsCredentials(cfg.msteams));
|
||||
const configured = Boolean(
|
||||
resolveMSTeamsCredentials(cfg.channels?.msteams),
|
||||
);
|
||||
return {
|
||||
provider,
|
||||
channel,
|
||||
configured,
|
||||
statusLines: [
|
||||
`MS Teams: ${configured ? "configured" : "needs app credentials"}`,
|
||||
@@ -68,11 +73,11 @@ export const msteamsOnboardingAdapter: ProviderOnboardingAdapter = {
|
||||
};
|
||||
},
|
||||
configure: async ({ cfg, prompter }) => {
|
||||
const resolved = resolveMSTeamsCredentials(cfg.msteams);
|
||||
const resolved = resolveMSTeamsCredentials(cfg.channels?.msteams);
|
||||
const hasConfigCreds = Boolean(
|
||||
cfg.msteams?.appId?.trim() &&
|
||||
cfg.msteams?.appPassword?.trim() &&
|
||||
cfg.msteams?.tenantId?.trim(),
|
||||
cfg.channels?.msteams?.appId?.trim() &&
|
||||
cfg.channels?.msteams?.appPassword?.trim() &&
|
||||
cfg.channels?.msteams?.tenantId?.trim(),
|
||||
);
|
||||
const canUseEnv = Boolean(
|
||||
!hasConfigCreds &&
|
||||
@@ -99,9 +104,9 @@ export const msteamsOnboardingAdapter: ProviderOnboardingAdapter = {
|
||||
if (keepEnv) {
|
||||
next = {
|
||||
...next,
|
||||
msteams: {
|
||||
...next.msteams,
|
||||
enabled: true,
|
||||
channels: {
|
||||
...next.channels,
|
||||
msteams: { ...next.channels?.msteams, enabled: true },
|
||||
},
|
||||
};
|
||||
} else {
|
||||
@@ -173,12 +178,15 @@ export const msteamsOnboardingAdapter: ProviderOnboardingAdapter = {
|
||||
if (appId && appPassword && tenantId) {
|
||||
next = {
|
||||
...next,
|
||||
msteams: {
|
||||
...next.msteams,
|
||||
enabled: true,
|
||||
appId,
|
||||
appPassword,
|
||||
tenantId,
|
||||
channels: {
|
||||
...next.channels,
|
||||
msteams: {
|
||||
...next.channels?.msteams,
|
||||
enabled: true,
|
||||
appId,
|
||||
appPassword,
|
||||
tenantId,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -188,6 +196,9 @@ export const msteamsOnboardingAdapter: ProviderOnboardingAdapter = {
|
||||
dmPolicy,
|
||||
disable: (cfg) => ({
|
||||
...cfg,
|
||||
msteams: { ...cfg.msteams, enabled: false },
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
msteams: { ...cfg.channels?.msteams, enabled: false },
|
||||
},
|
||||
}),
|
||||
};
|
||||
@@ -13,47 +13,50 @@ import {
|
||||
} from "../../../signal/accounts.js";
|
||||
import { formatDocsLink } from "../../../terminal/links.js";
|
||||
import type {
|
||||
ProviderOnboardingAdapter,
|
||||
ProviderOnboardingDmPolicy,
|
||||
ChannelOnboardingAdapter,
|
||||
ChannelOnboardingDmPolicy,
|
||||
} from "../onboarding-types.js";
|
||||
import { addWildcardAllowFrom, promptAccountId } from "./helpers.js";
|
||||
|
||||
const provider = "signal" as const;
|
||||
const channel = "signal" as const;
|
||||
|
||||
function setSignalDmPolicy(cfg: ClawdbotConfig, dmPolicy: DmPolicy) {
|
||||
const allowFrom =
|
||||
dmPolicy === "open"
|
||||
? addWildcardAllowFrom(cfg.signal?.allowFrom)
|
||||
? addWildcardAllowFrom(cfg.channels?.signal?.allowFrom)
|
||||
: undefined;
|
||||
return {
|
||||
...cfg,
|
||||
signal: {
|
||||
...cfg.signal,
|
||||
dmPolicy,
|
||||
...(allowFrom ? { allowFrom } : {}),
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
signal: {
|
||||
...cfg.channels?.signal,
|
||||
dmPolicy,
|
||||
...(allowFrom ? { allowFrom } : {}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const dmPolicy: ProviderOnboardingDmPolicy = {
|
||||
const dmPolicy: ChannelOnboardingDmPolicy = {
|
||||
label: "Signal",
|
||||
provider,
|
||||
policyKey: "signal.dmPolicy",
|
||||
allowFromKey: "signal.allowFrom",
|
||||
getCurrent: (cfg) => cfg.signal?.dmPolicy ?? "pairing",
|
||||
channel,
|
||||
policyKey: "channels.signal.dmPolicy",
|
||||
allowFromKey: "channels.signal.allowFrom",
|
||||
getCurrent: (cfg) => cfg.channels?.signal?.dmPolicy ?? "pairing",
|
||||
setPolicy: (cfg, policy) => setSignalDmPolicy(cfg, policy),
|
||||
};
|
||||
|
||||
export const signalOnboardingAdapter: ProviderOnboardingAdapter = {
|
||||
provider,
|
||||
export const signalOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
channel,
|
||||
getStatus: async ({ cfg }) => {
|
||||
const configured = listSignalAccountIds(cfg).some(
|
||||
(accountId) => resolveSignalAccount({ cfg, accountId }).configured,
|
||||
);
|
||||
const signalCliPath = cfg.signal?.cliPath ?? "signal-cli";
|
||||
const signalCliPath = cfg.channels?.signal?.cliPath ?? "signal-cli";
|
||||
const signalCliDetected = await detectBinary(signalCliPath);
|
||||
return {
|
||||
provider,
|
||||
channel,
|
||||
configured,
|
||||
statusLines: [
|
||||
`Signal: ${configured ? "configured" : "needs setup"}`,
|
||||
@@ -131,7 +134,7 @@ export const signalOnboardingAdapter: ProviderOnboardingAdapter = {
|
||||
|
||||
if (!cliDetected) {
|
||||
await prompter.note(
|
||||
"signal-cli not found. Install it, then rerun this step or set signal.cliPath.",
|
||||
"signal-cli not found. Install it, then rerun this step or set channels.signal.cliPath.",
|
||||
"Signal",
|
||||
);
|
||||
}
|
||||
@@ -158,27 +161,34 @@ export const signalOnboardingAdapter: ProviderOnboardingAdapter = {
|
||||
if (signalAccountId === DEFAULT_ACCOUNT_ID) {
|
||||
next = {
|
||||
...next,
|
||||
signal: {
|
||||
...next.signal,
|
||||
enabled: true,
|
||||
account,
|
||||
cliPath: resolvedCliPath ?? "signal-cli",
|
||||
channels: {
|
||||
...next.channels,
|
||||
signal: {
|
||||
...next.channels?.signal,
|
||||
enabled: true,
|
||||
account,
|
||||
cliPath: resolvedCliPath ?? "signal-cli",
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
next = {
|
||||
...next,
|
||||
signal: {
|
||||
...next.signal,
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...next.signal?.accounts,
|
||||
[signalAccountId]: {
|
||||
...next.signal?.accounts?.[signalAccountId],
|
||||
enabled:
|
||||
next.signal?.accounts?.[signalAccountId]?.enabled ?? true,
|
||||
account,
|
||||
cliPath: resolvedCliPath ?? "signal-cli",
|
||||
channels: {
|
||||
...next.channels,
|
||||
signal: {
|
||||
...next.channels?.signal,
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...next.channels?.signal?.accounts,
|
||||
[signalAccountId]: {
|
||||
...next.channels?.signal?.accounts?.[signalAccountId],
|
||||
enabled:
|
||||
next.channels?.signal?.accounts?.[signalAccountId]
|
||||
?.enabled ?? true,
|
||||
account,
|
||||
cliPath: resolvedCliPath ?? "signal-cli",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -190,7 +200,7 @@ export const signalOnboardingAdapter: ProviderOnboardingAdapter = {
|
||||
[
|
||||
'Link device with: signal-cli link -n "Clawdbot"',
|
||||
"Scan QR in Signal → Linked Devices",
|
||||
"Then run: clawdbot gateway call providers.status --params '{\"probe\":true}'",
|
||||
"Then run: clawdbot gateway call channels.status --params '{\"probe\":true}'",
|
||||
`Docs: ${formatDocsLink("/signal", "signal")}`,
|
||||
].join("\n"),
|
||||
"Signal next steps",
|
||||
@@ -201,6 +211,9 @@ export const signalOnboardingAdapter: ProviderOnboardingAdapter = {
|
||||
dmPolicy,
|
||||
disable: (cfg) => ({
|
||||
...cfg,
|
||||
signal: { ...cfg.signal, enabled: false },
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
signal: { ...cfg.channels?.signal, enabled: false },
|
||||
},
|
||||
}),
|
||||
};
|
||||
@@ -12,27 +12,30 @@ import {
|
||||
import { formatDocsLink } from "../../../terminal/links.js";
|
||||
import type { WizardPrompter } from "../../../wizard/prompts.js";
|
||||
import type {
|
||||
ProviderOnboardingAdapter,
|
||||
ProviderOnboardingDmPolicy,
|
||||
ChannelOnboardingAdapter,
|
||||
ChannelOnboardingDmPolicy,
|
||||
} from "../onboarding-types.js";
|
||||
import { addWildcardAllowFrom, promptAccountId } from "./helpers.js";
|
||||
|
||||
const provider = "slack" as const;
|
||||
const channel = "slack" as const;
|
||||
|
||||
function setSlackDmPolicy(cfg: ClawdbotConfig, dmPolicy: DmPolicy) {
|
||||
const allowFrom =
|
||||
dmPolicy === "open"
|
||||
? addWildcardAllowFrom(cfg.slack?.dm?.allowFrom)
|
||||
? addWildcardAllowFrom(cfg.channels?.slack?.dm?.allowFrom)
|
||||
: undefined;
|
||||
return {
|
||||
...cfg,
|
||||
slack: {
|
||||
...cfg.slack,
|
||||
dm: {
|
||||
...cfg.slack?.dm,
|
||||
enabled: cfg.slack?.dm?.enabled ?? true,
|
||||
policy: dmPolicy,
|
||||
...(allowFrom ? { allowFrom } : {}),
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
slack: {
|
||||
...cfg.channels?.slack,
|
||||
dm: {
|
||||
...cfg.channels?.slack?.dm,
|
||||
enabled: cfg.channels?.slack?.dm?.enabled ?? true,
|
||||
policy: dmPolicy,
|
||||
...(allowFrom ? { allowFrom } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -129,24 +132,24 @@ async function noteSlackTokenHelp(
|
||||
);
|
||||
}
|
||||
|
||||
const dmPolicy: ProviderOnboardingDmPolicy = {
|
||||
const dmPolicy: ChannelOnboardingDmPolicy = {
|
||||
label: "Slack",
|
||||
provider,
|
||||
policyKey: "slack.dm.policy",
|
||||
allowFromKey: "slack.dm.allowFrom",
|
||||
getCurrent: (cfg) => cfg.slack?.dm?.policy ?? "pairing",
|
||||
channel,
|
||||
policyKey: "channels.slack.dm.policy",
|
||||
allowFromKey: "channels.slack.dm.allowFrom",
|
||||
getCurrent: (cfg) => cfg.channels?.slack?.dm?.policy ?? "pairing",
|
||||
setPolicy: (cfg, policy) => setSlackDmPolicy(cfg, policy),
|
||||
};
|
||||
|
||||
export const slackOnboardingAdapter: ProviderOnboardingAdapter = {
|
||||
provider,
|
||||
export const slackOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
channel,
|
||||
getStatus: async ({ cfg }) => {
|
||||
const configured = listSlackAccountIds(cfg).some((accountId) => {
|
||||
const account = resolveSlackAccount({ cfg, accountId });
|
||||
return Boolean(account.botToken && account.appToken);
|
||||
});
|
||||
return {
|
||||
provider,
|
||||
channel,
|
||||
configured,
|
||||
statusLines: [`Slack: ${configured ? "configured" : "needs tokens"}`],
|
||||
selectionHint: configured ? "configured" : "needs tokens",
|
||||
@@ -214,9 +217,9 @@ export const slackOnboardingAdapter: ProviderOnboardingAdapter = {
|
||||
if (keepEnv) {
|
||||
next = {
|
||||
...next,
|
||||
slack: {
|
||||
...next.slack,
|
||||
enabled: true,
|
||||
channels: {
|
||||
...next.channels,
|
||||
slack: { ...next.channels?.slack, enabled: true },
|
||||
},
|
||||
};
|
||||
} else {
|
||||
@@ -271,27 +274,34 @@ export const slackOnboardingAdapter: ProviderOnboardingAdapter = {
|
||||
if (slackAccountId === DEFAULT_ACCOUNT_ID) {
|
||||
next = {
|
||||
...next,
|
||||
slack: {
|
||||
...next.slack,
|
||||
enabled: true,
|
||||
botToken,
|
||||
appToken,
|
||||
channels: {
|
||||
...next.channels,
|
||||
slack: {
|
||||
...next.channels?.slack,
|
||||
enabled: true,
|
||||
botToken,
|
||||
appToken,
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
next = {
|
||||
...next,
|
||||
slack: {
|
||||
...next.slack,
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...next.slack?.accounts,
|
||||
[slackAccountId]: {
|
||||
...next.slack?.accounts?.[slackAccountId],
|
||||
enabled:
|
||||
next.slack?.accounts?.[slackAccountId]?.enabled ?? true,
|
||||
botToken,
|
||||
appToken,
|
||||
channels: {
|
||||
...next.channels,
|
||||
slack: {
|
||||
...next.channels?.slack,
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...next.channels?.slack?.accounts,
|
||||
[slackAccountId]: {
|
||||
...next.channels?.slack?.accounts?.[slackAccountId],
|
||||
enabled:
|
||||
next.channels?.slack?.accounts?.[slackAccountId]?.enabled ??
|
||||
true,
|
||||
botToken,
|
||||
appToken,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -304,6 +314,9 @@ export const slackOnboardingAdapter: ProviderOnboardingAdapter = {
|
||||
dmPolicy,
|
||||
disable: (cfg) => ({
|
||||
...cfg,
|
||||
slack: { ...cfg.slack, enabled: false },
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
slack: { ...cfg.channels?.slack, enabled: false },
|
||||
},
|
||||
}),
|
||||
};
|
||||
@@ -12,24 +12,27 @@ import {
|
||||
import { formatDocsLink } from "../../../terminal/links.js";
|
||||
import type { WizardPrompter } from "../../../wizard/prompts.js";
|
||||
import type {
|
||||
ProviderOnboardingAdapter,
|
||||
ProviderOnboardingDmPolicy,
|
||||
ChannelOnboardingAdapter,
|
||||
ChannelOnboardingDmPolicy,
|
||||
} from "../onboarding-types.js";
|
||||
import { addWildcardAllowFrom, promptAccountId } from "./helpers.js";
|
||||
|
||||
const provider = "telegram" as const;
|
||||
const channel = "telegram" as const;
|
||||
|
||||
function setTelegramDmPolicy(cfg: ClawdbotConfig, dmPolicy: DmPolicy) {
|
||||
const allowFrom =
|
||||
dmPolicy === "open"
|
||||
? addWildcardAllowFrom(cfg.telegram?.allowFrom)
|
||||
? addWildcardAllowFrom(cfg.channels?.telegram?.allowFrom)
|
||||
: undefined;
|
||||
return {
|
||||
...cfg,
|
||||
telegram: {
|
||||
...cfg.telegram,
|
||||
dmPolicy,
|
||||
...(allowFrom ? { allowFrom } : {}),
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
telegram: {
|
||||
...cfg.channels?.telegram,
|
||||
dmPolicy,
|
||||
...(allowFrom ? { allowFrom } : {}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -79,50 +82,57 @@ async function promptTelegramAllowFrom(params: {
|
||||
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||
return {
|
||||
...cfg,
|
||||
telegram: {
|
||||
...cfg.telegram,
|
||||
enabled: true,
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: unique,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
telegram: {
|
||||
...cfg.channels?.telegram,
|
||||
enabled: true,
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: unique,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
telegram: {
|
||||
...cfg.telegram,
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...cfg.telegram?.accounts,
|
||||
[accountId]: {
|
||||
...cfg.telegram?.accounts?.[accountId],
|
||||
enabled: cfg.telegram?.accounts?.[accountId]?.enabled ?? true,
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: unique,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
telegram: {
|
||||
...cfg.channels?.telegram,
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...cfg.channels?.telegram?.accounts,
|
||||
[accountId]: {
|
||||
...cfg.channels?.telegram?.accounts?.[accountId],
|
||||
enabled:
|
||||
cfg.channels?.telegram?.accounts?.[accountId]?.enabled ?? true,
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: unique,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const dmPolicy: ProviderOnboardingDmPolicy = {
|
||||
const dmPolicy: ChannelOnboardingDmPolicy = {
|
||||
label: "Telegram",
|
||||
provider,
|
||||
policyKey: "telegram.dmPolicy",
|
||||
allowFromKey: "telegram.allowFrom",
|
||||
getCurrent: (cfg) => cfg.telegram?.dmPolicy ?? "pairing",
|
||||
channel,
|
||||
policyKey: "channels.telegram.dmPolicy",
|
||||
allowFromKey: "channels.telegram.allowFrom",
|
||||
getCurrent: (cfg) => cfg.channels?.telegram?.dmPolicy ?? "pairing",
|
||||
setPolicy: (cfg, policy) => setTelegramDmPolicy(cfg, policy),
|
||||
};
|
||||
|
||||
export const telegramOnboardingAdapter: ProviderOnboardingAdapter = {
|
||||
provider,
|
||||
export const telegramOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
channel,
|
||||
getStatus: async ({ cfg }) => {
|
||||
const configured = listTelegramAccountIds(cfg).some((accountId) =>
|
||||
Boolean(resolveTelegramAccount({ cfg, accountId }).token),
|
||||
);
|
||||
return {
|
||||
provider,
|
||||
channel,
|
||||
configured,
|
||||
statusLines: [`Telegram: ${configured ? "configured" : "needs token"}`],
|
||||
selectionHint: configured
|
||||
@@ -179,9 +189,12 @@ export const telegramOnboardingAdapter: ProviderOnboardingAdapter = {
|
||||
if (keepEnv) {
|
||||
next = {
|
||||
...next,
|
||||
telegram: {
|
||||
...next.telegram,
|
||||
enabled: true,
|
||||
channels: {
|
||||
...next.channels,
|
||||
telegram: {
|
||||
...next.channels?.telegram,
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
@@ -218,25 +231,32 @@ export const telegramOnboardingAdapter: ProviderOnboardingAdapter = {
|
||||
if (telegramAccountId === DEFAULT_ACCOUNT_ID) {
|
||||
next = {
|
||||
...next,
|
||||
telegram: {
|
||||
...next.telegram,
|
||||
enabled: true,
|
||||
botToken: token,
|
||||
channels: {
|
||||
...next.channels,
|
||||
telegram: {
|
||||
...next.channels?.telegram,
|
||||
enabled: true,
|
||||
botToken: token,
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
next = {
|
||||
...next,
|
||||
telegram: {
|
||||
...next.telegram,
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...next.telegram?.accounts,
|
||||
[telegramAccountId]: {
|
||||
...next.telegram?.accounts?.[telegramAccountId],
|
||||
enabled:
|
||||
next.telegram?.accounts?.[telegramAccountId]?.enabled ?? true,
|
||||
botToken: token,
|
||||
channels: {
|
||||
...next.channels,
|
||||
telegram: {
|
||||
...next.channels?.telegram,
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...next.channels?.telegram?.accounts,
|
||||
[telegramAccountId]: {
|
||||
...next.channels?.telegram?.accounts?.[telegramAccountId],
|
||||
enabled:
|
||||
next.channels?.telegram?.accounts?.[telegramAccountId]
|
||||
?.enabled ?? true,
|
||||
botToken: token,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -257,6 +277,9 @@ export const telegramOnboardingAdapter: ProviderOnboardingAdapter = {
|
||||
dmPolicy,
|
||||
disable: (cfg) => ({
|
||||
...cfg,
|
||||
telegram: { ...cfg.telegram, enabled: false },
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
telegram: { ...cfg.channels?.telegram, enabled: false },
|
||||
},
|
||||
}),
|
||||
};
|
||||
@@ -1,9 +1,9 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { loginWeb } from "../../../channel-web.js";
|
||||
import type { ClawdbotConfig } from "../../../config/config.js";
|
||||
import { mergeWhatsAppConfig } from "../../../config/merge-config.js";
|
||||
import type { DmPolicy } from "../../../config/types.js";
|
||||
import { loginWeb } from "../../../provider-web.js";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
@@ -17,10 +17,10 @@ import {
|
||||
resolveWhatsAppAuthDir,
|
||||
} from "../../../web/accounts.js";
|
||||
import type { WizardPrompter } from "../../../wizard/prompts.js";
|
||||
import type { ProviderOnboardingAdapter } from "../onboarding-types.js";
|
||||
import type { ChannelOnboardingAdapter } from "../onboarding-types.js";
|
||||
import { promptAccountId } from "./helpers.js";
|
||||
|
||||
const provider = "whatsapp" as const;
|
||||
const channel = "whatsapp" as const;
|
||||
|
||||
function setWhatsAppDmPolicy(
|
||||
cfg: ClawdbotConfig,
|
||||
@@ -84,8 +84,8 @@ async function promptWhatsAppAllowFrom(
|
||||
prompter: WizardPrompter,
|
||||
options?: { forceAllowlist?: boolean },
|
||||
): Promise<ClawdbotConfig> {
|
||||
const existingPolicy = cfg.whatsapp?.dmPolicy ?? "pairing";
|
||||
const existingAllowFrom = cfg.whatsapp?.allowFrom ?? [];
|
||||
const existingPolicy = cfg.channels?.whatsapp?.dmPolicy ?? "pairing";
|
||||
const existingAllowFrom = cfg.channels?.whatsapp?.allowFrom ?? [];
|
||||
const existingLabel =
|
||||
existingAllowFrom.length > 0 ? existingAllowFrom.join(", ") : "unset";
|
||||
const existingResponsePrefix = cfg.messages?.responsePrefix;
|
||||
@@ -138,7 +138,7 @@ async function promptWhatsAppAllowFrom(
|
||||
|
||||
await prompter.note(
|
||||
[
|
||||
"WhatsApp direct chats are gated by `whatsapp.dmPolicy` + `whatsapp.allowFrom`.",
|
||||
"WhatsApp direct chats are gated by `channels.whatsapp.dmPolicy` + `channels.whatsapp.allowFrom`.",
|
||||
"- pairing (default): unknown senders get a pairing code; owner approves",
|
||||
"- allowlist: unknown senders are blocked",
|
||||
'- open: public inbound DMs (requires allowFrom to include "*")',
|
||||
@@ -284,8 +284,8 @@ async function promptWhatsAppAllowFrom(
|
||||
return next;
|
||||
}
|
||||
|
||||
export const whatsappOnboardingAdapter: ProviderOnboardingAdapter = {
|
||||
provider,
|
||||
export const whatsappOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
channel,
|
||||
getStatus: async ({ cfg, accountOverrides }) => {
|
||||
const overrideId = accountOverrides.whatsapp?.trim();
|
||||
const defaultAccountId = resolveDefaultWhatsAppAccountId(cfg);
|
||||
@@ -296,7 +296,7 @@ export const whatsappOnboardingAdapter: ProviderOnboardingAdapter = {
|
||||
const accountLabel =
|
||||
accountId === DEFAULT_ACCOUNT_ID ? "default" : accountId;
|
||||
return {
|
||||
provider,
|
||||
channel,
|
||||
configured: linked,
|
||||
statusLines: [
|
||||
`WhatsApp (${accountLabel}): ${linked ? "linked" : "not linked"}`,
|
||||
@@ -335,13 +335,18 @@ export const whatsappOnboardingAdapter: ProviderOnboardingAdapter = {
|
||||
if (accountId !== DEFAULT_ACCOUNT_ID) {
|
||||
next = {
|
||||
...next,
|
||||
whatsapp: {
|
||||
...next.whatsapp,
|
||||
accounts: {
|
||||
...next.whatsapp?.accounts,
|
||||
[accountId]: {
|
||||
...next.whatsapp?.accounts?.[accountId],
|
||||
enabled: next.whatsapp?.accounts?.[accountId]?.enabled ?? true,
|
||||
channels: {
|
||||
...next.channels,
|
||||
whatsapp: {
|
||||
...next.channels?.whatsapp,
|
||||
accounts: {
|
||||
...next.channels?.whatsapp?.accounts,
|
||||
[accountId]: {
|
||||
...next.channels?.whatsapp?.accounts?.[accountId],
|
||||
enabled:
|
||||
next.channels?.whatsapp?.accounts?.[accountId]?.enabled ??
|
||||
true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -382,7 +387,7 @@ export const whatsappOnboardingAdapter: ProviderOnboardingAdapter = {
|
||||
}
|
||||
} else if (!linked) {
|
||||
await prompter.note(
|
||||
"Run `clawdbot providers login` later to link WhatsApp.",
|
||||
"Run `clawdbot channels login` later to link WhatsApp.",
|
||||
"WhatsApp",
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { sendMessageDiscord, sendPollDiscord } from "../../../discord/send.js";
|
||||
import type { ProviderOutboundAdapter } from "../types.js";
|
||||
import type { ChannelOutboundAdapter } from "../types.js";
|
||||
|
||||
export const discordOutbound: ProviderOutboundAdapter = {
|
||||
export const discordOutbound: ChannelOutboundAdapter = {
|
||||
deliveryMode: "direct",
|
||||
chunker: null,
|
||||
textChunkLimit: 2000,
|
||||
@@ -25,7 +25,7 @@ export const discordOutbound: ProviderOutboundAdapter = {
|
||||
replyTo: replyToId ?? undefined,
|
||||
accountId: accountId ?? undefined,
|
||||
});
|
||||
return { provider: "discord", ...result };
|
||||
return { channel: "discord", ...result };
|
||||
},
|
||||
sendMedia: async ({ to, text, mediaUrl, accountId, deps, replyToId }) => {
|
||||
const send = deps?.sendDiscord ?? sendMessageDiscord;
|
||||
@@ -35,7 +35,7 @@ export const discordOutbound: ProviderOutboundAdapter = {
|
||||
replyTo: replyToId ?? undefined,
|
||||
accountId: accountId ?? undefined,
|
||||
});
|
||||
return { provider: "discord", ...result };
|
||||
return { channel: "discord", ...result };
|
||||
},
|
||||
sendPoll: async ({ to, poll, accountId }) =>
|
||||
await sendPollDiscord(to, poll, {
|
||||
@@ -1,9 +1,9 @@
|
||||
import { chunkText } from "../../../auto-reply/chunk.js";
|
||||
import { sendMessageIMessage } from "../../../imessage/send.js";
|
||||
import { resolveProviderMediaMaxBytes } from "../media-limits.js";
|
||||
import type { ProviderOutboundAdapter } from "../types.js";
|
||||
import { resolveChannelMediaMaxBytes } from "../media-limits.js";
|
||||
import type { ChannelOutboundAdapter } from "../types.js";
|
||||
|
||||
export const imessageOutbound: ProviderOutboundAdapter = {
|
||||
export const imessageOutbound: ChannelOutboundAdapter = {
|
||||
deliveryMode: "direct",
|
||||
chunker: chunkText,
|
||||
textChunkLimit: 4000,
|
||||
@@ -21,26 +21,26 @@ export const imessageOutbound: ProviderOutboundAdapter = {
|
||||
},
|
||||
sendText: async ({ cfg, to, text, accountId, deps }) => {
|
||||
const send = deps?.sendIMessage ?? sendMessageIMessage;
|
||||
const maxBytes = resolveProviderMediaMaxBytes({
|
||||
const maxBytes = resolveChannelMediaMaxBytes({
|
||||
cfg,
|
||||
resolveProviderLimitMb: ({ cfg, accountId }) =>
|
||||
cfg.imessage?.accounts?.[accountId]?.mediaMaxMb ??
|
||||
cfg.imessage?.mediaMaxMb,
|
||||
resolveChannelLimitMb: ({ cfg, accountId }) =>
|
||||
cfg.channels?.imessage?.accounts?.[accountId]?.mediaMaxMb ??
|
||||
cfg.channels?.imessage?.mediaMaxMb,
|
||||
accountId,
|
||||
});
|
||||
const result = await send(to, text, {
|
||||
maxBytes,
|
||||
accountId: accountId ?? undefined,
|
||||
});
|
||||
return { provider: "imessage", ...result };
|
||||
return { channel: "imessage", ...result };
|
||||
},
|
||||
sendMedia: async ({ cfg, to, text, mediaUrl, accountId, deps }) => {
|
||||
const send = deps?.sendIMessage ?? sendMessageIMessage;
|
||||
const maxBytes = resolveProviderMediaMaxBytes({
|
||||
const maxBytes = resolveChannelMediaMaxBytes({
|
||||
cfg,
|
||||
resolveProviderLimitMb: ({ cfg, accountId }) =>
|
||||
cfg.imessage?.accounts?.[accountId]?.mediaMaxMb ??
|
||||
cfg.imessage?.mediaMaxMb,
|
||||
resolveChannelLimitMb: ({ cfg, accountId }) =>
|
||||
cfg.channels?.imessage?.accounts?.[accountId]?.mediaMaxMb ??
|
||||
cfg.channels?.imessage?.mediaMaxMb,
|
||||
accountId,
|
||||
});
|
||||
const result = await send(to, text, {
|
||||
@@ -48,6 +48,6 @@ export const imessageOutbound: ProviderOutboundAdapter = {
|
||||
maxBytes,
|
||||
accountId: accountId ?? undefined,
|
||||
});
|
||||
return { provider: "imessage", ...result };
|
||||
return { channel: "imessage", ...result };
|
||||
},
|
||||
};
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { ProviderId, ProviderOutboundAdapter } from "../types.js";
|
||||
import type { ChannelId, ChannelOutboundAdapter } from "../types.js";
|
||||
|
||||
type OutboundLoader = () => Promise<ProviderOutboundAdapter>;
|
||||
type OutboundLoader = () => Promise<ChannelOutboundAdapter>;
|
||||
|
||||
// Provider docking: outbound sends should stay cheap to import.
|
||||
// Channel docking: outbound sends should stay cheap to import.
|
||||
//
|
||||
// The full provider plugins (src/providers/plugins/*.ts) pull in status,
|
||||
// The full channel plugins (src/channels/plugins/*.ts) pull in status,
|
||||
// onboarding, gateway monitors, etc. Outbound delivery only needs chunking +
|
||||
// send primitives, so we keep a dedicated, lightweight loader here.
|
||||
const LOADERS: Record<ProviderId, OutboundLoader> = {
|
||||
const LOADERS: Record<ChannelId, OutboundLoader> = {
|
||||
telegram: async () => (await import("./telegram.js")).telegramOutbound,
|
||||
whatsapp: async () => (await import("./whatsapp.js")).whatsappOutbound,
|
||||
discord: async () => (await import("./discord.js")).discordOutbound,
|
||||
@@ -17,11 +17,11 @@ const LOADERS: Record<ProviderId, OutboundLoader> = {
|
||||
msteams: async () => (await import("./msteams.js")).msteamsOutbound,
|
||||
};
|
||||
|
||||
const cache = new Map<ProviderId, ProviderOutboundAdapter>();
|
||||
const cache = new Map<ChannelId, ChannelOutboundAdapter>();
|
||||
|
||||
export async function loadProviderOutboundAdapter(
|
||||
id: ProviderId,
|
||||
): Promise<ProviderOutboundAdapter | undefined> {
|
||||
export async function loadChannelOutboundAdapter(
|
||||
id: ChannelId,
|
||||
): Promise<ChannelOutboundAdapter | undefined> {
|
||||
const cached = cache.get(id);
|
||||
if (cached) return cached;
|
||||
const loader = LOADERS[id];
|
||||
@@ -1,9 +1,9 @@
|
||||
import { chunkMarkdownText } from "../../../auto-reply/chunk.js";
|
||||
import { createMSTeamsPollStoreFs } from "../../../msteams/polls.js";
|
||||
import { sendMessageMSTeams, sendPollMSTeams } from "../../../msteams/send.js";
|
||||
import type { ProviderOutboundAdapter } from "../types.js";
|
||||
import type { ChannelOutboundAdapter } from "../types.js";
|
||||
|
||||
export const msteamsOutbound: ProviderOutboundAdapter = {
|
||||
export const msteamsOutbound: ChannelOutboundAdapter = {
|
||||
deliveryMode: "direct",
|
||||
chunker: chunkMarkdownText,
|
||||
textChunkLimit: 4000,
|
||||
@@ -25,7 +25,7 @@ export const msteamsOutbound: ProviderOutboundAdapter = {
|
||||
deps?.sendMSTeams ??
|
||||
((to, text) => sendMessageMSTeams({ cfg, to, text }));
|
||||
const result = await send(to, text);
|
||||
return { provider: "msteams", ...result };
|
||||
return { channel: "msteams", ...result };
|
||||
},
|
||||
sendMedia: async ({ cfg, to, text, mediaUrl, deps }) => {
|
||||
const send =
|
||||
@@ -33,7 +33,7 @@ export const msteamsOutbound: ProviderOutboundAdapter = {
|
||||
((to, text, opts) =>
|
||||
sendMessageMSTeams({ cfg, to, text, mediaUrl: opts?.mediaUrl }));
|
||||
const result = await send(to, text, { mediaUrl });
|
||||
return { provider: "msteams", ...result };
|
||||
return { channel: "msteams", ...result };
|
||||
},
|
||||
sendPoll: async ({ cfg, to, poll }) => {
|
||||
const maxSelections = poll.maxSelections ?? 1;
|
||||
@@ -1,9 +1,9 @@
|
||||
import { chunkText } from "../../../auto-reply/chunk.js";
|
||||
import { sendMessageSignal } from "../../../signal/send.js";
|
||||
import { resolveProviderMediaMaxBytes } from "../media-limits.js";
|
||||
import type { ProviderOutboundAdapter } from "../types.js";
|
||||
import { resolveChannelMediaMaxBytes } from "../media-limits.js";
|
||||
import type { ChannelOutboundAdapter } from "../types.js";
|
||||
|
||||
export const signalOutbound: ProviderOutboundAdapter = {
|
||||
export const signalOutbound: ChannelOutboundAdapter = {
|
||||
deliveryMode: "direct",
|
||||
chunker: chunkText,
|
||||
textChunkLimit: 4000,
|
||||
@@ -21,24 +21,26 @@ export const signalOutbound: ProviderOutboundAdapter = {
|
||||
},
|
||||
sendText: async ({ cfg, to, text, accountId, deps }) => {
|
||||
const send = deps?.sendSignal ?? sendMessageSignal;
|
||||
const maxBytes = resolveProviderMediaMaxBytes({
|
||||
const maxBytes = resolveChannelMediaMaxBytes({
|
||||
cfg,
|
||||
resolveProviderLimitMb: ({ cfg, accountId }) =>
|
||||
cfg.signal?.accounts?.[accountId]?.mediaMaxMb ?? cfg.signal?.mediaMaxMb,
|
||||
resolveChannelLimitMb: ({ cfg, accountId }) =>
|
||||
cfg.channels?.signal?.accounts?.[accountId]?.mediaMaxMb ??
|
||||
cfg.channels?.signal?.mediaMaxMb,
|
||||
accountId,
|
||||
});
|
||||
const result = await send(to, text, {
|
||||
maxBytes,
|
||||
accountId: accountId ?? undefined,
|
||||
});
|
||||
return { provider: "signal", ...result };
|
||||
return { channel: "signal", ...result };
|
||||
},
|
||||
sendMedia: async ({ cfg, to, text, mediaUrl, accountId, deps }) => {
|
||||
const send = deps?.sendSignal ?? sendMessageSignal;
|
||||
const maxBytes = resolveProviderMediaMaxBytes({
|
||||
const maxBytes = resolveChannelMediaMaxBytes({
|
||||
cfg,
|
||||
resolveProviderLimitMb: ({ cfg, accountId }) =>
|
||||
cfg.signal?.accounts?.[accountId]?.mediaMaxMb ?? cfg.signal?.mediaMaxMb,
|
||||
resolveChannelLimitMb: ({ cfg, accountId }) =>
|
||||
cfg.channels?.signal?.accounts?.[accountId]?.mediaMaxMb ??
|
||||
cfg.channels?.signal?.mediaMaxMb,
|
||||
accountId,
|
||||
});
|
||||
const result = await send(to, text, {
|
||||
@@ -46,6 +48,6 @@ export const signalOutbound: ProviderOutboundAdapter = {
|
||||
maxBytes,
|
||||
accountId: accountId ?? undefined,
|
||||
});
|
||||
return { provider: "signal", ...result };
|
||||
return { channel: "signal", ...result };
|
||||
},
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user