@ebowwa/daemons 0.5.0
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/README.md +264 -0
- package/dist/bin/discord-cli.js +124118 -0
- package/dist/bin/manager.js +143 -0
- package/dist/bin/telegram-cli.js +124114 -0
- package/dist/index.js +125340 -0
- package/package.json +94 -0
- package/src/agent.ts +111 -0
- package/src/channels/base.ts +573 -0
- package/src/channels/discord.ts +306 -0
- package/src/channels/index.ts +169 -0
- package/src/channels/telegram.ts +315 -0
- package/src/daemon.ts +534 -0
- package/src/hooks.ts +97 -0
- package/src/index.ts +111 -0
- package/src/memory.ts +369 -0
- package/src/skills/coding/commit.ts +202 -0
- package/src/skills/coding/execute-subtask.ts +136 -0
- package/src/skills/coding/fix-issues.ts +126 -0
- package/src/skills/coding/index.ts +26 -0
- package/src/skills/coding/plan-task.ts +158 -0
- package/src/skills/coding/quality-check.ts +155 -0
- package/src/skills/index.ts +65 -0
- package/src/skills/registry.ts +380 -0
- package/src/skills/shared/index.ts +21 -0
- package/src/skills/shared/reflect.ts +156 -0
- package/src/skills/shared/review.ts +201 -0
- package/src/skills/shared/trajectory.ts +319 -0
- package/src/skills/trading/analyze-market.ts +144 -0
- package/src/skills/trading/check-risk.ts +176 -0
- package/src/skills/trading/execute-trade.ts +185 -0
- package/src/skills/trading/generate-signal.ts +160 -0
- package/src/skills/trading/index.ts +26 -0
- package/src/skills/trading/monitor-position.ts +179 -0
- package/src/skills/types.ts +235 -0
- package/src/skills/workflows.ts +340 -0
- package/src/state.ts +77 -0
- package/src/tools.ts +134 -0
- package/src/types.ts +314 -0
- package/src/workflow.ts +341 -0
- package/src/workflows/coding.ts +580 -0
- package/src/workflows/index.ts +61 -0
- package/src/workflows/trading.ts +608 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord Channel Adapter
|
|
3
|
+
*
|
|
4
|
+
* Implements ChannelConnector from @ebowwa/channel-types.
|
|
5
|
+
* Discord bot using discord.js.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Client, GatewayIntentBits, type Message, type TextChannel } from "discord.js";
|
|
9
|
+
import {
|
|
10
|
+
type ChannelId,
|
|
11
|
+
type ChannelMessage,
|
|
12
|
+
type ChannelResponse,
|
|
13
|
+
type ChannelCapabilities,
|
|
14
|
+
type MessageSender,
|
|
15
|
+
type MessageContext,
|
|
16
|
+
createChannelId,
|
|
17
|
+
} from "@ebowwa/channel-types";
|
|
18
|
+
import {
|
|
19
|
+
BaseChannel,
|
|
20
|
+
type GLMChannelConfig,
|
|
21
|
+
type BaseChannelConfig,
|
|
22
|
+
} from "./base.js";
|
|
23
|
+
|
|
24
|
+
// ============================================================
|
|
25
|
+
// DISCORD CHANNEL CONFIG
|
|
26
|
+
// ============================================================
|
|
27
|
+
|
|
28
|
+
export interface DiscordChannelConfig extends GLMChannelConfig {
|
|
29
|
+
platform: "discord";
|
|
30
|
+
/** Bot token */
|
|
31
|
+
botToken: string;
|
|
32
|
+
/** Application ID for slash commands */
|
|
33
|
+
applicationId?: string;
|
|
34
|
+
/** Guild ID to restrict to */
|
|
35
|
+
guildId?: string;
|
|
36
|
+
/** Channel IDs to listen to (empty = all) */
|
|
37
|
+
channelIds?: string[];
|
|
38
|
+
/** Enable slash commands */
|
|
39
|
+
enableSlashCommands?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Legacy type alias for backwards compat
|
|
43
|
+
export type { DiscordChannelConfig as DiscordConfig };
|
|
44
|
+
|
|
45
|
+
// ============================================================
|
|
46
|
+
// DISCORD CHANNEL
|
|
47
|
+
// ============================================================
|
|
48
|
+
|
|
49
|
+
export class DiscordChannel extends BaseChannel {
|
|
50
|
+
readonly id: ChannelId;
|
|
51
|
+
readonly label = "Discord";
|
|
52
|
+
readonly capabilities: ChannelCapabilities = {
|
|
53
|
+
supports: {
|
|
54
|
+
text: true,
|
|
55
|
+
media: true,
|
|
56
|
+
replies: true,
|
|
57
|
+
threads: true,
|
|
58
|
+
reactions: true,
|
|
59
|
+
editing: true,
|
|
60
|
+
streaming: false, // Discord doesn't support true streaming
|
|
61
|
+
},
|
|
62
|
+
media: {
|
|
63
|
+
maxFileSize: 25 * 1024 * 1024, // 25MB (nitro is higher)
|
|
64
|
+
supportedMimeTypes: ["image/*", "video/*", "audio/*", "application/*"],
|
|
65
|
+
},
|
|
66
|
+
rateLimits: {
|
|
67
|
+
messagesPerMinute: 50,
|
|
68
|
+
charactersPerMessage: 2000,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
private client: Client;
|
|
73
|
+
private token: string;
|
|
74
|
+
private guildId?: string;
|
|
75
|
+
private channelIds?: string[];
|
|
76
|
+
|
|
77
|
+
constructor(config: DiscordChannelConfig) {
|
|
78
|
+
super(config);
|
|
79
|
+
this.token = config.botToken;
|
|
80
|
+
this.guildId = config.guildId;
|
|
81
|
+
this.channelIds = config.channelIds;
|
|
82
|
+
this.id = this.createId();
|
|
83
|
+
this.client = new Client({
|
|
84
|
+
intents: [
|
|
85
|
+
GatewayIntentBits.Guilds,
|
|
86
|
+
GatewayIntentBits.GuildMessages,
|
|
87
|
+
GatewayIntentBits.MessageContent,
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ============================================================
|
|
93
|
+
// ChannelConnector Implementation
|
|
94
|
+
// ============================================================
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Send response to Discord
|
|
98
|
+
*/
|
|
99
|
+
async send(response: ChannelResponse): Promise<void> {
|
|
100
|
+
const { channelId, messageId } = response.replyTo;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const channel = await this.client.channels.fetch(channelId.accountId);
|
|
104
|
+
if (!channel || !channel.isTextBased()) {
|
|
105
|
+
console.error("[Discord] Cannot send: channel not found or not text-based");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const textChannel = channel as TextChannel;
|
|
110
|
+
const message = await textChannel.messages.fetch(messageId);
|
|
111
|
+
|
|
112
|
+
if (response.content.replyToOriginal) {
|
|
113
|
+
await message.reply(response.content.text);
|
|
114
|
+
} else {
|
|
115
|
+
await textChannel.send(response.content.text);
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.error("[Discord] Error sending response:", error);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ============================================================
|
|
123
|
+
// Platform-Specific Implementation
|
|
124
|
+
// ============================================================
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Start Discord bot
|
|
128
|
+
*/
|
|
129
|
+
protected async startPlatform(): Promise<void> {
|
|
130
|
+
console.log("[Discord] Starting bot...");
|
|
131
|
+
|
|
132
|
+
this.client.on("ready", () => {
|
|
133
|
+
console.log(`[Discord] Logged in as ${this.client.user?.tag}`);
|
|
134
|
+
console.log("[Discord] Ready to handle messages");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
this.client.on("messageCreate", async (message: Message) => {
|
|
138
|
+
// Ignore bot messages
|
|
139
|
+
if (message.author.bot) return;
|
|
140
|
+
|
|
141
|
+
// Check guild restriction
|
|
142
|
+
if (this.guildId && message.guildId !== this.guildId) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check channel restriction
|
|
147
|
+
if (this.channelIds?.length && !this.channelIds.includes(message.channelId)) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log(`[Discord] [${message.author.tag}]: ${message.content}`);
|
|
152
|
+
|
|
153
|
+
// Send typing indicator
|
|
154
|
+
if ("sendTyping" in message.channel && typeof message.channel.sendTyping === "function") {
|
|
155
|
+
await message.channel.sendTyping();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
// Create normalized ChannelMessage
|
|
160
|
+
const channelMessage = this.createChannelMessage(message);
|
|
161
|
+
|
|
162
|
+
// Route through base channel
|
|
163
|
+
const response = await this.routeChannelMessage(channelMessage);
|
|
164
|
+
|
|
165
|
+
// Send response
|
|
166
|
+
await this.send(response);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error("[Discord] Error handling message:", error);
|
|
169
|
+
await message.reply(
|
|
170
|
+
`Sorry, I encountered an error: ${error instanceof Error ? error.message : String(error)}`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
await this.client.login(this.token);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Stop Discord bot
|
|
180
|
+
*/
|
|
181
|
+
protected async stopPlatform(): Promise<void> {
|
|
182
|
+
console.log("[Discord] Shutting down...");
|
|
183
|
+
await this.client.destroy();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ============================================================
|
|
187
|
+
// Discord-Specific Helpers
|
|
188
|
+
// ============================================================
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Create normalized ChannelMessage from Discord message
|
|
192
|
+
*/
|
|
193
|
+
private createChannelMessage(msg: Message): ChannelMessage {
|
|
194
|
+
const sender: MessageSender = {
|
|
195
|
+
id: msg.author.id,
|
|
196
|
+
username: msg.author.username,
|
|
197
|
+
displayName: msg.author.displayName || msg.author.username,
|
|
198
|
+
isBot: msg.author.bot,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const context: MessageContext = {
|
|
202
|
+
isDM: msg.channel.isDMBased(),
|
|
203
|
+
groupName: !msg.channel.isDMBased() ? msg.guild?.name : undefined,
|
|
204
|
+
threadId: msg.channel.isThread() ? msg.channelId : undefined,
|
|
205
|
+
metadata: {
|
|
206
|
+
guildId: msg.guildId,
|
|
207
|
+
channelId: msg.channelId,
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// Extract media attachments
|
|
212
|
+
const media = msg.attachments.size > 0
|
|
213
|
+
? msg.attachments.map((att) => ({
|
|
214
|
+
type: att.contentType?.startsWith("image")
|
|
215
|
+
? "image" as const
|
|
216
|
+
: att.contentType?.startsWith("video")
|
|
217
|
+
? "video" as const
|
|
218
|
+
: att.contentType?.startsWith("audio")
|
|
219
|
+
? "audio" as const
|
|
220
|
+
: "file" as const,
|
|
221
|
+
url: att.url,
|
|
222
|
+
mimeType: att.contentType || undefined,
|
|
223
|
+
filename: att.name || undefined,
|
|
224
|
+
}))
|
|
225
|
+
: undefined;
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
messageId: msg.id,
|
|
229
|
+
channelId: {
|
|
230
|
+
platform: "discord",
|
|
231
|
+
accountId: msg.channelId,
|
|
232
|
+
},
|
|
233
|
+
timestamp: new Date(msg.createdTimestamp),
|
|
234
|
+
sender,
|
|
235
|
+
text: msg.content,
|
|
236
|
+
media,
|
|
237
|
+
context,
|
|
238
|
+
replyTo: msg.reference?.messageId
|
|
239
|
+
? {
|
|
240
|
+
messageId: msg.reference.messageId,
|
|
241
|
+
channelId: {
|
|
242
|
+
platform: "discord",
|
|
243
|
+
accountId: msg.reference.channelId,
|
|
244
|
+
},
|
|
245
|
+
}
|
|
246
|
+
: undefined,
|
|
247
|
+
quotedText: undefined,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Send response, handling chunking for long messages
|
|
253
|
+
*/
|
|
254
|
+
private async sendResponse(message: Message, response: string): Promise<void> {
|
|
255
|
+
const maxLength = 2000; // Discord message limit
|
|
256
|
+
|
|
257
|
+
if (response.length > maxLength) {
|
|
258
|
+
const chunks = response.match(/[\s\S]{1,2000}/g) || [];
|
|
259
|
+
for (const chunk of chunks) {
|
|
260
|
+
await message.reply(chunk);
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
await message.reply(response);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ============================================================
|
|
269
|
+
// FACTORY FUNCTION
|
|
270
|
+
// ============================================================
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Create a Discord channel from config
|
|
274
|
+
*/
|
|
275
|
+
export function createDiscordChannel(config: DiscordChannelConfig): DiscordChannel {
|
|
276
|
+
return new DiscordChannel(config);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Create Discord channel config from environment (Doppler)
|
|
281
|
+
*/
|
|
282
|
+
export function createDiscordConfigFromEnv(): DiscordChannelConfig | null {
|
|
283
|
+
const token = process.env.DISCORD_BOT_TOKEN;
|
|
284
|
+
if (!token) return null;
|
|
285
|
+
|
|
286
|
+
const channelIds = process.env.DISCORD_CHANNEL_IDS
|
|
287
|
+
?.split(",")
|
|
288
|
+
.map((s) => s.trim())
|
|
289
|
+
.filter(Boolean);
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
platform: "discord",
|
|
293
|
+
accountId: process.env.DISCORD_ACCOUNT_ID || "default",
|
|
294
|
+
instanceId: process.env.DISCORD_INSTANCE_ID,
|
|
295
|
+
botToken: token,
|
|
296
|
+
applicationId: process.env.DISCORD_APPLICATION_ID,
|
|
297
|
+
guildId: process.env.DISCORD_GUILD_ID,
|
|
298
|
+
channelIds,
|
|
299
|
+
enableSlashCommands: process.env.DISCORD_ENABLE_SLASH_COMMANDS !== "false",
|
|
300
|
+
daemonWorkdir: process.env.DAEMON_WORKDIR,
|
|
301
|
+
daemonBaseBranch: process.env.DAEMON_BASE_BRANCH,
|
|
302
|
+
enableDaemonAutoPR: process.env.DAEMON_AUTO_PR === "true",
|
|
303
|
+
enableDaemonAutoCommit: process.env.DAEMON_AUTO_COMMIT === "true",
|
|
304
|
+
butlerStorageDir: process.env.BUTLER_STORAGE_DIR,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GLM Daemon Channels
|
|
3
|
+
*
|
|
4
|
+
* Communication channel adapters for GLM Daemon.
|
|
5
|
+
* All channels implement ChannelConnector from @ebowwa/channel-types.
|
|
6
|
+
*
|
|
7
|
+
* Supported platforms:
|
|
8
|
+
* - Telegram
|
|
9
|
+
* - Discord
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Re-export types from channel-types for convenience
|
|
13
|
+
export type {
|
|
14
|
+
ChannelConnector,
|
|
15
|
+
ChannelId,
|
|
16
|
+
ChannelMessage,
|
|
17
|
+
ChannelResponse,
|
|
18
|
+
ChannelCapabilities,
|
|
19
|
+
MessageHandler,
|
|
20
|
+
MessageRef,
|
|
21
|
+
ResponseContent,
|
|
22
|
+
} from "@ebowwa/channel-types";
|
|
23
|
+
|
|
24
|
+
// Base channel
|
|
25
|
+
export {
|
|
26
|
+
BaseChannel,
|
|
27
|
+
type GLMChannelConfig,
|
|
28
|
+
type BaseChannelConfig,
|
|
29
|
+
type MessageContext,
|
|
30
|
+
type RouteResult,
|
|
31
|
+
type MessageClassification,
|
|
32
|
+
} from "./base.js";
|
|
33
|
+
|
|
34
|
+
// Telegram (GLM-powered channel wrapping @ebowwa/channel-telegram)
|
|
35
|
+
export {
|
|
36
|
+
GLMTelegramChannel,
|
|
37
|
+
type TelegramChannelConfig,
|
|
38
|
+
type TelegramConfig,
|
|
39
|
+
createTelegramChannel,
|
|
40
|
+
createTelegramConfigFromEnv,
|
|
41
|
+
} from "./telegram.js";
|
|
42
|
+
|
|
43
|
+
// Legacy alias for backwards compat
|
|
44
|
+
export { GLMTelegramChannel as TelegramChannel } from "./telegram.js";
|
|
45
|
+
|
|
46
|
+
// Discord
|
|
47
|
+
export {
|
|
48
|
+
DiscordChannel,
|
|
49
|
+
type DiscordChannelConfig,
|
|
50
|
+
type DiscordConfig,
|
|
51
|
+
createDiscordChannel,
|
|
52
|
+
createDiscordConfigFromEnv,
|
|
53
|
+
} from "./discord.js";
|
|
54
|
+
|
|
55
|
+
// ============================================================
|
|
56
|
+
// CHANNEL REGISTRY
|
|
57
|
+
// ============================================================
|
|
58
|
+
|
|
59
|
+
import type { ChannelConnector } from "@ebowwa/channel-types";
|
|
60
|
+
import { BaseChannel } from "./base.js";
|
|
61
|
+
import {
|
|
62
|
+
GLMTelegramChannel,
|
|
63
|
+
type TelegramChannelConfig,
|
|
64
|
+
createTelegramConfigFromEnv,
|
|
65
|
+
} from "./telegram.js";
|
|
66
|
+
import {
|
|
67
|
+
DiscordChannel,
|
|
68
|
+
type DiscordChannelConfig,
|
|
69
|
+
createDiscordConfigFromEnv,
|
|
70
|
+
} from "./discord.js";
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Channel registry for managing multiple channels
|
|
74
|
+
*/
|
|
75
|
+
export class ChannelRegistry {
|
|
76
|
+
private static channels = new Map<string, ChannelConnector>();
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Register a channel
|
|
80
|
+
*/
|
|
81
|
+
static register(name: string, channel: ChannelConnector): void {
|
|
82
|
+
this.channels.set(name, channel);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get a registered channel
|
|
87
|
+
*/
|
|
88
|
+
static get(name: string): ChannelConnector | undefined {
|
|
89
|
+
return this.channels.get(name);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Unregister a channel
|
|
94
|
+
*/
|
|
95
|
+
static unregister(name: string): void {
|
|
96
|
+
const channel = this.channels.get(name);
|
|
97
|
+
if (channel) {
|
|
98
|
+
channel.stop();
|
|
99
|
+
this.channels.delete(name);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get all registered channels
|
|
105
|
+
*/
|
|
106
|
+
static getAll(): Map<string, ChannelConnector> {
|
|
107
|
+
return new Map(this.channels);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Stop all channels
|
|
112
|
+
*/
|
|
113
|
+
static async stopAll(): Promise<void> {
|
|
114
|
+
const stopPromises = Array.from(this.channels.values()).map((channel) => channel.stop());
|
|
115
|
+
await Promise.all(stopPromises);
|
|
116
|
+
this.channels.clear();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Start all channels
|
|
121
|
+
*/
|
|
122
|
+
static async startAll(): Promise<void> {
|
|
123
|
+
const startPromises = Array.from(this.channels.values()).map((channel) => channel.start());
|
|
124
|
+
await Promise.all(startPromises);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ============================================================
|
|
129
|
+
// FACTORY FUNCTIONS
|
|
130
|
+
// ============================================================
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Create all channels from environment variables (Doppler)
|
|
134
|
+
*/
|
|
135
|
+
export function createChannelsFromEnv(): ChannelConnector[] {
|
|
136
|
+
const channels: ChannelConnector[] = [];
|
|
137
|
+
|
|
138
|
+
// Telegram
|
|
139
|
+
const telegramConfig = createTelegramConfigFromEnv();
|
|
140
|
+
if (telegramConfig) {
|
|
141
|
+
channels.push(new GLMTelegramChannel(telegramConfig));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Discord
|
|
145
|
+
const discordConfig = createDiscordConfigFromEnv();
|
|
146
|
+
if (discordConfig) {
|
|
147
|
+
channels.push(new DiscordChannel(discordConfig));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return channels;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Initialize channels from environment and register them
|
|
155
|
+
*/
|
|
156
|
+
export async function initializeChannelsFromEnv(): Promise<ChannelRegistry> {
|
|
157
|
+
const channels = createChannelsFromEnv();
|
|
158
|
+
|
|
159
|
+
for (const channel of channels) {
|
|
160
|
+
// Use platform:accountId as the key
|
|
161
|
+
const key = `${channel.id.platform}:${channel.id.accountId}`;
|
|
162
|
+
ChannelRegistry.register(key, channel);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Start all channels
|
|
166
|
+
await ChannelRegistry.startAll();
|
|
167
|
+
|
|
168
|
+
return ChannelRegistry;
|
|
169
|
+
}
|