mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-16 20:40:45 +00:00
refactor: move irc to setup wizard
This commit is contained in:
@@ -32,11 +32,11 @@ import {
|
||||
isChannelTarget,
|
||||
normalizeIrcAllowEntry,
|
||||
} from "./normalize.js";
|
||||
import { ircOnboardingAdapter } from "./onboarding.js";
|
||||
import { resolveIrcGroupMatch, resolveIrcRequireMention } from "./policy.js";
|
||||
import { probeIrc } from "./probe.js";
|
||||
import { getIrcRuntime } from "./runtime.js";
|
||||
import { sendMessageIrc } from "./send.js";
|
||||
import { ircSetupAdapter, ircSetupWizard } from "./setup-surface.js";
|
||||
import type { CoreConfig, IrcProbe } from "./types.js";
|
||||
|
||||
const meta = getChatChannelMeta("irc");
|
||||
@@ -66,7 +66,8 @@ export const ircPlugin: ChannelPlugin<ResolvedIrcAccount, IrcProbe> = {
|
||||
...meta,
|
||||
quickstartAllowFrom: true,
|
||||
},
|
||||
onboarding: ircOnboardingAdapter,
|
||||
setup: ircSetupAdapter,
|
||||
setupWizard: ircSetupWizard,
|
||||
pairing: {
|
||||
idLabel: "ircUser",
|
||||
normalizeAllowEntry: (entry) => normalizeIrcAllowEntry(entry),
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { RuntimeEnv, WizardPrompter } from "openclaw/plugin-sdk/irc";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { buildChannelOnboardingAdapterFromSetupWizard } from "../../../src/channels/plugins/setup-wizard.js";
|
||||
import { createRuntimeEnv } from "../../test-utils/runtime-env.js";
|
||||
import { ircOnboardingAdapter } from "./onboarding.js";
|
||||
import { ircPlugin } from "./channel.js";
|
||||
import type { CoreConfig } from "./types.js";
|
||||
|
||||
const selectFirstOption = async <T>(params: { options: Array<{ value: T }> }): Promise<T> => {
|
||||
@@ -26,7 +27,12 @@ function createPrompter(overrides: Partial<WizardPrompter>): WizardPrompter {
|
||||
};
|
||||
}
|
||||
|
||||
describe("irc onboarding", () => {
|
||||
const ircConfigureAdapter = buildChannelOnboardingAdapterFromSetupWizard({
|
||||
plugin: ircPlugin,
|
||||
wizard: ircPlugin.setupWizard!,
|
||||
});
|
||||
|
||||
describe("irc setup wizard", () => {
|
||||
it("configures host and nick via onboarding prompts", async () => {
|
||||
const prompter = createPrompter({
|
||||
text: vi.fn(async ({ message }: { message: string }) => {
|
||||
@@ -66,7 +72,7 @@ describe("irc onboarding", () => {
|
||||
|
||||
const runtime: RuntimeEnv = createRuntimeEnv();
|
||||
|
||||
const result = await ircOnboardingAdapter.configure({
|
||||
const result = await ircConfigureAdapter.configure({
|
||||
cfg: {} as CoreConfig,
|
||||
runtime,
|
||||
prompter,
|
||||
@@ -97,7 +103,7 @@ describe("irc onboarding", () => {
|
||||
confirm: vi.fn(async () => false),
|
||||
});
|
||||
|
||||
const promptAllowFrom = ircOnboardingAdapter.dmPolicy?.promptAllowFrom;
|
||||
const promptAllowFrom = ircConfigureAdapter.dmPolicy?.promptAllowFrom;
|
||||
expect(promptAllowFrom).toBeTypeOf("function");
|
||||
|
||||
const cfg: CoreConfig = {
|
||||
|
||||
@@ -1,444 +0,0 @@
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
formatDocsLink,
|
||||
patchScopedAccountConfig,
|
||||
promptChannelAccessConfig,
|
||||
resolveAccountIdForConfigure,
|
||||
setTopLevelChannelAllowFrom,
|
||||
setTopLevelChannelDmPolicyWithAllowFrom,
|
||||
type ChannelOnboardingAdapter,
|
||||
type ChannelOnboardingDmPolicy,
|
||||
type DmPolicy,
|
||||
type WizardPrompter,
|
||||
} from "openclaw/plugin-sdk/irc";
|
||||
import { listIrcAccountIds, resolveDefaultIrcAccountId, resolveIrcAccount } from "./accounts.js";
|
||||
import {
|
||||
isChannelTarget,
|
||||
normalizeIrcAllowEntry,
|
||||
normalizeIrcMessagingTarget,
|
||||
} from "./normalize.js";
|
||||
import type { CoreConfig, IrcAccountConfig, IrcNickServConfig } from "./types.js";
|
||||
|
||||
const channel = "irc" as const;
|
||||
|
||||
function parseListInput(raw: string): string[] {
|
||||
return raw
|
||||
.split(/[\n,;]+/g)
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function parsePort(raw: string, fallback: number): number {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
return fallback;
|
||||
}
|
||||
const parsed = Number.parseInt(trimmed, 10);
|
||||
if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) {
|
||||
return fallback;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function normalizeGroupEntry(raw: string): string | null {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
if (trimmed === "*") {
|
||||
return "*";
|
||||
}
|
||||
const normalized = normalizeIrcMessagingTarget(trimmed) ?? trimmed;
|
||||
if (isChannelTarget(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
return `#${normalized.replace(/^#+/, "")}`;
|
||||
}
|
||||
|
||||
function updateIrcAccountConfig(
|
||||
cfg: CoreConfig,
|
||||
accountId: string,
|
||||
patch: Partial<IrcAccountConfig>,
|
||||
): CoreConfig {
|
||||
return patchScopedAccountConfig({
|
||||
cfg,
|
||||
channelKey: channel,
|
||||
accountId,
|
||||
patch,
|
||||
ensureChannelEnabled: false,
|
||||
ensureAccountEnabled: false,
|
||||
}) as CoreConfig;
|
||||
}
|
||||
|
||||
function setIrcDmPolicy(cfg: CoreConfig, dmPolicy: DmPolicy): CoreConfig {
|
||||
return setTopLevelChannelDmPolicyWithAllowFrom({
|
||||
cfg,
|
||||
channel: "irc",
|
||||
dmPolicy,
|
||||
}) as CoreConfig;
|
||||
}
|
||||
|
||||
function setIrcAllowFrom(cfg: CoreConfig, allowFrom: string[]): CoreConfig {
|
||||
return setTopLevelChannelAllowFrom({
|
||||
cfg,
|
||||
channel: "irc",
|
||||
allowFrom,
|
||||
}) as CoreConfig;
|
||||
}
|
||||
|
||||
function setIrcNickServ(
|
||||
cfg: CoreConfig,
|
||||
accountId: string,
|
||||
nickserv?: IrcNickServConfig,
|
||||
): CoreConfig {
|
||||
return updateIrcAccountConfig(cfg, accountId, { nickserv });
|
||||
}
|
||||
|
||||
function setIrcGroupAccess(
|
||||
cfg: CoreConfig,
|
||||
accountId: string,
|
||||
policy: "open" | "allowlist" | "disabled",
|
||||
entries: string[],
|
||||
): CoreConfig {
|
||||
if (policy !== "allowlist") {
|
||||
return updateIrcAccountConfig(cfg, accountId, { enabled: true, groupPolicy: policy });
|
||||
}
|
||||
const normalizedEntries = [
|
||||
...new Set(entries.map((entry) => normalizeGroupEntry(entry)).filter(Boolean)),
|
||||
];
|
||||
const groups = Object.fromEntries(normalizedEntries.map((entry) => [entry, {}]));
|
||||
return updateIrcAccountConfig(cfg, accountId, {
|
||||
enabled: true,
|
||||
groupPolicy: "allowlist",
|
||||
groups,
|
||||
});
|
||||
}
|
||||
|
||||
async function noteIrcSetupHelp(prompter: WizardPrompter): Promise<void> {
|
||||
await prompter.note(
|
||||
[
|
||||
"IRC needs server host + bot nick.",
|
||||
"Recommended: TLS on port 6697.",
|
||||
"Optional: NickServ identify/register can be configured in onboarding.",
|
||||
'Set channels.irc.groupPolicy="allowlist" and channels.irc.groups for tighter channel control.',
|
||||
'Note: IRC channels are mention-gated by default. To allow unmentioned replies, set channels.irc.groups["#channel"].requireMention=false (or "*" for all).',
|
||||
"Env vars supported: IRC_HOST, IRC_PORT, IRC_TLS, IRC_NICK, IRC_USERNAME, IRC_REALNAME, IRC_PASSWORD, IRC_CHANNELS, IRC_NICKSERV_PASSWORD, IRC_NICKSERV_REGISTER_EMAIL.",
|
||||
`Docs: ${formatDocsLink("/channels/irc", "channels/irc")}`,
|
||||
].join("\n"),
|
||||
"IRC setup",
|
||||
);
|
||||
}
|
||||
|
||||
async function promptIrcAllowFrom(params: {
|
||||
cfg: CoreConfig;
|
||||
prompter: WizardPrompter;
|
||||
accountId?: string;
|
||||
}): Promise<CoreConfig> {
|
||||
const existing = params.cfg.channels?.irc?.allowFrom ?? [];
|
||||
|
||||
await params.prompter.note(
|
||||
[
|
||||
"Allowlist IRC DMs by sender.",
|
||||
"Examples:",
|
||||
"- alice",
|
||||
"- alice!ident@example.org",
|
||||
"Multiple entries: comma-separated.",
|
||||
].join("\n"),
|
||||
"IRC allowlist",
|
||||
);
|
||||
|
||||
const raw = await params.prompter.text({
|
||||
message: "IRC allowFrom (nick or nick!user@host)",
|
||||
placeholder: "alice, bob!ident@example.org",
|
||||
initialValue: existing[0] ? String(existing[0]) : undefined,
|
||||
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||
});
|
||||
|
||||
const parsed = parseListInput(String(raw));
|
||||
const normalized = [
|
||||
...new Set(
|
||||
parsed
|
||||
.map((entry) => normalizeIrcAllowEntry(entry))
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean),
|
||||
),
|
||||
];
|
||||
return setIrcAllowFrom(params.cfg, normalized);
|
||||
}
|
||||
|
||||
async function promptIrcNickServConfig(params: {
|
||||
cfg: CoreConfig;
|
||||
prompter: WizardPrompter;
|
||||
accountId: string;
|
||||
}): Promise<CoreConfig> {
|
||||
const resolved = resolveIrcAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||
const existing = resolved.config.nickserv;
|
||||
const hasExisting = Boolean(existing?.password || existing?.passwordFile);
|
||||
const wants = await params.prompter.confirm({
|
||||
message: hasExisting ? "Update NickServ settings?" : "Configure NickServ identify/register?",
|
||||
initialValue: hasExisting,
|
||||
});
|
||||
if (!wants) {
|
||||
return params.cfg;
|
||||
}
|
||||
|
||||
const service = String(
|
||||
await params.prompter.text({
|
||||
message: "NickServ service nick",
|
||||
initialValue: existing?.service || "NickServ",
|
||||
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
|
||||
const useEnvPassword =
|
||||
params.accountId === DEFAULT_ACCOUNT_ID &&
|
||||
Boolean(process.env.IRC_NICKSERV_PASSWORD?.trim()) &&
|
||||
!(existing?.password || existing?.passwordFile)
|
||||
? await params.prompter.confirm({
|
||||
message: "IRC_NICKSERV_PASSWORD detected. Use env var?",
|
||||
initialValue: true,
|
||||
})
|
||||
: false;
|
||||
|
||||
const password = useEnvPassword
|
||||
? undefined
|
||||
: String(
|
||||
await params.prompter.text({
|
||||
message: "NickServ password (blank to disable NickServ auth)",
|
||||
validate: () => undefined,
|
||||
}),
|
||||
).trim();
|
||||
|
||||
if (!password && !useEnvPassword) {
|
||||
return setIrcNickServ(params.cfg, params.accountId, {
|
||||
enabled: false,
|
||||
service,
|
||||
});
|
||||
}
|
||||
|
||||
const register = await params.prompter.confirm({
|
||||
message: "Send NickServ REGISTER on connect?",
|
||||
initialValue: existing?.register ?? false,
|
||||
});
|
||||
const registerEmail = register
|
||||
? String(
|
||||
await params.prompter.text({
|
||||
message: "NickServ register email",
|
||||
initialValue:
|
||||
existing?.registerEmail ||
|
||||
(params.accountId === DEFAULT_ACCOUNT_ID
|
||||
? process.env.IRC_NICKSERV_REGISTER_EMAIL
|
||||
: undefined),
|
||||
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim()
|
||||
: undefined;
|
||||
|
||||
return setIrcNickServ(params.cfg, params.accountId, {
|
||||
enabled: true,
|
||||
service,
|
||||
...(password ? { password } : {}),
|
||||
register,
|
||||
...(registerEmail ? { registerEmail } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
const dmPolicy: ChannelOnboardingDmPolicy = {
|
||||
label: "IRC",
|
||||
channel,
|
||||
policyKey: "channels.irc.dmPolicy",
|
||||
allowFromKey: "channels.irc.allowFrom",
|
||||
getCurrent: (cfg) => (cfg as CoreConfig).channels?.irc?.dmPolicy ?? "pairing",
|
||||
setPolicy: (cfg, policy) => setIrcDmPolicy(cfg as CoreConfig, policy),
|
||||
promptAllowFrom: promptIrcAllowFrom,
|
||||
};
|
||||
|
||||
export const ircOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
channel,
|
||||
getStatus: async ({ cfg }) => {
|
||||
const coreCfg = cfg as CoreConfig;
|
||||
const configured = listIrcAccountIds(coreCfg).some(
|
||||
(accountId) => resolveIrcAccount({ cfg: coreCfg, accountId }).configured,
|
||||
);
|
||||
return {
|
||||
channel,
|
||||
configured,
|
||||
statusLines: [`IRC: ${configured ? "configured" : "needs host + nick"}`],
|
||||
selectionHint: configured ? "configured" : "needs host + nick",
|
||||
quickstartScore: configured ? 1 : 0,
|
||||
};
|
||||
},
|
||||
configure: async ({
|
||||
cfg,
|
||||
prompter,
|
||||
accountOverrides,
|
||||
shouldPromptAccountIds,
|
||||
forceAllowFrom,
|
||||
}) => {
|
||||
let next = cfg as CoreConfig;
|
||||
const defaultAccountId = resolveDefaultIrcAccountId(next);
|
||||
const accountId = await resolveAccountIdForConfigure({
|
||||
cfg: next,
|
||||
prompter,
|
||||
label: "IRC",
|
||||
accountOverride: accountOverrides.irc,
|
||||
shouldPromptAccountIds,
|
||||
listAccountIds: listIrcAccountIds,
|
||||
defaultAccountId,
|
||||
});
|
||||
|
||||
const resolved = resolveIrcAccount({ cfg: next, accountId });
|
||||
const isDefaultAccount = accountId === DEFAULT_ACCOUNT_ID;
|
||||
const envHost = isDefaultAccount ? process.env.IRC_HOST?.trim() : "";
|
||||
const envNick = isDefaultAccount ? process.env.IRC_NICK?.trim() : "";
|
||||
const envReady = Boolean(envHost && envNick);
|
||||
|
||||
if (!resolved.configured) {
|
||||
await noteIrcSetupHelp(prompter);
|
||||
}
|
||||
|
||||
let useEnv = false;
|
||||
if (envReady && isDefaultAccount && !resolved.config.host && !resolved.config.nick) {
|
||||
useEnv = await prompter.confirm({
|
||||
message: "IRC_HOST and IRC_NICK detected. Use env vars?",
|
||||
initialValue: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (useEnv) {
|
||||
next = updateIrcAccountConfig(next, accountId, { enabled: true });
|
||||
} else {
|
||||
const host = String(
|
||||
await prompter.text({
|
||||
message: "IRC server host",
|
||||
initialValue: resolved.config.host || envHost || undefined,
|
||||
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
|
||||
const tls = await prompter.confirm({
|
||||
message: "Use TLS for IRC?",
|
||||
initialValue: resolved.config.tls ?? true,
|
||||
});
|
||||
const defaultPort = resolved.config.port ?? (tls ? 6697 : 6667);
|
||||
const portInput = await prompter.text({
|
||||
message: "IRC server port",
|
||||
initialValue: String(defaultPort),
|
||||
validate: (value) => {
|
||||
const parsed = Number.parseInt(String(value ?? "").trim(), 10);
|
||||
return Number.isFinite(parsed) && parsed >= 1 && parsed <= 65535
|
||||
? undefined
|
||||
: "Use a port between 1 and 65535";
|
||||
},
|
||||
});
|
||||
const port = parsePort(String(portInput), defaultPort);
|
||||
|
||||
const nick = String(
|
||||
await prompter.text({
|
||||
message: "IRC nick",
|
||||
initialValue: resolved.config.nick || envNick || undefined,
|
||||
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
|
||||
const username = String(
|
||||
await prompter.text({
|
||||
message: "IRC username",
|
||||
initialValue: resolved.config.username || nick || "openclaw",
|
||||
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
|
||||
const realname = String(
|
||||
await prompter.text({
|
||||
message: "IRC real name",
|
||||
initialValue: resolved.config.realname || "OpenClaw",
|
||||
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
|
||||
const channelsRaw = await prompter.text({
|
||||
message: "Auto-join IRC channels (optional, comma-separated)",
|
||||
placeholder: "#openclaw, #ops",
|
||||
initialValue: (resolved.config.channels ?? []).join(", "),
|
||||
});
|
||||
const channels = [
|
||||
...new Set(
|
||||
parseListInput(String(channelsRaw))
|
||||
.map((entry) => normalizeGroupEntry(entry))
|
||||
.filter((entry): entry is string => Boolean(entry && entry !== "*"))
|
||||
.filter((entry) => isChannelTarget(entry)),
|
||||
),
|
||||
];
|
||||
|
||||
next = updateIrcAccountConfig(next, accountId, {
|
||||
enabled: true,
|
||||
host,
|
||||
port,
|
||||
tls,
|
||||
nick,
|
||||
username,
|
||||
realname,
|
||||
channels: channels.length > 0 ? channels : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
const afterConfig = resolveIrcAccount({ cfg: next, accountId });
|
||||
const accessConfig = await promptChannelAccessConfig({
|
||||
prompter,
|
||||
label: "IRC channels",
|
||||
currentPolicy: afterConfig.config.groupPolicy ?? "allowlist",
|
||||
currentEntries: Object.keys(afterConfig.config.groups ?? {}),
|
||||
placeholder: "#openclaw, #ops, *",
|
||||
updatePrompt: Boolean(afterConfig.config.groups),
|
||||
});
|
||||
if (accessConfig) {
|
||||
next = setIrcGroupAccess(next, accountId, accessConfig.policy, accessConfig.entries);
|
||||
|
||||
// Mention gating: groups/channels are mention-gated by default. Make this explicit in onboarding.
|
||||
const wantsMentions = await prompter.confirm({
|
||||
message: "Require @mention to reply in IRC channels?",
|
||||
initialValue: true,
|
||||
});
|
||||
if (!wantsMentions) {
|
||||
const resolvedAfter = resolveIrcAccount({ cfg: next, accountId });
|
||||
const groups = resolvedAfter.config.groups ?? {};
|
||||
const patched = Object.fromEntries(
|
||||
Object.entries(groups).map(([key, value]) => [key, { ...value, requireMention: false }]),
|
||||
);
|
||||
next = updateIrcAccountConfig(next, accountId, { groups: patched });
|
||||
}
|
||||
}
|
||||
|
||||
if (forceAllowFrom) {
|
||||
next = await promptIrcAllowFrom({ cfg: next, prompter, accountId });
|
||||
}
|
||||
next = await promptIrcNickServConfig({
|
||||
cfg: next,
|
||||
prompter,
|
||||
accountId,
|
||||
});
|
||||
|
||||
await prompter.note(
|
||||
[
|
||||
"Next: restart gateway and verify status.",
|
||||
"Command: openclaw channels status --probe",
|
||||
`Docs: ${formatDocsLink("/channels/irc", "channels/irc")}`,
|
||||
].join("\n"),
|
||||
"IRC next steps",
|
||||
);
|
||||
|
||||
return { cfg: next, accountId };
|
||||
},
|
||||
dmPolicy,
|
||||
disable: (cfg) => ({
|
||||
...(cfg as CoreConfig),
|
||||
channels: {
|
||||
...(cfg as CoreConfig).channels,
|
||||
irc: {
|
||||
...(cfg as CoreConfig).channels?.irc,
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
586
extensions/irc/src/setup-surface.ts
Normal file
586
extensions/irc/src/setup-surface.ts
Normal file
@@ -0,0 +1,586 @@
|
||||
import type { ChannelOnboardingDmPolicy } from "../../../src/channels/plugins/onboarding-types.js";
|
||||
import {
|
||||
resolveOnboardingAccountId,
|
||||
setOnboardingChannelEnabled,
|
||||
setTopLevelChannelAllowFrom,
|
||||
setTopLevelChannelDmPolicyWithAllowFrom,
|
||||
} from "../../../src/channels/plugins/onboarding/helpers.js";
|
||||
import {
|
||||
applyAccountNameToChannelSection,
|
||||
patchScopedAccountConfig,
|
||||
} from "../../../src/channels/plugins/setup-helpers.js";
|
||||
import type { ChannelSetupWizard } from "../../../src/channels/plugins/setup-wizard.js";
|
||||
import type { ChannelSetupAdapter } from "../../../src/channels/plugins/types.adapters.js";
|
||||
import type { ChannelSetupInput } from "../../../src/channels/plugins/types.core.js";
|
||||
import type { DmPolicy } from "../../../src/config/types.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../src/routing/session-key.js";
|
||||
import { formatDocsLink } from "../../../src/terminal/links.js";
|
||||
import type { WizardPrompter } from "../../../src/wizard/prompts.js";
|
||||
import { listIrcAccountIds, resolveDefaultIrcAccountId, resolveIrcAccount } from "./accounts.js";
|
||||
import {
|
||||
isChannelTarget,
|
||||
normalizeIrcAllowEntry,
|
||||
normalizeIrcMessagingTarget,
|
||||
} from "./normalize.js";
|
||||
import type { CoreConfig, IrcAccountConfig, IrcNickServConfig } from "./types.js";
|
||||
|
||||
const channel = "irc" as const;
|
||||
const USE_ENV_FLAG = "__ircUseEnv";
|
||||
const TLS_FLAG = "__ircTls";
|
||||
|
||||
type IrcSetupInput = ChannelSetupInput & {
|
||||
host?: string;
|
||||
port?: number | string;
|
||||
tls?: boolean;
|
||||
nick?: string;
|
||||
username?: string;
|
||||
realname?: string;
|
||||
channels?: string[];
|
||||
password?: string;
|
||||
};
|
||||
|
||||
function parseListInput(raw: string): string[] {
|
||||
return raw
|
||||
.split(/[\n,;]+/g)
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function parsePort(raw: string, fallback: number): number {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
return fallback;
|
||||
}
|
||||
const parsed = Number.parseInt(trimmed, 10);
|
||||
if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) {
|
||||
return fallback;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function normalizeGroupEntry(raw: string): string | null {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
if (trimmed === "*") {
|
||||
return "*";
|
||||
}
|
||||
const normalized = normalizeIrcMessagingTarget(trimmed) ?? trimmed;
|
||||
if (isChannelTarget(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
return `#${normalized.replace(/^#+/, "")}`;
|
||||
}
|
||||
|
||||
function updateIrcAccountConfig(
|
||||
cfg: CoreConfig,
|
||||
accountId: string,
|
||||
patch: Partial<IrcAccountConfig>,
|
||||
): CoreConfig {
|
||||
return patchScopedAccountConfig({
|
||||
cfg,
|
||||
channelKey: channel,
|
||||
accountId,
|
||||
patch,
|
||||
ensureChannelEnabled: false,
|
||||
ensureAccountEnabled: false,
|
||||
}) as CoreConfig;
|
||||
}
|
||||
|
||||
function setIrcDmPolicy(cfg: CoreConfig, dmPolicy: DmPolicy): CoreConfig {
|
||||
return setTopLevelChannelDmPolicyWithAllowFrom({
|
||||
cfg,
|
||||
channel,
|
||||
dmPolicy,
|
||||
}) as CoreConfig;
|
||||
}
|
||||
|
||||
function setIrcAllowFrom(cfg: CoreConfig, allowFrom: string[]): CoreConfig {
|
||||
return setTopLevelChannelAllowFrom({
|
||||
cfg,
|
||||
channel,
|
||||
allowFrom,
|
||||
}) as CoreConfig;
|
||||
}
|
||||
|
||||
function setIrcNickServ(
|
||||
cfg: CoreConfig,
|
||||
accountId: string,
|
||||
nickserv?: IrcNickServConfig,
|
||||
): CoreConfig {
|
||||
return updateIrcAccountConfig(cfg, accountId, { nickserv });
|
||||
}
|
||||
|
||||
function setIrcGroupAccess(
|
||||
cfg: CoreConfig,
|
||||
accountId: string,
|
||||
policy: "open" | "allowlist" | "disabled",
|
||||
entries: string[],
|
||||
): CoreConfig {
|
||||
if (policy !== "allowlist") {
|
||||
return updateIrcAccountConfig(cfg, accountId, { enabled: true, groupPolicy: policy });
|
||||
}
|
||||
const normalizedEntries = [
|
||||
...new Set(entries.map((entry) => normalizeGroupEntry(entry)).filter(Boolean)),
|
||||
];
|
||||
const groups = Object.fromEntries(normalizedEntries.map((entry) => [entry, {}]));
|
||||
return updateIrcAccountConfig(cfg, accountId, {
|
||||
enabled: true,
|
||||
groupPolicy: "allowlist",
|
||||
groups,
|
||||
});
|
||||
}
|
||||
|
||||
async function promptIrcAllowFrom(params: {
|
||||
cfg: CoreConfig;
|
||||
prompter: WizardPrompter;
|
||||
accountId?: string;
|
||||
}): Promise<CoreConfig> {
|
||||
const existing = params.cfg.channels?.irc?.allowFrom ?? [];
|
||||
|
||||
await params.prompter.note(
|
||||
[
|
||||
"Allowlist IRC DMs by sender.",
|
||||
"Examples:",
|
||||
"- alice",
|
||||
"- alice!ident@example.org",
|
||||
"Multiple entries: comma-separated.",
|
||||
].join("\n"),
|
||||
"IRC allowlist",
|
||||
);
|
||||
|
||||
const raw = await params.prompter.text({
|
||||
message: "IRC allowFrom (nick or nick!user@host)",
|
||||
placeholder: "alice, bob!ident@example.org",
|
||||
initialValue: existing[0] ? String(existing[0]) : undefined,
|
||||
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||
});
|
||||
|
||||
const parsed = parseListInput(String(raw));
|
||||
const normalized = [
|
||||
...new Set(
|
||||
parsed
|
||||
.map((entry) => normalizeIrcAllowEntry(entry))
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean),
|
||||
),
|
||||
];
|
||||
return setIrcAllowFrom(params.cfg, normalized);
|
||||
}
|
||||
|
||||
async function promptIrcNickServConfig(params: {
|
||||
cfg: CoreConfig;
|
||||
prompter: WizardPrompter;
|
||||
accountId: string;
|
||||
}): Promise<CoreConfig> {
|
||||
const resolved = resolveIrcAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||
const existing = resolved.config.nickserv;
|
||||
const hasExisting = Boolean(existing?.password || existing?.passwordFile);
|
||||
const wants = await params.prompter.confirm({
|
||||
message: hasExisting ? "Update NickServ settings?" : "Configure NickServ identify/register?",
|
||||
initialValue: hasExisting,
|
||||
});
|
||||
if (!wants) {
|
||||
return params.cfg;
|
||||
}
|
||||
|
||||
const service = String(
|
||||
await params.prompter.text({
|
||||
message: "NickServ service nick",
|
||||
initialValue: existing?.service || "NickServ",
|
||||
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
|
||||
const useEnvPassword =
|
||||
params.accountId === DEFAULT_ACCOUNT_ID &&
|
||||
Boolean(process.env.IRC_NICKSERV_PASSWORD?.trim()) &&
|
||||
!(existing?.password || existing?.passwordFile)
|
||||
? await params.prompter.confirm({
|
||||
message: "IRC_NICKSERV_PASSWORD detected. Use env var?",
|
||||
initialValue: true,
|
||||
})
|
||||
: false;
|
||||
|
||||
const password = useEnvPassword
|
||||
? undefined
|
||||
: String(
|
||||
await params.prompter.text({
|
||||
message: "NickServ password (blank to disable NickServ auth)",
|
||||
validate: () => undefined,
|
||||
}),
|
||||
).trim();
|
||||
|
||||
if (!password && !useEnvPassword) {
|
||||
return setIrcNickServ(params.cfg, params.accountId, {
|
||||
enabled: false,
|
||||
service,
|
||||
});
|
||||
}
|
||||
|
||||
const register = await params.prompter.confirm({
|
||||
message: "Send NickServ REGISTER on connect?",
|
||||
initialValue: existing?.register ?? false,
|
||||
});
|
||||
const registerEmail = register
|
||||
? String(
|
||||
await params.prompter.text({
|
||||
message: "NickServ register email",
|
||||
initialValue:
|
||||
existing?.registerEmail ||
|
||||
(params.accountId === DEFAULT_ACCOUNT_ID
|
||||
? process.env.IRC_NICKSERV_REGISTER_EMAIL
|
||||
: undefined),
|
||||
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim()
|
||||
: undefined;
|
||||
|
||||
return setIrcNickServ(params.cfg, params.accountId, {
|
||||
enabled: true,
|
||||
service,
|
||||
...(password ? { password } : {}),
|
||||
register,
|
||||
...(registerEmail ? { registerEmail } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
const ircDmPolicy: ChannelOnboardingDmPolicy = {
|
||||
label: "IRC",
|
||||
channel,
|
||||
policyKey: "channels.irc.dmPolicy",
|
||||
allowFromKey: "channels.irc.allowFrom",
|
||||
getCurrent: (cfg) => (cfg as CoreConfig).channels?.irc?.dmPolicy ?? "pairing",
|
||||
setPolicy: (cfg, policy) => setIrcDmPolicy(cfg as CoreConfig, policy),
|
||||
promptAllowFrom: async ({ cfg, prompter, accountId }) =>
|
||||
await promptIrcAllowFrom({
|
||||
cfg: cfg as CoreConfig,
|
||||
prompter,
|
||||
accountId: resolveOnboardingAccountId({
|
||||
accountId,
|
||||
defaultAccountId: resolveDefaultIrcAccountId(cfg as CoreConfig),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
export const ircSetupAdapter: ChannelSetupAdapter = {
|
||||
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
||||
applyAccountName: ({ cfg, accountId, name }) =>
|
||||
applyAccountNameToChannelSection({
|
||||
cfg,
|
||||
channelKey: channel,
|
||||
accountId,
|
||||
name,
|
||||
}),
|
||||
validateInput: ({ input }) => {
|
||||
const setupInput = input as IrcSetupInput;
|
||||
if (!setupInput.host?.trim()) {
|
||||
return "IRC requires host.";
|
||||
}
|
||||
if (!setupInput.nick?.trim()) {
|
||||
return "IRC requires nick.";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
applyAccountConfig: ({ cfg, accountId, input }) => {
|
||||
const setupInput = input as IrcSetupInput;
|
||||
const namedConfig = applyAccountNameToChannelSection({
|
||||
cfg,
|
||||
channelKey: channel,
|
||||
accountId,
|
||||
name: setupInput.name,
|
||||
});
|
||||
const portInput =
|
||||
typeof setupInput.port === "number" ? String(setupInput.port) : String(setupInput.port ?? "");
|
||||
const patch: Partial<IrcAccountConfig> = {
|
||||
enabled: true,
|
||||
host: setupInput.host?.trim(),
|
||||
port: portInput ? parsePort(portInput, setupInput.tls === false ? 6667 : 6697) : undefined,
|
||||
tls: setupInput.tls,
|
||||
nick: setupInput.nick?.trim(),
|
||||
username: setupInput.username?.trim(),
|
||||
realname: setupInput.realname?.trim(),
|
||||
password: setupInput.password?.trim(),
|
||||
channels: setupInput.channels,
|
||||
};
|
||||
return patchScopedAccountConfig({
|
||||
cfg: namedConfig,
|
||||
channelKey: channel,
|
||||
accountId,
|
||||
patch,
|
||||
}) as CoreConfig;
|
||||
},
|
||||
};
|
||||
|
||||
export const ircSetupWizard: ChannelSetupWizard = {
|
||||
channel,
|
||||
status: {
|
||||
configuredLabel: "configured",
|
||||
unconfiguredLabel: "needs host + nick",
|
||||
configuredHint: "configured",
|
||||
unconfiguredHint: "needs host + nick",
|
||||
configuredScore: 1,
|
||||
unconfiguredScore: 0,
|
||||
resolveConfigured: ({ cfg }) =>
|
||||
listIrcAccountIds(cfg as CoreConfig).some(
|
||||
(accountId) => resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }).configured,
|
||||
),
|
||||
resolveStatusLines: ({ configured }) => [
|
||||
`IRC: ${configured ? "configured" : "needs host + nick"}`,
|
||||
],
|
||||
},
|
||||
introNote: {
|
||||
title: "IRC setup",
|
||||
lines: [
|
||||
"IRC needs server host + bot nick.",
|
||||
"Recommended: TLS on port 6697.",
|
||||
"Optional: NickServ identify/register can be configured after the basic account fields.",
|
||||
'Set channels.irc.groupPolicy="allowlist" and channels.irc.groups for tighter channel control.',
|
||||
'Note: IRC channels are mention-gated by default. To allow unmentioned replies, set channels.irc.groups["#channel"].requireMention=false (or "*" for all).',
|
||||
"Env vars supported: IRC_HOST, IRC_PORT, IRC_TLS, IRC_NICK, IRC_USERNAME, IRC_REALNAME, IRC_PASSWORD, IRC_CHANNELS, IRC_NICKSERV_PASSWORD, IRC_NICKSERV_REGISTER_EMAIL.",
|
||||
`Docs: ${formatDocsLink("/channels/irc", "channels/irc")}`,
|
||||
],
|
||||
shouldShow: ({ cfg, accountId }) =>
|
||||
!resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }).configured,
|
||||
},
|
||||
prepare: async ({ cfg, accountId, credentialValues, prompter }) => {
|
||||
const resolved = resolveIrcAccount({ cfg: cfg as CoreConfig, accountId });
|
||||
const isDefaultAccount = accountId === DEFAULT_ACCOUNT_ID;
|
||||
const envHost = isDefaultAccount ? process.env.IRC_HOST?.trim() : "";
|
||||
const envNick = isDefaultAccount ? process.env.IRC_NICK?.trim() : "";
|
||||
const envReady = Boolean(envHost && envNick && !resolved.config.host && !resolved.config.nick);
|
||||
|
||||
if (envReady) {
|
||||
const useEnv = await prompter.confirm({
|
||||
message: "IRC_HOST and IRC_NICK detected. Use env vars?",
|
||||
initialValue: true,
|
||||
});
|
||||
if (useEnv) {
|
||||
return {
|
||||
cfg: updateIrcAccountConfig(cfg as CoreConfig, accountId, { enabled: true }),
|
||||
credentialValues: {
|
||||
...credentialValues,
|
||||
[USE_ENV_FLAG]: "1",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const tls = await prompter.confirm({
|
||||
message: "Use TLS for IRC?",
|
||||
initialValue: resolved.config.tls ?? true,
|
||||
});
|
||||
return {
|
||||
cfg: updateIrcAccountConfig(cfg as CoreConfig, accountId, {
|
||||
enabled: true,
|
||||
tls,
|
||||
}),
|
||||
credentialValues: {
|
||||
...credentialValues,
|
||||
[USE_ENV_FLAG]: "0",
|
||||
[TLS_FLAG]: tls ? "1" : "0",
|
||||
},
|
||||
};
|
||||
},
|
||||
credentials: [],
|
||||
textInputs: [
|
||||
{
|
||||
inputKey: "httpHost",
|
||||
message: "IRC server host",
|
||||
currentValue: ({ cfg, accountId }) =>
|
||||
resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }).config.host || undefined,
|
||||
shouldPrompt: ({ credentialValues }) => credentialValues[USE_ENV_FLAG] !== "1",
|
||||
validate: ({ value }) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||
normalizeValue: ({ value }) => String(value).trim(),
|
||||
applySet: async ({ cfg, accountId, value }) =>
|
||||
updateIrcAccountConfig(cfg as CoreConfig, accountId, {
|
||||
enabled: true,
|
||||
host: value,
|
||||
}),
|
||||
},
|
||||
{
|
||||
inputKey: "httpPort",
|
||||
message: "IRC server port",
|
||||
currentValue: ({ cfg, accountId }) =>
|
||||
String(resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }).config.port ?? ""),
|
||||
shouldPrompt: ({ credentialValues }) => credentialValues[USE_ENV_FLAG] !== "1",
|
||||
initialValue: ({ cfg, accountId, credentialValues }) => {
|
||||
const resolved = resolveIrcAccount({ cfg: cfg as CoreConfig, accountId });
|
||||
const tls = credentialValues[TLS_FLAG] === "0" ? false : true;
|
||||
const defaultPort = resolved.config.port ?? (tls ? 6697 : 6667);
|
||||
return String(defaultPort);
|
||||
},
|
||||
validate: ({ value }) => {
|
||||
const parsed = Number.parseInt(String(value ?? "").trim(), 10);
|
||||
return Number.isFinite(parsed) && parsed >= 1 && parsed <= 65535
|
||||
? undefined
|
||||
: "Use a port between 1 and 65535";
|
||||
},
|
||||
normalizeValue: ({ value }) => String(parsePort(String(value), 6697)),
|
||||
applySet: async ({ cfg, accountId, value }) =>
|
||||
updateIrcAccountConfig(cfg as CoreConfig, accountId, {
|
||||
enabled: true,
|
||||
port: parsePort(String(value), 6697),
|
||||
}),
|
||||
},
|
||||
{
|
||||
inputKey: "token",
|
||||
message: "IRC nick",
|
||||
currentValue: ({ cfg, accountId }) =>
|
||||
resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }).config.nick || undefined,
|
||||
shouldPrompt: ({ credentialValues }) => credentialValues[USE_ENV_FLAG] !== "1",
|
||||
validate: ({ value }) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||
normalizeValue: ({ value }) => String(value).trim(),
|
||||
applySet: async ({ cfg, accountId, value }) =>
|
||||
updateIrcAccountConfig(cfg as CoreConfig, accountId, {
|
||||
enabled: true,
|
||||
nick: value,
|
||||
}),
|
||||
},
|
||||
{
|
||||
inputKey: "userId",
|
||||
message: "IRC username",
|
||||
currentValue: ({ cfg, accountId }) =>
|
||||
resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }).config.username || undefined,
|
||||
shouldPrompt: ({ credentialValues }) => credentialValues[USE_ENV_FLAG] !== "1",
|
||||
initialValue: ({ cfg, accountId, credentialValues }) =>
|
||||
resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }).config.username ||
|
||||
credentialValues.token ||
|
||||
"openclaw",
|
||||
validate: ({ value }) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||
normalizeValue: ({ value }) => String(value).trim(),
|
||||
applySet: async ({ cfg, accountId, value }) =>
|
||||
updateIrcAccountConfig(cfg as CoreConfig, accountId, {
|
||||
enabled: true,
|
||||
username: value,
|
||||
}),
|
||||
},
|
||||
{
|
||||
inputKey: "deviceName",
|
||||
message: "IRC real name",
|
||||
currentValue: ({ cfg, accountId }) =>
|
||||
resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }).config.realname || undefined,
|
||||
shouldPrompt: ({ credentialValues }) => credentialValues[USE_ENV_FLAG] !== "1",
|
||||
initialValue: ({ cfg, accountId }) =>
|
||||
resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }).config.realname || "OpenClaw",
|
||||
validate: ({ value }) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||
normalizeValue: ({ value }) => String(value).trim(),
|
||||
applySet: async ({ cfg, accountId, value }) =>
|
||||
updateIrcAccountConfig(cfg as CoreConfig, accountId, {
|
||||
enabled: true,
|
||||
realname: value,
|
||||
}),
|
||||
},
|
||||
{
|
||||
inputKey: "groupChannels",
|
||||
message: "Auto-join IRC channels (optional, comma-separated)",
|
||||
placeholder: "#openclaw, #ops",
|
||||
required: false,
|
||||
applyEmptyValue: true,
|
||||
currentValue: ({ cfg, accountId }) =>
|
||||
resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }).config.channels?.join(", "),
|
||||
shouldPrompt: ({ credentialValues }) => credentialValues[USE_ENV_FLAG] !== "1",
|
||||
normalizeValue: ({ value }) =>
|
||||
parseListInput(String(value))
|
||||
.map((entry) => normalizeGroupEntry(entry))
|
||||
.filter((entry): entry is string => Boolean(entry && entry !== "*"))
|
||||
.filter((entry) => isChannelTarget(entry))
|
||||
.join(", "),
|
||||
applySet: async ({ cfg, accountId, value }) => {
|
||||
const channels = parseListInput(String(value))
|
||||
.map((entry) => normalizeGroupEntry(entry))
|
||||
.filter((entry): entry is string => Boolean(entry && entry !== "*"))
|
||||
.filter((entry) => isChannelTarget(entry));
|
||||
return updateIrcAccountConfig(cfg as CoreConfig, accountId, {
|
||||
enabled: true,
|
||||
channels: channels.length > 0 ? channels : undefined,
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
groupAccess: {
|
||||
label: "IRC channels",
|
||||
placeholder: "#openclaw, #ops, *",
|
||||
currentPolicy: ({ cfg, accountId }) =>
|
||||
resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }).config.groupPolicy ?? "allowlist",
|
||||
currentEntries: ({ cfg, accountId }) =>
|
||||
Object.keys(resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }).config.groups ?? {}),
|
||||
updatePrompt: ({ cfg, accountId }) =>
|
||||
Boolean(resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }).config.groups),
|
||||
setPolicy: ({ cfg, accountId, policy }) =>
|
||||
setIrcGroupAccess(cfg as CoreConfig, accountId, policy, []),
|
||||
resolveAllowlist: async ({ entries }) =>
|
||||
[...new Set(entries.map((entry) => normalizeGroupEntry(entry)).filter(Boolean))] as string[],
|
||||
applyAllowlist: ({ cfg, accountId, resolved }) =>
|
||||
setIrcGroupAccess(cfg as CoreConfig, accountId, "allowlist", resolved as string[]),
|
||||
},
|
||||
allowFrom: {
|
||||
helpTitle: "IRC allowlist",
|
||||
helpLines: [
|
||||
"Allowlist IRC DMs by sender.",
|
||||
"Examples:",
|
||||
"- alice",
|
||||
"- alice!ident@example.org",
|
||||
"Multiple entries: comma-separated.",
|
||||
],
|
||||
message: "IRC allowFrom (nick or nick!user@host)",
|
||||
placeholder: "alice, bob!ident@example.org",
|
||||
invalidWithoutCredentialNote: "Use an IRC nick or nick!user@host entry.",
|
||||
parseId: (raw) => {
|
||||
const normalized = normalizeIrcAllowEntry(raw);
|
||||
return normalized || null;
|
||||
},
|
||||
resolveEntries: async ({ entries }) =>
|
||||
entries.map((entry) => {
|
||||
const normalized = normalizeIrcAllowEntry(entry);
|
||||
return {
|
||||
input: entry,
|
||||
resolved: Boolean(normalized),
|
||||
id: normalized || null,
|
||||
};
|
||||
}),
|
||||
apply: async ({ cfg, allowFrom }) => setIrcAllowFrom(cfg as CoreConfig, allowFrom),
|
||||
},
|
||||
finalize: async ({ cfg, accountId, prompter }) => {
|
||||
let next = cfg as CoreConfig;
|
||||
|
||||
const resolvedAfterGroups = resolveIrcAccount({ cfg: next, accountId });
|
||||
if (resolvedAfterGroups.config.groupPolicy === "allowlist") {
|
||||
const groupKeys = Object.keys(resolvedAfterGroups.config.groups ?? {});
|
||||
if (groupKeys.length > 0) {
|
||||
const wantsMentions = await prompter.confirm({
|
||||
message: "Require @mention to reply in IRC channels?",
|
||||
initialValue: true,
|
||||
});
|
||||
if (!wantsMentions) {
|
||||
const groups = resolvedAfterGroups.config.groups ?? {};
|
||||
const patched = Object.fromEntries(
|
||||
Object.entries(groups).map(([key, value]) => [
|
||||
key,
|
||||
{ ...value, requireMention: false },
|
||||
]),
|
||||
);
|
||||
next = updateIrcAccountConfig(next, accountId, { groups: patched });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
next = await promptIrcNickServConfig({
|
||||
cfg: next,
|
||||
prompter,
|
||||
accountId,
|
||||
});
|
||||
return { cfg: next };
|
||||
},
|
||||
completionNote: {
|
||||
title: "IRC next steps",
|
||||
lines: [
|
||||
"Next: restart gateway and verify status.",
|
||||
"Command: openclaw channels status --probe",
|
||||
`Docs: ${formatDocsLink("/channels/irc", "channels/irc")}`,
|
||||
],
|
||||
},
|
||||
dmPolicy: ircDmPolicy,
|
||||
disable: (cfg) => setOnboardingChannelEnabled(cfg, channel, false),
|
||||
};
|
||||
Reference in New Issue
Block a user