oomi-ai 0.2.6 → 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 +373 -19
- package/openclaw.plugin.json +1 -1
- 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',
|
|
@@ -771,10 +905,27 @@ function prepareGatewayFrameForLocalGateway(frameText, gatewayAuth, options = {}
|
|
|
771
905
|
const params = frame.params && typeof frame.params === 'object' ? frame.params : {};
|
|
772
906
|
|
|
773
907
|
const client = params.client && typeof params.client === 'object' ? params.client : {};
|
|
774
|
-
|
|
908
|
+
const incomingClientId = typeof client.id === 'string' ? client.id.trim().toLowerCase() : '';
|
|
909
|
+
const incomingClientMode = typeof client.mode === 'string' ? client.mode.trim().toLowerCase() : '';
|
|
910
|
+
const proxiedBrowserClient =
|
|
911
|
+
incomingClientMode === 'webchat' ||
|
|
912
|
+
incomingClientId === 'webchat-ui' ||
|
|
913
|
+
incomingClientId === 'webchat' ||
|
|
914
|
+
incomingClientId === 'clawdbot-control-ui';
|
|
915
|
+
|
|
916
|
+
// Frames relayed by this bridge originate from a local Node websocket, not a browser.
|
|
917
|
+
// Keep gateway auth/nonce flow, but normalize browser-mode connects to backend identity
|
|
918
|
+
// so Control UI/webchat Origin checks don't reject proxied sessions.
|
|
919
|
+
client.id = proxiedBrowserClient
|
|
920
|
+
? 'node-host'
|
|
921
|
+
: (typeof client.id === 'string' && client.id.trim() ? client.id.trim() : 'node-host');
|
|
775
922
|
client.version = typeof client.version === 'string' && client.version.trim() ? client.version.trim() : '0.1.0';
|
|
776
|
-
client.platform =
|
|
777
|
-
|
|
923
|
+
client.platform = proxiedBrowserClient
|
|
924
|
+
? process.platform
|
|
925
|
+
: (typeof client.platform === 'string' && client.platform.trim() ? client.platform.trim() : process.platform);
|
|
926
|
+
client.mode = proxiedBrowserClient
|
|
927
|
+
? 'backend'
|
|
928
|
+
: (typeof client.mode === 'string' && client.mode.trim() ? client.mode.trim() : 'backend');
|
|
778
929
|
params.client = client;
|
|
779
930
|
|
|
780
931
|
params.role = typeof params.role === 'string' && params.role.trim() ? params.role.trim() : 'operator';
|
|
@@ -1015,13 +1166,129 @@ function buildBridgeDetachArgs(rawFlags = {}) {
|
|
|
1015
1166
|
}
|
|
1016
1167
|
|
|
1017
1168
|
function startBridgeDetachedProcess(rawFlags = {}) {
|
|
1169
|
+
const existing = findRunningBridgeProcess();
|
|
1170
|
+
if (existing) {
|
|
1171
|
+
return {
|
|
1172
|
+
pid: existing.pid,
|
|
1173
|
+
alreadyRunning: true,
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1018
1177
|
const args = buildBridgeDetachArgs(rawFlags);
|
|
1019
1178
|
const child = spawn(process.execPath, args, {
|
|
1020
1179
|
detached: true,
|
|
1021
1180
|
stdio: 'ignore',
|
|
1022
1181
|
});
|
|
1023
1182
|
child.unref();
|
|
1024
|
-
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
|
+
};
|
|
1025
1292
|
}
|
|
1026
1293
|
|
|
1027
1294
|
async function resolveBridgeRuntimeConfig(flags, bridgeState) {
|
|
@@ -1062,6 +1329,13 @@ async function resolveBridgeRuntimeConfig(flags, bridgeState) {
|
|
|
1062
1329
|
}
|
|
1063
1330
|
|
|
1064
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
|
+
|
|
1065
1339
|
const bridgeState = readBridgeState();
|
|
1066
1340
|
const runtimeConfig = await resolveBridgeRuntimeConfig(flags, bridgeState);
|
|
1067
1341
|
const brokerHttp = runtimeConfig.brokerHttp;
|
|
@@ -1069,6 +1343,7 @@ async function startOpenclawBridge(flags) {
|
|
|
1069
1343
|
const deviceId = resolveDeviceId(flags, bridgeState);
|
|
1070
1344
|
const pairCode = String(flags['pair-code'] || '').trim().toUpperCase();
|
|
1071
1345
|
const explicitDeviceToken = String(flags['device-token'] || '').trim();
|
|
1346
|
+
const releaseBridgeLock = acquireBridgeLock(deviceId);
|
|
1072
1347
|
let deviceToken = explicitDeviceToken;
|
|
1073
1348
|
if (!deviceToken && String(bridgeState.deviceId || '').trim() === deviceId) {
|
|
1074
1349
|
deviceToken = String(bridgeState.deviceToken || '').trim();
|
|
@@ -1656,6 +1931,7 @@ async function startOpenclawBridge(flags) {
|
|
|
1656
1931
|
stopSignal: signal,
|
|
1657
1932
|
pid: process.pid,
|
|
1658
1933
|
});
|
|
1934
|
+
releaseBridgeLock();
|
|
1659
1935
|
process.exit(0);
|
|
1660
1936
|
};
|
|
1661
1937
|
process.once('SIGINT', () => markStopped('SIGINT'));
|
|
@@ -1750,13 +2026,17 @@ async function pairAndStartOpenclawBridge(flags) {
|
|
|
1750
2026
|
}
|
|
1751
2027
|
|
|
1752
2028
|
if (detach) {
|
|
1753
|
-
const
|
|
2029
|
+
const result = startBridgeDetachedProcess({
|
|
1754
2030
|
'broker-http': managedConfig.brokerHttpUrl,
|
|
1755
2031
|
'broker-ws': brokerWs,
|
|
1756
2032
|
'device-id': deviceId,
|
|
1757
2033
|
'device-token': deviceToken,
|
|
1758
2034
|
});
|
|
1759
|
-
|
|
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}).`);
|
|
1760
2040
|
return;
|
|
1761
2041
|
}
|
|
1762
2042
|
|
|
@@ -1954,6 +2234,86 @@ function printOpenclawPluginSetup(flags) {
|
|
|
1954
2234
|
}
|
|
1955
2235
|
}
|
|
1956
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
|
+
|
|
1957
2317
|
async function main() {
|
|
1958
2318
|
const args = parseArgs(process.argv);
|
|
1959
2319
|
const command = args.command;
|
|
@@ -1986,14 +2346,8 @@ async function main() {
|
|
|
1986
2346
|
}
|
|
1987
2347
|
|
|
1988
2348
|
if (command === 'openclaw' && subcommand === 'bridge') {
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
delete detachedFlags.detach;
|
|
1992
|
-
const pid = startBridgeDetachedProcess(detachedFlags);
|
|
1993
|
-
console.log(`Bridge started in background (pid: ${pid}).`);
|
|
1994
|
-
return;
|
|
1995
|
-
}
|
|
1996
|
-
await startOpenclawBridge(args.flags);
|
|
2349
|
+
const bridgeAction = args.positionals[0] || 'start';
|
|
2350
|
+
await handleBridgeLifecycleCommand(args.flags, bridgeAction);
|
|
1997
2351
|
return;
|
|
1998
2352
|
}
|
|
1999
2353
|
|
package/openclaw.plugin.json
CHANGED
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.
|