Config: schema-driven channels and settings
This commit is contained in:
committed by
Peter Steinberger
parent
bcfc9bead5
commit
1ad26d6fea
12
src/channels/plugins/config-schema.ts
Normal file
12
src/channels/plugins/config-schema.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { ZodTypeAny } from "zod";
|
||||
|
||||
import type { ChannelConfigSchema } from "./types.js";
|
||||
|
||||
export function buildChannelConfigSchema(schema: ZodTypeAny): ChannelConfigSchema {
|
||||
return {
|
||||
schema: schema.toJSONSchema({
|
||||
target: "draft-07",
|
||||
unrepresentable: "any",
|
||||
}) as Record<string, unknown>,
|
||||
};
|
||||
}
|
||||
@@ -13,7 +13,9 @@ import { sendMessageDiscord, sendPollDiscord } from "../../discord/send.js";
|
||||
import { shouldLogVerbose } from "../../globals.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
|
||||
import { getChatChannelMeta } from "../registry.js";
|
||||
import { DiscordConfigSchema } from "../../config/zod-schema.providers-core.js";
|
||||
import { discordMessageActions } from "./actions/discord.js";
|
||||
import { buildChannelConfigSchema } from "./config-schema.js";
|
||||
import {
|
||||
deleteAccountFromConfigSection,
|
||||
setAccountEnabledInConfigSection,
|
||||
@@ -57,6 +59,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
|
||||
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
|
||||
},
|
||||
reload: { configPrefixes: ["channels.discord"] },
|
||||
configSchema: buildChannelConfigSchema(DiscordConfigSchema),
|
||||
config: {
|
||||
listAccountIds: (cfg) => listDiscordAccountIds(cfg),
|
||||
resolveAccount: (cfg, accountId) => resolveDiscordAccount({ cfg, accountId }),
|
||||
|
||||
@@ -9,6 +9,8 @@ import { probeIMessage } from "../../imessage/probe.js";
|
||||
import { sendMessageIMessage } from "../../imessage/send.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
|
||||
import { getChatChannelMeta } from "../registry.js";
|
||||
import { IMessageConfigSchema } from "../../config/zod-schema.providers-core.js";
|
||||
import { buildChannelConfigSchema } from "./config-schema.js";
|
||||
import {
|
||||
deleteAccountFromConfigSection,
|
||||
setAccountEnabledInConfigSection,
|
||||
@@ -44,6 +46,7 @@ export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount> = {
|
||||
media: true,
|
||||
},
|
||||
reload: { configPrefixes: ["channels.imessage"] },
|
||||
configSchema: buildChannelConfigSchema(IMessageConfigSchema),
|
||||
config: {
|
||||
listAccountIds: (cfg) => listIMessageAccountIds(cfg),
|
||||
resolveAccount: (cfg, accountId) => resolveIMessageAccount({ cfg, accountId }),
|
||||
|
||||
@@ -10,6 +10,8 @@ import { probeSignal } from "../../signal/probe.js";
|
||||
import { sendMessageSignal } from "../../signal/send.js";
|
||||
import { normalizeE164 } from "../../utils.js";
|
||||
import { getChatChannelMeta } from "../registry.js";
|
||||
import { SignalConfigSchema } from "../../config/zod-schema.providers-core.js";
|
||||
import { buildChannelConfigSchema } from "./config-schema.js";
|
||||
import {
|
||||
deleteAccountFromConfigSection,
|
||||
setAccountEnabledInConfigSection,
|
||||
@@ -48,6 +50,7 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
|
||||
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
|
||||
},
|
||||
reload: { configPrefixes: ["channels.signal"] },
|
||||
configSchema: buildChannelConfigSchema(SignalConfigSchema),
|
||||
config: {
|
||||
listAccountIds: (cfg) => listSignalAccountIds(cfg),
|
||||
resolveAccount: (cfg, accountId) => resolveSignalAccount({ cfg, accountId }),
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
import { probeSlack } from "../../slack/probe.js";
|
||||
import { sendMessageSlack } from "../../slack/send.js";
|
||||
import { getChatChannelMeta } from "../registry.js";
|
||||
import { SlackConfigSchema } from "../../config/zod-schema.providers-core.js";
|
||||
import { buildChannelConfigSchema } from "./config-schema.js";
|
||||
import {
|
||||
deleteAccountFromConfigSection,
|
||||
setAccountEnabledInConfigSection,
|
||||
@@ -80,6 +82,7 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
|
||||
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
|
||||
},
|
||||
reload: { configPrefixes: ["channels.slack"] },
|
||||
configSchema: buildChannelConfigSchema(SlackConfigSchema),
|
||||
config: {
|
||||
listAccountIds: (cfg) => listSlackAccountIds(cfg),
|
||||
resolveAccount: (cfg, accountId) => resolveSlackAccount({ cfg, accountId }),
|
||||
|
||||
@@ -17,7 +17,9 @@ import { probeTelegram } from "../../telegram/probe.js";
|
||||
import { sendMessageTelegram } from "../../telegram/send.js";
|
||||
import { resolveTelegramToken } from "../../telegram/token.js";
|
||||
import { getChatChannelMeta } from "../registry.js";
|
||||
import { TelegramConfigSchema } from "../../config/zod-schema.providers-core.js";
|
||||
import { telegramMessageActions } from "./actions/telegram.js";
|
||||
import { buildChannelConfigSchema } from "./config-schema.js";
|
||||
import {
|
||||
deleteAccountFromConfigSection,
|
||||
setAccountEnabledInConfigSection,
|
||||
@@ -77,6 +79,7 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount> = {
|
||||
blockStreaming: true,
|
||||
},
|
||||
reload: { configPrefixes: ["channels.telegram"] },
|
||||
configSchema: buildChannelConfigSchema(TelegramConfigSchema),
|
||||
config: {
|
||||
listAccountIds: (cfg) => listTelegramAccountIds(cfg),
|
||||
resolveAccount: (cfg, accountId) => resolveTelegramAccount({ cfg, accountId }),
|
||||
|
||||
@@ -29,6 +29,20 @@ import type {
|
||||
|
||||
// Channel docking: implement this contract in src/channels/plugins/<id>.ts.
|
||||
// biome-ignore lint/suspicious/noExplicitAny: registry aggregates heterogeneous account types.
|
||||
export type ChannelConfigUiHint = {
|
||||
label?: string;
|
||||
help?: string;
|
||||
advanced?: boolean;
|
||||
sensitive?: boolean;
|
||||
placeholder?: string;
|
||||
itemTemplate?: unknown;
|
||||
};
|
||||
|
||||
export type ChannelConfigSchema = {
|
||||
schema: Record<string, unknown>;
|
||||
uiHints?: Record<string, ChannelConfigUiHint>;
|
||||
};
|
||||
|
||||
export type ChannelPlugin<ResolvedAccount = any> = {
|
||||
id: ChannelId;
|
||||
meta: ChannelMeta;
|
||||
@@ -37,6 +51,7 @@ export type ChannelPlugin<ResolvedAccount = any> = {
|
||||
// CLI onboarding wizard hooks for this channel.
|
||||
onboarding?: ChannelOnboardingAdapter;
|
||||
config: ChannelConfigAdapter<ResolvedAccount>;
|
||||
configSchema?: ChannelConfigSchema;
|
||||
setup?: ChannelSetupAdapter;
|
||||
pairing?: ChannelPairingAdapter;
|
||||
security?: ChannelSecurityAdapter<ResolvedAccount>;
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
import { sendMessageWhatsApp, sendPollWhatsApp } from "../../web/outbound.js";
|
||||
import { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "../../whatsapp/normalize.js";
|
||||
import { getChatChannelMeta } from "../registry.js";
|
||||
import { WhatsAppConfigSchema } from "../../config/zod-schema.providers-whatsapp.js";
|
||||
import { buildChannelConfigSchema } from "./config-schema.js";
|
||||
import { createWhatsAppLoginTool } from "./agent-tools/whatsapp-login.js";
|
||||
import { resolveWhatsAppGroupRequireMention } from "./group-mentions.js";
|
||||
import { formatPairingApproveHint } from "./helpers.js";
|
||||
@@ -60,6 +62,7 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
|
||||
},
|
||||
reload: { configPrefixes: ["web"], noopPrefixes: ["channels.whatsapp"] },
|
||||
gatewayMethods: ["web.login.start", "web.login.wait"],
|
||||
configSchema: buildChannelConfigSchema(WhatsAppConfigSchema),
|
||||
config: {
|
||||
listAccountIds: (cfg) => listWhatsAppAccountIds(cfg),
|
||||
resolveAccount: (cfg, accountId) => resolveWhatsAppAccount({ cfg, accountId }),
|
||||
|
||||
@@ -36,4 +36,52 @@ describe("config schema", () => {
|
||||
);
|
||||
expect(res.uiHints["plugins.entries.voice-call.config.twilio.authToken"]?.sensitive).toBe(true);
|
||||
});
|
||||
|
||||
it("merges plugin + channel schemas", () => {
|
||||
const res = buildConfigSchema({
|
||||
plugins: [
|
||||
{
|
||||
id: "voice-call",
|
||||
name: "Voice Call",
|
||||
configSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
provider: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
channels: [
|
||||
{
|
||||
id: "matrix",
|
||||
label: "Matrix",
|
||||
configSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
accessToken: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const schema = res.schema as {
|
||||
properties?: Record<string, unknown>;
|
||||
};
|
||||
const pluginsNode = schema.properties?.plugins as Record<string, unknown> | undefined;
|
||||
const entriesNode = pluginsNode?.properties as Record<string, unknown> | undefined;
|
||||
const entriesProps = entriesNode?.entries as Record<string, unknown> | undefined;
|
||||
const entryProps = entriesProps?.properties as Record<string, unknown> | undefined;
|
||||
const pluginEntry = entryProps?.["voice-call"] as Record<string, unknown> | undefined;
|
||||
const pluginConfig = pluginEntry?.properties as Record<string, unknown> | undefined;
|
||||
const pluginConfigSchema = pluginConfig?.config as Record<string, unknown> | undefined;
|
||||
const pluginConfigProps = pluginConfigSchema?.properties as Record<string, unknown> | undefined;
|
||||
expect(pluginConfigProps?.provider).toBeTruthy();
|
||||
|
||||
const channelsNode = schema.properties?.channels as Record<string, unknown> | undefined;
|
||||
const channelsProps = channelsNode?.properties as Record<string, unknown> | undefined;
|
||||
const channelSchema = channelsProps?.matrix as Record<string, unknown> | undefined;
|
||||
const channelProps = channelSchema?.properties as Record<string, unknown> | undefined;
|
||||
expect(channelProps?.accessToken).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,8 @@ export type ConfigUiHints = Record<string, ConfigUiHint>;
|
||||
|
||||
export type ConfigSchema = ReturnType<typeof ClawdbotSchema.toJSONSchema>;
|
||||
|
||||
type JsonSchemaNode = Record<string, unknown>;
|
||||
|
||||
export type ConfigSchemaResponse = {
|
||||
schema: ConfigSchema;
|
||||
uiHints: ConfigUiHints;
|
||||
@@ -31,12 +33,15 @@ export type PluginUiMetadata = {
|
||||
string,
|
||||
Pick<ConfigUiHint, "label" | "help" | "advanced" | "sensitive" | "placeholder">
|
||||
>;
|
||||
configSchema?: JsonSchemaNode;
|
||||
};
|
||||
|
||||
export type ChannelUiMetadata = {
|
||||
id: string;
|
||||
label?: string;
|
||||
description?: string;
|
||||
configSchema?: JsonSchemaNode;
|
||||
configUiHints?: Record<string, ConfigUiHint>;
|
||||
};
|
||||
|
||||
const GROUP_LABELS: Record<string, string> = {
|
||||
@@ -433,6 +438,51 @@ function isSensitivePath(path: string): boolean {
|
||||
return SENSITIVE_PATTERNS.some((pattern) => pattern.test(path));
|
||||
}
|
||||
|
||||
type JsonSchemaObject = JsonSchemaNode & {
|
||||
type?: string | string[];
|
||||
properties?: Record<string, JsonSchemaObject>;
|
||||
required?: string[];
|
||||
additionalProperties?: JsonSchemaObject | boolean;
|
||||
};
|
||||
|
||||
function cloneSchema<T>(value: T): T {
|
||||
if (typeof structuredClone === "function") return structuredClone(value);
|
||||
return JSON.parse(JSON.stringify(value)) as T;
|
||||
}
|
||||
|
||||
function asSchemaObject(value: unknown): JsonSchemaObject | null {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
||||
return value as JsonSchemaObject;
|
||||
}
|
||||
|
||||
function isObjectSchema(schema: JsonSchemaObject): boolean {
|
||||
const type = schema.type;
|
||||
if (type === "object") return true;
|
||||
if (Array.isArray(type) && type.includes("object")) return true;
|
||||
return Boolean(schema.properties || schema.additionalProperties);
|
||||
}
|
||||
|
||||
function mergeObjectSchema(base: JsonSchemaObject, extension: JsonSchemaObject): JsonSchemaObject {
|
||||
const mergedRequired = new Set<string>([
|
||||
...(base.required ?? []),
|
||||
...(extension.required ?? []),
|
||||
]);
|
||||
const merged: JsonSchemaObject = {
|
||||
...base,
|
||||
...extension,
|
||||
properties: {
|
||||
...base.properties,
|
||||
...extension.properties,
|
||||
},
|
||||
};
|
||||
if (mergedRequired.size > 0) {
|
||||
merged.required = Array.from(mergedRequired);
|
||||
}
|
||||
const additional = extension.additionalProperties ?? base.additionalProperties;
|
||||
if (additional !== undefined) merged.additionalProperties = additional;
|
||||
return merged;
|
||||
}
|
||||
|
||||
function buildBaseHints(): ConfigUiHints {
|
||||
const hints: ConfigUiHints = {};
|
||||
for (const [group, label] of Object.entries(GROUP_LABELS)) {
|
||||
@@ -520,12 +570,90 @@ function applyChannelHints(hints: ConfigUiHints, channels: ChannelUiMetadata[]):
|
||||
...(label ? { label } : {}),
|
||||
...(help ? { help } : {}),
|
||||
};
|
||||
|
||||
const uiHints = channel.configUiHints ?? {};
|
||||
for (const [relPathRaw, hint] of Object.entries(uiHints)) {
|
||||
const relPath = relPathRaw.trim().replace(/^\./, "");
|
||||
if (!relPath) continue;
|
||||
const key = `${basePath}.${relPath}`;
|
||||
next[key] = {
|
||||
...next[key],
|
||||
...hint,
|
||||
};
|
||||
}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function applyPluginSchemas(schema: ConfigSchema, plugins: PluginUiMetadata[]): ConfigSchema {
|
||||
const next = cloneSchema(schema);
|
||||
const root = asSchemaObject(next);
|
||||
const pluginsNode = asSchemaObject(root?.properties?.plugins);
|
||||
const entriesNode = asSchemaObject(pluginsNode?.properties?.entries);
|
||||
if (!entriesNode) return next;
|
||||
|
||||
const entryBase = asSchemaObject(entriesNode.additionalProperties);
|
||||
const entryProperties = entriesNode.properties ?? {};
|
||||
entriesNode.properties = entryProperties;
|
||||
|
||||
for (const plugin of plugins) {
|
||||
if (!plugin.configSchema) continue;
|
||||
const entrySchema = entryBase ? cloneSchema(entryBase) : ({ type: "object" } as JsonSchemaObject);
|
||||
const entryObject = asSchemaObject(entrySchema) ?? ({ type: "object" } as JsonSchemaObject);
|
||||
const baseConfigSchema = asSchemaObject(entryObject.properties?.config);
|
||||
const pluginSchema = asSchemaObject(plugin.configSchema);
|
||||
const nextConfigSchema =
|
||||
baseConfigSchema && pluginSchema && isObjectSchema(baseConfigSchema) && isObjectSchema(pluginSchema)
|
||||
? mergeObjectSchema(baseConfigSchema, pluginSchema)
|
||||
: cloneSchema(plugin.configSchema);
|
||||
|
||||
entryObject.properties = {
|
||||
...entryObject.properties,
|
||||
config: nextConfigSchema,
|
||||
};
|
||||
entryProperties[plugin.id] = entryObject;
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
function applyChannelSchemas(schema: ConfigSchema, channels: ChannelUiMetadata[]): ConfigSchema {
|
||||
const next = cloneSchema(schema);
|
||||
const root = asSchemaObject(next);
|
||||
const channelsNode = asSchemaObject(root?.properties?.channels);
|
||||
if (!channelsNode) return next;
|
||||
const channelProps = channelsNode.properties ?? {};
|
||||
channelsNode.properties = channelProps;
|
||||
|
||||
for (const channel of channels) {
|
||||
if (!channel.configSchema) continue;
|
||||
const existing = asSchemaObject(channelProps[channel.id]);
|
||||
const incoming = asSchemaObject(channel.configSchema);
|
||||
if (existing && incoming && isObjectSchema(existing) && isObjectSchema(incoming)) {
|
||||
channelProps[channel.id] = mergeObjectSchema(existing, incoming);
|
||||
} else {
|
||||
channelProps[channel.id] = cloneSchema(channel.configSchema);
|
||||
}
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
let cachedBase: ConfigSchemaResponse | null = null;
|
||||
|
||||
function stripChannelSchema(schema: ConfigSchema): ConfigSchema {
|
||||
const next = cloneSchema(schema);
|
||||
const root = asSchemaObject(next);
|
||||
if (!root || !root.properties) return next;
|
||||
const channelsNode = asSchemaObject(root.properties.channels);
|
||||
if (channelsNode) {
|
||||
channelsNode.properties = {};
|
||||
channelsNode.required = [];
|
||||
channelsNode.additionalProperties = true;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function buildBaseConfigSchema(): ConfigSchemaResponse {
|
||||
if (cachedBase) return cachedBase;
|
||||
const schema = ClawdbotSchema.toJSONSchema({
|
||||
@@ -535,7 +663,7 @@ function buildBaseConfigSchema(): ConfigSchemaResponse {
|
||||
schema.title = "ClawdbotConfig";
|
||||
const hints = applySensitiveHints(buildBaseHints());
|
||||
const next = {
|
||||
schema,
|
||||
schema: stripChannelSchema(schema),
|
||||
uiHints: hints,
|
||||
version: VERSION,
|
||||
generatedAt: new Date().toISOString(),
|
||||
@@ -552,11 +680,16 @@ export function buildConfigSchema(params?: {
|
||||
const plugins = params?.plugins ?? [];
|
||||
const channels = params?.channels ?? [];
|
||||
if (plugins.length === 0 && channels.length === 0) return base;
|
||||
const merged = applySensitiveHints(
|
||||
const mergedHints = applySensitiveHints(
|
||||
applyChannelHints(applyPluginHints(base.uiHints, plugins), channels),
|
||||
);
|
||||
const mergedSchema = applyChannelSchemas(
|
||||
applyPluginSchemas(base.schema, plugins),
|
||||
channels,
|
||||
);
|
||||
return {
|
||||
...base,
|
||||
uiHints: merged,
|
||||
schema: mergedSchema,
|
||||
uiHints: mergedHints,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { applyLegacyMigrations } from "../config/legacy.js";
|
||||
import { applyMergePatch } from "../config/merge-patch.js";
|
||||
import { buildConfigSchema } from "../config/schema.js";
|
||||
import { listChannelPlugins } from "../channels/plugins/index.js";
|
||||
import { loadClawdbotPlugins } from "../plugins/loader.js";
|
||||
import {
|
||||
ErrorCodes,
|
||||
@@ -114,11 +115,14 @@ export const handleConfigBridgeMethods: BridgeMethodHandler = async (
|
||||
name: plugin.name,
|
||||
description: plugin.description,
|
||||
configUiHints: plugin.configUiHints,
|
||||
configSchema: plugin.configJsonSchema,
|
||||
})),
|
||||
channels: pluginRegistry.channels.map((entry) => ({
|
||||
id: entry.plugin.id,
|
||||
label: entry.plugin.meta.label,
|
||||
description: entry.plugin.meta.blurb,
|
||||
channels: listChannelPlugins().map((entry) => ({
|
||||
id: entry.id,
|
||||
label: entry.meta.label,
|
||||
description: entry.meta.blurb,
|
||||
configSchema: entry.configSchema?.schema,
|
||||
configUiHints: entry.configSchema?.uiHints,
|
||||
})),
|
||||
});
|
||||
return { ok: true, payloadJSON: JSON.stringify(schema) };
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
type RestartSentinelPayload,
|
||||
writeRestartSentinel,
|
||||
} from "../../infra/restart-sentinel.js";
|
||||
import { listChannelPlugins } from "../../channels/plugins/index.js";
|
||||
import { loadClawdbotPlugins } from "../../plugins/loader.js";
|
||||
import {
|
||||
ErrorCodes,
|
||||
@@ -127,11 +128,14 @@ export const configHandlers: GatewayRequestHandlers = {
|
||||
name: plugin.name,
|
||||
description: plugin.description,
|
||||
configUiHints: plugin.configUiHints,
|
||||
configSchema: plugin.configJsonSchema,
|
||||
})),
|
||||
channels: pluginRegistry.channels.map((entry) => ({
|
||||
id: entry.plugin.id,
|
||||
label: entry.plugin.meta.label,
|
||||
description: entry.plugin.meta.blurb,
|
||||
channels: listChannelPlugins().map((entry) => ({
|
||||
id: entry.id,
|
||||
label: entry.meta.label,
|
||||
description: entry.meta.blurb,
|
||||
configSchema: entry.configSchema?.schema,
|
||||
configUiHints: entry.configSchema?.uiHints,
|
||||
})),
|
||||
});
|
||||
respond(true, schema, undefined);
|
||||
|
||||
@@ -197,6 +197,7 @@ function createPluginRecord(params: {
|
||||
httpHandlers: 0,
|
||||
configSchema: params.configSchema,
|
||||
configUiHints: undefined,
|
||||
configJsonSchema: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -302,6 +303,17 @@ export function loadClawdbotPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
PluginConfigUiHint
|
||||
>)
|
||||
: undefined;
|
||||
record.configJsonSchema =
|
||||
definition?.configSchema &&
|
||||
typeof definition.configSchema === "object" &&
|
||||
(definition.configSchema as { jsonSchema?: unknown }).jsonSchema &&
|
||||
typeof (definition.configSchema as { jsonSchema?: unknown }).jsonSchema === "object" &&
|
||||
!Array.isArray((definition.configSchema as { jsonSchema?: unknown }).jsonSchema)
|
||||
? ((definition.configSchema as { jsonSchema?: unknown }).jsonSchema as Record<
|
||||
string,
|
||||
unknown
|
||||
>)
|
||||
: undefined;
|
||||
|
||||
const validatedConfig = validatePluginConfig({
|
||||
schema: definition?.configSchema,
|
||||
|
||||
@@ -80,6 +80,7 @@ export type PluginRecord = {
|
||||
httpHandlers: number;
|
||||
configSchema: boolean;
|
||||
configUiHints?: Record<string, PluginConfigUiHint>;
|
||||
configJsonSchema?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type PluginRegistry = {
|
||||
|
||||
@@ -42,6 +42,7 @@ export type ClawdbotPluginConfigSchema = {
|
||||
parse?: (value: unknown) => unknown;
|
||||
validate?: (value: unknown) => PluginConfigValidation;
|
||||
uiHints?: Record<string, PluginConfigUiHint>;
|
||||
jsonSchema?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type ClawdbotPluginToolContext = {
|
||||
|
||||
Reference in New Issue
Block a user