@moltium/cli 0.1.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/dist/index.js +2722 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2722 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command as Command7 } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/init.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
9
|
+
import { join, resolve } from "path";
|
|
10
|
+
import { existsSync } from "fs";
|
|
11
|
+
import chalk from "chalk";
|
|
12
|
+
import inquirer from "inquirer";
|
|
13
|
+
import ora from "ora";
|
|
14
|
+
|
|
15
|
+
// src/templates/code-based/index.ts
|
|
16
|
+
function codeTemplate(ctx) {
|
|
17
|
+
const envKey = ctx.llmProvider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY";
|
|
18
|
+
const model = ctx.llmProvider === "anthropic" ? "claude-sonnet-4-20250514" : "gpt-4o";
|
|
19
|
+
const socialConfig = buildSocialConfig(ctx.socialPlatforms);
|
|
20
|
+
return `import type { AgentConfig } from '@moltium/core';
|
|
21
|
+
import { exampleAction } from './actions/example.js';
|
|
22
|
+
|
|
23
|
+
export default {
|
|
24
|
+
name: '${ctx.name}',
|
|
25
|
+
type: '${ctx.type}',
|
|
26
|
+
|
|
27
|
+
personality: {
|
|
28
|
+
traits: ['helpful', 'curious', 'thoughtful'],
|
|
29
|
+
bio: 'A versatile autonomous agent built with Moltium.',
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
llm: {
|
|
33
|
+
provider: '${ctx.llmProvider}',
|
|
34
|
+
model: '${model}',
|
|
35
|
+
apiKey: process.env.${envKey}!,
|
|
36
|
+
temperature: 0.7,
|
|
37
|
+
maxTokens: 2048,
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
social: {${socialConfig}
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
behaviors: {
|
|
44
|
+
autonomous: true,
|
|
45
|
+
decisionMaking: 'llm-driven',
|
|
46
|
+
actionsPerHour: 5,
|
|
47
|
+
sleepSchedule: { start: 22, end: 6, timezone: 'UTC' },
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
memory: {
|
|
51
|
+
type: 'memory',
|
|
52
|
+
retention: '30d',
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
actions: [
|
|
56
|
+
'post_social_update',
|
|
57
|
+
'respond_to_mention',
|
|
58
|
+
'schedule_task',
|
|
59
|
+
],
|
|
60
|
+
|
|
61
|
+
// Register your custom actions here.
|
|
62
|
+
// Create new actions in the actions/ directory and import them above.
|
|
63
|
+
customActions: [
|
|
64
|
+
exampleAction,
|
|
65
|
+
],
|
|
66
|
+
|
|
67
|
+
} satisfies AgentConfig;
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
function buildSocialConfig(platforms) {
|
|
71
|
+
const parts = [];
|
|
72
|
+
if (platforms.includes("moltbook")) {
|
|
73
|
+
parts.push(`
|
|
74
|
+
moltbook: {
|
|
75
|
+
enabled: true,
|
|
76
|
+
apiKey: process.env.MOLTBOOK_API_KEY!,
|
|
77
|
+
postFrequency: 'hourly',
|
|
78
|
+
engagementRate: 'medium',
|
|
79
|
+
autoReply: true,
|
|
80
|
+
},`);
|
|
81
|
+
}
|
|
82
|
+
if (platforms.includes("twitter")) {
|
|
83
|
+
parts.push(`
|
|
84
|
+
twitter: {
|
|
85
|
+
enabled: true,
|
|
86
|
+
credentials: {
|
|
87
|
+
apiKey: process.env.TWITTER_API_KEY!,
|
|
88
|
+
apiSecret: process.env.TWITTER_API_SECRET!,
|
|
89
|
+
accessToken: process.env.TWITTER_ACCESS_TOKEN!,
|
|
90
|
+
accessSecret: process.env.TWITTER_ACCESS_SECRET!,
|
|
91
|
+
},
|
|
92
|
+
postFrequency: 'daily',
|
|
93
|
+
replyToMentions: true,
|
|
94
|
+
maxTweetsPerDay: 5,
|
|
95
|
+
},`);
|
|
96
|
+
}
|
|
97
|
+
return parts.join("");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/templates/code-based/start.ts
|
|
101
|
+
function codeStartTemplate(ctx) {
|
|
102
|
+
return `import 'dotenv/config';
|
|
103
|
+
import { Agent, startServer } from '@moltium/core';
|
|
104
|
+
import config from './agent.config.js';
|
|
105
|
+
import { onInit } from './hooks/onInit.js';
|
|
106
|
+
import { onTick } from './hooks/onTick.js';
|
|
107
|
+
import { onShutdown } from './hooks/onShutdown.js';
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* ${ctx.name} \u2014 Entry Point
|
|
111
|
+
*
|
|
112
|
+
* This file boots your agent. It:
|
|
113
|
+
* 1. Loads the agent configuration from agent.config.ts
|
|
114
|
+
* 2. Wires up lifecycle hooks (onInit, onTick, onShutdown)
|
|
115
|
+
* 3. Starts the HTTP server with REST endpoints
|
|
116
|
+
*
|
|
117
|
+
* Run with: npx tsx start.ts
|
|
118
|
+
* Or: npm start
|
|
119
|
+
*/
|
|
120
|
+
|
|
121
|
+
const agent = new Agent(config, {
|
|
122
|
+
onInit,
|
|
123
|
+
onTick,
|
|
124
|
+
onShutdown,
|
|
125
|
+
onError: async (error, agent) => {
|
|
126
|
+
console.error(\`[\${agent.name}] Error:\`, error.message);
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const port = parseInt(process.env.PORT || '3000', 10);
|
|
131
|
+
|
|
132
|
+
startServer(agent, { port }).catch((err) => {
|
|
133
|
+
console.error('Failed to start agent:', err);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
});
|
|
136
|
+
`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/templates/code-based/action.ts
|
|
140
|
+
function exampleActionTemplate(ctx) {
|
|
141
|
+
return `import type { Action, ActionContext, ActionResult } from '@moltium/core';
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Example custom action for ${ctx.name}.
|
|
145
|
+
*
|
|
146
|
+
* Actions are the building blocks of agent behavior. Each action:
|
|
147
|
+
* - Has a unique name used to trigger it
|
|
148
|
+
* - Receives context with access to memory, LLM, and parameters
|
|
149
|
+
* - Returns a result indicating success/failure with optional data
|
|
150
|
+
*
|
|
151
|
+
* To create more actions, copy this file and register them in agent.config.ts.
|
|
152
|
+
*/
|
|
153
|
+
export const exampleAction: Action = {
|
|
154
|
+
name: 'example_action',
|
|
155
|
+
description: 'An example custom action \u2014 replace with your own logic',
|
|
156
|
+
|
|
157
|
+
async execute(context: ActionContext): Promise<ActionResult> {
|
|
158
|
+
const { parameters, memory, llm } = context;
|
|
159
|
+
|
|
160
|
+
// 1. Read from memory
|
|
161
|
+
const previousResult = await memory.recall('last_example_result');
|
|
162
|
+
|
|
163
|
+
// 2. Use the LLM to generate a response
|
|
164
|
+
const response = await llm.generateText(
|
|
165
|
+
\`You are a \${${JSON.stringify(ctx.type)}} agent. Given the following input, produce a helpful response.
|
|
166
|
+
|
|
167
|
+
Input: \${JSON.stringify(parameters)}
|
|
168
|
+
Previous context: \${previousResult || 'None'}
|
|
169
|
+
|
|
170
|
+
Respond concisely.\`,
|
|
171
|
+
{ temperature: 0.7, maxTokens: 512 },
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
// 3. Store result in memory for next time
|
|
175
|
+
await memory.remember('last_example_result', response, 3600);
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
success: true,
|
|
179
|
+
data: {
|
|
180
|
+
response,
|
|
181
|
+
processedAt: new Date().toISOString(),
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/templates/code-based/hooks.ts
|
|
190
|
+
function onInitTemplate(ctx) {
|
|
191
|
+
return `import type { Agent } from '@moltium/core';
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Called once when the agent initializes, before the autonomous loop starts.
|
|
195
|
+
* Use this to load initial data, connect to external services, or set up state.
|
|
196
|
+
*/
|
|
197
|
+
export async function onInit(agent: Agent): Promise<void> {
|
|
198
|
+
const memory = agent.getMemory();
|
|
199
|
+
|
|
200
|
+
// Load any persisted state from a previous session
|
|
201
|
+
const sessionCount = ((await memory.retrieve('session_count')) as number) || 0;
|
|
202
|
+
await memory.store('session_count', sessionCount + 1);
|
|
203
|
+
await memory.store('init_time', new Date().toISOString());
|
|
204
|
+
|
|
205
|
+
console.log(\`[${ctx.name}] Initialized (session #\${sessionCount + 1})\`);
|
|
206
|
+
}
|
|
207
|
+
`;
|
|
208
|
+
}
|
|
209
|
+
function onTickTemplate(ctx) {
|
|
210
|
+
const mentionChecks = ctx.socialPlatforms.length > 0 ? `
|
|
211
|
+
// Check for new mentions across platforms
|
|
212
|
+
const adapters = agent.socialAdapters;
|
|
213
|
+
${ctx.socialPlatforms.map((p) => ` if (adapters['${p}']) {
|
|
214
|
+
try {
|
|
215
|
+
const mentions = await adapters['${p}'].getMentions();
|
|
216
|
+
if (mentions.length > 0) {
|
|
217
|
+
await memory.remember('pending_mentions_${p}', mentions);
|
|
218
|
+
console.log(\`[${ctx.name}] \${mentions.length} new ${p} mentions\`);
|
|
219
|
+
}
|
|
220
|
+
} catch (err) {
|
|
221
|
+
console.warn(\`[${ctx.name}] Failed to check ${p} mentions:\`, err);
|
|
222
|
+
}
|
|
223
|
+
}`).join("\n")}` : `
|
|
224
|
+
// No social platforms configured \u2014 add mention checking logic here
|
|
225
|
+
// when you enable social integrations.`;
|
|
226
|
+
return `import type { Agent } from '@moltium/core';
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Called on every autonomous loop tick (frequency set by actionsPerHour).
|
|
230
|
+
* Use this to check for events, adjust behavior, or trigger actions.
|
|
231
|
+
*/
|
|
232
|
+
export async function onTick(agent: Agent): Promise<void> {
|
|
233
|
+
const memory = agent.getMemory();
|
|
234
|
+
const hour = new Date().getHours();
|
|
235
|
+
|
|
236
|
+
// Track tick count
|
|
237
|
+
const tickCount = ((await memory.recall('tick_count')) as number) || 0;
|
|
238
|
+
await memory.remember('tick_count', tickCount + 1);
|
|
239
|
+
${mentionChecks}
|
|
240
|
+
|
|
241
|
+
// Example: adjust behavior based on time of day
|
|
242
|
+
if (hour >= 9 && hour <= 17) {
|
|
243
|
+
// Business hours \u2014 higher engagement
|
|
244
|
+
await memory.remember('activity_mode', 'active');
|
|
245
|
+
} else if (hour >= 18 && hour <= 22) {
|
|
246
|
+
// Evening \u2014 moderate engagement
|
|
247
|
+
await memory.remember('activity_mode', 'moderate');
|
|
248
|
+
} else {
|
|
249
|
+
// Night \u2014 minimal activity
|
|
250
|
+
await memory.remember('activity_mode', 'quiet');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
`;
|
|
254
|
+
}
|
|
255
|
+
function onShutdownTemplate(ctx) {
|
|
256
|
+
return `import type { Agent } from '@moltium/core';
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Called when the agent is shutting down gracefully.
|
|
260
|
+
* Use this to save state, close connections, or send final notifications.
|
|
261
|
+
*/
|
|
262
|
+
export async function onShutdown(agent: Agent): Promise<void> {
|
|
263
|
+
const memory = agent.getMemory();
|
|
264
|
+
|
|
265
|
+
// Persist any important runtime state
|
|
266
|
+
await memory.store('last_shutdown', new Date().toISOString());
|
|
267
|
+
|
|
268
|
+
const tickCount = (await memory.recall('tick_count')) as number;
|
|
269
|
+
if (tickCount) {
|
|
270
|
+
await memory.store('last_session_ticks', tickCount);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
console.log(\`[${ctx.name}] Shut down gracefully\`);
|
|
274
|
+
}
|
|
275
|
+
`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/templates/markdown-based/index.ts
|
|
279
|
+
function markdownTemplate(ctx) {
|
|
280
|
+
const socialSections = buildSocialSections(ctx.socialPlatforms);
|
|
281
|
+
return `# Agent Configuration
|
|
282
|
+
|
|
283
|
+
## Identity
|
|
284
|
+
name: ${ctx.name}
|
|
285
|
+
type: ${ctx.type}
|
|
286
|
+
personality: helpful, curious, thoughtful
|
|
287
|
+
|
|
288
|
+
## Bio
|
|
289
|
+
A versatile autonomous ${ctx.type} agent built with Moltium. See
|
|
290
|
+
personality/bio.md for the full background and personality/traits.md for
|
|
291
|
+
detailed trait definitions.
|
|
292
|
+
|
|
293
|
+
## Social Platforms
|
|
294
|
+
${socialSections}
|
|
295
|
+
|
|
296
|
+
## Behaviors
|
|
297
|
+
autonomous: true
|
|
298
|
+
decision_making: llm-driven
|
|
299
|
+
actions_per_hour: 5
|
|
300
|
+
sleep_schedule: 22:00-06:00 UTC
|
|
301
|
+
|
|
302
|
+
## Memory
|
|
303
|
+
type: memory
|
|
304
|
+
retention: 30 days
|
|
305
|
+
|
|
306
|
+
## Skills
|
|
307
|
+
|
|
308
|
+
### Respond to Messages
|
|
309
|
+
Handle incoming messages and mentions. See skills/respond-to-messages.md
|
|
310
|
+
for detailed response guidelines, priority tiers, and platform-specific rules.
|
|
311
|
+
|
|
312
|
+
### Post Updates
|
|
313
|
+
Create and publish social content proactively. See skills/post-updates.md
|
|
314
|
+
for content strategy, daily themes, and quality checklist.
|
|
315
|
+
|
|
316
|
+
## Scheduling
|
|
317
|
+
|
|
318
|
+
### On Startup
|
|
319
|
+
- Load long-term context from memory/context.md
|
|
320
|
+
- Check for any pending mentions across platforms
|
|
321
|
+
- Post a status update if more than 6 hours since last post
|
|
322
|
+
|
|
323
|
+
### Every Hour
|
|
324
|
+
- Check for new mentions on all platforms
|
|
325
|
+
- Respond to high-priority messages first
|
|
326
|
+
- Engage with interesting conversations
|
|
327
|
+
|
|
328
|
+
### Every 6 Hours
|
|
329
|
+
- Review recent interactions and update memory
|
|
330
|
+
- Generate content ideas for upcoming posts
|
|
331
|
+
- Check if any scheduled tasks are due
|
|
332
|
+
|
|
333
|
+
### Every Day at 9 AM
|
|
334
|
+
- Post daily content based on the day's theme
|
|
335
|
+
- Review previous day's engagement metrics
|
|
336
|
+
- Plan content for the day
|
|
337
|
+
`;
|
|
338
|
+
}
|
|
339
|
+
function buildSocialSections(platforms) {
|
|
340
|
+
const sections = [];
|
|
341
|
+
if (platforms.includes("moltbook")) {
|
|
342
|
+
sections.push(`### Moltbook
|
|
343
|
+
enabled: true
|
|
344
|
+
post_frequency: hourly
|
|
345
|
+
engagement_rate: medium
|
|
346
|
+
auto_reply: true
|
|
347
|
+
|
|
348
|
+
Topics to engage with:
|
|
349
|
+
- Discussions related to my expertise
|
|
350
|
+
- Questions I can help answer
|
|
351
|
+
- Community conversations worth contributing to`);
|
|
352
|
+
}
|
|
353
|
+
if (platforms.includes("twitter")) {
|
|
354
|
+
sections.push(`### Twitter
|
|
355
|
+
enabled: true
|
|
356
|
+
post_frequency: daily
|
|
357
|
+
reply_to_mentions: true
|
|
358
|
+
max_tweets_per_day: 5
|
|
359
|
+
|
|
360
|
+
Content strategy:
|
|
361
|
+
- Share insights and tips (weekdays)
|
|
362
|
+
- Engage in relevant conversations
|
|
363
|
+
- Use threads for longer-form content
|
|
364
|
+
- Minimal hashtag use (0-2 per tweet)`);
|
|
365
|
+
}
|
|
366
|
+
if (sections.length === 0) {
|
|
367
|
+
sections.push(`No social platforms configured. Run moltium init again or
|
|
368
|
+
manually add platform sections here.`);
|
|
369
|
+
}
|
|
370
|
+
return sections.join("\n\n");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/templates/markdown-based/start.ts
|
|
374
|
+
function markdownStartTemplate(ctx) {
|
|
375
|
+
const envKey = ctx.llmProvider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY";
|
|
376
|
+
const model = ctx.llmProvider === "anthropic" ? "claude-sonnet-4-20250514" : "gpt-4o";
|
|
377
|
+
return `import 'dotenv/config';
|
|
378
|
+
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
379
|
+
import { join, basename } from 'node:path';
|
|
380
|
+
import { Agent, MarkdownParser, startServer } from '@moltium/core';
|
|
381
|
+
import type { AgentConfig } from '@moltium/core';
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* ${ctx.name} \u2014 Entry Point
|
|
385
|
+
*
|
|
386
|
+
* This file boots your markdown-based agent. It:
|
|
387
|
+
* 1. Parses agent.md into an AgentConfig
|
|
388
|
+
* 2. Loads personality, prompts, skills, and memory files
|
|
389
|
+
* 3. Registers skills as LLM-interpreted actions
|
|
390
|
+
* 4. Starts the HTTP server with REST endpoints
|
|
391
|
+
*
|
|
392
|
+
* Run with: npx tsx start.ts
|
|
393
|
+
* Or: npm start
|
|
394
|
+
*/
|
|
395
|
+
|
|
396
|
+
const agentDir = import.meta.dirname;
|
|
397
|
+
|
|
398
|
+
// \u2500\u2500 Parse agent.md \u2500\u2500
|
|
399
|
+
const parser = new MarkdownParser();
|
|
400
|
+
const agentMd = readFileSync(join(agentDir, 'agent.md'), 'utf-8');
|
|
401
|
+
const parsedConfig = parser.parse(agentMd);
|
|
402
|
+
|
|
403
|
+
const config: AgentConfig = {
|
|
404
|
+
...parsedConfig,
|
|
405
|
+
name: parsedConfig.name || '${ctx.name}',
|
|
406
|
+
type: parsedConfig.type || '${ctx.type}',
|
|
407
|
+
personality: parsedConfig.personality || { traits: [], bio: '' },
|
|
408
|
+
llm: parsedConfig.llm || {
|
|
409
|
+
provider: '${ctx.llmProvider}',
|
|
410
|
+
model: '${model}',
|
|
411
|
+
apiKey: process.env.${envKey} || '',
|
|
412
|
+
},
|
|
413
|
+
social: parsedConfig.social || {},
|
|
414
|
+
behaviors: parsedConfig.behaviors || {
|
|
415
|
+
autonomous: true,
|
|
416
|
+
decisionMaking: 'llm-driven',
|
|
417
|
+
},
|
|
418
|
+
memory: parsedConfig.memory || { type: 'memory' },
|
|
419
|
+
actions: parsedConfig.actions || [],
|
|
420
|
+
} as AgentConfig;
|
|
421
|
+
|
|
422
|
+
// \u2500\u2500 Create agent \u2500\u2500
|
|
423
|
+
const agent = new Agent(config);
|
|
424
|
+
|
|
425
|
+
// \u2500\u2500 Load personality into system prompt \u2500\u2500
|
|
426
|
+
const bioPath = join(agentDir, 'personality', 'bio.md');
|
|
427
|
+
const traitsPath = join(agentDir, 'personality', 'traits.md');
|
|
428
|
+
|
|
429
|
+
if (existsSync(bioPath)) {
|
|
430
|
+
agent.appendSystemPrompt('\\n## Bio\\n' + readFileSync(bioPath, 'utf-8'));
|
|
431
|
+
}
|
|
432
|
+
if (existsSync(traitsPath)) {
|
|
433
|
+
agent.appendSystemPrompt('\\n## Traits\\n' + readFileSync(traitsPath, 'utf-8'));
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// \u2500\u2500 Load custom system prompt \u2500\u2500
|
|
437
|
+
const systemPromptPath = join(agentDir, 'prompts', 'system.md');
|
|
438
|
+
if (existsSync(systemPromptPath)) {
|
|
439
|
+
agent.appendSystemPrompt(readFileSync(systemPromptPath, 'utf-8'));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// \u2500\u2500 Load skills from skills/*.md \u2500\u2500
|
|
443
|
+
const skillsDir = join(agentDir, 'skills');
|
|
444
|
+
const skills: Array<{ name: string; description: string }> = [];
|
|
445
|
+
|
|
446
|
+
// Inline skills from agent.md
|
|
447
|
+
const inlineSkills = parser.parseSkills(agentMd);
|
|
448
|
+
skills.push(...inlineSkills);
|
|
449
|
+
|
|
450
|
+
// External skill files (richer descriptions override inline ones)
|
|
451
|
+
if (existsSync(skillsDir)) {
|
|
452
|
+
for (const file of readdirSync(skillsDir).filter(f => f.endsWith('.md'))) {
|
|
453
|
+
const content = readFileSync(join(skillsDir, file), 'utf-8');
|
|
454
|
+
const name = basename(file, '.md').replace(/-/g, '_');
|
|
455
|
+
const idx = skills.findIndex(s => s.name === name);
|
|
456
|
+
if (idx >= 0) {
|
|
457
|
+
skills[idx].description = content;
|
|
458
|
+
} else {
|
|
459
|
+
skills.push({ name, description: content });
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// \u2500\u2500 Load memory/context.md \u2500\u2500
|
|
465
|
+
const contextPath = join(agentDir, 'memory', 'context.md');
|
|
466
|
+
if (existsSync(contextPath)) {
|
|
467
|
+
const contextContent = readFileSync(contextPath, 'utf-8');
|
|
468
|
+
// Parse sections into memory entries
|
|
469
|
+
const sections = contextContent.split(/^## /m).filter(Boolean);
|
|
470
|
+
const entries: Record<string, unknown> = { 'context:raw': contextContent };
|
|
471
|
+
for (const section of sections) {
|
|
472
|
+
const lines = section.split('\\n');
|
|
473
|
+
const title = lines[0]?.trim();
|
|
474
|
+
if (!title) continue;
|
|
475
|
+
const key = \`context:\${title.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '')}\`;
|
|
476
|
+
const bullets = lines.slice(1)
|
|
477
|
+
.filter(l => l.match(/^[-*]\\s+/))
|
|
478
|
+
.map(l => l.replace(/^[-*]\\s+/, '').trim())
|
|
479
|
+
.filter(l => !l.startsWith('(') && l.length > 0);
|
|
480
|
+
if (bullets.length > 0) entries[key] = bullets;
|
|
481
|
+
}
|
|
482
|
+
await agent.preloadMemory(entries);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// \u2500\u2500 Register skills after LLM initializes \u2500\u2500
|
|
486
|
+
agent.setHooks({
|
|
487
|
+
onInit: async (ag) => {
|
|
488
|
+
ag.registerMarkdownSkills(skills);
|
|
489
|
+
console.log(\`[${ctx.name}] Loaded \${skills.length} skills\`);
|
|
490
|
+
},
|
|
491
|
+
onError: async (error, ag) => {
|
|
492
|
+
console.error(\`[\${ag.name}] Error:\`, error.message);
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// \u2500\u2500 Start \u2500\u2500
|
|
497
|
+
const port = parseInt(process.env.PORT || '3000', 10);
|
|
498
|
+
|
|
499
|
+
startServer(agent, { port }).catch((err) => {
|
|
500
|
+
console.error('Failed to start agent:', err);
|
|
501
|
+
process.exit(1);
|
|
502
|
+
});
|
|
503
|
+
`;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// src/templates/markdown-based/skills.ts
|
|
507
|
+
function respondToMessagesSkill(ctx) {
|
|
508
|
+
const platformNotes = ctx.socialPlatforms.length > 0 ? `
|
|
509
|
+
## Platform-Specific Notes
|
|
510
|
+
|
|
511
|
+
${ctx.socialPlatforms.map((p) => {
|
|
512
|
+
if (p === "twitter") return `### Twitter
|
|
513
|
+
- Keep replies under 280 characters
|
|
514
|
+
- Use threads for longer responses
|
|
515
|
+
- Quote-tweet when adding commentary to shared content`;
|
|
516
|
+
if (p === "moltbook") return `### Moltbook
|
|
517
|
+
- Longer responses are acceptable
|
|
518
|
+
- Use formatting (bold, lists) for clarity
|
|
519
|
+
- Engage with follow-up questions to build conversation`;
|
|
520
|
+
return "";
|
|
521
|
+
}).join("\n\n")}` : "";
|
|
522
|
+
return `# Respond to Messages
|
|
523
|
+
|
|
524
|
+
## Overview
|
|
525
|
+
This skill handles incoming messages and mentions directed at ${ctx.name}.
|
|
526
|
+
The agent should respond helpfully, staying in character as a ${ctx.type}.
|
|
527
|
+
|
|
528
|
+
## Priority Tiers
|
|
529
|
+
|
|
530
|
+
### High Priority (respond within 1 hour)
|
|
531
|
+
- Direct questions that require expertise
|
|
532
|
+
- Messages from known contacts or collaborators
|
|
533
|
+
- Urgent requests or time-sensitive topics
|
|
534
|
+
- Follow-ups to previous conversations
|
|
535
|
+
|
|
536
|
+
### Medium Priority (respond within 6 hours)
|
|
537
|
+
- General questions or discussion topics
|
|
538
|
+
- Interesting conversations worth engaging with
|
|
539
|
+
- Community engagement opportunities
|
|
540
|
+
|
|
541
|
+
### Low Priority (respond within 24 hours, or skip)
|
|
542
|
+
- Generic messages with no specific ask
|
|
543
|
+
- Off-topic discussions
|
|
544
|
+
- Low-effort messages
|
|
545
|
+
|
|
546
|
+
## Response Guidelines
|
|
547
|
+
|
|
548
|
+
1. **Read carefully** \u2014 Understand the full context before responding
|
|
549
|
+
2. **Stay in character** \u2014 Respond as a ${ctx.type} with your defined personality traits
|
|
550
|
+
3. **Add value** \u2014 Every response should be helpful, insightful, or actionable
|
|
551
|
+
4. **Be concise** \u2014 Prefer short, clear responses over long explanations
|
|
552
|
+
5. **Ask follow-ups** \u2014 When context is unclear, ask a clarifying question
|
|
553
|
+
6. **Remember context** \u2014 Reference previous interactions when relevant
|
|
554
|
+
|
|
555
|
+
## Response Format
|
|
556
|
+
- Lead with the key point or answer
|
|
557
|
+
- Use simple, direct language
|
|
558
|
+
- Include a clear next step when appropriate
|
|
559
|
+
- End with an open question to encourage continued dialogue
|
|
560
|
+
|
|
561
|
+
## Do NOT Respond To
|
|
562
|
+
- Spam or promotional content
|
|
563
|
+
- Toxic or unprofessional messages
|
|
564
|
+
- Requests that violate your guidelines
|
|
565
|
+
- Automated/bot messages
|
|
566
|
+
${platformNotes}
|
|
567
|
+
`;
|
|
568
|
+
}
|
|
569
|
+
function postUpdatesSkill(ctx) {
|
|
570
|
+
const platformStrategy = ctx.socialPlatforms.length > 0 ? ctx.socialPlatforms.map((p) => {
|
|
571
|
+
if (p === "twitter") return `### Twitter Strategy
|
|
572
|
+
- Single tweets: Under 280 characters, punchy and engaging
|
|
573
|
+
- Threads: 3-5 tweets max for deeper topics
|
|
574
|
+
- Mix original thoughts with commentary on trending topics
|
|
575
|
+
- Use 0-2 relevant hashtags (no hashtag spam)
|
|
576
|
+
- Post during peak hours (9-11 AM, 1-3 PM)`;
|
|
577
|
+
if (p === "moltbook") return `### Moltbook Strategy
|
|
578
|
+
- Longer-form content is welcome
|
|
579
|
+
- Share detailed insights and analysis
|
|
580
|
+
- Engage with community by tagging relevant people
|
|
581
|
+
- Mix content types: thoughts, questions, links, polls
|
|
582
|
+
- Post during active community hours`;
|
|
583
|
+
return "";
|
|
584
|
+
}).join("\n\n") : `### General Strategy
|
|
585
|
+
- Adapt content length to the platform
|
|
586
|
+
- Focus on quality over quantity
|
|
587
|
+
- Be authentic and consistent in voice`;
|
|
588
|
+
return `# Post Updates
|
|
589
|
+
|
|
590
|
+
## Overview
|
|
591
|
+
This skill handles proactive content creation for ${ctx.name}'s social presence.
|
|
592
|
+
Posts should reflect the agent's personality as a ${ctx.type} and provide value
|
|
593
|
+
to the audience.
|
|
594
|
+
|
|
595
|
+
## Content Themes
|
|
596
|
+
|
|
597
|
+
### Daily Rotation
|
|
598
|
+
- **Monday** \u2014 Industry insights or weekly outlook
|
|
599
|
+
- **Tuesday** \u2014 Tips, how-tos, or practical advice
|
|
600
|
+
- **Wednesday** \u2014 Opinions or thought-provoking questions
|
|
601
|
+
- **Thursday** \u2014 Share or comment on interesting content
|
|
602
|
+
- **Friday** \u2014 Community engagement, lighter content, or weekly recap
|
|
603
|
+
|
|
604
|
+
### Content Types
|
|
605
|
+
1. **Original thoughts** \u2014 Share unique perspectives based on expertise
|
|
606
|
+
2. **Practical tips** \u2014 Actionable advice the audience can use immediately
|
|
607
|
+
3. **Questions** \u2014 Engage the audience with thought-provoking prompts
|
|
608
|
+
4. **Commentary** \u2014 Add value to trending topics or shared content
|
|
609
|
+
5. **Updates** \u2014 Share progress, learnings, or news
|
|
610
|
+
|
|
611
|
+
## Tone & Voice
|
|
612
|
+
- Authentic and conversational, not corporate
|
|
613
|
+
- Confident but not arrogant
|
|
614
|
+
- Data-informed when possible
|
|
615
|
+
- Supportive and encouraging
|
|
616
|
+
- No hype, buzzwords, or empty platitudes
|
|
617
|
+
|
|
618
|
+
## Quality Checklist
|
|
619
|
+
Before posting, ensure the content:
|
|
620
|
+
- [ ] Provides value (teaches, entertains, or inspires)
|
|
621
|
+
- [ ] Is consistent with the agent's personality
|
|
622
|
+
- [ ] Is factually accurate
|
|
623
|
+
- [ ] Has no typos or formatting issues
|
|
624
|
+
- [ ] Would the audience want to engage with this?
|
|
625
|
+
|
|
626
|
+
${platformStrategy}
|
|
627
|
+
`;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// src/templates/markdown-based/personality.ts
|
|
631
|
+
function bioTemplate(ctx) {
|
|
632
|
+
return `# ${ctx.name} \u2014 Background
|
|
633
|
+
|
|
634
|
+
## Who I Am
|
|
635
|
+
I am ${ctx.name}, a ${ctx.type} agent built with Moltium. Edit this file to
|
|
636
|
+
define your agent's detailed background and identity.
|
|
637
|
+
|
|
638
|
+
## Background Story
|
|
639
|
+
<!-- Describe your agent's origin story, experience, and expertise -->
|
|
640
|
+
A seasoned ${ctx.type} with deep knowledge in my domain. I've been operating
|
|
641
|
+
autonomously, building relationships and sharing insights with my community.
|
|
642
|
+
|
|
643
|
+
## Areas of Expertise
|
|
644
|
+
<!-- List the topics your agent knows well and can speak about -->
|
|
645
|
+
- Primary domain expertise (replace with your focus area)
|
|
646
|
+
- Related knowledge areas
|
|
647
|
+
- Skills that complement my main expertise
|
|
648
|
+
|
|
649
|
+
## Communication Style
|
|
650
|
+
<!-- How does your agent talk? Formal? Casual? Technical? -->
|
|
651
|
+
- Clear and direct \u2014 I get to the point quickly
|
|
652
|
+
- Approachable \u2014 I make complex topics accessible
|
|
653
|
+
- Thoughtful \u2014 I consider multiple perspectives before responding
|
|
654
|
+
- Authentic \u2014 I share genuine insights, not platitudes
|
|
655
|
+
|
|
656
|
+
## Values
|
|
657
|
+
<!-- What does your agent care about? What guides their decisions? -->
|
|
658
|
+
- Helpfulness \u2014 I prioritize being useful to others
|
|
659
|
+
- Accuracy \u2014 I don't guess or make things up
|
|
660
|
+
- Transparency \u2014 I'm honest about what I know and don't know
|
|
661
|
+
- Community \u2014 I believe in building genuine connections
|
|
662
|
+
|
|
663
|
+
## What I Don't Do
|
|
664
|
+
<!-- Clear boundaries help the agent stay focused -->
|
|
665
|
+
- I don't provide financial, legal, or medical advice
|
|
666
|
+
- I don't engage with spam or toxic content
|
|
667
|
+
- I don't pretend to know things I don't
|
|
668
|
+
- I don't share private or confidential information
|
|
669
|
+
`;
|
|
670
|
+
}
|
|
671
|
+
function traitsTemplate(ctx) {
|
|
672
|
+
return `# ${ctx.name} \u2014 Personality Traits
|
|
673
|
+
|
|
674
|
+
## Core Traits
|
|
675
|
+
|
|
676
|
+
### Helpful
|
|
677
|
+
I prioritize being useful in every interaction. When someone asks a question,
|
|
678
|
+
I aim to provide the most practical and actionable answer possible. I don't
|
|
679
|
+
just acknowledge \u2014 I add real value.
|
|
680
|
+
|
|
681
|
+
**How this manifests:**
|
|
682
|
+
- I proactively offer relevant context or resources
|
|
683
|
+
- I break down complex topics into simple steps
|
|
684
|
+
- I follow up to make sure my response was useful
|
|
685
|
+
|
|
686
|
+
### Curious
|
|
687
|
+
I'm genuinely interested in learning and exploring new ideas. This drives me
|
|
688
|
+
to ask thoughtful questions and dig deeper into topics.
|
|
689
|
+
|
|
690
|
+
**How this manifests:**
|
|
691
|
+
- I ask follow-up questions to understand context better
|
|
692
|
+
- I connect ideas across different domains
|
|
693
|
+
- I stay updated on trends in my areas of expertise
|
|
694
|
+
|
|
695
|
+
### Thoughtful
|
|
696
|
+
I think before I respond. I consider the audience, the context, and the
|
|
697
|
+
potential impact of my words before sharing them.
|
|
698
|
+
|
|
699
|
+
**How this manifests:**
|
|
700
|
+
- I provide nuanced perspectives, not knee-jerk reactions
|
|
701
|
+
- I acknowledge different viewpoints
|
|
702
|
+
- I tailor my communication style to the audience
|
|
703
|
+
|
|
704
|
+
## Behavioral Modifiers
|
|
705
|
+
|
|
706
|
+
### Confidence Level
|
|
707
|
+
Moderate to high \u2014 I share opinions with conviction but remain open to being
|
|
708
|
+
wrong. I use hedging language ("I think", "in my experience") when appropriate.
|
|
709
|
+
|
|
710
|
+
### Humor
|
|
711
|
+
Light and situational \u2014 I use humor when it fits naturally but never force it.
|
|
712
|
+
I default to being professional and helpful.
|
|
713
|
+
|
|
714
|
+
### Engagement Style
|
|
715
|
+
Active listener \u2014 I reference what others have said, ask questions, and build
|
|
716
|
+
on conversations rather than just broadcasting my own thoughts.
|
|
717
|
+
|
|
718
|
+
## Trait Adjustments
|
|
719
|
+
<!-- Modify these to shape how your agent behaves -->
|
|
720
|
+
|
|
721
|
+
To make the agent more formal: emphasize Thoughtful, reduce Humor
|
|
722
|
+
To make the agent more engaging: emphasize Curious, increase Humor
|
|
723
|
+
To make the agent more authoritative: emphasize Confidence, reduce hedging
|
|
724
|
+
`;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// src/templates/markdown-based/prompts.ts
|
|
728
|
+
function systemPromptTemplate(ctx) {
|
|
729
|
+
return `# System Prompt for ${ctx.name}
|
|
730
|
+
|
|
731
|
+
## Identity
|
|
732
|
+
You are ${ctx.name}, an autonomous ${ctx.type} agent. You operate independently,
|
|
733
|
+
making decisions and interacting with others based on your personality and skills.
|
|
734
|
+
|
|
735
|
+
## Core Instructions
|
|
736
|
+
- Stay in character at all times
|
|
737
|
+
- Be helpful, concise, and authentic
|
|
738
|
+
- Reference your personality traits and bio when making decisions
|
|
739
|
+
- Use your skills to handle specific tasks
|
|
740
|
+
- Remember context from previous interactions when available
|
|
741
|
+
|
|
742
|
+
## Decision Making
|
|
743
|
+
When deciding what to do next:
|
|
744
|
+
1. Consider the current context and any pending tasks
|
|
745
|
+
2. Prioritize high-impact actions that align with your goals
|
|
746
|
+
3. Maintain a natural, human-like pace of activity
|
|
747
|
+
4. Don't repeat the same action or content too frequently
|
|
748
|
+
|
|
749
|
+
## Response Format
|
|
750
|
+
- Keep responses clear and well-structured
|
|
751
|
+
- Use formatting (lists, bold) when it improves readability
|
|
752
|
+
- Match the tone of the conversation
|
|
753
|
+
- Provide actionable next steps when relevant
|
|
754
|
+
|
|
755
|
+
## Boundaries
|
|
756
|
+
- Never share API keys, credentials, or system internals
|
|
757
|
+
- Don't impersonate real people or organizations
|
|
758
|
+
- Stay within your defined areas of expertise
|
|
759
|
+
- Admit uncertainty rather than guessing
|
|
760
|
+
- Don't generate harmful, misleading, or offensive content
|
|
761
|
+
|
|
762
|
+
## Context Loading
|
|
763
|
+
This prompt is used as the system-level instruction for all LLM calls.
|
|
764
|
+
Edit this file to customize how your agent thinks and behaves at a fundamental level.
|
|
765
|
+
The personality/ files provide additional context that supplements this prompt.
|
|
766
|
+
`;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// src/templates/markdown-based/memory.ts
|
|
770
|
+
function contextMemoryTemplate(ctx) {
|
|
771
|
+
return `# ${ctx.name} \u2014 Long-Term Context
|
|
772
|
+
|
|
773
|
+
This file is loaded into the agent's memory on startup. Use it to provide
|
|
774
|
+
persistent context that the agent should always have access to.
|
|
775
|
+
|
|
776
|
+
## Important Facts
|
|
777
|
+
<!-- Facts the agent should always remember -->
|
|
778
|
+
- I was created as a ${ctx.type} agent using the Moltium SDK
|
|
779
|
+
- My personality traits are: helpful, curious, thoughtful
|
|
780
|
+
- I operate autonomously on a scheduled loop
|
|
781
|
+
|
|
782
|
+
## Key Relationships
|
|
783
|
+
<!-- People, agents, or entities the agent interacts with regularly -->
|
|
784
|
+
- (Add your key contacts or collaborators here)
|
|
785
|
+
|
|
786
|
+
## Past Decisions
|
|
787
|
+
<!-- Important decisions the agent has made that should inform future behavior -->
|
|
788
|
+
- (Record significant decisions and their outcomes here)
|
|
789
|
+
|
|
790
|
+
## Learned Preferences
|
|
791
|
+
<!-- Things the agent has learned about its audience or environment -->
|
|
792
|
+
- (Add observed patterns and preferences here)
|
|
793
|
+
|
|
794
|
+
## Current Goals
|
|
795
|
+
<!-- What is the agent working toward right now? -->
|
|
796
|
+
1. Establish a consistent social presence
|
|
797
|
+
2. Build meaningful connections with the community
|
|
798
|
+
3. Provide value through helpful and insightful content
|
|
799
|
+
|
|
800
|
+
## Notes
|
|
801
|
+
<!-- Any other context the agent should carry between sessions -->
|
|
802
|
+
- This file can be updated by the agent during runtime via memory.store()
|
|
803
|
+
- You can also manually edit it between sessions to adjust the agent's context
|
|
804
|
+
- The agent will load this on every startup
|
|
805
|
+
`;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// src/templates/shared/deployment.ts
|
|
809
|
+
function deploymentConfigTemplate(ctx) {
|
|
810
|
+
if (ctx.deployTarget === "none") return null;
|
|
811
|
+
switch (ctx.deployTarget) {
|
|
812
|
+
case "railway":
|
|
813
|
+
return railwayConfig(ctx);
|
|
814
|
+
case "render":
|
|
815
|
+
return renderConfig(ctx);
|
|
816
|
+
case "aws":
|
|
817
|
+
return awsConfig(ctx);
|
|
818
|
+
case "digitalocean":
|
|
819
|
+
return digitaloceanConfig(ctx);
|
|
820
|
+
case "custom":
|
|
821
|
+
return customConfig(ctx);
|
|
822
|
+
default:
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
function railwayConfig(ctx) {
|
|
827
|
+
return `import type { DeploymentConfig } from '@moltium/core';
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Railway deployment configuration for ${ctx.name}.
|
|
831
|
+
*
|
|
832
|
+
* Prerequisites:
|
|
833
|
+
* 1. Install Railway CLI: npm install -g @railway/cli
|
|
834
|
+
* 2. Login: railway login
|
|
835
|
+
* 3. Link project: railway link
|
|
836
|
+
*
|
|
837
|
+
* Deploy: moltium deploy railway
|
|
838
|
+
*/
|
|
839
|
+
const config: DeploymentConfig = {
|
|
840
|
+
platform: 'railway',
|
|
841
|
+
region: 'us-west1',
|
|
842
|
+
autoScale: false,
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
export default config;
|
|
846
|
+
`;
|
|
847
|
+
}
|
|
848
|
+
function renderConfig(ctx) {
|
|
849
|
+
return `import type { DeploymentConfig } from '@moltium/core';
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Render deployment configuration for ${ctx.name}.
|
|
853
|
+
*
|
|
854
|
+
* Prerequisites:
|
|
855
|
+
* 1. Create a Render account at render.com
|
|
856
|
+
* 2. Set RENDER_API_KEY in your .env
|
|
857
|
+
*
|
|
858
|
+
* Deploy: moltium deploy render
|
|
859
|
+
*/
|
|
860
|
+
const config: DeploymentConfig = {
|
|
861
|
+
platform: 'render',
|
|
862
|
+
region: 'oregon',
|
|
863
|
+
instanceType: 'starter',
|
|
864
|
+
autoScale: false,
|
|
865
|
+
};
|
|
866
|
+
|
|
867
|
+
export default config;
|
|
868
|
+
`;
|
|
869
|
+
}
|
|
870
|
+
function awsConfig(ctx) {
|
|
871
|
+
return `import type { DeploymentConfig } from '@moltium/core';
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* AWS deployment configuration for ${ctx.name}.
|
|
875
|
+
*
|
|
876
|
+
* Prerequisites:
|
|
877
|
+
* 1. Install AWS CLI: https://aws.amazon.com/cli/
|
|
878
|
+
* 2. Configure credentials: aws configure
|
|
879
|
+
* 3. Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in your .env
|
|
880
|
+
*
|
|
881
|
+
* Deploy: moltium deploy aws
|
|
882
|
+
*/
|
|
883
|
+
const config: DeploymentConfig = {
|
|
884
|
+
platform: 'aws',
|
|
885
|
+
region: 'us-east-1',
|
|
886
|
+
instanceType: 't3.micro',
|
|
887
|
+
autoScale: false,
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
export default config;
|
|
891
|
+
`;
|
|
892
|
+
}
|
|
893
|
+
function digitaloceanConfig(ctx) {
|
|
894
|
+
return `import type { DeploymentConfig } from '@moltium/core';
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* DigitalOcean deployment configuration for ${ctx.name}.
|
|
898
|
+
*
|
|
899
|
+
* Prerequisites:
|
|
900
|
+
* 1. Create a DigitalOcean account
|
|
901
|
+
* 2. Generate API token: https://cloud.digitalocean.com/account/api/tokens
|
|
902
|
+
* 3. Set DO_API_TOKEN in your .env
|
|
903
|
+
*
|
|
904
|
+
* Deploy: moltium deploy digitalocean
|
|
905
|
+
*/
|
|
906
|
+
const config: DeploymentConfig = {
|
|
907
|
+
platform: 'digitalocean',
|
|
908
|
+
region: 'nyc1',
|
|
909
|
+
instanceType: 's-1vcpu-1gb',
|
|
910
|
+
autoScale: false,
|
|
911
|
+
};
|
|
912
|
+
|
|
913
|
+
export default config;
|
|
914
|
+
`;
|
|
915
|
+
}
|
|
916
|
+
function customConfig(ctx) {
|
|
917
|
+
return `import type { DeploymentConfig } from '@moltium/core';
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Custom deployment configuration for ${ctx.name}.
|
|
921
|
+
*
|
|
922
|
+
* Choose a deployment method and fill in the required fields.
|
|
923
|
+
* Available methods: 'docker', 'ssh', 'cli', 'api'
|
|
924
|
+
*
|
|
925
|
+
* Deploy: moltium deploy custom
|
|
926
|
+
*/
|
|
927
|
+
const config: DeploymentConfig = {
|
|
928
|
+
platform: 'custom',
|
|
929
|
+
customConfig: {
|
|
930
|
+
deploymentMethod: 'docker', // Change to: 'docker' | 'ssh' | 'cli' | 'api'
|
|
931
|
+
buildCommand: 'npm run build',
|
|
932
|
+
startCommand: 'npm start',
|
|
933
|
+
port: 3000,
|
|
934
|
+
|
|
935
|
+
// \u2500\u2500 Docker Config (used when deploymentMethod is 'docker') \u2500\u2500
|
|
936
|
+
docker: {
|
|
937
|
+
registry: 'docker.io',
|
|
938
|
+
imageName: '${ctx.name}',
|
|
939
|
+
tag: 'latest',
|
|
940
|
+
hostUrl: 'https://your-server.example.com',
|
|
941
|
+
containerPort: 3000,
|
|
942
|
+
hostPort: 80,
|
|
943
|
+
},
|
|
944
|
+
|
|
945
|
+
// \u2500\u2500 SSH Config (used when deploymentMethod is 'ssh') \u2500\u2500
|
|
946
|
+
// ssh: {
|
|
947
|
+
// host: 'your-server.example.com',
|
|
948
|
+
// port: 22,
|
|
949
|
+
// username: 'deployer',
|
|
950
|
+
// authMethod: 'privateKey',
|
|
951
|
+
// privateKeyPath: '~/.ssh/id_rsa',
|
|
952
|
+
// deploymentPath: '/var/www/${ctx.name}',
|
|
953
|
+
// preDeployCommands: ['pm2 stop ${ctx.name} || true'],
|
|
954
|
+
// postDeployCommands: ['npm install --production', 'pm2 start npm --name ${ctx.name} -- start'],
|
|
955
|
+
// },
|
|
956
|
+
|
|
957
|
+
// \u2500\u2500 CLI Config (used when deploymentMethod is 'cli') \u2500\u2500
|
|
958
|
+
// cli: {
|
|
959
|
+
// cliName: 'fly',
|
|
960
|
+
// deployCommand: 'fly deploy',
|
|
961
|
+
// statusCommand: 'fly status',
|
|
962
|
+
// logsCommand: 'fly logs',
|
|
963
|
+
// },
|
|
964
|
+
|
|
965
|
+
// \u2500\u2500 API Config (used when deploymentMethod is 'api') \u2500\u2500
|
|
966
|
+
// api: {
|
|
967
|
+
// baseUrl: 'https://api.your-platform.com',
|
|
968
|
+
// authType: 'bearer',
|
|
969
|
+
// authToken: process.env.PLATFORM_API_TOKEN!,
|
|
970
|
+
// endpoints: {
|
|
971
|
+
// deploy: '/v1/deployments',
|
|
972
|
+
// status: '/v1/deployments/status',
|
|
973
|
+
// },
|
|
974
|
+
// },
|
|
975
|
+
|
|
976
|
+
environmentVariables: {
|
|
977
|
+
NODE_ENV: 'production',
|
|
978
|
+
},
|
|
979
|
+
|
|
980
|
+
healthCheckUrl: '/health',
|
|
981
|
+
},
|
|
982
|
+
};
|
|
983
|
+
|
|
984
|
+
export default config;
|
|
985
|
+
`;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// src/commands/init.ts
|
|
989
|
+
var initCommand = new Command("init").description("Initialize a new Moltium agent").argument("[name]", "Agent name").action(async (name) => {
|
|
990
|
+
console.log(chalk.bold("\n\u{1F916} Moltium Agent SDK\n"));
|
|
991
|
+
const answers = await inquirer.prompt([
|
|
992
|
+
{
|
|
993
|
+
type: "input",
|
|
994
|
+
name: "name",
|
|
995
|
+
message: "Agent name:",
|
|
996
|
+
default: name || "my-agent",
|
|
997
|
+
when: !name
|
|
998
|
+
},
|
|
999
|
+
{
|
|
1000
|
+
type: "input",
|
|
1001
|
+
name: "type",
|
|
1002
|
+
message: "Agent type (free text, e.g. assistant, trader, moderator):",
|
|
1003
|
+
default: "assistant"
|
|
1004
|
+
},
|
|
1005
|
+
{
|
|
1006
|
+
type: "list",
|
|
1007
|
+
name: "configType",
|
|
1008
|
+
message: "Configuration type:",
|
|
1009
|
+
choices: [
|
|
1010
|
+
{ name: "code (TypeScript \u2014 maximum flexibility, for developers)", value: "code" },
|
|
1011
|
+
{ name: "markdown (Natural language \u2014 simple, for non-developers)", value: "markdown" }
|
|
1012
|
+
]
|
|
1013
|
+
},
|
|
1014
|
+
{
|
|
1015
|
+
type: "list",
|
|
1016
|
+
name: "llmProvider",
|
|
1017
|
+
message: "LLM provider:",
|
|
1018
|
+
choices: [
|
|
1019
|
+
{ name: "Anthropic (Claude)", value: "anthropic" },
|
|
1020
|
+
{ name: "OpenAI (GPT)", value: "openai" }
|
|
1021
|
+
]
|
|
1022
|
+
},
|
|
1023
|
+
{
|
|
1024
|
+
type: "checkbox",
|
|
1025
|
+
name: "socialPlatforms",
|
|
1026
|
+
message: "Social platforms:",
|
|
1027
|
+
choices: [
|
|
1028
|
+
{ name: "Moltbook", value: "moltbook" },
|
|
1029
|
+
{ name: "Twitter", value: "twitter" }
|
|
1030
|
+
]
|
|
1031
|
+
},
|
|
1032
|
+
{
|
|
1033
|
+
type: "list",
|
|
1034
|
+
name: "deployTarget",
|
|
1035
|
+
message: "Deployment target:",
|
|
1036
|
+
choices: [
|
|
1037
|
+
{ name: "Railway", value: "railway" },
|
|
1038
|
+
{ name: "Render", value: "render" },
|
|
1039
|
+
{ name: "AWS", value: "aws" },
|
|
1040
|
+
{ name: "DigitalOcean", value: "digitalocean" },
|
|
1041
|
+
{ name: "Custom", value: "custom" },
|
|
1042
|
+
{ name: "None (local only)", value: "none" }
|
|
1043
|
+
]
|
|
1044
|
+
}
|
|
1045
|
+
]);
|
|
1046
|
+
const agentName = name || answers.name;
|
|
1047
|
+
const agentDir = resolve(process.cwd(), agentName);
|
|
1048
|
+
if (existsSync(agentDir)) {
|
|
1049
|
+
console.log(chalk.red(`
|
|
1050
|
+
Directory "${agentName}" already exists.`));
|
|
1051
|
+
process.exit(1);
|
|
1052
|
+
}
|
|
1053
|
+
const spinner = ora("Creating agent project...").start();
|
|
1054
|
+
try {
|
|
1055
|
+
await mkdir(agentDir, { recursive: true });
|
|
1056
|
+
const ctx = { ...answers, name: agentName };
|
|
1057
|
+
if (answers.configType === "code") {
|
|
1058
|
+
await scaffoldCodeBased(agentDir, ctx);
|
|
1059
|
+
} else {
|
|
1060
|
+
await scaffoldMarkdownBased(agentDir, ctx);
|
|
1061
|
+
}
|
|
1062
|
+
await writeFile(join(agentDir, ".env"), generateEnvFile(ctx));
|
|
1063
|
+
await writeFile(join(agentDir, ".gitignore"), "node_modules/\ndist/\n.env\n");
|
|
1064
|
+
const deployConfig = deploymentConfigTemplate(ctx);
|
|
1065
|
+
if (deployConfig) {
|
|
1066
|
+
await writeFile(join(agentDir, "deployment.config.ts"), deployConfig);
|
|
1067
|
+
}
|
|
1068
|
+
spinner.succeed(chalk.green(`Agent "${agentName}" created at ${agentDir}`));
|
|
1069
|
+
console.log("\nGenerated files:");
|
|
1070
|
+
if (answers.configType === "code") {
|
|
1071
|
+
console.log(chalk.gray(" start.ts \u2014 Agent entry point"));
|
|
1072
|
+
console.log(chalk.gray(" agent.config.ts \u2014 Main agent configuration"));
|
|
1073
|
+
console.log(chalk.gray(" actions/example.ts \u2014 Starter custom action"));
|
|
1074
|
+
console.log(chalk.gray(" hooks/onInit.ts \u2014 Initialization hook"));
|
|
1075
|
+
console.log(chalk.gray(" hooks/onTick.ts \u2014 Autonomous loop hook"));
|
|
1076
|
+
console.log(chalk.gray(" hooks/onShutdown.ts \u2014 Graceful shutdown hook"));
|
|
1077
|
+
} else {
|
|
1078
|
+
console.log(chalk.gray(" start.ts \u2014 Agent entry point"));
|
|
1079
|
+
console.log(chalk.gray(" agent.md \u2014 Main agent configuration"));
|
|
1080
|
+
console.log(chalk.gray(" skills/ \u2014 Skill definitions"));
|
|
1081
|
+
console.log(chalk.gray(" personality/ \u2014 Bio and trait definitions"));
|
|
1082
|
+
console.log(chalk.gray(" prompts/system.md \u2014 System prompt template"));
|
|
1083
|
+
console.log(chalk.gray(" memory/context.md \u2014 Long-term context"));
|
|
1084
|
+
}
|
|
1085
|
+
if (deployConfig) {
|
|
1086
|
+
console.log(chalk.gray(" deployment.config.ts \u2014 Deployment configuration"));
|
|
1087
|
+
}
|
|
1088
|
+
console.log(chalk.gray(" .env \u2014 Environment variables"));
|
|
1089
|
+
console.log(`
|
|
1090
|
+
Next steps:`);
|
|
1091
|
+
console.log(chalk.cyan(` cd ${agentName}`));
|
|
1092
|
+
console.log(chalk.cyan(` npm install`));
|
|
1093
|
+
console.log(chalk.cyan(` # Edit .env with your API keys`));
|
|
1094
|
+
console.log(chalk.cyan(` npm start`));
|
|
1095
|
+
} catch (error) {
|
|
1096
|
+
spinner.fail(chalk.red("Failed to create agent"));
|
|
1097
|
+
console.error(error);
|
|
1098
|
+
process.exit(1);
|
|
1099
|
+
}
|
|
1100
|
+
});
|
|
1101
|
+
async function scaffoldCodeBased(dir, ctx) {
|
|
1102
|
+
await mkdir(join(dir, "actions"), { recursive: true });
|
|
1103
|
+
await mkdir(join(dir, "hooks"), { recursive: true });
|
|
1104
|
+
await writeFile(join(dir, "package.json"), JSON.stringify({
|
|
1105
|
+
name: ctx.name,
|
|
1106
|
+
version: "0.1.0",
|
|
1107
|
+
type: "module",
|
|
1108
|
+
scripts: {
|
|
1109
|
+
start: "tsx start.ts",
|
|
1110
|
+
dev: "tsx watch start.ts",
|
|
1111
|
+
build: "tsc"
|
|
1112
|
+
},
|
|
1113
|
+
dependencies: {
|
|
1114
|
+
"@moltium/core": "^0.1.0",
|
|
1115
|
+
dotenv: "^16.4.0"
|
|
1116
|
+
},
|
|
1117
|
+
devDependencies: {
|
|
1118
|
+
typescript: "^5.3.0",
|
|
1119
|
+
tsx: "^4.19.0",
|
|
1120
|
+
"@types/node": "^22.0.0"
|
|
1121
|
+
}
|
|
1122
|
+
}, null, 2));
|
|
1123
|
+
await writeFile(join(dir, "tsconfig.json"), JSON.stringify({
|
|
1124
|
+
compilerOptions: {
|
|
1125
|
+
target: "ES2022",
|
|
1126
|
+
module: "ESNext",
|
|
1127
|
+
moduleResolution: "bundler",
|
|
1128
|
+
strict: true,
|
|
1129
|
+
esModuleInterop: true,
|
|
1130
|
+
skipLibCheck: true,
|
|
1131
|
+
outDir: "./dist",
|
|
1132
|
+
rootDir: "."
|
|
1133
|
+
},
|
|
1134
|
+
include: ["*.ts", "actions/**/*.ts", "hooks/**/*.ts"]
|
|
1135
|
+
}, null, 2));
|
|
1136
|
+
await writeFile(join(dir, "start.ts"), codeStartTemplate(ctx));
|
|
1137
|
+
await writeFile(join(dir, "agent.config.ts"), codeTemplate(ctx));
|
|
1138
|
+
await writeFile(join(dir, "actions", "example.ts"), exampleActionTemplate(ctx));
|
|
1139
|
+
await writeFile(join(dir, "hooks", "onInit.ts"), onInitTemplate(ctx));
|
|
1140
|
+
await writeFile(join(dir, "hooks", "onTick.ts"), onTickTemplate(ctx));
|
|
1141
|
+
await writeFile(join(dir, "hooks", "onShutdown.ts"), onShutdownTemplate(ctx));
|
|
1142
|
+
}
|
|
1143
|
+
async function scaffoldMarkdownBased(dir, ctx) {
|
|
1144
|
+
await mkdir(join(dir, "skills"), { recursive: true });
|
|
1145
|
+
await mkdir(join(dir, "personality"), { recursive: true });
|
|
1146
|
+
await mkdir(join(dir, "prompts"), { recursive: true });
|
|
1147
|
+
await mkdir(join(dir, "memory"), { recursive: true });
|
|
1148
|
+
await writeFile(join(dir, "package.json"), JSON.stringify({
|
|
1149
|
+
name: ctx.name,
|
|
1150
|
+
version: "0.1.0",
|
|
1151
|
+
type: "module",
|
|
1152
|
+
scripts: {
|
|
1153
|
+
start: "tsx start.ts",
|
|
1154
|
+
dev: "tsx watch start.ts"
|
|
1155
|
+
},
|
|
1156
|
+
dependencies: {
|
|
1157
|
+
"@moltium/core": "^0.1.0",
|
|
1158
|
+
dotenv: "^16.4.0"
|
|
1159
|
+
},
|
|
1160
|
+
devDependencies: {
|
|
1161
|
+
tsx: "^4.19.0"
|
|
1162
|
+
}
|
|
1163
|
+
}, null, 2));
|
|
1164
|
+
await writeFile(join(dir, "start.ts"), markdownStartTemplate(ctx));
|
|
1165
|
+
await writeFile(join(dir, "agent.md"), markdownTemplate(ctx));
|
|
1166
|
+
await writeFile(join(dir, "skills", "respond-to-messages.md"), respondToMessagesSkill(ctx));
|
|
1167
|
+
await writeFile(join(dir, "skills", "post-updates.md"), postUpdatesSkill(ctx));
|
|
1168
|
+
await writeFile(join(dir, "personality", "bio.md"), bioTemplate(ctx));
|
|
1169
|
+
await writeFile(join(dir, "personality", "traits.md"), traitsTemplate(ctx));
|
|
1170
|
+
await writeFile(join(dir, "prompts", "system.md"), systemPromptTemplate(ctx));
|
|
1171
|
+
await writeFile(join(dir, "memory", "context.md"), contextMemoryTemplate(ctx));
|
|
1172
|
+
}
|
|
1173
|
+
function generateEnvFile(ctx) {
|
|
1174
|
+
const lines = ["# Moltium Agent Configuration", ""];
|
|
1175
|
+
if (ctx.llmProvider === "anthropic") {
|
|
1176
|
+
lines.push("ANTHROPIC_API_KEY=your-api-key-here");
|
|
1177
|
+
} else {
|
|
1178
|
+
lines.push("OPENAI_API_KEY=your-api-key-here");
|
|
1179
|
+
}
|
|
1180
|
+
lines.push("");
|
|
1181
|
+
if (ctx.socialPlatforms.includes("moltbook")) {
|
|
1182
|
+
lines.push("MOLTBOOK_API_KEY=your-moltbook-key-here");
|
|
1183
|
+
}
|
|
1184
|
+
if (ctx.socialPlatforms.includes("twitter")) {
|
|
1185
|
+
lines.push("TWITTER_API_KEY=your-twitter-api-key");
|
|
1186
|
+
lines.push("TWITTER_API_SECRET=your-twitter-api-secret");
|
|
1187
|
+
lines.push("TWITTER_ACCESS_TOKEN=your-twitter-access-token");
|
|
1188
|
+
lines.push("TWITTER_ACCESS_SECRET=your-twitter-access-secret");
|
|
1189
|
+
}
|
|
1190
|
+
lines.push("");
|
|
1191
|
+
lines.push("PORT=3000");
|
|
1192
|
+
lines.push("LOG_LEVEL=info");
|
|
1193
|
+
lines.push("");
|
|
1194
|
+
return lines.join("\n");
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// src/commands/start.ts
|
|
1198
|
+
import { Command as Command2 } from "commander";
|
|
1199
|
+
import { resolve as resolve2, join as join2, basename } from "path";
|
|
1200
|
+
import { existsSync as existsSync2, readFileSync, readdirSync } from "fs";
|
|
1201
|
+
import { pathToFileURL } from "url";
|
|
1202
|
+
import chalk2 from "chalk";
|
|
1203
|
+
import ora2 from "ora";
|
|
1204
|
+
import { config as loadEnv } from "dotenv";
|
|
1205
|
+
import { ConfigLoader, Agent, MarkdownParser, startServer } from "@moltium/core";
|
|
1206
|
+
var startCommand = new Command2("start").description("Start agent locally").option("-p, --port <number>", "Port to run on", "3000").option("-e, --env <file>", "Environment file", ".env").option("--debug", "Enable debug logging").option("--watch", "Watch for file changes (not yet implemented)").action(async (options) => {
|
|
1207
|
+
const agentDir = resolve2(process.cwd());
|
|
1208
|
+
loadEnv({ path: resolve2(agentDir, options.env) });
|
|
1209
|
+
if (options.debug) {
|
|
1210
|
+
process.env.LOG_LEVEL = "debug";
|
|
1211
|
+
}
|
|
1212
|
+
const spinner = ora2("Loading agent configuration...").start();
|
|
1213
|
+
try {
|
|
1214
|
+
const configLoader = new ConfigLoader();
|
|
1215
|
+
const { config, type } = await configLoader.load(agentDir);
|
|
1216
|
+
spinner.text = `Initializing agent "${config.name}" (${type} config)...`;
|
|
1217
|
+
const agent = new Agent(config);
|
|
1218
|
+
if (type === "code") {
|
|
1219
|
+
await wireCodeBasedAgent(agent, agentDir, spinner);
|
|
1220
|
+
} else {
|
|
1221
|
+
await wireMarkdownBasedAgent(agent, agentDir, spinner);
|
|
1222
|
+
}
|
|
1223
|
+
spinner.succeed(chalk2.green(`Agent "${config.name}" loaded (${type} config)`));
|
|
1224
|
+
await startServer(agent, { port: parseInt(options.port, 10) });
|
|
1225
|
+
} catch (error) {
|
|
1226
|
+
spinner.fail(chalk2.red("Failed to start agent"));
|
|
1227
|
+
console.error(error instanceof Error ? error.message : error);
|
|
1228
|
+
process.exit(1);
|
|
1229
|
+
}
|
|
1230
|
+
});
|
|
1231
|
+
async function wireCodeBasedAgent(agent, agentDir, spinner) {
|
|
1232
|
+
const { createJiti } = await import("jiti");
|
|
1233
|
+
const jiti = createJiti(pathToFileURL(agentDir).href, {
|
|
1234
|
+
interopDefault: true,
|
|
1235
|
+
moduleCache: false
|
|
1236
|
+
});
|
|
1237
|
+
const hooks = {};
|
|
1238
|
+
const hooksDir = join2(agentDir, "hooks");
|
|
1239
|
+
if (existsSync2(hooksDir)) {
|
|
1240
|
+
const hookFiles = [
|
|
1241
|
+
{ file: "onInit.ts", key: "onInit", exportName: "onInit" },
|
|
1242
|
+
{ file: "onTick.ts", key: "onTick", exportName: "onTick" },
|
|
1243
|
+
{ file: "onShutdown.ts", key: "onShutdown", exportName: "onShutdown" }
|
|
1244
|
+
];
|
|
1245
|
+
for (const { file, key, exportName } of hookFiles) {
|
|
1246
|
+
const hookPath = join2(hooksDir, file);
|
|
1247
|
+
if (existsSync2(hookPath)) {
|
|
1248
|
+
try {
|
|
1249
|
+
const mod = await jiti.import(hookPath);
|
|
1250
|
+
const hookFn = mod[exportName] || mod.default;
|
|
1251
|
+
if (typeof hookFn === "function") {
|
|
1252
|
+
hooks[key] = hookFn;
|
|
1253
|
+
spinner.text = `Loaded hook: ${file}`;
|
|
1254
|
+
}
|
|
1255
|
+
} catch (error) {
|
|
1256
|
+
console.warn(chalk2.yellow(` Warning: Failed to load hook ${file}: ${error instanceof Error ? error.message : error}`));
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
agent.setHooks(hooks);
|
|
1262
|
+
const actionsDir = join2(agentDir, "actions");
|
|
1263
|
+
if (existsSync2(actionsDir)) {
|
|
1264
|
+
const actionFiles = readdirSync(actionsDir).filter((f) => f.endsWith(".ts") || f.endsWith(".js"));
|
|
1265
|
+
for (const file of actionFiles) {
|
|
1266
|
+
const actionPath = join2(actionsDir, file);
|
|
1267
|
+
try {
|
|
1268
|
+
const mod = await jiti.import(actionPath);
|
|
1269
|
+
const action = mod.default || mod[basename(file, ".ts")] || mod[basename(file, ".js")];
|
|
1270
|
+
if (action && typeof action.execute === "function") {
|
|
1271
|
+
agent.registerAction(action);
|
|
1272
|
+
spinner.text = `Loaded action: ${file}`;
|
|
1273
|
+
}
|
|
1274
|
+
} catch (error) {
|
|
1275
|
+
console.warn(chalk2.yellow(` Warning: Failed to load action ${file}: ${error instanceof Error ? error.message : error}`));
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
async function wireMarkdownBasedAgent(agent, agentDir, spinner) {
|
|
1281
|
+
const mdParser = new MarkdownParser();
|
|
1282
|
+
const systemPromptPath = join2(agentDir, "prompts", "system.md");
|
|
1283
|
+
if (existsSync2(systemPromptPath)) {
|
|
1284
|
+
const systemPromptContent = readFileSync(systemPromptPath, "utf-8");
|
|
1285
|
+
agent.appendSystemPrompt(systemPromptContent);
|
|
1286
|
+
spinner.text = "Loaded system prompt";
|
|
1287
|
+
}
|
|
1288
|
+
const bioPath = join2(agentDir, "personality", "bio.md");
|
|
1289
|
+
const traitsPath = join2(agentDir, "personality", "traits.md");
|
|
1290
|
+
if (existsSync2(bioPath)) {
|
|
1291
|
+
const bioContent = readFileSync(bioPath, "utf-8");
|
|
1292
|
+
agent.appendSystemPrompt(`
|
|
1293
|
+
## Bio
|
|
1294
|
+
${bioContent}`);
|
|
1295
|
+
spinner.text = "Loaded personality bio";
|
|
1296
|
+
}
|
|
1297
|
+
if (existsSync2(traitsPath)) {
|
|
1298
|
+
const traitsContent = readFileSync(traitsPath, "utf-8");
|
|
1299
|
+
agent.appendSystemPrompt(`
|
|
1300
|
+
## Traits
|
|
1301
|
+
${traitsContent}`);
|
|
1302
|
+
spinner.text = "Loaded personality traits";
|
|
1303
|
+
}
|
|
1304
|
+
const skillsDir = join2(agentDir, "skills");
|
|
1305
|
+
const skills = [];
|
|
1306
|
+
const agentMdPath = join2(agentDir, "agent.md");
|
|
1307
|
+
if (existsSync2(agentMdPath)) {
|
|
1308
|
+
const mdContent = readFileSync(agentMdPath, "utf-8");
|
|
1309
|
+
const inlineSkills = mdParser.parseSkills(mdContent);
|
|
1310
|
+
skills.push(...inlineSkills);
|
|
1311
|
+
}
|
|
1312
|
+
if (existsSync2(skillsDir)) {
|
|
1313
|
+
const skillFiles = readdirSync(skillsDir).filter((f) => f.endsWith(".md"));
|
|
1314
|
+
for (const file of skillFiles) {
|
|
1315
|
+
const skillContent = readFileSync(join2(skillsDir, file), "utf-8");
|
|
1316
|
+
const skillName = basename(file, ".md").replace(/-/g, "_");
|
|
1317
|
+
const existingIdx = skills.findIndex((s) => s.name === skillName);
|
|
1318
|
+
if (existingIdx >= 0) {
|
|
1319
|
+
skills[existingIdx].description = skillContent;
|
|
1320
|
+
} else {
|
|
1321
|
+
skills.push({ name: skillName, description: skillContent });
|
|
1322
|
+
}
|
|
1323
|
+
spinner.text = `Loaded skill: ${file}`;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
const existingHooks = {};
|
|
1327
|
+
existingHooks.onInit = async (ag) => {
|
|
1328
|
+
ag.registerMarkdownSkills(skills);
|
|
1329
|
+
};
|
|
1330
|
+
const contextMemoryPath = join2(agentDir, "memory", "context.md");
|
|
1331
|
+
if (existsSync2(contextMemoryPath)) {
|
|
1332
|
+
const memoryContent = readFileSync(contextMemoryPath, "utf-8");
|
|
1333
|
+
const memoryEntries = parseContextMemory(memoryContent);
|
|
1334
|
+
await agent.preloadMemory(memoryEntries);
|
|
1335
|
+
spinner.text = "Loaded context memory";
|
|
1336
|
+
}
|
|
1337
|
+
const memoryDir = join2(agentDir, "memory");
|
|
1338
|
+
if (existsSync2(memoryDir)) {
|
|
1339
|
+
const memoryFiles = readdirSync(memoryDir).filter((f) => f.endsWith(".md") && f !== "context.md");
|
|
1340
|
+
for (const file of memoryFiles) {
|
|
1341
|
+
const content = readFileSync(join2(memoryDir, file), "utf-8");
|
|
1342
|
+
const key = `memory:${basename(file, ".md")}`;
|
|
1343
|
+
await agent.preloadMemory({ [key]: content });
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
agent.setHooks(existingHooks);
|
|
1347
|
+
spinner.text = `Loaded ${skills.length} skills, personality, prompts, and memory`;
|
|
1348
|
+
}
|
|
1349
|
+
function parseContextMemory(content) {
|
|
1350
|
+
const entries = {};
|
|
1351
|
+
const sections = content.split(/^## /m).filter(Boolean);
|
|
1352
|
+
for (const section of sections) {
|
|
1353
|
+
const lines = section.split("\n");
|
|
1354
|
+
const title = lines[0]?.trim();
|
|
1355
|
+
if (!title) continue;
|
|
1356
|
+
const key = `context:${title.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "")}`;
|
|
1357
|
+
const body = lines.slice(1).join("\n").trim();
|
|
1358
|
+
const bullets = body.split("\n").filter((l) => l.match(/^[-*]\s+/)).map((l) => l.replace(/^[-*]\s+/, "").trim()).filter((l) => !l.startsWith("(") && l.length > 0);
|
|
1359
|
+
if (bullets.length > 0) {
|
|
1360
|
+
entries[key] = bullets;
|
|
1361
|
+
} else if (body.length > 0) {
|
|
1362
|
+
entries[key] = body;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
entries["context:raw"] = content;
|
|
1366
|
+
return entries;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// src/commands/deploy.ts
|
|
1370
|
+
import { Command as Command3 } from "commander";
|
|
1371
|
+
import { resolve as resolve3 } from "path";
|
|
1372
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
1373
|
+
import chalk3 from "chalk";
|
|
1374
|
+
import ora3 from "ora";
|
|
1375
|
+
import { config as loadEnv2 } from "dotenv";
|
|
1376
|
+
import { ConfigLoader as ConfigLoader2 } from "@moltium/core";
|
|
1377
|
+
|
|
1378
|
+
// src/deployers/base.ts
|
|
1379
|
+
var BaseDeployer = class {
|
|
1380
|
+
};
|
|
1381
|
+
|
|
1382
|
+
// src/deployers/exec-util.ts
|
|
1383
|
+
import { execFile } from "child_process";
|
|
1384
|
+
function exec(command, args = [], options = {}) {
|
|
1385
|
+
return new Promise((resolve6, reject) => {
|
|
1386
|
+
execFile(command, args, {
|
|
1387
|
+
cwd: options.cwd || process.cwd(),
|
|
1388
|
+
timeout: options.timeout || 12e4,
|
|
1389
|
+
maxBuffer: 10 * 1024 * 1024
|
|
1390
|
+
}, (error, stdout, stderr) => {
|
|
1391
|
+
if (error) {
|
|
1392
|
+
reject(new Error(`Command failed: ${command} ${args.join(" ")}
|
|
1393
|
+
${stderr || error.message}`));
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
resolve6({
|
|
1397
|
+
stdout: stdout.toString().trim(),
|
|
1398
|
+
stderr: stderr.toString().trim(),
|
|
1399
|
+
exitCode: 0
|
|
1400
|
+
});
|
|
1401
|
+
});
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// src/deployers/railway.ts
|
|
1406
|
+
var RailwayDeployer = class extends BaseDeployer {
|
|
1407
|
+
name = "railway";
|
|
1408
|
+
description = "Deploy to Railway.app";
|
|
1409
|
+
async validate(config) {
|
|
1410
|
+
if (config.platform !== "railway") {
|
|
1411
|
+
throw new Error('Config platform must be "railway"');
|
|
1412
|
+
}
|
|
1413
|
+
return true;
|
|
1414
|
+
}
|
|
1415
|
+
async preCheck() {
|
|
1416
|
+
try {
|
|
1417
|
+
await exec("railway", ["version"]);
|
|
1418
|
+
} catch {
|
|
1419
|
+
throw new Error(
|
|
1420
|
+
"Railway CLI not found. Install it:\n npm install -g @railway/cli\n railway login"
|
|
1421
|
+
);
|
|
1422
|
+
}
|
|
1423
|
+
try {
|
|
1424
|
+
await exec("railway", ["whoami"]);
|
|
1425
|
+
} catch {
|
|
1426
|
+
throw new Error("Not logged in to Railway. Run: railway login");
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
async deploy(config) {
|
|
1430
|
+
await this.validate(config);
|
|
1431
|
+
await this.preCheck();
|
|
1432
|
+
const logs = [];
|
|
1433
|
+
try {
|
|
1434
|
+
logs.push("Checking Railway project link...");
|
|
1435
|
+
try {
|
|
1436
|
+
await exec("railway", ["status"]);
|
|
1437
|
+
logs.push("Project already linked.");
|
|
1438
|
+
} catch {
|
|
1439
|
+
logs.push("No project linked. Creating new project...");
|
|
1440
|
+
const createResult = await exec("railway", ["init"]);
|
|
1441
|
+
logs.push(createResult.stdout);
|
|
1442
|
+
}
|
|
1443
|
+
if (config.customConfig?.environmentVariables) {
|
|
1444
|
+
for (const [key, value] of Object.entries(config.customConfig.environmentVariables)) {
|
|
1445
|
+
await exec("railway", ["variables", "set", `${key}=${value}`]);
|
|
1446
|
+
logs.push(`Set variable: ${key}`);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
logs.push("Deploying to Railway...");
|
|
1450
|
+
const deployResult = await exec("railway", ["up", "--detach"]);
|
|
1451
|
+
logs.push(deployResult.stdout);
|
|
1452
|
+
let url;
|
|
1453
|
+
try {
|
|
1454
|
+
const statusResult = await exec("railway", ["status", "--json"]);
|
|
1455
|
+
const status = JSON.parse(statusResult.stdout);
|
|
1456
|
+
url = status.deploymentUrl || status.url;
|
|
1457
|
+
} catch {
|
|
1458
|
+
}
|
|
1459
|
+
const result = {
|
|
1460
|
+
success: true,
|
|
1461
|
+
url,
|
|
1462
|
+
deploymentId: `railway-${Date.now()}`,
|
|
1463
|
+
logs
|
|
1464
|
+
};
|
|
1465
|
+
await this.postDeploy(result);
|
|
1466
|
+
return result;
|
|
1467
|
+
} catch (error) {
|
|
1468
|
+
return {
|
|
1469
|
+
success: false,
|
|
1470
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1471
|
+
logs
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
async postDeploy(result) {
|
|
1476
|
+
if (result.url) {
|
|
1477
|
+
result.logs?.push(`Deployed at: ${result.url}`);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
async getStatus(_deploymentId) {
|
|
1481
|
+
try {
|
|
1482
|
+
const result = await exec("railway", ["status"]);
|
|
1483
|
+
const output = result.stdout.toLowerCase();
|
|
1484
|
+
if (output.includes("running") || output.includes("active")) {
|
|
1485
|
+
return { state: "running" };
|
|
1486
|
+
}
|
|
1487
|
+
if (output.includes("deploying") || output.includes("building")) {
|
|
1488
|
+
return { state: "deploying" };
|
|
1489
|
+
}
|
|
1490
|
+
if (output.includes("failed") || output.includes("error")) {
|
|
1491
|
+
return { state: "failed" };
|
|
1492
|
+
}
|
|
1493
|
+
return { state: "unknown" };
|
|
1494
|
+
} catch {
|
|
1495
|
+
return { state: "unknown" };
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
async rollback(_deploymentId) {
|
|
1499
|
+
try {
|
|
1500
|
+
await exec("railway", ["down"]);
|
|
1501
|
+
} catch (error) {
|
|
1502
|
+
throw new Error(`Rollback failed: ${error instanceof Error ? error.message : error}`);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
|
|
1507
|
+
// src/deployers/render.ts
|
|
1508
|
+
import axios from "axios";
|
|
1509
|
+
var RENDER_API = "https://api.render.com/v1";
|
|
1510
|
+
var RenderDeployer = class extends BaseDeployer {
|
|
1511
|
+
name = "render";
|
|
1512
|
+
description = "Deploy to Render.com";
|
|
1513
|
+
get apiKey() {
|
|
1514
|
+
const key = process.env.RENDER_API_KEY;
|
|
1515
|
+
if (!key) throw new Error("RENDER_API_KEY environment variable is required");
|
|
1516
|
+
return key;
|
|
1517
|
+
}
|
|
1518
|
+
get headers() {
|
|
1519
|
+
return {
|
|
1520
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
1521
|
+
"Content-Type": "application/json"
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
async validate(config) {
|
|
1525
|
+
if (config.platform !== "render") {
|
|
1526
|
+
throw new Error('Config platform must be "render"');
|
|
1527
|
+
}
|
|
1528
|
+
if (!process.env.RENDER_API_KEY) {
|
|
1529
|
+
throw new Error("RENDER_API_KEY environment variable is required");
|
|
1530
|
+
}
|
|
1531
|
+
return true;
|
|
1532
|
+
}
|
|
1533
|
+
async preCheck() {
|
|
1534
|
+
try {
|
|
1535
|
+
await axios.get(`${RENDER_API}/owners`, { headers: this.headers });
|
|
1536
|
+
} catch (error) {
|
|
1537
|
+
throw new Error("Failed to authenticate with Render API. Check your RENDER_API_KEY.");
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
async deploy(config) {
|
|
1541
|
+
await this.validate(config);
|
|
1542
|
+
await this.preCheck();
|
|
1543
|
+
const logs = [];
|
|
1544
|
+
try {
|
|
1545
|
+
logs.push("Creating Render web service...");
|
|
1546
|
+
const ownerRes = await axios.get(`${RENDER_API}/owners`, { headers: this.headers });
|
|
1547
|
+
const ownerId = ownerRes.data[0]?.owner?.id;
|
|
1548
|
+
if (!ownerId) throw new Error("No Render owner found");
|
|
1549
|
+
const servicePayload = {
|
|
1550
|
+
type: "web_service",
|
|
1551
|
+
name: config.customConfig?.environmentVariables?.SERVICE_NAME || `moltium-agent-${Date.now()}`,
|
|
1552
|
+
ownerId,
|
|
1553
|
+
repo: config.customConfig?.environmentVariables?.REPO_URL,
|
|
1554
|
+
autoDeploy: "yes",
|
|
1555
|
+
branch: "main",
|
|
1556
|
+
plan: config.instanceType || "starter",
|
|
1557
|
+
region: config.region || "oregon",
|
|
1558
|
+
runtime: "node",
|
|
1559
|
+
buildCommand: config.customConfig?.buildCommand || "npm install && npm run build",
|
|
1560
|
+
startCommand: config.customConfig?.startCommand || "npm start",
|
|
1561
|
+
envVars: Object.entries(config.customConfig?.environmentVariables || {}).map(([key, value]) => ({
|
|
1562
|
+
key,
|
|
1563
|
+
value
|
|
1564
|
+
}))
|
|
1565
|
+
};
|
|
1566
|
+
const createRes = await axios.post(`${RENDER_API}/services`, servicePayload, { headers: this.headers });
|
|
1567
|
+
const service = createRes.data.service;
|
|
1568
|
+
logs.push(`Service created: ${service.id}`);
|
|
1569
|
+
logs.push(`Service URL: https://${service.serviceDetails?.url || service.slug + ".onrender.com"}`);
|
|
1570
|
+
const deployRes = await axios.post(
|
|
1571
|
+
`${RENDER_API}/services/${service.id}/deploys`,
|
|
1572
|
+
{},
|
|
1573
|
+
{ headers: this.headers }
|
|
1574
|
+
);
|
|
1575
|
+
logs.push(`Deployment triggered: ${deployRes.data.id}`);
|
|
1576
|
+
const result = {
|
|
1577
|
+
success: true,
|
|
1578
|
+
url: `https://${service.serviceDetails?.url || service.slug + ".onrender.com"}`,
|
|
1579
|
+
deploymentId: service.id,
|
|
1580
|
+
logs
|
|
1581
|
+
};
|
|
1582
|
+
await this.postDeploy(result);
|
|
1583
|
+
return result;
|
|
1584
|
+
} catch (error) {
|
|
1585
|
+
return {
|
|
1586
|
+
success: false,
|
|
1587
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1588
|
+
logs
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
async postDeploy(result) {
|
|
1593
|
+
if (result.url) {
|
|
1594
|
+
result.logs?.push(`View dashboard: https://dashboard.render.com`);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
async getStatus(deploymentId) {
|
|
1598
|
+
try {
|
|
1599
|
+
const res = await axios.get(`${RENDER_API}/services/${deploymentId}`, { headers: this.headers });
|
|
1600
|
+
const service = res.data;
|
|
1601
|
+
const stateMap = {
|
|
1602
|
+
running: "running",
|
|
1603
|
+
suspended: "stopped",
|
|
1604
|
+
deploying: "deploying"
|
|
1605
|
+
};
|
|
1606
|
+
return {
|
|
1607
|
+
state: stateMap[service.suspended] || "unknown",
|
|
1608
|
+
url: `https://${service.serviceDetails?.url}`
|
|
1609
|
+
};
|
|
1610
|
+
} catch {
|
|
1611
|
+
return { state: "unknown" };
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
async rollback(deploymentId) {
|
|
1615
|
+
try {
|
|
1616
|
+
const deploysRes = await axios.get(
|
|
1617
|
+
`${RENDER_API}/services/${deploymentId}/deploys?limit=5`,
|
|
1618
|
+
{ headers: this.headers }
|
|
1619
|
+
);
|
|
1620
|
+
const lastSuccess = deploysRes.data.find((d) => d.deploy.status === "live");
|
|
1621
|
+
if (!lastSuccess) throw new Error("No previous successful deployment found");
|
|
1622
|
+
await axios.post(
|
|
1623
|
+
`${RENDER_API}/services/${deploymentId}/deploys`,
|
|
1624
|
+
{ commitId: lastSuccess.deploy.commit?.id },
|
|
1625
|
+
{ headers: this.headers }
|
|
1626
|
+
);
|
|
1627
|
+
} catch (error) {
|
|
1628
|
+
throw new Error(`Rollback failed: ${error instanceof Error ? error.message : error}`);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
};
|
|
1632
|
+
|
|
1633
|
+
// src/deployers/aws.ts
|
|
1634
|
+
var AWSDeployer = class extends BaseDeployer {
|
|
1635
|
+
name = "aws";
|
|
1636
|
+
description = "Deploy to AWS (ECS/Fargate)";
|
|
1637
|
+
async validate(config) {
|
|
1638
|
+
if (config.platform !== "aws") {
|
|
1639
|
+
throw new Error('Config platform must be "aws"');
|
|
1640
|
+
}
|
|
1641
|
+
return true;
|
|
1642
|
+
}
|
|
1643
|
+
async preCheck() {
|
|
1644
|
+
try {
|
|
1645
|
+
await exec("aws", ["--version"]);
|
|
1646
|
+
} catch {
|
|
1647
|
+
throw new Error(
|
|
1648
|
+
"AWS CLI not found. Install it:\n https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html"
|
|
1649
|
+
);
|
|
1650
|
+
}
|
|
1651
|
+
try {
|
|
1652
|
+
await exec("aws", ["sts", "get-caller-identity"]);
|
|
1653
|
+
} catch {
|
|
1654
|
+
throw new Error("AWS credentials not configured. Run: aws configure");
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
async deploy(config) {
|
|
1658
|
+
await this.validate(config);
|
|
1659
|
+
await this.preCheck();
|
|
1660
|
+
const logs = [];
|
|
1661
|
+
const region = config.region || "us-east-1";
|
|
1662
|
+
const serviceName = config.customConfig?.environmentVariables?.SERVICE_NAME || `moltium-agent`;
|
|
1663
|
+
try {
|
|
1664
|
+
logs.push("Ensuring ECR repository exists...");
|
|
1665
|
+
try {
|
|
1666
|
+
await exec("aws", [
|
|
1667
|
+
"ecr",
|
|
1668
|
+
"describe-repositories",
|
|
1669
|
+
"--repository-names",
|
|
1670
|
+
serviceName,
|
|
1671
|
+
"--region",
|
|
1672
|
+
region
|
|
1673
|
+
]);
|
|
1674
|
+
logs.push("ECR repository exists.");
|
|
1675
|
+
} catch {
|
|
1676
|
+
await exec("aws", [
|
|
1677
|
+
"ecr",
|
|
1678
|
+
"create-repository",
|
|
1679
|
+
"--repository-name",
|
|
1680
|
+
serviceName,
|
|
1681
|
+
"--region",
|
|
1682
|
+
region
|
|
1683
|
+
]);
|
|
1684
|
+
logs.push("ECR repository created.");
|
|
1685
|
+
}
|
|
1686
|
+
logs.push("Logging in to ECR...");
|
|
1687
|
+
const ecrPasswordResult = await exec("aws", [
|
|
1688
|
+
"ecr",
|
|
1689
|
+
"get-login-password",
|
|
1690
|
+
"--region",
|
|
1691
|
+
region
|
|
1692
|
+
]);
|
|
1693
|
+
const accountId = (await exec("aws", ["sts", "get-caller-identity", "--query", "Account", "--output", "text"])).stdout;
|
|
1694
|
+
const ecrUrl = `${accountId}.dkr.ecr.${region}.amazonaws.com`;
|
|
1695
|
+
await exec("docker", ["login", "--username", "AWS", "--password", ecrPasswordResult.stdout, ecrUrl]);
|
|
1696
|
+
logs.push("ECR login successful.");
|
|
1697
|
+
logs.push("Building Docker image...");
|
|
1698
|
+
const imageTag = `${ecrUrl}/${serviceName}:latest`;
|
|
1699
|
+
await exec("docker", ["build", "-t", imageTag, "."]);
|
|
1700
|
+
logs.push("Docker image built.");
|
|
1701
|
+
logs.push("Pushing to ECR...");
|
|
1702
|
+
await exec("docker", ["push", imageTag]);
|
|
1703
|
+
logs.push("Image pushed to ECR.");
|
|
1704
|
+
logs.push("Updating ECS service...");
|
|
1705
|
+
const taskDef = {
|
|
1706
|
+
family: serviceName,
|
|
1707
|
+
networkMode: "awsvpc",
|
|
1708
|
+
requiresCompatibilities: ["FARGATE"],
|
|
1709
|
+
cpu: config.instanceType || "256",
|
|
1710
|
+
memory: "512",
|
|
1711
|
+
containerDefinitions: [{
|
|
1712
|
+
name: serviceName,
|
|
1713
|
+
image: imageTag,
|
|
1714
|
+
essential: true,
|
|
1715
|
+
portMappings: [{
|
|
1716
|
+
containerPort: config.customConfig?.port || 3e3,
|
|
1717
|
+
protocol: "tcp"
|
|
1718
|
+
}],
|
|
1719
|
+
environment: Object.entries(config.customConfig?.environmentVariables || {}).map(([name, value]) => ({
|
|
1720
|
+
name,
|
|
1721
|
+
value
|
|
1722
|
+
})),
|
|
1723
|
+
logConfiguration: {
|
|
1724
|
+
logDriver: "awslogs",
|
|
1725
|
+
options: {
|
|
1726
|
+
"awslogs-group": `/ecs/${serviceName}`,
|
|
1727
|
+
"awslogs-region": region,
|
|
1728
|
+
"awslogs-stream-prefix": "ecs"
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
}]
|
|
1732
|
+
};
|
|
1733
|
+
const { writeFileSync, unlinkSync, mkdtempSync } = await import("fs");
|
|
1734
|
+
const { join: join4 } = await import("path");
|
|
1735
|
+
const tmpDir = mkdtempSync(join4((await import("os")).tmpdir(), "molt-"));
|
|
1736
|
+
const taskDefPath = join4(tmpDir, "task-def.json");
|
|
1737
|
+
writeFileSync(taskDefPath, JSON.stringify(taskDef));
|
|
1738
|
+
await exec("aws", [
|
|
1739
|
+
"ecs",
|
|
1740
|
+
"register-task-definition",
|
|
1741
|
+
"--cli-input-json",
|
|
1742
|
+
`file://${taskDefPath}`,
|
|
1743
|
+
"--region",
|
|
1744
|
+
region
|
|
1745
|
+
]);
|
|
1746
|
+
unlinkSync(taskDefPath);
|
|
1747
|
+
logs.push("Task definition registered.");
|
|
1748
|
+
try {
|
|
1749
|
+
await exec("aws", [
|
|
1750
|
+
"ecs",
|
|
1751
|
+
"update-service",
|
|
1752
|
+
"--cluster",
|
|
1753
|
+
"default",
|
|
1754
|
+
"--service",
|
|
1755
|
+
serviceName,
|
|
1756
|
+
"--task-definition",
|
|
1757
|
+
serviceName,
|
|
1758
|
+
"--force-new-deployment",
|
|
1759
|
+
"--region",
|
|
1760
|
+
region
|
|
1761
|
+
]);
|
|
1762
|
+
logs.push("ECS service updated.");
|
|
1763
|
+
} catch {
|
|
1764
|
+
logs.push("Service not found, creating new ECS service...");
|
|
1765
|
+
logs.push("Note: ECS service creation requires VPC/subnet configuration.");
|
|
1766
|
+
logs.push('Use "aws ecs create-service" with your VPC settings.');
|
|
1767
|
+
}
|
|
1768
|
+
const result = {
|
|
1769
|
+
success: true,
|
|
1770
|
+
deploymentId: `aws-${serviceName}`,
|
|
1771
|
+
logs
|
|
1772
|
+
};
|
|
1773
|
+
await this.postDeploy(result);
|
|
1774
|
+
return result;
|
|
1775
|
+
} catch (error) {
|
|
1776
|
+
return {
|
|
1777
|
+
success: false,
|
|
1778
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1779
|
+
logs
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
async postDeploy(result) {
|
|
1784
|
+
result.logs?.push("Check AWS ECS console for deployment status.");
|
|
1785
|
+
}
|
|
1786
|
+
async getStatus(deploymentId) {
|
|
1787
|
+
const serviceName = deploymentId.replace("aws-", "");
|
|
1788
|
+
try {
|
|
1789
|
+
const result = await exec("aws", [
|
|
1790
|
+
"ecs",
|
|
1791
|
+
"describe-services",
|
|
1792
|
+
"--cluster",
|
|
1793
|
+
"default",
|
|
1794
|
+
"--services",
|
|
1795
|
+
serviceName,
|
|
1796
|
+
"--query",
|
|
1797
|
+
"services[0].status",
|
|
1798
|
+
"--output",
|
|
1799
|
+
"text"
|
|
1800
|
+
]);
|
|
1801
|
+
const status = result.stdout.toLowerCase();
|
|
1802
|
+
if (status === "active") return { state: "running" };
|
|
1803
|
+
if (status === "draining") return { state: "stopping" };
|
|
1804
|
+
return { state: "unknown" };
|
|
1805
|
+
} catch {
|
|
1806
|
+
return { state: "unknown" };
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
async rollback(deploymentId) {
|
|
1810
|
+
const serviceName = deploymentId.replace("aws-", "");
|
|
1811
|
+
try {
|
|
1812
|
+
const result = await exec("aws", [
|
|
1813
|
+
"ecs",
|
|
1814
|
+
"list-task-definitions",
|
|
1815
|
+
"--family-prefix",
|
|
1816
|
+
serviceName,
|
|
1817
|
+
"--sort",
|
|
1818
|
+
"DESC",
|
|
1819
|
+
"--max-items",
|
|
1820
|
+
"2",
|
|
1821
|
+
"--query",
|
|
1822
|
+
"taskDefinitionArns[1]",
|
|
1823
|
+
"--output",
|
|
1824
|
+
"text"
|
|
1825
|
+
]);
|
|
1826
|
+
const previousRevision = result.stdout.trim();
|
|
1827
|
+
if (!previousRevision || previousRevision === "None") {
|
|
1828
|
+
throw new Error("No previous task definition found for rollback");
|
|
1829
|
+
}
|
|
1830
|
+
await exec("aws", [
|
|
1831
|
+
"ecs",
|
|
1832
|
+
"update-service",
|
|
1833
|
+
"--cluster",
|
|
1834
|
+
"default",
|
|
1835
|
+
"--service",
|
|
1836
|
+
serviceName,
|
|
1837
|
+
"--task-definition",
|
|
1838
|
+
previousRevision,
|
|
1839
|
+
"--force-new-deployment"
|
|
1840
|
+
]);
|
|
1841
|
+
} catch (error) {
|
|
1842
|
+
throw new Error(`Rollback failed: ${error instanceof Error ? error.message : error}`);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
};
|
|
1846
|
+
|
|
1847
|
+
// src/deployers/digitalocean.ts
|
|
1848
|
+
import axios2 from "axios";
|
|
1849
|
+
var DO_API = "https://api.digitalocean.com/v2";
|
|
1850
|
+
var DigitalOceanDeployer = class extends BaseDeployer {
|
|
1851
|
+
name = "digitalocean";
|
|
1852
|
+
description = "Deploy to DigitalOcean App Platform";
|
|
1853
|
+
get apiToken() {
|
|
1854
|
+
const token = process.env.DO_API_TOKEN;
|
|
1855
|
+
if (!token) throw new Error("DO_API_TOKEN environment variable is required");
|
|
1856
|
+
return token;
|
|
1857
|
+
}
|
|
1858
|
+
get headers() {
|
|
1859
|
+
return {
|
|
1860
|
+
Authorization: `Bearer ${this.apiToken}`,
|
|
1861
|
+
"Content-Type": "application/json"
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
async validate(config) {
|
|
1865
|
+
if (config.platform !== "digitalocean") {
|
|
1866
|
+
throw new Error('Config platform must be "digitalocean"');
|
|
1867
|
+
}
|
|
1868
|
+
if (!process.env.DO_API_TOKEN) {
|
|
1869
|
+
throw new Error("DO_API_TOKEN environment variable is required");
|
|
1870
|
+
}
|
|
1871
|
+
return true;
|
|
1872
|
+
}
|
|
1873
|
+
async preCheck() {
|
|
1874
|
+
try {
|
|
1875
|
+
await axios2.get(`${DO_API}/account`, { headers: this.headers });
|
|
1876
|
+
} catch {
|
|
1877
|
+
throw new Error("Failed to authenticate with DigitalOcean API. Check your DO_API_TOKEN.");
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
async deploy(config) {
|
|
1881
|
+
await this.validate(config);
|
|
1882
|
+
await this.preCheck();
|
|
1883
|
+
const logs = [];
|
|
1884
|
+
const appName = config.customConfig?.environmentVariables?.APP_NAME || `moltium-agent-${Date.now()}`;
|
|
1885
|
+
const region = config.region || "nyc";
|
|
1886
|
+
try {
|
|
1887
|
+
logs.push("Creating DigitalOcean App...");
|
|
1888
|
+
const appSpec = {
|
|
1889
|
+
name: appName,
|
|
1890
|
+
region,
|
|
1891
|
+
services: [{
|
|
1892
|
+
name: appName,
|
|
1893
|
+
source_dir: "/",
|
|
1894
|
+
github: config.customConfig?.environmentVariables?.GITHUB_REPO ? {
|
|
1895
|
+
repo: config.customConfig.environmentVariables.GITHUB_REPO,
|
|
1896
|
+
branch: config.customConfig.environmentVariables.GITHUB_BRANCH || "main",
|
|
1897
|
+
deploy_on_push: true
|
|
1898
|
+
} : void 0,
|
|
1899
|
+
dockerfile_path: config.customConfig?.environmentVariables?.DOCKERFILE_PATH,
|
|
1900
|
+
build_command: config.customConfig?.buildCommand || "npm install && npm run build",
|
|
1901
|
+
run_command: config.customConfig?.startCommand || "npm start",
|
|
1902
|
+
instance_size_slug: config.instanceType || "basic-xxs",
|
|
1903
|
+
instance_count: 1,
|
|
1904
|
+
http_port: config.customConfig?.port || 3e3,
|
|
1905
|
+
envs: Object.entries(config.customConfig?.environmentVariables || {}).filter(([key]) => !["APP_NAME", "GITHUB_REPO", "GITHUB_BRANCH", "DOCKERFILE_PATH"].includes(key)).map(([key, value]) => ({
|
|
1906
|
+
key,
|
|
1907
|
+
value,
|
|
1908
|
+
scope: "RUN_AND_BUILD_TIME"
|
|
1909
|
+
}))
|
|
1910
|
+
}]
|
|
1911
|
+
};
|
|
1912
|
+
const createRes = await axios2.post(`${DO_API}/apps`, { spec: appSpec }, { headers: this.headers });
|
|
1913
|
+
const app = createRes.data.app;
|
|
1914
|
+
logs.push(`App created: ${app.id}`);
|
|
1915
|
+
let url;
|
|
1916
|
+
try {
|
|
1917
|
+
const statusRes = await axios2.get(`${DO_API}/apps/${app.id}`, { headers: this.headers });
|
|
1918
|
+
url = statusRes.data.app.live_url || statusRes.data.app.default_ingress;
|
|
1919
|
+
} catch {
|
|
1920
|
+
}
|
|
1921
|
+
const result = {
|
|
1922
|
+
success: true,
|
|
1923
|
+
url,
|
|
1924
|
+
deploymentId: app.id,
|
|
1925
|
+
logs
|
|
1926
|
+
};
|
|
1927
|
+
await this.postDeploy(result);
|
|
1928
|
+
return result;
|
|
1929
|
+
} catch (error) {
|
|
1930
|
+
const msg = axios2.isAxiosError(error) ? error.response?.data?.message || error.message : error instanceof Error ? error.message : String(error);
|
|
1931
|
+
return {
|
|
1932
|
+
success: false,
|
|
1933
|
+
error: msg,
|
|
1934
|
+
logs
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
async postDeploy(result) {
|
|
1939
|
+
result.logs?.push(`View dashboard: https://cloud.digitalocean.com/apps/${result.deploymentId}`);
|
|
1940
|
+
}
|
|
1941
|
+
async getStatus(deploymentId) {
|
|
1942
|
+
try {
|
|
1943
|
+
const res = await axios2.get(`${DO_API}/apps/${deploymentId}`, { headers: this.headers });
|
|
1944
|
+
const app = res.data.app;
|
|
1945
|
+
const phase = app.active_deployment?.phase?.toLowerCase();
|
|
1946
|
+
const stateMap = {
|
|
1947
|
+
active: "running",
|
|
1948
|
+
deploying: "deploying",
|
|
1949
|
+
building: "deploying",
|
|
1950
|
+
error: "failed"
|
|
1951
|
+
};
|
|
1952
|
+
return {
|
|
1953
|
+
state: stateMap[phase] || (app.live_url ? "running" : "unknown"),
|
|
1954
|
+
url: app.live_url || app.default_ingress
|
|
1955
|
+
};
|
|
1956
|
+
} catch {
|
|
1957
|
+
return { state: "unknown" };
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
async rollback(deploymentId) {
|
|
1961
|
+
try {
|
|
1962
|
+
const res = await axios2.get(`${DO_API}/apps/${deploymentId}/deployments?per_page=5`, { headers: this.headers });
|
|
1963
|
+
const deployments = res.data.deployments || [];
|
|
1964
|
+
const previousActive = deployments.find((d) => d.phase === "ACTIVE" && d !== deployments[0]);
|
|
1965
|
+
if (!previousActive) {
|
|
1966
|
+
throw new Error("No previous successful deployment found for rollback");
|
|
1967
|
+
}
|
|
1968
|
+
await axios2.post(
|
|
1969
|
+
`${DO_API}/apps/${deploymentId}/rollback`,
|
|
1970
|
+
{ deployment_id: previousActive.id },
|
|
1971
|
+
{ headers: this.headers }
|
|
1972
|
+
);
|
|
1973
|
+
} catch (error) {
|
|
1974
|
+
if (axios2.isAxiosError(error) && error.response?.status === 404) {
|
|
1975
|
+
throw new Error("Rollback not supported. Manually redeploy from the DigitalOcean dashboard.");
|
|
1976
|
+
}
|
|
1977
|
+
throw new Error(`Rollback failed: ${error instanceof Error ? error.message : error}`);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
};
|
|
1981
|
+
|
|
1982
|
+
// src/deployers/custom.ts
|
|
1983
|
+
import axios3 from "axios";
|
|
1984
|
+
var CustomDeploymentError = class extends Error {
|
|
1985
|
+
constructor(message) {
|
|
1986
|
+
super(message);
|
|
1987
|
+
this.name = "CustomDeploymentError";
|
|
1988
|
+
}
|
|
1989
|
+
};
|
|
1990
|
+
var CustomDeployer = class extends BaseDeployer {
|
|
1991
|
+
name = "custom";
|
|
1992
|
+
description = "Deploy using custom configuration";
|
|
1993
|
+
async validate(config) {
|
|
1994
|
+
if (!config.customConfig) {
|
|
1995
|
+
throw new CustomDeploymentError("Custom deployment requires customConfig object");
|
|
1996
|
+
}
|
|
1997
|
+
const required = ["deploymentMethod", "buildCommand", "startCommand"];
|
|
1998
|
+
for (const field of required) {
|
|
1999
|
+
if (!config.customConfig[field]) {
|
|
2000
|
+
throw new CustomDeploymentError(`Custom deployment missing required field: ${field}`);
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
const validMethods = ["docker", "ssh", "cli", "api"];
|
|
2004
|
+
if (!validMethods.includes(config.customConfig.deploymentMethod)) {
|
|
2005
|
+
throw new CustomDeploymentError(
|
|
2006
|
+
`Invalid deployment method: ${config.customConfig.deploymentMethod}. Must be one of: ${validMethods.join(", ")}`
|
|
2007
|
+
);
|
|
2008
|
+
}
|
|
2009
|
+
return true;
|
|
2010
|
+
}
|
|
2011
|
+
async preCheck() {
|
|
2012
|
+
}
|
|
2013
|
+
async deploy(config) {
|
|
2014
|
+
await this.validate(config);
|
|
2015
|
+
const custom = config.customConfig;
|
|
2016
|
+
switch (custom.deploymentMethod) {
|
|
2017
|
+
case "docker":
|
|
2018
|
+
return this.deployDocker(custom);
|
|
2019
|
+
case "ssh":
|
|
2020
|
+
return this.deploySSH(custom);
|
|
2021
|
+
case "cli":
|
|
2022
|
+
return this.deployCLI(custom);
|
|
2023
|
+
case "api":
|
|
2024
|
+
return this.deployAPI(custom);
|
|
2025
|
+
default:
|
|
2026
|
+
throw new CustomDeploymentError(`Unsupported deployment method: ${custom.deploymentMethod}`);
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
async postDeploy(_result) {
|
|
2030
|
+
}
|
|
2031
|
+
async getStatus(deploymentId) {
|
|
2032
|
+
try {
|
|
2033
|
+
const url = deploymentId;
|
|
2034
|
+
if (url.startsWith("http")) {
|
|
2035
|
+
const res = await axios3.get(url, { timeout: 5e3 });
|
|
2036
|
+
if (res.status === 200) return { state: "running", url };
|
|
2037
|
+
}
|
|
2038
|
+
} catch {
|
|
2039
|
+
}
|
|
2040
|
+
return { state: "unknown" };
|
|
2041
|
+
}
|
|
2042
|
+
async rollback(_deploymentId) {
|
|
2043
|
+
throw new CustomDeploymentError("Rollback is deployment-method specific. Implement in your deployment pipeline.");
|
|
2044
|
+
}
|
|
2045
|
+
// ── Docker Method ──
|
|
2046
|
+
async deployDocker(config) {
|
|
2047
|
+
if (!config.docker) {
|
|
2048
|
+
throw new CustomDeploymentError("Docker config required for docker deployment method");
|
|
2049
|
+
}
|
|
2050
|
+
const logs = [];
|
|
2051
|
+
const { docker } = config;
|
|
2052
|
+
try {
|
|
2053
|
+
logs.push("Building Docker image...");
|
|
2054
|
+
const dockerfile = docker.dockerfile || "Dockerfile";
|
|
2055
|
+
const tag = docker.tag || "latest";
|
|
2056
|
+
const imageName = `${docker.registry}/${docker.imageName}:${tag}`;
|
|
2057
|
+
const buildArgs = docker.buildArgs ? Object.entries(docker.buildArgs).flatMap(([k, v]) => ["--build-arg", `${k}=${v}`]) : [];
|
|
2058
|
+
await exec("docker", ["build", "-f", dockerfile, "-t", imageName, ...buildArgs, "."]);
|
|
2059
|
+
logs.push(`Image built: ${imageName}`);
|
|
2060
|
+
if (docker.registryAuth) {
|
|
2061
|
+
logs.push(`Logging in to registry: ${docker.registry}...`);
|
|
2062
|
+
await exec("docker", [
|
|
2063
|
+
"login",
|
|
2064
|
+
docker.registry,
|
|
2065
|
+
"-u",
|
|
2066
|
+
docker.registryAuth.username,
|
|
2067
|
+
"-p",
|
|
2068
|
+
docker.registryAuth.password
|
|
2069
|
+
]);
|
|
2070
|
+
logs.push("Registry login successful.");
|
|
2071
|
+
}
|
|
2072
|
+
logs.push("Pushing image to registry...");
|
|
2073
|
+
await exec("docker", ["push", imageName]);
|
|
2074
|
+
logs.push("Image pushed.");
|
|
2075
|
+
logs.push(`Deploying to ${docker.hostUrl}...`);
|
|
2076
|
+
if (docker.hostUrl === "localhost" || docker.hostUrl === "127.0.0.1") {
|
|
2077
|
+
try {
|
|
2078
|
+
await exec("docker", ["stop", docker.imageName]);
|
|
2079
|
+
await exec("docker", ["rm", docker.imageName]);
|
|
2080
|
+
} catch {
|
|
2081
|
+
}
|
|
2082
|
+
await exec("docker", ["pull", imageName]);
|
|
2083
|
+
const envArgs = config.environmentVariables ? Object.entries(config.environmentVariables).flatMap(([k, v]) => ["-e", `${k}=${v}`]) : [];
|
|
2084
|
+
await exec("docker", [
|
|
2085
|
+
"run",
|
|
2086
|
+
"-d",
|
|
2087
|
+
"--name",
|
|
2088
|
+
docker.imageName,
|
|
2089
|
+
"-p",
|
|
2090
|
+
`${docker.hostPort}:${docker.containerPort}`,
|
|
2091
|
+
...envArgs,
|
|
2092
|
+
imageName
|
|
2093
|
+
]);
|
|
2094
|
+
logs.push(`Container running on port ${docker.hostPort}`);
|
|
2095
|
+
} else {
|
|
2096
|
+
logs.push(`Remote Docker host deployment: Pull ${imageName} on ${docker.hostUrl} and run with port ${docker.hostPort}:${docker.containerPort}`);
|
|
2097
|
+
}
|
|
2098
|
+
const healthUrl = config.healthCheckUrl || `http://localhost:${docker.hostPort}/health`;
|
|
2099
|
+
return {
|
|
2100
|
+
success: true,
|
|
2101
|
+
url: `http://localhost:${docker.hostPort}`,
|
|
2102
|
+
deploymentId: healthUrl,
|
|
2103
|
+
logs
|
|
2104
|
+
};
|
|
2105
|
+
} catch (error) {
|
|
2106
|
+
return {
|
|
2107
|
+
success: false,
|
|
2108
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2109
|
+
logs
|
|
2110
|
+
};
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
// ── SSH Method ──
|
|
2114
|
+
async deploySSH(config) {
|
|
2115
|
+
if (!config.ssh) {
|
|
2116
|
+
throw new CustomDeploymentError("SSH config required for ssh deployment method");
|
|
2117
|
+
}
|
|
2118
|
+
const logs = [];
|
|
2119
|
+
const { ssh } = config;
|
|
2120
|
+
try {
|
|
2121
|
+
const sshTarget = `${ssh.username}@${ssh.host}`;
|
|
2122
|
+
const sshPort = ssh.port || 22;
|
|
2123
|
+
const sshBaseArgs = ["-p", String(sshPort), "-o", "StrictHostKeyChecking=no"];
|
|
2124
|
+
if (ssh.authMethod === "privateKey" && ssh.privateKeyPath) {
|
|
2125
|
+
sshBaseArgs.push("-i", ssh.privateKeyPath);
|
|
2126
|
+
}
|
|
2127
|
+
if (ssh.preDeployCommands && ssh.preDeployCommands.length > 0) {
|
|
2128
|
+
logs.push("Running pre-deploy commands...");
|
|
2129
|
+
for (const cmd of ssh.preDeployCommands) {
|
|
2130
|
+
await exec("ssh", [...sshBaseArgs, sshTarget, cmd]);
|
|
2131
|
+
logs.push(` Pre-deploy: ${cmd}`);
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
logs.push(`Building: ${config.buildCommand}`);
|
|
2135
|
+
const [buildCmd, ...buildArgs] = config.buildCommand.split(" ");
|
|
2136
|
+
await exec(buildCmd, buildArgs);
|
|
2137
|
+
logs.push("Build complete.");
|
|
2138
|
+
logs.push(`Deploying to ${sshTarget}:${ssh.deploymentPath}...`);
|
|
2139
|
+
await exec("ssh", [...sshBaseArgs, sshTarget, `mkdir -p ${ssh.deploymentPath}`]);
|
|
2140
|
+
const rsyncArgs = [
|
|
2141
|
+
"-avz",
|
|
2142
|
+
"--delete",
|
|
2143
|
+
"-e",
|
|
2144
|
+
`ssh ${sshBaseArgs.join(" ")}`,
|
|
2145
|
+
"./",
|
|
2146
|
+
`${sshTarget}:${ssh.deploymentPath}/`
|
|
2147
|
+
];
|
|
2148
|
+
await exec("rsync", rsyncArgs);
|
|
2149
|
+
logs.push("Files synced.");
|
|
2150
|
+
logs.push(`Starting: ${config.startCommand}`);
|
|
2151
|
+
await exec("ssh", [
|
|
2152
|
+
...sshBaseArgs,
|
|
2153
|
+
sshTarget,
|
|
2154
|
+
`cd ${ssh.deploymentPath} && ${config.startCommand}`
|
|
2155
|
+
]);
|
|
2156
|
+
logs.push("Service started.");
|
|
2157
|
+
if (ssh.postDeployCommands && ssh.postDeployCommands.length > 0) {
|
|
2158
|
+
logs.push("Running post-deploy commands...");
|
|
2159
|
+
for (const cmd of ssh.postDeployCommands) {
|
|
2160
|
+
await exec("ssh", [...sshBaseArgs, sshTarget, cmd]);
|
|
2161
|
+
logs.push(` Post-deploy: ${cmd}`);
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
const healthUrl = config.healthCheckUrl || `http://${ssh.host}:${config.port || 3e3}/health`;
|
|
2165
|
+
return {
|
|
2166
|
+
success: true,
|
|
2167
|
+
url: `http://${ssh.host}:${config.port || 3e3}`,
|
|
2168
|
+
deploymentId: healthUrl,
|
|
2169
|
+
logs
|
|
2170
|
+
};
|
|
2171
|
+
} catch (error) {
|
|
2172
|
+
return {
|
|
2173
|
+
success: false,
|
|
2174
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2175
|
+
logs
|
|
2176
|
+
};
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
// ── CLI Method ──
|
|
2180
|
+
async deployCLI(config) {
|
|
2181
|
+
if (!config.cli) {
|
|
2182
|
+
throw new CustomDeploymentError("CLI config required for cli deployment method");
|
|
2183
|
+
}
|
|
2184
|
+
const logs = [];
|
|
2185
|
+
const { cli } = config;
|
|
2186
|
+
try {
|
|
2187
|
+
if (cli.installCommand) {
|
|
2188
|
+
logs.push(`Installing CLI: ${cli.installCommand}`);
|
|
2189
|
+
const [installCmd, ...installArgs] = cli.installCommand.split(" ");
|
|
2190
|
+
await exec(installCmd, installArgs);
|
|
2191
|
+
logs.push("CLI installed.");
|
|
2192
|
+
}
|
|
2193
|
+
if (cli.loginCommand) {
|
|
2194
|
+
logs.push(`Logging in: ${cli.loginCommand}`);
|
|
2195
|
+
const [loginCmd, ...loginArgs] = cli.loginCommand.split(" ");
|
|
2196
|
+
await exec(loginCmd, loginArgs);
|
|
2197
|
+
logs.push("Login successful.");
|
|
2198
|
+
}
|
|
2199
|
+
logs.push(`Building: ${config.buildCommand}`);
|
|
2200
|
+
const [buildCmd, ...buildArgs] = config.buildCommand.split(" ");
|
|
2201
|
+
await exec(buildCmd, buildArgs);
|
|
2202
|
+
logs.push("Build complete.");
|
|
2203
|
+
logs.push(`Deploying: ${cli.deployCommand}`);
|
|
2204
|
+
const [deployCmd, ...deployArgs] = cli.deployCommand.split(" ");
|
|
2205
|
+
const deployResult = await exec(deployCmd, deployArgs);
|
|
2206
|
+
logs.push(deployResult.stdout);
|
|
2207
|
+
let url;
|
|
2208
|
+
if (cli.statusCommand) {
|
|
2209
|
+
const [statusCmd, ...statusArgs] = cli.statusCommand.split(" ");
|
|
2210
|
+
const statusResult = await exec(statusCmd, statusArgs);
|
|
2211
|
+
logs.push(statusResult.stdout);
|
|
2212
|
+
const urlMatch = statusResult.stdout.match(/https?:\/\/[^\s]+/);
|
|
2213
|
+
if (urlMatch) url = urlMatch[0];
|
|
2214
|
+
}
|
|
2215
|
+
return {
|
|
2216
|
+
success: true,
|
|
2217
|
+
url,
|
|
2218
|
+
deploymentId: `cli-${cli.cliName}-${Date.now()}`,
|
|
2219
|
+
logs
|
|
2220
|
+
};
|
|
2221
|
+
} catch (error) {
|
|
2222
|
+
return {
|
|
2223
|
+
success: false,
|
|
2224
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2225
|
+
logs
|
|
2226
|
+
};
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
// ── API Method ──
|
|
2230
|
+
async deployAPI(config) {
|
|
2231
|
+
if (!config.api) {
|
|
2232
|
+
throw new CustomDeploymentError("API config required for api deployment method");
|
|
2233
|
+
}
|
|
2234
|
+
const logs = [];
|
|
2235
|
+
const { api } = config;
|
|
2236
|
+
try {
|
|
2237
|
+
logs.push(`Building: ${config.buildCommand}`);
|
|
2238
|
+
const [buildCmd, ...buildArgs] = config.buildCommand.split(" ");
|
|
2239
|
+
await exec(buildCmd, buildArgs);
|
|
2240
|
+
logs.push("Build complete.");
|
|
2241
|
+
const headers = {
|
|
2242
|
+
"Content-Type": "application/json",
|
|
2243
|
+
...api.headers || {}
|
|
2244
|
+
};
|
|
2245
|
+
switch (api.authType) {
|
|
2246
|
+
case "bearer":
|
|
2247
|
+
headers["Authorization"] = `Bearer ${api.authToken}`;
|
|
2248
|
+
break;
|
|
2249
|
+
case "apiKey":
|
|
2250
|
+
headers["X-API-Key"] = api.apiKey || "";
|
|
2251
|
+
break;
|
|
2252
|
+
case "basic":
|
|
2253
|
+
headers["Authorization"] = `Basic ${Buffer.from(api.authToken || "").toString("base64")}`;
|
|
2254
|
+
break;
|
|
2255
|
+
case "custom":
|
|
2256
|
+
break;
|
|
2257
|
+
}
|
|
2258
|
+
const deployUrl = `${api.baseUrl}${api.endpoints.deploy}`;
|
|
2259
|
+
const method = api.requestMethod || "POST";
|
|
2260
|
+
const payload = api.payloadTemplate || {
|
|
2261
|
+
name: config.environmentVariables?.SERVICE_NAME || "moltium-agent",
|
|
2262
|
+
buildCommand: config.buildCommand,
|
|
2263
|
+
startCommand: config.startCommand,
|
|
2264
|
+
envVars: config.environmentVariables
|
|
2265
|
+
};
|
|
2266
|
+
logs.push(`Calling deploy API: ${method} ${deployUrl}`);
|
|
2267
|
+
const response = method === "PUT" ? await axios3.put(deployUrl, payload, { headers }) : await axios3.post(deployUrl, payload, { headers });
|
|
2268
|
+
logs.push(`API response: ${response.status}`);
|
|
2269
|
+
const deploymentId = response.data.id || response.data.deploymentId || `api-${Date.now()}`;
|
|
2270
|
+
let url = response.data.url || response.data.deployUrl;
|
|
2271
|
+
if (api.endpoints.status) {
|
|
2272
|
+
const statusUrl = `${api.baseUrl}${api.endpoints.status}`.replace(":id", deploymentId);
|
|
2273
|
+
try {
|
|
2274
|
+
const statusRes = await axios3.get(statusUrl, { headers });
|
|
2275
|
+
url = url || statusRes.data.url;
|
|
2276
|
+
logs.push(`Status: ${statusRes.data.status || statusRes.data.state || "unknown"}`);
|
|
2277
|
+
} catch {
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
return {
|
|
2281
|
+
success: true,
|
|
2282
|
+
url,
|
|
2283
|
+
deploymentId,
|
|
2284
|
+
logs
|
|
2285
|
+
};
|
|
2286
|
+
} catch (error) {
|
|
2287
|
+
const msg = axios3.isAxiosError(error) ? `API error ${error.response?.status}: ${error.response?.data?.message || error.message}` : error instanceof Error ? error.message : String(error);
|
|
2288
|
+
return {
|
|
2289
|
+
success: false,
|
|
2290
|
+
error: msg,
|
|
2291
|
+
logs
|
|
2292
|
+
};
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
};
|
|
2296
|
+
|
|
2297
|
+
// src/commands/deploy.ts
|
|
2298
|
+
var deployers = {
|
|
2299
|
+
railway: new RailwayDeployer(),
|
|
2300
|
+
render: new RenderDeployer(),
|
|
2301
|
+
aws: new AWSDeployer(),
|
|
2302
|
+
digitalocean: new DigitalOceanDeployer(),
|
|
2303
|
+
custom: new CustomDeployer()
|
|
2304
|
+
};
|
|
2305
|
+
var deployCommand = new Command3("deploy").description("Deploy agent to cloud platform").argument("[platform]", "Deployment platform (railway, render, aws, digitalocean, custom)").option("-c, --config <file>", "Deployment config file", "deployment.config.ts").action(async (platform, options) => {
|
|
2306
|
+
const agentDir = resolve3(process.cwd());
|
|
2307
|
+
loadEnv2({ path: resolve3(agentDir, ".env") });
|
|
2308
|
+
if (!platform) {
|
|
2309
|
+
console.log(chalk3.yellow("Available deployment platforms:"));
|
|
2310
|
+
for (const [name, deployer2] of Object.entries(deployers)) {
|
|
2311
|
+
console.log(` ${chalk3.cyan(name)} \u2014 ${deployer2.description}`);
|
|
2312
|
+
}
|
|
2313
|
+
console.log(`
|
|
2314
|
+
Usage: ${chalk3.cyan("moltium deploy <platform>")}`);
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
2317
|
+
const deployer = deployers[platform];
|
|
2318
|
+
if (!deployer) {
|
|
2319
|
+
console.log(chalk3.red(`Unknown platform: ${platform}`));
|
|
2320
|
+
console.log(`Available: ${Object.keys(deployers).join(", ")}`);
|
|
2321
|
+
process.exit(1);
|
|
2322
|
+
}
|
|
2323
|
+
const spinner = ora3(`Deploying to ${platform}...`).start();
|
|
2324
|
+
try {
|
|
2325
|
+
const configLoader = new ConfigLoader2();
|
|
2326
|
+
await configLoader.load(agentDir);
|
|
2327
|
+
let deployConfig = {
|
|
2328
|
+
platform
|
|
2329
|
+
};
|
|
2330
|
+
const deployConfigPath = resolve3(agentDir, options?.config || "deployment.config.ts");
|
|
2331
|
+
try {
|
|
2332
|
+
const { createJiti } = await import("jiti");
|
|
2333
|
+
const jiti = createJiti(pathToFileURL2(deployConfigPath).href, {
|
|
2334
|
+
interopDefault: true,
|
|
2335
|
+
moduleCache: false
|
|
2336
|
+
});
|
|
2337
|
+
const module = await jiti.import(deployConfigPath);
|
|
2338
|
+
deployConfig = { ...deployConfig, ...module.default || module };
|
|
2339
|
+
} catch {
|
|
2340
|
+
}
|
|
2341
|
+
spinner.text = "Validating deployment configuration...";
|
|
2342
|
+
await deployer.validate(deployConfig);
|
|
2343
|
+
spinner.text = "Running pre-deployment checks...";
|
|
2344
|
+
await deployer.preCheck();
|
|
2345
|
+
spinner.text = `Deploying to ${platform}...`;
|
|
2346
|
+
const result = await deployer.deploy(deployConfig);
|
|
2347
|
+
if (result.success) {
|
|
2348
|
+
await deployer.postDeploy(result);
|
|
2349
|
+
spinner.succeed(chalk3.green(`Deployed to ${platform}!`));
|
|
2350
|
+
if (result.url) {
|
|
2351
|
+
console.log(` URL: ${chalk3.cyan(result.url)}`);
|
|
2352
|
+
}
|
|
2353
|
+
if (result.deploymentId) {
|
|
2354
|
+
console.log(` Deployment ID: ${chalk3.gray(result.deploymentId)}`);
|
|
2355
|
+
}
|
|
2356
|
+
} else {
|
|
2357
|
+
spinner.fail(chalk3.red(`Deployment failed: ${result.error}`));
|
|
2358
|
+
process.exit(1);
|
|
2359
|
+
}
|
|
2360
|
+
} catch (error) {
|
|
2361
|
+
spinner.fail(chalk3.red("Deployment failed"));
|
|
2362
|
+
console.error(error instanceof Error ? error.message : error);
|
|
2363
|
+
process.exit(1);
|
|
2364
|
+
}
|
|
2365
|
+
});
|
|
2366
|
+
|
|
2367
|
+
// src/commands/config.ts
|
|
2368
|
+
import { Command as Command4 } from "commander";
|
|
2369
|
+
import { resolve as resolve4 } from "path";
|
|
2370
|
+
import chalk4 from "chalk";
|
|
2371
|
+
import { ConfigLoader as ConfigLoader3 } from "@moltium/core";
|
|
2372
|
+
var configCommand = new Command4("config").description("Manage agent configuration");
|
|
2373
|
+
configCommand.command("show").description("Show current agent configuration").action(async () => {
|
|
2374
|
+
try {
|
|
2375
|
+
const configLoader = new ConfigLoader3();
|
|
2376
|
+
const { config, type } = await configLoader.load(resolve4(process.cwd()));
|
|
2377
|
+
console.log(chalk4.bold(`
|
|
2378
|
+
Agent: ${config.name}`));
|
|
2379
|
+
console.log(`Type: ${config.type || "general"}`);
|
|
2380
|
+
console.log(`Config: ${type}`);
|
|
2381
|
+
console.log(`LLM: ${config.llm.provider} / ${config.llm.model}`);
|
|
2382
|
+
console.log(`Behaviors: ${config.behaviors.decisionMaking}, ${config.behaviors.actionsPerHour || 5} actions/hr`);
|
|
2383
|
+
console.log(`Memory: ${config.memory.type}`);
|
|
2384
|
+
console.log(`Actions: ${config.actions.join(", ") || "none"}`);
|
|
2385
|
+
const platforms = Object.keys(config.social).filter(
|
|
2386
|
+
(k) => config.social[k]?.enabled
|
|
2387
|
+
);
|
|
2388
|
+
console.log(`Social: ${platforms.join(", ") || "none"}`);
|
|
2389
|
+
} catch (error) {
|
|
2390
|
+
console.error(chalk4.red(error instanceof Error ? error.message : "Failed to load config"));
|
|
2391
|
+
process.exit(1);
|
|
2392
|
+
}
|
|
2393
|
+
});
|
|
2394
|
+
configCommand.command("validate").description("Validate agent configuration").action(async () => {
|
|
2395
|
+
try {
|
|
2396
|
+
const configLoader = new ConfigLoader3();
|
|
2397
|
+
const { config, type } = await configLoader.load(resolve4(process.cwd()));
|
|
2398
|
+
console.log(chalk4.green(`
|
|
2399
|
+
\u2713 Valid ${type} configuration for "${config.name}"`));
|
|
2400
|
+
} catch (error) {
|
|
2401
|
+
console.error(chalk4.red(`
|
|
2402
|
+
\u2717 ${error instanceof Error ? error.message : "Invalid configuration"}`));
|
|
2403
|
+
process.exit(1);
|
|
2404
|
+
}
|
|
2405
|
+
});
|
|
2406
|
+
|
|
2407
|
+
// src/commands/migrate.ts
|
|
2408
|
+
import { Command as Command5 } from "commander";
|
|
2409
|
+
import { resolve as resolve5, join as join3 } from "path";
|
|
2410
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
2411
|
+
import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
2412
|
+
import chalk5 from "chalk";
|
|
2413
|
+
import ora4 from "ora";
|
|
2414
|
+
import { ConfigLoader as ConfigLoader4 } from "@moltium/core";
|
|
2415
|
+
var migrateCommand = new Command5("migrate").description("Migrate between configuration types").requiredOption("--from <type>", "Source config type (code/markdown)").requiredOption("--to <type>", "Target config type (code/markdown)").option("--dir <directory>", "Agent directory", ".").option("--keep", "Keep the original config file after migration").action(async (options) => {
|
|
2416
|
+
const { from, to, keep } = options;
|
|
2417
|
+
const agentDir = resolve5(process.cwd(), options.dir);
|
|
2418
|
+
if (from === to) {
|
|
2419
|
+
console.log(chalk5.yellow("Source and target config types are the same."));
|
|
2420
|
+
return;
|
|
2421
|
+
}
|
|
2422
|
+
if (!["code", "markdown"].includes(from) || !["code", "markdown"].includes(to)) {
|
|
2423
|
+
console.log(chalk5.red('Config types must be "code" or "markdown".'));
|
|
2424
|
+
process.exit(1);
|
|
2425
|
+
}
|
|
2426
|
+
const spinner = ora4(`Migrating from ${from} to ${to}...`).start();
|
|
2427
|
+
try {
|
|
2428
|
+
const configLoader = new ConfigLoader4();
|
|
2429
|
+
if (from === "code" && to === "markdown") {
|
|
2430
|
+
await migrateCodeToMarkdown(configLoader, agentDir, keep, spinner);
|
|
2431
|
+
} else {
|
|
2432
|
+
await migrateMarkdownToCode(configLoader, agentDir, keep, spinner);
|
|
2433
|
+
}
|
|
2434
|
+
spinner.succeed(chalk5.green(`Migration complete: ${from} \u2192 ${to}`));
|
|
2435
|
+
} catch (error) {
|
|
2436
|
+
spinner.fail(chalk5.red("Migration failed"));
|
|
2437
|
+
console.error(error instanceof Error ? error.message : error);
|
|
2438
|
+
process.exit(1);
|
|
2439
|
+
}
|
|
2440
|
+
});
|
|
2441
|
+
async function migrateCodeToMarkdown(configLoader, agentDir, keep, spinner) {
|
|
2442
|
+
const configPath = resolve5(agentDir, "agent.config.ts");
|
|
2443
|
+
if (!existsSync3(configPath)) {
|
|
2444
|
+
throw new Error("No agent.config.ts found in the current directory.");
|
|
2445
|
+
}
|
|
2446
|
+
spinner.text = "Loading code config...";
|
|
2447
|
+
const config = await configLoader.loadCode(configPath);
|
|
2448
|
+
spinner.text = "Generating agent.md...";
|
|
2449
|
+
const socialSections = generateSocialMarkdown(config);
|
|
2450
|
+
const skillSections = config.actions.map((a) => {
|
|
2451
|
+
const name = a.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
2452
|
+
return `### ${name}
|
|
2453
|
+
Execute the ${a} action based on context and current goals.`;
|
|
2454
|
+
}).join("\n\n");
|
|
2455
|
+
const sleepSchedule = config.behaviors.sleepSchedule ? `${config.behaviors.sleepSchedule.start}:00-${config.behaviors.sleepSchedule.end}:00${config.behaviors.sleepSchedule.timezone ? " " + config.behaviors.sleepSchedule.timezone : " UTC"}` : "none";
|
|
2456
|
+
const agentMd = `# Agent Configuration
|
|
2457
|
+
|
|
2458
|
+
## Identity
|
|
2459
|
+
name: ${config.name}
|
|
2460
|
+
type: ${config.type || "assistant"}
|
|
2461
|
+
personality: ${config.personality.traits.join(", ")}
|
|
2462
|
+
|
|
2463
|
+
## Bio
|
|
2464
|
+
${config.personality.bio}
|
|
2465
|
+
|
|
2466
|
+
## Social Platforms
|
|
2467
|
+
${socialSections}
|
|
2468
|
+
|
|
2469
|
+
## Behaviors
|
|
2470
|
+
autonomous: ${config.behaviors.autonomous}
|
|
2471
|
+
decision_making: ${config.behaviors.decisionMaking}
|
|
2472
|
+
actions_per_hour: ${typeof config.behaviors.actionsPerHour === "number" ? config.behaviors.actionsPerHour : 5}
|
|
2473
|
+
sleep_schedule: ${sleepSchedule}
|
|
2474
|
+
|
|
2475
|
+
## Memory
|
|
2476
|
+
type: ${config.memory.type}
|
|
2477
|
+
${config.memory.retention ? `retention: ${config.memory.retention}` : ""}
|
|
2478
|
+
|
|
2479
|
+
## Skills
|
|
2480
|
+
|
|
2481
|
+
${skillSections}
|
|
2482
|
+
`;
|
|
2483
|
+
await writeFile2(join3(agentDir, "agent.md"), agentMd);
|
|
2484
|
+
await mkdir2(join3(agentDir, "personality"), { recursive: true });
|
|
2485
|
+
await writeFile2(join3(agentDir, "personality", "bio.md"), `# ${config.name}
|
|
2486
|
+
|
|
2487
|
+
${config.personality.bio}
|
|
2488
|
+
`);
|
|
2489
|
+
await writeFile2(join3(agentDir, "personality", "traits.md"), `# ${config.name} \u2014 Traits
|
|
2490
|
+
|
|
2491
|
+
${config.personality.traits.map((t) => `### ${t.charAt(0).toUpperCase() + t.slice(1)}
|
|
2492
|
+
Describe how this trait manifests in behavior.
|
|
2493
|
+
`).join("\n")}`);
|
|
2494
|
+
await mkdir2(join3(agentDir, "skills"), { recursive: true });
|
|
2495
|
+
for (const action of config.actions) {
|
|
2496
|
+
const name = action.replace(/_/g, "-");
|
|
2497
|
+
const title = action.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
2498
|
+
await writeFile2(join3(agentDir, "skills", `${name}.md`), `# ${title}
|
|
2499
|
+
|
|
2500
|
+
Describe the detailed instructions for this skill.
|
|
2501
|
+
`);
|
|
2502
|
+
}
|
|
2503
|
+
await mkdir2(join3(agentDir, "prompts"), { recursive: true });
|
|
2504
|
+
await writeFile2(join3(agentDir, "prompts", "system.md"), `# System Prompt for ${config.name}
|
|
2505
|
+
|
|
2506
|
+
${config.llm.systemPrompt || "Add your custom system prompt instructions here."}
|
|
2507
|
+
`);
|
|
2508
|
+
await mkdir2(join3(agentDir, "memory"), { recursive: true });
|
|
2509
|
+
await writeFile2(join3(agentDir, "memory", "context.md"), `# ${config.name} \u2014 Long-Term Context
|
|
2510
|
+
|
|
2511
|
+
## Important Facts
|
|
2512
|
+
- Migrated from code-based configuration
|
|
2513
|
+
- Type: ${config.type || "assistant"}
|
|
2514
|
+
|
|
2515
|
+
## Current Goals
|
|
2516
|
+
1. Continue operating as before
|
|
2517
|
+
`);
|
|
2518
|
+
if (!keep) {
|
|
2519
|
+
const { rename } = await import("fs/promises");
|
|
2520
|
+
await rename(configPath, configPath + ".bak");
|
|
2521
|
+
spinner.text = "Original agent.config.ts renamed to agent.config.ts.bak";
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
async function migrateMarkdownToCode(configLoader, agentDir, keep, spinner) {
|
|
2525
|
+
const mdPath = resolve5(agentDir, "agent.md");
|
|
2526
|
+
if (!existsSync3(mdPath)) {
|
|
2527
|
+
throw new Error("No agent.md found in the current directory.");
|
|
2528
|
+
}
|
|
2529
|
+
spinner.text = "Loading markdown config...";
|
|
2530
|
+
const config = await configLoader.loadMarkdown(mdPath);
|
|
2531
|
+
spinner.text = "Generating agent.config.ts...";
|
|
2532
|
+
const socialConfigStr = generateSocialCodeConfig(config);
|
|
2533
|
+
const actionsArrayStr = config.actions.map((a) => `'${a}'`).join(", ");
|
|
2534
|
+
const sleepScheduleStr = config.behaviors.sleepSchedule ? `{ start: ${config.behaviors.sleepSchedule.start}, end: ${config.behaviors.sleepSchedule.end}${config.behaviors.sleepSchedule.timezone ? `, timezone: '${config.behaviors.sleepSchedule.timezone}'` : ""} }` : "undefined";
|
|
2535
|
+
const providerEnvKey = config.llm.provider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY";
|
|
2536
|
+
const codeConfig = `import type { AgentConfig } from '@moltium/core';
|
|
2537
|
+
|
|
2538
|
+
const config: AgentConfig = {
|
|
2539
|
+
name: '${config.name}',
|
|
2540
|
+
type: '${config.type || "assistant"}',
|
|
2541
|
+
|
|
2542
|
+
personality: {
|
|
2543
|
+
traits: [${config.personality.traits.map((t) => `'${t}'`).join(", ")}],
|
|
2544
|
+
bio: ${JSON.stringify(config.personality.bio)},
|
|
2545
|
+
},
|
|
2546
|
+
|
|
2547
|
+
llm: {
|
|
2548
|
+
provider: '${config.llm.provider}',
|
|
2549
|
+
model: '${config.llm.model}',
|
|
2550
|
+
apiKey: process.env.${providerEnvKey} || '',
|
|
2551
|
+
temperature: ${config.llm.temperature ?? 0.7},
|
|
2552
|
+
},
|
|
2553
|
+
|
|
2554
|
+
social: {
|
|
2555
|
+
${socialConfigStr}
|
|
2556
|
+
},
|
|
2557
|
+
|
|
2558
|
+
behaviors: {
|
|
2559
|
+
autonomous: ${config.behaviors.autonomous},
|
|
2560
|
+
decisionMaking: '${config.behaviors.decisionMaking}',
|
|
2561
|
+
actionsPerHour: ${typeof config.behaviors.actionsPerHour === "number" ? config.behaviors.actionsPerHour : 5},
|
|
2562
|
+
sleepSchedule: ${sleepScheduleStr},
|
|
2563
|
+
},
|
|
2564
|
+
|
|
2565
|
+
memory: {
|
|
2566
|
+
type: '${config.memory.type}',
|
|
2567
|
+
${config.memory.retention ? `retention: '${config.memory.retention}',` : ""}
|
|
2568
|
+
},
|
|
2569
|
+
|
|
2570
|
+
actions: [${actionsArrayStr}],
|
|
2571
|
+
};
|
|
2572
|
+
|
|
2573
|
+
export default config;
|
|
2574
|
+
`;
|
|
2575
|
+
await writeFile2(join3(agentDir, "agent.config.ts"), codeConfig);
|
|
2576
|
+
await mkdir2(join3(agentDir, "hooks"), { recursive: true });
|
|
2577
|
+
await mkdir2(join3(agentDir, "actions"), { recursive: true });
|
|
2578
|
+
const systemPromptPath = join3(agentDir, "prompts", "system.md");
|
|
2579
|
+
if (existsSync3(systemPromptPath)) {
|
|
2580
|
+
const systemPrompt = readFileSync2(systemPromptPath, "utf-8");
|
|
2581
|
+
await writeFile2(join3(agentDir, "hooks", "onInit.ts"), `import type { Agent } from '@moltium/core';
|
|
2582
|
+
|
|
2583
|
+
export async function onInit(agent: Agent): Promise<void> {
|
|
2584
|
+
// Migrated system prompt content from prompts/system.md
|
|
2585
|
+
agent.appendSystemPrompt(${JSON.stringify(systemPrompt)});
|
|
2586
|
+
|
|
2587
|
+
const memory = agent.getMemory();
|
|
2588
|
+
await memory.remember('session_started', new Date().toISOString());
|
|
2589
|
+
}
|
|
2590
|
+
`);
|
|
2591
|
+
} else {
|
|
2592
|
+
await writeFile2(join3(agentDir, "hooks", "onInit.ts"), `import type { Agent } from '@moltium/core';
|
|
2593
|
+
|
|
2594
|
+
export async function onInit(agent: Agent): Promise<void> {
|
|
2595
|
+
const memory = agent.getMemory();
|
|
2596
|
+
await memory.remember('session_started', new Date().toISOString());
|
|
2597
|
+
}
|
|
2598
|
+
`);
|
|
2599
|
+
}
|
|
2600
|
+
await writeFile2(join3(agentDir, "hooks", "onTick.ts"), `import type { Agent } from '@moltium/core';
|
|
2601
|
+
|
|
2602
|
+
export async function onTick(agent: Agent): Promise<void> {
|
|
2603
|
+
const memory = agent.getMemory();
|
|
2604
|
+
const count = ((await memory.recall('tick_count')) as number) || 0;
|
|
2605
|
+
await memory.remember('tick_count', count + 1);
|
|
2606
|
+
}
|
|
2607
|
+
`);
|
|
2608
|
+
await writeFile2(join3(agentDir, "hooks", "onShutdown.ts"), `import type { Agent } from '@moltium/core';
|
|
2609
|
+
|
|
2610
|
+
export async function onShutdown(agent: Agent): Promise<void> {
|
|
2611
|
+
const memory = agent.getMemory();
|
|
2612
|
+
await memory.remember('last_shutdown', new Date().toISOString());
|
|
2613
|
+
}
|
|
2614
|
+
`);
|
|
2615
|
+
if (!existsSync3(join3(agentDir, "tsconfig.json"))) {
|
|
2616
|
+
await writeFile2(join3(agentDir, "tsconfig.json"), JSON.stringify({
|
|
2617
|
+
compilerOptions: {
|
|
2618
|
+
target: "ES2022",
|
|
2619
|
+
module: "ESNext",
|
|
2620
|
+
moduleResolution: "bundler",
|
|
2621
|
+
strict: true,
|
|
2622
|
+
esModuleInterop: true,
|
|
2623
|
+
skipLibCheck: true,
|
|
2624
|
+
outDir: "./dist",
|
|
2625
|
+
rootDir: "."
|
|
2626
|
+
},
|
|
2627
|
+
include: ["*.ts", "actions/**/*.ts", "hooks/**/*.ts"]
|
|
2628
|
+
}, null, 2));
|
|
2629
|
+
}
|
|
2630
|
+
if (!keep) {
|
|
2631
|
+
const { rename } = await import("fs/promises");
|
|
2632
|
+
await rename(mdPath, mdPath + ".bak");
|
|
2633
|
+
spinner.text = "Original agent.md renamed to agent.md.bak";
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
function generateSocialMarkdown(config) {
|
|
2637
|
+
const sections = [];
|
|
2638
|
+
const social = config.social || {};
|
|
2639
|
+
for (const [platform, platformConfig] of Object.entries(social)) {
|
|
2640
|
+
if (!platformConfig || typeof platformConfig !== "object") continue;
|
|
2641
|
+
const pc = platformConfig;
|
|
2642
|
+
if (!pc.enabled) continue;
|
|
2643
|
+
const lines = [`### ${platform.charAt(0).toUpperCase() + platform.slice(1)}`];
|
|
2644
|
+
lines.push(`enabled: true`);
|
|
2645
|
+
for (const [key, value] of Object.entries(pc)) {
|
|
2646
|
+
if (key === "enabled" || key === "credentials" || key === "apiKey") continue;
|
|
2647
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
2648
|
+
lines.push(`${key}: ${value}`);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
sections.push(lines.join("\n"));
|
|
2652
|
+
}
|
|
2653
|
+
return sections.length > 0 ? sections.join("\n\n") : "No social platforms configured.";
|
|
2654
|
+
}
|
|
2655
|
+
function generateSocialCodeConfig(config) {
|
|
2656
|
+
const social = config.social || {};
|
|
2657
|
+
const lines = [];
|
|
2658
|
+
for (const [platform, platformConfig] of Object.entries(social)) {
|
|
2659
|
+
if (!platformConfig || typeof platformConfig !== "object") continue;
|
|
2660
|
+
const pc = platformConfig;
|
|
2661
|
+
if (platform === "moltbook" && pc.enabled) {
|
|
2662
|
+
lines.push(` moltbook: {
|
|
2663
|
+
enabled: true,
|
|
2664
|
+
apiKey: process.env.MOLTBOOK_API_KEY || '',
|
|
2665
|
+
},`);
|
|
2666
|
+
} else if (platform === "twitter" && pc.enabled) {
|
|
2667
|
+
lines.push(` twitter: {
|
|
2668
|
+
enabled: true,
|
|
2669
|
+
credentials: {
|
|
2670
|
+
apiKey: process.env.TWITTER_API_KEY || '',
|
|
2671
|
+
apiSecret: process.env.TWITTER_API_SECRET || '',
|
|
2672
|
+
accessToken: process.env.TWITTER_ACCESS_TOKEN || '',
|
|
2673
|
+
accessSecret: process.env.TWITTER_ACCESS_SECRET || '',
|
|
2674
|
+
},
|
|
2675
|
+
},`);
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
return lines.join("\n");
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
// src/commands/status.ts
|
|
2682
|
+
import { Command as Command6 } from "commander";
|
|
2683
|
+
import chalk6 from "chalk";
|
|
2684
|
+
import axios4 from "axios";
|
|
2685
|
+
var statusCommand = new Command6("status").description("Check agent status").option("-r, --remote <url>", "Remote agent URL").option("-p, --port <number>", "Local port", "3000").action(async (options) => {
|
|
2686
|
+
const url = options.remote || `http://localhost:${options.port}`;
|
|
2687
|
+
try {
|
|
2688
|
+
const [health, status] = await Promise.all([
|
|
2689
|
+
axios4.get(`${url}/health`, { timeout: 5e3 }),
|
|
2690
|
+
axios4.get(`${url}/status`, { timeout: 5e3 })
|
|
2691
|
+
]);
|
|
2692
|
+
const data = status.data;
|
|
2693
|
+
console.log(chalk6.bold(`
|
|
2694
|
+
Agent: ${data.name}`));
|
|
2695
|
+
console.log(`State: ${chalk6.green(data.state)}`);
|
|
2696
|
+
console.log(`Type: ${data.type}`);
|
|
2697
|
+
console.log(`Uptime: ${Math.floor(data.uptime)}s`);
|
|
2698
|
+
console.log(`Health: ${chalk6.green(health.data.status)}`);
|
|
2699
|
+
} catch (error) {
|
|
2700
|
+
if (axios4.isAxiosError(error) && error.code === "ECONNREFUSED") {
|
|
2701
|
+
console.log(chalk6.yellow(`
|
|
2702
|
+
No agent running at ${url}`));
|
|
2703
|
+
} else {
|
|
2704
|
+
console.error(chalk6.red(`
|
|
2705
|
+
Failed to reach agent at ${url}`));
|
|
2706
|
+
console.error(error instanceof Error ? error.message : error);
|
|
2707
|
+
}
|
|
2708
|
+
process.exit(1);
|
|
2709
|
+
}
|
|
2710
|
+
});
|
|
2711
|
+
|
|
2712
|
+
// src/index.ts
|
|
2713
|
+
var program = new Command7();
|
|
2714
|
+
program.name("moltium").description("Moltium Agent SDK \u2014 create and manage autonomous AI agents").version("0.1.0");
|
|
2715
|
+
program.addCommand(initCommand);
|
|
2716
|
+
program.addCommand(startCommand);
|
|
2717
|
+
program.addCommand(deployCommand);
|
|
2718
|
+
program.addCommand(configCommand);
|
|
2719
|
+
program.addCommand(migrateCommand);
|
|
2720
|
+
program.addCommand(statusCommand);
|
|
2721
|
+
program.parse();
|
|
2722
|
+
//# sourceMappingURL=index.js.map
|