@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,42 @@
|
|
|
1
|
+
// Private runtime barrel for the bundled IRC extension.
|
|
2
|
+
// Keep this barrel thin and generic-only.
|
|
3
|
+
|
|
4
|
+
export type { BaseProbeResult } from "actagent/plugin-sdk/channel-contract";
|
|
5
|
+
export type { ChannelPlugin } from "actagent/plugin-sdk/channel-core";
|
|
6
|
+
export type { ACTAgentConfig } from "actagent/plugin-sdk/config-contracts";
|
|
7
|
+
export type { PluginRuntime } from "actagent/plugin-sdk/runtime-store";
|
|
8
|
+
export type { RuntimeEnv } from "actagent/plugin-sdk/runtime";
|
|
9
|
+
export type {
|
|
10
|
+
BlockStreamingCoalesceConfig,
|
|
11
|
+
DmConfig,
|
|
12
|
+
DmPolicy,
|
|
13
|
+
GroupPolicy,
|
|
14
|
+
GroupToolPolicyBySenderConfig,
|
|
15
|
+
GroupToolPolicyConfig,
|
|
16
|
+
MarkdownConfig,
|
|
17
|
+
} from "actagent/plugin-sdk/config-contracts";
|
|
18
|
+
export type { OutboundReplyPayload } from "actagent/plugin-sdk/reply-payload";
|
|
19
|
+
export { DEFAULT_ACCOUNT_ID } from "actagent/plugin-sdk/account-id";
|
|
20
|
+
export { buildChannelConfigSchema } from "actagent/plugin-sdk/channel-config-primitives";
|
|
21
|
+
export {
|
|
22
|
+
PAIRING_APPROVED_MESSAGE,
|
|
23
|
+
buildBaseChannelStatusSummary,
|
|
24
|
+
} from "actagent/plugin-sdk/channel-status";
|
|
25
|
+
export { createChannelPairingController } from "actagent/plugin-sdk/channel-pairing";
|
|
26
|
+
export { createAccountStatusSink } from "actagent/plugin-sdk/channel-outbound";
|
|
27
|
+
export { resolveControlCommandGate } from "actagent/plugin-sdk/command-auth-native";
|
|
28
|
+
export { createChannelMessageReplyPipeline } from "actagent/plugin-sdk/channel-outbound";
|
|
29
|
+
export { chunkTextForOutbound } from "actagent/plugin-sdk/text-chunking";
|
|
30
|
+
export {
|
|
31
|
+
deliverFormattedTextWithAttachments,
|
|
32
|
+
formatTextWithAttachmentLinks,
|
|
33
|
+
resolveOutboundMediaUrls,
|
|
34
|
+
} from "actagent/plugin-sdk/reply-payload";
|
|
35
|
+
export {
|
|
36
|
+
GROUP_POLICY_BLOCKED_LABEL,
|
|
37
|
+
resolveAllowlistProviderRuntimeGroupPolicy,
|
|
38
|
+
resolveDefaultGroupPolicy,
|
|
39
|
+
warnMissingProviderGroupPolicyFallbackOnce,
|
|
40
|
+
} from "actagent/plugin-sdk/runtime-group-policy";
|
|
41
|
+
export { isDangerousNameMatchingEnabled } from "actagent/plugin-sdk/dangerous-name-runtime";
|
|
42
|
+
export { logInboundDrop } from "actagent/plugin-sdk/channel-inbound";
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Irc plugin module implements runtime behavior.
|
|
2
|
+
import { createPluginRuntimeStore } from "actagent/plugin-sdk/runtime-store";
|
|
3
|
+
import type { PluginRuntime } from "./runtime-api.js";
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
setRuntime: setIrcRuntime,
|
|
7
|
+
clearRuntime: clearStoredIrcRuntime,
|
|
8
|
+
getRuntime: getIrcRuntime,
|
|
9
|
+
} = createPluginRuntimeStore<PluginRuntime>({
|
|
10
|
+
pluginId: "irc",
|
|
11
|
+
errorMessage: "IRC runtime not initialized",
|
|
12
|
+
});
|
|
13
|
+
export { getIrcRuntime, setIrcRuntime };
|
|
14
|
+
export function clearIrcRuntime() {
|
|
15
|
+
clearStoredIrcRuntime();
|
|
16
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Irc plugin module implements secret contract behavior.
|
|
2
|
+
import {
|
|
3
|
+
collectNestedChannelFieldAssignments,
|
|
4
|
+
collectSimpleChannelFieldAssignments,
|
|
5
|
+
getChannelSurface,
|
|
6
|
+
isBaseFieldActiveForChannelSurface,
|
|
7
|
+
isEnabledFlag,
|
|
8
|
+
isRecord,
|
|
9
|
+
type ResolverContext,
|
|
10
|
+
type SecretDefaults,
|
|
11
|
+
} from "actagent/plugin-sdk/channel-secret-basic-runtime";
|
|
12
|
+
|
|
13
|
+
export const secretTargetRegistryEntries: import("actagent/plugin-sdk/channel-secret-basic-runtime").SecretTargetRegistryEntry[] =
|
|
14
|
+
[
|
|
15
|
+
{
|
|
16
|
+
id: "channels.irc.accounts.*.nickserv.password",
|
|
17
|
+
targetType: "channels.irc.accounts.*.nickserv.password",
|
|
18
|
+
configFile: "actagent.json",
|
|
19
|
+
pathPattern: "channels.irc.accounts.*.nickserv.password",
|
|
20
|
+
secretShape: "secret_input",
|
|
21
|
+
expectedResolvedValue: "string",
|
|
22
|
+
includeInPlan: true,
|
|
23
|
+
includeInConfigure: true,
|
|
24
|
+
includeInAudit: true,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "channels.irc.accounts.*.password",
|
|
28
|
+
targetType: "channels.irc.accounts.*.password",
|
|
29
|
+
configFile: "actagent.json",
|
|
30
|
+
pathPattern: "channels.irc.accounts.*.password",
|
|
31
|
+
secretShape: "secret_input",
|
|
32
|
+
expectedResolvedValue: "string",
|
|
33
|
+
includeInPlan: true,
|
|
34
|
+
includeInConfigure: true,
|
|
35
|
+
includeInAudit: true,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "channels.irc.nickserv.password",
|
|
39
|
+
targetType: "channels.irc.nickserv.password",
|
|
40
|
+
configFile: "actagent.json",
|
|
41
|
+
pathPattern: "channels.irc.nickserv.password",
|
|
42
|
+
secretShape: "secret_input",
|
|
43
|
+
expectedResolvedValue: "string",
|
|
44
|
+
includeInPlan: true,
|
|
45
|
+
includeInConfigure: true,
|
|
46
|
+
includeInAudit: true,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "channels.irc.password",
|
|
50
|
+
targetType: "channels.irc.password",
|
|
51
|
+
configFile: "actagent.json",
|
|
52
|
+
pathPattern: "channels.irc.password",
|
|
53
|
+
secretShape: "secret_input",
|
|
54
|
+
expectedResolvedValue: "string",
|
|
55
|
+
includeInPlan: true,
|
|
56
|
+
includeInConfigure: true,
|
|
57
|
+
includeInAudit: true,
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
export function collectRuntimeConfigAssignments(params: {
|
|
62
|
+
config: { channels?: Record<string, unknown> };
|
|
63
|
+
defaults?: SecretDefaults;
|
|
64
|
+
context: ResolverContext;
|
|
65
|
+
}): void {
|
|
66
|
+
const resolved = getChannelSurface(params.config, "irc");
|
|
67
|
+
if (!resolved) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const { channel: irc, surface } = resolved;
|
|
71
|
+
collectSimpleChannelFieldAssignments({
|
|
72
|
+
channelKey: "irc",
|
|
73
|
+
field: "password",
|
|
74
|
+
channel: irc,
|
|
75
|
+
surface,
|
|
76
|
+
defaults: params.defaults,
|
|
77
|
+
context: params.context,
|
|
78
|
+
topInactiveReason: "no enabled account inherits this top-level IRC password.",
|
|
79
|
+
accountInactiveReason: "IRC account is disabled.",
|
|
80
|
+
});
|
|
81
|
+
collectNestedChannelFieldAssignments({
|
|
82
|
+
channelKey: "irc",
|
|
83
|
+
nestedKey: "nickserv",
|
|
84
|
+
field: "password",
|
|
85
|
+
channel: irc,
|
|
86
|
+
surface,
|
|
87
|
+
defaults: params.defaults,
|
|
88
|
+
context: params.context,
|
|
89
|
+
topLevelActive:
|
|
90
|
+
isBaseFieldActiveForChannelSurface(surface, "nickserv") &&
|
|
91
|
+
isRecord(irc.nickserv) &&
|
|
92
|
+
isEnabledFlag(irc.nickserv),
|
|
93
|
+
topInactiveReason:
|
|
94
|
+
"no enabled account inherits this top-level IRC nickserv config or NickServ is disabled.",
|
|
95
|
+
accountActive: ({ account, enabled }) =>
|
|
96
|
+
enabled && isRecord(account.nickserv) && isEnabledFlag(account.nickserv),
|
|
97
|
+
accountInactiveReason: "IRC account is disabled or NickServ is disabled for this account.",
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const channelSecrets = {
|
|
102
|
+
secretTargetRegistryEntries,
|
|
103
|
+
collectRuntimeConfigAssignments,
|
|
104
|
+
};
|
package/src/send.test.ts
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
// Irc tests cover send plugin behavior.
|
|
2
|
+
import { verifyChannelMessageAdapterCapabilityProofs } from "actagent/plugin-sdk/channel-outbound";
|
|
3
|
+
import { createSendCfgThreadingRuntime } from "actagent/plugin-sdk/channel-test-helpers";
|
|
4
|
+
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import type { IrcClient } from "./client.js";
|
|
6
|
+
import { clearIrcRuntime, setIrcRuntime } from "./runtime.js";
|
|
7
|
+
import type { CoreConfig } from "./types.js";
|
|
8
|
+
|
|
9
|
+
const hoisted = vi.hoisted(() => {
|
|
10
|
+
const loadConfig = vi.fn();
|
|
11
|
+
const resolveMarkdownTableMode = vi.fn(() => "preserve");
|
|
12
|
+
const convertMarkdownTables = vi.fn((text: string) => text);
|
|
13
|
+
const record = vi.fn();
|
|
14
|
+
return {
|
|
15
|
+
loadConfig,
|
|
16
|
+
resolveMarkdownTableMode,
|
|
17
|
+
convertMarkdownTables,
|
|
18
|
+
record,
|
|
19
|
+
normalizeIrcMessagingTarget: vi.fn((value: string) => value.trim()),
|
|
20
|
+
connectIrcClient: vi.fn(),
|
|
21
|
+
buildIrcConnectOptions: vi.fn(() => ({})),
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
vi.mock("./normalize.js", () => ({
|
|
26
|
+
normalizeIrcMessagingTarget: hoisted.normalizeIrcMessagingTarget,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
vi.mock("./client.js", () => ({
|
|
30
|
+
connectIrcClient: hoisted.connectIrcClient,
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
vi.mock("./connect-options.js", () => ({
|
|
34
|
+
buildIrcConnectOptions: hoisted.buildIrcConnectOptions,
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
vi.mock("./protocol.js", async () => {
|
|
38
|
+
const actual = await vi.importActual<typeof import("./protocol.js")>("./protocol.js");
|
|
39
|
+
return {
|
|
40
|
+
...actual,
|
|
41
|
+
makeIrcMessageId: () => "irc-msg-1",
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
vi.mock("actagent/plugin-sdk/plugin-config-runtime", async () => {
|
|
46
|
+
const original = (await vi.importActual("actagent/plugin-sdk/plugin-config-runtime")) as Record<
|
|
47
|
+
string,
|
|
48
|
+
unknown
|
|
49
|
+
>;
|
|
50
|
+
return {
|
|
51
|
+
...original,
|
|
52
|
+
resolveMarkdownTableMode: hoisted.resolveMarkdownTableMode,
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
vi.mock("actagent/plugin-sdk/text-chunking", async () => {
|
|
57
|
+
const original = (await vi.importActual("actagent/plugin-sdk/text-chunking")) as Record<
|
|
58
|
+
string,
|
|
59
|
+
unknown
|
|
60
|
+
>;
|
|
61
|
+
return {
|
|
62
|
+
...original,
|
|
63
|
+
convertMarkdownTables: hoisted.convertMarkdownTables,
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
import { ircMessageAdapter } from "./message-adapter.js";
|
|
68
|
+
import { sendMessageIrc } from "./send.js";
|
|
69
|
+
|
|
70
|
+
function resetHoistedMocks() {
|
|
71
|
+
hoisted.loadConfig.mockReset();
|
|
72
|
+
hoisted.resolveMarkdownTableMode.mockReset().mockReturnValue("preserve");
|
|
73
|
+
hoisted.convertMarkdownTables.mockReset().mockImplementation((text: string) => text);
|
|
74
|
+
hoisted.record.mockReset();
|
|
75
|
+
hoisted.normalizeIrcMessagingTarget
|
|
76
|
+
.mockReset()
|
|
77
|
+
.mockImplementation((value: string) => value.trim());
|
|
78
|
+
hoisted.connectIrcClient.mockReset();
|
|
79
|
+
hoisted.buildIrcConnectOptions.mockReset().mockReturnValue({});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
afterAll(() => {
|
|
83
|
+
vi.doUnmock("./normalize.js");
|
|
84
|
+
vi.doUnmock("./client.js");
|
|
85
|
+
vi.doUnmock("./connect-options.js");
|
|
86
|
+
vi.doUnmock("./protocol.js");
|
|
87
|
+
vi.doUnmock("actagent/plugin-sdk/plugin-config-runtime");
|
|
88
|
+
vi.doUnmock("actagent/plugin-sdk/text-chunking");
|
|
89
|
+
vi.resetModules();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("sendMessageIrc cfg threading", () => {
|
|
93
|
+
beforeEach(() => {
|
|
94
|
+
resetHoistedMocks();
|
|
95
|
+
setIrcRuntime(createSendCfgThreadingRuntime(hoisted) as never);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
afterEach(() => {
|
|
99
|
+
clearIrcRuntime();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("uses explicitly provided cfg without loading runtime config", async () => {
|
|
103
|
+
const providedCfg = {
|
|
104
|
+
channels: {
|
|
105
|
+
irc: {
|
|
106
|
+
host: "irc.example.com",
|
|
107
|
+
nick: "actagent",
|
|
108
|
+
accounts: {
|
|
109
|
+
work: {
|
|
110
|
+
host: "irc.example.com",
|
|
111
|
+
nick: "workbot",
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
} as unknown as CoreConfig;
|
|
117
|
+
const client = {
|
|
118
|
+
isReady: vi.fn(() => true),
|
|
119
|
+
sendPrivmsg: vi.fn(),
|
|
120
|
+
} as unknown as IrcClient;
|
|
121
|
+
|
|
122
|
+
const result = await sendMessageIrc("#room", "hello", {
|
|
123
|
+
cfg: providedCfg,
|
|
124
|
+
client,
|
|
125
|
+
accountId: "work",
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(hoisted.loadConfig).not.toHaveBeenCalled();
|
|
129
|
+
expect(client.sendPrivmsg).toHaveBeenCalledWith("#room", "hello");
|
|
130
|
+
expect(hoisted.record).toHaveBeenCalledWith({
|
|
131
|
+
channel: "irc",
|
|
132
|
+
accountId: "work",
|
|
133
|
+
direction: "outbound",
|
|
134
|
+
});
|
|
135
|
+
expect(result.target).toBe("#room");
|
|
136
|
+
expect(result.messageId).toBeTypeOf("string");
|
|
137
|
+
expect(result.messageId.length).toBeGreaterThan(0);
|
|
138
|
+
expect(result.receipt.sentAt).toBeTypeOf("number");
|
|
139
|
+
expect(result.receipt.sentAt).toBeGreaterThan(0);
|
|
140
|
+
expect({ ...result.receipt, sentAt: 123 }).toEqual({
|
|
141
|
+
primaryPlatformMessageId: "irc-msg-1",
|
|
142
|
+
platformMessageIds: ["irc-msg-1"],
|
|
143
|
+
parts: [
|
|
144
|
+
{
|
|
145
|
+
platformMessageId: "irc-msg-1",
|
|
146
|
+
kind: "text",
|
|
147
|
+
index: 0,
|
|
148
|
+
raw: {
|
|
149
|
+
channel: "irc",
|
|
150
|
+
conversationId: "#room",
|
|
151
|
+
messageId: "irc-msg-1",
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
sentAt: 123,
|
|
156
|
+
raw: [
|
|
157
|
+
{
|
|
158
|
+
channel: "irc",
|
|
159
|
+
conversationId: "#room",
|
|
160
|
+
messageId: "irc-msg-1",
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("fails hard when cfg is omitted", async () => {
|
|
167
|
+
const client = {
|
|
168
|
+
isReady: vi.fn(() => true),
|
|
169
|
+
sendPrivmsg: vi.fn(),
|
|
170
|
+
} as unknown as IrcClient;
|
|
171
|
+
|
|
172
|
+
await expect(sendMessageIrc("#ops", "ping", { client } as never)).rejects.toThrow(
|
|
173
|
+
"IRC send requires a resolved runtime config",
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
expect(hoisted.loadConfig).not.toHaveBeenCalled();
|
|
177
|
+
expect(client.sendPrivmsg).not.toHaveBeenCalled();
|
|
178
|
+
expect(hoisted.record).not.toHaveBeenCalled();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("sends with provided cfg even when the runtime store is not initialized", async () => {
|
|
182
|
+
const providedCfg = {
|
|
183
|
+
channels: {
|
|
184
|
+
irc: {
|
|
185
|
+
host: "irc.example.com",
|
|
186
|
+
nick: "actagent",
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
} as unknown as CoreConfig;
|
|
190
|
+
const client = {
|
|
191
|
+
isReady: vi.fn(() => true),
|
|
192
|
+
sendPrivmsg: vi.fn(),
|
|
193
|
+
} as unknown as IrcClient;
|
|
194
|
+
hoisted.record.mockImplementation(() => {
|
|
195
|
+
throw new Error("IRC runtime not initialized");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const result = await sendMessageIrc("#room", "hello", {
|
|
199
|
+
cfg: providedCfg,
|
|
200
|
+
client,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
expect(hoisted.loadConfig).not.toHaveBeenCalled();
|
|
204
|
+
expect(client.sendPrivmsg).toHaveBeenCalledWith("#room", "hello");
|
|
205
|
+
expect(result.target).toBe("#room");
|
|
206
|
+
expect(result.messageId).toBeTypeOf("string");
|
|
207
|
+
expect(result.messageId.length).toBeGreaterThan(0);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("preserves reply ids in receipts", async () => {
|
|
211
|
+
const providedCfg = {
|
|
212
|
+
channels: {
|
|
213
|
+
irc: {
|
|
214
|
+
host: "irc.example.com",
|
|
215
|
+
nick: "actagent",
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
} as unknown as CoreConfig;
|
|
219
|
+
const client = {
|
|
220
|
+
isReady: vi.fn(() => true),
|
|
221
|
+
sendPrivmsg: vi.fn(),
|
|
222
|
+
} as unknown as IrcClient;
|
|
223
|
+
|
|
224
|
+
const result = await sendMessageIrc("#room", "hello", {
|
|
225
|
+
cfg: providedCfg,
|
|
226
|
+
client,
|
|
227
|
+
replyTo: "irc-parent-1",
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
expect(client.sendPrivmsg).toHaveBeenCalledWith("#room", "hello\n\n[reply:irc-parent-1]");
|
|
231
|
+
expect(result.receipt.sentAt).toBeTypeOf("number");
|
|
232
|
+
expect(result.receipt.sentAt).toBeGreaterThan(0);
|
|
233
|
+
expect({ ...result.receipt, sentAt: 123 }).toEqual({
|
|
234
|
+
primaryPlatformMessageId: "irc-msg-1",
|
|
235
|
+
platformMessageIds: ["irc-msg-1"],
|
|
236
|
+
replyToId: "irc-parent-1",
|
|
237
|
+
parts: [
|
|
238
|
+
{
|
|
239
|
+
platformMessageId: "irc-msg-1",
|
|
240
|
+
kind: "text",
|
|
241
|
+
index: 0,
|
|
242
|
+
replyToId: "irc-parent-1",
|
|
243
|
+
raw: {
|
|
244
|
+
channel: "irc",
|
|
245
|
+
conversationId: "#room",
|
|
246
|
+
messageId: "irc-msg-1",
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
sentAt: 123,
|
|
251
|
+
raw: [
|
|
252
|
+
{
|
|
253
|
+
channel: "irc",
|
|
254
|
+
conversationId: "#room",
|
|
255
|
+
messageId: "irc-msg-1",
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("declares message adapter durable text, media, and reply with receipt proofs", async () => {
|
|
262
|
+
const providedCfg = {
|
|
263
|
+
channels: {
|
|
264
|
+
irc: {
|
|
265
|
+
host: "irc.example.com",
|
|
266
|
+
nick: "actagent",
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
} as unknown as CoreConfig;
|
|
270
|
+
const client = {
|
|
271
|
+
isReady: vi.fn(() => true),
|
|
272
|
+
join: vi.fn(),
|
|
273
|
+
sendPrivmsg: vi.fn(),
|
|
274
|
+
quit: vi.fn(),
|
|
275
|
+
} as unknown as IrcClient & {
|
|
276
|
+
join: ReturnType<typeof vi.fn>;
|
|
277
|
+
quit: ReturnType<typeof vi.fn>;
|
|
278
|
+
};
|
|
279
|
+
hoisted.connectIrcClient.mockResolvedValue(client);
|
|
280
|
+
|
|
281
|
+
const proofResults = await verifyChannelMessageAdapterCapabilityProofs({
|
|
282
|
+
adapterName: "irc",
|
|
283
|
+
adapter: ircMessageAdapter,
|
|
284
|
+
proofs: {
|
|
285
|
+
text: async () => {
|
|
286
|
+
const result = await ircMessageAdapter.send?.text?.({
|
|
287
|
+
cfg: providedCfg,
|
|
288
|
+
to: "#room",
|
|
289
|
+
text: "hello",
|
|
290
|
+
});
|
|
291
|
+
expect(result?.receipt.platformMessageIds).toEqual(["irc-msg-1"]);
|
|
292
|
+
expect(client.join).toHaveBeenCalledWith("#room");
|
|
293
|
+
expect(client.sendPrivmsg).toHaveBeenCalledWith("#room", "hello");
|
|
294
|
+
},
|
|
295
|
+
media: async () => {
|
|
296
|
+
const result = await ircMessageAdapter.send?.media?.({
|
|
297
|
+
cfg: providedCfg,
|
|
298
|
+
to: "#room",
|
|
299
|
+
text: "image",
|
|
300
|
+
mediaUrl: "https://example.com/image.png",
|
|
301
|
+
});
|
|
302
|
+
expect(result?.receipt.platformMessageIds).toEqual(["irc-msg-1"]);
|
|
303
|
+
expect(client.join).toHaveBeenCalledWith("#room");
|
|
304
|
+
expect(client.sendPrivmsg).toHaveBeenCalledWith(
|
|
305
|
+
"#room",
|
|
306
|
+
"image\n\nAttachment: https://example.com/image.png",
|
|
307
|
+
);
|
|
308
|
+
},
|
|
309
|
+
replyTo: async () => {
|
|
310
|
+
const result = await ircMessageAdapter.send?.text?.({
|
|
311
|
+
cfg: providedCfg,
|
|
312
|
+
to: "#room",
|
|
313
|
+
text: "threaded",
|
|
314
|
+
replyToId: "parent-1",
|
|
315
|
+
});
|
|
316
|
+
expect(result?.receipt.replyToId).toBe("parent-1");
|
|
317
|
+
expect(client.join).toHaveBeenCalledWith("#room");
|
|
318
|
+
expect(client.sendPrivmsg).toHaveBeenCalledWith("#room", "threaded\n\n[reply:parent-1]");
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
expect(proofResults.find((result) => result.capability === "text")?.status).toBe("verified");
|
|
324
|
+
expect(proofResults.find((result) => result.capability === "media")?.status).toBe("verified");
|
|
325
|
+
expect(proofResults.find((result) => result.capability === "replyTo")?.status).toBe("verified");
|
|
326
|
+
});
|
|
327
|
+
});
|
package/src/send.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Irc plugin module implements send behavior.
|
|
2
|
+
import {
|
|
3
|
+
createMessageReceiptFromOutboundResults,
|
|
4
|
+
type MessageReceipt,
|
|
5
|
+
} from "actagent/plugin-sdk/channel-outbound";
|
|
6
|
+
import { resolveMarkdownTableMode } from "actagent/plugin-sdk/markdown-table-runtime";
|
|
7
|
+
import { requireRuntimeConfig } from "actagent/plugin-sdk/plugin-config-runtime";
|
|
8
|
+
import { convertMarkdownTables } from "actagent/plugin-sdk/text-chunking";
|
|
9
|
+
import { resolveIrcAccount } from "./accounts.js";
|
|
10
|
+
import type { IrcClient } from "./client.js";
|
|
11
|
+
import { connectIrcClient } from "./client.js";
|
|
12
|
+
import { buildIrcConnectOptions } from "./connect-options.js";
|
|
13
|
+
import { normalizeIrcMessagingTarget } from "./normalize.js";
|
|
14
|
+
import { makeIrcMessageId } from "./protocol.js";
|
|
15
|
+
import { getIrcRuntime } from "./runtime.js";
|
|
16
|
+
import type { CoreConfig } from "./types.js";
|
|
17
|
+
|
|
18
|
+
type SendIrcOptions = {
|
|
19
|
+
cfg: CoreConfig;
|
|
20
|
+
accountId?: string;
|
|
21
|
+
replyTo?: string;
|
|
22
|
+
target?: string;
|
|
23
|
+
client?: IrcClient;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type SendIrcResult = {
|
|
27
|
+
messageId: string;
|
|
28
|
+
target: string;
|
|
29
|
+
receipt: MessageReceipt;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function recordIrcOutboundActivity(accountId: string): void {
|
|
33
|
+
try {
|
|
34
|
+
getIrcRuntime().channel.activity.record({
|
|
35
|
+
channel: "irc",
|
|
36
|
+
accountId,
|
|
37
|
+
direction: "outbound",
|
|
38
|
+
});
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (!(error instanceof Error) || error.message !== "IRC runtime not initialized") {
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function resolveTarget(to: string, opts?: SendIrcOptions): string {
|
|
47
|
+
const fromArg = normalizeIrcMessagingTarget(to);
|
|
48
|
+
if (fromArg) {
|
|
49
|
+
return fromArg;
|
|
50
|
+
}
|
|
51
|
+
const fromOpt = normalizeIrcMessagingTarget(opts?.target ?? "");
|
|
52
|
+
if (fromOpt) {
|
|
53
|
+
return fromOpt;
|
|
54
|
+
}
|
|
55
|
+
throw new Error(`Invalid IRC target: ${to}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function sendMessageIrc(
|
|
59
|
+
to: string,
|
|
60
|
+
text: string,
|
|
61
|
+
opts: SendIrcOptions,
|
|
62
|
+
): Promise<SendIrcResult> {
|
|
63
|
+
const cfg = requireRuntimeConfig(opts.cfg, "IRC send") as CoreConfig;
|
|
64
|
+
const account = resolveIrcAccount({
|
|
65
|
+
cfg,
|
|
66
|
+
accountId: opts.accountId,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (!account.configured) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`IRC is not configured for account "${account.accountId}" (need host and nick in channels.irc).`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const target = resolveTarget(to, opts);
|
|
76
|
+
const tableMode = resolveMarkdownTableMode({
|
|
77
|
+
cfg,
|
|
78
|
+
channel: "irc",
|
|
79
|
+
accountId: account.accountId,
|
|
80
|
+
});
|
|
81
|
+
const prepared = convertMarkdownTables(text.trim(), tableMode);
|
|
82
|
+
const payload = opts.replyTo ? `${prepared}\n\n[reply:${opts.replyTo}]` : prepared;
|
|
83
|
+
|
|
84
|
+
if (!payload.trim()) {
|
|
85
|
+
throw new Error("Message must be non-empty for IRC sends");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const client = opts.client;
|
|
89
|
+
if (client?.isReady()) {
|
|
90
|
+
client.sendPrivmsg(target, payload);
|
|
91
|
+
} else {
|
|
92
|
+
const transient = await connectIrcClient(
|
|
93
|
+
buildIrcConnectOptions(account, {
|
|
94
|
+
connectTimeoutMs: 12000,
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
if (target.startsWith("#") || target.startsWith("&")) {
|
|
98
|
+
transient.join(target);
|
|
99
|
+
}
|
|
100
|
+
transient.sendPrivmsg(target, payload);
|
|
101
|
+
transient.quit("sent");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
recordIrcOutboundActivity(account.accountId);
|
|
105
|
+
|
|
106
|
+
const messageId = makeIrcMessageId();
|
|
107
|
+
return {
|
|
108
|
+
messageId,
|
|
109
|
+
target,
|
|
110
|
+
receipt: createMessageReceiptFromOutboundResults({
|
|
111
|
+
results: [
|
|
112
|
+
{
|
|
113
|
+
channel: "irc",
|
|
114
|
+
messageId,
|
|
115
|
+
conversationId: target,
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
kind: "text",
|
|
119
|
+
...(opts.replyTo ? { replyToId: opts.replyTo } : {}),
|
|
120
|
+
}),
|
|
121
|
+
};
|
|
122
|
+
}
|