oomi-ai 0.2.7 → 0.2.10
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/README.md +9 -0
- package/agent_instructions.md +5 -2
- package/bin/oomi-ai.js +356 -17
- package/package.json +1 -1
- package/skills/oomi/agent_instructions.md +5 -2
package/README.md
CHANGED
|
@@ -59,6 +59,15 @@ If you need a fresh auth link later (without re-pairing), run:
|
|
|
59
59
|
oomi openclaw invite --app-url https://your-oomi-app.vercel.app
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
+
Bridge lifecycle handler (singleton, one bridge per host/device):
|
|
63
|
+
```
|
|
64
|
+
oomi openclaw bridge ensure --detach # start if needed; no-op if already running
|
|
65
|
+
oomi openclaw bridge ps # list bridge pids
|
|
66
|
+
oomi openclaw bridge stop # stop all bridge workers
|
|
67
|
+
oomi openclaw bridge restart --detach # clean restart as background worker
|
|
68
|
+
```
|
|
69
|
+
`oomi openclaw bridge --detach` is equivalent to `oomi openclaw bridge start --detach`.
|
|
70
|
+
|
|
62
71
|
Agent-intent mapping (recommended):
|
|
63
72
|
- If user says `Connect yourself to Oomi. Use app URL https://www.oomi.ai.`
|
|
64
73
|
- Run:
|
package/agent_instructions.md
CHANGED
|
@@ -26,10 +26,13 @@ If the user asks to connect OpenClaw to Oomi voice/web:
|
|
|
26
26
|
5. Configure channel account (`channels.oomi.accounts.default`) from:
|
|
27
27
|
- `oomi openclaw plugin --show-secrets --backend-url https://api.oomi.ai`
|
|
28
28
|
6. Restart OpenClaw.
|
|
29
|
-
7.
|
|
29
|
+
7. Ensure exactly one bridge worker is running (singleton handler):
|
|
30
|
+
- `oomi openclaw bridge ensure --detach`
|
|
31
|
+
- If stale/disconnected: `oomi openclaw bridge restart --detach`
|
|
32
|
+
8. If user provides an app URL (for example "Connect yourself to Oomi. Use app URL <URL>."):
|
|
30
33
|
- Use that URL directly in the pair command.
|
|
31
34
|
- Example: `oomi openclaw pair --app-url <URL> --no-start`
|
|
32
|
-
|
|
35
|
+
9. Return this exact result format to the user:
|
|
33
36
|
- `Oomi Connect Ready`
|
|
34
37
|
- `Auth Link: ...`
|
|
35
38
|
- No extra narrative text before or after those lines.
|
package/bin/oomi-ai.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import path from 'path';
|
|
5
|
-
import { spawn } from 'child_process';
|
|
5
|
+
import { spawn, spawnSync } from 'child_process';
|
|
6
6
|
import { createPrivateKey, createPublicKey, randomUUID, sign as cryptoSign } from 'crypto';
|
|
7
7
|
import net from 'net';
|
|
8
8
|
import { lookup as dnsLookup } from 'dns/promises';
|
|
@@ -151,8 +151,8 @@ Commands:
|
|
|
151
151
|
openclaw install
|
|
152
152
|
Install agent instructions and the Oomi skill into OpenClaw.
|
|
153
153
|
|
|
154
|
-
openclaw bridge
|
|
155
|
-
|
|
154
|
+
openclaw bridge [start|ensure|stop|restart|ps]
|
|
155
|
+
Manage local OpenClaw-to-Oomi bridge lifecycle (singleton).
|
|
156
156
|
|
|
157
157
|
openclaw pair
|
|
158
158
|
Pair this OpenClaw host with Oomi and start bridge (single command).
|
|
@@ -204,8 +204,8 @@ function readFile(filePath) {
|
|
|
204
204
|
return fs.readFileSync(filePath, 'utf-8');
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
-
function writeFile(filePath, content) {
|
|
208
|
-
fs.writeFileSync(filePath, content);
|
|
207
|
+
function writeFile(filePath, content, options = undefined) {
|
|
208
|
+
fs.writeFileSync(filePath, content, options);
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
function resolveWorkspace() {
|
|
@@ -558,6 +558,10 @@ function resolveBridgeStatusPath() {
|
|
|
558
558
|
return path.join(os.homedir(), '.openclaw', 'oomi-bridge-status.json');
|
|
559
559
|
}
|
|
560
560
|
|
|
561
|
+
function resolveBridgeLockPath() {
|
|
562
|
+
return path.join(os.homedir(), '.openclaw', 'oomi-bridge.lock');
|
|
563
|
+
}
|
|
564
|
+
|
|
561
565
|
function defaultDeviceId() {
|
|
562
566
|
const host = os.hostname().toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 24) || 'openclaw';
|
|
563
567
|
return `oomi-${host}-${randomUUID().slice(0, 8)}`;
|
|
@@ -608,6 +612,136 @@ function updateBridgeStatus(partial) {
|
|
|
608
612
|
return next;
|
|
609
613
|
}
|
|
610
614
|
|
|
615
|
+
function normalizePid(value) {
|
|
616
|
+
const pid = Number(value);
|
|
617
|
+
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
618
|
+
return pid;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function isPidAlive(pid) {
|
|
622
|
+
const normalized = normalizePid(pid);
|
|
623
|
+
if (!normalized) return false;
|
|
624
|
+
try {
|
|
625
|
+
process.kill(normalized, 0);
|
|
626
|
+
return true;
|
|
627
|
+
} catch {
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function isBridgeWorkerCommand(command) {
|
|
633
|
+
const text = String(command || '').trim().toLowerCase();
|
|
634
|
+
if (!text.includes('openclaw bridge')) return false;
|
|
635
|
+
if (/\bopenclaw\s+bridge\s+(ps|stop|restart|ensure)\b/.test(text)) return false;
|
|
636
|
+
if (/\bopenclaw\s+bridge\s+start\b/.test(text)) return true;
|
|
637
|
+
if (/\bopenclaw\s+bridge(\s+--|$)/.test(text)) return true;
|
|
638
|
+
return false;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function isBridgeProcess(pid) {
|
|
642
|
+
const normalized = normalizePid(pid);
|
|
643
|
+
if (!normalized) return false;
|
|
644
|
+
if (!isPidAlive(normalized)) return false;
|
|
645
|
+
|
|
646
|
+
try {
|
|
647
|
+
const result = spawnSync('ps', ['-p', String(normalized), '-o', 'command='], {
|
|
648
|
+
encoding: 'utf8',
|
|
649
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
650
|
+
});
|
|
651
|
+
const command = String(result.stdout || '').trim();
|
|
652
|
+
if (!command) return true;
|
|
653
|
+
return isBridgeWorkerCommand(command);
|
|
654
|
+
} catch {
|
|
655
|
+
return true;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function readBridgeLock() {
|
|
660
|
+
return readJsonSafe(resolveBridgeLockPath()) || {};
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function clearStaleBridgeLock() {
|
|
664
|
+
const lockPath = resolveBridgeLockPath();
|
|
665
|
+
if (!fs.existsSync(lockPath)) return;
|
|
666
|
+
const lock = readBridgeLock();
|
|
667
|
+
const lockPid = normalizePid(lock.pid);
|
|
668
|
+
if (lockPid && isBridgeProcess(lockPid)) return;
|
|
669
|
+
try {
|
|
670
|
+
fs.unlinkSync(lockPath);
|
|
671
|
+
} catch {
|
|
672
|
+
// no-op
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function findRunningBridgeProcess() {
|
|
677
|
+
clearStaleBridgeLock();
|
|
678
|
+
|
|
679
|
+
const lock = readBridgeLock();
|
|
680
|
+
const lockPid = normalizePid(lock.pid);
|
|
681
|
+
if (lockPid && isBridgeProcess(lockPid)) {
|
|
682
|
+
return {
|
|
683
|
+
pid: lockPid,
|
|
684
|
+
source: 'lock',
|
|
685
|
+
deviceId: typeof lock.deviceId === 'string' ? lock.deviceId : '',
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const status = readBridgeStatus();
|
|
690
|
+
const statusPid = normalizePid(status.pid);
|
|
691
|
+
if (statusPid && isBridgeProcess(statusPid)) {
|
|
692
|
+
return {
|
|
693
|
+
pid: statusPid,
|
|
694
|
+
source: 'status',
|
|
695
|
+
deviceId: typeof status.deviceId === 'string' ? status.deviceId : '',
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function acquireBridgeLock(deviceId) {
|
|
703
|
+
const lockPath = resolveBridgeLockPath();
|
|
704
|
+
ensureDir(path.dirname(lockPath));
|
|
705
|
+
const payload = {
|
|
706
|
+
pid: process.pid,
|
|
707
|
+
deviceId,
|
|
708
|
+
acquiredAt: bridgeNowIso(),
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
const writeLock = () => writeFile(lockPath, JSON.stringify(payload, null, 2) + '\n', { flag: 'wx' });
|
|
712
|
+
|
|
713
|
+
try {
|
|
714
|
+
writeLock();
|
|
715
|
+
} catch (err) {
|
|
716
|
+
const code = err && typeof err === 'object' ? err.code : '';
|
|
717
|
+
if (code !== 'EEXIST') {
|
|
718
|
+
throw err;
|
|
719
|
+
}
|
|
720
|
+
clearStaleBridgeLock();
|
|
721
|
+
const existing = findRunningBridgeProcess();
|
|
722
|
+
if (existing && existing.pid !== process.pid) {
|
|
723
|
+
throw new Error(
|
|
724
|
+
`Bridge already running (pid ${existing.pid})${existing.deviceId ? ` for device ${existing.deviceId}` : ''}.`
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
writeLock();
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const release = () => {
|
|
731
|
+
const current = readBridgeLock();
|
|
732
|
+
const currentPid = normalizePid(current.pid);
|
|
733
|
+
if (currentPid && currentPid !== process.pid) return;
|
|
734
|
+
try {
|
|
735
|
+
fs.unlinkSync(lockPath);
|
|
736
|
+
} catch {
|
|
737
|
+
// no-op
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
process.once('exit', release);
|
|
742
|
+
return release;
|
|
743
|
+
}
|
|
744
|
+
|
|
611
745
|
async function claimBridgeDeviceToken({ brokerHttp, pairCode, deviceId }) {
|
|
612
746
|
const response = await fetch(`${brokerHttp.replace(/\/$/, '')}/v1/pair/claim`, {
|
|
613
747
|
method: 'POST',
|
|
@@ -786,7 +920,9 @@ function prepareGatewayFrameForLocalGateway(frameText, gatewayAuth, options = {}
|
|
|
786
920
|
? 'node-host'
|
|
787
921
|
: (typeof client.id === 'string' && client.id.trim() ? client.id.trim() : 'node-host');
|
|
788
922
|
client.version = typeof client.version === 'string' && client.version.trim() ? client.version.trim() : '0.1.0';
|
|
789
|
-
client.platform =
|
|
923
|
+
client.platform = proxiedBrowserClient
|
|
924
|
+
? process.platform
|
|
925
|
+
: (typeof client.platform === 'string' && client.platform.trim() ? client.platform.trim() : process.platform);
|
|
790
926
|
client.mode = proxiedBrowserClient
|
|
791
927
|
? 'backend'
|
|
792
928
|
: (typeof client.mode === 'string' && client.mode.trim() ? client.mode.trim() : 'backend');
|
|
@@ -1030,13 +1166,129 @@ function buildBridgeDetachArgs(rawFlags = {}) {
|
|
|
1030
1166
|
}
|
|
1031
1167
|
|
|
1032
1168
|
function startBridgeDetachedProcess(rawFlags = {}) {
|
|
1169
|
+
const existing = findRunningBridgeProcess();
|
|
1170
|
+
if (existing) {
|
|
1171
|
+
return {
|
|
1172
|
+
pid: existing.pid,
|
|
1173
|
+
alreadyRunning: true,
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1033
1177
|
const args = buildBridgeDetachArgs(rawFlags);
|
|
1034
1178
|
const child = spawn(process.execPath, args, {
|
|
1035
1179
|
detached: true,
|
|
1036
1180
|
stdio: 'ignore',
|
|
1037
1181
|
});
|
|
1038
1182
|
child.unref();
|
|
1039
|
-
return
|
|
1183
|
+
return {
|
|
1184
|
+
pid: child.pid,
|
|
1185
|
+
alreadyRunning: false,
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function listBridgeProcessPids() {
|
|
1190
|
+
const pids = new Set();
|
|
1191
|
+
const addPid = (value) => {
|
|
1192
|
+
const pid = normalizePid(value);
|
|
1193
|
+
if (!pid || pid === process.pid) return;
|
|
1194
|
+
if (!isBridgeProcess(pid)) return;
|
|
1195
|
+
pids.add(pid);
|
|
1196
|
+
};
|
|
1197
|
+
|
|
1198
|
+
const lock = readBridgeLock();
|
|
1199
|
+
addPid(lock.pid);
|
|
1200
|
+
|
|
1201
|
+
const status = readBridgeStatus();
|
|
1202
|
+
addPid(status.pid);
|
|
1203
|
+
|
|
1204
|
+
try {
|
|
1205
|
+
const result = spawnSync('ps', ['-Ao', 'pid=,command='], {
|
|
1206
|
+
encoding: 'utf8',
|
|
1207
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1208
|
+
});
|
|
1209
|
+
const output = String(result.stdout || '');
|
|
1210
|
+
for (const rawLine of output.split('\n')) {
|
|
1211
|
+
const line = rawLine.trim();
|
|
1212
|
+
if (!line) continue;
|
|
1213
|
+
const match = line.match(/^(\d+)\s+(.+)$/);
|
|
1214
|
+
if (!match) continue;
|
|
1215
|
+
const pid = Number(match[1]);
|
|
1216
|
+
const command = String(match[2] || '');
|
|
1217
|
+
if (!isBridgeWorkerCommand(command)) continue;
|
|
1218
|
+
addPid(pid);
|
|
1219
|
+
}
|
|
1220
|
+
} catch {
|
|
1221
|
+
// best-effort process scan
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
return Array.from(pids).sort((a, b) => a - b);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
async function waitForBridgePidsToExit(pids, timeoutMs) {
|
|
1228
|
+
const deadline = Date.now() + timeoutMs;
|
|
1229
|
+
while (Date.now() < deadline) {
|
|
1230
|
+
const alive = pids.filter((pid) => isBridgeProcess(pid));
|
|
1231
|
+
if (alive.length === 0) return [];
|
|
1232
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1233
|
+
}
|
|
1234
|
+
return pids.filter((pid) => isBridgeProcess(pid));
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
async function stopBridgeProcesses() {
|
|
1238
|
+
const targets = listBridgeProcessPids();
|
|
1239
|
+
if (targets.length === 0) {
|
|
1240
|
+
clearStaleBridgeLock();
|
|
1241
|
+
updateBridgeStatus({
|
|
1242
|
+
status: 'stopped',
|
|
1243
|
+
stopSignal: 'none',
|
|
1244
|
+
lastDisconnectAt: bridgeNowIso(),
|
|
1245
|
+
pid: null,
|
|
1246
|
+
});
|
|
1247
|
+
return {
|
|
1248
|
+
stopped: [],
|
|
1249
|
+
forceKilled: [],
|
|
1250
|
+
found: [],
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
for (const pid of targets) {
|
|
1255
|
+
try {
|
|
1256
|
+
process.kill(pid, 'SIGTERM');
|
|
1257
|
+
} catch {
|
|
1258
|
+
// no-op
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
let remaining = await waitForBridgePidsToExit(targets, 2500);
|
|
1263
|
+
const forceKilled = [];
|
|
1264
|
+
if (remaining.length > 0) {
|
|
1265
|
+
for (const pid of remaining) {
|
|
1266
|
+
try {
|
|
1267
|
+
process.kill(pid, 'SIGKILL');
|
|
1268
|
+
forceKilled.push(pid);
|
|
1269
|
+
} catch {
|
|
1270
|
+
// no-op
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
remaining = await waitForBridgePidsToExit(remaining, 1000);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
clearStaleBridgeLock();
|
|
1277
|
+
|
|
1278
|
+
const stopped = targets.filter((pid) => !remaining.includes(pid));
|
|
1279
|
+
updateBridgeStatus({
|
|
1280
|
+
status: 'stopped',
|
|
1281
|
+
stopSignal: forceKilled.length > 0 ? 'SIGKILL' : 'SIGTERM',
|
|
1282
|
+
lastDisconnectAt: bridgeNowIso(),
|
|
1283
|
+
pid: null,
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
return {
|
|
1287
|
+
stopped,
|
|
1288
|
+
forceKilled,
|
|
1289
|
+
found: targets,
|
|
1290
|
+
stillAlive: remaining,
|
|
1291
|
+
};
|
|
1040
1292
|
}
|
|
1041
1293
|
|
|
1042
1294
|
async function resolveBridgeRuntimeConfig(flags, bridgeState) {
|
|
@@ -1077,6 +1329,13 @@ async function resolveBridgeRuntimeConfig(flags, bridgeState) {
|
|
|
1077
1329
|
}
|
|
1078
1330
|
|
|
1079
1331
|
async function startOpenclawBridge(flags) {
|
|
1332
|
+
const runningBridge = findRunningBridgeProcess();
|
|
1333
|
+
if (runningBridge && runningBridge.pid !== process.pid) {
|
|
1334
|
+
throw new Error(
|
|
1335
|
+
`Bridge already running (pid ${runningBridge.pid})${runningBridge.deviceId ? ` for device ${runningBridge.deviceId}` : ''}.`
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1080
1339
|
const bridgeState = readBridgeState();
|
|
1081
1340
|
const runtimeConfig = await resolveBridgeRuntimeConfig(flags, bridgeState);
|
|
1082
1341
|
const brokerHttp = runtimeConfig.brokerHttp;
|
|
@@ -1084,6 +1343,7 @@ async function startOpenclawBridge(flags) {
|
|
|
1084
1343
|
const deviceId = resolveDeviceId(flags, bridgeState);
|
|
1085
1344
|
const pairCode = String(flags['pair-code'] || '').trim().toUpperCase();
|
|
1086
1345
|
const explicitDeviceToken = String(flags['device-token'] || '').trim();
|
|
1346
|
+
const releaseBridgeLock = acquireBridgeLock(deviceId);
|
|
1087
1347
|
let deviceToken = explicitDeviceToken;
|
|
1088
1348
|
if (!deviceToken && String(bridgeState.deviceId || '').trim() === deviceId) {
|
|
1089
1349
|
deviceToken = String(bridgeState.deviceToken || '').trim();
|
|
@@ -1671,6 +1931,7 @@ async function startOpenclawBridge(flags) {
|
|
|
1671
1931
|
stopSignal: signal,
|
|
1672
1932
|
pid: process.pid,
|
|
1673
1933
|
});
|
|
1934
|
+
releaseBridgeLock();
|
|
1674
1935
|
process.exit(0);
|
|
1675
1936
|
};
|
|
1676
1937
|
process.once('SIGINT', () => markStopped('SIGINT'));
|
|
@@ -1765,13 +2026,17 @@ async function pairAndStartOpenclawBridge(flags) {
|
|
|
1765
2026
|
}
|
|
1766
2027
|
|
|
1767
2028
|
if (detach) {
|
|
1768
|
-
const
|
|
2029
|
+
const result = startBridgeDetachedProcess({
|
|
1769
2030
|
'broker-http': managedConfig.brokerHttpUrl,
|
|
1770
2031
|
'broker-ws': brokerWs,
|
|
1771
2032
|
'device-id': deviceId,
|
|
1772
2033
|
'device-token': deviceToken,
|
|
1773
2034
|
});
|
|
1774
|
-
|
|
2035
|
+
if (result.alreadyRunning) {
|
|
2036
|
+
console.log(`Bridge already running (pid: ${result.pid}).`);
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
2039
|
+
console.log(`Bridge started in background (pid: ${result.pid}).`);
|
|
1775
2040
|
return;
|
|
1776
2041
|
}
|
|
1777
2042
|
|
|
@@ -1969,6 +2234,86 @@ function printOpenclawPluginSetup(flags) {
|
|
|
1969
2234
|
}
|
|
1970
2235
|
}
|
|
1971
2236
|
|
|
2237
|
+
async function startBridgeLifecycle(flags = {}) {
|
|
2238
|
+
if (Boolean(flags.detach)) {
|
|
2239
|
+
const detachedFlags = { ...flags };
|
|
2240
|
+
delete detachedFlags.detach;
|
|
2241
|
+
const result = startBridgeDetachedProcess(detachedFlags);
|
|
2242
|
+
if (result.alreadyRunning) {
|
|
2243
|
+
console.log(`Bridge already running (pid: ${result.pid}).`);
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
console.log(`Bridge started in background (pid: ${result.pid}).`);
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
const running = findRunningBridgeProcess();
|
|
2251
|
+
if (running) {
|
|
2252
|
+
console.log(
|
|
2253
|
+
`Bridge already running (pid ${running.pid})${running.deviceId ? ` for device ${running.deviceId}` : ''}.`
|
|
2254
|
+
);
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
await startOpenclawBridge(flags);
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
async function handleBridgeLifecycleCommand(flags = {}, actionRaw = '') {
|
|
2262
|
+
const action = String(actionRaw || 'start').trim().toLowerCase();
|
|
2263
|
+
|
|
2264
|
+
if (action === 'start' || action === 'ensure') {
|
|
2265
|
+
await startBridgeLifecycle(flags);
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
if (action === 'ps') {
|
|
2270
|
+
const pids = listBridgeProcessPids();
|
|
2271
|
+
if (pids.length === 0) {
|
|
2272
|
+
console.log('No bridge processes running.');
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
console.log(`Bridge processes: ${pids.join(', ')}`);
|
|
2276
|
+
return;
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
if (action === 'stop') {
|
|
2280
|
+
const result = await stopBridgeProcesses();
|
|
2281
|
+
if (result.found.length === 0) {
|
|
2282
|
+
console.log('No bridge processes running.');
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
console.log(`Stopped bridge processes: ${result.stopped.join(', ') || 'none'}.`);
|
|
2286
|
+
if (result.forceKilled.length > 0) {
|
|
2287
|
+
console.log(`Force-killed bridge processes: ${result.forceKilled.join(', ')}.`);
|
|
2288
|
+
}
|
|
2289
|
+
if (Array.isArray(result.stillAlive) && result.stillAlive.length > 0) {
|
|
2290
|
+
throw new Error(`Failed to stop bridge processes: ${result.stillAlive.join(', ')}`);
|
|
2291
|
+
}
|
|
2292
|
+
return;
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
if (action === 'restart') {
|
|
2296
|
+
const result = await stopBridgeProcesses();
|
|
2297
|
+
if (result.found.length > 0) {
|
|
2298
|
+
console.log(`Stopped bridge processes: ${result.stopped.join(', ') || 'none'}.`);
|
|
2299
|
+
if (result.forceKilled.length > 0) {
|
|
2300
|
+
console.log(`Force-killed bridge processes: ${result.forceKilled.join(', ')}.`);
|
|
2301
|
+
}
|
|
2302
|
+
} else {
|
|
2303
|
+
console.log('No existing bridge process found; starting fresh bridge.');
|
|
2304
|
+
}
|
|
2305
|
+
if (Array.isArray(result.stillAlive) && result.stillAlive.length > 0) {
|
|
2306
|
+
throw new Error(`Failed to stop bridge processes: ${result.stillAlive.join(', ')}`);
|
|
2307
|
+
}
|
|
2308
|
+
await startBridgeLifecycle(flags);
|
|
2309
|
+
return;
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
throw new Error(
|
|
2313
|
+
`Unknown bridge action: ${action}. Use: oomi openclaw bridge [start|ensure|stop|restart|ps]`
|
|
2314
|
+
);
|
|
2315
|
+
}
|
|
2316
|
+
|
|
1972
2317
|
async function main() {
|
|
1973
2318
|
const args = parseArgs(process.argv);
|
|
1974
2319
|
const command = args.command;
|
|
@@ -2001,14 +2346,8 @@ async function main() {
|
|
|
2001
2346
|
}
|
|
2002
2347
|
|
|
2003
2348
|
if (command === 'openclaw' && subcommand === 'bridge') {
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
delete detachedFlags.detach;
|
|
2007
|
-
const pid = startBridgeDetachedProcess(detachedFlags);
|
|
2008
|
-
console.log(`Bridge started in background (pid: ${pid}).`);
|
|
2009
|
-
return;
|
|
2010
|
-
}
|
|
2011
|
-
await startOpenclawBridge(args.flags);
|
|
2349
|
+
const bridgeAction = args.positionals[0] || 'start';
|
|
2350
|
+
await handleBridgeLifecycleCommand(args.flags, bridgeAction);
|
|
2012
2351
|
return;
|
|
2013
2352
|
}
|
|
2014
2353
|
|
package/package.json
CHANGED
|
@@ -26,10 +26,13 @@ If the user asks to connect OpenClaw to Oomi voice/web:
|
|
|
26
26
|
5. Configure channel account (`channels.oomi.accounts.default`) from:
|
|
27
27
|
- `oomi openclaw plugin --show-secrets --backend-url https://api.oomi.ai`
|
|
28
28
|
6. Restart OpenClaw.
|
|
29
|
-
7.
|
|
29
|
+
7. Ensure exactly one bridge worker is running (singleton handler):
|
|
30
|
+
- `oomi openclaw bridge ensure --detach`
|
|
31
|
+
- If stale/disconnected: `oomi openclaw bridge restart --detach`
|
|
32
|
+
8. If user provides an app URL (for example "Connect yourself to Oomi. Use app URL <URL>."):
|
|
30
33
|
- Use that URL directly in the pair command.
|
|
31
34
|
- Example: `oomi openclaw pair --app-url <URL> --no-start`
|
|
32
|
-
|
|
35
|
+
9. Return this exact result format to the user:
|
|
33
36
|
- `Oomi Connect Ready`
|
|
34
37
|
- `Auth Link: ...`
|
|
35
38
|
- No extra narrative text before or after those lines.
|