@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,573 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Channel - Implements ChannelConnector from @ebowwa/channel-types
|
|
3
|
+
*
|
|
4
|
+
* Abstract base class for all communication channels (Telegram, Discord, etc.)
|
|
5
|
+
* Provides shared functionality for message routing, error handling, and conversation management.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type ChannelConnector,
|
|
10
|
+
type ChannelId,
|
|
11
|
+
type ChannelMessage,
|
|
12
|
+
type ChannelResponse,
|
|
13
|
+
type ChannelCapabilities,
|
|
14
|
+
type MessageHandler,
|
|
15
|
+
type MessageRef,
|
|
16
|
+
type ResponseContent,
|
|
17
|
+
type StreamChunk,
|
|
18
|
+
createChannelId,
|
|
19
|
+
createMessageRef,
|
|
20
|
+
RICH_CAPABILITIES,
|
|
21
|
+
} from "@ebowwa/channel-types";
|
|
22
|
+
import { GLMClient } from "@ebowwa/ai";
|
|
23
|
+
import { ToolExecutor, BUILTIN_TOOLS } from "@ebowwa/ai/tools";
|
|
24
|
+
import { GLMDaemon, type GLMDaemonConfig } from "../daemon.js";
|
|
25
|
+
|
|
26
|
+
// ============================================================
|
|
27
|
+
// CHANNEL CONFIG (extends base config from channel-types)
|
|
28
|
+
// ============================================================
|
|
29
|
+
|
|
30
|
+
export interface GLMChannelConfig {
|
|
31
|
+
/** Platform identifier */
|
|
32
|
+
platform: "telegram" | "discord" | "whatsapp" | "cli";
|
|
33
|
+
|
|
34
|
+
/** Account/bot ID */
|
|
35
|
+
accountId: string;
|
|
36
|
+
|
|
37
|
+
/** Instance ID for multiple instances */
|
|
38
|
+
instanceId?: string;
|
|
39
|
+
|
|
40
|
+
/** Butler storage directory */
|
|
41
|
+
butlerStorageDir?: string;
|
|
42
|
+
|
|
43
|
+
/** Working directory for daemon tasks */
|
|
44
|
+
daemonWorkdir?: string;
|
|
45
|
+
|
|
46
|
+
/** Base branch for daemon */
|
|
47
|
+
daemonBaseBranch?: string;
|
|
48
|
+
|
|
49
|
+
/** Auto-PR on completion */
|
|
50
|
+
enableDaemonAutoPR?: boolean;
|
|
51
|
+
|
|
52
|
+
/** Auto-commit during work */
|
|
53
|
+
enableDaemonAutoCommit?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ============================================================
|
|
57
|
+
// INTERNAL TYPES (kept for compatibility)
|
|
58
|
+
// ============================================================
|
|
59
|
+
|
|
60
|
+
export interface MessageContext {
|
|
61
|
+
userId: string;
|
|
62
|
+
userName?: string;
|
|
63
|
+
channelId?: string;
|
|
64
|
+
timestamp: Date;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface RouteResult {
|
|
68
|
+
source: "butler" | "daemon" | "error";
|
|
69
|
+
response: string;
|
|
70
|
+
metadata?: {
|
|
71
|
+
daemonId?: string;
|
|
72
|
+
phase?: string;
|
|
73
|
+
executionTime?: number;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface MessageClassification {
|
|
78
|
+
type: "chat" | "task" | "status" | "unknown";
|
|
79
|
+
confidence: number;
|
|
80
|
+
suggestedAction: "handle_locally" | "delegate_daemon" | "both";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Legacy config type for backwards compat
|
|
84
|
+
export type BaseChannelConfig = GLMChannelConfig;
|
|
85
|
+
|
|
86
|
+
// ============================================================
|
|
87
|
+
// ABSTRACT BASE CHANNEL
|
|
88
|
+
// ============================================================
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Abstract base class for communication channels.
|
|
92
|
+
* Implements ChannelConnector from @ebowwa/channel-types.
|
|
93
|
+
*/
|
|
94
|
+
export abstract class BaseChannel implements ChannelConnector {
|
|
95
|
+
// ChannelConnector interface
|
|
96
|
+
abstract readonly id: ChannelId;
|
|
97
|
+
abstract readonly label: string;
|
|
98
|
+
abstract readonly capabilities: ChannelCapabilities;
|
|
99
|
+
|
|
100
|
+
// Internal state
|
|
101
|
+
protected glmClient: GLMClient;
|
|
102
|
+
protected daemon: GLMDaemon | null = null;
|
|
103
|
+
protected activeDaemonTask: string | null = null;
|
|
104
|
+
protected config: GLMChannelConfig;
|
|
105
|
+
protected conversationHistory: Map<string, Array<{ role: string; content: string }>> = new Map();
|
|
106
|
+
protected messageHandler?: MessageHandler;
|
|
107
|
+
protected connected: boolean = false;
|
|
108
|
+
|
|
109
|
+
constructor(config: GLMChannelConfig) {
|
|
110
|
+
this.config = config;
|
|
111
|
+
this.glmClient = new GLMClient();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ============================================================
|
|
115
|
+
// ChannelConnector Interface Implementation
|
|
116
|
+
// ============================================================
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Start the channel
|
|
120
|
+
*/
|
|
121
|
+
async start(): Promise<void> {
|
|
122
|
+
await this.startPlatform();
|
|
123
|
+
this.connected = true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Stop the channel
|
|
128
|
+
*/
|
|
129
|
+
async stop(): Promise<void> {
|
|
130
|
+
await this.stopPlatform();
|
|
131
|
+
await this.cleanup();
|
|
132
|
+
this.connected = false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Register message handler (from ChannelConnector interface)
|
|
137
|
+
*/
|
|
138
|
+
onMessage(handler: MessageHandler): void {
|
|
139
|
+
this.messageHandler = handler;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Send response (from ChannelConnector interface)
|
|
144
|
+
* Must be implemented by subclasses for platform-specific delivery
|
|
145
|
+
*/
|
|
146
|
+
abstract send(response: ChannelResponse): Promise<void>;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Optional streaming support
|
|
150
|
+
*/
|
|
151
|
+
async stream?(response: ChannelResponse, chunks: AsyncIterable<StreamChunk>): Promise<void>;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Check if connected
|
|
155
|
+
*/
|
|
156
|
+
isConnected(): boolean {
|
|
157
|
+
return this.connected;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ============================================================
|
|
161
|
+
// Platform-specific abstract methods
|
|
162
|
+
// ============================================================
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Start the platform-specific bot (must be implemented by subclasses)
|
|
166
|
+
*/
|
|
167
|
+
protected abstract startPlatform(): Promise<void>;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Stop the platform-specific bot (must be implemented by subclasses)
|
|
171
|
+
*/
|
|
172
|
+
protected abstract stopPlatform(): Promise<void>;
|
|
173
|
+
|
|
174
|
+
// ============================================================
|
|
175
|
+
// Message Routing (Core Logic)
|
|
176
|
+
// ============================================================
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Route a ChannelMessage through the internal handler or use default routing.
|
|
180
|
+
* This bridges ChannelMessage (from channel-types) to the internal routing logic.
|
|
181
|
+
*/
|
|
182
|
+
async routeChannelMessage(message: ChannelMessage): Promise<ChannelResponse> {
|
|
183
|
+
// If external handler is registered, use it
|
|
184
|
+
if (this.messageHandler) {
|
|
185
|
+
return this.messageHandler(message) as Promise<ChannelResponse>;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Otherwise use internal routing
|
|
189
|
+
const context: MessageContext = {
|
|
190
|
+
userId: message.sender.id,
|
|
191
|
+
userName: message.sender.displayName || message.sender.username,
|
|
192
|
+
channelId: message.context.groupName || message.channelId.accountId,
|
|
193
|
+
timestamp: message.timestamp,
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const result = await this.routeMessage(message.text, context);
|
|
197
|
+
|
|
198
|
+
// Convert RouteResult to ChannelResponse
|
|
199
|
+
return this.resultToResponse(result, message);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Route a message to appropriate handler (internal routing logic)
|
|
204
|
+
*/
|
|
205
|
+
async routeMessage(content: string, context: MessageContext): Promise<RouteResult> {
|
|
206
|
+
// Update conversation history
|
|
207
|
+
this.addToHistory(context.userId, { role: "user", content });
|
|
208
|
+
|
|
209
|
+
// Classify message
|
|
210
|
+
const classification = await this.classifyMessage(content);
|
|
211
|
+
|
|
212
|
+
console.log(
|
|
213
|
+
`[${this.constructor.name}] Message classified: ${classification.type} (${classification.confidence})`
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
// Route based on classification
|
|
217
|
+
switch (classification.type) {
|
|
218
|
+
case "chat":
|
|
219
|
+
return await this.handleChat(content, context);
|
|
220
|
+
|
|
221
|
+
case "task":
|
|
222
|
+
return await this.handleTask(content, context);
|
|
223
|
+
|
|
224
|
+
case "status":
|
|
225
|
+
return await this.handleStatus();
|
|
226
|
+
|
|
227
|
+
default:
|
|
228
|
+
return await this.handleUnknown(content);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Convert internal RouteResult to ChannelResponse
|
|
234
|
+
*/
|
|
235
|
+
protected resultToResponse(result: RouteResult, originalMessage: ChannelMessage): ChannelResponse {
|
|
236
|
+
const replyTo: MessageRef = createMessageRef(
|
|
237
|
+
originalMessage.messageId,
|
|
238
|
+
originalMessage.channelId
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const content: ResponseContent = {
|
|
242
|
+
text: result.response,
|
|
243
|
+
replyToOriginal: true,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
content,
|
|
248
|
+
replyTo,
|
|
249
|
+
isComplete: result.source !== "daemon" || !this.activeDaemonTask,
|
|
250
|
+
metadata: {
|
|
251
|
+
source: result.source,
|
|
252
|
+
...result.metadata,
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ============================================================
|
|
258
|
+
// Message Classification
|
|
259
|
+
// ============================================================
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Classify incoming message
|
|
263
|
+
*/
|
|
264
|
+
protected async classifyMessage(content: string): Promise<MessageClassification> {
|
|
265
|
+
const lower = content.toLowerCase();
|
|
266
|
+
|
|
267
|
+
// Status queries
|
|
268
|
+
if (
|
|
269
|
+
lower.includes("status") ||
|
|
270
|
+
lower.includes("progress") ||
|
|
271
|
+
lower.includes("what are you doing")
|
|
272
|
+
) {
|
|
273
|
+
return { type: "status", confidence: 0.95, suggestedAction: "handle_locally" };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Task indicators
|
|
277
|
+
const taskKeywords = [
|
|
278
|
+
"implement",
|
|
279
|
+
"build",
|
|
280
|
+
"create",
|
|
281
|
+
"fix",
|
|
282
|
+
"add",
|
|
283
|
+
"remove",
|
|
284
|
+
"refactor",
|
|
285
|
+
"deploy",
|
|
286
|
+
"test",
|
|
287
|
+
"debug",
|
|
288
|
+
"optimize",
|
|
289
|
+
"write",
|
|
290
|
+
"generate code",
|
|
291
|
+
"make a",
|
|
292
|
+
"create a",
|
|
293
|
+
"build a",
|
|
294
|
+
"pr for",
|
|
295
|
+
"branch for",
|
|
296
|
+
];
|
|
297
|
+
|
|
298
|
+
const hasTaskKeyword = taskKeywords.some((kw) => lower.includes(kw));
|
|
299
|
+
const isComplex = content.length > 100 || content.split("\n").length > 2;
|
|
300
|
+
|
|
301
|
+
if (hasTaskKeyword && isComplex) {
|
|
302
|
+
return { type: "task", confidence: 0.85, suggestedAction: "delegate_daemon" };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Chat indicators
|
|
306
|
+
const chatKeywords = [
|
|
307
|
+
"hello",
|
|
308
|
+
"hi",
|
|
309
|
+
"hey",
|
|
310
|
+
"thanks",
|
|
311
|
+
"help",
|
|
312
|
+
"what can you",
|
|
313
|
+
"how are you",
|
|
314
|
+
"remember",
|
|
315
|
+
"tell me",
|
|
316
|
+
"explain",
|
|
317
|
+
"describe",
|
|
318
|
+
"list",
|
|
319
|
+
];
|
|
320
|
+
|
|
321
|
+
const hasChatKeyword = chatKeywords.some((kw) => lower.includes(kw));
|
|
322
|
+
|
|
323
|
+
if (hasChatKeyword || !hasTaskKeyword) {
|
|
324
|
+
return { type: "chat", confidence: 0.75, suggestedAction: "handle_locally" };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return { type: "unknown", confidence: 0.5, suggestedAction: "handle_locally" };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ============================================================
|
|
331
|
+
// Message Handlers
|
|
332
|
+
// ============================================================
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Handle chat messages locally with GLM + tools
|
|
336
|
+
*
|
|
337
|
+
* Uses ToolExecutor from @ebowwa/ai/tools for the tool call loop.
|
|
338
|
+
* Refactored from manual tool loop on Feb 15, 2026.
|
|
339
|
+
*/
|
|
340
|
+
protected async handleChat(content: string, context: MessageContext): Promise<RouteResult> {
|
|
341
|
+
const start = Date.now();
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
const history = this.conversationHistory.get(context.userId) || [];
|
|
345
|
+
|
|
346
|
+
// Build system prompt with context
|
|
347
|
+
const systemPrompt = `You are a helpful AI assistant with tool access. You have access to conversation history and user context.
|
|
348
|
+
|
|
349
|
+
User: ${context.userName || context.userId}
|
|
350
|
+
Time: ${context.timestamp.toISOString()}
|
|
351
|
+
Working Directory: ${this.config.daemonWorkdir || process.cwd()}
|
|
352
|
+
|
|
353
|
+
You have access to tools for:
|
|
354
|
+
- File operations (read_file, write_file, edit_file, list_dir)
|
|
355
|
+
- Shell commands (run_command) - use for git, system info, etc.
|
|
356
|
+
- Git operations (git_status)
|
|
357
|
+
- System info (system_info)
|
|
358
|
+
|
|
359
|
+
USE TOOLS when the user asks you to do something (not just chat). Examples:
|
|
360
|
+
- "list files" → use list_dir
|
|
361
|
+
- "read package.json" → use read_file
|
|
362
|
+
- "what's the git status" → use git_status
|
|
363
|
+
- "run npm test" → use run_command
|
|
364
|
+
|
|
365
|
+
Always use tools when appropriate. Be helpful and execute tasks.`;
|
|
366
|
+
|
|
367
|
+
// Build messages array from history (exclude system - ToolExecutor adds it)
|
|
368
|
+
const messages = [
|
|
369
|
+
...history.slice(-10).map(msg => ({
|
|
370
|
+
role: msg.role as "user" | "assistant",
|
|
371
|
+
content: msg.content
|
|
372
|
+
})),
|
|
373
|
+
{ role: "user" as const, content }
|
|
374
|
+
];
|
|
375
|
+
|
|
376
|
+
// Use ToolExecutor from @ebowwa/ai/tools
|
|
377
|
+
const executor = new ToolExecutor(this.glmClient, BUILTIN_TOOLS);
|
|
378
|
+
const result = await executor.executeWithTools(messages, {
|
|
379
|
+
systemPrompt,
|
|
380
|
+
maxIterations: 100, // High limit for complex tasks (was 5)
|
|
381
|
+
temperature: 0.7,
|
|
382
|
+
maxTokens: 4096,
|
|
383
|
+
logger: (msg: string) => console.log(`[${this.constructor.name}] ${msg}`),
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
const responseText = result.content;
|
|
387
|
+
|
|
388
|
+
this.addToHistory(context.userId, { role: "assistant", content: responseText });
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
source: "butler",
|
|
392
|
+
response: responseText,
|
|
393
|
+
metadata: { executionTime: Date.now() - start, toolIterations: result.iterations },
|
|
394
|
+
};
|
|
395
|
+
} catch (error) {
|
|
396
|
+
console.error(`[${this.constructor.name}] handleChat error:`, error);
|
|
397
|
+
return {
|
|
398
|
+
source: "error",
|
|
399
|
+
response: `Sorry, I encountered an error: ${error instanceof Error ? error.message : String(error)}`,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Handle task messages by delegating to GLMDaemon
|
|
406
|
+
*/
|
|
407
|
+
protected async handleTask(content: string, context: MessageContext): Promise<RouteResult> {
|
|
408
|
+
const start = Date.now();
|
|
409
|
+
|
|
410
|
+
if (this.activeDaemonTask && this.daemon) {
|
|
411
|
+
const status = this.daemon.getStatus();
|
|
412
|
+
return {
|
|
413
|
+
source: "daemon",
|
|
414
|
+
response: `I'm currently working on: "${this.activeDaemonTask}"\n\nStatus: ${status?.phase || "unknown"}\n\nPlease wait for this to complete, or use "stop" to cancel it.`,
|
|
415
|
+
metadata: {
|
|
416
|
+
daemonId: status?.id,
|
|
417
|
+
phase: status?.phase,
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (content.toLowerCase().includes("stop") && this.daemon) {
|
|
423
|
+
await this.daemon.stop();
|
|
424
|
+
this.daemon = null;
|
|
425
|
+
this.activeDaemonTask = null;
|
|
426
|
+
return {
|
|
427
|
+
source: "daemon",
|
|
428
|
+
response: "Task stopped. Ready for new instructions.",
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
try {
|
|
433
|
+
const daemonConfig: GLMDaemonConfig = {
|
|
434
|
+
cwd: this.config.daemonWorkdir || process.cwd(),
|
|
435
|
+
teamName: `${this.constructor.name.toLowerCase()}-task-${Date.now()}`,
|
|
436
|
+
model: "glm-4.7",
|
|
437
|
+
autoCommit: this.config.enableDaemonAutoCommit || false,
|
|
438
|
+
autoPR: this.config.enableDaemonAutoPR || false,
|
|
439
|
+
baseBranch: this.config.daemonBaseBranch || "dev",
|
|
440
|
+
completionPromise: "TASK_COMPLETE",
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
this.daemon = new GLMDaemon(daemonConfig);
|
|
444
|
+
this.activeDaemonTask = content;
|
|
445
|
+
|
|
446
|
+
const teamName = await this.daemon.start(content);
|
|
447
|
+
const status = this.daemon.getStatus();
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
source: "daemon",
|
|
451
|
+
response: `Started working on: "${content}"\n\nTeam: ${teamName}\nPhase: ${status.phase}\n\nI'll work through this using SLAM (Planning → Executing → Reviewing → Fixing → Committing).\n\nCheck status anytime with "status" or "progress".`,
|
|
452
|
+
metadata: {
|
|
453
|
+
daemonId: status.id,
|
|
454
|
+
phase: status.phase,
|
|
455
|
+
executionTime: Date.now() - start,
|
|
456
|
+
},
|
|
457
|
+
};
|
|
458
|
+
} catch (error) {
|
|
459
|
+
this.daemon = null;
|
|
460
|
+
this.activeDaemonTask = null;
|
|
461
|
+
return {
|
|
462
|
+
source: "error",
|
|
463
|
+
response: `Failed to start task: ${error instanceof Error ? error.message : String(error)}`,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Handle status queries
|
|
470
|
+
*/
|
|
471
|
+
protected async handleStatus(): Promise<RouteResult> {
|
|
472
|
+
const parts: string[] = [];
|
|
473
|
+
|
|
474
|
+
parts.push("**Butler (Chat):** Ready");
|
|
475
|
+
parts.push("- Memory: Active");
|
|
476
|
+
parts.push("- Scheduled tasks: Running");
|
|
477
|
+
|
|
478
|
+
if (this.daemon && this.activeDaemonTask) {
|
|
479
|
+
const status = this.daemon.getStatus();
|
|
480
|
+
parts.push(`\n**Daemon (Task):** Active`);
|
|
481
|
+
parts.push(`- Task: "${this.activeDaemonTask}"`);
|
|
482
|
+
parts.push(`- Phase: ${status.phase}`);
|
|
483
|
+
parts.push(`- Iteration: ${status.iteration}`);
|
|
484
|
+
parts.push(`- Team: ${status.id}`);
|
|
485
|
+
|
|
486
|
+
if (status.totalSubtasks > 0) {
|
|
487
|
+
parts.push(`- Progress: ${status.completedSubtasks}/${status.totalSubtasks} subtasks`);
|
|
488
|
+
}
|
|
489
|
+
} else {
|
|
490
|
+
parts.push("\n**Daemon (Task):** Idle");
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
parts.push(`\n**System:**`);
|
|
494
|
+
parts.push(`- Active conversations: ${this.conversationHistory.size}`);
|
|
495
|
+
parts.push(`- Uptime: ${process.uptime().toFixed(0)}s`);
|
|
496
|
+
|
|
497
|
+
return {
|
|
498
|
+
source: "butler",
|
|
499
|
+
response: parts.join("\n"),
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Handle unknown messages - ask for clarification
|
|
505
|
+
*/
|
|
506
|
+
protected async handleUnknown(content: string): Promise<RouteResult> {
|
|
507
|
+
return {
|
|
508
|
+
source: "butler",
|
|
509
|
+
response: `I'm not sure what you want me to do with: "${content}"\n\n**Quick guide:**\n- For questions, chat, or status → I handle directly\n- For coding tasks, bug fixes, or features → I'll use the daemon\n- Example: "implement a new feature" → Daemon\n- Example: "what can you do?" → Butler\n\nCould you clarify what you need?`,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ============================================================
|
|
514
|
+
// Utilities
|
|
515
|
+
// ============================================================
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Add message to conversation history
|
|
519
|
+
*/
|
|
520
|
+
protected addToHistory(userId: string, message: { role: string; content: string }): void {
|
|
521
|
+
if (!this.conversationHistory.has(userId)) {
|
|
522
|
+
this.conversationHistory.set(userId, []);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const history = this.conversationHistory.get(userId)!;
|
|
526
|
+
history.push(message);
|
|
527
|
+
|
|
528
|
+
if (history.length > 20) {
|
|
529
|
+
history.splice(0, history.length - 20);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Get current status
|
|
535
|
+
*/
|
|
536
|
+
getStatus(): {
|
|
537
|
+
hasActiveDaemon: boolean;
|
|
538
|
+
activeTask: string | null;
|
|
539
|
+
conversationCount: number;
|
|
540
|
+
daemonPhase: string | null;
|
|
541
|
+
} {
|
|
542
|
+
return {
|
|
543
|
+
hasActiveDaemon: this.daemon !== null,
|
|
544
|
+
activeTask: this.activeDaemonTask,
|
|
545
|
+
conversationCount: this.conversationHistory.size,
|
|
546
|
+
daemonPhase: this.daemon?.getStatus()?.phase || null,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Cleanup resources
|
|
552
|
+
*/
|
|
553
|
+
async cleanup(): Promise<void> {
|
|
554
|
+
if (this.daemon) {
|
|
555
|
+
await this.daemon.stop();
|
|
556
|
+
this.daemon = null;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
this.conversationHistory.clear();
|
|
560
|
+
this.activeDaemonTask = null;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Helper to create ChannelId for this channel
|
|
565
|
+
*/
|
|
566
|
+
protected createId(): ChannelId {
|
|
567
|
+
return createChannelId(
|
|
568
|
+
this.config.platform,
|
|
569
|
+
this.config.accountId,
|
|
570
|
+
this.config.instanceId
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
}
|