@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
package/src/index.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemons - Autonomous Agent Daemon Framework
|
|
3
|
+
*
|
|
4
|
+
* A daemon framework for autonomous AI agents.
|
|
5
|
+
* Implements SLAM pattern (State → Loop → Action → Memory).
|
|
6
|
+
* Supports hooks, tools via MCP, multi-agent coordination, and communication channels.
|
|
7
|
+
*
|
|
8
|
+
* @module @ebowwa/daemons
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Main daemon class
|
|
12
|
+
export { GLMDaemon, type GLMDaemonConfigWithWorkflow } from "./daemon.js";
|
|
13
|
+
|
|
14
|
+
// Agent class
|
|
15
|
+
export { GLMAgent } from "./agent.js";
|
|
16
|
+
|
|
17
|
+
// Supporting systems
|
|
18
|
+
export { HookSystem } from "./hooks.js";
|
|
19
|
+
export { ToolBridge } from "./tools.js";
|
|
20
|
+
export { StateManager } from "./state.js";
|
|
21
|
+
|
|
22
|
+
// Workflow system
|
|
23
|
+
export {
|
|
24
|
+
// Classes
|
|
25
|
+
BaseWorkflow,
|
|
26
|
+
WorkflowRegistry,
|
|
27
|
+
workflowRegistry,
|
|
28
|
+
// Functions
|
|
29
|
+
initializeWorkflows,
|
|
30
|
+
getWorkflow,
|
|
31
|
+
getDefaultWorkflow,
|
|
32
|
+
// Built-in workflows
|
|
33
|
+
CodingWorkflow,
|
|
34
|
+
codingWorkflow,
|
|
35
|
+
CODING_PHASES,
|
|
36
|
+
CODING_TRANSITIONS,
|
|
37
|
+
CODING_WORKFLOW_CONFIG,
|
|
38
|
+
TradingWorkflow,
|
|
39
|
+
tradingWorkflow,
|
|
40
|
+
TRADING_PHASES,
|
|
41
|
+
TRADING_TRANSITIONS,
|
|
42
|
+
TRADING_WORKFLOW_CONFIG,
|
|
43
|
+
// Types
|
|
44
|
+
type GLMWorkflowPhase,
|
|
45
|
+
type GLMWorkflowContext,
|
|
46
|
+
type GLMWorkflowPhaseResult,
|
|
47
|
+
type GLMWorkflowTransition,
|
|
48
|
+
type GLMWorkflowConfig,
|
|
49
|
+
type GLMWorkflowExecutor,
|
|
50
|
+
type CodingPhase,
|
|
51
|
+
type TradingPhase,
|
|
52
|
+
} from "./workflows/index.js";
|
|
53
|
+
|
|
54
|
+
// Built-in tools - re-exported from @ebowwa/ai/tools for convenience
|
|
55
|
+
export {
|
|
56
|
+
BUILTIN_TOOLS,
|
|
57
|
+
getBuiltinTool,
|
|
58
|
+
getBuiltinToolNames,
|
|
59
|
+
toGLMFormat,
|
|
60
|
+
executeBuiltinTool,
|
|
61
|
+
ToolExecutor,
|
|
62
|
+
type ToolDefinition,
|
|
63
|
+
type ToolCall,
|
|
64
|
+
type ToolExecutorOptions,
|
|
65
|
+
type ToolExecutionResult,
|
|
66
|
+
} from "@ebowwa/ai/tools";
|
|
67
|
+
|
|
68
|
+
// Memory systems
|
|
69
|
+
export {
|
|
70
|
+
ConversationMemory,
|
|
71
|
+
NumericConversationMemory,
|
|
72
|
+
StringConversationMemory,
|
|
73
|
+
type ConversationMessage,
|
|
74
|
+
type ConversationMemoryConfig,
|
|
75
|
+
} from "./memory.js";
|
|
76
|
+
|
|
77
|
+
// Communication channels
|
|
78
|
+
export {
|
|
79
|
+
BaseChannel,
|
|
80
|
+
TelegramChannel,
|
|
81
|
+
DiscordChannel,
|
|
82
|
+
ChannelRegistry,
|
|
83
|
+
type BaseChannelConfig,
|
|
84
|
+
type TelegramChannelConfig,
|
|
85
|
+
type DiscordChannelConfig,
|
|
86
|
+
type MessageContext,
|
|
87
|
+
type RouteResult,
|
|
88
|
+
type MessageClassification,
|
|
89
|
+
} from "./channels/index.js";
|
|
90
|
+
|
|
91
|
+
// Types
|
|
92
|
+
export * from "./types.js";
|
|
93
|
+
|
|
94
|
+
// Convenience function to create and start a daemon
|
|
95
|
+
import { GLMDaemon } from "./daemon.js";
|
|
96
|
+
import type { GLMDaemonConfig } from "./types.js";
|
|
97
|
+
|
|
98
|
+
export async function createGLMDaemon(
|
|
99
|
+
config: GLMDaemonConfig
|
|
100
|
+
): Promise<GLMDaemon> {
|
|
101
|
+
const daemon = new GLMDaemon(config);
|
|
102
|
+
return daemon;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function startGLMDaemon(
|
|
106
|
+
config: GLMDaemonConfig,
|
|
107
|
+
prompt: string
|
|
108
|
+
): Promise<string> {
|
|
109
|
+
const daemon = new GLMDaemon(config);
|
|
110
|
+
return await daemon.start(prompt);
|
|
111
|
+
}
|
package/src/memory.ts
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversation Memory - Generic conversation tracking for any channel
|
|
3
|
+
*
|
|
4
|
+
* Tracks chat history per user/conversation with persistence.
|
|
5
|
+
* Can be used by Telegram, Discord, or any other channel.
|
|
6
|
+
*
|
|
7
|
+
* Storage format: JSONL (JSON Lines)
|
|
8
|
+
* Each line is a JSON object: {"id": "...", "messages": [...], "lastUpdated": timestamp}
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
readFileSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
existsSync,
|
|
15
|
+
mkdirSync,
|
|
16
|
+
renameSync,
|
|
17
|
+
} from "fs";
|
|
18
|
+
import { dirname } from "path";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A single message in a conversation
|
|
22
|
+
*/
|
|
23
|
+
export interface ConversationMessage {
|
|
24
|
+
role: "user" | "assistant" | "system";
|
|
25
|
+
content: string;
|
|
26
|
+
timestamp?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Internal storage format for JSONL
|
|
31
|
+
*/
|
|
32
|
+
interface ConversationEntry {
|
|
33
|
+
id: string;
|
|
34
|
+
messages: ConversationMessage[];
|
|
35
|
+
lastUpdated: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Configuration for ConversationMemory
|
|
40
|
+
*/
|
|
41
|
+
export interface ConversationMemoryConfig {
|
|
42
|
+
/** Path to persistence file (default: ./conversations.jsonl) */
|
|
43
|
+
file?: string;
|
|
44
|
+
/** Maximum messages to keep per conversation (default: 10) */
|
|
45
|
+
maxMessages?: number;
|
|
46
|
+
/** Include timestamps in messages (default: true) */
|
|
47
|
+
includeTimestamps?: boolean;
|
|
48
|
+
/** Auto-migrate old .json files to .jsonl (default: true) */
|
|
49
|
+
autoMigrate?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Generic conversation memory that tracks chat history per conversation ID.
|
|
54
|
+
*
|
|
55
|
+
* Uses JSONL format for efficient append-only writes and streaming reads.
|
|
56
|
+
* Automatically migrates from old JSON format on first load.
|
|
57
|
+
*
|
|
58
|
+
* The ID type is generic (string | number) to support different channel types:
|
|
59
|
+
* - Telegram: number (chatId)
|
|
60
|
+
* - Discord: string (channelId/userId)
|
|
61
|
+
* - Slack: string (channelId)
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* const memory = new ConversationMemory({ maxMessages: 20 });
|
|
66
|
+
* memory.add(12345, 'user', 'Hello!');
|
|
67
|
+
* memory.add(12345, 'assistant', 'Hi there!');
|
|
68
|
+
* const history = memory.get(12345);
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export class ConversationMemory<IdType = string | number> {
|
|
72
|
+
private conversations = new Map<IdType, ConversationMessage[]>();
|
|
73
|
+
private maxMessages: number;
|
|
74
|
+
private includeTimestamps: boolean;
|
|
75
|
+
private file: string;
|
|
76
|
+
private needsFullSave = false;
|
|
77
|
+
|
|
78
|
+
constructor(config: ConversationMemoryConfig = {}) {
|
|
79
|
+
// Default to .jsonl extension
|
|
80
|
+
const defaultFile = "./conversations.jsonl";
|
|
81
|
+
this.file = config.file || defaultFile;
|
|
82
|
+
|
|
83
|
+
// Auto-migrate from .json to .jsonl if needed
|
|
84
|
+
if (config.autoMigrate !== false && !existsSync(this.file)) {
|
|
85
|
+
this.migrateFromJson(this.file);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this.maxMessages = config.maxMessages ?? 10;
|
|
89
|
+
this.includeTimestamps = config.includeTimestamps ?? true;
|
|
90
|
+
this.load();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Migrate from old JSON format to JSONL
|
|
95
|
+
*/
|
|
96
|
+
private migrateFromJson(jsonlFile: string): void {
|
|
97
|
+
// Check for old .json file
|
|
98
|
+
const jsonFile = jsonlFile.replace(/\.jsonl$/, ".json");
|
|
99
|
+
if (!existsSync(jsonFile)) return;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const data = JSON.parse(readFileSync(jsonFile, "utf-8"));
|
|
103
|
+
const entries: ConversationEntry[] = [];
|
|
104
|
+
|
|
105
|
+
for (const [id, messages] of Object.entries(data)) {
|
|
106
|
+
entries.push({
|
|
107
|
+
id,
|
|
108
|
+
messages: messages as ConversationMessage[],
|
|
109
|
+
lastUpdated: Date.now(),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Write as JSONL
|
|
114
|
+
const dir = dirname(jsonlFile);
|
|
115
|
+
if (!existsSync(dir)) {
|
|
116
|
+
mkdirSync(dir, { recursive: true });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const lines = entries.map((e) => JSON.stringify(e)).join("\n");
|
|
120
|
+
writeFileSync(jsonlFile, lines + (lines ? "\n" : ""));
|
|
121
|
+
|
|
122
|
+
// Backup old file instead of deleting
|
|
123
|
+
const backupFile = jsonFile + ".backup";
|
|
124
|
+
renameSync(jsonFile, backupFile);
|
|
125
|
+
|
|
126
|
+
console.log(`Migrated ${jsonFile} to ${jsonlFile} (${entries.length} conversations)`);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.error(`Migration failed: ${error}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Load conversations from JSONL file
|
|
134
|
+
* Uses synchronous read for constructor compatibility
|
|
135
|
+
*/
|
|
136
|
+
private load(): void {
|
|
137
|
+
if (!existsSync(this.file)) return;
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const content = readFileSync(this.file, "utf-8");
|
|
141
|
+
const lines = content.split("\n");
|
|
142
|
+
|
|
143
|
+
for (const line of lines) {
|
|
144
|
+
if (!line.trim()) continue;
|
|
145
|
+
try {
|
|
146
|
+
const entry: ConversationEntry = JSON.parse(line);
|
|
147
|
+
const key = this.parseKey(entry.id);
|
|
148
|
+
this.conversations.set(key, entry.messages);
|
|
149
|
+
} catch {
|
|
150
|
+
// Skip malformed lines
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
// Start fresh on error
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Parse string key back to original type
|
|
160
|
+
*/
|
|
161
|
+
private parseKey(id: string): IdType {
|
|
162
|
+
// Try to parse as number if it looks like one
|
|
163
|
+
if (/^-?\d+$/.test(id)) {
|
|
164
|
+
return Number(id) as IdType;
|
|
165
|
+
}
|
|
166
|
+
return id as IdType;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Convert IdType to string for storage
|
|
171
|
+
*/
|
|
172
|
+
private stringifyKey(id: IdType): string {
|
|
173
|
+
return String(id);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Save all conversations to disk (full rewrite)
|
|
178
|
+
* Used when entries are deleted or modified
|
|
179
|
+
*/
|
|
180
|
+
private saveFull(): void {
|
|
181
|
+
try {
|
|
182
|
+
const dir = dirname(this.file);
|
|
183
|
+
if (!existsSync(dir)) {
|
|
184
|
+
mkdirSync(dir, { recursive: true });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const lines: string[] = [];
|
|
188
|
+
for (const [id, messages] of this.conversations) {
|
|
189
|
+
const entry: ConversationEntry = {
|
|
190
|
+
id: this.stringifyKey(id),
|
|
191
|
+
messages,
|
|
192
|
+
lastUpdated: Date.now(),
|
|
193
|
+
};
|
|
194
|
+
lines.push(JSON.stringify(entry));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
writeFileSync(this.file, lines.join("\n") + (lines.length ? "\n" : ""));
|
|
198
|
+
this.needsFullSave = false;
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.error(`Save failed: ${error}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Append a single entry to the file (efficient for adds)
|
|
206
|
+
*/
|
|
207
|
+
private appendEntry(conversationId: IdType, messages: ConversationMessage[]): void {
|
|
208
|
+
try {
|
|
209
|
+
const dir = dirname(this.file);
|
|
210
|
+
if (!existsSync(dir)) {
|
|
211
|
+
mkdirSync(dir, { recursive: true });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const entry: ConversationEntry = {
|
|
215
|
+
id: this.stringifyKey(conversationId),
|
|
216
|
+
messages,
|
|
217
|
+
lastUpdated: Date.now(),
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// Check if file exists and has content to determine if we need newline
|
|
221
|
+
const hasContent = existsSync(this.file) && readFileSync(this.file, "utf-8").length > 0;
|
|
222
|
+
const prefix = hasContent ? "" : "";
|
|
223
|
+
const line = JSON.stringify(entry) + "\n";
|
|
224
|
+
|
|
225
|
+
// We need to do a full save to replace the old entry for this conversation
|
|
226
|
+
// JSONL doesn't support in-place updates, so we mark for full save
|
|
227
|
+
this.needsFullSave = true;
|
|
228
|
+
|
|
229
|
+
// For now, do a full save since we need to replace the entry
|
|
230
|
+
this.saveFull();
|
|
231
|
+
} catch (error) {
|
|
232
|
+
console.error(`Append failed: ${error}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Add a message to a conversation
|
|
238
|
+
*/
|
|
239
|
+
add(
|
|
240
|
+
conversationId: IdType,
|
|
241
|
+
role: "user" | "assistant" | "system",
|
|
242
|
+
content: string
|
|
243
|
+
): void {
|
|
244
|
+
if (!this.conversations.has(conversationId)) {
|
|
245
|
+
this.conversations.set(conversationId, []);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const history = this.conversations.get(conversationId)!;
|
|
249
|
+
const message: ConversationMessage = { role, content };
|
|
250
|
+
|
|
251
|
+
if (this.includeTimestamps) {
|
|
252
|
+
message.timestamp = Date.now();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
history.push(message);
|
|
256
|
+
|
|
257
|
+
// Keep only last N messages
|
|
258
|
+
if (history.length > this.maxMessages) {
|
|
259
|
+
history.splice(0, history.length - this.maxMessages);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Save the updated conversation
|
|
263
|
+
this.appendEntry(conversationId, history);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Get conversation history for a specific conversation
|
|
268
|
+
*/
|
|
269
|
+
get(conversationId: IdType): ConversationMessage[] {
|
|
270
|
+
return this.conversations.get(conversationId) || [];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Get all conversations
|
|
275
|
+
*/
|
|
276
|
+
getAll(): Map<IdType, ConversationMessage[]> {
|
|
277
|
+
return new Map(this.conversations);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Clear a specific conversation
|
|
282
|
+
*/
|
|
283
|
+
clear(conversationId: IdType): void {
|
|
284
|
+
this.conversations.delete(conversationId);
|
|
285
|
+
this.saveFull();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Clear all conversations
|
|
290
|
+
*/
|
|
291
|
+
clearAll(): void {
|
|
292
|
+
this.conversations.clear();
|
|
293
|
+
this.saveFull();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Check if a conversation exists
|
|
298
|
+
*/
|
|
299
|
+
has(conversationId: IdType): boolean {
|
|
300
|
+
return this.conversations.has(conversationId);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Get the number of conversations
|
|
305
|
+
*/
|
|
306
|
+
size(): number {
|
|
307
|
+
return this.conversations.size;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Get the messages count for a specific conversation
|
|
312
|
+
*/
|
|
313
|
+
messageCount(conversationId: IdType): number {
|
|
314
|
+
return this.conversations.get(conversationId)?.length || 0;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Get formatted history for API calls (OpenAI/Anthropic format)
|
|
319
|
+
*/
|
|
320
|
+
getForAPI(
|
|
321
|
+
conversationId: IdType
|
|
322
|
+
): Array<{ role: string; content: string }> {
|
|
323
|
+
return this.get(conversationId).map(({ role, content }) => ({
|
|
324
|
+
role,
|
|
325
|
+
content,
|
|
326
|
+
}));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Force a full save (useful before shutdown)
|
|
331
|
+
*/
|
|
332
|
+
flush(): void {
|
|
333
|
+
if (this.needsFullSave || this.conversations.size > 0) {
|
|
334
|
+
this.saveFull();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Get the file path being used
|
|
340
|
+
*/
|
|
341
|
+
getFilePath(): string {
|
|
342
|
+
return this.file;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Check if the storage uses JSONL format
|
|
347
|
+
*/
|
|
348
|
+
isJsonl(): boolean {
|
|
349
|
+
return this.file.endsWith(".jsonl");
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Pre-configured memory for numeric IDs (like Telegram chat IDs)
|
|
355
|
+
*/
|
|
356
|
+
export class NumericConversationMemory extends ConversationMemory<number> {
|
|
357
|
+
constructor(config: Omit<ConversationMemoryConfig, "file"> & { file?: string } = {}) {
|
|
358
|
+
super(config);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Pre-configured memory for string IDs (like Discord/Slack channel IDs)
|
|
364
|
+
*/
|
|
365
|
+
export class StringConversationMemory extends ConversationMemory<string> {
|
|
366
|
+
constructor(config: Omit<ConversationMemoryConfig, "file"> & { file?: string } = {}) {
|
|
367
|
+
super(config);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GLM Daemon - Commit Skill
|
|
3
|
+
*
|
|
4
|
+
* Commit changes to git with auto-generated message.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { exec } from "child_process";
|
|
9
|
+
import { promisify } from "util";
|
|
10
|
+
import type { Skill, SkillContext, SkillResult } from "../types.js";
|
|
11
|
+
import { skillRegistry } from "../registry.js";
|
|
12
|
+
|
|
13
|
+
const execAsync = promisify(exec);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Commit parameters
|
|
17
|
+
*/
|
|
18
|
+
const CommitParams = z.object({
|
|
19
|
+
/** Custom commit message */
|
|
20
|
+
message: z.string().optional(),
|
|
21
|
+
/** Whether to push after commit */
|
|
22
|
+
push: z.boolean().optional(),
|
|
23
|
+
/** Skip hooks */
|
|
24
|
+
noVerify: z.boolean().optional(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Commit result data
|
|
29
|
+
*/
|
|
30
|
+
interface CommitData {
|
|
31
|
+
commitHash: string;
|
|
32
|
+
message: string;
|
|
33
|
+
filesCount: number;
|
|
34
|
+
pushed: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Commit skill
|
|
39
|
+
*/
|
|
40
|
+
export const commitSkill: Skill<z.infer<typeof CommitParams>, CommitData> = {
|
|
41
|
+
id: "/commit",
|
|
42
|
+
name: "Commit",
|
|
43
|
+
description: "Commit changes to git with an auto-generated or custom message.",
|
|
44
|
+
paramsSchema: CommitParams,
|
|
45
|
+
tags: ["coding", "git"],
|
|
46
|
+
parallelizable: false,
|
|
47
|
+
|
|
48
|
+
async execute(params, context): Promise<SkillResult<CommitData>> {
|
|
49
|
+
const filesCount = context.state.filesChanged.length;
|
|
50
|
+
|
|
51
|
+
if (filesCount === 0) {
|
|
52
|
+
console.log("[/commit] No files to commit");
|
|
53
|
+
return {
|
|
54
|
+
success: true,
|
|
55
|
+
data: {
|
|
56
|
+
commitHash: "",
|
|
57
|
+
message: "No changes to commit",
|
|
58
|
+
filesCount: 0,
|
|
59
|
+
pushed: false,
|
|
60
|
+
},
|
|
61
|
+
nextSkill: "/complete",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(`[/commit] Committing ${filesCount} files`);
|
|
66
|
+
|
|
67
|
+
// Generate commit message if not provided
|
|
68
|
+
let message = params?.message;
|
|
69
|
+
if (!message) {
|
|
70
|
+
const prompt = `Generate a concise git commit message for these changes:
|
|
71
|
+
|
|
72
|
+
Task: ${context.state.prompt}
|
|
73
|
+
Files: ${context.state.filesChanged.join(", ")}
|
|
74
|
+
|
|
75
|
+
Format: <type>: <description>
|
|
76
|
+
|
|
77
|
+
Types: feat, fix, refactor, docs, test, chore
|
|
78
|
+
|
|
79
|
+
Just output the commit message, nothing else.`;
|
|
80
|
+
|
|
81
|
+
const response = await context.agent.execute(prompt);
|
|
82
|
+
message = response.split("\n")[0].trim();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log(`[/commit] Message: ${message}`);
|
|
86
|
+
|
|
87
|
+
// Get the working directory from workflow config or use current directory
|
|
88
|
+
const cwd = (context.workflowConfig?.cwd as string) || process.cwd();
|
|
89
|
+
const pushed = params?.push ?? false;
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
// Step 1: Stage all changed files
|
|
93
|
+
const filesToStage = context.state.filesChanged.length > 0
|
|
94
|
+
? context.state.filesChanged.join(" ")
|
|
95
|
+
: "."; // Stage all if no specific files tracked
|
|
96
|
+
|
|
97
|
+
console.log(`[/commit] Staging files: ${filesToStage}`);
|
|
98
|
+
await execAsync(`git add ${filesToStage}`, { cwd });
|
|
99
|
+
|
|
100
|
+
// Step 2: Commit with message
|
|
101
|
+
const noVerifyFlag = params?.noVerify ? " --no-verify" : "";
|
|
102
|
+
const escapedMessage = message.replace(/"/g, '\\"');
|
|
103
|
+
const commitCommand = `git commit -m "${escapedMessage}"${noVerifyFlag}`;
|
|
104
|
+
|
|
105
|
+
console.log(`[/commit] Executing commit...`);
|
|
106
|
+
const { stdout: commitOutput } = await execAsync(commitCommand, { cwd });
|
|
107
|
+
|
|
108
|
+
// Extract commit hash from output (format: [main abc1234] message)
|
|
109
|
+
const hashMatch = commitOutput.match(/\[[\w-]+\s+([a-f0-9]+)\]/);
|
|
110
|
+
const commitHash = hashMatch ? hashMatch[1] : "";
|
|
111
|
+
|
|
112
|
+
if (!commitHash) {
|
|
113
|
+
console.warn("[/commit] Could not extract commit hash from output");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log(`[/commit] Committed: ${commitHash}`);
|
|
117
|
+
|
|
118
|
+
// Step 3: Push if requested
|
|
119
|
+
if (pushed) {
|
|
120
|
+
console.log(`[/commit] Pushing to remote...`);
|
|
121
|
+
try {
|
|
122
|
+
await execAsync("git push", { cwd });
|
|
123
|
+
console.log(`[/commit] Pushed successfully`);
|
|
124
|
+
} catch (pushError) {
|
|
125
|
+
console.error("[/commit] Push failed:", pushError);
|
|
126
|
+
// Don't fail the whole commit if push fails
|
|
127
|
+
return {
|
|
128
|
+
success: true,
|
|
129
|
+
data: {
|
|
130
|
+
commitHash,
|
|
131
|
+
message: message || "Changes committed",
|
|
132
|
+
filesCount,
|
|
133
|
+
pushed: false,
|
|
134
|
+
},
|
|
135
|
+
contextUpdates: {
|
|
136
|
+
lastCommit: commitHash,
|
|
137
|
+
lastCommitMessage: message,
|
|
138
|
+
},
|
|
139
|
+
stateUpdates: {
|
|
140
|
+
git: {
|
|
141
|
+
...context.state.git,
|
|
142
|
+
currentCommit: commitHash,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
nextSkill: "/complete",
|
|
146
|
+
error: `Commit succeeded but push failed: ${pushError instanceof Error ? pushError.message : String(pushError)}`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
success: true,
|
|
153
|
+
data: {
|
|
154
|
+
commitHash,
|
|
155
|
+
message: message || "Changes committed",
|
|
156
|
+
filesCount,
|
|
157
|
+
pushed,
|
|
158
|
+
},
|
|
159
|
+
contextUpdates: {
|
|
160
|
+
lastCommit: commitHash,
|
|
161
|
+
lastCommitMessage: message,
|
|
162
|
+
},
|
|
163
|
+
stateUpdates: {
|
|
164
|
+
git: {
|
|
165
|
+
...context.state.git,
|
|
166
|
+
currentCommit: commitHash,
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
nextSkill: "/complete",
|
|
170
|
+
};
|
|
171
|
+
} catch (error) {
|
|
172
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
173
|
+
console.error("[/commit] Git operation failed:", errorMessage);
|
|
174
|
+
|
|
175
|
+
// Check if it's just "nothing to commit"
|
|
176
|
+
if (errorMessage.includes("nothing to commit") || errorMessage.includes("no changes added to commit")) {
|
|
177
|
+
console.log("[/commit] No changes to commit (already clean)");
|
|
178
|
+
return {
|
|
179
|
+
success: true,
|
|
180
|
+
data: {
|
|
181
|
+
commitHash: context.state.git.currentCommit || "",
|
|
182
|
+
message: "No changes to commit",
|
|
183
|
+
filesCount: 0,
|
|
184
|
+
pushed: false,
|
|
185
|
+
},
|
|
186
|
+
nextSkill: "/complete",
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
success: false,
|
|
192
|
+
error: `Git operation failed: ${errorMessage}`,
|
|
193
|
+
nextSkill: "/fix-issues",
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
skillRegistry.register(commitSkill);
|
|
200
|
+
|
|
201
|
+
export { CommitParams };
|
|
202
|
+
export type { CommitData };
|