neoagent 2.2.1-beta.7 → 2.3.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/docs/index.md +1 -0
- package/docs/migration.md +238 -0
- package/lib/manager.js +99 -2
- package/lib/migrations.js +409 -0
- package/package.json +1 -1
- package/server/catalog_sources/store-bundles/skills/productivity/migration/SKILL.md +173 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +72170 -70842
- package/server/routes/auth.js +13 -5
- package/server/routes/integrations.js +22 -0
- package/server/routes/messaging.js +41 -5
- package/server/routes/settings.js +1 -0
- package/server/services/integrations/google/provider.js +20 -2
- package/server/services/integrations/manager.js +79 -8
- package/server/services/messaging/access_policy.js +703 -0
- package/server/services/messaging/access_policy.test.js +228 -0
- package/server/services/messaging/automation.js +32 -95
- package/server/services/messaging/base.js +39 -0
- package/server/services/messaging/discord.js +61 -46
- package/server/services/messaging/http_platforms.js +178 -15
- package/server/services/messaging/manager.js +136 -69
- package/server/services/messaging/telegram.js +54 -40
- package/server/services/messaging/telnyx.js +43 -14
- package/server/services/messaging/whatsapp.js +27 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const readline = require('readline');
|
|
5
|
+
const { parseEnv } = require('../runtime/env');
|
|
6
|
+
const { ENV_FILE, AGENT_DATA_DIR, DATA_DIR } = require('../runtime/paths');
|
|
7
|
+
|
|
8
|
+
const HOME_DIR = os.homedir();
|
|
9
|
+
|
|
10
|
+
const OPENCLAW_PATHS = {
|
|
11
|
+
config: path.join(HOME_DIR, '.openclaw', 'openclaw.json'),
|
|
12
|
+
workspace: path.join(HOME_DIR, '.openclaw', 'workspace'),
|
|
13
|
+
skills: path.join(HOME_DIR, '.openclaw', 'skills'),
|
|
14
|
+
soul: path.join(HOME_DIR, '.openclaw', 'workspace', 'SOUL.md'),
|
|
15
|
+
memory: path.join(HOME_DIR, '.openclaw', 'workspace', 'MEMORY.md'),
|
|
16
|
+
user: path.join(HOME_DIR, '.openclaw', 'workspace', 'USER.md'),
|
|
17
|
+
agents: path.join(HOME_DIR, '.openclaw', 'agents'),
|
|
18
|
+
env: path.join(HOME_DIR, '.openclaw', '.env'),
|
|
19
|
+
legacyConfig: path.join(HOME_DIR, '.clawdbot', 'config.json')
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const HERMES_PATHS = {
|
|
23
|
+
config: path.join(HOME_DIR, '.hermes', 'config.yaml'),
|
|
24
|
+
env: path.join(HOME_DIR, '.hermes', '.env'),
|
|
25
|
+
skills: path.join(HOME_DIR, '.hermes', 'skills'),
|
|
26
|
+
memories: path.join(HOME_DIR, '.hermes', 'memories'),
|
|
27
|
+
memory: path.join(HOME_DIR, '.hermes', 'memories', 'MEMORY.md'),
|
|
28
|
+
user: path.join(HOME_DIR, '.hermes', 'memories', 'USER.md'),
|
|
29
|
+
logs: path.join(HOME_DIR, '.hermes', 'logs')
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const NEOAGENT_SKILLS_DIR = path.join(AGENT_DATA_DIR, 'skills');
|
|
33
|
+
const NEOAGENT_MEMORY_DIR = path.join(AGENT_DATA_DIR, 'memory');
|
|
34
|
+
|
|
35
|
+
const API_KEY_NAMES = [
|
|
36
|
+
'ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'XAI_API_KEY', 'GOOGLE_AI_KEY',
|
|
37
|
+
'MINIMAX_API_KEY', 'BRAVE_SEARCH_API_KEY', 'DEEPGRAM_API_KEY',
|
|
38
|
+
'TELEGRAM_BOT_TOKEN', 'OPENROUTER_API_KEY', 'ELEVENLABS_API_KEY',
|
|
39
|
+
'VOICE_TOOLS_OPENAI_KEY', 'SLACK_BOT_TOKEN', 'DISCORD_BOT_TOKEN'
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
function detectSourceAgents() {
|
|
43
|
+
const detected = { openclaw: false, hermes: false };
|
|
44
|
+
|
|
45
|
+
if (fs.existsSync(OPENCLAW_PATHS.config) || fs.existsSync(OPENCLAW_PATHS.legacyConfig)) {
|
|
46
|
+
detected.openclaw = true;
|
|
47
|
+
}
|
|
48
|
+
if (fs.existsSync(HERMES_PATHS.config) || fs.existsSync(HERMES_PATHS.env)) {
|
|
49
|
+
detected.hermes = true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return detected;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function scanOpenClaw() {
|
|
56
|
+
const scan = { skills: [], memories: [], apiKeys: {}, connections: [] };
|
|
57
|
+
|
|
58
|
+
if (fs.existsSync(OPENCLAW_PATHS.skills)) {
|
|
59
|
+
const files = fs.readdirSync(OPENCLAW_PATHS.skills).filter(f => f.endsWith('.md'));
|
|
60
|
+
scan.skills = files.map(f => ({ name: f.replace('.md', ''), path: path.join(OPENCLAW_PATHS.skills, f) }));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const [key, filePath] of Object.entries({
|
|
64
|
+
soul: OPENCLAW_PATHS.soul,
|
|
65
|
+
memory: OPENCLAW_PATHS.memory,
|
|
66
|
+
user: OPENCLAW_PATHS.user
|
|
67
|
+
})) {
|
|
68
|
+
if (fs.existsSync(filePath)) {
|
|
69
|
+
scan.memories.push({ type: key, path: filePath });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (fs.existsSync(OPENCLAW_PATHS.env)) {
|
|
74
|
+
const envMap = parseEnv(fs.readFileSync(OPENCLAW_PATHS.env, 'utf8'));
|
|
75
|
+
for (const [key, value] of envMap.entries()) {
|
|
76
|
+
if (API_KEY_NAMES.includes(key) && value) {
|
|
77
|
+
scan.apiKeys[key] = value;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return scan;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function scanHermes() {
|
|
86
|
+
const scan = { skills: [], memories: [], apiKeys: {}, connections: [] };
|
|
87
|
+
|
|
88
|
+
if (fs.existsSync(HERMES_PATHS.skills)) {
|
|
89
|
+
const files = fs.readdirSync(HERMES_PATHS.skills).filter(f => f.endsWith('.md'));
|
|
90
|
+
scan.skills = files.map(f => ({ name: f.replace('.md', ''), path: path.join(HERMES_PATHS.skills, f) }));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const [key, filePath] of Object.entries({
|
|
94
|
+
memory: HERMES_PATHS.memory,
|
|
95
|
+
user: HERMES_PATHS.user
|
|
96
|
+
})) {
|
|
97
|
+
if (fs.existsSync(filePath)) {
|
|
98
|
+
scan.memories.push({ type: key, path: filePath });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (fs.existsSync(HERMES_PATHS.env)) {
|
|
103
|
+
const envMap = parseEnv(fs.readFileSync(HERMES_PATHS.env, 'utf8'));
|
|
104
|
+
for (const [key, value] of envMap.entries()) {
|
|
105
|
+
if (API_KEY_NAMES.includes(key) && value) {
|
|
106
|
+
scan.apiKeys[key] = value;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return scan;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function copyFileIfExists(src, dest) {
|
|
115
|
+
if (!fs.existsSync(src)) return false;
|
|
116
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
117
|
+
fs.copyFileSync(src, dest);
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function copyDirContents(src, dest, extensions = ['.md']) {
|
|
122
|
+
if (!fs.existsSync(src)) return [];
|
|
123
|
+
const copied = [];
|
|
124
|
+
const files = fs.readdirSync(src);
|
|
125
|
+
for (const file of files) {
|
|
126
|
+
if (extensions && !extensions.some(ext => file.endsWith(ext))) continue;
|
|
127
|
+
const srcPath = path.join(src, file);
|
|
128
|
+
const destPath = path.join(dest, file);
|
|
129
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
130
|
+
fs.copyFileSync(srcPath, destPath);
|
|
131
|
+
copied.push(file);
|
|
132
|
+
}
|
|
133
|
+
return copied;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function ask(question, defaultValue = '') {
|
|
137
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
138
|
+
return new Promise((resolve) => {
|
|
139
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : '';
|
|
140
|
+
rl.question(` ? ${question}${suffix}: `, (answer) => {
|
|
141
|
+
rl.close();
|
|
142
|
+
resolve(answer.trim() || defaultValue);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function askChoice(question, options) {
|
|
148
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
149
|
+
return new Promise((resolve) => {
|
|
150
|
+
console.log(` ? ${question}`);
|
|
151
|
+
options.forEach((opt, i) => console.log(` [${i + 1}] ${opt}`));
|
|
152
|
+
rl.question(' Choice: ', (answer) => {
|
|
153
|
+
rl.close();
|
|
154
|
+
const idx = parseInt(answer, 10) - 1;
|
|
155
|
+
if (idx >= 0 && idx < options.length) {
|
|
156
|
+
resolve(options[idx]);
|
|
157
|
+
} else {
|
|
158
|
+
resolve(options[0]);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function askOverwriteKey(key, existingSource, newSource) {
|
|
165
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
166
|
+
return new Promise((resolve) => {
|
|
167
|
+
console.log(` ⚠️ Conflict: ${key}`);
|
|
168
|
+
console.log(` Existing in: ${existingSource}`);
|
|
169
|
+
console.log(` Incoming from: ${newSource}`);
|
|
170
|
+
console.log(' [1] Keep existing');
|
|
171
|
+
console.log(' [2] Overwrite with new');
|
|
172
|
+
console.log(' [3] Skip this key');
|
|
173
|
+
rl.question(' Choice [1]: ', (answer) => {
|
|
174
|
+
rl.close();
|
|
175
|
+
const choice = answer.trim() || '1';
|
|
176
|
+
if (choice === '2') resolve('overwrite');
|
|
177
|
+
else if (choice === '3') resolve('skip');
|
|
178
|
+
else resolve('keep');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function migrateOpenClaw(scan, options = {}) {
|
|
184
|
+
const { dryRun = false, conflictStrategy = 'ask' } = options;
|
|
185
|
+
const results = { skills: [], memories: [], apiKeys: {}, connections: [], errors: [] };
|
|
186
|
+
|
|
187
|
+
const skillsDest = path.join(NEOAGENT_SKILLS_DIR, 'openclaw-imports');
|
|
188
|
+
if (!dryRun) fs.mkdirSync(skillsDest, { recursive: true });
|
|
189
|
+
results.skills = copyDirContents(OPENCLAW_PATHS.skills, skillsDest, ['.md']);
|
|
190
|
+
if (results.skills.length > 0) {
|
|
191
|
+
console.log(` → Copied ${results.skills.length} skills to openclaw-imports/`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const memoryDest = path.join(NEOAGENT_MEMORY_DIR, 'openclaw');
|
|
195
|
+
if (!dryRun) fs.mkdirSync(memoryDest, { recursive: true });
|
|
196
|
+
for (const mem of scan.memories) {
|
|
197
|
+
const destPath = path.join(memoryDest, path.basename(mem.path));
|
|
198
|
+
if (!dryRun) {
|
|
199
|
+
copyFileIfExists(mem.path, destPath);
|
|
200
|
+
}
|
|
201
|
+
results.memories.push(mem.type);
|
|
202
|
+
}
|
|
203
|
+
if (results.memories.length > 0) {
|
|
204
|
+
console.log(` → Copied ${results.memories.length} memory files`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return results;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function migrateHermes(scan, options = {}) {
|
|
211
|
+
const { dryRun = false } = options;
|
|
212
|
+
const results = { skills: [], memories: [], apiKeys: {}, connections: [], errors: [] };
|
|
213
|
+
|
|
214
|
+
const skillsDest = path.join(NEOAGENT_SKILLS_DIR, 'hermes-imports');
|
|
215
|
+
if (!dryRun) fs.mkdirSync(skillsDest, { recursive: true });
|
|
216
|
+
results.skills = copyDirContents(HERMES_PATHS.skills, skillsDest, ['.md']);
|
|
217
|
+
if (results.skills.length > 0) {
|
|
218
|
+
console.log(` → Copied ${results.skills.length} skills to hermes-imports/`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const memoryDest = path.join(NEOAGENT_MEMORY_DIR, 'hermes');
|
|
222
|
+
if (!dryRun) fs.mkdirSync(memoryDest, { recursive: true });
|
|
223
|
+
for (const mem of scan.memories) {
|
|
224
|
+
const destPath = path.join(memoryDest, path.basename(mem.path));
|
|
225
|
+
if (!dryRun) {
|
|
226
|
+
copyFileIfExists(mem.path, destPath);
|
|
227
|
+
}
|
|
228
|
+
results.memories.push(mem.type);
|
|
229
|
+
}
|
|
230
|
+
if (results.memories.length > 0) {
|
|
231
|
+
console.log(` → Copied ${results.memories.length} memory files`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return results;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function mergeApiKeys(sources, options = {}) {
|
|
238
|
+
const { conflictStrategy = 'ask' } = options;
|
|
239
|
+
const results = {};
|
|
240
|
+
const conflicts = [];
|
|
241
|
+
const currentEnv = fs.existsSync(ENV_FILE)
|
|
242
|
+
? parseEnv(fs.readFileSync(ENV_FILE, 'utf8'))
|
|
243
|
+
: new Map();
|
|
244
|
+
|
|
245
|
+
for (const [source, apiKeys] of Object.entries(sources)) {
|
|
246
|
+
for (const [key, value] of Object.entries(apiKeys)) {
|
|
247
|
+
if (!value) continue;
|
|
248
|
+
const existingValue = currentEnv.get ? currentEnv.get(key) : currentEnv[key];
|
|
249
|
+
if (existingValue && existingValue !== value) {
|
|
250
|
+
conflicts.push({ key, existingValue, newValue: value, source });
|
|
251
|
+
} else {
|
|
252
|
+
results[key] = value;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
for (const conflict of conflicts) {
|
|
258
|
+
let resolution;
|
|
259
|
+
if (conflictStrategy === 'overwrite') {
|
|
260
|
+
resolution = 'overwrite';
|
|
261
|
+
} else if (conflictStrategy === 'skip') {
|
|
262
|
+
resolution = 'skip';
|
|
263
|
+
} else {
|
|
264
|
+
resolution = await askOverwriteKey(conflict.key, 'neoagent', conflict.source);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (resolution === 'overwrite') {
|
|
268
|
+
results[conflict.key] = conflict.newValue;
|
|
269
|
+
} else if (resolution === 'skip') {
|
|
270
|
+
// keep existing, do nothing
|
|
271
|
+
} else {
|
|
272
|
+
results[conflict.key] = conflict.existingValue;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return results;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function writeMergedApiKeys(apiKeysToWrite) {
|
|
280
|
+
if (Object.keys(apiKeysToWrite).length === 0) return;
|
|
281
|
+
|
|
282
|
+
const currentLines = fs.existsSync(ENV_FILE)
|
|
283
|
+
? fs.readFileSync(ENV_FILE, 'utf8').split('\n')
|
|
284
|
+
: [];
|
|
285
|
+
|
|
286
|
+
const existingKeys = new Set();
|
|
287
|
+
const newLines = [];
|
|
288
|
+
|
|
289
|
+
for (const line of currentLines) {
|
|
290
|
+
const match = line.match(/^([A-Z_]+)=/);
|
|
291
|
+
if (match && API_KEY_NAMES.includes(match[1])) {
|
|
292
|
+
if (apiKeysToWrite[match[1]] !== undefined) {
|
|
293
|
+
newLines.push(`${match[1]}=${apiKeysToWrite[match[1]]}`);
|
|
294
|
+
existingKeys.add(match[1]);
|
|
295
|
+
delete apiKeysToWrite[match[1]];
|
|
296
|
+
} else {
|
|
297
|
+
newLines.push(line);
|
|
298
|
+
}
|
|
299
|
+
} else {
|
|
300
|
+
newLines.push(line);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
for (const [key, value] of Object.entries(apiKeysToWrite)) {
|
|
305
|
+
if (value) {
|
|
306
|
+
newLines.push(`${key}=${value}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
fs.writeFileSync(ENV_FILE, newLines.join('\n') + '\n', { mode: 0o600 });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function cmdMigrateDryRun(sources) {
|
|
314
|
+
console.log('\n=== Migration Dry Run ===\n');
|
|
315
|
+
|
|
316
|
+
if (sources.openclaw) {
|
|
317
|
+
console.log('OpenClaw detection: FOUND');
|
|
318
|
+
const scan = scanOpenClaw();
|
|
319
|
+
console.log(` Skills: ${scan.skills.length}`);
|
|
320
|
+
console.log(` Memories: ${scan.memories.length}`);
|
|
321
|
+
console.log(` API keys: ${Object.keys(scan.apiKeys).join(', ') || 'none'}`);
|
|
322
|
+
console.log(` Config: ${OPENCLAW_PATHS.config}`);
|
|
323
|
+
} else {
|
|
324
|
+
console.log('OpenClaw detection: NOT FOUND');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
console.log();
|
|
328
|
+
|
|
329
|
+
if (sources.hermes) {
|
|
330
|
+
console.log('Hermes detection: FOUND');
|
|
331
|
+
const scan = scanHermes();
|
|
332
|
+
console.log(` Skills: ${scan.skills.length}`);
|
|
333
|
+
console.log(` Memories: ${scan.memories.length}`);
|
|
334
|
+
console.log(` API keys: ${Object.keys(scan.apiKeys).join(', ') || 'none'}`);
|
|
335
|
+
console.log(` Config: ${HERMES_PATHS.config}`);
|
|
336
|
+
} else {
|
|
337
|
+
console.log('Hermes detection: NOT FOUND');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
console.log('\nWould migrate to:');
|
|
341
|
+
console.log(` Skills → ${NEOAGENT_SKILLS_DIR}`);
|
|
342
|
+
console.log(` Memories → ${NEOAGENT_MEMORY_DIR}`);
|
|
343
|
+
console.log(` API keys → ${ENV_FILE}`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function cmdMigrateRun(sources, options = {}) {
|
|
347
|
+
const { apiKeyStrategy = 'ask' } = options;
|
|
348
|
+
|
|
349
|
+
console.log('\n=== NeoAgent Migration ===\n');
|
|
350
|
+
|
|
351
|
+
const openclawScan = sources.openclaw ? scanOpenClaw() : null;
|
|
352
|
+
const hermesScan = sources.hermes ? scanHermes() : null;
|
|
353
|
+
|
|
354
|
+
console.log('Scanning sources...');
|
|
355
|
+
if (openclawScan) console.log(` OpenClaw: ${openclawScan.skills.length} skills, ${openclawScan.memories.length} memories, ${Object.keys(openclawScan.apiKeys).length} API keys`);
|
|
356
|
+
if (hermesScan) console.log(` Hermes: ${hermesScan.skills.length} skills, ${hermesScan.memories.length} memories, ${Object.keys(hermesScan.apiKeys).length} API keys`);
|
|
357
|
+
|
|
358
|
+
const allApiKeys = {};
|
|
359
|
+
if (openclawScan) Object.assign(allApiKeys, openclawScan.apiKeys);
|
|
360
|
+
if (hermesScan) Object.assign(allApiKeys, hermesScan.apiKeys);
|
|
361
|
+
|
|
362
|
+
console.log('\nMigrating skills and memories...');
|
|
363
|
+
const migratePromises = [];
|
|
364
|
+
if (sources.openclaw && openclawScan) {
|
|
365
|
+
migratePromises.push(migrateOpenClaw(openclawScan, options));
|
|
366
|
+
}
|
|
367
|
+
if (sources.hermes && hermesScan) {
|
|
368
|
+
migratePromises.push(migrateHermes(hermesScan, options));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const results = await Promise.all(migratePromises);
|
|
372
|
+
|
|
373
|
+
if (Object.keys(allApiKeys).length > 0) {
|
|
374
|
+
console.log('\nMerging API keys...');
|
|
375
|
+
const merged = await mergeApiKeys(
|
|
376
|
+
{ openclaw: openclawScan?.apiKeys || {}, hermes: hermesScan?.apiKeys || {} },
|
|
377
|
+
{ conflictStrategy: apiKeyStrategy }
|
|
378
|
+
);
|
|
379
|
+
writeMergedApiKeys(merged);
|
|
380
|
+
const addedCount = Object.keys(merged).filter(k => allApiKeys[k]).length;
|
|
381
|
+
console.log(` → Merged ${addedCount} API keys`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
console.log('\n=== Migration Complete ===\n');
|
|
385
|
+
console.log('Skills migrated to:');
|
|
386
|
+
if (sources.openclaw) console.log(` openclaw-imports/`);
|
|
387
|
+
if (sources.hermes) console.log(` hermes-imports/`);
|
|
388
|
+
console.log('\nMemories migrated to:');
|
|
389
|
+
if (sources.openclaw) console.log(` memory/openclaw/`);
|
|
390
|
+
if (sources.hermes) console.log(` memory/hermes/`);
|
|
391
|
+
console.log('\nRun `neoagent status` to verify the installation.');
|
|
392
|
+
console.log('Run `neoagent start` to start the server.\n');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
module.exports = {
|
|
396
|
+
detectSourceAgents,
|
|
397
|
+
scanOpenClaw,
|
|
398
|
+
scanHermes,
|
|
399
|
+
migrateOpenClaw,
|
|
400
|
+
migrateHermes,
|
|
401
|
+
mergeApiKeys,
|
|
402
|
+
writeMergedApiKeys,
|
|
403
|
+
cmdMigrateDryRun,
|
|
404
|
+
cmdMigrateRun,
|
|
405
|
+
NEOAGENT_SKILLS_DIR,
|
|
406
|
+
NEOAGENT_MEMORY_DIR,
|
|
407
|
+
OPENCLAW_PATHS,
|
|
408
|
+
HERMES_PATHS
|
|
409
|
+
};
|
package/package.json
CHANGED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-migration
|
|
3
|
+
description: Guides users through migrating from OpenClaw or Hermes to NeoAgent. Run this skill when a user wants to migrate their existing agent setup. Provides step-by-step guidance, dry-run previews, and handles API key conflicts interactively.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
metadata:
|
|
6
|
+
neoagent:
|
|
7
|
+
tags: [migration, openclaw, hermes, setup, import]
|
|
8
|
+
related_skills: []
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Agent Migration Guide
|
|
12
|
+
|
|
13
|
+
Guides users through migrating their existing OpenClaw or Hermes agent setup to NeoAgent.
|
|
14
|
+
|
|
15
|
+
## When to Use This Skill
|
|
16
|
+
|
|
17
|
+
Use this skill when:
|
|
18
|
+
- A user says they're migrating from OpenClaw or Hermes
|
|
19
|
+
- A user wants to import their existing skills, memories, or API keys
|
|
20
|
+
- A user runs `neoagent migrate` and needs guidance
|
|
21
|
+
|
|
22
|
+
## How Migration Works
|
|
23
|
+
|
|
24
|
+
NeoAgent's migration system automatically detects existing OpenClaw (`~/.openclaw/`) and Hermes (`~/.hermes/`) installations and migrates:
|
|
25
|
+
|
|
26
|
+
| Data | Source | Destination |
|
|
27
|
+
|------|--------|-------------|
|
|
28
|
+
| **Skills** | `skills/*.md` in source | `~/.neoagent/agent-data/skills/[source]-imports/` |
|
|
29
|
+
| **Memories** | `SOUL.md`, `MEMORY.md`, `USER.md` | `~/.neoagent/agent-data/memory/[source]/` |
|
|
30
|
+
| **API Keys** | `.env` files | `~/.neoagent/.env` (merged with conflict prompts) |
|
|
31
|
+
|
|
32
|
+
## Step-by-Step Migration Flow
|
|
33
|
+
|
|
34
|
+
### Step 1: Check Migration Status
|
|
35
|
+
|
|
36
|
+
Ask the user to run:
|
|
37
|
+
```
|
|
38
|
+
neoagent migrate status
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
This will show which source agents are detected.
|
|
42
|
+
|
|
43
|
+
### Step 2: Preview (Optional - Dry Run)
|
|
44
|
+
|
|
45
|
+
Ask the user to run:
|
|
46
|
+
```
|
|
47
|
+
neoagent migrate dry-run
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
This shows exactly what would be migrated without making any changes.
|
|
51
|
+
|
|
52
|
+
### Step 3: Run Full Migration
|
|
53
|
+
|
|
54
|
+
Ask the user to run:
|
|
55
|
+
```
|
|
56
|
+
neoagent migrate
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The migration will:
|
|
60
|
+
1. Scan both sources (if present)
|
|
61
|
+
2. Copy skills to import directories
|
|
62
|
+
3. Copy memory files
|
|
63
|
+
4. Merge API keys with interactive conflict resolution
|
|
64
|
+
|
|
65
|
+
### Step 4: Verify Migration
|
|
66
|
+
|
|
67
|
+
After migration completes, advise the user to:
|
|
68
|
+
```
|
|
69
|
+
neoagent status # Check server is running
|
|
70
|
+
neoagent start # Start the server if not running
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Imported skills appear in:
|
|
74
|
+
- `~/.neoagent/agent-data/skills/openclaw-imports/`
|
|
75
|
+
- `~/.neoagent/agent-data/skills/hermes-imports/`
|
|
76
|
+
|
|
77
|
+
Imported memories appear in:
|
|
78
|
+
- `~/.neoagent/agent-data/memory/openclaw/`
|
|
79
|
+
- `~/.neoagent/agent-data/memory/hermes/`
|
|
80
|
+
|
|
81
|
+
## API Key Conflict Resolution
|
|
82
|
+
|
|
83
|
+
During migration, if an API key exists in multiple sources (including an existing NeoAgent config), the user will be prompted:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
⚠️ Conflict: OPENAI_API_KEY
|
|
87
|
+
Existing in: neoagent
|
|
88
|
+
Incoming from: openclaw
|
|
89
|
+
[1] Keep existing (neoagent)
|
|
90
|
+
[2] Overwrite with new (openclaw)
|
|
91
|
+
[3] Skip this key
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Guide the user to choose based on their needs:
|
|
95
|
+
- **Keep existing**: If they want to preserve the current NeoAgent configuration
|
|
96
|
+
- **Overwrite**: If the incoming key is the one they want to use
|
|
97
|
+
- **Skip**: If they want to deal with it manually later
|
|
98
|
+
|
|
99
|
+
## Troubleshooting
|
|
100
|
+
|
|
101
|
+
### "No OpenClaw or Hermes installation detected"
|
|
102
|
+
|
|
103
|
+
**Cause**: The source directories don't exist at default locations.
|
|
104
|
+
|
|
105
|
+
**Solution**:
|
|
106
|
+
1. Check if the installation is at a custom path
|
|
107
|
+
2. Manually copy data:
|
|
108
|
+
- Skills: Copy `*.md` files to `~/.neoagent/agent-data/skills/`
|
|
109
|
+
- Memories: Copy to `~/.neoagent/agent-data/memory/`
|
|
110
|
+
- API keys: Edit `~/.neoagent/.env` manually
|
|
111
|
+
|
|
112
|
+
### "Permission denied" errors
|
|
113
|
+
|
|
114
|
+
**Cause**: Missing read permissions on source or write permissions on target.
|
|
115
|
+
|
|
116
|
+
**Solution**: Ensure the user has appropriate permissions:
|
|
117
|
+
```bash
|
|
118
|
+
chmod 755 ~/.openclaw
|
|
119
|
+
chmod 755 ~/.hermes
|
|
120
|
+
chmod 755 ~/.neoagent
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Migration seems incomplete
|
|
124
|
+
|
|
125
|
+
**Cause**: Some files may have already existed and were skipped (migration is idempotent).
|
|
126
|
+
|
|
127
|
+
**Solution**: Re-run migration - it won't overwrite existing files.
|
|
128
|
+
|
|
129
|
+
## Manual Migration (If Automated Fails)
|
|
130
|
+
|
|
131
|
+
If the automated migration doesn't work:
|
|
132
|
+
|
|
133
|
+
### For Skills
|
|
134
|
+
```bash
|
|
135
|
+
# Create import directory
|
|
136
|
+
mkdir -p ~/.neoagent/agent-data/skills/openclaw-imports
|
|
137
|
+
|
|
138
|
+
# Copy all .md files
|
|
139
|
+
cp ~/.openclaw/skills/*.md ~/.neoagent/agent-data/skills/openclaw-imports/
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### For Memories
|
|
143
|
+
```bash
|
|
144
|
+
# Create source directory
|
|
145
|
+
mkdir -p ~/.neoagent/agent-data/memory/openclaw
|
|
146
|
+
|
|
147
|
+
# Copy memory files
|
|
148
|
+
cp ~/.openclaw/workspace/SOUL.md ~/.neoagent/agent-data/memory/openclaw/
|
|
149
|
+
cp ~/.openclaw/workspace/MEMORY.md ~/.neoagent/agent-data/memory/openclaw/
|
|
150
|
+
cp ~/.openclaw/workspace/USER.md ~/.neoagent/agent-data/memory/openclaw/
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### For API Keys
|
|
154
|
+
```bash
|
|
155
|
+
# Edit the .env file
|
|
156
|
+
nano ~/.neoagent/.env
|
|
157
|
+
|
|
158
|
+
# Add keys from source, e.g.:
|
|
159
|
+
# OPENAI_API_KEY=sk-...
|
|
160
|
+
# ANTHROPIC_API_KEY=sk-ant-...
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Post-Migration Checklist
|
|
164
|
+
|
|
165
|
+
After successful migration, advise the user to:
|
|
166
|
+
|
|
167
|
+
- [ ] Run `neoagent status` to verify server is healthy
|
|
168
|
+
- [ ] Run `neoagent start` if server isn't running
|
|
169
|
+
- [ ] Review imported skills in `~/.neoagent/agent-data/skills/`
|
|
170
|
+
- [ ] Review imported memories in `~/.neoagent/agent-data/memory/`
|
|
171
|
+
- [ ] Verify API keys are correctly set with `neoagent env list`
|
|
172
|
+
- [ ] Test the agent by sending a message in the UI
|
|
173
|
+
- [ ] Check logs if anything seems wrong: `neoagent logs`
|
|
Binary file
|
|
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"59aa584fdf100e6c78c785d8a5b565d1de4b48
|
|
|
37
37
|
|
|
38
38
|
_flutter.loader.load({
|
|
39
39
|
serviceWorkerSettings: {
|
|
40
|
-
serviceWorkerVersion: "
|
|
40
|
+
serviceWorkerVersion: "282358507" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
|
|
41
41
|
}
|
|
42
42
|
});
|