bus-agent 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/.env.coco +11 -0
- package/AGENTS.md +37 -0
- package/LICENSE +21 -0
- package/README.md +370 -0
- package/SKILL.md +314 -0
- package/backup.js +57 -0
- package/bin/cli.js +41 -0
- package/bridge.js +325 -0
- package/claude-mcp.json +10 -0
- package/clients/coco-client.ts +245 -0
- package/clients/coco_client.py +216 -0
- package/coco-aliases.sh +10 -0
- package/coco-cli.js +1002 -0
- package/coco-tool.js +177 -0
- package/coco.js +26 -0
- package/cursor-mcp.json +3 -0
- package/doctor.js +24 -0
- package/hermes-forwarder.js +152 -0
- package/hermes.example.json +9 -0
- package/index.js +52 -0
- package/lib/backup.js +256 -0
- package/lib/bus.js +516 -0
- package/lib/daemon.js +96 -0
- package/lib/doctor.js +333 -0
- package/lib/hermes.js +162 -0
- package/lib/mcp.js +730 -0
- package/lib/memory.js +667 -0
- package/lib/orchestrator.js +426 -0
- package/lib/scheduler.js +259 -0
- package/lib/tunnel.js +317 -0
- package/mcporter.example.json +14 -0
- package/opencode-mcp.json +10 -0
- package/package.json +76 -0
- package/scripts/install.bat +5 -0
- package/scripts/install.ps1 +100 -0
- package/setup.js +320 -0
- package/tunnel.js +66 -0
- package/webhook-gateway.js +420 -0
package/lib/backup.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CoCo Backup & Restore — Bus State Management Module
|
|
3
|
+
*
|
|
4
|
+
* Export functions, no process.exit, accepts busDir as parameter.
|
|
5
|
+
* Used by: coco-cli.js, backup.js (thin CLI wrapper)
|
|
6
|
+
*/
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const zlib = require('zlib');
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
|
|
12
|
+
const BACKUP_DIR_NAME = '.backups';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create a compressed backup of the bus directory
|
|
16
|
+
*/
|
|
17
|
+
function createBackup(busDir, outFile) {
|
|
18
|
+
if (!fs.existsSync(busDir)) throw new Error(`Bus directory not found: ${busDir}`);
|
|
19
|
+
|
|
20
|
+
const backupDir = path.join(path.dirname(busDir), BACKUP_DIR_NAME);
|
|
21
|
+
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true });
|
|
22
|
+
|
|
23
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
24
|
+
const output = outFile || path.join(backupDir, `coco-bus-${timestamp}.coco`);
|
|
25
|
+
|
|
26
|
+
console.log(`\n CoCo Backup — Creating backup...`);
|
|
27
|
+
console.log(` Source: ${busDir}`);
|
|
28
|
+
console.log(` Output: ${output}\n`);
|
|
29
|
+
|
|
30
|
+
// Collect all bus files
|
|
31
|
+
const entries = [];
|
|
32
|
+
function walk(dir, relativePath = '') {
|
|
33
|
+
if (!fs.existsSync(dir)) return;
|
|
34
|
+
for (const item of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
35
|
+
const full = path.join(dir, item.name);
|
|
36
|
+
const rel = relativePath ? `${relativePath}/${item.name}` : item.name;
|
|
37
|
+
if (item.isDirectory()) walk(full, rel);
|
|
38
|
+
else if (item.isFile()) {
|
|
39
|
+
try { entries.push({ path: rel, content: fs.readFileSync(full, 'utf-8') }); }
|
|
40
|
+
catch (err) { console.warn(` ⚠️ Cannot read: ${rel} (${err.message})`); }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
walk(busDir);
|
|
45
|
+
|
|
46
|
+
const metadata = {
|
|
47
|
+
version: '2.1.0', tool: 'coco-backup', created_at: new Date().toISOString(),
|
|
48
|
+
hostname: require('os').hostname(), bus_path: busDir, file_count: entries.length,
|
|
49
|
+
checksum: crypto.createHash('sha256').update(entries.map(e => e.path + e.content).join('')).digest('hex'),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const compressed = zlib.gzipSync(JSON.stringify({ metadata, entries }));
|
|
53
|
+
fs.writeFileSync(output, compressed);
|
|
54
|
+
|
|
55
|
+
const sizeKB = (compressed.length / 1024).toFixed(1);
|
|
56
|
+
console.log(` ✅ Backup complete: ${entries.length} files, ${sizeKB} KB`);
|
|
57
|
+
console.log(` Checksum: ${metadata.checksum.substring(0, 16)}...\n`);
|
|
58
|
+
|
|
59
|
+
return output;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Restore bus from a backup file
|
|
64
|
+
*/
|
|
65
|
+
function restoreBackup(file, busDir) {
|
|
66
|
+
if (!fs.existsSync(file)) throw new Error(`Backup file not found: ${file}`);
|
|
67
|
+
|
|
68
|
+
console.log(`\n CoCo Restore — Restoring from backup...`);
|
|
69
|
+
console.log(` Source: ${file}`);
|
|
70
|
+
|
|
71
|
+
let archive;
|
|
72
|
+
try {
|
|
73
|
+
archive = JSON.parse(zlib.gunzipSync(fs.readFileSync(file)).toString('utf-8'));
|
|
74
|
+
} catch (err) { throw new Error(`Cannot read backup: ${err.message}`); }
|
|
75
|
+
|
|
76
|
+
const meta = archive.metadata;
|
|
77
|
+
console.log(` Backup: ${meta.created_at}`);
|
|
78
|
+
console.log(` Files: ${meta.file_count}`);
|
|
79
|
+
console.log(` Host: ${meta.hostname}\n`);
|
|
80
|
+
|
|
81
|
+
const actualChecksum = crypto.createHash('sha256').update(archive.entries.map(e => e.path + e.content).join('')).digest('hex');
|
|
82
|
+
if (actualChecksum !== meta.checksum) throw new Error('Backup checksum mismatch — file may be corrupted');
|
|
83
|
+
console.log(' ✅ Checksum verified');
|
|
84
|
+
|
|
85
|
+
// Pre-restore backup
|
|
86
|
+
const backupDir = path.join(path.dirname(busDir), BACKUP_DIR_NAME);
|
|
87
|
+
const preBackupPath = path.join(backupDir, 'pre-restore-auto.coco');
|
|
88
|
+
console.log(' Creating pre-restore backup...');
|
|
89
|
+
try { createBackup(busDir, preBackupPath); console.log(` Pre-restore backup saved: ${preBackupPath}`); } catch {}
|
|
90
|
+
|
|
91
|
+
// Restore files
|
|
92
|
+
let restored = 0, errors = 0;
|
|
93
|
+
for (const entry of archive.entries) {
|
|
94
|
+
const targetPath = path.join(busDir, entry.path);
|
|
95
|
+
try {
|
|
96
|
+
if (!fs.existsSync(path.dirname(targetPath))) fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
97
|
+
fs.writeFileSync(targetPath, entry.content, 'utf-8');
|
|
98
|
+
restored++;
|
|
99
|
+
} catch (err) { console.error(` ❌ Failed to restore ${entry.path}: ${err.message}`); errors++; }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log(`\n ✅ Restore complete: ${restored} files${errors > 0 ? `, ${errors} errors` : ''}\n`);
|
|
103
|
+
return { restored, errors };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* List available backups
|
|
108
|
+
*/
|
|
109
|
+
function listBackups(busDir) {
|
|
110
|
+
const backupDir = path.join(path.dirname(busDir), BACKUP_DIR_NAME);
|
|
111
|
+
if (!fs.existsSync(backupDir)) { console.log('No backups found.'); return []; }
|
|
112
|
+
|
|
113
|
+
const files = fs.readdirSync(backupDir).filter(f => f.endsWith('.coco')).sort().reverse();
|
|
114
|
+
if (files.length === 0) { console.log('No backups found.'); return []; }
|
|
115
|
+
|
|
116
|
+
console.log(`\n CoCo Backups (${files.length} total):`);
|
|
117
|
+
console.log(' ──────────────────────────────────────────────');
|
|
118
|
+
|
|
119
|
+
let totalSize = 0;
|
|
120
|
+
for (const f of files) {
|
|
121
|
+
const filePath = path.join(backupDir, f);
|
|
122
|
+
const sizeKB = (fs.statSync(filePath).size / 1024).toFixed(1);
|
|
123
|
+
totalSize += fs.statSync(filePath).size;
|
|
124
|
+
try {
|
|
125
|
+
const meta = JSON.parse(zlib.gunzipSync(fs.readFileSync(filePath)).toString('utf-8')).metadata;
|
|
126
|
+
console.log(` ${f.substring(0, 40).padEnd(42)} ${sizeKB.padStart(7)} KB (${meta.file_count} files, ${meta.created_at.substring(0, 19)})`);
|
|
127
|
+
} catch { console.log(` ${f.substring(0, 40).padEnd(42)} ${sizeKB.padStart(7)} KB (corrupted?)`); }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
console.log(` ──────────────────────────────────────────────`);
|
|
131
|
+
console.log(` Total: ${(totalSize / 1024 / 1024).toFixed(2)} MB\n`);
|
|
132
|
+
return files;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Show backup info
|
|
137
|
+
*/
|
|
138
|
+
function backupInfo(file) {
|
|
139
|
+
if (!fs.existsSync(file)) throw new Error(`File not found: ${file}`);
|
|
140
|
+
const archive = JSON.parse(zlib.gunzipSync(fs.readFileSync(file)).toString('utf-8'));
|
|
141
|
+
const meta = archive.metadata;
|
|
142
|
+
|
|
143
|
+
console.log(`\n Backup Info: ${path.basename(file)}`);
|
|
144
|
+
console.log(' ──────────────────────────────────────────────');
|
|
145
|
+
console.log(` Created: ${meta.created_at}`);
|
|
146
|
+
console.log(` Hostname: ${meta.hostname}`);
|
|
147
|
+
console.log(` Bus path: ${meta.bus_path}`);
|
|
148
|
+
console.log(` File count: ${meta.file_count}`);
|
|
149
|
+
console.log(` Checksum: ${meta.checksum.substring(0, 24)}...`);
|
|
150
|
+
|
|
151
|
+
const dirs = {};
|
|
152
|
+
for (const entry of archive.entries) {
|
|
153
|
+
const dir = path.dirname(entry.path) || '.';
|
|
154
|
+
dirs[dir] = (dirs[dir] || 0) + 1;
|
|
155
|
+
}
|
|
156
|
+
console.log(' Contents:');
|
|
157
|
+
for (const [dir, count] of Object.entries(dirs).sort()) console.log(` ${dir.padEnd(30)} ${count} file(s)`);
|
|
158
|
+
console.log();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Diff backup vs current state
|
|
163
|
+
*/
|
|
164
|
+
function diffBackup(file, busDir) {
|
|
165
|
+
if (!fs.existsSync(file)) throw new Error(`Backup file not found: ${file}`);
|
|
166
|
+
const archive = JSON.parse(zlib.gunzipSync(fs.readFileSync(file)).toString('utf-8'));
|
|
167
|
+
|
|
168
|
+
console.log(`\n CoCo Diff — Backup vs Current State`);
|
|
169
|
+
console.log(` Backup: ${archive.metadata.created_at}`);
|
|
170
|
+
console.log(' ──────────────────────────────────────────────');
|
|
171
|
+
|
|
172
|
+
const backupIndex = {};
|
|
173
|
+
for (const entry of archive.entries) backupIndex[entry.path] = entry.content;
|
|
174
|
+
|
|
175
|
+
const currentIndex = {};
|
|
176
|
+
function walk(dir, rel = '') {
|
|
177
|
+
if (!fs.existsSync(dir)) return;
|
|
178
|
+
for (const item of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
179
|
+
const full = path.join(dir, item.name);
|
|
180
|
+
const r = rel ? `${rel}/${item.name}` : item.name;
|
|
181
|
+
if (item.isDirectory()) walk(full, r);
|
|
182
|
+
else if (item.isFile()) { try { currentIndex[r] = fs.readFileSync(full, 'utf-8'); } catch {} }
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
walk(busDir);
|
|
186
|
+
|
|
187
|
+
let added = 0, removed = 0, modified = 0, unchanged = 0;
|
|
188
|
+
const allPaths = new Set([...Object.keys(backupIndex), ...Object.keys(currentIndex)]);
|
|
189
|
+
for (const p of allPaths) {
|
|
190
|
+
const inB = p in backupIndex, inC = p in currentIndex;
|
|
191
|
+
if (!inB && inC) { added++; console.log(` ➕ Added: ${p}`); }
|
|
192
|
+
else if (inB && !inC) { removed++; console.log(` ➖ Removed: ${p}`); }
|
|
193
|
+
else if (backupIndex[p] === currentIndex[p]) unchanged++;
|
|
194
|
+
else { modified++; console.log(` ✏️ Modified: ${p} (${(backupIndex[p].length / 1024).toFixed(1)} KB → ${(currentIndex[p].length / 1024).toFixed(1)} KB)`); }
|
|
195
|
+
}
|
|
196
|
+
console.log(` ──────────────────────────────────────────────`);
|
|
197
|
+
console.log(` Added: ${added}, Removed: ${removed}, Modified: ${modified}, Unchanged: ${unchanged}\n`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Cleanup old backups
|
|
202
|
+
*/
|
|
203
|
+
function cleanupBackups(busDir, maxAgeDays = 30) {
|
|
204
|
+
const backupDir = path.join(path.dirname(busDir), BACKUP_DIR_NAME);
|
|
205
|
+
if (!fs.existsSync(backupDir)) { console.log('No backups to clean up.'); return; }
|
|
206
|
+
|
|
207
|
+
const cutoff = Date.now() - (maxAgeDays * 86400000);
|
|
208
|
+
const files = fs.readdirSync(backupDir).filter(f => f.endsWith('.coco'))
|
|
209
|
+
.map(f => ({ name: f, path: path.join(backupDir, f), stat: fs.statSync(path.join(backupDir, f)) }))
|
|
210
|
+
.filter(f => f.stat.mtimeMs < cutoff).sort((a, b) => a.stat.mtimeMs - b.stat.mtimeMs);
|
|
211
|
+
|
|
212
|
+
if (files.length === 0) { console.log(`No backups older than ${maxAgeDays} days.`); return; }
|
|
213
|
+
|
|
214
|
+
let deleted = 0, freed = 0;
|
|
215
|
+
for (const f of files) {
|
|
216
|
+
const sizeKB = (f.stat.size / 1024).toFixed(1);
|
|
217
|
+
fs.unlinkSync(f.path);
|
|
218
|
+
deleted++; freed += f.stat.size;
|
|
219
|
+
console.log(` 🗑️ ${f.name.substring(0, 40).padEnd(42)} ${sizeKB} KB`);
|
|
220
|
+
}
|
|
221
|
+
console.log(`\n ✅ Deleted ${deleted} backups, freed ${(freed / 1024).toFixed(1)} KB\n`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Auto-backup — only if changes detected
|
|
226
|
+
*/
|
|
227
|
+
function autoBackup(busDir) {
|
|
228
|
+
const backupDir = path.join(path.dirname(busDir), BACKUP_DIR_NAME);
|
|
229
|
+
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true });
|
|
230
|
+
|
|
231
|
+
const existing = fs.readdirSync(backupDir).filter(f => f.endsWith('.coco')).sort().reverse();
|
|
232
|
+
let lastChecksum = null;
|
|
233
|
+
if (existing.length > 0) {
|
|
234
|
+
try { lastChecksum = JSON.parse(zlib.gunzipSync(fs.readFileSync(path.join(backupDir, existing[0]))).toString('utf-8')).metadata.checksum; } catch {}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const backupPath = createBackup(busDir);
|
|
238
|
+
if (lastChecksum) {
|
|
239
|
+
try {
|
|
240
|
+
const newChecksum = JSON.parse(zlib.gunzipSync(fs.readFileSync(backupPath)).toString('utf-8')).metadata.checksum;
|
|
241
|
+
if (newChecksum === lastChecksum) { fs.unlinkSync(backupPath); console.log(' ℹ️ No changes since last backup — skipped.\n'); return { skipped: true }; }
|
|
242
|
+
} catch {}
|
|
243
|
+
}
|
|
244
|
+
return { skipped: false, path: backupPath };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function watchMode(busDir, intervalMinutes = 30) {
|
|
248
|
+
const intervalMs = intervalMinutes * 60 * 1000;
|
|
249
|
+
console.log(`\n CoCo Auto-Backup — Every ${intervalMinutes} minutes`);
|
|
250
|
+
console.log(` Bus: ${busDir}`);
|
|
251
|
+
console.log(` Press Ctrl+C to stop\n`);
|
|
252
|
+
autoBackup(busDir);
|
|
253
|
+
setInterval(() => { console.log(`[${new Date().toISOString()}] Auto-backup check...`); autoBackup(busDir); }, intervalMs);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
module.exports = { createBackup, restoreBackup, listBackups, backupInfo, diffBackup, cleanupBackups, autoBackup, watchMode, BACKUP_DIR_NAME };
|