@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.
Files changed (42) hide show
  1. package/README.md +264 -0
  2. package/dist/bin/discord-cli.js +124118 -0
  3. package/dist/bin/manager.js +143 -0
  4. package/dist/bin/telegram-cli.js +124114 -0
  5. package/dist/index.js +125340 -0
  6. package/package.json +94 -0
  7. package/src/agent.ts +111 -0
  8. package/src/channels/base.ts +573 -0
  9. package/src/channels/discord.ts +306 -0
  10. package/src/channels/index.ts +169 -0
  11. package/src/channels/telegram.ts +315 -0
  12. package/src/daemon.ts +534 -0
  13. package/src/hooks.ts +97 -0
  14. package/src/index.ts +111 -0
  15. package/src/memory.ts +369 -0
  16. package/src/skills/coding/commit.ts +202 -0
  17. package/src/skills/coding/execute-subtask.ts +136 -0
  18. package/src/skills/coding/fix-issues.ts +126 -0
  19. package/src/skills/coding/index.ts +26 -0
  20. package/src/skills/coding/plan-task.ts +158 -0
  21. package/src/skills/coding/quality-check.ts +155 -0
  22. package/src/skills/index.ts +65 -0
  23. package/src/skills/registry.ts +380 -0
  24. package/src/skills/shared/index.ts +21 -0
  25. package/src/skills/shared/reflect.ts +156 -0
  26. package/src/skills/shared/review.ts +201 -0
  27. package/src/skills/shared/trajectory.ts +319 -0
  28. package/src/skills/trading/analyze-market.ts +144 -0
  29. package/src/skills/trading/check-risk.ts +176 -0
  30. package/src/skills/trading/execute-trade.ts +185 -0
  31. package/src/skills/trading/generate-signal.ts +160 -0
  32. package/src/skills/trading/index.ts +26 -0
  33. package/src/skills/trading/monitor-position.ts +179 -0
  34. package/src/skills/types.ts +235 -0
  35. package/src/skills/workflows.ts +340 -0
  36. package/src/state.ts +77 -0
  37. package/src/tools.ts +134 -0
  38. package/src/types.ts +314 -0
  39. package/src/workflow.ts +341 -0
  40. package/src/workflows/coding.ts +580 -0
  41. package/src/workflows/index.ts +61 -0
  42. 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
+ }