@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.
Files changed (54) hide show
  1. package/actagent.plugin.json +26 -0
  2. package/api.ts +11 -0
  3. package/channel-config-api.ts +2 -0
  4. package/channel-plugin-api.ts +3 -0
  5. package/configured-state.ts +9 -0
  6. package/contract-api.ts +5 -0
  7. package/index.test.ts +14 -0
  8. package/index.ts +21 -0
  9. package/package.json +44 -0
  10. package/runtime-api.test.ts +24 -0
  11. package/runtime-api.ts +3 -0
  12. package/secret-contract-api.ts +6 -0
  13. package/setup-entry.ts +14 -0
  14. package/src/accounts.test.ts +224 -0
  15. package/src/accounts.ts +240 -0
  16. package/src/channel-api.ts +7 -0
  17. package/src/channel-runtime.ts +4 -0
  18. package/src/channel.test.ts +17 -0
  19. package/src/channel.ts +367 -0
  20. package/src/client.test.ts +44 -0
  21. package/src/client.ts +443 -0
  22. package/src/config-schema.test.ts +117 -0
  23. package/src/config-schema.ts +97 -0
  24. package/src/config-ui-hints.ts +41 -0
  25. package/src/connect-options.test.ts +48 -0
  26. package/src/connect-options.ts +31 -0
  27. package/src/control-chars.test.ts +18 -0
  28. package/src/control-chars.ts +23 -0
  29. package/src/doctor.ts +55 -0
  30. package/src/gateway.ts +54 -0
  31. package/src/inbound.behavior.test.ts +247 -0
  32. package/src/inbound.ts +440 -0
  33. package/src/message-adapter.ts +29 -0
  34. package/src/monitor.test.ts +44 -0
  35. package/src/monitor.ts +150 -0
  36. package/src/normalize.test.ts +56 -0
  37. package/src/normalize.ts +111 -0
  38. package/src/outbound-base.ts +11 -0
  39. package/src/policy.test.ts +56 -0
  40. package/src/policy.ts +79 -0
  41. package/src/probe.test.ts +111 -0
  42. package/src/probe.ts +54 -0
  43. package/src/protocol.test.ts +49 -0
  44. package/src/protocol.ts +170 -0
  45. package/src/runtime-api.ts +42 -0
  46. package/src/runtime.ts +16 -0
  47. package/src/secret-contract.ts +104 -0
  48. package/src/send.test.ts +327 -0
  49. package/src/send.ts +122 -0
  50. package/src/setup-core.ts +152 -0
  51. package/src/setup-surface.ts +451 -0
  52. package/src/setup.test.ts +487 -0
  53. package/src/types.ts +101 -0
  54. package/tsconfig.json +16 -0
