feat: load channel plugins
This commit is contained in:
@@ -165,6 +165,45 @@ Plugins export either:
|
|||||||
- A function: `(api) => { ... }`
|
- A function: `(api) => { ... }`
|
||||||
- An object: `{ id, name, configSchema, register(api) { ... } }`
|
- An object: `{ id, name, configSchema, register(api) { ... } }`
|
||||||
|
|
||||||
|
### Register a messaging channel
|
||||||
|
|
||||||
|
Plugins can register **channel plugins** that behave like built‑in channels
|
||||||
|
(WhatsApp, Telegram, etc.). Channel config lives under `channels.<id>` and is
|
||||||
|
validated by your channel plugin code.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const myChannel = {
|
||||||
|
id: "acmechat",
|
||||||
|
meta: {
|
||||||
|
id: "acmechat",
|
||||||
|
label: "AcmeChat",
|
||||||
|
selectionLabel: "AcmeChat (API)",
|
||||||
|
docsPath: "/channels/acmechat",
|
||||||
|
blurb: "demo channel plugin.",
|
||||||
|
aliases: ["acme"],
|
||||||
|
},
|
||||||
|
capabilities: { chatTypes: ["direct"] },
|
||||||
|
config: {
|
||||||
|
listAccountIds: (cfg) => Object.keys(cfg.channels?.acmechat?.accounts ?? {}),
|
||||||
|
resolveAccount: (cfg, accountId) =>
|
||||||
|
(cfg.channels?.acmechat?.accounts?.[accountId ?? "default"] ?? { accountId }),
|
||||||
|
},
|
||||||
|
outbound: {
|
||||||
|
deliveryMode: "direct",
|
||||||
|
sendText: async () => ({ ok: true }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function (api) {
|
||||||
|
api.registerChannel({ plugin: myChannel });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Put config under `channels.<id>` (not `plugins.entries`).
|
||||||
|
- `meta.label` is used for labels in CLI/UI lists.
|
||||||
|
- `meta.aliases` adds alternate ids for normalization and CLI inputs.
|
||||||
|
|
||||||
### Register a tool
|
### Register a tool
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import type { ReasoningLevel, ThinkLevel } from "../auto-reply/thinking.js";
|
import type { ReasoningLevel, ThinkLevel } from "../auto-reply/thinking.js";
|
||||||
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
|
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
|
||||||
import { CHANNEL_IDS } from "../channels/registry.js";
|
import { listDeliverableMessageChannels } from "../utils/message-channel.js";
|
||||||
import type { EmbeddedContextFile } from "./pi-embedded-helpers.js";
|
import type { EmbeddedContextFile } from "./pi-embedded-helpers.js";
|
||||||
|
|
||||||
const MESSAGE_CHANNEL_OPTIONS = CHANNEL_IDS.join("|");
|
|
||||||
|
|
||||||
export function buildAgentSystemPrompt(params: {
|
export function buildAgentSystemPrompt(params: {
|
||||||
workspaceDir: string;
|
workspaceDir: string;
|
||||||
defaultThinkLevel?: ThinkLevel;
|
defaultThinkLevel?: ThinkLevel;
|
||||||
@@ -169,6 +167,7 @@ export function buildAgentSystemPrompt(params: {
|
|||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
const runtimeCapabilitiesLower = new Set(runtimeCapabilities.map((cap) => cap.toLowerCase()));
|
const runtimeCapabilitiesLower = new Set(runtimeCapabilities.map((cap) => cap.toLowerCase()));
|
||||||
const inlineButtonsEnabled = runtimeCapabilitiesLower.has("inlinebuttons");
|
const inlineButtonsEnabled = runtimeCapabilitiesLower.has("inlinebuttons");
|
||||||
|
const messageChannelOptions = listDeliverableMessageChannels().join("|");
|
||||||
const skillsLines = skillsPrompt ? [skillsPrompt, ""] : [];
|
const skillsLines = skillsPrompt ? [skillsPrompt, ""] : [];
|
||||||
const skillsSection = skillsPrompt
|
const skillsSection = skillsPrompt
|
||||||
? [
|
? [
|
||||||
@@ -319,7 +318,7 @@ export function buildAgentSystemPrompt(params: {
|
|||||||
"### message tool",
|
"### message tool",
|
||||||
"- Use `message` for proactive sends + channel actions (polls, reactions, etc.).",
|
"- Use `message` for proactive sends + channel actions (polls, reactions, etc.).",
|
||||||
"- For `action=send`, include `to` and `message`.",
|
"- For `action=send`, include `to` and `message`.",
|
||||||
`- If multiple channels are configured, pass \`channel\` (${MESSAGE_CHANNEL_OPTIONS}).`,
|
`- If multiple channels are configured, pass \`channel\` (${messageChannelOptions}).`,
|
||||||
inlineButtonsEnabled
|
inlineButtonsEnabled
|
||||||
? "- Inline buttons supported. Use `action=send` with `buttons=[[{text,callback_data}]]` (callback_data routes back as a user message)."
|
? "- Inline buttons supported. Use `action=send` with `buttons=[[{text,callback_data}]]` (callback_data routes back as a user message)."
|
||||||
: runtimeChannel
|
: runtimeChannel
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ChannelDock } from "../channels/dock.js";
|
import type { ChannelDock } from "../channels/dock.js";
|
||||||
import { getChannelDock, listChannelDocks } from "../channels/dock.js";
|
import { getChannelDock, listChannelDocks } from "../channels/dock.js";
|
||||||
import type { ChannelId } from "../channels/plugins/types.js";
|
import type { ChannelId } from "../channels/plugins/types.js";
|
||||||
import { normalizeChannelId } from "../channels/registry.js";
|
import { normalizeChannelId } from "../channels/plugins/index.js";
|
||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
import type { MsgContext } from "./templating.js";
|
import type { MsgContext } from "./templating.js";
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { NormalizedUsage } from "../../agents/usage.js";
|
import type { NormalizedUsage } from "../../agents/usage.js";
|
||||||
import { getChannelDock } from "../../channels/dock.js";
|
import { getChannelDock } from "../../channels/dock.js";
|
||||||
import type { ChannelThreadingToolContext } from "../../channels/plugins/types.js";
|
import type { ChannelThreadingToolContext } from "../../channels/plugins/types.js";
|
||||||
import { normalizeChannelId } from "../../channels/registry.js";
|
import { normalizeChannelId } from "../../channels/plugins/index.js";
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import { isReasoningTagProvider } from "../../utils/provider-utils.js";
|
import { isReasoningTagProvider } from "../../utils/provider-utils.js";
|
||||||
import { estimateUsageCost, formatTokenCount, formatUsd } from "../../utils/usage-format.js";
|
import { estimateUsageCost, formatTokenCount, formatUsd } from "../../utils/usage-format.js";
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
import { getChannelDock } from "../../channels/dock.js";
|
import { getChannelDock } from "../../channels/dock.js";
|
||||||
import { CHANNEL_IDS, normalizeChannelId } from "../../channels/registry.js";
|
import { normalizeChannelId } from "../../channels/plugins/index.js";
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import type { BlockStreamingCoalesceConfig } from "../../config/types.js";
|
import type { BlockStreamingCoalesceConfig } from "../../config/types.js";
|
||||||
import { normalizeAccountId } from "../../routing/session-key.js";
|
import { normalizeAccountId } from "../../routing/session-key.js";
|
||||||
import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
|
import {
|
||||||
|
INTERNAL_MESSAGE_CHANNEL,
|
||||||
|
listDeliverableMessageChannels,
|
||||||
|
} from "../../utils/message-channel.js";
|
||||||
import { resolveTextChunkLimit, type TextChunkProvider } from "../chunk.js";
|
import { resolveTextChunkLimit, type TextChunkProvider } from "../chunk.js";
|
||||||
|
|
||||||
const DEFAULT_BLOCK_STREAM_MIN = 800;
|
const DEFAULT_BLOCK_STREAM_MIN = 800;
|
||||||
const DEFAULT_BLOCK_STREAM_MAX = 1200;
|
const DEFAULT_BLOCK_STREAM_MAX = 1200;
|
||||||
const DEFAULT_BLOCK_STREAM_COALESCE_IDLE_MS = 1000;
|
const DEFAULT_BLOCK_STREAM_COALESCE_IDLE_MS = 1000;
|
||||||
const BLOCK_CHUNK_PROVIDERS = new Set<TextChunkProvider>([
|
const getBlockChunkProviders = () =>
|
||||||
...CHANNEL_IDS,
|
new Set<TextChunkProvider>([...listDeliverableMessageChannels(), INTERNAL_MESSAGE_CHANNEL]);
|
||||||
INTERNAL_MESSAGE_CHANNEL,
|
|
||||||
]);
|
|
||||||
|
|
||||||
function normalizeChunkProvider(provider?: string): TextChunkProvider | undefined {
|
function normalizeChunkProvider(provider?: string): TextChunkProvider | undefined {
|
||||||
if (!provider) return undefined;
|
if (!provider) return undefined;
|
||||||
const cleaned = provider.trim().toLowerCase();
|
const cleaned = provider.trim().toLowerCase();
|
||||||
return BLOCK_CHUNK_PROVIDERS.has(cleaned as TextChunkProvider)
|
return getBlockChunkProviders().has(cleaned as TextChunkProvider)
|
||||||
? (cleaned as TextChunkProvider)
|
? (cleaned as TextChunkProvider)
|
||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getChannelDock } from "../../channels/dock.js";
|
import { getChannelDock } from "../../channels/dock.js";
|
||||||
import { getChatChannelMeta, normalizeChannelId } from "../../channels/registry.js";
|
import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js";
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import type { GroupKeyResolution, SessionEntry } from "../../config/sessions.js";
|
import type { GroupKeyResolution, SessionEntry } from "../../config/sessions.js";
|
||||||
import { isInternalMessageChannel } from "../../utils/message-channel.js";
|
import { isInternalMessageChannel } from "../../utils/message-channel.js";
|
||||||
@@ -50,7 +50,7 @@ export function buildGroupIntro(params: {
|
|||||||
const providerLabel = (() => {
|
const providerLabel = (() => {
|
||||||
if (!providerKey) return "chat";
|
if (!providerKey) return "chat";
|
||||||
if (isInternalMessageChannel(providerKey)) return "WebChat";
|
if (isInternalMessageChannel(providerKey)) return "WebChat";
|
||||||
if (providerId) return getChatChannelMeta(providerId).label;
|
if (providerId) return getChannelPlugin(providerId)?.meta.label ?? providerId;
|
||||||
return `${providerKey.at(0)?.toUpperCase() ?? ""}${providerKey.slice(1)}`;
|
return `${providerKey.at(0)?.toUpperCase() ?? ""}${providerKey.slice(1)}`;
|
||||||
})();
|
})();
|
||||||
const subjectLine = subject
|
const subjectLine = subject
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { resolveAgentConfig } from "../../agents/agent-scope.js";
|
import { resolveAgentConfig } from "../../agents/agent-scope.js";
|
||||||
import { getChannelDock } from "../../channels/dock.js";
|
import { getChannelDock } from "../../channels/dock.js";
|
||||||
import { normalizeChannelId } from "../../channels/registry.js";
|
import { normalizeChannelId } from "../../channels/plugins/index.js";
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import type { MsgContext } from "../templating.js";
|
import type { MsgContext } from "../templating.js";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { resolveAgentConfig } from "../../agents/agent-scope.js";
|
import { resolveAgentConfig } from "../../agents/agent-scope.js";
|
||||||
import { getChannelDock } from "../../channels/dock.js";
|
import { getChannelDock } from "../../channels/dock.js";
|
||||||
import { CHAT_CHANNEL_ORDER, normalizeChannelId } from "../../channels/registry.js";
|
import { normalizeChannelId } from "../../channels/plugins/index.js";
|
||||||
|
import { CHAT_CHANNEL_ORDER } from "../../channels/registry.js";
|
||||||
import type { AgentElevatedAllowFromConfig, ClawdbotConfig } from "../../config/config.js";
|
import type { AgentElevatedAllowFromConfig, ClawdbotConfig } from "../../config/config.js";
|
||||||
import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
|
import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
|
||||||
import type { MsgContext } from "../templating.js";
|
import type { MsgContext } from "../templating.js";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getChannelDock } from "../../channels/dock.js";
|
import { getChannelDock } from "../../channels/dock.js";
|
||||||
import { normalizeChannelId } from "../../channels/registry.js";
|
import { normalizeChannelId } from "../../channels/plugins/index.js";
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import type { ReplyToMode } from "../../config/types.js";
|
import type { ReplyToMode } from "../../config/types.js";
|
||||||
import type { OriginatingChannelType } from "../templating.js";
|
import type { OriginatingChannelType } from "../templating.js";
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
import { resolveSessionAgentId } from "../../agents/agent-scope.js";
|
import { resolveSessionAgentId } from "../../agents/agent-scope.js";
|
||||||
import { resolveEffectiveMessagesConfig } from "../../agents/identity.js";
|
import { resolveEffectiveMessagesConfig } from "../../agents/identity.js";
|
||||||
import { normalizeChannelId } from "../../channels/registry.js";
|
import { normalizeChannelId } from "../../channels/plugins/index.js";
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
|
import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
|
||||||
import type { OriginatingChannelType } from "../templating.js";
|
import type { OriginatingChannelType } from "../templating.js";
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import path from "node:path";
|
|||||||
import { CURRENT_SESSION_VERSION, SessionManager } from "@mariozechner/pi-coding-agent";
|
import { CURRENT_SESSION_VERSION, SessionManager } from "@mariozechner/pi-coding-agent";
|
||||||
import { resolveSessionAgentId } from "../../agents/agent-scope.js";
|
import { resolveSessionAgentId } from "../../agents/agent-scope.js";
|
||||||
import { getChannelDock } from "../../channels/dock.js";
|
import { getChannelDock } from "../../channels/dock.js";
|
||||||
import { normalizeChannelId } from "../../channels/registry.js";
|
import { normalizeChannelId } from "../../channels/plugins/index.js";
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import {
|
import {
|
||||||
buildGroupDisplayName,
|
buildGroupDisplayName,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { resolveTelegramAccount } from "../telegram/accounts.js";
|
|||||||
import { normalizeE164 } from "../utils.js";
|
import { normalizeE164 } from "../utils.js";
|
||||||
import { resolveWhatsAppAccount } from "../web/accounts.js";
|
import { resolveWhatsAppAccount } from "../web/accounts.js";
|
||||||
import { normalizeWhatsAppTarget } from "../whatsapp/normalize.js";
|
import { normalizeWhatsAppTarget } from "../whatsapp/normalize.js";
|
||||||
|
import { getActivePluginRegistry } from "../plugins/runtime.js";
|
||||||
import {
|
import {
|
||||||
resolveDiscordGroupRequireMention,
|
resolveDiscordGroupRequireMention,
|
||||||
resolveIMessageGroupRequireMention,
|
resolveIMessageGroupRequireMention,
|
||||||
@@ -21,9 +22,10 @@ import type {
|
|||||||
ChannelGroupAdapter,
|
ChannelGroupAdapter,
|
||||||
ChannelId,
|
ChannelId,
|
||||||
ChannelMentionAdapter,
|
ChannelMentionAdapter,
|
||||||
|
ChannelPlugin,
|
||||||
ChannelThreadingAdapter,
|
ChannelThreadingAdapter,
|
||||||
} from "./plugins/types.js";
|
} from "./plugins/types.js";
|
||||||
import { CHAT_CHANNEL_ORDER } from "./registry.js";
|
import { CHAT_CHANNEL_ORDER, type ChatChannelId, getChatChannelMeta } from "./registry.js";
|
||||||
|
|
||||||
export type ChannelDock = {
|
export type ChannelDock = {
|
||||||
id: ChannelId;
|
id: ChannelId;
|
||||||
@@ -75,7 +77,7 @@ const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\
|
|||||||
// Adding a channel:
|
// Adding a channel:
|
||||||
// - add a new entry to `DOCKS`
|
// - add a new entry to `DOCKS`
|
||||||
// - keep it cheap; push heavy logic into `src/channels/plugins/<id>.ts` or channel modules
|
// - keep it cheap; push heavy logic into `src/channels/plugins/<id>.ts` or channel modules
|
||||||
const DOCKS: Record<ChannelId, ChannelDock> = {
|
const DOCKS: Record<ChatChannelId, ChannelDock> = {
|
||||||
telegram: {
|
telegram: {
|
||||||
id: "telegram",
|
id: "telegram",
|
||||||
capabilities: {
|
capabilities: {
|
||||||
@@ -311,10 +313,71 @@ const DOCKS: Record<ChannelId, ChannelDock> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function buildDockFromPlugin(plugin: ChannelPlugin): ChannelDock {
|
||||||
|
return {
|
||||||
|
id: plugin.id,
|
||||||
|
capabilities: plugin.capabilities,
|
||||||
|
commands: plugin.commands,
|
||||||
|
outbound: plugin.outbound?.textChunkLimit
|
||||||
|
? { textChunkLimit: plugin.outbound.textChunkLimit }
|
||||||
|
: undefined,
|
||||||
|
streaming: plugin.streaming
|
||||||
|
? { blockStreamingCoalesceDefaults: plugin.streaming.blockStreamingCoalesceDefaults }
|
||||||
|
: undefined,
|
||||||
|
elevated: plugin.elevated,
|
||||||
|
config: plugin.config
|
||||||
|
? {
|
||||||
|
resolveAllowFrom: plugin.config.resolveAllowFrom,
|
||||||
|
formatAllowFrom: plugin.config.formatAllowFrom,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
groups: plugin.groups,
|
||||||
|
mentions: plugin.mentions,
|
||||||
|
threading: plugin.threading,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function listPluginDockEntries(): Array<{ id: ChannelId; dock: ChannelDock; order?: number }> {
|
||||||
|
const registry = getActivePluginRegistry();
|
||||||
|
if (!registry) return [];
|
||||||
|
const entries: Array<{ id: ChannelId; dock: ChannelDock; order?: number }> = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
for (const entry of registry.channels) {
|
||||||
|
const plugin = entry.plugin;
|
||||||
|
const id = String(plugin.id).trim();
|
||||||
|
if (!id || seen.has(id)) continue;
|
||||||
|
seen.add(id);
|
||||||
|
if (CHAT_CHANNEL_ORDER.includes(plugin.id as ChatChannelId)) continue;
|
||||||
|
const dock = entry.dock ?? buildDockFromPlugin(plugin);
|
||||||
|
entries.push({ id: plugin.id, dock, order: plugin.meta.order });
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
export function listChannelDocks(): ChannelDock[] {
|
export function listChannelDocks(): ChannelDock[] {
|
||||||
return CHAT_CHANNEL_ORDER.map((id) => DOCKS[id]);
|
const baseEntries = CHAT_CHANNEL_ORDER.map((id) => ({
|
||||||
|
id,
|
||||||
|
dock: DOCKS[id],
|
||||||
|
order: getChatChannelMeta(id).order,
|
||||||
|
}));
|
||||||
|
const pluginEntries = listPluginDockEntries();
|
||||||
|
const combined = [...baseEntries, ...pluginEntries];
|
||||||
|
combined.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.order ?? (indexA === -1 ? 999 : indexA);
|
||||||
|
const orderB = b.order ?? (indexB === -1 ? 999 : indexB);
|
||||||
|
if (orderA !== orderB) return orderA - orderB;
|
||||||
|
return String(a.id).localeCompare(String(b.id));
|
||||||
|
});
|
||||||
|
return combined.map((entry) => entry.dock);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getChannelDock(id: ChannelId): ChannelDock | undefined {
|
export function getChannelDock(id: ChannelId): ChannelDock | undefined {
|
||||||
return DOCKS[id];
|
const core = DOCKS[id as ChatChannelId];
|
||||||
|
if (core) return core;
|
||||||
|
const registry = getActivePluginRegistry();
|
||||||
|
const pluginEntry = registry?.channels.find((entry) => entry.plugin.id === id);
|
||||||
|
if (!pluginEntry) return undefined;
|
||||||
|
return pluginEntry.dock ?? buildDockFromPlugin(pluginEntry.plugin);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,10 @@ import { CHANNEL_IDS } from "../registry.js";
|
|||||||
import { listChannelPlugins } from "./index.js";
|
import { listChannelPlugins } from "./index.js";
|
||||||
|
|
||||||
describe("channel plugin registry", () => {
|
describe("channel plugin registry", () => {
|
||||||
it("stays in sync with channel ids", () => {
|
it("includes the built-in channel ids", () => {
|
||||||
const pluginIds = listChannelPlugins()
|
const pluginIds = listChannelPlugins().map((plugin) => plugin.id);
|
||||||
.map((plugin) => plugin.id)
|
for (const id of CHANNEL_IDS) {
|
||||||
.slice()
|
expect(pluginIds).toContain(id);
|
||||||
.sort();
|
}
|
||||||
const channelIds = [...CHANNEL_IDS].slice().sort();
|
|
||||||
expect(pluginIds).toEqual(channelIds);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { slackPlugin } from "./slack.js";
|
|||||||
import { telegramPlugin } from "./telegram.js";
|
import { telegramPlugin } from "./telegram.js";
|
||||||
import type { ChannelId, ChannelPlugin } from "./types.js";
|
import type { ChannelId, ChannelPlugin } from "./types.js";
|
||||||
import { whatsappPlugin } from "./whatsapp.js";
|
import { whatsappPlugin } from "./whatsapp.js";
|
||||||
|
import { getActivePluginRegistry } from "../../plugins/runtime.js";
|
||||||
|
|
||||||
// Channel plugins registry (runtime).
|
// Channel plugins registry (runtime).
|
||||||
//
|
//
|
||||||
@@ -18,7 +19,7 @@ import { whatsappPlugin } from "./whatsapp.js";
|
|||||||
// - add `<id>Plugin` import + entry in `resolveChannels()`
|
// - add `<id>Plugin` import + entry in `resolveChannels()`
|
||||||
// - add an entry to `src/channels/dock.ts` for shared behavior (capabilities, allowFrom, threading, …)
|
// - add an entry to `src/channels/dock.ts` for shared behavior (capabilities, allowFrom, threading, …)
|
||||||
// - add ids/aliases in `src/channels/registry.ts`
|
// - add ids/aliases in `src/channels/registry.ts`
|
||||||
function resolveChannels(): ChannelPlugin[] {
|
function resolveCoreChannels(): ChannelPlugin[] {
|
||||||
return [
|
return [
|
||||||
telegramPlugin,
|
telegramPlugin,
|
||||||
whatsappPlugin,
|
whatsappPlugin,
|
||||||
@@ -30,8 +31,27 @@ function resolveChannels(): ChannelPlugin[] {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function listPluginChannels(): ChannelPlugin[] {
|
||||||
|
const registry = getActivePluginRegistry();
|
||||||
|
if (!registry) return [];
|
||||||
|
return registry.channels.map((entry) => entry.plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dedupeChannels(channels: ChannelPlugin[]): ChannelPlugin[] {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const resolved: ChannelPlugin[] = [];
|
||||||
|
for (const plugin of channels) {
|
||||||
|
const id = String(plugin.id).trim();
|
||||||
|
if (!id || seen.has(id)) continue;
|
||||||
|
seen.add(id);
|
||||||
|
resolved.push(plugin);
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
export function listChannelPlugins(): ChannelPlugin[] {
|
export function listChannelPlugins(): ChannelPlugin[] {
|
||||||
return resolveChannels().sort((a, b) => {
|
const combined = dedupeChannels([...resolveCoreChannels(), ...listPluginChannels()]);
|
||||||
|
return combined.sort((a, b) => {
|
||||||
const indexA = CHAT_CHANNEL_ORDER.indexOf(a.id as ChatChannelId);
|
const indexA = CHAT_CHANNEL_ORDER.indexOf(a.id as ChatChannelId);
|
||||||
const indexB = CHAT_CHANNEL_ORDER.indexOf(b.id as ChatChannelId);
|
const indexB = CHAT_CHANNEL_ORDER.indexOf(b.id as ChatChannelId);
|
||||||
const orderA = a.meta.order ?? (indexA === -1 ? 999 : indexA);
|
const orderA = a.meta.order ?? (indexA === -1 ? 999 : indexA);
|
||||||
@@ -42,13 +62,24 @@ export function listChannelPlugins(): ChannelPlugin[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getChannelPlugin(id: ChannelId): ChannelPlugin | undefined {
|
export function getChannelPlugin(id: ChannelId): ChannelPlugin | undefined {
|
||||||
return resolveChannels().find((plugin) => plugin.id === id);
|
const resolvedId = String(id).trim();
|
||||||
|
if (!resolvedId) return undefined;
|
||||||
|
return listChannelPlugins().find((plugin) => plugin.id === resolvedId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeChannelId(raw?: string | null): ChannelId | null {
|
export function normalizeChannelId(raw?: string | null): ChannelId | null {
|
||||||
// Channel docking: keep input normalization centralized in src/channels/registry.ts
|
// 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.
|
// so CLI/API/protocol can rely on stable aliases without plugin init side effects.
|
||||||
return normalizeChatChannelId(raw);
|
const normalized = normalizeChatChannelId(raw);
|
||||||
|
if (normalized) return normalized;
|
||||||
|
const trimmed = raw?.trim();
|
||||||
|
if (!trimmed) return null;
|
||||||
|
const key = trimmed.toLowerCase();
|
||||||
|
const plugin = listChannelPlugins().find((entry) => {
|
||||||
|
if (entry.id.toLowerCase() === key) return true;
|
||||||
|
return (entry.meta.aliases ?? []).some((alias) => alias.trim().toLowerCase() === key);
|
||||||
|
});
|
||||||
|
return plugin?.id ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import type { ChannelId, ChannelPlugin } from "./types.js";
|
import type { ChannelId, ChannelPlugin } from "./types.js";
|
||||||
|
import type { ChatChannelId } from "../registry.js";
|
||||||
|
import { getActivePluginRegistry } from "../../plugins/runtime.js";
|
||||||
|
|
||||||
type PluginLoader = () => Promise<ChannelPlugin>;
|
type PluginLoader = () => Promise<ChannelPlugin>;
|
||||||
|
|
||||||
@@ -6,7 +8,7 @@ type PluginLoader = () => Promise<ChannelPlugin>;
|
|||||||
//
|
//
|
||||||
// This avoids importing `src/channels/plugins/index.ts` (intentionally heavy)
|
// This avoids importing `src/channels/plugins/index.ts` (intentionally heavy)
|
||||||
// from shared flows like outbound delivery / followup routing.
|
// from shared flows like outbound delivery / followup routing.
|
||||||
const LOADERS: Record<ChannelId, PluginLoader> = {
|
const LOADERS: Record<ChatChannelId, PluginLoader> = {
|
||||||
telegram: async () => (await import("./telegram.js")).telegramPlugin,
|
telegram: async () => (await import("./telegram.js")).telegramPlugin,
|
||||||
whatsapp: async () => (await import("./whatsapp.js")).whatsappPlugin,
|
whatsapp: async () => (await import("./whatsapp.js")).whatsappPlugin,
|
||||||
discord: async () => (await import("./discord.js")).discordPlugin,
|
discord: async () => (await import("./discord.js")).discordPlugin,
|
||||||
@@ -21,7 +23,13 @@ const cache = new Map<ChannelId, ChannelPlugin>();
|
|||||||
export async function loadChannelPlugin(id: ChannelId): Promise<ChannelPlugin | undefined> {
|
export async function loadChannelPlugin(id: ChannelId): Promise<ChannelPlugin | undefined> {
|
||||||
const cached = cache.get(id);
|
const cached = cache.get(id);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
const loader = LOADERS[id];
|
const registry = getActivePluginRegistry();
|
||||||
|
const pluginEntry = registry?.channels.find((entry) => entry.plugin.id === id);
|
||||||
|
if (pluginEntry) {
|
||||||
|
cache.set(id, pluginEntry.plugin);
|
||||||
|
return pluginEntry.plugin;
|
||||||
|
}
|
||||||
|
const loader = LOADERS[id as ChatChannelId];
|
||||||
if (!loader) return undefined;
|
if (!loader) return undefined;
|
||||||
const plugin = await loader();
|
const plugin = await loader();
|
||||||
cache.set(id, plugin);
|
cache.set(id, plugin);
|
||||||
|
|||||||
@@ -2,23 +2,23 @@ import type { ClawdbotConfig } from "../../config/config.js";
|
|||||||
import type { DmPolicy } from "../../config/types.js";
|
import type { DmPolicy } from "../../config/types.js";
|
||||||
import type { RuntimeEnv } from "../../runtime.js";
|
import type { RuntimeEnv } from "../../runtime.js";
|
||||||
import type { WizardPrompter } from "../../wizard/prompts.js";
|
import type { WizardPrompter } from "../../wizard/prompts.js";
|
||||||
import type { ChatChannelId } from "../registry.js";
|
import type { ChannelId } from "./types.js";
|
||||||
|
|
||||||
export type SetupChannelsOptions = {
|
export type SetupChannelsOptions = {
|
||||||
allowDisable?: boolean;
|
allowDisable?: boolean;
|
||||||
allowSignalInstall?: boolean;
|
allowSignalInstall?: boolean;
|
||||||
onSelection?: (selection: ChatChannelId[]) => void;
|
onSelection?: (selection: ChannelId[]) => void;
|
||||||
accountIds?: Partial<Record<ChatChannelId, string>>;
|
accountIds?: Partial<Record<ChannelId, string>>;
|
||||||
onAccountId?: (channel: ChatChannelId, accountId: string) => void;
|
onAccountId?: (channel: ChannelId, accountId: string) => void;
|
||||||
promptAccountIds?: boolean;
|
promptAccountIds?: boolean;
|
||||||
whatsappAccountId?: string;
|
whatsappAccountId?: string;
|
||||||
promptWhatsAppAccountId?: boolean;
|
promptWhatsAppAccountId?: boolean;
|
||||||
onWhatsAppAccountId?: (accountId: string) => void;
|
onWhatsAppAccountId?: (accountId: string) => void;
|
||||||
forceAllowFromChannels?: ChatChannelId[];
|
forceAllowFromChannels?: ChannelId[];
|
||||||
skipDmPolicyPrompt?: boolean;
|
skipDmPolicyPrompt?: boolean;
|
||||||
skipConfirm?: boolean;
|
skipConfirm?: boolean;
|
||||||
quickstartDefaults?: boolean;
|
quickstartDefaults?: boolean;
|
||||||
initialSelection?: ChatChannelId[];
|
initialSelection?: ChannelId[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PromptAccountIdParams = {
|
export type PromptAccountIdParams = {
|
||||||
@@ -33,7 +33,7 @@ export type PromptAccountIdParams = {
|
|||||||
export type PromptAccountId = (params: PromptAccountIdParams) => Promise<string>;
|
export type PromptAccountId = (params: PromptAccountIdParams) => Promise<string>;
|
||||||
|
|
||||||
export type ChannelOnboardingStatus = {
|
export type ChannelOnboardingStatus = {
|
||||||
channel: ChatChannelId;
|
channel: ChannelId;
|
||||||
configured: boolean;
|
configured: boolean;
|
||||||
statusLines: string[];
|
statusLines: string[];
|
||||||
selectionHint?: string;
|
selectionHint?: string;
|
||||||
@@ -43,7 +43,7 @@ export type ChannelOnboardingStatus = {
|
|||||||
export type ChannelOnboardingStatusContext = {
|
export type ChannelOnboardingStatusContext = {
|
||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
options?: SetupChannelsOptions;
|
options?: SetupChannelsOptions;
|
||||||
accountOverrides: Partial<Record<ChatChannelId, string>>;
|
accountOverrides: Partial<Record<ChannelId, string>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ChannelOnboardingConfigureContext = {
|
export type ChannelOnboardingConfigureContext = {
|
||||||
@@ -51,7 +51,7 @@ export type ChannelOnboardingConfigureContext = {
|
|||||||
runtime: RuntimeEnv;
|
runtime: RuntimeEnv;
|
||||||
prompter: WizardPrompter;
|
prompter: WizardPrompter;
|
||||||
options?: SetupChannelsOptions;
|
options?: SetupChannelsOptions;
|
||||||
accountOverrides: Partial<Record<ChatChannelId, string>>;
|
accountOverrides: Partial<Record<ChannelId, string>>;
|
||||||
shouldPromptAccountIds: boolean;
|
shouldPromptAccountIds: boolean;
|
||||||
forceAllowFrom: boolean;
|
forceAllowFrom: boolean;
|
||||||
};
|
};
|
||||||
@@ -63,7 +63,7 @@ export type ChannelOnboardingResult = {
|
|||||||
|
|
||||||
export type ChannelOnboardingDmPolicy = {
|
export type ChannelOnboardingDmPolicy = {
|
||||||
label: string;
|
label: string;
|
||||||
channel: ChatChannelId;
|
channel: ChannelId;
|
||||||
policyKey: string;
|
policyKey: string;
|
||||||
allowFromKey: string;
|
allowFromKey: string;
|
||||||
getCurrent: (cfg: ClawdbotConfig) => DmPolicy;
|
getCurrent: (cfg: ClawdbotConfig) => DmPolicy;
|
||||||
@@ -71,7 +71,7 @@ export type ChannelOnboardingDmPolicy = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type ChannelOnboardingAdapter = {
|
export type ChannelOnboardingAdapter = {
|
||||||
channel: ChatChannelId;
|
channel: ChannelId;
|
||||||
getStatus: (ctx: ChannelOnboardingStatusContext) => Promise<ChannelOnboardingStatus>;
|
getStatus: (ctx: ChannelOnboardingStatusContext) => Promise<ChannelOnboardingStatus>;
|
||||||
configure: (ctx: ChannelOnboardingConfigureContext) => Promise<ChannelOnboardingResult>;
|
configure: (ctx: ChannelOnboardingConfigureContext) => Promise<ChannelOnboardingResult>;
|
||||||
dmPolicy?: ChannelOnboardingDmPolicy;
|
dmPolicy?: ChannelOnboardingDmPolicy;
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import type { ChannelId, ChannelOutboundAdapter } from "../types.js";
|
import type { ChannelId, ChannelOutboundAdapter } from "../types.js";
|
||||||
|
import type { ChatChannelId } from "../../registry.js";
|
||||||
|
import { getActivePluginRegistry } from "../../../plugins/runtime.js";
|
||||||
|
|
||||||
type OutboundLoader = () => Promise<ChannelOutboundAdapter>;
|
type OutboundLoader = () => Promise<ChannelOutboundAdapter>;
|
||||||
|
|
||||||
@@ -7,7 +9,7 @@ type OutboundLoader = () => Promise<ChannelOutboundAdapter>;
|
|||||||
// The full channel plugins (src/channels/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 +
|
// onboarding, gateway monitors, etc. Outbound delivery only needs chunking +
|
||||||
// send primitives, so we keep a dedicated, lightweight loader here.
|
// send primitives, so we keep a dedicated, lightweight loader here.
|
||||||
const LOADERS: Record<ChannelId, OutboundLoader> = {
|
const LOADERS: Record<ChatChannelId, OutboundLoader> = {
|
||||||
telegram: async () => (await import("./telegram.js")).telegramOutbound,
|
telegram: async () => (await import("./telegram.js")).telegramOutbound,
|
||||||
whatsapp: async () => (await import("./whatsapp.js")).whatsappOutbound,
|
whatsapp: async () => (await import("./whatsapp.js")).whatsappOutbound,
|
||||||
discord: async () => (await import("./discord.js")).discordOutbound,
|
discord: async () => (await import("./discord.js")).discordOutbound,
|
||||||
@@ -24,9 +26,16 @@ export async function loadChannelOutboundAdapter(
|
|||||||
): Promise<ChannelOutboundAdapter | undefined> {
|
): Promise<ChannelOutboundAdapter | undefined> {
|
||||||
const cached = cache.get(id);
|
const cached = cache.get(id);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
const loader = LOADERS[id];
|
const registry = getActivePluginRegistry();
|
||||||
|
const pluginEntry = registry?.channels.find((entry) => entry.plugin.id === id);
|
||||||
|
const outbound = pluginEntry?.plugin.outbound;
|
||||||
|
if (outbound) {
|
||||||
|
cache.set(id, outbound);
|
||||||
|
return outbound;
|
||||||
|
}
|
||||||
|
const loader = LOADERS[id as ChatChannelId];
|
||||||
if (!loader) return undefined;
|
if (!loader) return undefined;
|
||||||
const outbound = await loader();
|
const loaded = await loader();
|
||||||
cache.set(id, outbound);
|
cache.set(id, loaded);
|
||||||
return outbound;
|
return loaded;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type { GatewayClientMode, GatewayClientName } from "../../utils/message-c
|
|||||||
import type { ChatChannelId } from "../registry.js";
|
import type { ChatChannelId } from "../registry.js";
|
||||||
import type { ChannelMessageActionName as ChannelMessageActionNameFromList } from "./message-action-names.js";
|
import type { ChannelMessageActionName as ChannelMessageActionNameFromList } from "./message-action-names.js";
|
||||||
|
|
||||||
export type ChannelId = ChatChannelId;
|
export type ChannelId = ChatChannelId | (string & {});
|
||||||
|
|
||||||
export type ChannelOutboundTargetMode = "explicit" | "implicit" | "heartbeat";
|
export type ChannelOutboundTargetMode = "explicit" | "implicit" | "heartbeat";
|
||||||
|
|
||||||
@@ -62,6 +62,10 @@ export type ChannelMeta = {
|
|||||||
docsLabel?: string;
|
docsLabel?: string;
|
||||||
blurb: string;
|
blurb: string;
|
||||||
order?: number;
|
order?: number;
|
||||||
|
aliases?: string[];
|
||||||
|
selectionDocsPrefix?: string;
|
||||||
|
selectionDocsOmitLabel?: boolean;
|
||||||
|
selectionExtras?: string[];
|
||||||
showConfigured?: boolean;
|
showConfigured?: boolean;
|
||||||
quickstartAllowFrom?: boolean;
|
quickstartAllowFrom?: boolean;
|
||||||
forceAccountBinding?: boolean;
|
forceAccountBinding?: boolean;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { ChannelMeta } from "./plugins/types.js";
|
||||||
|
|
||||||
// Channel docking: add new channels here (order + meta + aliases), then
|
// Channel docking: add new channels here (order + meta + aliases), then
|
||||||
// register the plugin in src/channels/plugins/index.ts and keep protocol IDs in sync.
|
// register the plugin in src/channels/plugins/index.ts and keep protocol IDs in sync.
|
||||||
export const CHAT_CHANNEL_ORDER = [
|
export const CHAT_CHANNEL_ORDER = [
|
||||||
@@ -16,23 +18,11 @@ export const CHANNEL_IDS = [...CHAT_CHANNEL_ORDER] as const;
|
|||||||
|
|
||||||
export const DEFAULT_CHAT_CHANNEL: ChatChannelId = "whatsapp";
|
export const DEFAULT_CHAT_CHANNEL: ChatChannelId = "whatsapp";
|
||||||
|
|
||||||
export type ChatChannelMeta = {
|
export type ChatChannelMeta = ChannelMeta;
|
||||||
id: ChatChannelId;
|
|
||||||
label: string;
|
|
||||||
selectionLabel: string;
|
|
||||||
docsPath: string;
|
|
||||||
docsLabel?: string;
|
|
||||||
blurb: string;
|
|
||||||
// Channel docking: selection-line formatting for onboarding prompts.
|
|
||||||
// Keep this data-driven to avoid channel-specific branches in shared code.
|
|
||||||
selectionDocsPrefix?: string;
|
|
||||||
selectionDocsOmitLabel?: boolean;
|
|
||||||
selectionExtras?: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const WEBSITE_URL = "https://clawd.bot";
|
const WEBSITE_URL = "https://clawd.bot";
|
||||||
|
|
||||||
const CHAT_CHANNEL_META: Record<ChatChannelId, ChatChannelMeta> = {
|
const CHAT_CHANNEL_META: Record<ChatChannelId, ChannelMeta> = {
|
||||||
telegram: {
|
telegram: {
|
||||||
id: "telegram",
|
id: "telegram",
|
||||||
label: "Telegram",
|
label: "Telegram",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Command } from "commander";
|
import type { Command } from "commander";
|
||||||
import { listChatChannels } from "../channels/registry.js";
|
import { listChannelPlugins } from "../channels/plugins/index.js";
|
||||||
import {
|
import {
|
||||||
channelsAddCommand,
|
channelsAddCommand,
|
||||||
channelsListCommand,
|
channelsListCommand,
|
||||||
@@ -36,11 +36,10 @@ const optionNamesAdd = [
|
|||||||
|
|
||||||
const optionNamesRemove = ["channel", "account", "delete"] as const;
|
const optionNamesRemove = ["channel", "account", "delete"] as const;
|
||||||
|
|
||||||
const channelNames = listChatChannels()
|
|
||||||
.map((meta) => meta.id)
|
|
||||||
.join("|");
|
|
||||||
|
|
||||||
export function registerChannelsCli(program: Command) {
|
export function registerChannelsCli(program: Command) {
|
||||||
|
const channelNames = listChannelPlugins()
|
||||||
|
.map((plugin) => plugin.id)
|
||||||
|
.join("|");
|
||||||
const channels = program
|
const channels = program
|
||||||
.command("channels")
|
.command("channels")
|
||||||
.description("Manage chat channel accounts")
|
.description("Manage chat channel accounts")
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type { GatewayRpcOpts } from "../gateway-rpc.js";
|
|||||||
import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js";
|
import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js";
|
||||||
import { parsePositiveIntOrUndefined } from "../program/helpers.js";
|
import { parsePositiveIntOrUndefined } from "../program/helpers.js";
|
||||||
import {
|
import {
|
||||||
CRON_CHANNEL_OPTIONS,
|
getCronChannelOptions,
|
||||||
parseAtMs,
|
parseAtMs,
|
||||||
parseDurationMs,
|
parseDurationMs,
|
||||||
printCronList,
|
printCronList,
|
||||||
@@ -81,7 +81,7 @@ export function registerCronAddCommand(cron: Command) {
|
|||||||
.option("--model <model>", "Model override for agent jobs (provider/model or alias)")
|
.option("--model <model>", "Model override for agent jobs (provider/model or alias)")
|
||||||
.option("--timeout-seconds <n>", "Timeout seconds for agent jobs")
|
.option("--timeout-seconds <n>", "Timeout seconds for agent jobs")
|
||||||
.option("--deliver", "Deliver agent output", false)
|
.option("--deliver", "Deliver agent output", false)
|
||||||
.option("--channel <channel>", `Delivery channel (${CRON_CHANNEL_OPTIONS})`, "last")
|
.option("--channel <channel>", `Delivery channel (${getCronChannelOptions()})`, "last")
|
||||||
.option(
|
.option(
|
||||||
"--to <dest>",
|
"--to <dest>",
|
||||||
"Delivery destination (E.164, Telegram chatId, or Discord channel/user)",
|
"Delivery destination (E.164, Telegram chatId, or Discord channel/user)",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { normalizeAgentId } from "../../routing/session-key.js";
|
|||||||
import { defaultRuntime } from "../../runtime.js";
|
import { defaultRuntime } from "../../runtime.js";
|
||||||
import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js";
|
import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js";
|
||||||
import {
|
import {
|
||||||
CRON_CHANNEL_OPTIONS,
|
getCronChannelOptions,
|
||||||
parseAtMs,
|
parseAtMs,
|
||||||
parseDurationMs,
|
parseDurationMs,
|
||||||
warnIfCronSchedulerDisabled,
|
warnIfCronSchedulerDisabled,
|
||||||
@@ -36,7 +36,7 @@ export function registerCronEditCommand(cron: Command) {
|
|||||||
.option("--model <model>", "Model override for agent jobs")
|
.option("--model <model>", "Model override for agent jobs")
|
||||||
.option("--timeout-seconds <n>", "Timeout seconds for agent jobs")
|
.option("--timeout-seconds <n>", "Timeout seconds for agent jobs")
|
||||||
.option("--deliver", "Deliver agent output", false)
|
.option("--deliver", "Deliver agent output", false)
|
||||||
.option("--channel <channel>", `Delivery channel (${CRON_CHANNEL_OPTIONS})`)
|
.option("--channel <channel>", `Delivery channel (${getCronChannelOptions()})`)
|
||||||
.option(
|
.option(
|
||||||
"--to <dest>",
|
"--to <dest>",
|
||||||
"Delivery destination (E.164, Telegram chatId, or Discord channel/user)",
|
"Delivery destination (E.164, Telegram chatId, or Discord channel/user)",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CHANNEL_IDS } from "../../channels/registry.js";
|
import { listChannelPlugins } from "../../channels/plugins/index.js";
|
||||||
import { parseAbsoluteTimeMs } from "../../cron/parse.js";
|
import { parseAbsoluteTimeMs } from "../../cron/parse.js";
|
||||||
import type { CronJob, CronSchedule } from "../../cron/types.js";
|
import type { CronJob, CronSchedule } from "../../cron/types.js";
|
||||||
import { defaultRuntime } from "../../runtime.js";
|
import { defaultRuntime } from "../../runtime.js";
|
||||||
@@ -6,7 +6,8 @@ import { colorize, isRich, theme } from "../../terminal/theme.js";
|
|||||||
import type { GatewayRpcOpts } from "../gateway-rpc.js";
|
import type { GatewayRpcOpts } from "../gateway-rpc.js";
|
||||||
import { callGatewayFromCli } from "../gateway-rpc.js";
|
import { callGatewayFromCli } from "../gateway-rpc.js";
|
||||||
|
|
||||||
export const CRON_CHANNEL_OPTIONS = ["last", ...CHANNEL_IDS].join("|");
|
export const getCronChannelOptions = () =>
|
||||||
|
["last", ...listChannelPlugins().map((plugin) => plugin.id)].join("|");
|
||||||
|
|
||||||
export async function warnIfCronSchedulerDisabled(opts: GatewayRpcOpts) {
|
export async function warnIfCronSchedulerDisabled(opts: GatewayRpcOpts) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { listChannelPlugins } from "../../channels/plugins/index.js";
|
import { listChannelPlugins } from "../../channels/plugins/index.js";
|
||||||
|
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
|
||||||
|
import { loadConfig } from "../../config/config.js";
|
||||||
|
import { createSubsystemLogger } from "../../logging.js";
|
||||||
|
import { loadClawdbotPlugins } from "../../plugins/loader.js";
|
||||||
import { VERSION } from "../../version.js";
|
import { VERSION } from "../../version.js";
|
||||||
|
|
||||||
export type ProgramContext = {
|
export type ProgramContext = {
|
||||||
@@ -8,7 +12,25 @@ export type ProgramContext = {
|
|||||||
agentChannelOptions: string;
|
agentChannelOptions: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const log = createSubsystemLogger("plugins");
|
||||||
|
|
||||||
|
function primePluginRegistry() {
|
||||||
|
const config = loadConfig();
|
||||||
|
const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config));
|
||||||
|
loadClawdbotPlugins({
|
||||||
|
config,
|
||||||
|
workspaceDir,
|
||||||
|
logger: {
|
||||||
|
info: (msg) => log.info(msg),
|
||||||
|
warn: (msg) => log.warn(msg),
|
||||||
|
error: (msg) => log.error(msg),
|
||||||
|
debug: (msg) => log.debug(msg),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function createProgramContext(): ProgramContext {
|
export function createProgramContext(): ProgramContext {
|
||||||
|
primePluginRegistry();
|
||||||
const channelOptions = listChannelPlugins().map((plugin) => plugin.id);
|
const channelOptions = listChannelPlugins().map((plugin) => plugin.id);
|
||||||
return {
|
return {
|
||||||
programVersion: VERSION,
|
programVersion: VERSION,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
|
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
|
||||||
import { getChannelPlugin } from "../channels/plugins/index.js";
|
import { getChannelPlugin, normalizeChannelId } from "../channels/plugins/index.js";
|
||||||
import type { ChatChannelId } from "../channels/registry.js";
|
import type { ChannelId } from "../channels/plugins/types.js";
|
||||||
import { normalizeChatChannelId } from "../channels/registry.js";
|
|
||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
import type { AgentBinding } from "../config/types.js";
|
import type { AgentBinding } from "../config/types.js";
|
||||||
import { DEFAULT_ACCOUNT_ID, normalizeAgentId } from "../routing/session-key.js";
|
import { DEFAULT_ACCOUNT_ID, normalizeAgentId } from "../routing/session-key.js";
|
||||||
@@ -82,7 +81,7 @@ export function applyAgentBindings(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveDefaultAccountId(cfg: ClawdbotConfig, provider: ChatChannelId): string {
|
function resolveDefaultAccountId(cfg: ClawdbotConfig, provider: ChannelId): string {
|
||||||
const plugin = getChannelPlugin(provider);
|
const plugin = getChannelPlugin(provider);
|
||||||
if (!plugin) return DEFAULT_ACCOUNT_ID;
|
if (!plugin) return DEFAULT_ACCOUNT_ID;
|
||||||
return resolveChannelDefaultAccountId({ plugin, cfg });
|
return resolveChannelDefaultAccountId({ plugin, cfg });
|
||||||
@@ -125,7 +124,7 @@ export function parseBindingSpecs(params: {
|
|||||||
const trimmed = raw?.trim();
|
const trimmed = raw?.trim();
|
||||||
if (!trimmed) continue;
|
if (!trimmed) continue;
|
||||||
const [channelRaw, accountRaw] = trimmed.split(":", 2);
|
const [channelRaw, accountRaw] = trimmed.split(":", 2);
|
||||||
const channel = normalizeChatChannelId(channelRaw);
|
const channel = normalizeChannelId(channelRaw);
|
||||||
if (!channel) {
|
if (!channel) {
|
||||||
errors.push(`Unknown channel "${channelRaw}".`);
|
errors.push(`Unknown channel "${channelRaw}".`);
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
|
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
|
||||||
import { getChannelPlugin, listChannelPlugins } from "../channels/plugins/index.js";
|
import { getChannelPlugin, listChannelPlugins, normalizeChannelId } from "../channels/plugins/index.js";
|
||||||
import type { ChatChannelId } from "../channels/registry.js";
|
import type { ChannelId } from "../channels/plugins/types.js";
|
||||||
import { getChatChannelMeta, normalizeChatChannelId } from "../channels/registry.js";
|
|
||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
import type { AgentBinding } from "../config/types.js";
|
import type { AgentBinding } from "../config/types.js";
|
||||||
import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js";
|
import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js";
|
||||||
|
|
||||||
type ProviderAccountStatus = {
|
type ProviderAccountStatus = {
|
||||||
provider: ChatChannelId;
|
provider: ChannelId;
|
||||||
accountId: string;
|
accountId: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
state: "linked" | "not linked" | "configured" | "not configured" | "enabled" | "disabled";
|
state: "linked" | "not linked" | "configured" | "not configured" | "enabled" | "disabled";
|
||||||
@@ -15,16 +14,16 @@ type ProviderAccountStatus = {
|
|||||||
configured?: boolean;
|
configured?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function providerAccountKey(provider: ChatChannelId, accountId?: string) {
|
function providerAccountKey(provider: ChannelId, accountId?: string) {
|
||||||
return `${provider}:${accountId ?? DEFAULT_ACCOUNT_ID}`;
|
return `${provider}:${accountId ?? DEFAULT_ACCOUNT_ID}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatChannelAccountLabel(params: {
|
function formatChannelAccountLabel(params: {
|
||||||
provider: ChatChannelId;
|
provider: ChannelId;
|
||||||
accountId: string;
|
accountId: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
}): string {
|
}): string {
|
||||||
const label = getChatChannelMeta(params.provider).label;
|
const label = getChannelPlugin(params.provider)?.meta.label ?? params.provider;
|
||||||
const account = params.name?.trim()
|
const account = params.name?.trim()
|
||||||
? `${params.accountId} (${params.name.trim()})`
|
? `${params.accountId} (${params.name.trim()})`
|
||||||
: params.accountId;
|
: params.accountId;
|
||||||
@@ -88,7 +87,7 @@ export async function buildProviderStatusIndex(
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveDefaultAccountId(cfg: ClawdbotConfig, provider: ChatChannelId): string {
|
function resolveDefaultAccountId(cfg: ClawdbotConfig, provider: ChannelId): string {
|
||||||
const plugin = getChannelPlugin(provider);
|
const plugin = getChannelPlugin(provider);
|
||||||
if (!plugin) return DEFAULT_ACCOUNT_ID;
|
if (!plugin) return DEFAULT_ACCOUNT_ID;
|
||||||
return resolveChannelDefaultAccountId({ plugin, cfg });
|
return resolveChannelDefaultAccountId({ plugin, cfg });
|
||||||
@@ -117,7 +116,7 @@ export function summarizeBindings(cfg: ClawdbotConfig, bindings: AgentBinding[])
|
|||||||
if (bindings.length === 0) return [];
|
if (bindings.length === 0) return [];
|
||||||
const seen = new Map<string, string>();
|
const seen = new Map<string, string>();
|
||||||
for (const binding of bindings) {
|
for (const binding of bindings) {
|
||||||
const channel = normalizeChatChannelId(binding.match.channel);
|
const channel = normalizeChannelId(binding.match.channel);
|
||||||
if (!channel) continue;
|
if (!channel) continue;
|
||||||
const accountId = binding.match.accountId ?? resolveDefaultAccountId(cfg, channel);
|
const accountId = binding.match.accountId ?? resolveDefaultAccountId(cfg, channel);
|
||||||
const key = providerAccountKey(channel, accountId);
|
const key = providerAccountKey(channel, accountId);
|
||||||
@@ -143,7 +142,7 @@ export function listProvidersForAgent(params: {
|
|||||||
if (params.bindings.length > 0) {
|
if (params.bindings.length > 0) {
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
for (const binding of params.bindings) {
|
for (const binding of params.bindings) {
|
||||||
const channel = normalizeChatChannelId(binding.match.channel);
|
const channel = normalizeChannelId(binding.match.channel);
|
||||||
if (!channel) continue;
|
if (!channel) continue;
|
||||||
const accountId = binding.match.accountId ?? resolveDefaultAccountId(params.cfg, channel);
|
const accountId = binding.match.accountId ?? resolveDefaultAccountId(params.cfg, channel);
|
||||||
const key = providerAccountKey(channel, accountId);
|
const key = providerAccountKey(channel, accountId);
|
||||||
|
|||||||
@@ -15,12 +15,14 @@ type LogLine = ReturnType<typeof parseLogLine>;
|
|||||||
|
|
||||||
const DEFAULT_LIMIT = 200;
|
const DEFAULT_LIMIT = 200;
|
||||||
const MAX_BYTES = 1_000_000;
|
const MAX_BYTES = 1_000_000;
|
||||||
const CHANNELS = new Set<string>([...listChannelPlugins().map((plugin) => plugin.id), "all"]);
|
|
||||||
|
const getChannelSet = () =>
|
||||||
|
new Set<string>([...listChannelPlugins().map((plugin) => plugin.id), "all"]);
|
||||||
|
|
||||||
function parseChannelFilter(raw?: string) {
|
function parseChannelFilter(raw?: string) {
|
||||||
const trimmed = raw?.trim().toLowerCase();
|
const trimmed = raw?.trim().toLowerCase();
|
||||||
if (!trimmed) return "all";
|
if (!trimmed) return "all";
|
||||||
return CHANNELS.has(trimmed) ? trimmed : "all";
|
return getChannelSet().has(trimmed) ? trimmed : "all";
|
||||||
}
|
}
|
||||||
|
|
||||||
function matchesChannel(line: NonNullable<LogLine>, channel: string) {
|
function matchesChannel(line: NonNullable<LogLine>, channel: string) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { listChatChannels } from "../channels/registry.js";
|
import { getChannelPlugin, listChannelPlugins } from "../channels/plugins/index.js";
|
||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
import { CONFIG_PATH_CLAWDBOT } from "../config/config.js";
|
import { CONFIG_PATH_CLAWDBOT } from "../config/config.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
@@ -13,7 +13,9 @@ export async function removeChannelConfigWizard(
|
|||||||
let next = { ...cfg };
|
let next = { ...cfg };
|
||||||
|
|
||||||
const listConfiguredChannels = () =>
|
const listConfiguredChannels = () =>
|
||||||
listChatChannels().filter((meta) => next.channels?.[meta.id] !== undefined);
|
listChannelPlugins()
|
||||||
|
.map((plugin) => plugin.meta)
|
||||||
|
.filter((meta) => next.channels?.[meta.id] !== undefined);
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const configured = listConfiguredChannels();
|
const configured = listConfiguredChannels();
|
||||||
@@ -45,7 +47,7 @@ export async function removeChannelConfigWizard(
|
|||||||
|
|
||||||
if (channel === "done") return next;
|
if (channel === "done") return next;
|
||||||
|
|
||||||
const label = listChatChannels().find((meta) => meta.id === channel)?.label ?? channel;
|
const label = getChannelPlugin(channel)?.meta.label ?? channel;
|
||||||
const confirmed = guardCancel(
|
const confirmed = guardCancel(
|
||||||
await confirm({
|
await confirm({
|
||||||
message: `Delete ${label} configuration from ${CONFIG_PATH_CLAWDBOT}?`,
|
message: `Delete ${label} configuration from ${CONFIG_PATH_CLAWDBOT}?`,
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import {
|
import { listChannelPlugins, getChannelPlugin } from "../channels/plugins/index.js";
|
||||||
formatChannelPrimerLine,
|
import { formatChannelPrimerLine, formatChannelSelectionLine } from "../channels/registry.js";
|
||||||
formatChannelSelectionLine,
|
|
||||||
getChatChannelMeta,
|
|
||||||
listChatChannels,
|
|
||||||
} from "../channels/registry.js";
|
|
||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
import type { DmPolicy } from "../config/types.js";
|
import type { DmPolicy } from "../config/types.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
@@ -17,7 +13,7 @@ import {
|
|||||||
import type { ChannelOnboardingDmPolicy, SetupChannelsOptions } from "./onboarding/types.js";
|
import type { ChannelOnboardingDmPolicy, SetupChannelsOptions } from "./onboarding/types.js";
|
||||||
|
|
||||||
async function noteChannelPrimer(prompter: WizardPrompter): Promise<void> {
|
async function noteChannelPrimer(prompter: WizardPrompter): Promise<void> {
|
||||||
const channelLines = listChatChannels().map((meta) => formatChannelPrimerLine(meta));
|
const channelLines = listChannelPlugins().map((plugin) => formatChannelPrimerLine(plugin.meta));
|
||||||
await prompter.note(
|
await prompter.note(
|
||||||
[
|
[
|
||||||
"DM security: default is pairing; unknown DMs get a pairing code.",
|
"DM security: default is pairing; unknown DMs get a pairing code.",
|
||||||
@@ -130,11 +126,12 @@ export async function setupChannels(
|
|||||||
|
|
||||||
await noteChannelPrimer(prompter);
|
await noteChannelPrimer(prompter);
|
||||||
|
|
||||||
const selectionOptions = listChatChannels().map((meta) => {
|
const selectionOptions = listChannelPlugins().map((plugin) => {
|
||||||
|
const meta = plugin.meta;
|
||||||
const status = statusByChannel.get(meta.id as ChannelChoice);
|
const status = statusByChannel.get(meta.id as ChannelChoice);
|
||||||
return {
|
return {
|
||||||
value: meta.id,
|
value: meta.id,
|
||||||
label: meta.selectionLabel,
|
label: meta.selectionLabel ?? meta.label,
|
||||||
...(status?.selectionHint ? { hint: status.selectionHint } : {}),
|
...(status?.selectionHint ? { hint: status.selectionHint } : {}),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -169,7 +166,10 @@ export async function setupChannels(
|
|||||||
options?.onSelection?.(selection);
|
options?.onSelection?.(selection);
|
||||||
|
|
||||||
const selectionNotes = new Map(
|
const selectionNotes = new Map(
|
||||||
listChatChannels().map((meta) => [meta.id, formatChannelSelectionLine(meta, formatDocsLink)]),
|
listChannelPlugins().map((plugin) => [
|
||||||
|
plugin.id,
|
||||||
|
formatChannelSelectionLine(plugin.meta, formatDocsLink),
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
const selectedLines = selection
|
const selectedLines = selection
|
||||||
.map((channel) => selectionNotes.get(channel))
|
.map((channel) => selectionNotes.get(channel))
|
||||||
@@ -214,9 +214,9 @@ export async function setupChannels(
|
|||||||
if (!status.configured) continue;
|
if (!status.configured) continue;
|
||||||
const adapter = getChannelOnboardingAdapter(channelId);
|
const adapter = getChannelOnboardingAdapter(channelId);
|
||||||
if (!adapter?.disable) continue;
|
if (!adapter?.disable) continue;
|
||||||
const meta = getChatChannelMeta(channelId);
|
const meta = getChannelPlugin(channelId)?.meta;
|
||||||
const disable = await prompter.confirm({
|
const disable = await prompter.confirm({
|
||||||
message: `Disable ${meta.label} channel?`,
|
message: `Disable ${meta?.label ?? channelId} channel?`,
|
||||||
initialValue: false,
|
initialValue: false,
|
||||||
});
|
});
|
||||||
if (disable) {
|
if (disable) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ChatChannelId } from "../channels/registry.js";
|
import type { ChannelId } from "../channels/plugins/types.js";
|
||||||
import type { GatewayDaemonRuntime } from "./daemon-runtime.js";
|
import type { GatewayDaemonRuntime } from "./daemon-runtime.js";
|
||||||
|
|
||||||
export type OnboardMode = "local" | "remote";
|
export type OnboardMode = "local" | "remote";
|
||||||
@@ -31,7 +31,7 @@ export type ResetScope = "config" | "config+creds+sessions" | "full";
|
|||||||
export type GatewayBind = "loopback" | "lan" | "auto" | "custom";
|
export type GatewayBind = "loopback" | "lan" | "auto" | "custom";
|
||||||
export type TailscaleMode = "off" | "serve" | "funnel";
|
export type TailscaleMode = "off" | "serve" | "funnel";
|
||||||
export type NodeManagerChoice = "npm" | "pnpm" | "bun";
|
export type NodeManagerChoice = "npm" | "pnpm" | "bun";
|
||||||
export type ChannelChoice = ChatChannelId;
|
export type ChannelChoice = ChannelId;
|
||||||
// Legacy alias (pre-rename).
|
// Legacy alias (pre-rename).
|
||||||
export type ProviderChoice = ChannelChoice;
|
export type ProviderChoice = ChannelChoice;
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
resolveSandboxConfigForAgent,
|
resolveSandboxConfigForAgent,
|
||||||
resolveSandboxToolPolicyForAgent,
|
resolveSandboxToolPolicyForAgent,
|
||||||
} from "../agents/sandbox.js";
|
} from "../agents/sandbox.js";
|
||||||
import { normalizeChannelId } from "../channels/registry.js";
|
import { normalizeChannelId } from "../channels/plugins/index.js";
|
||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { normalizeChannelId } from "../channels/registry.js";
|
import { normalizeChannelId } from "../channels/plugins/index.js";
|
||||||
import { normalizeAccountId } from "../routing/session-key.js";
|
import { normalizeAccountId } from "../routing/session-key.js";
|
||||||
import type { ClawdbotConfig } from "./config.js";
|
import type { ClawdbotConfig } from "./config.js";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ChannelId } from "../channels/plugins/types.js";
|
import type { ChannelId } from "../channels/plugins/types.js";
|
||||||
import { normalizeChannelId } from "../channels/registry.js";
|
import { normalizeChannelId } from "../channels/plugins/index.js";
|
||||||
import type { NativeCommandsSetting } from "./types.js";
|
import type { NativeCommandsSetting } from "./types.js";
|
||||||
|
|
||||||
function resolveAutoDefault(providerId?: ChannelId): boolean {
|
function resolveAutoDefault(providerId?: ChannelId): boolean {
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ export type PluginUiMetadata = {
|
|||||||
>;
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ChannelUiMetadata = {
|
||||||
|
id: string;
|
||||||
|
label?: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
const GROUP_LABELS: Record<string, string> = {
|
const GROUP_LABELS: Record<string, string> = {
|
||||||
wizard: "Wizard",
|
wizard: "Wizard",
|
||||||
logging: "Logging",
|
logging: "Logging",
|
||||||
@@ -413,6 +419,24 @@ function applyPluginHints(hints: ConfigUiHints, plugins: PluginUiMetadata[]): Co
|
|||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyChannelHints(hints: ConfigUiHints, channels: ChannelUiMetadata[]): ConfigUiHints {
|
||||||
|
const next: ConfigUiHints = { ...hints };
|
||||||
|
for (const channel of channels) {
|
||||||
|
const id = channel.id.trim();
|
||||||
|
if (!id) continue;
|
||||||
|
const basePath = `channels.${id}`;
|
||||||
|
const current = next[basePath] ?? {};
|
||||||
|
const label = channel.label?.trim();
|
||||||
|
const help = channel.description?.trim();
|
||||||
|
next[basePath] = {
|
||||||
|
...current,
|
||||||
|
...(label ? { label } : {}),
|
||||||
|
...(help ? { help } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
let cachedBase: ConfigSchemaResponse | null = null;
|
let cachedBase: ConfigSchemaResponse | null = null;
|
||||||
|
|
||||||
function buildBaseConfigSchema(): ConfigSchemaResponse {
|
function buildBaseConfigSchema(): ConfigSchemaResponse {
|
||||||
@@ -433,11 +457,17 @@ function buildBaseConfigSchema(): ConfigSchemaResponse {
|
|||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildConfigSchema(params?: { plugins?: PluginUiMetadata[] }): ConfigSchemaResponse {
|
export function buildConfigSchema(params?: {
|
||||||
|
plugins?: PluginUiMetadata[];
|
||||||
|
channels?: ChannelUiMetadata[];
|
||||||
|
}): ConfigSchemaResponse {
|
||||||
const base = buildBaseConfigSchema();
|
const base = buildBaseConfigSchema();
|
||||||
const plugins = params?.plugins ?? [];
|
const plugins = params?.plugins ?? [];
|
||||||
if (plugins.length === 0) return base;
|
const channels = params?.channels ?? [];
|
||||||
const merged = applySensitiveHints(applyPluginHints(base.uiHints, plugins));
|
if (plugins.length === 0 && channels.length === 0) return base;
|
||||||
|
const merged = applySensitiveHints(
|
||||||
|
applyChannelHints(applyPluginHints(base.uiHints, plugins), channels),
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
uiHints: merged,
|
uiHints: merged,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { MsgContext } from "../../auto-reply/templating.js";
|
import type { MsgContext } from "../../auto-reply/templating.js";
|
||||||
import { CHANNEL_IDS } from "../../channels/registry.js";
|
import { listDeliverableMessageChannels } from "../../utils/message-channel.js";
|
||||||
import type { GroupKeyResolution } from "./types.js";
|
import type { GroupKeyResolution } from "./types.js";
|
||||||
|
|
||||||
const GROUP_SURFACES = new Set<string>([...CHANNEL_IDS, "webchat"]);
|
const getGroupSurfaces = () => new Set<string>([...listDeliverableMessageChannels(), "webchat"]);
|
||||||
|
|
||||||
function normalizeGroupLabel(raw?: string) {
|
function normalizeGroupLabel(raw?: string) {
|
||||||
const trimmed = raw?.trim().toLowerCase() ?? "";
|
const trimmed = raw?.trim().toLowerCase() ?? "";
|
||||||
@@ -76,7 +76,7 @@ export function resolveGroupSessionKey(ctx: MsgContext): GroupKeyResolution | nu
|
|||||||
};
|
};
|
||||||
|
|
||||||
const parseParts = (parts: string[]) => {
|
const parseParts = (parts: string[]) => {
|
||||||
if (parts.length >= 2 && GROUP_SURFACES.has(parts[0])) {
|
if (parts.length >= 2 && getGroupSurfaces().has(parts[0])) {
|
||||||
provider = parts[0];
|
provider = parts[0];
|
||||||
if (parts.length >= 3) {
|
if (parts.length >= 3) {
|
||||||
const kindCandidate = parts[1];
|
const kindCandidate = parts[1];
|
||||||
|
|||||||
@@ -14,4 +14,5 @@ export type ChannelsConfig = {
|
|||||||
signal?: SignalConfig;
|
signal?: SignalConfig;
|
||||||
imessage?: IMessageConfig;
|
imessage?: IMessageConfig;
|
||||||
msteams?: MSTeamsConfig;
|
msteams?: MSTeamsConfig;
|
||||||
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,4 +23,5 @@ export const ChannelsSchema = z
|
|||||||
imessage: IMessageConfigSchema.optional(),
|
imessage: IMessageConfigSchema.optional(),
|
||||||
msteams: MSTeamsConfigSchema.optional(),
|
msteams: MSTeamsConfigSchema.optional(),
|
||||||
})
|
})
|
||||||
|
.catchall(z.unknown())
|
||||||
.optional();
|
.optional();
|
||||||
|
|||||||
@@ -136,18 +136,19 @@ export type HookAgentPayload = {
|
|||||||
timeoutSeconds?: number;
|
timeoutSeconds?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const HOOK_CHANNEL_VALUES = ["last", ...listChannelPlugins().map((plugin) => plugin.id)];
|
const listHookChannelValues = () => ["last", ...listChannelPlugins().map((plugin) => plugin.id)];
|
||||||
|
|
||||||
export type HookMessageChannel = ChannelId | "last";
|
export type HookMessageChannel = ChannelId | "last";
|
||||||
|
|
||||||
const hookChannelSet = new Set<string>(HOOK_CHANNEL_VALUES);
|
const getHookChannelSet = () => new Set<string>(listHookChannelValues());
|
||||||
export const HOOK_CHANNEL_ERROR = `channel must be ${HOOK_CHANNEL_VALUES.join("|")}`;
|
export const getHookChannelError = () =>
|
||||||
|
`channel must be ${listHookChannelValues().join("|")}`;
|
||||||
|
|
||||||
export function resolveHookChannel(raw: unknown): HookMessageChannel | null {
|
export function resolveHookChannel(raw: unknown): HookMessageChannel | null {
|
||||||
if (raw === undefined) return "last";
|
if (raw === undefined) return "last";
|
||||||
if (typeof raw !== "string") return null;
|
if (typeof raw !== "string") return null;
|
||||||
const normalized = normalizeMessageChannel(raw);
|
const normalized = normalizeMessageChannel(raw);
|
||||||
if (!normalized || !hookChannelSet.has(normalized)) return null;
|
if (!normalized || !getHookChannelSet().has(normalized)) return null;
|
||||||
return normalized as HookMessageChannel;
|
return normalized as HookMessageChannel;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,7 +177,7 @@ export function normalizeAgentPayload(
|
|||||||
? sessionKeyRaw.trim()
|
? sessionKeyRaw.trim()
|
||||||
: `hook:${idFactory()}`;
|
: `hook:${idFactory()}`;
|
||||||
const channel = resolveHookChannel(payload.channel);
|
const channel = resolveHookChannel(payload.channel);
|
||||||
if (!channel) return { ok: false, error: HOOK_CHANNEL_ERROR };
|
if (!channel) return { ok: false, error: getHookChannelError() };
|
||||||
const toRaw = payload.to;
|
const toRaw = payload.to;
|
||||||
const to = typeof toRaw === "string" && toRaw.trim() ? toRaw.trim() : undefined;
|
const to = typeof toRaw === "string" && toRaw.trim() ? toRaw.trim() : undefined;
|
||||||
const modelRaw = payload.model;
|
const modelRaw = payload.model;
|
||||||
|
|||||||
@@ -67,6 +67,11 @@ export const handleConfigBridgeMethods: BridgeMethodHandler = async (
|
|||||||
description: plugin.description,
|
description: plugin.description,
|
||||||
configUiHints: plugin.configUiHints,
|
configUiHints: plugin.configUiHints,
|
||||||
})),
|
})),
|
||||||
|
channels: pluginRegistry.channels.map((entry) => ({
|
||||||
|
id: entry.plugin.id,
|
||||||
|
label: entry.plugin.meta.label,
|
||||||
|
description: entry.plugin.meta.blurb,
|
||||||
|
})),
|
||||||
});
|
});
|
||||||
return { ok: true, payloadJSON: JSON.stringify(schema) };
|
return { ok: true, payloadJSON: JSON.stringify(schema) };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import type { createSubsystemLogger } from "../logging.js";
|
|||||||
import { handleControlUiHttpRequest } from "./control-ui.js";
|
import { handleControlUiHttpRequest } from "./control-ui.js";
|
||||||
import {
|
import {
|
||||||
extractHookToken,
|
extractHookToken,
|
||||||
HOOK_CHANNEL_ERROR,
|
getHookChannelError,
|
||||||
type HookMessageChannel,
|
type HookMessageChannel,
|
||||||
type HooksConfigResolved,
|
type HooksConfigResolved,
|
||||||
normalizeAgentPayload,
|
normalizeAgentPayload,
|
||||||
@@ -152,7 +152,7 @@ export function createHooksRequestHandler(
|
|||||||
}
|
}
|
||||||
const channel = resolveHookChannel(mapped.action.channel);
|
const channel = resolveHookChannel(mapped.action.channel);
|
||||||
if (!channel) {
|
if (!channel) {
|
||||||
sendJson(res, 400, { ok: false, error: HOOK_CHANNEL_ERROR });
|
sendJson(res, 400, { ok: false, error: getHookChannelError() });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const runId = dispatchAgentHook({
|
const runId = dispatchAgentHook({
|
||||||
|
|||||||
@@ -59,9 +59,10 @@ const BASE_METHODS = [
|
|||||||
"chat.send",
|
"chat.send",
|
||||||
];
|
];
|
||||||
|
|
||||||
const CHANNEL_METHODS = listChannelPlugins().flatMap((plugin) => plugin.gatewayMethods ?? []);
|
export function listGatewayMethods(): string[] {
|
||||||
|
const channelMethods = listChannelPlugins().flatMap((plugin) => plugin.gatewayMethods ?? []);
|
||||||
export const GATEWAY_METHODS = Array.from(new Set([...BASE_METHODS, ...CHANNEL_METHODS]));
|
return Array.from(new Set([...BASE_METHODS, ...channelMethods]));
|
||||||
|
}
|
||||||
|
|
||||||
export const GATEWAY_EVENTS = [
|
export const GATEWAY_EVENTS = [
|
||||||
"agent",
|
"agent",
|
||||||
|
|||||||
@@ -73,6 +73,11 @@ export const configHandlers: GatewayRequestHandlers = {
|
|||||||
description: plugin.description,
|
description: plugin.description,
|
||||||
configUiHints: plugin.configUiHints,
|
configUiHints: plugin.configUiHints,
|
||||||
})),
|
})),
|
||||||
|
channels: pluginRegistry.channels.map((entry) => ({
|
||||||
|
id: entry.plugin.id,
|
||||||
|
label: entry.plugin.meta.label,
|
||||||
|
description: entry.plugin.meta.blurb,
|
||||||
|
})),
|
||||||
});
|
});
|
||||||
respond(true, schema, undefined);
|
respond(true, schema, undefined);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import { buildGatewayCronService } from "./server-cron.js";
|
|||||||
import { applyGatewayLaneConcurrency } from "./server-lanes.js";
|
import { applyGatewayLaneConcurrency } from "./server-lanes.js";
|
||||||
import { startGatewayMaintenanceTimers } from "./server-maintenance.js";
|
import { startGatewayMaintenanceTimers } from "./server-maintenance.js";
|
||||||
import { coreGatewayHandlers } from "./server-methods.js";
|
import { coreGatewayHandlers } from "./server-methods.js";
|
||||||
import { GATEWAY_EVENTS, GATEWAY_METHODS } from "./server-methods-list.js";
|
import { GATEWAY_EVENTS, listGatewayMethods } from "./server-methods-list.js";
|
||||||
import { hasConnectedMobileNode as hasConnectedMobileNodeFromBridge } from "./server-mobile-nodes.js";
|
import { hasConnectedMobileNode as hasConnectedMobileNodeFromBridge } from "./server-mobile-nodes.js";
|
||||||
import { loadGatewayModelCatalog } from "./server-model-catalog.js";
|
import { loadGatewayModelCatalog } from "./server-model-catalog.js";
|
||||||
import { loadGatewayPlugins } from "./server-plugins.js";
|
import { loadGatewayPlugins } from "./server-plugins.js";
|
||||||
@@ -69,14 +69,6 @@ const logReload = log.child("reload");
|
|||||||
const logHooks = log.child("hooks");
|
const logHooks = log.child("hooks");
|
||||||
const logWsControl = log.child("ws");
|
const logWsControl = log.child("ws");
|
||||||
const canvasRuntime = runtimeForLogger(logCanvas);
|
const canvasRuntime = runtimeForLogger(logCanvas);
|
||||||
const channelLogs = Object.fromEntries(
|
|
||||||
listChannelPlugins().map((plugin) => [plugin.id, logChannels.child(plugin.id)]),
|
|
||||||
) as Record<ChannelId, ReturnType<typeof createSubsystemLogger>>;
|
|
||||||
const channelRuntimeEnvs = Object.fromEntries(
|
|
||||||
Object.entries(channelLogs).map(([id, logger]) => [id, runtimeForLogger(logger)]),
|
|
||||||
) as Record<ChannelId, RuntimeEnv>;
|
|
||||||
|
|
||||||
const METHODS = GATEWAY_METHODS;
|
|
||||||
|
|
||||||
export type GatewayServer = {
|
export type GatewayServer = {
|
||||||
close: (opts?: { reason?: string; restartExpectedMs?: number | null }) => Promise<void>;
|
close: (opts?: { reason?: string; restartExpectedMs?: number | null }) => Promise<void>;
|
||||||
@@ -163,13 +155,22 @@ export async function startGatewayServer(
|
|||||||
await autoMigrateLegacyState({ cfg: cfgAtStart, log });
|
await autoMigrateLegacyState({ cfg: cfgAtStart, log });
|
||||||
const defaultAgentId = resolveDefaultAgentId(cfgAtStart);
|
const defaultAgentId = resolveDefaultAgentId(cfgAtStart);
|
||||||
const defaultWorkspaceDir = resolveAgentWorkspaceDir(cfgAtStart, defaultAgentId);
|
const defaultWorkspaceDir = resolveAgentWorkspaceDir(cfgAtStart, defaultAgentId);
|
||||||
const { pluginRegistry, gatewayMethods } = loadGatewayPlugins({
|
const baseMethods = listGatewayMethods();
|
||||||
|
const { pluginRegistry, gatewayMethods: baseGatewayMethods } = loadGatewayPlugins({
|
||||||
cfg: cfgAtStart,
|
cfg: cfgAtStart,
|
||||||
workspaceDir: defaultWorkspaceDir,
|
workspaceDir: defaultWorkspaceDir,
|
||||||
log,
|
log,
|
||||||
coreGatewayHandlers,
|
coreGatewayHandlers,
|
||||||
baseMethods: METHODS,
|
baseMethods,
|
||||||
});
|
});
|
||||||
|
const channelLogs = Object.fromEntries(
|
||||||
|
listChannelPlugins().map((plugin) => [plugin.id, logChannels.child(plugin.id)]),
|
||||||
|
) as Record<ChannelId, ReturnType<typeof createSubsystemLogger>>;
|
||||||
|
const channelRuntimeEnvs = Object.fromEntries(
|
||||||
|
Object.entries(channelLogs).map(([id, logger]) => [id, runtimeForLogger(logger)]),
|
||||||
|
) as Record<ChannelId, RuntimeEnv>;
|
||||||
|
const channelMethods = listChannelPlugins().flatMap((plugin) => plugin.gatewayMethods ?? []);
|
||||||
|
const gatewayMethods = Array.from(new Set([...baseGatewayMethods, ...channelMethods]));
|
||||||
let pluginServices: PluginServicesHandle | null = null;
|
let pluginServices: PluginServicesHandle | null = null;
|
||||||
const runtimeConfig = await resolveGatewayRuntimeConfig({
|
const runtimeConfig = await resolveGatewayRuntimeConfig({
|
||||||
cfg: cfgAtStart,
|
cfg: cfgAtStart,
|
||||||
|
|||||||
@@ -2,17 +2,17 @@ import { listChannelPlugins } from "../../channels/plugins/index.js";
|
|||||||
import type { ChannelPlugin } from "../../channels/plugins/types.js";
|
import type { ChannelPlugin } from "../../channels/plugins/types.js";
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import {
|
import {
|
||||||
DELIVERABLE_MESSAGE_CHANNELS,
|
listDeliverableMessageChannels,
|
||||||
type DeliverableMessageChannel,
|
type DeliverableMessageChannel,
|
||||||
normalizeMessageChannel,
|
normalizeMessageChannel,
|
||||||
} from "../../utils/message-channel.js";
|
} from "../../utils/message-channel.js";
|
||||||
|
|
||||||
export type MessageChannelId = DeliverableMessageChannel;
|
export type MessageChannelId = DeliverableMessageChannel;
|
||||||
|
|
||||||
const MESSAGE_CHANNELS = [...DELIVERABLE_MESSAGE_CHANNELS];
|
const getMessageChannels = () => listDeliverableMessageChannels();
|
||||||
|
|
||||||
function isKnownChannel(value: string): value is MessageChannelId {
|
function isKnownChannel(value: string): value is MessageChannelId {
|
||||||
return (MESSAGE_CHANNELS as readonly string[]).includes(value);
|
return getMessageChannels().includes(value as MessageChannelId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isAccountEnabled(account: unknown): boolean {
|
function isAccountEnabled(account: unknown): boolean {
|
||||||
|
|||||||
@@ -102,4 +102,44 @@ describe("loadClawdbotPlugins", () => {
|
|||||||
expect(registry.plugins[0]?.status).toBe("error");
|
expect(registry.plugins[0]?.status).toBe("error");
|
||||||
expect(registry.diagnostics.some((d) => d.level === "error")).toBe(true);
|
expect(registry.diagnostics.some((d) => d.level === "error")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("registers channel plugins", () => {
|
||||||
|
const plugin = writePlugin({
|
||||||
|
id: "channel-demo",
|
||||||
|
body: `export default function (api) {
|
||||||
|
api.registerChannel({
|
||||||
|
plugin: {
|
||||||
|
id: "demo",
|
||||||
|
meta: {
|
||||||
|
id: "demo",
|
||||||
|
label: "Demo",
|
||||||
|
selectionLabel: "Demo",
|
||||||
|
docsPath: "/channels/demo",
|
||||||
|
blurb: "demo channel"
|
||||||
|
},
|
||||||
|
capabilities: { chatTypes: ["direct"] },
|
||||||
|
config: {
|
||||||
|
listAccountIds: () => [],
|
||||||
|
resolveAccount: () => ({ accountId: "default" })
|
||||||
|
},
|
||||||
|
outbound: { deliveryMode: "direct" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const registry = loadClawdbotPlugins({
|
||||||
|
cache: false,
|
||||||
|
workspaceDir: plugin.dir,
|
||||||
|
config: {
|
||||||
|
plugins: {
|
||||||
|
load: { paths: [plugin.file] },
|
||||||
|
allow: ["channel-demo"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(registry.channels.length).toBe(1);
|
||||||
|
expect(registry.channels[0]?.plugin.id).toBe("demo");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { createSubsystemLogger } from "../logging.js";
|
|||||||
import { resolveUserPath } from "../utils.js";
|
import { resolveUserPath } from "../utils.js";
|
||||||
import { discoverClawdbotPlugins } from "./discovery.js";
|
import { discoverClawdbotPlugins } from "./discovery.js";
|
||||||
import { createPluginRegistry, type PluginRecord, type PluginRegistry } from "./registry.js";
|
import { createPluginRegistry, type PluginRecord, type PluginRegistry } from "./registry.js";
|
||||||
|
import { setActivePluginRegistry } from "./runtime.js";
|
||||||
import type {
|
import type {
|
||||||
ClawdbotPluginConfigSchema,
|
ClawdbotPluginConfigSchema,
|
||||||
ClawdbotPluginDefinition,
|
ClawdbotPluginDefinition,
|
||||||
@@ -188,6 +189,7 @@ function createPluginRecord(params: {
|
|||||||
enabled: params.enabled,
|
enabled: params.enabled,
|
||||||
status: params.enabled ? "loaded" : "disabled",
|
status: params.enabled ? "loaded" : "disabled",
|
||||||
toolNames: [],
|
toolNames: [],
|
||||||
|
channelIds: [],
|
||||||
gatewayMethods: [],
|
gatewayMethods: [],
|
||||||
cliCommands: [],
|
cliCommands: [],
|
||||||
services: [],
|
services: [],
|
||||||
@@ -211,7 +213,10 @@ export function loadClawdbotPlugins(options: PluginLoadOptions = {}): PluginRegi
|
|||||||
const cacheEnabled = options.cache !== false;
|
const cacheEnabled = options.cache !== false;
|
||||||
if (cacheEnabled) {
|
if (cacheEnabled) {
|
||||||
const cached = registryCache.get(cacheKey);
|
const cached = registryCache.get(cacheKey);
|
||||||
if (cached) return cached;
|
if (cached) {
|
||||||
|
setActivePluginRegistry(cached, cacheKey);
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { registry, createApi } = createPluginRegistry({
|
const { registry, createApi } = createPluginRegistry({
|
||||||
@@ -359,5 +364,6 @@ export function loadClawdbotPlugins(options: PluginLoadOptions = {}): PluginRegi
|
|||||||
if (cacheEnabled) {
|
if (cacheEnabled) {
|
||||||
registryCache.set(cacheKey, registry);
|
registryCache.set(cacheKey, registry);
|
||||||
}
|
}
|
||||||
|
setActivePluginRegistry(registry, cacheKey);
|
||||||
return registry;
|
return registry;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import type { AnyAgentTool } from "../agents/tools/common.js";
|
import type { AnyAgentTool } from "../agents/tools/common.js";
|
||||||
|
import type { ChannelDock } from "../channels/dock.js";
|
||||||
|
import type { ChannelPlugin } from "../channels/plugins/types.js";
|
||||||
import type {
|
import type {
|
||||||
GatewayRequestHandler,
|
GatewayRequestHandler,
|
||||||
GatewayRequestHandlers,
|
GatewayRequestHandlers,
|
||||||
@@ -6,6 +8,7 @@ import type {
|
|||||||
import { resolveUserPath } from "../utils.js";
|
import { resolveUserPath } from "../utils.js";
|
||||||
import type {
|
import type {
|
||||||
ClawdbotPluginApi,
|
ClawdbotPluginApi,
|
||||||
|
ClawdbotPluginChannelRegistration,
|
||||||
ClawdbotPluginCliRegistrar,
|
ClawdbotPluginCliRegistrar,
|
||||||
ClawdbotPluginService,
|
ClawdbotPluginService,
|
||||||
ClawdbotPluginToolContext,
|
ClawdbotPluginToolContext,
|
||||||
@@ -30,6 +33,13 @@ export type PluginCliRegistration = {
|
|||||||
source: string;
|
source: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PluginChannelRegistration = {
|
||||||
|
pluginId: string;
|
||||||
|
plugin: ChannelPlugin;
|
||||||
|
dock?: ChannelDock;
|
||||||
|
source: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type PluginServiceRegistration = {
|
export type PluginServiceRegistration = {
|
||||||
pluginId: string;
|
pluginId: string;
|
||||||
service: ClawdbotPluginService;
|
service: ClawdbotPluginService;
|
||||||
@@ -48,6 +58,7 @@ export type PluginRecord = {
|
|||||||
status: "loaded" | "disabled" | "error";
|
status: "loaded" | "disabled" | "error";
|
||||||
error?: string;
|
error?: string;
|
||||||
toolNames: string[];
|
toolNames: string[];
|
||||||
|
channelIds: string[];
|
||||||
gatewayMethods: string[];
|
gatewayMethods: string[];
|
||||||
cliCommands: string[];
|
cliCommands: string[];
|
||||||
services: string[];
|
services: string[];
|
||||||
@@ -58,6 +69,7 @@ export type PluginRecord = {
|
|||||||
export type PluginRegistry = {
|
export type PluginRegistry = {
|
||||||
plugins: PluginRecord[];
|
plugins: PluginRecord[];
|
||||||
tools: PluginToolRegistration[];
|
tools: PluginToolRegistration[];
|
||||||
|
channels: PluginChannelRegistration[];
|
||||||
gatewayHandlers: GatewayRequestHandlers;
|
gatewayHandlers: GatewayRequestHandlers;
|
||||||
cliRegistrars: PluginCliRegistration[];
|
cliRegistrars: PluginCliRegistration[];
|
||||||
services: PluginServiceRegistration[];
|
services: PluginServiceRegistration[];
|
||||||
@@ -73,6 +85,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
|||||||
const registry: PluginRegistry = {
|
const registry: PluginRegistry = {
|
||||||
plugins: [],
|
plugins: [],
|
||||||
tools: [],
|
tools: [],
|
||||||
|
channels: [],
|
||||||
gatewayHandlers: {},
|
gatewayHandlers: {},
|
||||||
cliRegistrars: [],
|
cliRegistrars: [],
|
||||||
services: [],
|
services: [],
|
||||||
@@ -129,6 +142,34 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
|||||||
record.gatewayMethods.push(trimmed);
|
record.gatewayMethods.push(trimmed);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const registerChannel = (
|
||||||
|
record: PluginRecord,
|
||||||
|
registration: ClawdbotPluginChannelRegistration | ChannelPlugin,
|
||||||
|
) => {
|
||||||
|
const normalized =
|
||||||
|
typeof (registration as ClawdbotPluginChannelRegistration).plugin === "object"
|
||||||
|
? (registration as ClawdbotPluginChannelRegistration)
|
||||||
|
: { plugin: registration as ChannelPlugin };
|
||||||
|
const plugin = normalized.plugin;
|
||||||
|
const id = typeof plugin?.id === "string" ? plugin.id.trim() : String(plugin?.id ?? "").trim();
|
||||||
|
if (!id) {
|
||||||
|
pushDiagnostic({
|
||||||
|
level: "error",
|
||||||
|
pluginId: record.id,
|
||||||
|
source: record.source,
|
||||||
|
message: "channel registration missing id",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
record.channelIds.push(id);
|
||||||
|
registry.channels.push({
|
||||||
|
pluginId: record.id,
|
||||||
|
plugin,
|
||||||
|
dock: normalized.dock,
|
||||||
|
source: record.source,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const registerCli = (
|
const registerCli = (
|
||||||
record: PluginRecord,
|
record: PluginRecord,
|
||||||
registrar: ClawdbotPluginCliRegistrar,
|
registrar: ClawdbotPluginCliRegistrar,
|
||||||
@@ -179,6 +220,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
|||||||
pluginConfig: params.pluginConfig,
|
pluginConfig: params.pluginConfig,
|
||||||
logger: normalizeLogger(registryParams.logger),
|
logger: normalizeLogger(registryParams.logger),
|
||||||
registerTool: (tool, opts) => registerTool(record, tool, opts),
|
registerTool: (tool, opts) => registerTool(record, tool, opts),
|
||||||
|
registerChannel: (registration) => registerChannel(record, registration),
|
||||||
registerGatewayMethod: (method, handler) => registerGatewayMethod(record, method, handler),
|
registerGatewayMethod: (method, handler) => registerGatewayMethod(record, method, handler),
|
||||||
registerCli: (registrar, opts) => registerCli(record, registrar, opts),
|
registerCli: (registrar, opts) => registerCli(record, registrar, opts),
|
||||||
registerService: (service) => registerService(record, service),
|
registerService: (service) => registerService(record, service),
|
||||||
@@ -191,6 +233,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
|||||||
createApi,
|
createApi,
|
||||||
pushDiagnostic,
|
pushDiagnostic,
|
||||||
registerTool,
|
registerTool,
|
||||||
|
registerChannel,
|
||||||
registerGatewayMethod,
|
registerGatewayMethod,
|
||||||
registerCli,
|
registerCli,
|
||||||
registerService,
|
registerService,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { Command } from "commander";
|
import type { Command } from "commander";
|
||||||
|
|
||||||
import type { AnyAgentTool } from "../agents/tools/common.js";
|
import type { AnyAgentTool } from "../agents/tools/common.js";
|
||||||
|
import type { ChannelDock } from "../channels/dock.js";
|
||||||
|
import type { ChannelPlugin } from "../channels/plugins/types.js";
|
||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
import type { GatewayRequestHandler } from "../gateway/server-methods/types.js";
|
import type { GatewayRequestHandler } from "../gateway/server-methods/types.js";
|
||||||
|
|
||||||
@@ -78,6 +80,11 @@ export type ClawdbotPluginService = {
|
|||||||
stop?: (ctx: ClawdbotPluginServiceContext) => void | Promise<void>;
|
stop?: (ctx: ClawdbotPluginServiceContext) => void | Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ClawdbotPluginChannelRegistration = {
|
||||||
|
plugin: ChannelPlugin;
|
||||||
|
dock?: ChannelDock;
|
||||||
|
};
|
||||||
|
|
||||||
export type ClawdbotPluginDefinition = {
|
export type ClawdbotPluginDefinition = {
|
||||||
id?: string;
|
id?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -105,6 +112,9 @@ export type ClawdbotPluginApi = {
|
|||||||
tool: AnyAgentTool | ClawdbotPluginToolFactory,
|
tool: AnyAgentTool | ClawdbotPluginToolFactory,
|
||||||
opts?: { name?: string; names?: string[] },
|
opts?: { name?: string; names?: string[] },
|
||||||
) => void;
|
) => void;
|
||||||
|
registerChannel: (
|
||||||
|
registration: ClawdbotPluginChannelRegistration | ChannelPlugin,
|
||||||
|
) => void;
|
||||||
registerGatewayMethod: (method: string, handler: GatewayRequestHandler) => void;
|
registerGatewayMethod: (method: string, handler: GatewayRequestHandler) => void;
|
||||||
registerCli: (registrar: ClawdbotPluginCliRegistrar, opts?: { commands?: string[] }) => void;
|
registerCli: (registrar: ClawdbotPluginCliRegistrar, opts?: { commands?: string[] }) => void;
|
||||||
registerService: (service: ClawdbotPluginService) => void;
|
registerService: (service: ClawdbotPluginService) => void;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
listChatChannelAliases,
|
listChatChannelAliases,
|
||||||
normalizeChatChannelId,
|
normalizeChatChannelId,
|
||||||
} from "../channels/registry.js";
|
} from "../channels/registry.js";
|
||||||
|
import type { ChannelId } from "../channels/plugins/types.js";
|
||||||
import {
|
import {
|
||||||
GATEWAY_CLIENT_MODES,
|
GATEWAY_CLIENT_MODES,
|
||||||
GATEWAY_CLIENT_NAMES,
|
GATEWAY_CLIENT_NAMES,
|
||||||
@@ -11,6 +12,7 @@ import {
|
|||||||
normalizeGatewayClientMode,
|
normalizeGatewayClientMode,
|
||||||
normalizeGatewayClientName,
|
normalizeGatewayClientName,
|
||||||
} from "../gateway/protocol/client-info.js";
|
} from "../gateway/protocol/client-info.js";
|
||||||
|
import { getActivePluginRegistry } from "../plugins/runtime.js";
|
||||||
|
|
||||||
export const INTERNAL_MESSAGE_CHANNEL = "webchat" as const;
|
export const INTERNAL_MESSAGE_CHANNEL = "webchat" as const;
|
||||||
export type InternalMessageChannel = typeof INTERNAL_MESSAGE_CHANNEL;
|
export type InternalMessageChannel = typeof INTERNAL_MESSAGE_CHANNEL;
|
||||||
@@ -42,34 +44,56 @@ export function normalizeMessageChannel(raw?: string | null): string | undefined
|
|||||||
const normalized = raw?.trim().toLowerCase();
|
const normalized = raw?.trim().toLowerCase();
|
||||||
if (!normalized) return undefined;
|
if (!normalized) return undefined;
|
||||||
if (normalized === INTERNAL_MESSAGE_CHANNEL) return INTERNAL_MESSAGE_CHANNEL;
|
if (normalized === INTERNAL_MESSAGE_CHANNEL) return INTERNAL_MESSAGE_CHANNEL;
|
||||||
return normalizeChatChannelId(normalized) ?? normalized;
|
const builtIn = normalizeChatChannelId(normalized);
|
||||||
|
if (builtIn) return builtIn;
|
||||||
|
const registry = getActivePluginRegistry();
|
||||||
|
const pluginMatch = registry?.channels.find((entry) => {
|
||||||
|
if (entry.plugin.id.toLowerCase() === normalized) return true;
|
||||||
|
return (entry.plugin.meta.aliases ?? []).some(
|
||||||
|
(alias) => alias.trim().toLowerCase() === normalized,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return pluginMatch?.plugin.id ?? normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DELIVERABLE_MESSAGE_CHANNELS = CHANNEL_IDS;
|
const listPluginChannelIds = (): string[] => {
|
||||||
|
const registry = getActivePluginRegistry();
|
||||||
|
if (!registry) return [];
|
||||||
|
return registry.channels.map((entry) => entry.plugin.id);
|
||||||
|
};
|
||||||
|
|
||||||
export type DeliverableMessageChannel = (typeof DELIVERABLE_MESSAGE_CHANNELS)[number];
|
const listPluginChannelAliases = (): string[] => {
|
||||||
|
const registry = getActivePluginRegistry();
|
||||||
|
if (!registry) return [];
|
||||||
|
return registry.channels.flatMap((entry) => entry.plugin.meta.aliases ?? []);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const listDeliverableMessageChannels = (): ChannelId[] =>
|
||||||
|
Array.from(new Set([...CHANNEL_IDS, ...listPluginChannelIds()]));
|
||||||
|
|
||||||
|
export type DeliverableMessageChannel = ChannelId;
|
||||||
|
|
||||||
export type GatewayMessageChannel = DeliverableMessageChannel | InternalMessageChannel;
|
export type GatewayMessageChannel = DeliverableMessageChannel | InternalMessageChannel;
|
||||||
|
|
||||||
export const GATEWAY_MESSAGE_CHANNELS = [
|
export const listGatewayMessageChannels = (): GatewayMessageChannel[] => [
|
||||||
...DELIVERABLE_MESSAGE_CHANNELS,
|
...listDeliverableMessageChannels(),
|
||||||
INTERNAL_MESSAGE_CHANNEL,
|
INTERNAL_MESSAGE_CHANNEL,
|
||||||
] as const;
|
];
|
||||||
|
|
||||||
export const GATEWAY_AGENT_CHANNEL_ALIASES = listChatChannelAliases();
|
export const listGatewayAgentChannelAliases = (): string[] =>
|
||||||
|
Array.from(new Set([...listChatChannelAliases(), ...listPluginChannelAliases()]));
|
||||||
|
|
||||||
export type GatewayAgentChannelHint = GatewayMessageChannel | "last";
|
export type GatewayAgentChannelHint = GatewayMessageChannel | "last";
|
||||||
|
|
||||||
export const GATEWAY_AGENT_CHANNEL_VALUES = Array.from(
|
export const listGatewayAgentChannelValues = (): string[] =>
|
||||||
new Set([...GATEWAY_MESSAGE_CHANNELS, "last", ...GATEWAY_AGENT_CHANNEL_ALIASES]),
|
Array.from(new Set([...listGatewayMessageChannels(), "last", ...listGatewayAgentChannelAliases()]));
|
||||||
);
|
|
||||||
|
|
||||||
export function isGatewayMessageChannel(value: string): value is GatewayMessageChannel {
|
export function isGatewayMessageChannel(value: string): value is GatewayMessageChannel {
|
||||||
return (GATEWAY_MESSAGE_CHANNELS as readonly string[]).includes(value);
|
return listGatewayMessageChannels().includes(value as GatewayMessageChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isDeliverableMessageChannel(value: string): value is DeliverableMessageChannel {
|
export function isDeliverableMessageChannel(value: string): value is DeliverableMessageChannel {
|
||||||
return (DELIVERABLE_MESSAGE_CHANNELS as readonly string[]).includes(value);
|
return listDeliverableMessageChannels().includes(value as DeliverableMessageChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveGatewayMessageChannel(
|
export function resolveGatewayMessageChannel(
|
||||||
|
|||||||
Reference in New Issue
Block a user