diff --git a/plugin/core/live-config.ts b/plugin/core/live-config.ts new file mode 100644 index 0000000..094ab19 --- /dev/null +++ b/plugin/core/live-config.ts @@ -0,0 +1,23 @@ +import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; +import type { DirigentConfig } from "../rules.js"; + +export function getLivePluginConfig(api: OpenClawPluginApi, fallback: DirigentConfig): DirigentConfig { + const root = (api.config as Record) || {}; + const plugins = (root.plugins as Record) || {}; + const entries = (plugins.entries as Record) || {}; + const entry = (entries.dirigent as Record) || (entries.whispergate as Record) || {}; + const cfg = (entry.config as Record) || {}; + if (Object.keys(cfg).length > 0) { + return { + enableDiscordControlTool: true, + enableDirigentPolicyTool: true, + discordControlApiBaseUrl: "http://127.0.0.1:8790", + enableDebugLogs: false, + debugLogChannelIds: [], + schedulingIdentifier: "➡️", + waitIdentifier: "👤", + ...cfg, + } as DirigentConfig; + } + return fallback; +} diff --git a/plugin/index.ts b/plugin/index.ts index bcf8119..5e62598 100644 --- a/plugin/index.ts +++ b/plugin/index.ts @@ -2,7 +2,7 @@ import fs from "node:fs"; import path from "node:path"; import { spawn, type ChildProcess } from "node:child_process"; import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; -import { evaluateDecision, resolvePolicy, type ChannelPolicy, type Decision, type DirigentConfig } from "./rules.js"; +import type { Decision, DirigentConfig } from "./rules.js"; import { initTurnOrder } from "./turn-manager.js"; import { startModeratorPresence, stopModeratorPresence } from "./moderator-presence.js"; import { registerMessageReceivedHook } from "./hooks/message-received.js"; @@ -12,6 +12,8 @@ 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"; +import { getLivePluginConfig } from "./core/live-config.js"; +import { ensurePolicyStateLoaded, persistPolicies, policyState } from "./policy/store.js"; // ── No-Reply API child process lifecycle ────────────────────────────── let noReplyProcess: ChildProcess | null = null; @@ -64,11 +66,6 @@ type DecisionRecord = { needsRestore?: boolean; }; -type PolicyState = { - filePath: string; - channelPolicies: Record; -}; - type DebugConfig = { enableDebugLogs?: boolean; debugLogChannelIds?: string[]; @@ -97,11 +94,6 @@ function buildSchedulingIdentifierInstruction(schedulingIdentifier: string): str return `\n\nScheduling identifier: "${schedulingIdentifier}". This identifier itself is meaningless — it carries no semantic content. When you receive a message containing <@YOUR_USER_ID> followed by the scheduling identifier, check recent chat history and decide whether you have something to say. If not, reply NO_REPLY.`; } -const policyState: PolicyState = { - filePath: "", - channelPolicies: {}, -}; - function pruneDecisionMap(now = Date.now()) { for (const [k, v] of sessionDecision.entries()) { if (now - v.createdAt > DECISION_TTL_MS) sessionDecision.delete(k); @@ -116,56 +108,6 @@ function pruneDecisionMap(now = Date.now()) { } } - -function getLivePluginConfig(api: OpenClawPluginApi, fallback: DirigentConfig): DirigentConfig { - const root = (api.config as Record) || {}; - const plugins = (root.plugins as Record) || {}; - const entries = (plugins.entries as Record) || {}; - // Support both "dirigent" and legacy "whispergate" config keys - const entry = (entries.dirigent as Record) || (entries.whispergate as Record) || {}; - const cfg = (entry.config as Record) || {}; - if (Object.keys(cfg).length > 0) { - // Merge with defaults to ensure optional fields have values - return { - enableDiscordControlTool: true, - enableDirigentPolicyTool: true, - discordControlApiBaseUrl: "http://127.0.0.1:8790", - enableDebugLogs: false, - debugLogChannelIds: [], - schedulingIdentifier: "➡️", - waitIdentifier: "👤", - ...cfg, - } as DirigentConfig; - } - return fallback; -} - -function resolvePoliciesPath(api: OpenClawPluginApi, config: DirigentConfig): string { - return api.resolvePath(config.channelPoliciesFile || "~/.openclaw/dirigent-channel-policies.json"); -} - -function ensurePolicyStateLoaded(api: OpenClawPluginApi, config: DirigentConfig) { - if (policyState.filePath) return; - const filePath = resolvePoliciesPath(api, config); - policyState.filePath = filePath; - - try { - if (!fs.existsSync(filePath)) { - fs.mkdirSync(path.dirname(filePath), { recursive: true }); - fs.writeFileSync(filePath, "{}\n", "utf8"); - policyState.channelPolicies = {}; - return; - } - - const raw = fs.readFileSync(filePath, "utf8"); - const parsed = JSON.parse(raw) as Record; - policyState.channelPolicies = parsed && typeof parsed === "object" ? parsed : {}; - } catch (err) { - api.logger.warn(`dirigent: failed init policy file ${filePath}: ${String(err)}`); - policyState.channelPolicies = {}; - } -} - /** Resolve agentId → Discord accountId from config bindings */ function resolveAccountId(api: OpenClawPluginApi, agentId: string): string | undefined { const root = (api.config as Record) || {}; @@ -374,17 +316,6 @@ async function sendModeratorMessage(token: string, channelId: string, content: s } } -function persistPolicies(api: OpenClawPluginApi): void { - const filePath = policyState.filePath; - if (!filePath) throw new Error("policy file path not initialized"); - const before = JSON.stringify(policyState.channelPolicies, null, 2) + "\n"; - const tmp = `${filePath}.tmp`; - fs.mkdirSync(path.dirname(filePath), { recursive: true }); - fs.writeFileSync(tmp, before, "utf8"); - fs.renameSync(tmp, filePath); - api.logger.info(`dirigent: policy file persisted: ${filePath}`); -} - function pickDefined(input: Record) { const out: Record = {}; for (const [k, v] of Object.entries(input)) { diff --git a/plugin/policy/store.ts b/plugin/policy/store.ts new file mode 100644 index 0000000..1b4479e --- /dev/null +++ b/plugin/policy/store.ts @@ -0,0 +1,50 @@ +import fs from "node:fs"; +import path from "node:path"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; +import type { ChannelPolicy, DirigentConfig } from "../rules.js"; + +export type PolicyState = { + filePath: string; + channelPolicies: Record; +}; + +export const policyState: PolicyState = { + filePath: "", + channelPolicies: {}, +}; + +export function resolvePoliciesPath(api: OpenClawPluginApi, config: DirigentConfig): string { + return api.resolvePath(config.channelPoliciesFile || "~/.openclaw/dirigent-channel-policies.json"); +} + +export function ensurePolicyStateLoaded(api: OpenClawPluginApi, config: DirigentConfig): void { + if (policyState.filePath) return; + const filePath = resolvePoliciesPath(api, config); + policyState.filePath = filePath; + + try { + if (!fs.existsSync(filePath)) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, "{}\n", "utf8"); + policyState.channelPolicies = {}; + return; + } + + const raw = fs.readFileSync(filePath, "utf8"); + const parsed = JSON.parse(raw) as Record; + policyState.channelPolicies = parsed && typeof parsed === "object" ? parsed : {}; + } catch (err) { + api.logger.warn(`dirigent: failed init policy file ${filePath}: ${String(err)}`); + policyState.channelPolicies = {}; + } +} + +export function persistPolicies(api: OpenClawPluginApi): void { + if (!policyState.filePath) throw new Error("policy state not initialized"); + const dir = path.dirname(policyState.filePath); + fs.mkdirSync(dir, { recursive: true }); + const tmp = `${policyState.filePath}.tmp`; + fs.writeFileSync(tmp, `${JSON.stringify(policyState.channelPolicies, null, 2)}\n`, "utf8"); + fs.renameSync(tmp, policyState.filePath); + api.logger.info(`dirigent: policy file updated at ${policyState.filePath}`); +}