@dambrogia/openclaw-agents-backup 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/src/backup.ts ADDED
@@ -0,0 +1,190 @@
1
+ import * as path from 'path';
2
+ import * as fs from 'fs';
3
+ import { BackupConfig, BackupResult, BackupChange, AgentArchiveMetadata } from './types';
4
+ import {
5
+ pathExists,
6
+ rsyncDirectory,
7
+ writeJsonFile,
8
+ ensureDirectoryExists,
9
+ executeCommand,
10
+ getCurrentTimestamp,
11
+ findAllFiles
12
+ } from './utils';
13
+ import { listAgents, validateAgentBinding } from './agentLister';
14
+ import { encryptFile } from './encryptionService';
15
+
16
+ /**
17
+ * Perform backup of all agents to the configured backup repository
18
+ */
19
+ export async function performBackup(workspacePath: string): Promise<BackupResult> {
20
+ try {
21
+ // Load backup configuration
22
+ const configPath = path.join(workspacePath, '.backupconfig.json');
23
+ if (!pathExists(configPath)) {
24
+ return {
25
+ success: false,
26
+ message: 'Backup config not found at .backupconfig.json',
27
+ agentsProcessed: 0,
28
+ changes: [],
29
+ error: 'Missing .backupconfig.json'
30
+ };
31
+ }
32
+
33
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8')) as BackupConfig;
34
+ const backupRepoPath = config.backupRepoPath;
35
+
36
+ if (!pathExists(backupRepoPath)) {
37
+ return {
38
+ success: false,
39
+ message: `Backup repository not found at ${backupRepoPath}`,
40
+ agentsProcessed: 0,
41
+ changes: [],
42
+ error: 'Backup repo not initialized'
43
+ };
44
+ }
45
+
46
+ // Get encryption password from environment
47
+ const encryptionPassword = process.env.BACKUP_ENCRYPTION_PASSWORD;
48
+ if (!encryptionPassword) {
49
+ return {
50
+ success: false,
51
+ message: 'Backup encryption password not set',
52
+ agentsProcessed: 0,
53
+ changes: [],
54
+ error: 'Missing BACKUP_ENCRYPTION_PASSWORD environment variable'
55
+ };
56
+ }
57
+
58
+ // Get list of agents
59
+ const agents = await listAgents();
60
+
61
+ // Validate agents
62
+ const validAgents = agents.filter((agent) => {
63
+ const valid = validateAgentBinding(agent);
64
+ if (!valid) {
65
+ console.warn(`Agent ${agent.id} failed validation, skipping`);
66
+ }
67
+ return valid;
68
+ });
69
+
70
+ const changes: BackupChange[] = [];
71
+ const archivesPath = path.join(backupRepoPath, 'archives');
72
+ ensureDirectoryExists(archivesPath);
73
+
74
+ // Back up each agent
75
+ for (const agent of validAgents) {
76
+ const agentArchivePath = path.join(archivesPath, agent.id);
77
+ ensureDirectoryExists(agentArchivePath);
78
+
79
+ const backupChange: BackupChange = {
80
+ agentId: agent.id,
81
+ workspaceChanged: false,
82
+ agentDirChanged: false
83
+ };
84
+
85
+ try {
86
+ // Write agent metadata
87
+ const metadata: AgentArchiveMetadata = {
88
+ id: agent.id,
89
+ identityName: agent.identityName,
90
+ identityEmoji: agent.identityEmoji,
91
+ identitySource: agent.identitySource,
92
+ workspace: agent.workspace,
93
+ agentDir: agent.agentDir,
94
+ model: agent.model,
95
+ bindings: agent.bindings,
96
+ isDefault: agent.isDefault,
97
+ routes: agent.routes,
98
+ backedUpAt: getCurrentTimestamp()
99
+ };
100
+ writeJsonFile(path.join(agentArchivePath, 'agent.json'), metadata);
101
+
102
+ // Sync workspace
103
+ const workspaceDestination = path.join(agentArchivePath, 'workspace');
104
+ ensureDirectoryExists(workspaceDestination);
105
+ backupChange.workspaceChanged = rsyncDirectory(agent.workspace, workspaceDestination);
106
+
107
+ // Sync agent directory (includes both agent/ and sessions/ directories)
108
+ // Source: ${agentDir}/.. to capture agent/ and sessions/ together
109
+ const agentDirParent = path.join(agent.agentDir, '..');
110
+ const agentDirDestination = path.join(agentArchivePath, 'agentDir');
111
+ ensureDirectoryExists(agentDirDestination);
112
+ backupChange.agentDirChanged = rsyncDirectory(agentDirParent, agentDirDestination);
113
+
114
+ // Encrypt all files in agentDir backup
115
+ const allFiles = findAllFiles(agentDirDestination);
116
+ for (const file of allFiles) {
117
+ // Skip already encrypted files
118
+ if (file.endsWith('.enc')) {
119
+ continue;
120
+ }
121
+
122
+ try {
123
+ const encryptedPath = `${file}.enc`;
124
+ encryptFile(file, encryptedPath, encryptionPassword);
125
+ // Delete plaintext after successful encryption
126
+ fs.unlinkSync(file);
127
+ } catch (error) {
128
+ backupChange.error = `Failed to encrypt ${file}: ${error}`;
129
+ throw error;
130
+ }
131
+ }
132
+
133
+ changes.push(backupChange);
134
+ } catch (error) {
135
+ backupChange.error = String(error);
136
+ changes.push(backupChange);
137
+ }
138
+ }
139
+
140
+ // Always commit since metadata (backedUpAt) is always updated
141
+ try {
142
+ await gitCommitBackup(backupRepoPath);
143
+ } catch (error) {
144
+ return {
145
+ success: false,
146
+ message: 'Backup completed but git commit failed',
147
+ agentsProcessed: validAgents.length,
148
+ changes,
149
+ error: String(error)
150
+ };
151
+ }
152
+
153
+ return {
154
+ success: true,
155
+ message: `Backed up ${validAgents.length} agents. Changes: ${changes.filter((c) => c.workspaceChanged || c.agentDirChanged).length}`,
156
+ agentsProcessed: validAgents.length,
157
+ changes
158
+ };
159
+ } catch (error) {
160
+ return {
161
+ success: false,
162
+ message: 'Backup failed',
163
+ agentsProcessed: 0,
164
+ changes: [],
165
+ error: String(error)
166
+ };
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Commit backup changes to git repository
172
+ */
173
+ async function gitCommitBackup(repoPath: string): Promise<void> {
174
+ const currentDir = process.cwd();
175
+ try {
176
+ process.chdir(repoPath);
177
+
178
+ // Stage all changes
179
+ executeCommand('git add -A');
180
+
181
+ // Create commit message
182
+ const timestamp = getCurrentTimestamp();
183
+ const commitMessage = `Backup: ${timestamp}`;
184
+
185
+ // Commit changes
186
+ executeCommand(`git commit -m "${commitMessage}"`);
187
+ } finally {
188
+ process.chdir(currentDir);
189
+ }
190
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env node
2
+
3
+ import * as path from 'path';
4
+ import * as fs from 'fs';
5
+ import { performBackup, performRestore } from './index';
6
+
7
+ /**
8
+ * CLI entry point for backup/restore operations
9
+ */
10
+ async function main(): Promise<void> {
11
+ const args = process.argv.slice(2);
12
+ const command = args[0];
13
+
14
+ const workspace = process.env.OPENCLAW_WORKSPACE || '/root/.openclaw/workspace';
15
+ const configPath = path.join(workspace, '.backupconfig.json');
16
+
17
+ if (!fs.existsSync(configPath)) {
18
+ console.error('❌ Error: .backupconfig.json not found at ' + configPath);
19
+ process.exit(1);
20
+ }
21
+
22
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
23
+ const backupRepoPath = config.backupRepoPath;
24
+
25
+ try {
26
+ switch (command) {
27
+ case 'backup':
28
+ await handleBackup(workspace);
29
+ break;
30
+
31
+ case 'restore':
32
+ await handleRestore(backupRepoPath, workspace, args);
33
+ break;
34
+
35
+ case 'history':
36
+ await handleHistory(backupRepoPath);
37
+ break;
38
+
39
+ case '--help':
40
+ case '-h':
41
+ case undefined:
42
+ showHelp();
43
+ break;
44
+
45
+ default:
46
+ console.error('❌ Unknown command: ' + command);
47
+ showHelp();
48
+ process.exit(1);
49
+ }
50
+ } catch (error) {
51
+ console.error(`❌ Error: ${error}`);
52
+ process.exit(1);
53
+ }
54
+ }
55
+
56
+ async function handleBackup(workspace: string): Promise<void> {
57
+ console.log('🔄 Starting backup...');
58
+ const result = await performBackup(workspace);
59
+
60
+ if (result.success) {
61
+ console.log('✅ Backup complete');
62
+ console.log(` Agents processed: ${result.agentsProcessed}`);
63
+ console.log(
64
+ ` Changes: ${result.changes.filter((c) => c.workspaceChanged || c.agentDirChanged).length}`
65
+ );
66
+ result.changes.forEach((change) => {
67
+ if (change.workspaceChanged || change.agentDirChanged) {
68
+ console.log(` - ${change.agentId}: workspace=${change.workspaceChanged}, agentDir=${change.agentDirChanged}`);
69
+ }
70
+ });
71
+ } else {
72
+ console.error(`❌ Backup failed: ${result.message}`);
73
+ if (result.error) {
74
+ console.error(` ${result.error}`);
75
+ }
76
+ process.exit(1);
77
+ }
78
+ }
79
+
80
+ async function handleRestore(
81
+ backupRepoPath: string,
82
+ workspace: string,
83
+ args: string[]
84
+ ): Promise<void> {
85
+ let targetSha: string | null = null;
86
+ let confirmAuthOverwrite = false;
87
+
88
+ // Check for --sha argument
89
+ const shaIndex = args.indexOf('--sha');
90
+ if (shaIndex !== -1 && shaIndex + 1 < args.length) {
91
+ targetSha = args[shaIndex + 1];
92
+ console.log(`🔄 Restoring to commit: ${targetSha}`);
93
+ } else {
94
+ console.log('🔄 Restoring to latest backup...');
95
+ }
96
+
97
+ // Check for --confirm-auth-overwrite flag
98
+ if (args.includes('--confirm-auth-overwrite')) {
99
+ confirmAuthOverwrite = true;
100
+ }
101
+
102
+ const result = await performRestore(backupRepoPath, targetSha, workspace, confirmAuthOverwrite);
103
+
104
+ if (result.success) {
105
+ console.log('✅ Restore complete');
106
+ console.log(` Agents restored: ${result.agentsRestored}`);
107
+ } else {
108
+ if (result.authOverwriteWarning) {
109
+ console.warn(`⚠️ ${result.message}`);
110
+ console.warn(`\n To restore including sensitive credentials, run:\n backup-agents restore ${targetSha ? `--sha ${targetSha}` : ''} --confirm-auth-overwrite`);
111
+ } else {
112
+ console.error(`❌ Restore failed: ${result.message}`);
113
+ if (result.error) {
114
+ console.error(` ${result.error}`);
115
+ }
116
+ }
117
+ process.exit(1);
118
+ }
119
+ }
120
+
121
+ async function handleHistory(backupRepoPath: string): Promise<void> {
122
+ const { executeCommand } = require('./utils');
123
+
124
+ console.log('📜 Backup history:\n');
125
+ try {
126
+ const log = executeCommand(`cd ${backupRepoPath} && git log --oneline -20`);
127
+ console.log(log);
128
+ } catch (error) {
129
+ console.error(`❌ Failed to fetch history: ${error}`);
130
+ process.exit(1);
131
+ }
132
+ }
133
+
134
+ function showHelp(): void {
135
+ console.log(`
136
+ @dambrogia/openclaw-agents-backup CLI
137
+
138
+ Usage:
139
+ backup-agents [command] [options]
140
+
141
+ Commands:
142
+ backup Backup all agents now
143
+ restore [options] Restore agents
144
+ history Show recent backup history
145
+ --help, -h Show this help message
146
+
147
+ Restore Options:
148
+ --sha SHA Restore to specific commit
149
+ --confirm-auth-overwrite Allow overwriting auth-profiles.json (API tokens)
150
+
151
+ Examples:
152
+ # Backup now
153
+ backup-agents backup
154
+
155
+ # Restore latest
156
+ backup-agents restore
157
+
158
+ # Restore to specific point in time
159
+ backup-agents restore --sha abc123def456
160
+
161
+ # Restore including sensitive credentials
162
+ backup-agents restore --confirm-auth-overwrite
163
+
164
+ # View backup history
165
+ backup-agents history
166
+
167
+ Environment:
168
+ OPENCLAW_WORKSPACE Path to OpenClaw workspace (default: /root/.openclaw/workspace)
169
+ BACKUP_ENCRYPTION_PASSWORD Password for encrypting/decrypting files (required)
170
+ `);
171
+ }
172
+
173
+ main().catch((error) => {
174
+ console.error(`❌ Unexpected error: ${error}`);
175
+ process.exit(1);
176
+ });
@@ -0,0 +1,126 @@
1
+ import * as crypto from 'crypto';
2
+ import * as fs from 'fs';
3
+
4
+ const ALGORITHM = 'aes-256-gcm';
5
+ const SALT_LENGTH = 16;
6
+ const IV_LENGTH = 16;
7
+ const AUTH_TAG_LENGTH = 16;
8
+ const PBKDF2_ITERATIONS = 100000;
9
+ const PBKDF2_DIGEST = 'sha256';
10
+
11
+ export interface EncryptionHeader {
12
+ salt: Buffer;
13
+ iv: Buffer;
14
+ authTag: Buffer;
15
+ }
16
+
17
+ /**
18
+ * Encrypt a file with AES-256-GCM using PBKDF2 key derivation
19
+ * @param inputPath Path to input file
20
+ * @param outputPath Path to output file (.jsonl.enc)
21
+ * @param password Encryption password
22
+ */
23
+ export function encryptFile(inputPath: string, outputPath: string, password: string): void {
24
+ // Generate random salt and IV
25
+ const salt = crypto.randomBytes(SALT_LENGTH);
26
+ const iv = crypto.randomBytes(IV_LENGTH);
27
+
28
+ // Derive key from password using PBKDF2
29
+ const key = crypto.pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, 32, PBKDF2_DIGEST);
30
+
31
+ // Read plaintext
32
+ const plaintext = fs.readFileSync(inputPath);
33
+
34
+ // Encrypt
35
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
36
+ const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
37
+ const authTag = cipher.getAuthTag();
38
+
39
+ // Write: [salt][iv][authTag][ciphertext]
40
+ const output = Buffer.concat([salt, iv, authTag, encrypted]);
41
+ fs.writeFileSync(outputPath, output);
42
+ }
43
+
44
+ /**
45
+ * Decrypt a file encrypted with encryptFile()
46
+ * @param inputPath Path to input file (.jsonl.enc)
47
+ * @param outputPath Path to output file
48
+ * @param password Encryption password
49
+ */
50
+ export function decryptFile(inputPath: string, outputPath: string, password: string): void {
51
+ // Read encrypted file
52
+ const encrypted = fs.readFileSync(inputPath);
53
+
54
+ // Parse header: [salt][iv][authTag][ciphertext]
55
+ if (encrypted.length < SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH) {
56
+ throw new Error('Invalid encrypted file: too short');
57
+ }
58
+
59
+ let offset = 0;
60
+ const salt = encrypted.subarray(offset, (offset += SALT_LENGTH));
61
+ const iv = encrypted.subarray(offset, (offset += IV_LENGTH));
62
+ const authTag = encrypted.subarray(offset, (offset += AUTH_TAG_LENGTH));
63
+ const ciphertext = encrypted.subarray(offset);
64
+
65
+ // Derive key from password using same parameters
66
+ const key = crypto.pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, 32, PBKDF2_DIGEST);
67
+
68
+ // Decrypt
69
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
70
+ decipher.setAuthTag(authTag);
71
+
72
+ try {
73
+ const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
74
+ fs.writeFileSync(outputPath, plaintext);
75
+ } catch (error) {
76
+ throw new Error(`Decryption failed: invalid password or corrupted file - ${error}`);
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Encrypt a buffer with AES-256-GCM
82
+ * @param data Buffer to encrypt
83
+ * @param password Encryption password
84
+ * @returns Encrypted buffer with header
85
+ */
86
+ export function encryptBuffer(data: Buffer, password: string): Buffer {
87
+ const salt = crypto.randomBytes(SALT_LENGTH);
88
+ const iv = crypto.randomBytes(IV_LENGTH);
89
+
90
+ const key = crypto.pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, 32, PBKDF2_DIGEST);
91
+
92
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
93
+ const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
94
+ const authTag = cipher.getAuthTag();
95
+
96
+ return Buffer.concat([salt, iv, authTag, encrypted]);
97
+ }
98
+
99
+ /**
100
+ * Decrypt a buffer encrypted with encryptBuffer()
101
+ * @param encrypted Encrypted buffer with header
102
+ * @param password Encryption password
103
+ * @returns Decrypted buffer
104
+ */
105
+ export function decryptBuffer(encrypted: Buffer, password: string): Buffer {
106
+ if (encrypted.length < SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH) {
107
+ throw new Error('Invalid encrypted buffer: too short');
108
+ }
109
+
110
+ let offset = 0;
111
+ const salt = encrypted.subarray(offset, (offset += SALT_LENGTH));
112
+ const iv = encrypted.subarray(offset, (offset += IV_LENGTH));
113
+ const authTag = encrypted.subarray(offset, (offset += AUTH_TAG_LENGTH));
114
+ const ciphertext = encrypted.subarray(offset);
115
+
116
+ const key = crypto.pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, 32, PBKDF2_DIGEST);
117
+
118
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
119
+ decipher.setAuthTag(authTag);
120
+
121
+ try {
122
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
123
+ } catch (error) {
124
+ throw new Error(`Decryption failed: invalid password or corrupted data - ${error}`);
125
+ }
126
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from './types';
2
+ export * from './backup';
3
+ export * from './restore';
4
+ export * from './agentLister';
5
+ export * from './utils';
package/src/restore.ts ADDED
@@ -0,0 +1,143 @@
1
+ import * as path from 'path';
2
+ import * as fs from 'fs';
3
+ import { RestoreResult, AgentArchiveMetadata } from './types';
4
+ import { pathExists, rsyncDirectory, readJsonFile, ensureDirectoryExists, findEncryptedFiles } from './utils';
5
+ import { decryptFile } from './encryptionService';
6
+
7
+ /**
8
+ * Restore agents from backup to specified git SHA or latest
9
+ * @param backupRepoPath Path to backup repository
10
+ * @param targetSha Git SHA to restore from (if null, uses current state)
11
+ * @param workspacePath Base path where agents will be restored
12
+ * @param confirmAuthOverwrite If true, allows overwriting auth-profiles.json without warning
13
+ */
14
+ export async function performRestore(
15
+ backupRepoPath: string,
16
+ targetSha: string | null,
17
+ _workspacePath: string,
18
+ confirmAuthOverwrite: boolean = false
19
+ ): Promise<RestoreResult> {
20
+ try {
21
+ if (!pathExists(backupRepoPath)) {
22
+ return {
23
+ success: false,
24
+ message: `Backup repository not found at ${backupRepoPath}`,
25
+ agentsRestored: 0,
26
+ error: 'Backup repo not found'
27
+ };
28
+ }
29
+
30
+ const archivesPath = path.join(backupRepoPath, 'archives');
31
+ if (!pathExists(archivesPath)) {
32
+ return {
33
+ success: false,
34
+ message: 'Archives directory not found in backup repository',
35
+ agentsRestored: 0,
36
+ error: 'No archives found'
37
+ };
38
+ }
39
+
40
+ // Get encryption password from environment
41
+ const encryptionPassword = process.env.BACKUP_ENCRYPTION_PASSWORD;
42
+ if (!encryptionPassword) {
43
+ return {
44
+ success: false,
45
+ message: 'Backup encryption password not set',
46
+ agentsRestored: 0,
47
+ error: 'Missing BACKUP_ENCRYPTION_PASSWORD environment variable'
48
+ };
49
+ }
50
+
51
+ // If target SHA specified, we would ideally check out that commit
52
+ // For now, we work with current state (could be enhanced)
53
+ if (targetSha) {
54
+ console.warn(`Note: targetSha specified (${targetSha}) but not implemented in this version`);
55
+ }
56
+
57
+ // List all agent directories
58
+ const agentDirs = fs
59
+ .readdirSync(archivesPath, { withFileTypes: true })
60
+ .filter((dirent) => dirent.isDirectory())
61
+ .map((dirent) => dirent.name);
62
+
63
+ let agentsRestored = 0;
64
+ const errors: string[] = [];
65
+
66
+ // Restore each agent
67
+ for (const agentId of agentDirs) {
68
+ const agentArchivePath = path.join(archivesPath, agentId);
69
+ const metadataPath = path.join(agentArchivePath, 'agent.json');
70
+
71
+ try {
72
+ // Read metadata
73
+ const metadata = readJsonFile<AgentArchiveMetadata>(metadataPath);
74
+
75
+ // Restore workspace
76
+ const workspaceSource = path.join(agentArchivePath, 'workspace');
77
+ if (pathExists(workspaceSource)) {
78
+ ensureDirectoryExists(metadata.workspace);
79
+ rsyncDirectory(workspaceSource, metadata.workspace);
80
+ }
81
+
82
+ // Restore agent directory
83
+ const agentDirSource = path.join(agentArchivePath, 'agentDir');
84
+ if (pathExists(agentDirSource)) {
85
+ // Decrypt all .enc files before restoring
86
+ const encryptedFiles = findEncryptedFiles(agentDirSource);
87
+ for (const encFile of encryptedFiles) {
88
+ try {
89
+ const plainPath = encFile.substring(0, encFile.length - 4); // Remove .enc suffix
90
+ decryptFile(encFile, plainPath, encryptionPassword);
91
+ // Delete encrypted file after successful decryption
92
+ fs.unlinkSync(encFile);
93
+ } catch (error) {
94
+ throw new Error(`Failed to decrypt ${encFile}: ${error}`);
95
+ }
96
+ }
97
+
98
+ // Check if auth-profiles.json exists in the backup
99
+ const backupAuthPath = path.join(agentDirSource, 'agent', 'auth-profiles.json');
100
+ if (pathExists(backupAuthPath)) {
101
+ if (!confirmAuthOverwrite) {
102
+ return {
103
+ success: false,
104
+ message: 'Backup contains sensitive auth-profiles.json (API tokens). Use --confirm-auth-overwrite to proceed.',
105
+ agentsRestored: 0,
106
+ authOverwriteWarning: true
107
+ };
108
+ }
109
+ }
110
+
111
+ ensureDirectoryExists(metadata.agentDir);
112
+ rsyncDirectory(agentDirSource, metadata.agentDir);
113
+ }
114
+
115
+ agentsRestored++;
116
+ } catch (error) {
117
+ errors.push(`Agent ${agentId}: ${error}`);
118
+ }
119
+ }
120
+
121
+ if (errors.length > 0) {
122
+ return {
123
+ success: false,
124
+ message: `Restored ${agentsRestored} agents with errors`,
125
+ agentsRestored,
126
+ error: errors.join('\n')
127
+ };
128
+ }
129
+
130
+ return {
131
+ success: true,
132
+ message: `Successfully restored ${agentsRestored} agents`,
133
+ agentsRestored
134
+ };
135
+ } catch (error) {
136
+ return {
137
+ success: false,
138
+ message: 'Restore operation failed',
139
+ agentsRestored: 0,
140
+ error: String(error)
141
+ };
142
+ }
143
+ }