openclaw-bridge 0.3.2 → 0.4.1

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.
Files changed (52) hide show
  1. package/README.md +43 -16
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +843 -0
  4. package/dist/config.d.ts +3 -0
  5. package/dist/config.js +64 -0
  6. package/dist/discovery.d.ts +4 -0
  7. package/dist/discovery.js +6 -0
  8. package/dist/file-ops.d.ts +22 -0
  9. package/dist/file-ops.js +253 -0
  10. package/dist/heartbeat.d.ts +21 -0
  11. package/dist/heartbeat.js +152 -0
  12. package/dist/index.d.ts +9 -0
  13. package/dist/index.js +624 -0
  14. package/dist/manager/hub-client.d.ts +18 -0
  15. package/dist/manager/hub-client.js +89 -0
  16. package/dist/manager/local-manager.d.ts +12 -0
  17. package/dist/manager/local-manager.js +117 -0
  18. package/dist/manager/pm2-bridge.d.ts +17 -0
  19. package/dist/manager/pm2-bridge.js +113 -0
  20. package/dist/message-relay.d.ts +32 -0
  21. package/dist/message-relay.js +229 -0
  22. package/dist/permissions.d.ts +3 -0
  23. package/dist/permissions.js +14 -0
  24. package/dist/registry.d.ts +13 -0
  25. package/dist/registry.js +103 -0
  26. package/dist/restart.d.ts +15 -0
  27. package/dist/restart.js +107 -0
  28. package/dist/router.d.ts +11 -0
  29. package/dist/router.js +18 -0
  30. package/dist/session.d.ts +11 -0
  31. package/dist/session.js +21 -0
  32. package/dist/types.d.ts +90 -0
  33. package/dist/types.js +1 -0
  34. package/openclaw.plugin.json +6 -92
  35. package/package.json +15 -5
  36. package/src/cli.ts +0 -842
  37. package/src/config.ts +0 -72
  38. package/src/discovery.ts +0 -17
  39. package/src/file-ops.ts +0 -320
  40. package/src/heartbeat.ts +0 -196
  41. package/src/index.ts +0 -681
  42. package/src/manager/hub-client.ts +0 -114
  43. package/src/manager/local-manager.ts +0 -121
  44. package/src/manager/pm2-bridge.ts +0 -125
  45. package/src/message-relay.ts +0 -184
  46. package/src/permissions.ts +0 -18
  47. package/src/registry.ts +0 -107
  48. package/src/restart.ts +0 -137
  49. package/src/router.ts +0 -40
  50. package/src/session.ts +0 -33
  51. package/src/types.ts +0 -100
  52. package/tsconfig.json +0 -14
