oomi-ai 0.3.7 → 0.4.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/README.md +51 -40
- package/agent_instructions.md +38 -139
- package/bin/oomi-ai.js +198 -35
- package/lib/gatewayProbe.js +60 -0
- package/lib/localAgent.js +234 -0
- package/lib/personaApiClient.js +6 -2
- package/package.json +5 -3
- package/skills/oomi/SKILL.md +53 -67
- package/skills/oomi/agent_instructions.md +149 -69
package/bin/oomi-ai.js
CHANGED
|
@@ -8,6 +8,8 @@ import net from 'net';
|
|
|
8
8
|
import { lookup as dnsLookup } from 'dns/promises';
|
|
9
9
|
import { fileURLToPath } from 'url';
|
|
10
10
|
import WebSocket from 'ws';
|
|
11
|
+
import { LOCAL_PROTOCOL_MIN, LOCAL_PROTOCOL_MAX, probeGateway } from '../lib/gatewayProbe.js';
|
|
12
|
+
import { LocalAgentRuntime, localAgentConfig, runLocalAgent, writePrivateJson, startChannelWorker } from '../lib/localAgent.js';
|
|
11
13
|
import { createPersonaApiClient } from '../lib/personaApiClient.js';
|
|
12
14
|
import { inferSpokenMetadataFromContent, normalizeSpokenMetadata } from '../lib/spokenMetadata.js';
|
|
13
15
|
import {
|
|
@@ -31,6 +33,8 @@ import {
|
|
|
31
33
|
} from '../lib/openclawProfile.js';
|
|
32
34
|
import { ensureSessionBridge, flushSessionQueue, flushWaitingForConnect, forwardFrameToSession } from './sessionBridgeState.js';
|
|
33
35
|
|
|
36
|
+
let bridgeConnectionName = '';
|
|
37
|
+
|
|
34
38
|
const MARKER_START = '<oomi-agent-instructions>';
|
|
35
39
|
const MARKER_END = '</oomi-agent-instructions>';
|
|
36
40
|
|
|
@@ -53,7 +57,6 @@ const BRIDGE_GATEWAY_REQUEST_TIMEOUT_MS = parsePositiveInteger(
|
|
|
53
57
|
30000
|
|
54
58
|
);
|
|
55
59
|
const BRIDGE_LAUNCHD_LABEL = 'ai.oomi.bridge';
|
|
56
|
-
const DEVICE_IDENTITY_PATH = resolveOpenclawIdentityPath();
|
|
57
60
|
const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
|
|
58
61
|
const BRIDGE_DEBUG_ENABLED = process.env.OOMI_BRIDGE_DEBUG === '1';
|
|
59
62
|
|
|
@@ -195,6 +198,20 @@ function usage() {
|
|
|
195
198
|
console.log(`oomi <command>
|
|
196
199
|
|
|
197
200
|
Commands:
|
|
201
|
+
connect --runtime openclaw|hermes|command [--name NAME] [--app-url URL]
|
|
202
|
+
Verify the selected local agent, pair once, start its bridge, and return an Auth Link.
|
|
203
|
+
Hermes: --profile PROFILE and --resume SESSION select the intended agent/session.
|
|
204
|
+
Other agents: --command '["/absolute/path/to/adapter"]' (JSON stdin / JSON stdout).
|
|
205
|
+
--cwd PATH selects the agent working directory; --name isolates multiple agents.
|
|
206
|
+
status [--name NAME]
|
|
207
|
+
Show broker connection and verified local agent health separately.
|
|
208
|
+
verify [--name NAME]
|
|
209
|
+
Send a harmless local test message and require a real agent reply.
|
|
210
|
+
invite [--name NAME] [--app-url URL]
|
|
211
|
+
Generate a fresh Auth Link from saved pairing without changing agents.
|
|
212
|
+
bridge [start|ensure|stop|restart|ps] [--name NAME] [--detach]
|
|
213
|
+
Manage only the named agent's bridge.
|
|
214
|
+
|
|
198
215
|
init
|
|
199
216
|
Install Oomi agent instructions into OpenClaw AGENTS.md.
|
|
200
217
|
|
|
@@ -233,7 +250,9 @@ Commands:
|
|
|
233
250
|
persona-apps apply-action <slug>
|
|
234
251
|
Apply an approved persona app action through the Oomi backend.
|
|
235
252
|
|
|
236
|
-
context health
|
|
253
|
+
context health [--window day|week|month|year|all|range] [--date YYYY-MM-DD]
|
|
254
|
+
[--from YYYY-MM-DD --to YYYY-MM-DD] [--shape summary|series|both]
|
|
255
|
+
[--group-by day|week|month|year] [--timezone IANA-zone] [--cursor YYYY-MM-DD]
|
|
237
256
|
Show the latest account-linked health context available to this paired OpenClaw device.
|
|
238
257
|
context health sync [--wait]
|
|
239
258
|
Request the linked Oomi mobile app to sync fresh HealthKit context.
|
|
@@ -572,7 +591,10 @@ async function handlePersonaAppApplyActionCommand(slug, flags = {}) {
|
|
|
572
591
|
|
|
573
592
|
async function handleContextHealthCommand(flags = {}) {
|
|
574
593
|
const client = createCliPersonaApiClient(flags);
|
|
575
|
-
const payload = await client.getHealthContext(
|
|
594
|
+
const payload = await client.getHealthContext({
|
|
595
|
+
window: flags.window, date: flags.date, from: flags.from, to: flags.to,
|
|
596
|
+
shape: flags.shape, groupBy: flags['group-by'], timezone: flags.timezone, cursor: flags.cursor,
|
|
597
|
+
});
|
|
576
598
|
printStructuredResult(payload, isTruthyFlag(flags.json));
|
|
577
599
|
}
|
|
578
600
|
|
|
@@ -770,19 +792,19 @@ function readOpenclawDefaultSessionKey() {
|
|
|
770
792
|
}
|
|
771
793
|
|
|
772
794
|
function resolveBridgeStatePath() {
|
|
773
|
-
return resolveOpenclawBridgeStatePath();
|
|
795
|
+
return bridgeConnectionName ? path.join(connectionDirectory(), 'oomi-bridge.json') : resolveOpenclawBridgeStatePath();
|
|
774
796
|
}
|
|
775
797
|
|
|
776
798
|
function resolveBridgeStatusPath() {
|
|
777
|
-
return resolveOpenclawBridgeStatusPath();
|
|
799
|
+
return bridgeConnectionName ? path.join(connectionDirectory(), 'oomi-bridge-status.json') : resolveOpenclawBridgeStatusPath();
|
|
778
800
|
}
|
|
779
801
|
|
|
780
802
|
function resolveBridgeLockPath() {
|
|
781
|
-
return resolveOpenclawBridgeLockPath();
|
|
803
|
+
return bridgeConnectionName ? path.join(connectionDirectory(), 'oomi-bridge.lock') : resolveOpenclawBridgeLockPath();
|
|
782
804
|
}
|
|
783
805
|
|
|
784
806
|
function resolveBridgeLiveLogPath() {
|
|
785
|
-
return resolveOpenclawBridgeLiveLogPath();
|
|
807
|
+
return bridgeConnectionName ? path.join(connectionDirectory(), 'logs', 'oomi-bridge-live.log') : resolveOpenclawBridgeLiveLogPath();
|
|
786
808
|
}
|
|
787
809
|
|
|
788
810
|
function resolveBridgeLaunchAgentPlistPath() {
|
|
@@ -815,7 +837,7 @@ function readBridgeState() {
|
|
|
815
837
|
function writeBridgeState(nextState) {
|
|
816
838
|
const statePath = resolveBridgeStatePath();
|
|
817
839
|
ensureDir(path.dirname(statePath));
|
|
818
|
-
|
|
840
|
+
writePrivateJson(statePath, nextState);
|
|
819
841
|
}
|
|
820
842
|
|
|
821
843
|
function readBridgeStatus() {
|
|
@@ -933,11 +955,11 @@ function isPidAlive(pid) {
|
|
|
933
955
|
|
|
934
956
|
function isBridgeWorkerCommand(command) {
|
|
935
957
|
const text = String(command || '').trim().toLowerCase();
|
|
936
|
-
if (
|
|
937
|
-
|
|
938
|
-
if (
|
|
939
|
-
if (/\
|
|
940
|
-
return
|
|
958
|
+
if (!/(?:oomi(?:-ai\.js)?|openclaw)\s+bridge\b/.test(text)) return false;
|
|
959
|
+
const connection = text.match(/--(?:connection|name)[=\s]+([a-z0-9_-]+)/)?.[1] || '';
|
|
960
|
+
if (connection !== bridgeConnectionName) return false;
|
|
961
|
+
if (/\bbridge\s+(ps|stop|restart|ensure|service)\b/.test(text)) return false;
|
|
962
|
+
return /\bbridge(?:\s+start)?(?:\s+--|$)/.test(text);
|
|
941
963
|
}
|
|
942
964
|
|
|
943
965
|
function isBridgeProcess(pid) {
|
|
@@ -1047,6 +1069,7 @@ function acquireBridgeLock(deviceId) {
|
|
|
1047
1069
|
async function claimBridgeDeviceToken({ brokerHttp, pairCode, deviceId }) {
|
|
1048
1070
|
const response = await fetch(`${brokerHttp.replace(/\/$/, '')}/v1/pair/claim`, {
|
|
1049
1071
|
method: 'POST',
|
|
1072
|
+
signal: AbortSignal.timeout(15000),
|
|
1050
1073
|
headers: { 'Content-Type': 'application/json' },
|
|
1051
1074
|
body: JSON.stringify({ pairCode, deviceId }),
|
|
1052
1075
|
});
|
|
@@ -1064,6 +1087,7 @@ async function requestManagedPairCode({ appUrl, label }) {
|
|
|
1064
1087
|
const baseUrl = appUrl.replace(/\/$/, '');
|
|
1065
1088
|
const response = await fetch(`${baseUrl}/api/gateway/managed/pair/start`, {
|
|
1066
1089
|
method: 'POST',
|
|
1090
|
+
signal: AbortSignal.timeout(15000),
|
|
1067
1091
|
headers: { 'Content-Type': 'application/json' },
|
|
1068
1092
|
body: JSON.stringify({ label }),
|
|
1069
1093
|
});
|
|
@@ -1080,6 +1104,7 @@ async function requestManagedPairCode({ appUrl, label }) {
|
|
|
1080
1104
|
async function requestBrokerPairCode({ brokerHttp, adminKey, label }) {
|
|
1081
1105
|
const response = await fetch(`${brokerHttp.replace(/\/$/, '')}/v1/pair/start`, {
|
|
1082
1106
|
method: 'POST',
|
|
1107
|
+
signal: AbortSignal.timeout(15000),
|
|
1083
1108
|
headers: {
|
|
1084
1109
|
'Content-Type': 'application/json',
|
|
1085
1110
|
Authorization: `Bearer ${adminKey}`,
|
|
@@ -1099,6 +1124,7 @@ async function requestBrokerPairCode({ brokerHttp, adminKey, label }) {
|
|
|
1099
1124
|
async function requestConnectInviteLink({ backendHttp, appUrl, sessionKey, deviceToken }) {
|
|
1100
1125
|
const response = await fetch(`${backendHttp.replace(/\/$/, '')}/v1/invite_links/start`, {
|
|
1101
1126
|
method: 'POST',
|
|
1127
|
+
signal: AbortSignal.timeout(15000),
|
|
1102
1128
|
headers: {
|
|
1103
1129
|
'Content-Type': 'application/json',
|
|
1104
1130
|
Authorization: `Bearer ${deviceToken}`,
|
|
@@ -1119,6 +1145,7 @@ async function fetchManagedGatewayConfig({ appUrl }) {
|
|
|
1119
1145
|
const baseUrl = appUrl.replace(/\/$/, '');
|
|
1120
1146
|
const response = await fetch(`${baseUrl}/api/gateway/managed/config`, {
|
|
1121
1147
|
method: 'GET',
|
|
1148
|
+
signal: AbortSignal.timeout(15000),
|
|
1122
1149
|
headers: { 'Content-Type': 'application/json' },
|
|
1123
1150
|
});
|
|
1124
1151
|
const payload = await response.json().catch(() => ({}));
|
|
@@ -1218,11 +1245,12 @@ function buildDeviceAuthPayloadV3({
|
|
|
1218
1245
|
}
|
|
1219
1246
|
|
|
1220
1247
|
function loadGatewayDeviceIdentity() {
|
|
1221
|
-
|
|
1248
|
+
const identityPath = resolveOpenclawIdentityPath();
|
|
1249
|
+
if (!fs.existsSync(identityPath)) {
|
|
1222
1250
|
return null;
|
|
1223
1251
|
}
|
|
1224
1252
|
try {
|
|
1225
|
-
const parsed = JSON.parse(readFile(
|
|
1253
|
+
const parsed = JSON.parse(readFile(identityPath));
|
|
1226
1254
|
if (
|
|
1227
1255
|
parsed &&
|
|
1228
1256
|
parsed.version === 1 &&
|
|
@@ -1312,12 +1340,8 @@ function prepareGatewayFrameForLocalGateway(frameText, gatewayAuth, options = {}
|
|
|
1312
1340
|
const rawParams = frame.params && typeof frame.params === 'object' ? frame.params : {};
|
|
1313
1341
|
const params = {};
|
|
1314
1342
|
|
|
1315
|
-
params.minProtocol =
|
|
1316
|
-
|
|
1317
|
-
: 3;
|
|
1318
|
-
params.maxProtocol = Number.isInteger(rawParams.maxProtocol) && rawParams.maxProtocol >= 1
|
|
1319
|
-
? rawParams.maxProtocol
|
|
1320
|
-
: 3;
|
|
1343
|
+
params.minProtocol = gatewayAuth.protocol || LOCAL_PROTOCOL_MIN;
|
|
1344
|
+
params.maxProtocol = gatewayAuth.protocol || LOCAL_PROTOCOL_MAX;
|
|
1321
1345
|
|
|
1322
1346
|
const clientInput = rawParams.client && typeof rawParams.client === 'object' ? rawParams.client : {};
|
|
1323
1347
|
const client = {};
|
|
@@ -2174,6 +2198,7 @@ async function runBridgePreflight({ brokerWs, gatewayUrl, gatewayConfigPath }) {
|
|
|
2174
2198
|
throw new Error('Broker WS URL is missing hostname.');
|
|
2175
2199
|
}
|
|
2176
2200
|
await dnsLookup(brokerHost);
|
|
2201
|
+
if (gatewayUrl === 'local-agent:') return;
|
|
2177
2202
|
|
|
2178
2203
|
let parsedGatewayUrl;
|
|
2179
2204
|
try {
|
|
@@ -2197,6 +2222,7 @@ function buildBridgeDetachArgs(rawFlags = {}) {
|
|
|
2197
2222
|
'device-token',
|
|
2198
2223
|
];
|
|
2199
2224
|
const args = [process.argv[1], 'openclaw', 'bridge'];
|
|
2225
|
+
if (bridgeConnectionName) args.push('--connection', bridgeConnectionName);
|
|
2200
2226
|
|
|
2201
2227
|
for (const key of orderedKeys) {
|
|
2202
2228
|
const value = rawFlags[key];
|
|
@@ -2262,6 +2288,10 @@ function listBridgeProcessPids() {
|
|
|
2262
2288
|
const status = readBridgeStatus();
|
|
2263
2289
|
addPid(status.pid);
|
|
2264
2290
|
|
|
2291
|
+
// Named connections are owned by their own lock and status, including when a
|
|
2292
|
+
// custom connections directory is used. Never sweep another installation.
|
|
2293
|
+
if (bridgeConnectionName) return Array.from(pids);
|
|
2294
|
+
|
|
2265
2295
|
try {
|
|
2266
2296
|
const result = spawnSync('ps', ['-Ao', 'pid=,command='], {
|
|
2267
2297
|
encoding: 'utf8',
|
|
@@ -2491,7 +2521,7 @@ async function stopBridgeLaunchdService() {
|
|
|
2491
2521
|
async function resolveBridgeRuntimeConfig(flags, bridgeState) {
|
|
2492
2522
|
const explicitBrokerHttp = String(flags['broker-http'] || '').trim();
|
|
2493
2523
|
const explicitBrokerWs = String(flags['broker-ws'] || '').trim();
|
|
2494
|
-
const appUrl = String(flags['app-url'] || process.env.OOMI_APP_URL || '').trim();
|
|
2524
|
+
const appUrl = String(flags['app-url'] || process.env.OOMI_APP_URL || bridgeState.appUrl || '').trim();
|
|
2495
2525
|
|
|
2496
2526
|
let brokerHttp = String(explicitBrokerHttp || process.env.OOMI_CHAT_BROKER_HTTP_URL || bridgeState.brokerHttp || '').trim();
|
|
2497
2527
|
let brokerWs = String(explicitBrokerWs || process.env.OOMI_CHAT_BROKER_DEVICE_WS_URL || bridgeState.brokerWs || '').trim();
|
|
@@ -2534,6 +2564,7 @@ async function startOpenclawBridge(flags) {
|
|
|
2534
2564
|
}
|
|
2535
2565
|
|
|
2536
2566
|
const bridgeState = readBridgeState();
|
|
2567
|
+
if (bridgeState.openclawHome) process.env.OPENCLAW_HOME = bridgeState.openclawHome;
|
|
2537
2568
|
const runtimeConfig = await resolveBridgeRuntimeConfig(flags, bridgeState);
|
|
2538
2569
|
const brokerHttp = runtimeConfig.brokerHttp;
|
|
2539
2570
|
const brokerWs = runtimeConfig.brokerWs;
|
|
@@ -2574,14 +2605,17 @@ async function startOpenclawBridge(flags) {
|
|
|
2574
2605
|
console.log(`Claimed bridge device token for ${deviceId}.`);
|
|
2575
2606
|
}
|
|
2576
2607
|
|
|
2577
|
-
const
|
|
2578
|
-
|
|
2608
|
+
const localRuntime = bridgeState.localAgent
|
|
2609
|
+
? new LocalAgentRuntime(bridgeState.localAgent, path.dirname(resolveBridgeStatePath()), (health) => updateBridgeStatus(health))
|
|
2610
|
+
: null;
|
|
2611
|
+
const gateway = localRuntime ? { gatewayUrl: 'local-agent:', configPath: resolveBridgeStatePath() } : readOpenclawGatewayConfig();
|
|
2612
|
+
if (!localRuntime && !gateway.token && !gateway.password) {
|
|
2579
2613
|
throw new Error(`Gateway auth token/password not found in ${gateway.configPath}.`);
|
|
2580
2614
|
}
|
|
2581
|
-
const gatewayDeviceIdentity = loadGatewayDeviceIdentity();
|
|
2582
|
-
if (!gatewayDeviceIdentity) {
|
|
2615
|
+
const gatewayDeviceIdentity = localRuntime ? null : loadGatewayDeviceIdentity();
|
|
2616
|
+
if (!localRuntime && !gatewayDeviceIdentity) {
|
|
2583
2617
|
console.warn(
|
|
2584
|
-
`[bridge] OpenClaw device identity not found at ${
|
|
2618
|
+
`[bridge] OpenClaw device identity not found at ${resolveOpenclawIdentityPath()}; device-signed connect may fail on newer gateways.`
|
|
2585
2619
|
);
|
|
2586
2620
|
}
|
|
2587
2621
|
|
|
@@ -2599,10 +2633,16 @@ async function startOpenclawBridge(flags) {
|
|
|
2599
2633
|
gatewayUrl: gateway.gatewayUrl,
|
|
2600
2634
|
gatewayConfigPath: gateway.configPath,
|
|
2601
2635
|
});
|
|
2636
|
+
if (!localRuntime) {
|
|
2637
|
+
const result = await probeGateway({ gateway, deviceIdentity: gatewayDeviceIdentity, prepareFrame: prepareGatewayFrameForLocalGateway });
|
|
2638
|
+
gateway.protocol = result.protocol;
|
|
2639
|
+
}
|
|
2640
|
+
updateBridgeStatus({ runtime: bridgeState.runtime || 'openclaw', runtimeReady: Boolean(bridgeState.verifiedAt), verifiedAt: bridgeState.verifiedAt || null, gatewayProtocol: gateway.protocol || null });
|
|
2602
2641
|
} catch (err) {
|
|
2603
2642
|
const failure = classifyBridgeFailure({ err });
|
|
2604
2643
|
updateBridgeStatus({
|
|
2605
2644
|
status: 'error',
|
|
2645
|
+
runtimeReady: false,
|
|
2606
2646
|
deviceId,
|
|
2607
2647
|
brokerWs,
|
|
2608
2648
|
brokerHttp,
|
|
@@ -2632,7 +2672,8 @@ async function startOpenclawBridge(flags) {
|
|
|
2632
2672
|
startedAt: bridgeNowIso(),
|
|
2633
2673
|
});
|
|
2634
2674
|
|
|
2635
|
-
console.log(`Starting
|
|
2675
|
+
console.log(`Starting ${bridgeState.runtime || 'openclaw'} bridge: device=${deviceId}`);
|
|
2676
|
+
const stopChannelWorker = localRuntime ? startChannelWorker(bridgeState, localRuntime) : () => {};
|
|
2636
2677
|
console.log(`Local gateway: ${gateway.gatewayUrl}`);
|
|
2637
2678
|
console.log(`Broker WS: ${brokerWs}`);
|
|
2638
2679
|
bridgeDebugLog(
|
|
@@ -3316,6 +3357,7 @@ async function startOpenclawBridge(flags) {
|
|
|
3316
3357
|
}
|
|
3317
3358
|
}
|
|
3318
3359
|
} else if (requestMeta.method === 'connect' && !responseMeta.ok) {
|
|
3360
|
+
updateBridgeStatus({ status: 'degraded', runtimeReady: false, lastErrorCode: 'gateway_connect_rejected', lastErrorMessage: gatewayPayload?.error?.message || 'Local agent rejected the gateway handshake.', hint: 'Run oomi verify for this connection, repair the local runtime, and restart its bridge.' });
|
|
3319
3361
|
const deferredFrames = Array.isArray(sessionBridge.waitingForConnect)
|
|
3320
3362
|
? sessionBridge.waitingForConnect.splice(0, sessionBridge.waitingForConnect.length)
|
|
3321
3363
|
: [];
|
|
@@ -3353,6 +3395,12 @@ async function startOpenclawBridge(flags) {
|
|
|
3353
3395
|
}
|
|
3354
3396
|
}
|
|
3355
3397
|
|
|
3398
|
+
if (gatewayPayload?.event === 'chat' && gatewayPayload.payload?.state === 'final') {
|
|
3399
|
+
updateBridgeStatus({ runtimeReady: true, lastReplyAt: bridgeNowIso() });
|
|
3400
|
+
} else if (gatewayPayload?.event === 'chat' && gatewayPayload.payload?.state === 'error') {
|
|
3401
|
+
updateBridgeStatus({ status: 'degraded', runtimeReady: false, lastErrorMessage: gatewayPayload.payload.errorMessage || 'The local agent failed to answer.' });
|
|
3402
|
+
}
|
|
3403
|
+
|
|
3356
3404
|
if (isGatewayRunStartedFrame(frame)) {
|
|
3357
3405
|
sendGatewayAck(brokerSocket, {
|
|
3358
3406
|
sessionId,
|
|
@@ -3445,7 +3493,7 @@ async function startOpenclawBridge(flags) {
|
|
|
3445
3493
|
const sessionBridge = ensureSessionBridge({
|
|
3446
3494
|
sessions: activeGatewaySockets,
|
|
3447
3495
|
sessionId,
|
|
3448
|
-
createSocket: () => new WebSocket(gateway.gatewayUrl),
|
|
3496
|
+
createSocket: () => localRuntime ? localRuntime.socket() : new WebSocket(gateway.gatewayUrl),
|
|
3449
3497
|
});
|
|
3450
3498
|
if (sessionBridge) setupGatewaySession(sessionId, sessionBridge);
|
|
3451
3499
|
return sessionBridge;
|
|
@@ -3798,8 +3846,10 @@ async function startOpenclawBridge(flags) {
|
|
|
3798
3846
|
}));
|
|
3799
3847
|
};
|
|
3800
3848
|
|
|
3801
|
-
const markStopped = (signal) => {
|
|
3849
|
+
const markStopped = async (signal) => {
|
|
3802
3850
|
reconnectState.stopped = true;
|
|
3851
|
+
stopChannelWorker();
|
|
3852
|
+
if (localRuntime) await localRuntime.queue;
|
|
3803
3853
|
if (reconnectState.timer) {
|
|
3804
3854
|
clearTimeout(reconnectState.timer);
|
|
3805
3855
|
reconnectState.timer = null;
|
|
@@ -3828,7 +3878,7 @@ async function startOpenclawBridge(flags) {
|
|
|
3828
3878
|
|
|
3829
3879
|
async function pairAndStartOpenclawBridge(flags) {
|
|
3830
3880
|
const bridgeState = readBridgeState();
|
|
3831
|
-
const appUrl = String(flags['app-url'] || process.env.OOMI_APP_URL || '
|
|
3881
|
+
const appUrl = String(flags['app-url'] || process.env.OOMI_APP_URL || bridgeState.appUrl || 'https://www.oomi.ai').trim();
|
|
3832
3882
|
const deviceId = resolveDeviceId(flags, bridgeState);
|
|
3833
3883
|
const label = String(flags.label || `${deviceId}-bridge`).trim();
|
|
3834
3884
|
const sessionKey = String(
|
|
@@ -3963,8 +4013,8 @@ async function createOpenclawInviteLink(flags) {
|
|
|
3963
4013
|
bridgeState.brokerHttp ||
|
|
3964
4014
|
''
|
|
3965
4015
|
).trim();
|
|
3966
|
-
const appUrl = String(flags['app-url'] || process.env.OOMI_APP_URL || '
|
|
3967
|
-
const configSessionKey = readOpenclawDefaultSessionKey();
|
|
4016
|
+
const appUrl = String(flags['app-url'] || process.env.OOMI_APP_URL || bridgeState.appUrl || 'https://www.oomi.ai').trim();
|
|
4017
|
+
const configSessionKey = bridgeConnectionName ? '' : readOpenclawDefaultSessionKey();
|
|
3968
4018
|
const sessionKey = String(
|
|
3969
4019
|
flags['session-key'] ||
|
|
3970
4020
|
process.env.OOMI_SESSION_KEY ||
|
|
@@ -4013,6 +4063,7 @@ async function createOpenclawInviteLink(flags) {
|
|
|
4013
4063
|
function printOpenclawBridgeStatus(flags) {
|
|
4014
4064
|
const bridgeState = readBridgeState();
|
|
4015
4065
|
const runtimeStatus = readBridgeStatus();
|
|
4066
|
+
if (runtimeStatus.pid && !isBridgeProcess(runtimeStatus.pid)) { runtimeStatus.status = 'stopped'; runtimeStatus.runtimeReady = false; }
|
|
4016
4067
|
const jsonOutput = isTruthyFlag(flags.json);
|
|
4017
4068
|
const redactToken = (value) => {
|
|
4018
4069
|
const text = String(value || '').trim();
|
|
@@ -4051,7 +4102,9 @@ function printOpenclawBridgeStatus(flags) {
|
|
|
4051
4102
|
console.log(`Device token: ${payload.bridgeState.deviceToken}`);
|
|
4052
4103
|
}
|
|
4053
4104
|
if (payload.runtime && typeof payload.runtime === 'object' && Object.keys(payload.runtime).length > 0) {
|
|
4054
|
-
console.log(`
|
|
4105
|
+
console.log(`Bridge state: ${String(payload.runtime.status || 'unknown')}`);
|
|
4106
|
+
console.log(`Agent runtime: ${bridgeState.runtime || 'openclaw'}${bridgeState.localAgent?.profile ? ` (profile ${bridgeState.localAgent.profile})` : ''}`);
|
|
4107
|
+
console.log(`Local reply verified: ${payload.runtime.runtimeReady ? 'yes' : 'no'}${payload.runtime.verifiedAt ? ` at ${payload.runtime.verifiedAt}` : ''}`);
|
|
4055
4108
|
if (payload.runtime.lastConnectedAt) {
|
|
4056
4109
|
console.log(`Last connected: ${payload.runtime.lastConnectedAt}`);
|
|
4057
4110
|
}
|
|
@@ -4329,6 +4382,90 @@ async function handleBridgeLifecycleCommand(flags = {}, actionRaw = '') {
|
|
|
4329
4382
|
);
|
|
4330
4383
|
}
|
|
4331
4384
|
|
|
4385
|
+
function connectionDirectory() {
|
|
4386
|
+
return path.join(process.env.OOMI_CONNECTIONS_DIR || path.join(os.homedir(), '.oomi', 'connections'), bridgeConnectionName);
|
|
4387
|
+
}
|
|
4388
|
+
|
|
4389
|
+
async function verifyLocalAgent(state) {
|
|
4390
|
+
if (state.openclawHome) process.env.OPENCLAW_HOME = state.openclawHome;
|
|
4391
|
+
let result;
|
|
4392
|
+
if (state.localAgent) {
|
|
4393
|
+
await runLocalAgent(state.localAgent, {
|
|
4394
|
+
sessionKey: `oomi:verify:${randomUUID()}`, idempotencyKey: randomUUID(), history: [],
|
|
4395
|
+
message: 'Oomi connection check. Reply with OOMI_OK only. Do not use tools or change anything.',
|
|
4396
|
+
});
|
|
4397
|
+
result = { verifiedAt: new Date().toISOString() };
|
|
4398
|
+
} else {
|
|
4399
|
+
result = await probeGateway({ gateway: readOpenclawGatewayConfig(), deviceIdentity: loadGatewayDeviceIdentity(), prepareFrame: prepareGatewayFrameForLocalGateway, verifyReply: true, timeoutMs: 180000 });
|
|
4400
|
+
}
|
|
4401
|
+
return { runtime: state.runtime || 'openclaw', ...result };
|
|
4402
|
+
}
|
|
4403
|
+
|
|
4404
|
+
async function connectLocalAgent(flags) {
|
|
4405
|
+
const runtime = String(flags.runtime || '');
|
|
4406
|
+
if (!['openclaw', 'hermes', 'command'].includes(runtime)) throw new Error('Specify --runtime openclaw, hermes, or command. Never select a different agent just because it is installed.');
|
|
4407
|
+
const previous = readBridgeState();
|
|
4408
|
+
if (previous.runtime && previous.runtime !== runtime) throw new Error('This connection belongs to a different runtime. Choose a different --name.');
|
|
4409
|
+
const appUrl = String(flags['app-url'] || previous.appUrl || 'https://www.oomi.ai').replace(/\/$/, '');
|
|
4410
|
+
if (previous.appUrl && previous.appUrl !== appUrl) throw new Error('This connection belongs to another Oomi site. Choose a different --name.');
|
|
4411
|
+
const state = { ...previous, runtime, appUrl };
|
|
4412
|
+
if (runtime === 'openclaw') state.openclawHome = previous.openclawHome || resolveOpenclawHome();
|
|
4413
|
+
if (runtime !== 'openclaw') {
|
|
4414
|
+
const saved = previous.localAgent;
|
|
4415
|
+
state.localAgent = localAgentConfig({
|
|
4416
|
+
...flags,
|
|
4417
|
+
command: flags.command || (saved ? JSON.stringify(saved.command) : undefined),
|
|
4418
|
+
profile: flags.profile ?? saved?.profile,
|
|
4419
|
+
'hermes-home': saved?.hermesHome,
|
|
4420
|
+
resume: flags.resume ?? saved?.resume,
|
|
4421
|
+
cwd: flags.cwd ?? saved?.cwd,
|
|
4422
|
+
});
|
|
4423
|
+
if (saved && JSON.stringify(saved) !== JSON.stringify(state.localAgent)) throw new Error('This name already identifies a different agent configuration. Use a new --name to change its command, profile, session, or working directory.');
|
|
4424
|
+
}
|
|
4425
|
+
console.log(`Verifying ${runtime}${state.localAgent?.profile ? ` profile ${state.localAgent.profile}` : ''} with a local test message...`);
|
|
4426
|
+
try {
|
|
4427
|
+
const verified = await verifyLocalAgent(state);
|
|
4428
|
+
state.verifiedAt = verified.verifiedAt;
|
|
4429
|
+
} catch (err) {
|
|
4430
|
+
updateBridgeStatus({ runtime, runtimeReady: false, status: 'degraded', lastErrorMessage: err.message });
|
|
4431
|
+
throw err;
|
|
4432
|
+
}
|
|
4433
|
+
if (!state.deviceToken) {
|
|
4434
|
+
const config = await fetchManagedGatewayConfig({ appUrl });
|
|
4435
|
+
state.brokerHttp = rewriteLoopbackBrokerUrlForAppHost(config.brokerHttpUrl, appUrl);
|
|
4436
|
+
state.brokerWs = rewriteLoopbackBrokerUrlForAppHost(config.brokerDeviceWsUrl, appUrl);
|
|
4437
|
+
const label = String(flags.label || flags.name || runtime);
|
|
4438
|
+
const pair = await requestManagedPairCode({ appUrl, label });
|
|
4439
|
+
state.deviceId = `${runtime}-${randomUUID()}`;
|
|
4440
|
+
const claim = await claimBridgeDeviceToken({ brokerHttp: state.brokerHttp, pairCode: pair.pairCode, deviceId: state.deviceId });
|
|
4441
|
+
state.deviceToken = claim.deviceToken;
|
|
4442
|
+
state.sessionKey = String(flags['session-key'] || (runtime === 'openclaw' ? readOpenclawDefaultSessionKey() : '') || 'agent:main:webchat:channel:oomi');
|
|
4443
|
+
state.claimedAt = new Date().toISOString();
|
|
4444
|
+
}
|
|
4445
|
+
// Stop this connection only; never leave a worker using an old adapter after a change.
|
|
4446
|
+
const supervised = !bridgeConnectionName && process.platform === 'darwin' && readBridgeLaunchdStatus().installed;
|
|
4447
|
+
const stopped = supervised ? await stopBridgeLaunchdService() : await stopBridgeProcesses();
|
|
4448
|
+
if (stopped.stillAlive?.length) throw new Error('The existing bridge did not stop. Resolve it before reconnecting.');
|
|
4449
|
+
writeBridgeState(state);
|
|
4450
|
+
updateBridgeStatus({ status: 'starting', runtime, runtimeReady: true, verifiedAt: state.verifiedAt, lastErrorMessage: '' });
|
|
4451
|
+
const child = supervised ? (startBridgeLaunchdService(), { pid: null }) : startBridgeDetachedProcess({ 'app-url': appUrl });
|
|
4452
|
+
const deadline = Date.now() + 20000;
|
|
4453
|
+
let connected = false;
|
|
4454
|
+
while (Date.now() < deadline) {
|
|
4455
|
+
const status = readBridgeStatus();
|
|
4456
|
+
if (status.pid && (!child.pid || status.pid === child.pid) && status.status === 'connected') { connected = true; break; }
|
|
4457
|
+
if ((!child.pid || status.pid === child.pid) && status.status === 'error') throw new Error(status.lastErrorMessage || 'Bridge startup failed.');
|
|
4458
|
+
if (child.pid && !isPidAlive(child.pid)) throw new Error(`Bridge exited. Check ${resolveBridgeLiveLogPath()}.`);
|
|
4459
|
+
await delay(200);
|
|
4460
|
+
}
|
|
4461
|
+
if (!connected) throw new Error('The local agent replied, but the Oomi bridge did not connect. Run oomi status for this connection.');
|
|
4462
|
+
const invite = await requestConnectInviteLink({ backendHttp: state.brokerHttp, appUrl, sessionKey: state.sessionKey, deviceToken: state.deviceToken });
|
|
4463
|
+
console.log(`Verified agent: ${runtime}${state.localAgent?.profile ? ` (${state.localAgent.profile})` : ''}`);
|
|
4464
|
+
console.log('Oomi Connect Ready');
|
|
4465
|
+
console.log(`Auth Link: ${invite.inviteUrl}`);
|
|
4466
|
+
console.log('Open the Auth Link to approve ownership, then send a message in Oomi to verify delivery end to end.');
|
|
4467
|
+
}
|
|
4468
|
+
|
|
4332
4469
|
async function main() {
|
|
4333
4470
|
const args = parseArgs(process.argv);
|
|
4334
4471
|
const command = args.command;
|
|
@@ -4339,6 +4476,29 @@ async function main() {
|
|
|
4339
4476
|
process.exit(0);
|
|
4340
4477
|
}
|
|
4341
4478
|
|
|
4479
|
+
const connection = args.flags.connection || args.flags.name || (command === 'connect' && args.flags.runtime !== 'openclaw' ? args.flags.runtime : '');
|
|
4480
|
+
if (connection && !/^[a-z0-9][a-z0-9_-]{0,63}$/.test(connection)) throw new Error('Connection name must use lowercase letters, numbers, underscores, or hyphens.');
|
|
4481
|
+
bridgeConnectionName = connection || '';
|
|
4482
|
+
|
|
4483
|
+
if (command === 'connect') return connectLocalAgent(args.flags);
|
|
4484
|
+
if (command === 'verify') {
|
|
4485
|
+
const state = readBridgeState();
|
|
4486
|
+
if (!state.deviceToken) throw new Error('No saved connection. Run oomi connect with an explicit runtime first.');
|
|
4487
|
+
try {
|
|
4488
|
+
const result = await verifyLocalAgent(state);
|
|
4489
|
+
writeBridgeState({ ...state, verifiedAt: result.verifiedAt });
|
|
4490
|
+
updateBridgeStatus({ runtimeReady: true, verifiedAt: result.verifiedAt, lastErrorMessage: '' });
|
|
4491
|
+
console.log(JSON.stringify(result, null, 2));
|
|
4492
|
+
} catch (err) {
|
|
4493
|
+
updateBridgeStatus({ runtimeReady: false, status: 'degraded', lastErrorMessage: err.message });
|
|
4494
|
+
throw err;
|
|
4495
|
+
}
|
|
4496
|
+
return;
|
|
4497
|
+
}
|
|
4498
|
+
if (command === 'status') return printOpenclawBridgeStatus(args.flags);
|
|
4499
|
+
if (command === 'invite') return createOpenclawInviteLink(args.flags);
|
|
4500
|
+
if (command === 'bridge') return handleBridgeLifecycleCommand(args.flags, subcommand || 'start');
|
|
4501
|
+
|
|
4342
4502
|
await maybeNotifyUpdate(command);
|
|
4343
4503
|
|
|
4344
4504
|
if (command === 'init') {
|
|
@@ -4363,6 +4523,7 @@ async function main() {
|
|
|
4363
4523
|
if (command === 'openclaw' && subcommand === 'bridge') {
|
|
4364
4524
|
const bridgeAction = String(args.positionals[0] || 'start').trim().toLowerCase();
|
|
4365
4525
|
if (bridgeAction === 'service') {
|
|
4526
|
+
if (bridgeConnectionName) throw new Error('Named connections use oomi bridge start --name NAME with your host process supervisor. The legacy OpenClaw service is only for the default connection.');
|
|
4366
4527
|
const serviceAction = args.positionals[1] || 'status';
|
|
4367
4528
|
await handleBridgeServiceCommand(serviceAction, args.flags);
|
|
4368
4529
|
return;
|
|
@@ -4461,7 +4622,9 @@ async function main() {
|
|
|
4461
4622
|
|
|
4462
4623
|
const __currentFilePath = fileURLToPath(import.meta.url);
|
|
4463
4624
|
const __invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : '';
|
|
4464
|
-
const __isDirectExecution = Boolean(__invokedPath)
|
|
4625
|
+
const __isDirectExecution = Boolean(__invokedPath)
|
|
4626
|
+
&& fs.existsSync(__invokedPath)
|
|
4627
|
+
&& fs.realpathSync(__invokedPath) === fs.realpathSync(__currentFilePath);
|
|
4465
4628
|
|
|
4466
4629
|
if (__isDirectExecution) {
|
|
4467
4630
|
main().catch((err) => {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
// The broker's protocol is independent of the installed local gateway version.
|
|
5
|
+
export const LOCAL_PROTOCOL_MIN = 3;
|
|
6
|
+
export const LOCAL_PROTOCOL_MAX = 4;
|
|
7
|
+
|
|
8
|
+
export function probeGateway({ gateway, deviceIdentity, prepareFrame, verifyReply = false, timeoutMs = 15000 }) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const socket = new WebSocket(gateway.gatewayUrl);
|
|
11
|
+
const connectId = randomUUID();
|
|
12
|
+
const runId = randomUUID();
|
|
13
|
+
let settled = false;
|
|
14
|
+
let sent = false;
|
|
15
|
+
let protocol;
|
|
16
|
+
const finish = (error, result) => {
|
|
17
|
+
if (settled) return;
|
|
18
|
+
settled = true;
|
|
19
|
+
clearTimeout(timer);
|
|
20
|
+
socket.removeAllListeners('message');
|
|
21
|
+
socket.close();
|
|
22
|
+
if (error) reject(error); else resolve(result);
|
|
23
|
+
};
|
|
24
|
+
const timer = setTimeout(() => finish(new Error('Local agent verification timed out. Check its gateway, model credentials, and logs.')), timeoutMs);
|
|
25
|
+
const connect = (nonce = '') => {
|
|
26
|
+
if (sent) return;
|
|
27
|
+
const frame = prepareFrame(JSON.stringify({ type: 'req', id: connectId, method: 'connect', params: {} }), gateway, { deviceIdentity, connectNonce: nonce });
|
|
28
|
+
if (frame.waitForChallenge) return;
|
|
29
|
+
sent = true;
|
|
30
|
+
socket.send(frame.frameText);
|
|
31
|
+
};
|
|
32
|
+
socket.on('open', () => { if (!deviceIdentity) connect(); });
|
|
33
|
+
socket.on('error', (err) => finish(new Error(`Local gateway verification failed: ${err.message}`)));
|
|
34
|
+
socket.on('close', (code) => finish(new Error(`Local gateway closed before verification completed (${code}).`)));
|
|
35
|
+
socket.on('message', (raw) => {
|
|
36
|
+
let frame;
|
|
37
|
+
try { frame = JSON.parse(raw.toString()); } catch { return; }
|
|
38
|
+
if (frame.event === 'connect.challenge') connect(frame.payload?.nonce);
|
|
39
|
+
if (frame.type === 'res' && (frame.id === connectId || frame.id === runId) && !frame.ok) {
|
|
40
|
+
finish(new Error(`Local agent rejected ${frame.id === connectId ? 'connect' : 'verification'}: ${frame.error?.message || frame.error?.code || 'unknown error'}`));
|
|
41
|
+
} else if (frame.type === 'res' && frame.id === connectId && frame.ok) {
|
|
42
|
+
protocol = frame.payload?.protocol;
|
|
43
|
+
if (![3, 4].includes(protocol)) return finish(new Error('Local gateway returned an unsupported protocol version. Update oomi-ai.'));
|
|
44
|
+
if (!verifyReply) return finish(null, { protocol });
|
|
45
|
+
socket.send(JSON.stringify({ type: 'req', id: runId, method: 'chat.send', params: {
|
|
46
|
+
sessionKey: `agent:main:oomi:verify:${runId}`, idempotencyKey: runId,
|
|
47
|
+
message: 'Oomi connection check. Reply with OOMI_OK only. Do not use tools or change anything.',
|
|
48
|
+
} }));
|
|
49
|
+
} else if (verifyReply && frame.event === 'chat' && frame.payload?.runId === runId) {
|
|
50
|
+
if (frame.payload.state === 'error' || frame.payload.state === 'aborted') return finish(new Error('Local agent could not complete the verification message.'));
|
|
51
|
+
if (frame.payload.state === 'final') {
|
|
52
|
+
const content = frame.payload.message?.content;
|
|
53
|
+
const text = typeof content === 'string' ? content : (Array.isArray(content) ? content.map((part) => part.text || '').join('') : '');
|
|
54
|
+
if (!text.trim()) return finish(new Error('Local agent returned an empty verification reply.'));
|
|
55
|
+
finish(null, { protocol, verifiedAt: new Date().toISOString() });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|