@@ -0,0 +1,26 @@
1
+ {
2
+ "id": "irc",
3
+ "activation": {
4
+ "onStartup": false
5
+ },
6
+ "channels": ["irc"],
7
+ "channelEnvVars": {
8
+ "irc": [
9
+ "IRC_HOST",
10
+ "IRC_PORT",
11
+ "IRC_TLS",
12
+ "IRC_NICK",
13
+ "IRC_USERNAME",
14
+ "IRC_REALNAME",
15
+ "IRC_PASSWORD",
16
+ "IRC_CHANNELS",
17
+ "IRC_NICKSERV_PASSWORD",
18
+ "IRC_NICKSERV_REGISTER_EMAIL"
19
+ ]
20
+ },
21
+ "configSchema": {
22
+ "type": "object",
23
+ "additionalProperties": false,
24
+ "properties": {}
25
+ }
26
+ }
package/api.ts ADDED
@@ -0,0 +1,11 @@
1
+ // Irc API module exposes the plugin public contract.
2
+ export { ircPlugin } from "./src/channel.js";
3
+ export { setIrcRuntime } from "./src/runtime.js";
4
+ export {
5
+ listEnabledIrcAccounts,
6
+ listIrcAccountIds,
7
+ resolveDefaultIrcAccountId,
8
+ type ResolvedIrcAccount,
9
+ resolveIrcAccount,
10
+ } from "./src/accounts.js";
11
+ export { ircSetupAdapter, ircSetupWizard } from "./src/setup-surface.js";
@@ -0,0 +1,2 @@
1
+ // Irc API module exposes the plugin public contract.
2
+ export { IrcChannelConfigSchema } from "./src/config-schema.js";
@@ -0,0 +1,3 @@
1
+ // Keep bundled channel entry imports narrow so bootstrap/discovery paths do
2
+ // not drag IRC runtime/send/monitor surfaces into lightweight plugin loads.
3
+ export { ircPlugin } from "./src/channel.js";
@@ -0,0 +1,9 @@
1
+ // Irc helper module supports configured state behavior.
2
+ export function hasIrcConfiguredState(params: { env?: NodeJS.ProcessEnv }): boolean {
3
+ return (
4
+ typeof params.env?.IRC_HOST === "string" &&
5
+ params.env.IRC_HOST.trim().length > 0 &&
6
+ typeof params.env?.IRC_NICK === "string" &&
7
+ params.env.IRC_NICK.trim().length > 0
8
+ );
9
+ }
@@ -0,0 +1,5 @@
1
+ // Irc API module exposes the plugin public contract.
2
+ export {
3
+ collectRuntimeConfigAssignments,
4
+ secretTargetRegistryEntries,
5
+ } from "./src/secret-contract.js";
package/index.test.ts ADDED
@@ -0,0 +1,14 @@
1
+ // Irc tests cover index plugin behavior.
2
+ import { assertBundledChannelEntries } from "actagent/plugin-sdk/channel-test-helpers";
3
+ import { describe } from "vitest";
4
+ import entry from "./index.js";
5
+ import setupEntry from "./setup-entry.js";
6
+
7
+ describe("irc bundled entries", () => {
8
+ assertBundledChannelEntries({
9
+ entry,
10
+ expectedId: "irc",
11
+ expectedName: "IRC",
12
+ setupEntry,
13
+ });
14
+ });
package/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ // Irc plugin entrypoint registers its ACTAgent integration.
2
+ import { defineBundledChannelEntry } from "actagent/plugin-sdk/channel-entry-contract";
3
+
4
+ export default defineBundledChannelEntry({
5
+ id: "irc",
6
+ name: "IRC",
7
+ description: "IRC channel plugin",
8
+ importMetaUrl: import.meta.url,
9
+ plugin: {
10
+ specifier: "./channel-plugin-api.js",
11
+ exportName: "ircPlugin",
12
+ },
13
+ secrets: {
14
+ specifier: "./secret-contract-api.js",
15
+ exportName: "channelSecrets",
16
+ },
17
+ runtime: {
18
+ specifier: "./runtime-api.js",
19
+ exportName: "setIrcRuntime",
20
+ },
21
+ });
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@actagent/irc",
3
+ "version": "2026.6.2",
4
+ "description": "ACTAgent IRC channel plugin",
5
+ "type": "module",
6
+ "devDependencies": {
7
+ "@actagent/plugin-sdk": "workspace:*"
8
+ },
9
+ "actagent": {
10
+ "extensions": [
11
+ "./index.ts"
12
+ ],
13
+ "install": {
14
+ "minHostVersion": ">=2026.4.10"
15
+ },
16
+ "setupEntry": "./setup-entry.ts",
17
+ "channel": {
18
+ "id": "irc",
19
+ "label": "IRC",
20
+ "selectionLabel": "IRC (Server + Nick)",
21
+ "detailLabel": "IRC",
22
+ "docsPath": "/channels/irc",
23
+ "docsLabel": "irc",
24
+ "blurb": "classic IRC networks with DM/channel routing and pairing controls.",
25
+ "aliases": [
26
+ "internet-relay-chat"
27
+ ],
28
+ "systemImage": "network",
29
+ "configuredState": {
30
+ "env": {
31
+ "allOf": [
32
+ "IRC_HOST",
33
+ "IRC_NICK"
34
+ ]
35
+ },
36
+ "specifier": "./configured-state",
37
+ "exportName": "hasIrcConfiguredState"
38
+ }
39
+ }
40
+ },
41
+ "dependencies": {
42
+ "zod": "4.4.3"
43
+ }
44
+ }
@@ -0,0 +1,24 @@
1
+ // Irc tests cover runtime api plugin behavior.
2
+ import { runDirectImportSmoke } from "actagent/plugin-sdk/plugin-test-contracts";
3
+ import { beforeAll, describe, expect, it } from "vitest";
4
+
5
+ describe("irc bundled api seams", () => {
6
+ let directSmokeStdout = "";
7
+
8
+ beforeAll(async () => {
9
+ directSmokeStdout = await runDirectImportSmoke(
10
+ `const channel = await import("./extensions/irc/channel-plugin-api.ts");
11
+ const runtime = await import("./extensions/irc/runtime-api.ts");
12
+ process.stdout.write(JSON.stringify({
13
+ channel: { keys: Object.keys(channel).sort(), id: channel.ircPlugin.id },
14
+ runtime: { keys: Object.keys(runtime).sort(), type: typeof runtime.setIrcRuntime },
15
+ }));`,
16
+ );
17
+ }, 45_000);
18
+
19
+ it("loads narrow public api modules in direct smoke", () => {
20
+ expect(directSmokeStdout).toBe(
21
+ '{"channel":{"keys":["ircPlugin"],"id":"irc"},"runtime":{"keys":["setIrcRuntime"],"type":"function"}}',
22
+ );
23
+ });
24
+ });
package/runtime-api.ts ADDED
@@ -0,0 +1,3 @@
1
+ // Keep the bundled runtime entry narrow so generic runtime activation does not
2
+ // import the broad IRC API barrel just to install runtime state.
3
+ export { setIrcRuntime } from "./src/runtime.js";
@@ -0,0 +1,6 @@
1
+ // Irc API module exposes the plugin public contract.
2
+ export {
3
+ channelSecrets,
4
+ collectRuntimeConfigAssignments,
5
+ secretTargetRegistryEntries,
6
+ } from "./src/secret-contract.js";
package/setup-entry.ts ADDED
@@ -0,0 +1,14 @@
1
+ // Irc plugin module implements setup entry behavior.
2
+ import { defineBundledChannelSetupEntry } from "actagent/plugin-sdk/channel-entry-contract";
3
+
4
+ export default defineBundledChannelSetupEntry({
5
+ importMetaUrl: import.meta.url,
6
+ plugin: {
7
+ specifier: "./channel-plugin-api.js",
8
+ exportName: "ircPlugin",
9
+ },
10
+ secrets: {
11
+ specifier: "./secret-contract-api.js",
12
+ exportName: "channelSecrets",
13
+ },
14
+ });
@@ -0,0 +1,224 @@
1
+ // Irc tests cover accounts plugin behavior.
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { describe, expect, it, vi } from "vitest";
6
+ import { listIrcAccountIds, resolveDefaultIrcAccountId, resolveIrcAccount } from "./accounts.js";
7
+ import type { CoreConfig } from "./types.js";
8
+
9
+ function asConfig(value: unknown): CoreConfig {
10
+ return value as CoreConfig;
11
+ }
12
+
13
+ describe("listIrcAccountIds", () => {
14
+ it("returns default when no accounts are configured", () => {
15
+ expect(listIrcAccountIds(asConfig({}))).toEqual(["default"]);
16
+ });
17
+
18
+ it("normalizes, deduplicates, and sorts configured account ids", () => {
19
+ const cfg = asConfig({
20
+ channels: {
21
+ irc: {
22
+ accounts: {
23
+ "Ops Team": {},
24
+ "ops-team": {},
25
+ Work: {},
26
+ },
27
+ },
28
+ },
29
+ });
30
+
31
+ expect(listIrcAccountIds(cfg)).toEqual(["ops-team", "work"]);
32
+ });
33
+
34
+ it("keeps the implicit default account when named accounts are added to top-level connection config", () => {
35
+ const cfg = asConfig({
36
+ channels: {
37
+ irc: {
38
+ host: "irc.example.com",
39
+ nick: "actagent",
40
+ accounts: {
41
+ work: {
42
+ enabled: false,
43
+ host: "irc-work.example.com",
44
+ nick: "actagent-work",
45
+ },
46
+ },
47
+ },
48
+ },
49
+ });
50
+
51
+ expect(listIrcAccountIds(cfg)).toEqual(["default", "work"]);
52
+ expect(resolveDefaultIrcAccountId(cfg)).toBe("default");
53
+ });
54
+ });
55
+
56
+ describe("resolveDefaultIrcAccountId", () => {
57
+ it("prefers configured defaultAccount when it matches", () => {
58
+ const cfg = asConfig({
59
+ channels: {
60
+ irc: {
61
+ defaultAccount: "Ops Team",
62
+ accounts: {
63
+ default: {},
64
+ "ops-team": {},
65
+ },
66
+ },
67
+ },
68
+ });
69
+
70
+ expect(resolveDefaultIrcAccountId(cfg)).toBe("ops-team");
71
+ });
72
+
73
+ it("falls back to default when configured defaultAccount is missing", () => {
74
+ const cfg = asConfig({
75
+ channels: {
76
+ irc: {
77
+ defaultAccount: "missing",
78
+ accounts: {
79
+ default: {},
80
+ work: {},
81
+ },
82
+ },
83
+ },
84
+ });
85
+
86
+ expect(resolveDefaultIrcAccountId(cfg)).toBe("default");
87
+ });
88
+
89
+ it("falls back to first sorted account when default is absent", () => {
90
+ const cfg = asConfig({
91
+ channels: {
92
+ irc: {
93
+ accounts: {
94
+ zzz: {},
95
+ aaa: {},
96
+ },
97
+ },
98
+ },
99
+ });
100
+
101
+ expect(resolveDefaultIrcAccountId(cfg)).toBe("aaa");
102
+ });
103
+ });
104
+
105
+ describe("resolveIrcAccount", () => {
106
+ it("matches normalized configured account ids", () => {
107
+ const account = resolveIrcAccount({
108
+ cfg: asConfig({
109
+ channels: {
110
+ irc: {
111
+ accounts: {
112
+ "Ops Team": {
113
+ host: "irc.example.com",
114
+ nick: "actagent",
115
+ },
116
+ },
117
+ },
118
+ },
119
+ }),
120
+ accountId: "ops-team",
121
+ });
122
+
123
+ expect(account.accountId).toBe("ops-team");
124
+ expect(account.host).toBe("irc.example.com");
125
+ expect(account.nick).toBe("actagent");
126
+ expect(account.configured).toBe(true);
127
+ });
128
+
129
+ it("parses delimited IRC_CHANNELS env values for the default account", () => {
130
+ vi.stubEnv("IRC_CHANNELS", "alpha, beta\ngamma; delta");
131
+
132
+ try {
133
+ const account = resolveIrcAccount({
134
+ cfg: asConfig({
135
+ channels: {
136
+ irc: {
137
+ host: "irc.example.com",
138
+ nick: "actagent",
139
+ },
140
+ },
141
+ }),
142
+ });
143
+
144
+ expect(account.config.channels).toEqual(["alpha", "beta", "gamma", "delta"]);
145
+ } finally {
146
+ vi.unstubAllEnvs();
147
+ }
148
+ });
149
+
150
+ it.runIf(process.platform !== "win32")("rejects symlinked password files", () => {
151
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "actagent-irc-account-"));
152
+ const passwordFile = path.join(dir, "password.txt");
153
+ const passwordLink = path.join(dir, "password-link.txt");
154
+ fs.writeFileSync(passwordFile, "secret-pass\n", "utf8");
155
+ fs.symlinkSync(passwordFile, passwordLink);
156
+
157
+ const cfg = asConfig({
158
+ channels: {
159
+ irc: {
160
+ host: "irc.example.com",
161
+ nick: "actagent",
162
+ passwordFile: passwordLink,
163
+ },
164
+ },
165
+ });
166
+
167
+ expect(() => resolveIrcAccount({ cfg })).toThrow(/IRC password file.*must not be a symlink/);
168
+ fs.rmSync(dir, { recursive: true, force: true });
169
+ });
170
+
171
+ it.runIf(process.platform !== "win32")("rejects symlinked NickServ password files", () => {
172
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "actagent-irc-nickserv-"));
173
+ const passwordFile = path.join(dir, "nickserv-password.txt");
174
+ const passwordLink = path.join(dir, "nickserv-password-link.txt");
175
+ fs.writeFileSync(passwordFile, "nickserv-pass\n", "utf8");
176
+ fs.symlinkSync(passwordFile, passwordLink);
177
+
178
+ const cfg = asConfig({
179
+ channels: {
180
+ irc: {
181
+ host: "irc.example.com",
182
+ nick: "actagent",
183
+ nickserv: {
184
+ passwordFile: passwordLink,
185
+ },
186
+ },
187
+ },
188
+ });
189
+
190
+ expect(() => resolveIrcAccount({ cfg })).toThrow(
191
+ /IRC NickServ password file.*must not be a symlink/,
192
+ );
193
+ fs.rmSync(dir, { recursive: true, force: true });
194
+ });
195
+
196
+ it("preserves shared NickServ config when an account overrides one NickServ field", () => {
197
+ const account = resolveIrcAccount({
198
+ cfg: asConfig({
199
+ channels: {
200
+ irc: {
201
+ host: "irc.example.com",
202
+ nick: "actagent",
203
+ nickserv: {
204
+ service: "NickServ",
205
+ },
206
+ accounts: {
207
+ work: {
208
+ nickserv: {
209
+ registerEmail: "work@example.com",
210
+ },
211
+ },
212
+ },
213
+ },
214
+ },
215
+ }),
216
+ accountId: "work",
217
+ });
218
+
219
+ expect(account.config.nickserv).toEqual({
220
+ service: "NickServ",
221
+ registerEmail: "work@example.com",
222
+ });
223
+ });
224
+ });
@@ -0,0 +1,240 @@
1
+ // Irc plugin module implements accounts behavior.
2
+ import { createAccountListHelpers } from "actagent/plugin-sdk/account-helpers";
3
+ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "actagent/plugin-sdk/account-id";
4
+ import { resolveMergedAccountConfig } from "actagent/plugin-sdk/account-resolution";
5
+ import {
6
+ parseOptionalDelimitedEntries,
7
+ tryReadSecretFileSync,
8
+ } from "actagent/plugin-sdk/channel-core";
9
+ import { parseStrictPositiveInteger } from "actagent/plugin-sdk/number-runtime";
10
+ import { normalizeResolvedSecretInputString } from "actagent/plugin-sdk/secret-input";
11
+ import {
12
+ normalizeLowercaseStringOrEmpty,
13
+ normalizeOptionalString,
14
+ } from "actagent/plugin-sdk/string-coerce-runtime";
15
+ import type { CoreConfig, IrcAccountConfig, IrcNickServConfig } from "./types.js";
16
+
17
+ const TRUTHY_ENV = new Set(["true", "1", "yes", "on"]);
18
+
19
+ export type ResolvedIrcAccount = {
20
+ accountId: string;
21
+ enabled: boolean;
22
+ name?: string;
23
+ configured: boolean;
24
+ host: string;
25
+ port: number;
26
+ tls: boolean;
27
+ nick: string;
28
+ username: string;
29
+ realname: string;
30
+ password: string;
31
+ passwordSource: "env" | "passwordFile" | "config" | "none";
32
+ config: IrcAccountConfig;
33
+ };
34
+
35
+ function parseTruthy(value?: string): boolean {
36
+ if (!value) {
37
+ return false;
38
+ }
39
+ return TRUTHY_ENV.has(normalizeLowercaseStringOrEmpty(value));
40
+ }
41
+
42
+ function parseIntEnv(value?: string): number | undefined {
43
+ if (!value?.trim()) {
44
+ return undefined;
45
+ }
46
+ const parsed = parseStrictPositiveInteger(value);
47
+ if (parsed === undefined || parsed > 65535) {
48
+ return undefined;
49
+ }
50
+ return parsed;
51
+ }
52
+
53
+ const { listAccountIds: listIrcAccountIds, resolveDefaultAccountId: resolveDefaultIrcAccountId } =
54
+ createAccountListHelpers("irc", {
55
+ normalizeAccountId,
56
+ hasImplicitDefaultAccount: (cfg) =>
57
+ Boolean(
58
+ (cfg.channels?.irc?.host?.trim() || process.env.IRC_HOST?.trim()) &&
59
+ (cfg.channels?.irc?.nick?.trim() || process.env.IRC_NICK?.trim()),
60
+ ),
61
+ });
62
+ export { listIrcAccountIds, resolveDefaultIrcAccountId };
63
+
64
+ function mergeIrcAccountConfig(cfg: CoreConfig, accountId: string): IrcAccountConfig {
65
+ return resolveMergedAccountConfig<IrcAccountConfig>({
66
+ channelConfig: cfg.channels?.irc as IrcAccountConfig | undefined,
67
+ accounts: cfg.channels?.irc?.accounts as Record<string, Partial<IrcAccountConfig>> | undefined,
68
+ accountId,
69
+ omitKeys: ["defaultAccount"],
70
+ normalizeAccountId,
71
+ nestedObjectKeys: ["nickserv"],
72
+ });
73
+ }
74
+
75
+ function resolvePassword(accountId: string, merged: IrcAccountConfig) {
76
+ if (accountId === DEFAULT_ACCOUNT_ID) {
77
+ const envPassword = process.env.IRC_PASSWORD?.trim();
78
+ if (envPassword) {
79
+ return { password: envPassword, source: "env" as const };
80
+ }
81
+ }
82
+
83
+ if (merged.passwordFile?.trim()) {
84
+ const filePassword = tryReadSecretFileSync(merged.passwordFile, "IRC password file", {
85
+ rejectSymlink: true,
86
+ });
87
+ if (filePassword) {
88
+ return { password: filePassword, source: "passwordFile" as const };
89
+ }
90
+ }
91
+
92
+ const configPassword = normalizeResolvedSecretInputString({
93
+ value: merged.password,
94
+ path: `channels.irc.accounts.${accountId}.password`,
95
+ });
96
+ if (configPassword) {
97
+ return { password: configPassword, source: "config" as const };
98
+ }
99
+
100
+ return { password: "", source: "none" as const };
101
+ }
102
+
103
+ function resolveNickServConfig(accountId: string, nickserv?: IrcNickServConfig): IrcNickServConfig {
104
+ const base = nickserv ?? {};
105
+ const envPassword =
106
+ accountId === DEFAULT_ACCOUNT_ID ? process.env.IRC_NICKSERV_PASSWORD?.trim() : undefined;
107
+ const envRegisterEmail =
108
+ accountId === DEFAULT_ACCOUNT_ID ? process.env.IRC_NICKSERV_REGISTER_EMAIL?.trim() : undefined;
109
+
110
+ const passwordFile = base.passwordFile?.trim();
111
+ let resolvedPassword =
112
+ normalizeResolvedSecretInputString({
113
+ value: base.password,
114
+ path: `channels.irc.accounts.${accountId}.nickserv.password`,
115
+ }) ||
116
+ envPassword ||
117
+ "";
118
+ if (!resolvedPassword && passwordFile) {
119
+ resolvedPassword =
120
+ tryReadSecretFileSync(passwordFile, "IRC NickServ password file", {
121
+ rejectSymlink: true,
122
+ }) ?? "";
123
+ }
124
+
125
+ const merged: IrcNickServConfig = {
126
+ ...base,
127
+ service: normalizeOptionalString(base.service),
128
+ passwordFile: passwordFile || undefined,
129
+ password: resolvedPassword || undefined,
130
+ registerEmail: base.registerEmail?.trim() || envRegisterEmail || undefined,
131
+ };
132
+ return merged;
133
+ }
134
+
135
+ export function resolveIrcAccount(params: {
136
+ cfg: CoreConfig;
137
+ accountId?: string | null;
138
+ }): ResolvedIrcAccount {
139
+ const hasExplicitAccountId = Boolean(params.accountId?.trim());
140
+ const baseEnabled = params.cfg.channels?.irc?.enabled !== false;
141
+
142
+ const resolve = (accountId: string) => {
143
+ const merged = mergeIrcAccountConfig(params.cfg, accountId);
144
+ const accountEnabled = merged.enabled !== false;
145
+ const enabled = baseEnabled && accountEnabled;
146
+
147
+ const tls =
148
+ typeof merged.tls === "boolean"
149
+ ? merged.tls
150
+ : accountId === DEFAULT_ACCOUNT_ID && process.env.IRC_TLS
151
+ ? parseTruthy(process.env.IRC_TLS)
152
+ : true;
153
+
154
+ const envPort =
155
+ accountId === DEFAULT_ACCOUNT_ID ? parseIntEnv(process.env.IRC_PORT) : undefined;
156
+ const port = merged.port ?? envPort ?? (tls ? 6697 : 6667);
157
+ const envChannels =
158
+ accountId === DEFAULT_ACCOUNT_ID
159
+ ? parseOptionalDelimitedEntries(process.env.IRC_CHANNELS)
160
+ : undefined;
161
+
162
+ const host = (
163
+ merged.host?.trim() ||
164
+ (accountId === DEFAULT_ACCOUNT_ID ? process.env.IRC_HOST?.trim() : "") ||
165
+ ""
166
+ ).trim();
167
+ const nick = (
168
+ merged.nick?.trim() ||
169
+ (accountId === DEFAULT_ACCOUNT_ID ? process.env.IRC_NICK?.trim() : "") ||
170
+ ""
171
+ ).trim();
172
+ const username = (
173
+ merged.username?.trim() ||
174
+ (accountId === DEFAULT_ACCOUNT_ID ? process.env.IRC_USERNAME?.trim() : "") ||
175
+ nick ||
176
+ "actagent"
177
+ ).trim();
178
+ const realname = (
179
+ merged.realname?.trim() ||
180
+ (accountId === DEFAULT_ACCOUNT_ID ? process.env.IRC_REALNAME?.trim() : "") ||
181
+ "ACTAgent"
182
+ ).trim();
183
+
184
+ const passwordResolution = resolvePassword(accountId, merged);
185
+ const nickserv = resolveNickServConfig(accountId, merged.nickserv);
186
+
187
+ const config: IrcAccountConfig = {
188
+ ...merged,
189
+ channels: merged.channels ?? envChannels,
190
+ tls,
191
+ port,
192
+ host,
193
+ nick,
194
+ username,
195
+ realname,
196
+ nickserv,
197
+ };
198
+
199
+ return {
200
+ accountId,
201
+ enabled,
202
+ name: normalizeOptionalString(merged.name),
203
+ configured: Boolean(host && nick),
204
+ host,
205
+ port,
206
+ tls,
207
+ nick,
208
+ username,
209
+ realname,
210
+ password: passwordResolution.password,
211
+ passwordSource: passwordResolution.source,
212
+ config,
213
+ } satisfies ResolvedIrcAccount;
214
+ };
215
+
216
+ const normalized = normalizeAccountId(params.accountId);
217
+ const primary = resolve(normalized);
218
+ if (hasExplicitAccountId) {
219
+ return primary;
220
+ }
221
+ if (primary.configured) {
222
+ return primary;
223
+ }
224
+
225
+ const fallbackId = resolveDefaultIrcAccountId(params.cfg);
226
+ if (fallbackId === primary.accountId) {
227
+ return primary;
228
+ }
229
+ const fallback = resolve(fallbackId);
230
+ if (!fallback.configured) {
231
+ return primary;
232
+ }
233
+ return fallback;
234
+ }
235
+
236
+ export function listEnabledIrcAccounts(cfg: CoreConfig): ResolvedIrcAccount[] {
237
+ return listIrcAccountIds(cfg)
238
+ .map((accountId) => resolveIrcAccount({ cfg, accountId }))
239
+ .filter((account) => account.enabled);
240
+ }
@@ -0,0 +1,7 @@
1
+ // Irc API module exposes the plugin public contract.
2
+ export { createAccountStatusSink } from "actagent/plugin-sdk/channel-outbound";
3
+ export { DEFAULT_ACCOUNT_ID } from "actagent/plugin-sdk/account-id";
4
+ export type { ChannelPlugin } from "actagent/plugin-sdk/channel-core";
5
+ export { PAIRING_APPROVED_MESSAGE } from "actagent/plugin-sdk/channel-status";
6
+ export { buildBaseChannelStatusSummary } from "actagent/plugin-sdk/status-helpers";
7
+ export { chunkTextForOutbound } from "actagent/plugin-sdk/text-chunking";
@@ -0,0 +1,4 @@
1
+ // Runtime-only IRC helpers for lazy chat plugin hooks.
2
+ // Keeping this boundary separate keeps bundled entry loads off monitor/send.
3
+ export { monitorIrcProvider } from "./monitor.js";
4
+ export { sendMessageIrc } from "./send.js";