@actagent/irc 2026.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/actagent.plugin.json +26 -0
- package/api.ts +11 -0
- package/channel-config-api.ts +2 -0
- package/channel-plugin-api.ts +3 -0
- package/configured-state.ts +9 -0
- package/contract-api.ts +5 -0
- package/index.test.ts +14 -0
- package/index.ts +21 -0
- package/package.json +44 -0
- package/runtime-api.test.ts +24 -0
- package/runtime-api.ts +3 -0
- package/secret-contract-api.ts +6 -0
- package/setup-entry.ts +14 -0
- package/src/accounts.test.ts +224 -0
- package/src/accounts.ts +240 -0
- package/src/channel-api.ts +7 -0
- package/src/channel-runtime.ts +4 -0
- package/src/channel.test.ts +17 -0
- package/src/channel.ts +367 -0
- package/src/client.test.ts +44 -0
- package/src/client.ts +443 -0
- package/src/config-schema.test.ts +117 -0
- package/src/config-schema.ts +97 -0
- package/src/config-ui-hints.ts +41 -0
- package/src/connect-options.test.ts +48 -0
- package/src/connect-options.ts +31 -0
- package/src/control-chars.test.ts +18 -0
- package/src/control-chars.ts +23 -0
- package/src/doctor.ts +55 -0
- package/src/gateway.ts +54 -0
- package/src/inbound.behavior.test.ts +247 -0
- package/src/inbound.ts +440 -0
- package/src/message-adapter.ts +29 -0
- package/src/monitor.test.ts +44 -0
- package/src/monitor.ts +150 -0
- package/src/normalize.test.ts +56 -0
- package/src/normalize.ts +111 -0
- package/src/outbound-base.ts +11 -0
- package/src/policy.test.ts +56 -0
- package/src/policy.ts +79 -0
- package/src/probe.test.ts +111 -0
- package/src/probe.ts +54 -0
- package/src/protocol.test.ts +49 -0
- package/src/protocol.ts +170 -0
- package/src/runtime-api.ts +42 -0
- package/src/runtime.ts +16 -0
- package/src/secret-contract.ts +104 -0
- package/src/send.test.ts +327 -0
- package/src/send.ts +122 -0
- package/src/setup-core.ts +152 -0
- package/src/setup-surface.ts +451 -0
- package/src/setup.test.ts +487 -0
- package/src/types.ts +101 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// Irc plugin module implements setup core behavior.
|
|
2
|
+
import type { ChannelSetupAdapter, ChannelSetupInput } from "actagent/plugin-sdk/channel-setup";
|
|
3
|
+
import type { DmPolicy } from "actagent/plugin-sdk/config-contracts";
|
|
4
|
+
import { parseStrictPositiveInteger } from "actagent/plugin-sdk/number-runtime";
|
|
5
|
+
import { normalizeAccountId } from "actagent/plugin-sdk/routing";
|
|
6
|
+
import {
|
|
7
|
+
applyAccountNameToChannelSection,
|
|
8
|
+
createSetupInputPresenceValidator,
|
|
9
|
+
createTopLevelChannelAllowFromSetter,
|
|
10
|
+
createTopLevelChannelDmPolicySetter,
|
|
11
|
+
patchScopedAccountConfig,
|
|
12
|
+
} from "actagent/plugin-sdk/setup";
|
|
13
|
+
import type { CoreConfig, IrcAccountConfig, IrcNickServConfig } from "./types.js";
|
|
14
|
+
|
|
15
|
+
const channel = "irc" as const;
|
|
16
|
+
const setIrcTopLevelDmPolicy = createTopLevelChannelDmPolicySetter({
|
|
17
|
+
channel,
|
|
18
|
+
});
|
|
19
|
+
const setIrcTopLevelAllowFrom = createTopLevelChannelAllowFromSetter({
|
|
20
|
+
channel,
|
|
21
|
+
});
|
|
22
|
+
const validateIrcRequiredSetupInput = createSetupInputPresenceValidator({
|
|
23
|
+
whenNotUseEnv: [
|
|
24
|
+
{ someOf: ["host"], message: "IRC requires host." },
|
|
25
|
+
{ someOf: ["nick"], message: "IRC requires nick." },
|
|
26
|
+
],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
type IrcSetupInput = ChannelSetupInput & {
|
|
30
|
+
host?: string;
|
|
31
|
+
port?: number | string;
|
|
32
|
+
tls?: boolean;
|
|
33
|
+
nick?: string;
|
|
34
|
+
username?: string;
|
|
35
|
+
realname?: string;
|
|
36
|
+
channels?: string[];
|
|
37
|
+
password?: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function parsePort(raw: string, fallback: number): number {
|
|
41
|
+
const trimmed = raw.trim();
|
|
42
|
+
if (!trimmed) {
|
|
43
|
+
return fallback;
|
|
44
|
+
}
|
|
45
|
+
const parsed = parseStrictPositiveInteger(trimmed);
|
|
46
|
+
if (parsed === undefined || parsed > 65535) {
|
|
47
|
+
return fallback;
|
|
48
|
+
}
|
|
49
|
+
return parsed;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function validateIrcPortInput(input: ChannelSetupInput): string | null {
|
|
53
|
+
const raw = (input as IrcSetupInput).port;
|
|
54
|
+
if (raw === undefined || raw === null || raw === "") {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const parsed = parseStrictPositiveInteger(String(raw));
|
|
58
|
+
return parsed !== undefined && parsed <= 65535 ? null : "IRC port must be between 1 and 65535.";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function updateIrcAccountConfig(
|
|
62
|
+
cfg: CoreConfig,
|
|
63
|
+
accountId: string,
|
|
64
|
+
patch: Partial<IrcAccountConfig>,
|
|
65
|
+
): CoreConfig {
|
|
66
|
+
return patchScopedAccountConfig({
|
|
67
|
+
cfg,
|
|
68
|
+
channelKey: channel,
|
|
69
|
+
accountId,
|
|
70
|
+
patch,
|
|
71
|
+
ensureChannelEnabled: false,
|
|
72
|
+
ensureAccountEnabled: false,
|
|
73
|
+
}) as CoreConfig;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function setIrcDmPolicy(cfg: CoreConfig, dmPolicy: DmPolicy): CoreConfig {
|
|
77
|
+
return setIrcTopLevelDmPolicy(cfg, dmPolicy) as CoreConfig;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function setIrcAllowFrom(cfg: CoreConfig, allowFrom: string[]): CoreConfig {
|
|
81
|
+
return setIrcTopLevelAllowFrom(cfg, allowFrom) as CoreConfig;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function setIrcNickServ(
|
|
85
|
+
cfg: CoreConfig,
|
|
86
|
+
accountId: string,
|
|
87
|
+
nickserv?: IrcNickServConfig,
|
|
88
|
+
): CoreConfig {
|
|
89
|
+
return updateIrcAccountConfig(cfg, accountId, { nickserv });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function setIrcGroupAccess(
|
|
93
|
+
cfg: CoreConfig,
|
|
94
|
+
accountId: string,
|
|
95
|
+
policy: "open" | "allowlist" | "disabled",
|
|
96
|
+
entries: string[],
|
|
97
|
+
normalizeGroupEntry: (raw: string) => string | null,
|
|
98
|
+
): CoreConfig {
|
|
99
|
+
if (policy !== "allowlist") {
|
|
100
|
+
return updateIrcAccountConfig(cfg, accountId, { enabled: true, groupPolicy: policy });
|
|
101
|
+
}
|
|
102
|
+
const normalizedEntries = [
|
|
103
|
+
...new Set(entries.flatMap((entry) => normalizeGroupEntry(entry) ?? [])),
|
|
104
|
+
];
|
|
105
|
+
const groups = Object.fromEntries(normalizedEntries.map((entry) => [entry, {}]));
|
|
106
|
+
return updateIrcAccountConfig(cfg, accountId, {
|
|
107
|
+
enabled: true,
|
|
108
|
+
groupPolicy: "allowlist",
|
|
109
|
+
groups,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const ircSetupAdapter: ChannelSetupAdapter = {
|
|
114
|
+
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
|
115
|
+
applyAccountName: ({ cfg, accountId, name }) =>
|
|
116
|
+
applyAccountNameToChannelSection({
|
|
117
|
+
cfg,
|
|
118
|
+
channelKey: channel,
|
|
119
|
+
accountId,
|
|
120
|
+
name,
|
|
121
|
+
}),
|
|
122
|
+
validateInput: (params) =>
|
|
123
|
+
validateIrcRequiredSetupInput(params) ?? validateIrcPortInput(params.input),
|
|
124
|
+
applyAccountConfig: ({ cfg, accountId, input }) => {
|
|
125
|
+
const setupInput = input as IrcSetupInput;
|
|
126
|
+
const namedConfig = applyAccountNameToChannelSection({
|
|
127
|
+
cfg,
|
|
128
|
+
channelKey: channel,
|
|
129
|
+
accountId,
|
|
130
|
+
name: setupInput.name,
|
|
131
|
+
});
|
|
132
|
+
const portInput =
|
|
133
|
+
typeof setupInput.port === "number" ? String(setupInput.port) : (setupInput.port ?? "");
|
|
134
|
+
const patch: Partial<IrcAccountConfig> = {
|
|
135
|
+
enabled: true,
|
|
136
|
+
host: setupInput.host?.trim(),
|
|
137
|
+
port: portInput ? parsePort(portInput, setupInput.tls === false ? 6667 : 6697) : undefined,
|
|
138
|
+
tls: setupInput.tls,
|
|
139
|
+
nick: setupInput.nick?.trim(),
|
|
140
|
+
username: setupInput.username?.trim(),
|
|
141
|
+
realname: setupInput.realname?.trim(),
|
|
142
|
+
password: setupInput.password?.trim(),
|
|
143
|
+
channels: setupInput.channels,
|
|
144
|
+
};
|
|
145
|
+
return patchScopedAccountConfig({
|
|
146
|
+
cfg: namedConfig,
|
|
147
|
+
channelKey: channel,
|
|
148
|
+
accountId,
|
|
149
|
+
patch,
|
|
150
|
+
}) as CoreConfig;
|
|
151
|
+
},
|
|
152
|
+
};
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
// Irc plugin module implements setup surface behavior.
|
|
2
|
+
import { parseStrictPositiveInteger } from "actagent/plugin-sdk/number-runtime";
|
|
3
|
+
import { DEFAULT_ACCOUNT_ID } from "actagent/plugin-sdk/routing";
|
|
4
|
+
import type {
|
|
5
|
+
ChannelSetupDmPolicy,
|
|
6
|
+
ChannelSetupWizard,
|
|
7
|
+
WizardPrompter,
|
|
8
|
+
} from "actagent/plugin-sdk/setup";
|
|
9
|
+
import {
|
|
10
|
+
createAllowFromSection,
|
|
11
|
+
createPromptParsedAllowFromForAccount,
|
|
12
|
+
createSetupTranslator,
|
|
13
|
+
createStandardChannelSetupStatus,
|
|
14
|
+
formatDocsLink,
|
|
15
|
+
setSetupChannelEnabled,
|
|
16
|
+
} from "actagent/plugin-sdk/setup";
|
|
17
|
+
import {
|
|
18
|
+
normalizeOptionalString,
|
|
19
|
+
normalizeStringEntries,
|
|
20
|
+
normalizeStringifiedOptionalString,
|
|
21
|
+
uniqueStrings,
|
|
22
|
+
} from "actagent/plugin-sdk/string-coerce-runtime";
|
|
23
|
+
import { resolveDefaultIrcAccountId, resolveIrcAccount } from "./accounts.js";
|
|
24
|
+
import {
|
|
25
|
+
isChannelTarget,
|
|
26
|
+
normalizeIrcAllowEntry,
|
|
27
|
+
normalizeIrcMessagingTarget,
|
|
28
|
+
} from "./normalize.js";
|
|
29
|
+
import {
|
|
30
|
+
ircSetupAdapter,
|
|
31
|
+
parsePort,
|
|
32
|
+
setIrcAllowFrom,
|
|
33
|
+
setIrcDmPolicy,
|
|
34
|
+
setIrcGroupAccess,
|
|
35
|
+
setIrcNickServ,
|
|
36
|
+
updateIrcAccountConfig,
|
|
37
|
+
} from "./setup-core.js";
|
|
38
|
+
import type { CoreConfig } from "./types.js";
|
|
39
|
+
|
|
40
|
+
const t = createSetupTranslator();
|
|
41
|
+
|
|
42
|
+
const channel = "irc" as const;
|
|
43
|
+
const USE_ENV_FLAG = "__ircUseEnv";
|
|
44
|
+
const TLS_FLAG = "__ircTls";
|
|
45
|
+
|
|
46
|
+
function parseListInput(raw: string): string[] {
|
|
47
|
+
return normalizeStringEntries(raw.split(/[\n,;]+/g));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeGroupEntry(raw: string): string | null {
|
|
51
|
+
const trimmed = raw.trim();
|
|
52
|
+
if (!trimmed) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
if (trimmed === "*") {
|
|
56
|
+
return "*";
|
|
57
|
+
}
|
|
58
|
+
const normalized = normalizeIrcMessagingTarget(trimmed) ?? trimmed;
|
|
59
|
+
if (isChannelTarget(normalized)) {
|
|
60
|
+
return normalized;
|
|
61
|
+
}
|
|
62
|
+
return `#${normalized.replace(/^#+/, "")}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const promptIrcAllowFrom = createPromptParsedAllowFromForAccount<CoreConfig>({
|
|
66
|
+
defaultAccountId: (cfg) => resolveDefaultIrcAccountId(cfg),
|
|
67
|
+
noteTitle: t("wizard.irc.allowlistTitle"),
|
|
68
|
+
noteLines: [
|
|
69
|
+
t("wizard.irc.allowlistIntro"),
|
|
70
|
+
t("wizard.irc.examples"),
|
|
71
|
+
"- alice",
|
|
72
|
+
"- alice!ident@example.org",
|
|
73
|
+
t("wizard.irc.multipleEntries"),
|
|
74
|
+
],
|
|
75
|
+
message: t("wizard.irc.allowFromPrompt"),
|
|
76
|
+
placeholder: "alice, bob!ident@example.org",
|
|
77
|
+
parseEntries: (raw) => ({
|
|
78
|
+
entries: normalizeStringEntries(
|
|
79
|
+
parseListInput(raw).map((entry) => normalizeIrcAllowEntry(entry)),
|
|
80
|
+
),
|
|
81
|
+
}),
|
|
82
|
+
getExistingAllowFrom: ({ cfg }) => cfg.channels?.irc?.allowFrom ?? [],
|
|
83
|
+
applyAllowFrom: ({ cfg, allowFrom }) => setIrcAllowFrom(cfg, allowFrom),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
async function promptIrcNickServConfig(params: {
|
|
87
|
+
cfg: CoreConfig;
|
|
88
|
+
prompter: WizardPrompter;
|
|
89
|
+
accountId: string;
|
|
90
|
+
}): Promise<CoreConfig> {
|
|
91
|
+
const resolved = resolveIrcAccount({ cfg: params.cfg, accountId: params.accountId });
|
|
92
|
+
const existing = resolved.config.nickserv;
|
|
93
|
+
const hasExisting = Boolean(existing?.password || existing?.passwordFile);
|
|
94
|
+
const wants = await params.prompter.confirm({
|
|
95
|
+
message: hasExisting
|
|
96
|
+
? t("wizard.irc.nickServUpdatePrompt")
|
|
97
|
+
: t("wizard.irc.nickServConfigurePrompt"),
|
|
98
|
+
initialValue: hasExisting,
|
|
99
|
+
});
|
|
100
|
+
if (!wants) {
|
|
101
|
+
return params.cfg;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const service = (
|
|
105
|
+
await params.prompter.text({
|
|
106
|
+
message: t("wizard.irc.nickServServicePrompt"),
|
|
107
|
+
initialValue: existing?.service || "NickServ",
|
|
108
|
+
validate: (value) => (normalizeStringifiedOptionalString(value) ? undefined : "Required"),
|
|
109
|
+
})
|
|
110
|
+
).trim();
|
|
111
|
+
|
|
112
|
+
const useEnvPassword =
|
|
113
|
+
params.accountId === DEFAULT_ACCOUNT_ID &&
|
|
114
|
+
Boolean(process.env.IRC_NICKSERV_PASSWORD?.trim()) &&
|
|
115
|
+
!(existing?.password || existing?.passwordFile)
|
|
116
|
+
? await params.prompter.confirm({
|
|
117
|
+
message: t("wizard.irc.nickServPasswordEnvPrompt"),
|
|
118
|
+
initialValue: true,
|
|
119
|
+
})
|
|
120
|
+
: false;
|
|
121
|
+
|
|
122
|
+
const password = useEnvPassword
|
|
123
|
+
? undefined
|
|
124
|
+
: (
|
|
125
|
+
await params.prompter.text({
|
|
126
|
+
message: t("wizard.irc.nickServPasswordPrompt"),
|
|
127
|
+
validate: () => undefined,
|
|
128
|
+
})
|
|
129
|
+
).trim();
|
|
130
|
+
|
|
131
|
+
if (!password && !useEnvPassword) {
|
|
132
|
+
return setIrcNickServ(params.cfg, params.accountId, {
|
|
133
|
+
enabled: false,
|
|
134
|
+
service,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const register = await params.prompter.confirm({
|
|
139
|
+
message: t("wizard.irc.nickServRegisterPrompt"),
|
|
140
|
+
initialValue: existing?.register ?? false,
|
|
141
|
+
});
|
|
142
|
+
const registerEmail = register
|
|
143
|
+
? (
|
|
144
|
+
await params.prompter.text({
|
|
145
|
+
message: t("wizard.irc.nickServRegisterEmailPrompt"),
|
|
146
|
+
initialValue:
|
|
147
|
+
existing?.registerEmail ||
|
|
148
|
+
(params.accountId === DEFAULT_ACCOUNT_ID
|
|
149
|
+
? process.env.IRC_NICKSERV_REGISTER_EMAIL
|
|
150
|
+
: undefined),
|
|
151
|
+
validate: (value) => (normalizeStringifiedOptionalString(value) ? undefined : "Required"),
|
|
152
|
+
})
|
|
153
|
+
).trim()
|
|
154
|
+
: undefined;
|
|
155
|
+
|
|
156
|
+
return setIrcNickServ(params.cfg, params.accountId, {
|
|
157
|
+
enabled: true,
|
|
158
|
+
service,
|
|
159
|
+
...(password ? { password } : {}),
|
|
160
|
+
register,
|
|
161
|
+
...(registerEmail ? { registerEmail } : {}),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const ircDmPolicy: ChannelSetupDmPolicy = {
|
|
166
|
+
label: "IRC",
|
|
167
|
+
channel,
|
|
168
|
+
policyKey: "channels.irc.dmPolicy",
|
|
169
|
+
allowFromKey: "channels.irc.allowFrom",
|
|
170
|
+
getCurrent: (cfg) => (cfg as CoreConfig).channels?.irc?.dmPolicy ?? "pairing",
|
|
171
|
+
setPolicy: (cfg, policy) => setIrcDmPolicy(cfg as CoreConfig, policy),
|
|
172
|
+
promptAllowFrom: async ({ cfg, prompter, accountId }) =>
|
|
173
|
+
await promptIrcAllowFrom({
|
|
174
|
+
cfg: cfg as CoreConfig,
|
|
175
|
+
prompter,
|
|
176
|
+
accountId,
|
|
177
|
+
}),
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export const ircSetupWizard: ChannelSetupWizard = {
|
|
181
|
+
channel,
|
|
182
|
+
status: createStandardChannelSetupStatus({
|
|
183
|
+
channelLabel: "IRC",
|
|
184
|
+
configuredLabel: t("wizard.channels.statusConfigured"),
|
|
185
|
+
unconfiguredLabel: t("wizard.channels.statusNeedsHostNick"),
|
|
186
|
+
configuredHint: t("wizard.channels.statusConfigured"),
|
|
187
|
+
unconfiguredHint: t("wizard.channels.statusNeedsHostNick"),
|
|
188
|
+
configuredScore: 1,
|
|
189
|
+
unconfiguredScore: 0,
|
|
190
|
+
includeStatusLine: true,
|
|
191
|
+
resolveConfigured: ({ cfg, accountId }) =>
|
|
192
|
+
resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }).configured,
|
|
193
|
+
}),
|
|
194
|
+
introNote: {
|
|
195
|
+
title: t("wizard.irc.setupTitle"),
|
|
196
|
+
lines: [
|
|
197
|
+
t("wizard.irc.helpNeedsHostNick"),
|
|
198
|
+
t("wizard.irc.helpRecommendedTls"),
|
|
199
|
+
t("wizard.irc.helpNickServOptional"),
|
|
200
|
+
t("wizard.irc.helpGroupControl"),
|
|
201
|
+
t("wizard.irc.helpMentionGate"),
|
|
202
|
+
t("wizard.irc.helpEnvVars"),
|
|
203
|
+
`Docs: ${formatDocsLink("/channels/irc", "channels/irc")}`,
|
|
204
|
+
],
|
|
205
|
+
shouldShow: ({ cfg, accountId }) =>
|
|
206
|
+
!resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }).configured,
|
|
207
|
+
},
|
|
208
|
+
prepare: async ({ cfg, accountId, credentialValues, prompter }) => {
|
|
209
|
+
const resolved = resolveIrcAccount({ cfg: cfg as CoreConfig, accountId });
|
|
210
|
+
const isDefaultAccount = accountId === DEFAULT_ACCOUNT_ID;
|
|
211
|
+
const envHost = isDefaultAccount ? (normalizeOptionalString(process.env.IRC_HOST) ?? "") : "";
|
|
212
|
+
const envNick = isDefaultAccount ? (normalizeOptionalString(process.env.IRC_NICK) ?? "") : "";
|
|
213
|
+
const envReady = Boolean(envHost && envNick && !resolved.config.host && !resolved.config.nick);
|
|
214
|
+
|
|
215
|
+
if (envReady) {
|
|
216
|
+
const useEnv = await prompter.confirm({
|
|
217
|
+
message: t("wizard.irc.envPrompt"),
|
|
218
|
+
initialValue: true,
|
|
219
|
+
});
|
|
220
|
+
if (useEnv) {
|
|
221
|
+
return {
|
|
222
|
+
cfg: updateIrcAccountConfig(cfg as CoreConfig, accountId, { enabled: true }),
|
|
223
|
+
credentialValues: {
|
|
224
|
+
...credentialValues,
|
|
225
|
+
[USE_ENV_FLAG]: "1",
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const tls = await prompter.confirm({
|
|
232
|
+
message: t("wizard.irc.tlsPrompt"),
|
|
233
|
+
initialValue: resolved.config.tls ?? true,
|
|
234
|
+
});
|
|
235
|
+
return {
|
|
236
|
+
cfg: updateIrcAccountConfig(cfg as CoreConfig, accountId, {
|
|
237
|
+
enabled: true,
|
|
238
|
+
tls,
|
|
239
|
+
}),
|
|
240
|
+
credentialValues: {
|
|
241
|
+
...credentialValues,
|
|
242
|
+
[USE_ENV_FLAG]: "0",
|
|
243
|
+
[TLS_FLAG]: tls ? "1" : "0",
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
},
|
|
247
|
+
credentials: [],
|
|
248
|
+
textInputs: [
|
|
249
|
+
{
|
|
250
|
+
inputKey: "httpHost",
|
|
251
|
+
message: t("wizard.irc.serverHostPrompt"),
|
|
252
|
+
currentValue: ({ cfg, accountId }) =>
|
|
253
|
+
resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }).config.host || undefined,
|
|
254
|
+
shouldPrompt: ({ credentialValues }) => credentialValues[USE_ENV_FLAG] !== "1",
|
|
255
|
+
validate: ({ value }) => (normalizeStringifiedOptionalString(value) ? undefined : "Required"),
|
|
256
|
+
normalizeValue: ({ value }) => normalizeStringifiedOptionalString(value) ?? "",
|
|
257
|
+
applySet: async ({ cfg, accountId, value }) =>
|
|
258
|
+
updateIrcAccountConfig(cfg as CoreConfig, accountId, {
|
|
259
|
+
enabled: true,
|
|
260
|
+
host: value,
|
|
261
|
+
}),
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
inputKey: "httpPort",
|
|
265
|
+
message: t("wizard.irc.serverPortPrompt"),
|
|
266
|
+
currentValue: ({ cfg, accountId }) =>
|
|
267
|
+
String(resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }).config.port ?? ""),
|
|
268
|
+
shouldPrompt: ({ credentialValues }) => credentialValues[USE_ENV_FLAG] !== "1",
|
|
269
|
+
initialValue: ({ cfg, accountId, credentialValues }) => {
|
|
270
|
+
const resolved = resolveIrcAccount({ cfg: cfg as CoreConfig, accountId });
|
|
271
|
+
const tls = credentialValues[TLS_FLAG] !== "0";
|
|
272
|
+
const defaultPort = resolved.config.port ?? (tls ? 6697 : 6667);
|
|
273
|
+
return String(defaultPort);
|
|
274
|
+
},
|
|
275
|
+
validate: ({ value }) => {
|
|
276
|
+
const raw = normalizeStringifiedOptionalString(value) ?? "";
|
|
277
|
+
const parsed = parseStrictPositiveInteger(raw);
|
|
278
|
+
return parsed !== undefined && parsed <= 65535
|
|
279
|
+
? undefined
|
|
280
|
+
: "Use a port between 1 and 65535";
|
|
281
|
+
},
|
|
282
|
+
normalizeValue: ({ value }) => String(parsePort(value, 6697)),
|
|
283
|
+
applySet: async ({ cfg, accountId, value }) =>
|
|
284
|
+
updateIrcAccountConfig(cfg as CoreConfig, accountId, {
|
|
285
|
+
enabled: true,
|
|
286
|
+
port: parsePort(value, 6697),
|
|
287
|
+
}),
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
inputKey: "token",
|
|
291
|
+
message: t("wizard.irc.nickPrompt"),
|
|
292
|
+
currentValue: ({ cfg, accountId }) =>
|
|
293
|
+
resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }).config.nick || undefined,
|
|
294
|
+
shouldPrompt: ({ credentialValues }) => credentialValues[USE_ENV_FLAG] !== "1",
|
|
295
|
+
validate: ({ value }) => (normalizeStringifiedOptionalString(value) ? undefined : "Required"),
|
|
296
|
+
normalizeValue: ({ value }) => normalizeStringifiedOptionalString(value) ?? "",
|
|
297
|
+
applySet: async ({ cfg, accountId, value }) =>
|
|
298
|
+
updateIrcAccountConfig(cfg as CoreConfig, accountId, {
|
|
299
|
+
enabled: true,
|
|
300
|
+
nick: value,
|
|
301
|
+
}),
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
inputKey: "userId",
|
|
305
|
+
message: t("wizard.irc.usernamePrompt"),
|
|
306
|
+
currentValue: ({ cfg, accountId }) =>
|
|
307
|
+
resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }).config.username || undefined,
|
|
308
|
+
shouldPrompt: ({ credentialValues }) => credentialValues[USE_ENV_FLAG] !== "1",
|
|
309
|
+
initialValue: ({ cfg, accountId, credentialValues }) =>
|
|
310
|
+
resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }).config.username ||
|
|
311
|
+
credentialValues.token ||
|
|
312
|
+
"actagent",
|
|
313
|
+
validate: ({ value }) => (normalizeStringifiedOptionalString(value) ? undefined : "Required"),
|
|
314
|
+
normalizeValue: ({ value }) => normalizeStringifiedOptionalString(value) ?? "",
|
|
315
|
+
applySet: async ({ cfg, accountId, value }) =>
|
|
316
|
+
updateIrcAccountConfig(cfg as CoreConfig, accountId, {
|
|
317
|
+
enabled: true,
|
|
318
|
+
username: value,
|
|
319
|
+
}),
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
inputKey: "deviceName",
|
|
323
|
+
message: t("wizard.irc.realNamePrompt"),
|
|
324
|
+
currentValue: ({ cfg, accountId }) =>
|
|
325
|
+
resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }).config.realname || undefined,
|
|
326
|
+
shouldPrompt: ({ credentialValues }) => credentialValues[USE_ENV_FLAG] !== "1",
|
|
327
|
+
initialValue: ({ cfg, accountId }) =>
|
|
328
|
+
resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }).config.realname || "ACTAgent",
|
|
329
|
+
validate: ({ value }) => (normalizeStringifiedOptionalString(value) ? undefined : "Required"),
|
|
330
|
+
normalizeValue: ({ value }) => normalizeStringifiedOptionalString(value) ?? "",
|
|
331
|
+
applySet: async ({ cfg, accountId, value }) =>
|
|
332
|
+
updateIrcAccountConfig(cfg as CoreConfig, accountId, {
|
|
333
|
+
enabled: true,
|
|
334
|
+
realname: value,
|
|
335
|
+
}),
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
inputKey: "groupChannels",
|
|
339
|
+
message: t("wizard.irc.autoJoinPrompt"),
|
|
340
|
+
placeholder: "#actagent, #ops",
|
|
341
|
+
required: false,
|
|
342
|
+
applyEmptyValue: true,
|
|
343
|
+
currentValue: ({ cfg, accountId }) =>
|
|
344
|
+
resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }).config.channels?.join(", "),
|
|
345
|
+
shouldPrompt: ({ credentialValues }) => credentialValues[USE_ENV_FLAG] !== "1",
|
|
346
|
+
normalizeValue: ({ value }) =>
|
|
347
|
+
parseListInput(value)
|
|
348
|
+
.map((entry) => normalizeGroupEntry(entry))
|
|
349
|
+
.filter((entry): entry is string => Boolean(entry && entry !== "*"))
|
|
350
|
+
.filter((entry) => isChannelTarget(entry))
|
|
351
|
+
.join(", "),
|
|
352
|
+
applySet: async ({ cfg, accountId, value }) => {
|
|
353
|
+
const channels = parseListInput(value)
|
|
354
|
+
.map((entry) => normalizeGroupEntry(entry))
|
|
355
|
+
.filter((entry): entry is string => Boolean(entry && entry !== "*"))
|
|
356
|
+
.filter((entry) => isChannelTarget(entry));
|
|
357
|
+
return updateIrcAccountConfig(cfg as CoreConfig, accountId, {
|
|
358
|
+
enabled: true,
|
|
359
|
+
channels: channels.length > 0 ? channels : undefined,
|
|
360
|
+
});
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
],
|
|
364
|
+
groupAccess: {
|
|
365
|
+
label: "IRC channels",
|
|
366
|
+
placeholder: "#actagent, #ops, *",
|
|
367
|
+
currentPolicy: ({ cfg, accountId }) =>
|
|
368
|
+
resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }).config.groupPolicy ?? "allowlist",
|
|
369
|
+
currentEntries: ({ cfg, accountId }) =>
|
|
370
|
+
Object.keys(resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }).config.groups ?? {}),
|
|
371
|
+
updatePrompt: ({ cfg, accountId }) =>
|
|
372
|
+
Boolean(resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }).config.groups),
|
|
373
|
+
setPolicy: ({ cfg, accountId, policy }) =>
|
|
374
|
+
setIrcGroupAccess(cfg as CoreConfig, accountId, policy, [], normalizeGroupEntry),
|
|
375
|
+
resolveAllowlist: async ({ entries }) =>
|
|
376
|
+
uniqueStrings(
|
|
377
|
+
entries
|
|
378
|
+
.map((entry) => normalizeGroupEntry(entry))
|
|
379
|
+
.filter((entry): entry is string => Boolean(entry)),
|
|
380
|
+
),
|
|
381
|
+
applyAllowlist: ({ cfg, accountId, resolved }) =>
|
|
382
|
+
setIrcGroupAccess(
|
|
383
|
+
cfg as CoreConfig,
|
|
384
|
+
accountId,
|
|
385
|
+
"allowlist",
|
|
386
|
+
resolved as string[],
|
|
387
|
+
normalizeGroupEntry,
|
|
388
|
+
),
|
|
389
|
+
},
|
|
390
|
+
allowFrom: createAllowFromSection({
|
|
391
|
+
helpTitle: t("wizard.irc.allowlistTitle"),
|
|
392
|
+
helpLines: [
|
|
393
|
+
t("wizard.irc.allowlistIntro"),
|
|
394
|
+
t("wizard.irc.examples"),
|
|
395
|
+
"- alice",
|
|
396
|
+
"- alice!ident@example.org",
|
|
397
|
+
t("wizard.irc.multipleEntries"),
|
|
398
|
+
],
|
|
399
|
+
message: t("wizard.irc.allowFromPrompt"),
|
|
400
|
+
placeholder: "alice, bob!ident@example.org",
|
|
401
|
+
invalidWithoutCredentialNote: t("wizard.irc.allowFromInvalid"),
|
|
402
|
+
parseId: (raw) => {
|
|
403
|
+
const normalized = normalizeIrcAllowEntry(raw);
|
|
404
|
+
return normalized || null;
|
|
405
|
+
},
|
|
406
|
+
apply: async ({ cfg, allowFrom }) => setIrcAllowFrom(cfg as CoreConfig, allowFrom),
|
|
407
|
+
}),
|
|
408
|
+
finalize: async ({ cfg, accountId, prompter }) => {
|
|
409
|
+
let next = cfg as CoreConfig;
|
|
410
|
+
|
|
411
|
+
const resolvedAfterGroups = resolveIrcAccount({ cfg: next, accountId });
|
|
412
|
+
if (resolvedAfterGroups.config.groupPolicy === "allowlist") {
|
|
413
|
+
const groupKeys = Object.keys(resolvedAfterGroups.config.groups ?? {});
|
|
414
|
+
if (groupKeys.length > 0) {
|
|
415
|
+
const wantsMentions = await prompter.confirm({
|
|
416
|
+
message: t("wizard.irc.requireMentionPrompt"),
|
|
417
|
+
initialValue: true,
|
|
418
|
+
});
|
|
419
|
+
if (!wantsMentions) {
|
|
420
|
+
const groups = resolvedAfterGroups.config.groups ?? {};
|
|
421
|
+
const patched = Object.fromEntries(
|
|
422
|
+
Object.entries(groups).map(([key, value]) => [
|
|
423
|
+
key,
|
|
424
|
+
{ ...value, requireMention: false },
|
|
425
|
+
]),
|
|
426
|
+
);
|
|
427
|
+
next = updateIrcAccountConfig(next, accountId, { groups: patched });
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
next = await promptIrcNickServConfig({
|
|
433
|
+
cfg: next,
|
|
434
|
+
prompter,
|
|
435
|
+
accountId,
|
|
436
|
+
});
|
|
437
|
+
return { cfg: next };
|
|
438
|
+
},
|
|
439
|
+
completionNote: {
|
|
440
|
+
title: t("wizard.irc.nextStepsTitle"),
|
|
441
|
+
lines: [
|
|
442
|
+
t("wizard.irc.nextRestartGateway"),
|
|
443
|
+
t("wizard.irc.nextStatusCommand"),
|
|
444
|
+
`Docs: ${formatDocsLink("/channels/irc", "channels/irc")}`,
|
|
445
|
+
],
|
|
446
|
+
},
|
|
447
|
+
dmPolicy: ircDmPolicy,
|
|
448
|
+
disable: (cfg) => setSetupChannelEnabled(cfg, channel, false),
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
export { ircSetupAdapter };
|