From d021b0c06c5a77c9ce1896c8548deba0a759929d Mon Sep 17 00:00:00 2001 From: orion Date: Sat, 7 Mar 2026 22:33:28 +0000 Subject: [PATCH] refactor(plugin): extract discord/policy tool registration module --- plugin/index.ts | 217 ++------------------------------ plugin/tools/register-tools.ts | 218 +++++++++++++++++++++++++++++++++ 2 files changed, 229 insertions(+), 206 deletions(-) create mode 100644 plugin/tools/register-tools.ts diff --git a/plugin/index.ts b/plugin/index.ts index bd5583e..bcf8119 100644 --- a/plugin/index.ts +++ b/plugin/index.ts @@ -11,6 +11,7 @@ import { registerBeforePromptBuildHook } from "./hooks/before-prompt-build.js"; import { registerBeforeMessageWriteHook } from "./hooks/before-message-write.js"; import { registerMessageSentHook } from "./hooks/message-sent.js"; import { registerDirigentCommand } from "./commands/dirigent-command.js"; +import { registerDirigentTools } from "./tools/register-tools.js"; // ── No-Reply API child process lifecycle ────────────────────────────── let noReplyProcess: ChildProcess | null = null; @@ -57,8 +58,6 @@ function stopNoReplyApi(logger: { info: (m: string) => void }): void { noReplyProcess = null; } -type DiscordControlAction = "channel-private-create" | "channel-private-update" | "member-list"; - type DecisionRecord = { decision: Decision; createdAt: number; @@ -492,210 +491,16 @@ export default { api.logger.info("dirigent: gateway stopping, services shut down"); }); - // ── Helper: execute Discord control API action ── - async function executeDiscordAction(action: DiscordControlAction, params: Record) { - const live = getLivePluginConfig(api, baseConfig as DirigentConfig) as DirigentConfig & { - discordControlApiBaseUrl?: string; - discordControlApiToken?: string; - discordControlCallerId?: string; - enableDiscordControlTool?: boolean; - }; - if (live.enableDiscordControlTool === false) { - return { content: [{ type: "text", text: "discord actions disabled by config" }], isError: true }; - } - const baseUrl = (live.discordControlApiBaseUrl || "http://127.0.0.1:8790").replace(/\/$/, ""); - const body = pickDefined({ ...params, action }); - const headers: Record = { "Content-Type": "application/json" }; - if (live.discordControlApiToken) headers.Authorization = `Bearer ${live.discordControlApiToken}`; - if (live.discordControlCallerId) headers["X-OpenClaw-Caller-Id"] = live.discordControlCallerId; - const r = await fetch(`${baseUrl}/v1/discord/action`, { - method: "POST", - headers, - body: JSON.stringify(body), - }); - const text = await r.text(); - if (!r.ok) { - return { content: [{ type: "text", text: `discord action failed (${r.status}): ${text}` }], isError: true }; - } - return { content: [{ type: "text", text }] }; - } - - // ── Discord control tools ── - - api.registerTool( - { - name: "dirigent_discord_channel_create", - description: "Create a private Discord channel with specific user/role permissions.", - parameters: { - type: "object", - additionalProperties: false, - properties: { - guildId: { type: "string" }, - name: { type: "string" }, - type: { type: "number" }, - parentId: { type: "string" }, - topic: { type: "string" }, - position: { type: "number" }, - nsfw: { type: "boolean" }, - allowedUserIds: { type: "array", items: { type: "string" } }, - allowedRoleIds: { type: "array", items: { type: "string" } }, - allowMask: { type: "string" }, - denyEveryoneMask: { type: "string" }, - }, - required: [], - }, - async execute(_id: string, params: Record) { - return executeDiscordAction("channel-private-create", params); - }, - }, - { optional: false }, - ); - - api.registerTool( - { - name: "dirigent_discord_channel_update", - description: "Update permissions on an existing private Discord channel.", - parameters: { - type: "object", - additionalProperties: false, - properties: { - channelId: { type: "string" }, - mode: { type: "string", enum: ["merge", "replace"] }, - addUserIds: { type: "array", items: { type: "string" } }, - addRoleIds: { type: "array", items: { type: "string" } }, - removeTargetIds: { type: "array", items: { type: "string" } }, - allowMask: { type: "string" }, - denyMask: { type: "string" }, - }, - required: [], - }, - async execute(_id: string, params: Record) { - return executeDiscordAction("channel-private-update", params); - }, - }, - { optional: false }, - ); - - api.registerTool( - { - name: "dirigent_discord_member_list", - description: "List members of a Discord guild.", - parameters: { - type: "object", - additionalProperties: false, - properties: { - guildId: { type: "string" }, - limit: { type: "number" }, - after: { type: "string" }, - fields: { anyOf: [{ type: "string" }, { type: "array", items: { type: "string" } }] }, - }, - required: [], - }, - async execute(_id: string, params: Record) { - return executeDiscordAction("member-list", params); - }, - }, - { optional: false }, - ); - - // ── Policy tools ── - - api.registerTool( - { - name: "dirigent_policy_get", - description: "Get all Dirigent channel policies.", - parameters: { type: "object", additionalProperties: false, properties: {}, required: [] }, - async execute() { - const live = getLivePluginConfig(api, baseConfig as DirigentConfig) as DirigentConfig & { enableDirigentPolicyTool?: boolean }; - if (live.enableDirigentPolicyTool === false) { - return { content: [{ type: "text", text: "policy actions disabled by config" }], isError: true }; - } - ensurePolicyStateLoaded(api, live); - return { - content: [{ type: "text", text: JSON.stringify({ file: policyState.filePath, policies: policyState.channelPolicies }, null, 2) }], - }; - }, - }, - { optional: false }, - ); - - api.registerTool( - { - name: "dirigent_policy_set", - description: "Set or update a Dirigent channel policy.", - parameters: { - type: "object", - additionalProperties: false, - properties: { - channelId: { type: "string" }, - listMode: { type: "string", enum: ["human-list", "agent-list"] }, - humanList: { type: "array", items: { type: "string" } }, - agentList: { type: "array", items: { type: "string" } }, - endSymbols: { type: "array", items: { type: "string" } }, - }, - required: ["channelId"], - }, - async execute(_id: string, params: Record) { - const live = getLivePluginConfig(api, baseConfig as DirigentConfig) as DirigentConfig & { enableDirigentPolicyTool?: boolean }; - if (live.enableDirigentPolicyTool === false) { - return { content: [{ type: "text", text: "policy actions disabled by config" }], isError: true }; - } - ensurePolicyStateLoaded(api, live); - const channelId = String(params.channelId || "").trim(); - if (!channelId) return { content: [{ type: "text", text: "channelId is required" }], isError: true }; - const prev = JSON.parse(JSON.stringify(policyState.channelPolicies)); - try { - const next: ChannelPolicy = { - listMode: (params.listMode as "human-list" | "agent-list" | undefined) || undefined, - humanList: Array.isArray(params.humanList) ? (params.humanList as string[]) : undefined, - agentList: Array.isArray(params.agentList) ? (params.agentList as string[]) : undefined, - endSymbols: Array.isArray(params.endSymbols) ? (params.endSymbols as string[]) : undefined, - }; - policyState.channelPolicies[channelId] = pickDefined(next as unknown as Record) as ChannelPolicy; - persistPolicies(api); - return { content: [{ type: "text", text: JSON.stringify({ ok: true, channelId, policy: policyState.channelPolicies[channelId] }) }] }; - } catch (err) { - policyState.channelPolicies = prev; - return { content: [{ type: "text", text: `persist failed: ${String(err)}` }], isError: true }; - } - }, - }, - { optional: false }, - ); - - api.registerTool( - { - name: "dirigent_policy_delete", - description: "Delete a Dirigent channel policy.", - parameters: { - type: "object", - additionalProperties: false, - properties: { - channelId: { type: "string" }, - }, - required: ["channelId"], - }, - async execute(_id: string, params: Record) { - const live = getLivePluginConfig(api, baseConfig as DirigentConfig) as DirigentConfig & { enableDirigentPolicyTool?: boolean }; - if (live.enableDirigentPolicyTool === false) { - return { content: [{ type: "text", text: "policy actions disabled by config" }], isError: true }; - } - ensurePolicyStateLoaded(api, live); - const channelId = String(params.channelId || "").trim(); - if (!channelId) return { content: [{ type: "text", text: "channelId is required" }], isError: true }; - const prev = JSON.parse(JSON.stringify(policyState.channelPolicies)); - try { - delete policyState.channelPolicies[channelId]; - persistPolicies(api); - return { content: [{ type: "text", text: JSON.stringify({ ok: true, channelId, deleted: true }) }] }; - } catch (err) { - policyState.channelPolicies = prev; - return { content: [{ type: "text", text: `persist failed: ${String(err)}` }], isError: true }; - } - }, - }, - { optional: false }, - ); + // Register tools + registerDirigentTools({ + api, + baseConfig: baseConfig as DirigentConfig, + policyState, + pickDefined, + persistPolicies, + ensurePolicyStateLoaded, + getLivePluginConfig, + }); // Turn management is handled internally by the plugin (not exposed as tools). // Use `/dirigent turn-status`, `/dirigent turn-advance`, `/dirigent turn-reset` for manual control. diff --git a/plugin/tools/register-tools.ts b/plugin/tools/register-tools.ts new file mode 100644 index 0000000..bca3a6b --- /dev/null +++ b/plugin/tools/register-tools.ts @@ -0,0 +1,218 @@ +import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; +import type { ChannelPolicy, DirigentConfig } from "../rules.js"; + +type DiscordControlAction = "channel-private-create" | "channel-private-update" | "member-list"; + +type ToolDeps = { + api: OpenClawPluginApi; + baseConfig: DirigentConfig; + policyState: { filePath: string; channelPolicies: Record }; + pickDefined: (obj: Record) => Record; + persistPolicies: (api: OpenClawPluginApi) => void; + ensurePolicyStateLoaded: (api: OpenClawPluginApi, config: DirigentConfig) => void; + getLivePluginConfig: (api: OpenClawPluginApi, fallback: DirigentConfig) => DirigentConfig; +}; + +export function registerDirigentTools(deps: ToolDeps): void { + const { api, baseConfig, policyState, pickDefined, persistPolicies, ensurePolicyStateLoaded, getLivePluginConfig } = deps; + + async function executeDiscordAction(action: DiscordControlAction, params: Record) { + const live = getLivePluginConfig(api, baseConfig as DirigentConfig) as DirigentConfig & { + discordControlApiBaseUrl?: string; + discordControlApiToken?: string; + discordControlCallerId?: string; + enableDiscordControlTool?: boolean; + }; + if (live.enableDiscordControlTool === false) { + return { content: [{ type: "text", text: "discord actions disabled by config" }], isError: true }; + } + const baseUrl = (live.discordControlApiBaseUrl || "http://127.0.0.1:8790").replace(/\/$/, ""); + const body = pickDefined({ ...params, action }); + const headers: Record = { "Content-Type": "application/json" }; + if (live.discordControlApiToken) headers.Authorization = `Bearer ${live.discordControlApiToken}`; + if (live.discordControlCallerId) headers["X-OpenClaw-Caller-Id"] = live.discordControlCallerId; + const r = await fetch(`${baseUrl}/v1/discord/action`, { + method: "POST", + headers, + body: JSON.stringify(body), + }); + const text = await r.text(); + if (!r.ok) { + return { content: [{ type: "text", text: `discord action failed (${r.status}): ${text}` }], isError: true }; + } + return { content: [{ type: "text", text }] }; + } + + api.registerTool( + { + name: "dirigent_discord_channel_create", + description: "Create a private Discord channel with specific user/role permissions.", + parameters: { + type: "object", + additionalProperties: false, + properties: { + guildId: { type: "string" }, + name: { type: "string" }, + type: { type: "number" }, + parentId: { type: "string" }, + topic: { type: "string" }, + position: { type: "number" }, + nsfw: { type: "boolean" }, + allowedUserIds: { type: "array", items: { type: "string" } }, + allowedRoleIds: { type: "array", items: { type: "string" } }, + allowMask: { type: "string" }, + denyEveryoneMask: { type: "string" }, + }, + required: [], + }, + async execute(_id: string, params: Record) { + return executeDiscordAction("channel-private-create", params); + }, + }, + { optional: false }, + ); + + api.registerTool( + { + name: "dirigent_discord_channel_update", + description: "Update permissions on an existing private Discord channel.", + parameters: { + type: "object", + additionalProperties: false, + properties: { + channelId: { type: "string" }, + mode: { type: "string", enum: ["merge", "replace"] }, + addUserIds: { type: "array", items: { type: "string" } }, + addRoleIds: { type: "array", items: { type: "string" } }, + removeTargetIds: { type: "array", items: { type: "string" } }, + allowMask: { type: "string" }, + denyMask: { type: "string" }, + }, + required: [], + }, + async execute(_id: string, params: Record) { + return executeDiscordAction("channel-private-update", params); + }, + }, + { optional: false }, + ); + + api.registerTool( + { + name: "dirigent_discord_member_list", + description: "List members of a Discord guild.", + parameters: { + type: "object", + additionalProperties: false, + properties: { + guildId: { type: "string" }, + limit: { type: "number" }, + after: { type: "string" }, + fields: { anyOf: [{ type: "string" }, { type: "array", items: { type: "string" } }] }, + }, + required: [], + }, + async execute(_id: string, params: Record) { + return executeDiscordAction("member-list", params); + }, + }, + { optional: false }, + ); + + api.registerTool( + { + name: "dirigent_policy_get", + description: "Get all Dirigent channel policies.", + parameters: { type: "object", additionalProperties: false, properties: {}, required: [] }, + async execute() { + const live = getLivePluginConfig(api, baseConfig as DirigentConfig) as DirigentConfig & { enableDirigentPolicyTool?: boolean }; + if (live.enableDirigentPolicyTool === false) { + return { content: [{ type: "text", text: "policy actions disabled by config" }], isError: true }; + } + ensurePolicyStateLoaded(api, live); + return { + content: [{ type: "text", text: JSON.stringify({ file: policyState.filePath, policies: policyState.channelPolicies }, null, 2) }], + }; + }, + }, + { optional: false }, + ); + + api.registerTool( + { + name: "dirigent_policy_set", + description: "Set or update a Dirigent channel policy.", + parameters: { + type: "object", + additionalProperties: false, + properties: { + channelId: { type: "string" }, + listMode: { type: "string", enum: ["human-list", "agent-list"] }, + humanList: { type: "array", items: { type: "string" } }, + agentList: { type: "array", items: { type: "string" } }, + endSymbols: { type: "array", items: { type: "string" } }, + }, + required: ["channelId"], + }, + async execute(_id: string, params: Record) { + const live = getLivePluginConfig(api, baseConfig as DirigentConfig) as DirigentConfig & { enableDirigentPolicyTool?: boolean }; + if (live.enableDirigentPolicyTool === false) { + return { content: [{ type: "text", text: "policy actions disabled by config" }], isError: true }; + } + ensurePolicyStateLoaded(api, live); + const channelId = String(params.channelId || "").trim(); + if (!channelId) return { content: [{ type: "text", text: "channelId is required" }], isError: true }; + const prev = JSON.parse(JSON.stringify(policyState.channelPolicies)); + try { + const next: ChannelPolicy = { + listMode: (params.listMode as "human-list" | "agent-list" | undefined) || undefined, + humanList: Array.isArray(params.humanList) ? (params.humanList as string[]) : undefined, + agentList: Array.isArray(params.agentList) ? (params.agentList as string[]) : undefined, + endSymbols: Array.isArray(params.endSymbols) ? (params.endSymbols as string[]) : undefined, + }; + policyState.channelPolicies[channelId] = pickDefined(next as unknown as Record) as ChannelPolicy; + persistPolicies(api); + return { content: [{ type: "text", text: JSON.stringify({ ok: true, channelId, policy: policyState.channelPolicies[channelId] }) }] }; + } catch (err) { + policyState.channelPolicies = prev; + return { content: [{ type: "text", text: `persist failed: ${String(err)}` }], isError: true }; + } + }, + }, + { optional: false }, + ); + + api.registerTool( + { + name: "dirigent_policy_delete", + description: "Delete a Dirigent channel policy.", + parameters: { + type: "object", + additionalProperties: false, + properties: { + channelId: { type: "string" }, + }, + required: ["channelId"], + }, + async execute(_id: string, params: Record) { + const live = getLivePluginConfig(api, baseConfig as DirigentConfig) as DirigentConfig & { enableDirigentPolicyTool?: boolean }; + if (live.enableDirigentPolicyTool === false) { + return { content: [{ type: "text", text: "policy actions disabled by config" }], isError: true }; + } + ensurePolicyStateLoaded(api, live); + const channelId = String(params.channelId || "").trim(); + if (!channelId) return { content: [{ type: "text", text: "channelId is required" }], isError: true }; + const prev = JSON.parse(JSON.stringify(policyState.channelPolicies)); + try { + delete policyState.channelPolicies[channelId]; + persistPolicies(api); + return { content: [{ type: "text", text: JSON.stringify({ ok: true, channelId, deleted: true }) }] }; + } catch (err) { + policyState.channelPolicies = prev; + return { content: [{ type: "text", text: `persist failed: ${String(err)}` }], isError: true }; + } + }, + }, + { optional: false }, + ); +}