package/dist/cli.js ADDED
@@ -0,0 +1,843 @@
1
+ #!/usr/bin/env node
2
+ import { mkdirSync, writeFileSync, readFileSync, existsSync, readdirSync, statSync, unlinkSync } from "node:fs";
3
+ import { join, dirname, basename } from "node:path";
4
+ import { execSync } from "node:child_process";
5
+ import { homedir, platform } from "node:os";
6
+ import { createInterface } from "node:readline/promises";
7
+ import { stdin as input, stdout as output } from "node:process";
8
+ // ── Config ──────────────────────────────────────────────────────────────────
9
+ const CONFIG_DIR = join(homedir(), ".openclaw-bridge");
10
+ const CONFIG_FILE = join(CONFIG_DIR, "config.json");
11
+ const IS_WINDOWS = platform() === "win32";
12
+ function loadConfig() {
13
+ if (!existsSync(CONFIG_FILE))
14
+ return null;
15
+ try {
16
+ return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ function saveConfig(config) {
23
+ mkdirSync(CONFIG_DIR, { recursive: true });
24
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
25
+ }
26
+ // ── Helpers ──────────────────────────────────────────────────────────────────
27
+ function run(cmd, opts = {}) {
28
+ try {
29
+ return execSync(cmd, {
30
+ encoding: "utf-8",
31
+ stdio: opts.silent ? ["pipe", "pipe", "pipe"] : ["inherit", "pipe", "inherit"],
32
+ }).trim();
33
+ }
34
+ catch (err) {
35
+ if (opts.silent)
36
+ return "";
37
+ throw err;
38
+ }
39
+ }
40
+ function runInherit(cmd) {
41
+ execSync(cmd, { stdio: "inherit" });
42
+ }
43
+ async function fetchJson(url, apiKey) {
44
+ const headers = {};
45
+ if (apiKey)
46
+ headers["x-api-key"] = apiKey;
47
+ const res = await fetch(url, { headers, signal: AbortSignal.timeout(8000) });
48
+ if (!res.ok)
49
+ throw new Error(`HTTP ${res.status}`);
50
+ return res.json();
51
+ }
52
+ /** Resolve ~ to homedir for any path string */
53
+ function expandHome(p) {
54
+ if (p.startsWith("~/") || p === "~")
55
+ return join(homedir(), p.slice(2));
56
+ return p;
57
+ }
58
+ /** Find the ecosystem.config.cjs in common locations */
59
+ function findEcosystem() {
60
+ const candidates = [
61
+ join(process.cwd(), "ecosystem.config.cjs"),
62
+ join(dirname(process.cwd()), "ecosystem.config.cjs"),
63
+ IS_WINDOWS ? "C:\\openclaw-instances\\ecosystem.config.cjs" : "",
64
+ join(homedir(), "openclaw-instances", "ecosystem.config.cjs"),
65
+ join(homedir(), ".openclaw", "ecosystem.config.cjs"),
66
+ ].filter(Boolean);
67
+ for (const p of candidates) {
68
+ if (existsSync(p))
69
+ return p;
70
+ }
71
+ return null;
72
+ }
73
+ /** Get the openclaw-instances directory from the ecosystem path */
74
+ function instancesDirFromEcosystem(ecosystemPath) {
75
+ return dirname(ecosystemPath);
76
+ }
77
+ function getPm2Processes() {
78
+ try {
79
+ const out = run("pm2 jlist", { silent: true });
80
+ if (!out)
81
+ return [];
82
+ const list = JSON.parse(out);
83
+ return list.map((p) => ({
84
+ name: p.name,
85
+ pid: p.pid ?? "-",
86
+ status: p.pm2_env?.status ?? "unknown",
87
+ memory: p.monit?.memory ?? 0,
88
+ restarts: p.pm2_env?.restart_time ?? 0,
89
+ uptime: p.pm2_env?.pm_uptime ?? 0,
90
+ }));
91
+ }
92
+ catch {
93
+ return [];
94
+ }
95
+ }
96
+ function formatBytes(bytes) {
97
+ if (bytes < 1024)
98
+ return `${bytes}B`;
99
+ if (bytes < 1024 * 1024)
100
+ return `${(bytes / 1024).toFixed(1)}KB`;
101
+ return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
102
+ }
103
+ function formatUptime(ms) {
104
+ if (!ms || ms <= 0)
105
+ return "-";
106
+ const s = Math.floor((Date.now() - ms) / 1000);
107
+ if (s < 60)
108
+ return `${s}s`;
109
+ if (s < 3600)
110
+ return `${Math.floor(s / 60)}m`;
111
+ if (s < 86400)
112
+ return `${Math.floor(s / 3600)}h`;
113
+ return `${Math.floor(s / 86400)}d`;
114
+ }
115
+ function printTable(processes) {
116
+ if (processes.length === 0) {
117
+ console.log(" (no processes found)");
118
+ return;
119
+ }
120
+ const cols = {
121
+ name: Math.max(4, ...processes.map((p) => p.name.length)),
122
+ status: 8,
123
+ pid: 7,
124
+ memory: 8,
125
+ restarts: 8,
126
+ uptime: 8,
127
+ };
128
+ const header = [
129
+ "Name".padEnd(cols.name),
130
+ "Status".padEnd(cols.status),
131
+ "PID".padEnd(cols.pid),
132
+ "Memory".padEnd(cols.memory),
133
+ "Restarts".padEnd(cols.restarts),
134
+ "Uptime".padEnd(cols.uptime),
135
+ ].join(" ");
136
+ const sep = "-".repeat(header.length);
137
+ console.log(`\n ${header}`);
138
+ console.log(` ${sep}`);
139
+ for (const p of processes) {
140
+ const statusIcon = p.status === "online" ? "online " : p.status.padEnd(cols.status);
141
+ const row = [
142
+ p.name.padEnd(cols.name),
143
+ statusIcon,
144
+ String(p.pid).padEnd(cols.pid),
145
+ formatBytes(p.memory).padEnd(cols.memory),
146
+ String(p.restarts).padEnd(cols.restarts),
147
+ formatUptime(p.uptime).padEnd(cols.uptime),
148
+ ].join(" ");
149
+ console.log(` ${row}`);
150
+ }
151
+ console.log();
152
+ }
153
+ // ── Commands ─────────────────────────────────────────────────────────────────
154
+ async function cmdSetup() {
155
+ console.log("\nOpenClaw Bridge — Interactive Setup");
156
+ console.log("=====================================\n");
157
+ const rl = createInterface({ input, output });
158
+ const existing = loadConfig();
159
+ const hubUrl = (await rl.question(`Hub URL [${existing?.hubUrl ?? "http://localhost:3080"}]: `)).trim() || existing?.hubUrl || "http://localhost:3080";
160
+ const apiKey = (await rl.question(`API Key [${existing?.apiKey ? "***" + existing.apiKey.slice(-4) : "none"}]: `)).trim() || existing?.apiKey || "";
161
+ const managerPass = (await rl.question(`Manager Password [${existing?.managerPass ? "***" : "none"}]: `)).trim() || existing?.managerPass || "";
162
+ rl.close();
163
+ const config = { hubUrl, apiKey, managerPass };
164
+ saveConfig(config);
165
+ console.log(`\nConfig saved to ${CONFIG_FILE}`);
166
+ // Test connection
167
+ console.log(`\nTesting connection to ${hubUrl} ...`);
168
+ try {
169
+ await fetchJson(`${hubUrl}/api/v1/registry/discover`, apiKey);
170
+ console.log(" Connected to Hub successfully.");
171
+ }
172
+ catch (err) {
173
+ console.log(` Could not reach Hub: ${err.message}`);
174
+ console.log(" (Config saved anyway — check your Hub URL and API key)");
175
+ }
176
+ console.log();
177
+ }
178
+ async function cmdStatus() {
179
+ console.log("\nOpenClaw Bridge — Status");
180
+ console.log("=========================\n");
181
+ // PM2 processes
182
+ console.log("PM2 Processes:");
183
+ const processes = getPm2Processes();
184
+ printTable(processes);
185
+ // Hub connection
186
+ const config = loadConfig();
187
+ if (!config) {
188
+ console.log("Hub: not configured (run: openclaw-bridge setup)");
189
+ }
190
+ else {
191
+ process.stdout.write(`Hub (${config.hubUrl}): `);
192
+ try {
193
+ await fetchJson(`${config.hubUrl}/api/v1/registry/discover`, config.apiKey);
194
+ console.log("connected");
195
+ }
196
+ catch (err) {
197
+ console.log(`unreachable — ${err.message}`);
198
+ }
199
+ }
200
+ console.log();
201
+ }
202
+ async function cmdStart() {
203
+ const ecosystem = findEcosystem();
204
+ if (!ecosystem) {
205
+ console.error("Could not find ecosystem.config.cjs.\n" +
206
+ "Searched: current dir, parent dir, C:\\openclaw-instances, ~/openclaw-instances, ~/.openclaw\n" +
207
+ "Run from your openclaw-instances directory.");
208
+ process.exit(1);
209
+ }
210
+ console.log(`\nStarting instances from: ${ecosystem}`);
211
+ const before = getPm2Processes().length;
212
+ runInherit(`pm2 start "${ecosystem}"`);
213
+ const after = getPm2Processes().length;
214
+ const started = Math.max(0, after - before);
215
+ console.log(`\nStarted ${started > 0 ? started : "all configured"} process(es). Run 'openclaw-bridge status' to verify.\n`);
216
+ }
217
+ async function cmdStop() {
218
+ console.log("\nStopping all PM2 processes...");
219
+ runInherit("pm2 stop all");
220
+ console.log();
221
+ }
222
+ async function cmdRestart(agent) {
223
+ if (!agent) {
224
+ console.log("\nRestarting all PM2 processes...");
225
+ runInherit("pm2 restart all");
226
+ }
227
+ else {
228
+ const withPrefix = `gw-${agent}`;
229
+ // Try gw- prefix first
230
+ const processes = getPm2Processes();
231
+ const hasPrefixed = processes.some((p) => p.name === withPrefix);
232
+ const target = hasPrefixed ? withPrefix : agent;
233
+ console.log(`\nRestarting ${target}...`);
234
+ try {
235
+ runInherit(`pm2 restart "${target}"`);
236
+ }
237
+ catch {
238
+ // Fallback: try raw name if prefix attempt failed
239
+ if (target === withPrefix) {
240
+ console.log(` (gw- prefix not found, trying raw name: ${agent})`);
241
+ runInherit(`pm2 restart "${agent}"`);
242
+ }
243
+ else {
244
+ throw new Error(`Process "${agent}" not found in PM2`);
245
+ }
246
+ }
247
+ }
248
+ console.log();
249
+ }
250
+ async function cmdLogs(agent) {
251
+ if (!agent) {
252
+ runInherit("pm2 logs --nostream --lines 50");
253
+ }
254
+ else {
255
+ const withPrefix = `gw-${agent}`;
256
+ const processes = getPm2Processes();
257
+ const hasPrefixed = processes.some((p) => p.name === withPrefix);
258
+ const target = hasPrefixed ? withPrefix : agent;
259
+ try {
260
+ runInherit(`pm2 logs "${target}" --nostream --lines 100`);
261
+ }
262
+ catch {
263
+ if (target === withPrefix) {
264
+ console.log(` (gw- prefix not found, trying raw name: ${agent})`);
265
+ runInherit(`pm2 logs "${agent}" --nostream --lines 100`);
266
+ }
267
+ }
268
+ }
269
+ }
270
+ async function cmdBackup() {
271
+ console.log("\nOpenClaw Bridge — Backup");
272
+ console.log("=========================\n");
273
+ const ecosystem = findEcosystem();
274
+ if (!ecosystem) {
275
+ console.error("Could not find openclaw-instances directory.");
276
+ process.exit(1);
277
+ }
278
+ const instancesDir = instancesDirFromEcosystem(ecosystem);
279
+ const timestamp = new Date().toISOString().slice(0, 10).replace(/-/g, "");
280
+ const backupName = `openclaw-backup-${timestamp}`;
281
+ const backupPath = join(process.cwd(), `${backupName}.tar.gz`);
282
+ const rl = createInterface({ input, output });
283
+ const password = (await rl.question("Encryption password for sensitive files: ")).trim();
284
+ rl.close();
285
+ if (!password) {
286
+ console.log("Password required for backup encryption.");
287
+ process.exit(1);
288
+ }
289
+ console.log(`\nCreating backup of: ${instancesDir}`);
290
+ console.log(`Output: ${backupPath}`);
291
+ // Exclusions: node_modules/, */state/, */workspace/, *.log, .claude/
292
+ const excludes = [
293
+ "--exclude=*/node_modules",
294
+ "--exclude=node_modules",
295
+ "--exclude=*/state",
296
+ "--exclude=*/workspace",
297
+ "--exclude=*.log",
298
+ "--exclude=.claude",
299
+ "--exclude=*/.claude",
300
+ ].join(" ");
301
+ if (IS_WINDOWS) {
302
+ // On Windows, use tar (available in Windows 10+)
303
+ runInherit(`tar -czf "${backupPath}" ${excludes} -C "${dirname(instancesDir)}" "${basename(instancesDir)}"`);
304
+ }
305
+ else {
306
+ runInherit(`tar -czf "${backupPath}" ${excludes} -C "${dirname(instancesDir)}" "${basename(instancesDir)}"`);
307
+ }
308
+ // Encrypt sensitive files (openclaw.json, config.json) inside the archive
309
+ // We do this by listing them and producing encrypted sidecar files
310
+ console.log("\nEncrypting sensitive config files...");
311
+ const sensitiveFiles = findFilesRecursive(instancesDir, (f) => (f === "openclaw.json" || f === "config.json") &&
312
+ !f.includes("node_modules") && !f.includes("state") && !f.includes("workspace"));
313
+ let encryptedCount = 0;
314
+ for (const filePath of sensitiveFiles) {
315
+ const encPath = `${filePath}.enc`;
316
+ try {
317
+ runInherit(`openssl enc -aes-256-cbc -pbkdf2 -in "${filePath}" -out "${encPath}" -pass pass:"${password}"`);
318
+ encryptedCount++;
319
+ }
320
+ catch {
321
+ console.log(` Warning: could not encrypt ${filePath}`);
322
+ }
323
+ }
324
+ if (encryptedCount > 0) {
325
+ console.log(` Encrypted ${encryptedCount} config file(s) alongside backup.`);
326
+ }
327
+ // Report size
328
+ try {
329
+ const stats = statSync(backupPath);
330
+ console.log(`\nBackup complete!`);
331
+ console.log(` Location: ${backupPath}`);
332
+ console.log(` Size: ${formatBytes(stats.size)}`);
333
+ }
334
+ catch {
335
+ console.log(`\nBackup file: ${backupPath}`);
336
+ }
337
+ console.log();
338
+ }
339
+ function findFilesRecursive(dir, predicate) {
340
+ const results = [];
341
+ if (!existsSync(dir))
342
+ return results;
343
+ const entries = readdirSync(dir, { withFileTypes: true });
344
+ for (const entry of entries) {
345
+ const fullPath = join(dir, entry.name);
346
+ if (entry.isDirectory()) {
347
+ if (entry.name !== "node_modules" && entry.name !== "state" && entry.name !== "workspace" && entry.name !== ".claude") {
348
+ results.push(...findFilesRecursive(fullPath, predicate));
349
+ }
350
+ }
351
+ else if (predicate(entry.name)) {
352
+ results.push(fullPath);
353
+ }
354
+ }
355
+ return results;
356
+ }
357
+ async function cmdCleanSessions() {
358
+ console.log("\nOpenClaw Bridge — Clean Sessions");
359
+ console.log("=================================\n");
360
+ const ecosystem = findEcosystem();
361
+ if (!ecosystem) {
362
+ console.error("Could not find openclaw-instances directory.");
363
+ process.exit(1);
364
+ }
365
+ const instancesDir = instancesDirFromEcosystem(ecosystem);
366
+ let totalFiles = 0;
367
+ let totalBytes = 0;
368
+ // Find all */agent/sessions/ directories
369
+ if (!existsSync(instancesDir)) {
370
+ console.log("Instances directory not found.");
371
+ return;
372
+ }
373
+ const agentDirs = readdirSync(instancesDir, { withFileTypes: true })
374
+ .filter((e) => e.isDirectory())
375
+ .map((e) => e.name);
376
+ for (const agentDir of agentDirs) {
377
+ const sessionsDir = join(instancesDir, agentDir, "agent", "sessions");
378
+ if (!existsSync(sessionsDir))
379
+ continue;
380
+ const files = readdirSync(sessionsDir).filter((f) => /\.(deleted|reset)\.|old-session/.test(f));
381
+ for (const file of files) {
382
+ const filePath = join(sessionsDir, file);
383
+ try {
384
+ const stats = statSync(filePath);
385
+ totalBytes += stats.size;
386
+ unlinkSync(filePath);
387
+ totalFiles++;
388
+ console.log(` Deleted: ${agentDir}/agent/sessions/${file}`);
389
+ }
390
+ catch {
391
+ console.log(` Warning: could not delete ${file}`);
392
+ }
393
+ }
394
+ }
395
+ console.log(`\nCleaned ${totalFiles} file(s), freed ${formatBytes(totalBytes)}.\n`);
396
+ }
397
+ async function cmdAddAgent() {
398
+ console.log("\nOpenClaw Bridge — Add Agent Wizard");
399
+ console.log("====================================\n");
400
+ const ecosystem = findEcosystem();
401
+ if (!ecosystem) {
402
+ console.error("Could not find ecosystem.config.cjs.");
403
+ process.exit(1);
404
+ }
405
+ const instancesDir = instancesDirFromEcosystem(ecosystem);
406
+ const bridgeConfig = loadConfig();
407
+ const rl = createInterface({ input, output });
408
+ const agentName = (await rl.question("Agent name (e.g. Designer): ")).trim();
409
+ if (!agentName) {
410
+ rl.close();
411
+ console.error("Agent name is required.");
412
+ process.exit(1);
413
+ }
414
+ const suggestedId = agentName.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
415
+ const agentIdInput = (await rl.question(`Agent ID [${suggestedId}]: `)).trim();
416
+ const agentId = agentIdInput || suggestedId;
417
+ const description = (await rl.question(`Description [${agentName} agent]: `)).trim() || `${agentName} agent`;
418
+ const modelChoices = [
419
+ "claude-sonnet-4-5-20250514",
420
+ "claude-opus-4-5-20250514",
421
+ "gpt-4o",
422
+ "gpt-4o-mini",
423
+ "gemini-2.5-flash",
424
+ "gemini-2.5-pro",
425
+ ];
426
+ console.log("\nAvailable models:");
427
+ modelChoices.forEach((m, i) => console.log(` ${i + 1}. ${m}`));
428
+ const modelInput = (await rl.question(`\nModel [1 = ${modelChoices[0]}]: `)).trim();
429
+ let model = modelChoices[0];
430
+ const modelNum = parseInt(modelInput, 10);
431
+ if (!isNaN(modelNum) && modelNum >= 1 && modelNum <= modelChoices.length) {
432
+ model = modelChoices[modelNum - 1];
433
+ }
434
+ else if (modelInput && !isNaN(parseInt(modelInput, 10)) === false) {
435
+ model = modelInput; // custom model string
436
+ }
437
+ rl.close();
438
+ // Auto-assign port by reading ecosystem.config.cjs
439
+ let nextPort = 18790;
440
+ try {
441
+ const ecosystemContent = readFileSync(ecosystem, "utf-8");
442
+ const portMatches = [...ecosystemContent.matchAll(/PORT['":\s]*[=:]?\s*['"]?(\d{4,5})/g)];
443
+ if (portMatches.length > 0) {
444
+ const ports = portMatches.map((m) => parseInt(m[1], 10)).filter((p) => p >= 18780 && p <= 19999);
445
+ if (ports.length > 0) {
446
+ nextPort = Math.max(...ports) + 1;
447
+ }
448
+ }
449
+ }
450
+ catch {
451
+ // Keep default
452
+ }
453
+ const agentDir = join(instancesDir, agentId);
454
+ if (existsSync(agentDir)) {
455
+ console.error(`\nDirectory already exists: ${agentDir}`);
456
+ process.exit(1);
457
+ }
458
+ // Determine load paths (platform-specific)
459
+ const extensionsDir = IS_WINDOWS
460
+ ? "C:\\\\openclaw-extensions"
461
+ : join(homedir(), "openclaw-extensions");
462
+ const hubUrl = bridgeConfig?.hubUrl ?? "http://localhost:3080";
463
+ const apiKey = bridgeConfig?.apiKey ?? "";
464
+ const managerPass = bridgeConfig?.managerPass ?? "";
465
+ // Create openclaw.json
466
+ const openclawJson = {
467
+ meta: { lastTouchedVersion: "2026.3.24" },
468
+ models: {
469
+ default: model,
470
+ mode: "merge",
471
+ },
472
+ plugins: {
473
+ allow: ["openclaw-bridge"],
474
+ load: { paths: [IS_WINDOWS ? "C:\\openclaw-extensions" : join(homedir(), "openclaw-extensions")] },
475
+ entries: {
476
+ "openclaw-bridge": {
477
+ enabled: true,
478
+ config: {
479
+ role: "normal",
480
+ agentId,
481
+ agentName,
482
+ description,
483
+ registry: {
484
+ baseUrl: hubUrl,
485
+ apiKey,
486
+ },
487
+ fileRelay: {
488
+ baseUrl: hubUrl,
489
+ apiKey,
490
+ },
491
+ localManager: {
492
+ enabled: true,
493
+ hubUrl,
494
+ managerPass,
495
+ },
496
+ },
497
+ },
498
+ },
499
+ },
500
+ gateway: {
501
+ http: {
502
+ endpoints: {
503
+ chatCompletions: { enabled: true },
504
+ },
505
+ },
506
+ },
507
+ };
508
+ // Create run.sh
509
+ const runSh = `#!/usr/bin/env bash
510
+ cd "$(dirname "$0")"
511
+ export OPENCLAW_HOME="$(pwd)/home"
512
+ export OPENCLAW_STATE_DIR="$(pwd)/state"
513
+ export OPENCLAW_CONFIG_PATH="$(pwd)/openclaw.json"
514
+ export NODE_OPTIONS="--max-old-space-size=256"
515
+ export OPENCLAW_PROFILE="${agentId}"
516
+ export OPENCLAW_GATEWAY_PORT="${nextPort}"
517
+
518
+ # Kill any orphan process on our port before starting
519
+ if [[ "$OSTYPE" == "darwin"* ]] || [[ "$OSTYPE" == "linux"* ]]; then
520
+ lsof -ti:$OPENCLAW_GATEWAY_PORT | xargs kill -9 2>/dev/null && sleep 1
521
+ elif command -v netstat &>/dev/null; then
522
+ orphan=$(netstat -ano 2>/dev/null | grep ":$OPENCLAW_GATEWAY_PORT.*LISTEN" | awk '{print $5}' | head -1)
523
+ [ -n "$orphan" ] && taskkill //F //PID $orphan 2>/dev/null && sleep 1
524
+ fi
525
+
526
+ exec openclaw gateway --port ${nextPort}
527
+ `;
528
+ // Create run.ps1
529
+ const runPs1 = `# Run script for ${agentName}
530
+ $PORT = ${nextPort}
531
+ $AgentDir = Split-Path -Parent $MyInvocation.MyCommand.Path
532
+
533
+ # Kill any process using our port (orphan cleanup)
534
+ try {
535
+ $connections = netstat -ano | Select-String ":$PORT "
536
+ foreach ($conn in $connections) {
537
+ $pid = ($conn -split '\\s+')[-1]
538
+ if ($pid -match '^\\d+$') {
539
+ Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue
540
+ }
541
+ }
542
+ } catch {}
543
+
544
+ $env:OPENCLAW_GATEWAY_PORT = $PORT
545
+ $env:OPENCLAW_CONFIG_PATH = "$AgentDir\\openclaw.json"
546
+
547
+ openclaw start
548
+ `;
549
+ // Write files
550
+ mkdirSync(agentDir, { recursive: true });
551
+ writeFileSync(join(agentDir, "openclaw.json"), JSON.stringify(openclawJson, null, 2), "utf-8");
552
+ writeFileSync(join(agentDir, "run.sh"), runSh, { encoding: "utf-8", mode: 0o755 });
553
+ writeFileSync(join(agentDir, "run.ps1"), runPs1, "utf-8");
554
+ // Update ecosystem.config.cjs — add to instances array
555
+ let ecosystemContent = readFileSync(ecosystem, "utf-8");
556
+ const newEntry = ` { name: 'gw-${agentId}', dir: '${agentId}', port: '${nextPort}', profile: '${agentId}' },`;
557
+ // Insert before the closing ]; of the instances array
558
+ const instancesArrayEnd = /(\n\];)/;
559
+ if (instancesArrayEnd.test(ecosystemContent)) {
560
+ ecosystemContent = ecosystemContent.replace(instancesArrayEnd, `\n${newEntry}$1`);
561
+ }
562
+ else {
563
+ console.log(`\n ⚠️ Could not auto-update ecosystem.config.cjs. Add manually:`);
564
+ console.log(` ${newEntry}`);
565
+ }
566
+ writeFileSync(ecosystem, ecosystemContent, "utf-8");
567
+ console.log(`\nAgent "${agentName}" (${agentId}) created successfully!`);
568
+ console.log(`\n Directory: ${agentDir}`);
569
+ console.log(` Port: ${nextPort}`);
570
+ console.log(` Model: ${model}`);
571
+ console.log(` Config: ${join(agentDir, "openclaw.json")}`);
572
+ console.log(` Ecosystem: updated ${ecosystem}`);
573
+ console.log(`\nNext steps:`);
574
+ console.log(` 1. Review ${join(agentDir, "openclaw.json")}`);
575
+ console.log(` 2. Run: openclaw-bridge start`);
576
+ console.log(` 3. Run: openclaw-bridge status\n`);
577
+ }
578
+ async function cmdDoctor() {
579
+ console.log("\nOpenClaw Bridge — Doctor");
580
+ console.log("=========================\n");
581
+ const checks = [];
582
+ // PM2 installed
583
+ {
584
+ let ok = false;
585
+ let detail = "";
586
+ try {
587
+ detail = run("pm2 --version", { silent: true });
588
+ ok = !!detail;
589
+ }
590
+ catch {
591
+ detail = "not found";
592
+ }
593
+ checks.push({ label: "PM2 installed", ok, detail: ok ? `v${detail.trim()}` : detail });
594
+ }
595
+ // openclaw CLI installed
596
+ {
597
+ let ok = false;
598
+ let detail = "";
599
+ try {
600
+ detail = run("openclaw --version", { silent: true });
601
+ ok = !!detail;
602
+ }
603
+ catch {
604
+ detail = "not found";
605
+ }
606
+ checks.push({ label: "openclaw CLI installed", ok, detail: ok ? detail.trim() : detail });
607
+ }
608
+ // Node version
609
+ {
610
+ const nodeVer = process.version;
611
+ const major = parseInt(nodeVer.slice(1), 10);
612
+ const ok = major >= 18;
613
+ checks.push({ label: "Node.js version", ok, detail: `${nodeVer}${ok ? "" : " (need >=18)"}` });
614
+ }
615
+ // ecosystem.config.cjs
616
+ {
617
+ const ecosystem = findEcosystem();
618
+ const ok = !!ecosystem;
619
+ checks.push({ label: "ecosystem.config.cjs found", ok, detail: ok ? ecosystem : "not found" });
620
+ }
621
+ // Hub reachable
622
+ const config = loadConfig();
623
+ if (!config) {
624
+ checks.push({ label: "Hub reachable", ok: false, detail: "not configured (run: openclaw-bridge setup)" });
625
+ }
626
+ else {
627
+ let ok = false;
628
+ let detail = "";
629
+ try {
630
+ await fetchJson(`${config.hubUrl}/api/v1/registry/discover`, config.apiKey);
631
+ ok = true;
632
+ detail = config.hubUrl;
633
+ }
634
+ catch (err) {
635
+ detail = `${config.hubUrl} — ${err.message}`;
636
+ }
637
+ checks.push({ label: "Hub reachable", ok, detail });
638
+ }
639
+ // Port conflicts (scan 18790-18799)
640
+ {
641
+ const portRange = Array.from({ length: 10 }, (_, i) => 18790 + i);
642
+ const conflictPorts = [];
643
+ for (const port of portRange) {
644
+ try {
645
+ const out = run(IS_WINDOWS
646
+ ? `netstat -ano | findstr ":${port} "`
647
+ : `ss -tlnp 2>/dev/null | grep :${port} || lsof -ti tcp:${port} 2>/dev/null || true`, { silent: true });
648
+ if (out.trim())
649
+ conflictPorts.push(port);
650
+ }
651
+ catch {
652
+ // no conflict
653
+ }
654
+ }
655
+ const ok = conflictPorts.length === 0;
656
+ checks.push({
657
+ label: "Port conflicts (18790-18799)",
658
+ ok,
659
+ detail: ok ? "none" : `conflicts on: ${conflictPorts.join(", ")}`,
660
+ });
661
+ }
662
+ // Print results
663
+ for (const check of checks) {
664
+ const icon = check.ok ? "✅" : "❌";
665
+ const detail = check.detail ? ` (${check.detail})` : "";
666
+ console.log(` ${icon} ${check.label}${detail}`);
667
+ }
668
+ const failCount = checks.filter((c) => !c.ok).length;
669
+ console.log(`\n${failCount === 0 ? "All checks passed." : `${failCount} issue(s) found.`}\n`);
670
+ }
671
+ async function cmdUpgrade() {
672
+ console.log("\nOpenClaw Bridge — Upgrade");
673
+ console.log("==========================\n");
674
+ let updatedPlugin = false;
675
+ let updatedCli = false;
676
+ // 1. Try installing/upgrading as OpenClaw plugin
677
+ console.log("Checking OpenClaw plugin installation...");
678
+ try {
679
+ // First attempt: just try to install
680
+ const installResult = run("openclaw plugins install openclaw-bridge 2>&1", { silent: true });
681
+ if (installResult.includes("openclaw-bridge")) {
682
+ updatedPlugin = true;
683
+ console.log(" Plugin installed/updated.");
684
+ }
685
+ }
686
+ catch (installErr) {
687
+ const errMsg = String(installErr?.stdout || installErr?.message || installErr || "");
688
+ if (errMsg.includes("plugin already exists") || errMsg.includes("already exists")) {
689
+ // Parse the existing path from error: "plugin already exists: /path/to/openclaw-bridge (delete it first)"
690
+ const pathMatch = errMsg.match(/already exists:\s*(.+?)\s*\(/);
691
+ const existingPath = pathMatch?.[1]?.trim();
692
+ if (existingPath && existsSync(existingPath)) {
693
+ console.log(` Old plugin found at ${existingPath}. Removing...`);
694
+ run(IS_WINDOWS ? `rmdir /s /q "${existingPath}"` : `rm -rf "${existingPath}"`, { silent: true });
695
+ }
696
+ else {
697
+ // Fallback: search common plugin directories
698
+ const candidates = [
699
+ join(homedir(), ".openclaw", "extensions", "openclaw-bridge"),
700
+ join(homedir(), "openclaw-extensions", "openclaw-bridge"),
701
+ IS_WINDOWS ? "C:\\openclaw-extensions\\openclaw-bridge" : "",
702
+ ].filter(Boolean);
703
+ for (const dir of candidates) {
704
+ if (existsSync(dir)) {
705
+ console.log(` Old plugin found at ${dir}. Removing...`);
706
+ run(IS_WINDOWS ? `rmdir /s /q "${dir}"` : `rm -rf "${dir}"`, { silent: true });
707
+ break;
708
+ }
709
+ }
710
+ }
711
+ // Retry install after removing old version
712
+ console.log(" Installing new version...");
713
+ try {
714
+ runInherit("openclaw plugins install openclaw-bridge");
715
+ updatedPlugin = true;
716
+ console.log(" Plugin updated.");
717
+ }
718
+ catch {
719
+ console.log(" Plugin install failed after removing old version. Try manually:");
720
+ console.log(" openclaw plugins install openclaw-bridge");
721
+ }
722
+ }
723
+ else {
724
+ console.log(" openclaw CLI not found or plugins command failed. Skipping plugin check.");
725
+ }
726
+ }
727
+ // 2. Check if installed as global npm package
728
+ console.log("\nChecking global npm installation...");
729
+ try {
730
+ const globalPath = run("npm list -g openclaw-bridge --depth=0", { silent: true });
731
+ if (globalPath.includes("openclaw-bridge")) {
732
+ console.log(" Found global openclaw-bridge. Updating...");
733
+ runInherit("npm install -g openclaw-bridge");
734
+ updatedCli = true;
735
+ console.log(" CLI updated.");
736
+ }
737
+ else {
738
+ console.log(" Not installed as global npm package.");
739
+ }
740
+ }
741
+ catch {
742
+ console.log(" Not installed as global npm package.");
743
+ }
744
+ // 3. If neither found, suggest installation
745
+ if (!updatedPlugin && !updatedCli) {
746
+ console.log("\nopenclaw-bridge is not installed. Choose an installation method:");
747
+ console.log(" 1. As OpenClaw plugin: openclaw plugins install openclaw-bridge");
748
+ console.log(" 2. As CLI tool: npm install -g openclaw-bridge");
749
+ console.log(" 3. Both (recommended for full functionality)");
750
+ }
751
+ else {
752
+ // Print new version
753
+ try {
754
+ const ver = run("npm view openclaw-bridge version", { silent: true });
755
+ console.log(`\nUpgraded to openclaw-bridge v${ver.trim()}`);
756
+ }
757
+ catch {
758
+ console.log("\nUpgrade complete.");
759
+ }
760
+ }
761
+ console.log();
762
+ }
763
+ // ── Usage ─────────────────────────────────────────────────────────────────────
764
+ function printUsage() {
765
+ console.log(`
766
+ openclaw-bridge — OpenClaw Bridge CLI
767
+
768
+ Usage:
769
+ openclaw-bridge <command> [args]
770
+
771
+ Commands:
772
+ setup Interactive setup (Hub URL, API key, manager password)
773
+ status Show PM2 processes and Hub connection status
774
+ start Start all openclaw instances via ecosystem.config.cjs
775
+ stop Stop all PM2 processes
776
+ restart [agent] Restart specific agent or all (gw- prefix auto-applied)
777
+ logs [agent] View PM2 logs for agent or all
778
+ backup Backup openclaw instances (tar.gz with encryption)
779
+ clean-sessions Clean old session files (*.deleted.*, *.reset.*, *.old-session*)
780
+ add-agent Wizard to create a new agent instance
781
+ doctor Diagnose common issues
782
+ upgrade Upgrade openclaw-bridge (plugin + CLI)
783
+
784
+ Examples:
785
+ openclaw-bridge setup
786
+ openclaw-bridge status
787
+ openclaw-bridge start
788
+ openclaw-bridge restart writer
789
+ openclaw-bridge logs pm
790
+ openclaw-bridge add-agent
791
+ openclaw-bridge doctor
792
+ `);
793
+ }
794
+ // ── Entry point ───────────────────────────────────────────────────────────────
795
+ const command = process.argv[2];
796
+ const arg = process.argv[3];
797
+ (async () => {
798
+ switch (command) {
799
+ case "setup":
800
+ await cmdSetup();
801
+ break;
802
+ case "status":
803
+ await cmdStatus();
804
+ break;
805
+ case "start":
806
+ await cmdStart();
807
+ break;
808
+ case "stop":
809
+ await cmdStop();
810
+ break;
811
+ case "restart":
812
+ await cmdRestart(arg);
813
+ break;
814
+ case "logs":
815
+ await cmdLogs(arg);
816
+ break;
817
+ case "backup":
818
+ await cmdBackup();
819
+ break;
820
+ case "clean-sessions":
821
+ await cmdCleanSessions();
822
+ break;
823
+ case "add-agent":
824
+ await cmdAddAgent();
825
+ break;
826
+ case "doctor":
827
+ await cmdDoctor();
828
+ break;
829
+ case "upgrade":
830
+ await cmdUpgrade();
831
+ break;
832
+ case "--help":
833
+ case "-h":
834
+ case "help":
835
+ case undefined:
836
+ printUsage();
837
+ break;
838
+ default:
839
+ console.error(`Unknown command: ${command}\n`);
840
+ printUsage();
841
+ process.exit(1);
842
+ }
843
+ })();