clawkeep 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/LICENSE +21 -0
- package/README.md +295 -0
- package/bin/clawkeep.js +145 -0
- package/package.json +59 -0
- package/src/commands/backup.js +276 -0
- package/src/commands/diff.js +44 -0
- package/src/commands/export.js +55 -0
- package/src/commands/import.js +39 -0
- package/src/commands/init.js +46 -0
- package/src/commands/log.js +77 -0
- package/src/commands/pull.js +34 -0
- package/src/commands/push.js +45 -0
- package/src/commands/restore.js +42 -0
- package/src/commands/snap.js +52 -0
- package/src/commands/status.js +84 -0
- package/src/commands/ui.js +334 -0
- package/src/commands/watch.js +180 -0
- package/src/core/backup.js +263 -0
- package/src/core/crypto.js +104 -0
- package/src/core/detect.js +128 -0
- package/src/core/git.js +426 -0
- package/src/core/sync-crypto.js +90 -0
- package/src/core/sync.js +448 -0
- package/src/core/transport.js +102 -0
- package/src/index.js +10 -0
- package/ui/app.js +1191 -0
- package/ui/index.html +78 -0
- package/ui/style.css +386 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const { hashPassword, verifyPassword } = require('./sync-crypto');
|
|
7
|
+
const { createTransport, LocalTransport } = require('./transport');
|
|
8
|
+
const SyncManager = require('./sync');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* BackupManager — handles syncing version history to backup targets.
|
|
12
|
+
* Supports encrypted incremental chunk-based sync (local) and native git push.
|
|
13
|
+
*/
|
|
14
|
+
class BackupManager {
|
|
15
|
+
constructor(clawGit) {
|
|
16
|
+
this.claw = clawGit;
|
|
17
|
+
this.dir = clawGit.dir;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Get current backup config + status */
|
|
21
|
+
getConfig() {
|
|
22
|
+
const config = this.claw.loadConfig();
|
|
23
|
+
const backup = config.backup || {};
|
|
24
|
+
const target = backup.target || null;
|
|
25
|
+
|
|
26
|
+
let targetLabel = 'Not configured';
|
|
27
|
+
if (target === 'local' && backup.local?.path) {
|
|
28
|
+
targetLabel = backup.local.path;
|
|
29
|
+
} else if (target === 'cloud') {
|
|
30
|
+
targetLabel = 'ClawKeep Cloud';
|
|
31
|
+
} else if (target === 's3' && backup.s3?.bucket) {
|
|
32
|
+
targetLabel = `s3://${backup.s3.bucket}/${backup.s3.prefix || ''}`;
|
|
33
|
+
} else if (target === 'git') {
|
|
34
|
+
targetLabel = config.remote || 'git remote';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
target,
|
|
39
|
+
targetLabel,
|
|
40
|
+
autoSync: backup.autoSync || false,
|
|
41
|
+
lastSync: backup.lastSync || null,
|
|
42
|
+
local: backup.local || null,
|
|
43
|
+
cloud: backup.cloud || null,
|
|
44
|
+
s3: backup.s3 || null,
|
|
45
|
+
passwordSet: !!backup.passwordHash,
|
|
46
|
+
workspaceId: backup.workspaceId || null,
|
|
47
|
+
chunkCount: backup.chunkCount || 0,
|
|
48
|
+
lastSyncCommit: backup.lastSyncCommit || null,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Set encryption password (stores hash only) */
|
|
53
|
+
setPassword(password) {
|
|
54
|
+
if (!password) throw new Error('Password is required');
|
|
55
|
+
const config = this.claw.loadConfig();
|
|
56
|
+
if (!config.backup) config.backup = {};
|
|
57
|
+
config.backup.passwordHash = hashPassword(password);
|
|
58
|
+
this.claw.saveConfig(config);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Check if password is set */
|
|
62
|
+
hasPassword() {
|
|
63
|
+
const config = this.claw.loadConfig();
|
|
64
|
+
return !!(config.backup?.passwordHash);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Verify a password against stored hash */
|
|
68
|
+
checkPassword(password) {
|
|
69
|
+
const config = this.claw.loadConfig();
|
|
70
|
+
const hash = config.backup?.passwordHash;
|
|
71
|
+
if (!hash) return false;
|
|
72
|
+
return verifyPassword(password, hash);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Set backup target */
|
|
76
|
+
async setTarget(type, options = {}) {
|
|
77
|
+
const config = this.claw.loadConfig();
|
|
78
|
+
if (!config.backup) {
|
|
79
|
+
config.backup = {
|
|
80
|
+
target: null,
|
|
81
|
+
local: { path: null },
|
|
82
|
+
cloud: { token: null, endpoint: 'https://api.clawkeep.com' },
|
|
83
|
+
s3: { bucket: null, prefix: null, region: null },
|
|
84
|
+
autoSync: true,
|
|
85
|
+
lastSync: null,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
config.backup.target = type;
|
|
90
|
+
|
|
91
|
+
if (type === 'local' && options.path) {
|
|
92
|
+
const absPath = path.resolve(options.path);
|
|
93
|
+
config.backup.local = { path: absPath };
|
|
94
|
+
// Ensure directory exists
|
|
95
|
+
if (!fs.existsSync(absPath)) {
|
|
96
|
+
fs.mkdirSync(absPath, { recursive: true });
|
|
97
|
+
}
|
|
98
|
+
// Generate workspace ID if not set
|
|
99
|
+
if (!config.backup.workspaceId) {
|
|
100
|
+
const dirname = path.basename(this.dir);
|
|
101
|
+
const suffix = crypto.randomBytes(4).toString('hex');
|
|
102
|
+
config.backup.workspaceId = dirname + '-' + suffix;
|
|
103
|
+
}
|
|
104
|
+
} else if (type === 'git' && options.url) {
|
|
105
|
+
config.remote = options.url;
|
|
106
|
+
await this.claw.setRemote(options.url);
|
|
107
|
+
} else if (type === 's3') {
|
|
108
|
+
config.backup.s3 = {
|
|
109
|
+
bucket: options.bucket || null,
|
|
110
|
+
prefix: options.prefix || null,
|
|
111
|
+
region: options.region || null,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.claw.saveConfig(config);
|
|
116
|
+
return this.getConfig();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Sync to backup target */
|
|
120
|
+
async sync(password) {
|
|
121
|
+
const config = this.claw.loadConfig();
|
|
122
|
+
const backup = config.backup || {};
|
|
123
|
+
const target = backup.target;
|
|
124
|
+
|
|
125
|
+
if (!target) throw new Error('No backup target configured');
|
|
126
|
+
|
|
127
|
+
let result;
|
|
128
|
+
if (target === 'local') {
|
|
129
|
+
// Encrypted incremental sync
|
|
130
|
+
if (!password) throw new Error('Password required for encrypted sync');
|
|
131
|
+
const transport = createTransport(backup, this.claw);
|
|
132
|
+
const sm = new SyncManager(this.claw, transport, password);
|
|
133
|
+
result = await sm.sync();
|
|
134
|
+
} else if (target === 'git') {
|
|
135
|
+
await this.claw.push();
|
|
136
|
+
result = { ok: true, target: 'git', synced: true };
|
|
137
|
+
} else if (target === 'cloud') {
|
|
138
|
+
throw new Error('ClawKeep Cloud is coming soon');
|
|
139
|
+
} else if (target === 's3') {
|
|
140
|
+
throw new Error('S3 backup is not yet implemented');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Reload config (SyncManager may have saved changes)
|
|
144
|
+
const freshConfig = this.claw.loadConfig();
|
|
145
|
+
freshConfig.backup.lastSync = new Date().toISOString();
|
|
146
|
+
this.claw.saveConfig(freshConfig);
|
|
147
|
+
|
|
148
|
+
return { ...result, lastSync: freshConfig.backup.lastSync };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Pull from backup target */
|
|
152
|
+
async pull() {
|
|
153
|
+
const config = this.claw.loadConfig();
|
|
154
|
+
const backup = config.backup || {};
|
|
155
|
+
const target = backup.target;
|
|
156
|
+
|
|
157
|
+
if (!target) throw new Error('No backup target configured');
|
|
158
|
+
|
|
159
|
+
if (target === 'git') {
|
|
160
|
+
await this.claw.pull();
|
|
161
|
+
return { ok: true };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
throw new Error(`Pull not supported for target: ${target} (use 'backup restore' instead)`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Test connection to backup target */
|
|
168
|
+
async test() {
|
|
169
|
+
const config = this.claw.loadConfig();
|
|
170
|
+
const backup = config.backup || {};
|
|
171
|
+
const target = backup.target;
|
|
172
|
+
|
|
173
|
+
if (!target) return { ok: false, message: 'No backup target configured' };
|
|
174
|
+
|
|
175
|
+
const start = Date.now();
|
|
176
|
+
|
|
177
|
+
if (target === 'local') {
|
|
178
|
+
const localPath = backup.local?.path;
|
|
179
|
+
if (!localPath) return { ok: false, message: 'No local path configured' };
|
|
180
|
+
if (!fs.existsSync(localPath)) return { ok: false, message: 'Path does not exist: ' + localPath };
|
|
181
|
+
// Check writable
|
|
182
|
+
try {
|
|
183
|
+
const testFile = path.join(localPath, '.clawkeep-test-' + Date.now());
|
|
184
|
+
fs.writeFileSync(testFile, 'test');
|
|
185
|
+
fs.unlinkSync(testFile);
|
|
186
|
+
} catch {
|
|
187
|
+
return { ok: false, message: 'Path is not writable: ' + localPath };
|
|
188
|
+
}
|
|
189
|
+
return { ok: true, message: 'Connected', latencyMs: Date.now() - start };
|
|
190
|
+
} else if (target === 'git') {
|
|
191
|
+
try {
|
|
192
|
+
await this.claw.git.listRemote(['--heads', 'origin']);
|
|
193
|
+
return { ok: true, message: 'Connected', latencyMs: Date.now() - start };
|
|
194
|
+
} catch (e) {
|
|
195
|
+
return { ok: false, message: 'Remote unreachable: ' + e.message };
|
|
196
|
+
}
|
|
197
|
+
} else if (target === 'cloud') {
|
|
198
|
+
return { ok: false, message: 'ClawKeep Cloud is coming soon' };
|
|
199
|
+
} else if (target === 's3') {
|
|
200
|
+
return { ok: false, message: 'S3 backup not yet implemented' };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return { ok: false, message: 'Unknown target: ' + target };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Compact chunks into single full bundle */
|
|
207
|
+
async compact(password) {
|
|
208
|
+
const config = this.claw.loadConfig();
|
|
209
|
+
const backup = config.backup || {};
|
|
210
|
+
if (backup.target !== 'local') throw new Error('Compact only supported for local target');
|
|
211
|
+
if (!password) throw new Error('Password required for compact');
|
|
212
|
+
|
|
213
|
+
const transport = createTransport(backup, this.claw);
|
|
214
|
+
const sm = new SyncManager(this.claw, transport, password);
|
|
215
|
+
return await sm.compact();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Get sync status (chunk count, sizes, etc.) */
|
|
219
|
+
async getSyncStatus(password) {
|
|
220
|
+
const config = this.claw.loadConfig();
|
|
221
|
+
const backup = config.backup || {};
|
|
222
|
+
if (backup.target !== 'local' || !password) {
|
|
223
|
+
return {
|
|
224
|
+
synced: false,
|
|
225
|
+
chunkCount: backup.chunkCount || 0,
|
|
226
|
+
lastSync: backup.lastSync || null,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
const transport = createTransport(backup, this.claw);
|
|
232
|
+
const sm = new SyncManager(this.claw, transport, password);
|
|
233
|
+
return await sm.getStatus();
|
|
234
|
+
} catch {
|
|
235
|
+
return {
|
|
236
|
+
synced: false,
|
|
237
|
+
chunkCount: backup.chunkCount || 0,
|
|
238
|
+
lastSync: backup.lastSync || null,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Restore from an encrypted backup directory.
|
|
245
|
+
* @param {string} sourcePath - Path to the workspace backup dir (contains manifest.enc + chunks)
|
|
246
|
+
* @param {string} destDir - Where to restore
|
|
247
|
+
* @param {string} password - Decryption password
|
|
248
|
+
*/
|
|
249
|
+
static async restoreFromBackup(sourcePath, destDir, password) {
|
|
250
|
+
if (!password) throw new Error('Password required for restore');
|
|
251
|
+
sourcePath = path.resolve(sourcePath);
|
|
252
|
+
if (!fs.existsSync(sourcePath)) throw new Error('Backup path does not exist: ' + sourcePath);
|
|
253
|
+
|
|
254
|
+
// Determine workspace ID from directory name
|
|
255
|
+
const workspaceId = path.basename(sourcePath);
|
|
256
|
+
const parentDir = path.dirname(sourcePath);
|
|
257
|
+
const transport = new LocalTransport(parentDir);
|
|
258
|
+
|
|
259
|
+
return await SyncManager.restoreFrom(transport, workspaceId, password, destDir);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
module.exports = BackupManager;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const { pipeline } = require('stream/promises');
|
|
6
|
+
const { createGzip, createGunzip } = require('zlib');
|
|
7
|
+
const tar = require('tar');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const ALGORITHM = 'aes-256-ctr';
|
|
11
|
+
const SALT_LENGTH = 16;
|
|
12
|
+
const IV_LENGTH = 16;
|
|
13
|
+
const KEY_LENGTH = 32;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Derive encryption key from password using scrypt.
|
|
17
|
+
*/
|
|
18
|
+
function deriveKey(password, salt) {
|
|
19
|
+
return crypto.scryptSync(password, salt, KEY_LENGTH);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Export a directory (including .git) to an encrypted archive.
|
|
24
|
+
* Format: [salt:16][iv:16][encrypted tar.gz data]
|
|
25
|
+
*/
|
|
26
|
+
async function exportEncrypted(dir, outputPath, password) {
|
|
27
|
+
dir = path.resolve(dir);
|
|
28
|
+
|
|
29
|
+
const salt = crypto.randomBytes(SALT_LENGTH);
|
|
30
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
31
|
+
const key = deriveKey(password, salt);
|
|
32
|
+
|
|
33
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
34
|
+
const output = fs.createWriteStream(outputPath);
|
|
35
|
+
|
|
36
|
+
// Write salt and IV as header
|
|
37
|
+
output.write(salt);
|
|
38
|
+
output.write(iv);
|
|
39
|
+
|
|
40
|
+
// Create tar.gz stream of the entire directory (including .git)
|
|
41
|
+
const tarStream = tar.create(
|
|
42
|
+
{
|
|
43
|
+
gzip: true,
|
|
44
|
+
cwd: path.dirname(dir),
|
|
45
|
+
filter: (entryPath) => {
|
|
46
|
+
// Skip node_modules to keep archives sane
|
|
47
|
+
return !entryPath.includes('node_modules');
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
[path.basename(dir)]
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// Pipe: tar.gz -> encrypt -> file
|
|
54
|
+
await pipeline(tarStream, cipher, output);
|
|
55
|
+
|
|
56
|
+
const stats = fs.statSync(outputPath);
|
|
57
|
+
return {
|
|
58
|
+
path: outputPath,
|
|
59
|
+
size: stats.size,
|
|
60
|
+
salt: salt.toString('hex'),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Import from an encrypted archive.
|
|
66
|
+
* Decrypts and extracts to the target directory.
|
|
67
|
+
*/
|
|
68
|
+
async function importEncrypted(archivePath, targetDir, password) {
|
|
69
|
+
targetDir = path.resolve(targetDir);
|
|
70
|
+
|
|
71
|
+
const input = fs.openSync(archivePath, 'r');
|
|
72
|
+
|
|
73
|
+
// Read salt and IV from header
|
|
74
|
+
const salt = Buffer.alloc(SALT_LENGTH);
|
|
75
|
+
const iv = Buffer.alloc(IV_LENGTH);
|
|
76
|
+
fs.readSync(input, salt, 0, SALT_LENGTH, 0);
|
|
77
|
+
fs.readSync(input, iv, 0, IV_LENGTH, SALT_LENGTH);
|
|
78
|
+
fs.closeSync(input);
|
|
79
|
+
|
|
80
|
+
const key = deriveKey(password, salt);
|
|
81
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
82
|
+
|
|
83
|
+
// Create a readable stream starting after the header
|
|
84
|
+
const encryptedStream = fs.createReadStream(archivePath, {
|
|
85
|
+
start: SALT_LENGTH + IV_LENGTH,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Ensure target dir exists
|
|
89
|
+
if (!fs.existsSync(targetDir)) {
|
|
90
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Pipe: file -> decrypt -> untar
|
|
94
|
+
const extractStream = tar.extract({
|
|
95
|
+
cwd: targetDir,
|
|
96
|
+
strip: 1, // Remove the top-level directory
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
await pipeline(encryptedStream, decipher, createGunzip(), extractStream);
|
|
100
|
+
|
|
101
|
+
return { path: targetDir };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = { exportEncrypted, importEncrypted };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Auto-detect which agent framework is in use.
|
|
8
|
+
* Returns framework info for config.
|
|
9
|
+
*/
|
|
10
|
+
function detectFramework(dir) {
|
|
11
|
+
dir = path.resolve(dir);
|
|
12
|
+
|
|
13
|
+
// Clawdbot — look for AGENTS.md, SOUL.md, or clawdbot config
|
|
14
|
+
if (
|
|
15
|
+
fs.existsSync(path.join(dir, 'AGENTS.md')) ||
|
|
16
|
+
fs.existsSync(path.join(dir, 'SOUL.md')) ||
|
|
17
|
+
fs.existsSync(path.join(dir, 'MEMORY.md'))
|
|
18
|
+
) {
|
|
19
|
+
const agentName = _extractAgentName(dir, 'clawdbot');
|
|
20
|
+
return {
|
|
21
|
+
framework: 'clawdbot',
|
|
22
|
+
agentName,
|
|
23
|
+
dataFiles: ['MEMORY.md', 'SOUL.md', 'AGENTS.md', 'USER.md', 'TOOLS.md', 'IDENTITY.md', 'HEARTBEAT.md'],
|
|
24
|
+
memoryDir: 'memory/',
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// OpenClaw — look for .openclaw directory or config
|
|
29
|
+
if (
|
|
30
|
+
fs.existsSync(path.join(dir, '.openclaw')) ||
|
|
31
|
+
fs.existsSync(path.join(dir, 'config.json'))
|
|
32
|
+
) {
|
|
33
|
+
const agentName = _extractAgentName(dir, 'openclaw');
|
|
34
|
+
return {
|
|
35
|
+
framework: 'openclaw',
|
|
36
|
+
agentName,
|
|
37
|
+
dataFiles: ['config.json', 'memory.md'],
|
|
38
|
+
memoryDir: 'framework/',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Nanobot — look for nanobot.yml or .nanobot
|
|
43
|
+
if (
|
|
44
|
+
fs.existsSync(path.join(dir, 'nanobot.yml')) ||
|
|
45
|
+
fs.existsSync(path.join(dir, '.nanobot'))
|
|
46
|
+
) {
|
|
47
|
+
const agentName = _extractAgentName(dir, 'nanobot');
|
|
48
|
+
return {
|
|
49
|
+
framework: 'nanobot',
|
|
50
|
+
agentName,
|
|
51
|
+
dataFiles: ['nanobot.yml'],
|
|
52
|
+
memoryDir: 'state/',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Claude Code / Anthropic — look for CLAUDE.md
|
|
57
|
+
if (fs.existsSync(path.join(dir, 'CLAUDE.md'))) {
|
|
58
|
+
return {
|
|
59
|
+
framework: 'claude-code',
|
|
60
|
+
agentName: 'claude',
|
|
61
|
+
dataFiles: ['CLAUDE.md'],
|
|
62
|
+
memoryDir: null,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Codex — look for AGENTS.md pattern (OpenAI)
|
|
67
|
+
if (fs.existsSync(path.join(dir, 'codex.md'))) {
|
|
68
|
+
return {
|
|
69
|
+
framework: 'codex',
|
|
70
|
+
agentName: 'codex',
|
|
71
|
+
dataFiles: ['codex.md'],
|
|
72
|
+
memoryDir: null,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Generic / unknown
|
|
77
|
+
return {
|
|
78
|
+
framework: 'generic',
|
|
79
|
+
agentName: path.basename(dir),
|
|
80
|
+
dataFiles: [],
|
|
81
|
+
memoryDir: null,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Try to extract the agent's name from config files */
|
|
86
|
+
function _extractAgentName(dir, framework) {
|
|
87
|
+
try {
|
|
88
|
+
if (framework === 'clawdbot') {
|
|
89
|
+
// Try IDENTITY.md
|
|
90
|
+
const identPath = path.join(dir, 'IDENTITY.md');
|
|
91
|
+
if (fs.existsSync(identPath)) {
|
|
92
|
+
const content = fs.readFileSync(identPath, 'utf8');
|
|
93
|
+
const match = content.match(/\*\*Name:\*\*\s*(.+)/);
|
|
94
|
+
if (match) return match[1].trim();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (framework === 'openclaw') {
|
|
99
|
+
const configPath = path.join(dir, '.openclaw', 'config.json');
|
|
100
|
+
if (fs.existsSync(configPath)) {
|
|
101
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
102
|
+
if (config.name) return config.name;
|
|
103
|
+
}
|
|
104
|
+
// Fallback: config.json in root
|
|
105
|
+
const rootConfig = path.join(dir, 'config.json');
|
|
106
|
+
if (fs.existsSync(rootConfig)) {
|
|
107
|
+
const config = JSON.parse(fs.readFileSync(rootConfig, 'utf8'));
|
|
108
|
+
if (config.name) return config.name;
|
|
109
|
+
if (config.agent_name) return config.agent_name;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (framework === 'nanobot') {
|
|
114
|
+
const nanobotPath = path.join(dir, 'nanobot.yml');
|
|
115
|
+
if (fs.existsSync(nanobotPath)) {
|
|
116
|
+
const content = fs.readFileSync(nanobotPath, 'utf8');
|
|
117
|
+
const match = content.match(/name:\s*(.+)/);
|
|
118
|
+
if (match) return match[1].trim();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} catch (e) {
|
|
122
|
+
// Ignore parse errors
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return path.basename(dir);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = { detectFramework };
|