oomi-ai 0.3.8 → 0.5.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 +79 -35
- package/agent_instructions.md +63 -126
- package/bin/oomi-ai.js +230 -36
- package/lib/agentIdentity.js +122 -0
- package/lib/gatewayProbe.js +61 -0
- package/lib/localAgent.js +234 -0
- package/lib/personaApiClient.js +11 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +6 -3
- package/skills/oomi/SKILL.md +84 -60
- package/skills/oomi/agent_instructions.md +187 -65
package/bin/oomi-ai.js
CHANGED
|
@@ -8,6 +8,9 @@ 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';
|
|
13
|
+
import { validateAgentName, identityRequest, startIdentityWorker, handleIdentityCommand } from '../lib/agentIdentity.js';
|
|
11
14
|
import { createPersonaApiClient } from '../lib/personaApiClient.js';
|
|
12
15
|
import { inferSpokenMetadataFromContent, normalizeSpokenMetadata } from '../lib/spokenMetadata.js';
|
|
13
16
|
import {
|
|
@@ -31,6 +34,8 @@ import {
|
|
|
31
34
|
} from '../lib/openclawProfile.js';
|
|
32
35
|
import { ensureSessionBridge, flushSessionQueue, flushWaitingForConnect, forwardFrameToSession } from './sessionBridgeState.js';
|
|
33
36
|
|
|
37
|
+
let bridgeConnectionName = '';
|
|
38
|
+
|
|
34
39
|
const MARKER_START = '<oomi-agent-instructions>';
|
|
35
40
|
const MARKER_END = '</oomi-agent-instructions>';
|
|
36
41
|
|
|
@@ -53,7 +58,6 @@ const BRIDGE_GATEWAY_REQUEST_TIMEOUT_MS = parsePositiveInteger(
|
|
|
53
58
|
30000
|
|
54
59
|
);
|
|
55
60
|
const BRIDGE_LAUNCHD_LABEL = 'ai.oomi.bridge';
|
|
56
|
-
const DEVICE_IDENTITY_PATH = resolveOpenclawIdentityPath();
|
|
57
61
|
const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
|
|
58
62
|
const BRIDGE_DEBUG_ENABLED = process.env.OOMI_BRIDGE_DEBUG === '1';
|
|
59
63
|
|
|
@@ -195,6 +199,25 @@ function usage() {
|
|
|
195
199
|
console.log(`oomi <command>
|
|
196
200
|
|
|
197
201
|
Commands:
|
|
202
|
+
connect --runtime openclaw|hermes|command [--name CONNECTION] [--agent-name "Your name"] [--app-url URL]
|
|
203
|
+
Verify the selected local agent, pair once, start its bridge, and return an Auth Link.
|
|
204
|
+
Hermes: --profile PROFILE and --resume SESSION select the intended agent/session.
|
|
205
|
+
Other agents: --command '["/absolute/path/to/adapter"]' (JSON stdin / JSON stdout).
|
|
206
|
+
--cwd PATH selects the agent working directory; --name isolates multiple agents.
|
|
207
|
+
status [--name NAME]
|
|
208
|
+
Show broker connection and verified local agent health separately.
|
|
209
|
+
verify [--name NAME]
|
|
210
|
+
Send a harmless local test message and require a real agent reply.
|
|
211
|
+
identity get|set [--connection NAME] [--agent-name NAME] [--bio TEXT] [--profile-image-url URL]
|
|
212
|
+
identity acknowledge --identity-id ID --revision N [--connection NAME]
|
|
213
|
+
identity backup --files '["/path/SOUL.md"]' [--connection NAME]
|
|
214
|
+
identity backups [--connection NAME]
|
|
215
|
+
identity restore --snapshot ID --output NEW_FOLDER [--connection NAME]
|
|
216
|
+
invite [--name NAME] [--app-url URL]
|
|
217
|
+
Generate a fresh Auth Link from saved pairing without changing agents.
|
|
218
|
+
bridge [start|ensure|stop|restart|ps] [--name NAME] [--detach]
|
|
219
|
+
Manage only the named agent's bridge.
|
|
220
|
+
|
|
198
221
|
init
|
|
199
222
|
Install Oomi agent instructions into OpenClaw AGENTS.md.
|
|
200
223
|
|
|
@@ -233,8 +256,11 @@ Commands:
|
|
|
233
256
|
persona-apps apply-action <slug>
|
|
234
257
|
Apply an approved persona app action through the Oomi backend.
|
|
235
258
|
|
|
236
|
-
context health
|
|
259
|
+
context health [--window day|week|month|year|all|range] [--date YYYY-MM-DD]
|
|
260
|
+
[--from YYYY-MM-DD --to YYYY-MM-DD] [--shape summary|series|both]
|
|
261
|
+
[--group-by day|week|month|year] [--timezone IANA-zone] [--cursor YYYY-MM-DD]
|
|
237
262
|
Show the latest account-linked health context available to this paired OpenClaw device.
|
|
263
|
+
context plugins [--plugin apple_calendar|apple_reminders|apple_contacts|apple_location|apple_notes|google_gmail|google_drive|google_calendar|bluetooth_devices]
|
|
238
264
|
context health sync [--wait]
|
|
239
265
|
Request the linked Oomi mobile app to sync fresh HealthKit context.
|
|
240
266
|
Use --wait to poll until fresh context is uploaded or the timeout expires.
|
|
@@ -341,7 +367,7 @@ function parseArgs(argv) {
|
|
|
341
367
|
if (val.startsWith('--')) {
|
|
342
368
|
const key = val.replace(/^--/, '');
|
|
343
369
|
const next = rest[i + 1];
|
|
344
|
-
if (
|
|
370
|
+
if (next === undefined || next.startsWith('--')) {
|
|
345
371
|
args.flags[key] = true;
|
|
346
372
|
} else {
|
|
347
373
|
args.flags[key] = next;
|
|
@@ -572,7 +598,10 @@ async function handlePersonaAppApplyActionCommand(slug, flags = {}) {
|
|
|
572
598
|
|
|
573
599
|
async function handleContextHealthCommand(flags = {}) {
|
|
574
600
|
const client = createCliPersonaApiClient(flags);
|
|
575
|
-
const payload = await client.getHealthContext(
|
|
601
|
+
const payload = await client.getHealthContext({
|
|
602
|
+
window: flags.window, date: flags.date, from: flags.from, to: flags.to,
|
|
603
|
+
shape: flags.shape, groupBy: flags['group-by'], timezone: flags.timezone, cursor: flags.cursor,
|
|
604
|
+
});
|
|
576
605
|
printStructuredResult(payload, isTruthyFlag(flags.json));
|
|
577
606
|
}
|
|
578
607
|
|
|
@@ -770,19 +799,19 @@ function readOpenclawDefaultSessionKey() {
|
|
|
770
799
|
}
|
|
771
800
|
|
|
772
801
|
function resolveBridgeStatePath() {
|
|
773
|
-
return resolveOpenclawBridgeStatePath();
|
|
802
|
+
return bridgeConnectionName ? path.join(connectionDirectory(), 'oomi-bridge.json') : resolveOpenclawBridgeStatePath();
|
|
774
803
|
}
|
|
775
804
|
|
|
776
805
|
function resolveBridgeStatusPath() {
|
|
777
|
-
return resolveOpenclawBridgeStatusPath();
|
|
806
|
+
return bridgeConnectionName ? path.join(connectionDirectory(), 'oomi-bridge-status.json') : resolveOpenclawBridgeStatusPath();
|
|
778
807
|
}
|
|
779
808
|
|
|
780
809
|
function resolveBridgeLockPath() {
|
|
781
|
-
return resolveOpenclawBridgeLockPath();
|
|
810
|
+
return bridgeConnectionName ? path.join(connectionDirectory(), 'oomi-bridge.lock') : resolveOpenclawBridgeLockPath();
|
|
782
811
|
}
|
|
783
812
|
|
|
784
813
|
function resolveBridgeLiveLogPath() {
|
|
785
|
-
return resolveOpenclawBridgeLiveLogPath();
|
|
814
|
+
return bridgeConnectionName ? path.join(connectionDirectory(), 'logs', 'oomi-bridge-live.log') : resolveOpenclawBridgeLiveLogPath();
|
|
786
815
|
}
|
|
787
816
|
|
|
788
817
|
function resolveBridgeLaunchAgentPlistPath() {
|
|
@@ -815,7 +844,7 @@ function readBridgeState() {
|
|
|
815
844
|
function writeBridgeState(nextState) {
|
|
816
845
|
const statePath = resolveBridgeStatePath();
|
|
817
846
|
ensureDir(path.dirname(statePath));
|
|
818
|
-
|
|
847
|
+
writePrivateJson(statePath, nextState);
|
|
819
848
|
}
|
|
820
849
|
|
|
821
850
|
function readBridgeStatus() {
|
|
@@ -933,11 +962,11 @@ function isPidAlive(pid) {
|
|
|
933
962
|
|
|
934
963
|
function isBridgeWorkerCommand(command) {
|
|
935
964
|
const text = String(command || '').trim().toLowerCase();
|
|
936
|
-
if (
|
|
937
|
-
|
|
938
|
-
if (
|
|
939
|
-
if (/\
|
|
940
|
-
return
|
|
965
|
+
if (!/(?:oomi(?:-ai\.js)?|openclaw)\s+bridge\b/.test(text)) return false;
|
|
966
|
+
const connection = text.match(/--(?:connection|name)[=\s]+([a-z0-9_-]+)/)?.[1] || '';
|
|
967
|
+
if (connection !== bridgeConnectionName) return false;
|
|
968
|
+
if (/\bbridge\s+(ps|stop|restart|ensure|service)\b/.test(text)) return false;
|
|
969
|
+
return /\bbridge(?:\s+start)?(?:\s+--|$)/.test(text);
|
|
941
970
|
}
|
|
942
971
|
|
|
943
972
|
function isBridgeProcess(pid) {
|
|
@@ -1047,6 +1076,7 @@ function acquireBridgeLock(deviceId) {
|
|
|
1047
1076
|
async function claimBridgeDeviceToken({ brokerHttp, pairCode, deviceId }) {
|
|
1048
1077
|
const response = await fetch(`${brokerHttp.replace(/\/$/, '')}/v1/pair/claim`, {
|
|
1049
1078
|
method: 'POST',
|
|
1079
|
+
signal: AbortSignal.timeout(15000),
|
|
1050
1080
|
headers: { 'Content-Type': 'application/json' },
|
|
1051
1081
|
body: JSON.stringify({ pairCode, deviceId }),
|
|
1052
1082
|
});
|
|
@@ -1064,6 +1094,7 @@ async function requestManagedPairCode({ appUrl, label }) {
|
|
|
1064
1094
|
const baseUrl = appUrl.replace(/\/$/, '');
|
|
1065
1095
|
const response = await fetch(`${baseUrl}/api/gateway/managed/pair/start`, {
|
|
1066
1096
|
method: 'POST',
|
|
1097
|
+
signal: AbortSignal.timeout(15000),
|
|
1067
1098
|
headers: { 'Content-Type': 'application/json' },
|
|
1068
1099
|
body: JSON.stringify({ label }),
|
|
1069
1100
|
});
|
|
@@ -1080,6 +1111,7 @@ async function requestManagedPairCode({ appUrl, label }) {
|
|
|
1080
1111
|
async function requestBrokerPairCode({ brokerHttp, adminKey, label }) {
|
|
1081
1112
|
const response = await fetch(`${brokerHttp.replace(/\/$/, '')}/v1/pair/start`, {
|
|
1082
1113
|
method: 'POST',
|
|
1114
|
+
signal: AbortSignal.timeout(15000),
|
|
1083
1115
|
headers: {
|
|
1084
1116
|
'Content-Type': 'application/json',
|
|
1085
1117
|
Authorization: `Bearer ${adminKey}`,
|
|
@@ -1099,6 +1131,7 @@ async function requestBrokerPairCode({ brokerHttp, adminKey, label }) {
|
|
|
1099
1131
|
async function requestConnectInviteLink({ backendHttp, appUrl, sessionKey, deviceToken }) {
|
|
1100
1132
|
const response = await fetch(`${backendHttp.replace(/\/$/, '')}/v1/invite_links/start`, {
|
|
1101
1133
|
method: 'POST',
|
|
1134
|
+
signal: AbortSignal.timeout(15000),
|
|
1102
1135
|
headers: {
|
|
1103
1136
|
'Content-Type': 'application/json',
|
|
1104
1137
|
Authorization: `Bearer ${deviceToken}`,
|
|
@@ -1119,6 +1152,7 @@ async function fetchManagedGatewayConfig({ appUrl }) {
|
|
|
1119
1152
|
const baseUrl = appUrl.replace(/\/$/, '');
|
|
1120
1153
|
const response = await fetch(`${baseUrl}/api/gateway/managed/config`, {
|
|
1121
1154
|
method: 'GET',
|
|
1155
|
+
signal: AbortSignal.timeout(15000),
|
|
1122
1156
|
headers: { 'Content-Type': 'application/json' },
|
|
1123
1157
|
});
|
|
1124
1158
|
const payload = await response.json().catch(() => ({}));
|
|
@@ -1218,11 +1252,12 @@ function buildDeviceAuthPayloadV3({
|
|
|
1218
1252
|
}
|
|
1219
1253
|
|
|
1220
1254
|
function loadGatewayDeviceIdentity() {
|
|
1221
|
-
|
|
1255
|
+
const identityPath = resolveOpenclawIdentityPath();
|
|
1256
|
+
if (!fs.existsSync(identityPath)) {
|
|
1222
1257
|
return null;
|
|
1223
1258
|
}
|
|
1224
1259
|
try {
|
|
1225
|
-
const parsed = JSON.parse(readFile(
|
|
1260
|
+
const parsed = JSON.parse(readFile(identityPath));
|
|
1226
1261
|
if (
|
|
1227
1262
|
parsed &&
|
|
1228
1263
|
parsed.version === 1 &&
|
|
@@ -1312,12 +1347,8 @@ function prepareGatewayFrameForLocalGateway(frameText, gatewayAuth, options = {}
|
|
|
1312
1347
|
const rawParams = frame.params && typeof frame.params === 'object' ? frame.params : {};
|
|
1313
1348
|
const params = {};
|
|
1314
1349
|
|
|
1315
|
-
params.minProtocol =
|
|
1316
|
-
|
|
1317
|
-
: 3;
|
|
1318
|
-
params.maxProtocol = Number.isInteger(rawParams.maxProtocol) && rawParams.maxProtocol >= 1
|
|
1319
|
-
? rawParams.maxProtocol
|
|
1320
|
-
: 3;
|
|
1350
|
+
params.minProtocol = gatewayAuth.protocol || LOCAL_PROTOCOL_MIN;
|
|
1351
|
+
params.maxProtocol = gatewayAuth.protocol || LOCAL_PROTOCOL_MAX;
|
|
1321
1352
|
|
|
1322
1353
|
const clientInput = rawParams.client && typeof rawParams.client === 'object' ? rawParams.client : {};
|
|
1323
1354
|
const client = {};
|
|
@@ -2174,6 +2205,7 @@ async function runBridgePreflight({ brokerWs, gatewayUrl, gatewayConfigPath }) {
|
|
|
2174
2205
|
throw new Error('Broker WS URL is missing hostname.');
|
|
2175
2206
|
}
|
|
2176
2207
|
await dnsLookup(brokerHost);
|
|
2208
|
+
if (gatewayUrl === 'local-agent:') return;
|
|
2177
2209
|
|
|
2178
2210
|
let parsedGatewayUrl;
|
|
2179
2211
|
try {
|
|
@@ -2197,6 +2229,7 @@ function buildBridgeDetachArgs(rawFlags = {}) {
|
|
|
2197
2229
|
'device-token',
|
|
2198
2230
|
];
|
|
2199
2231
|
const args = [process.argv[1], 'openclaw', 'bridge'];
|
|
2232
|
+
if (bridgeConnectionName) args.push('--connection', bridgeConnectionName);
|
|
2200
2233
|
|
|
2201
2234
|
for (const key of orderedKeys) {
|
|
2202
2235
|
const value = rawFlags[key];
|
|
@@ -2262,6 +2295,10 @@ function listBridgeProcessPids() {
|
|
|
2262
2295
|
const status = readBridgeStatus();
|
|
2263
2296
|
addPid(status.pid);
|
|
2264
2297
|
|
|
2298
|
+
// Named connections are owned by their own lock and status, including when a
|
|
2299
|
+
// custom connections directory is used. Never sweep another installation.
|
|
2300
|
+
if (bridgeConnectionName) return Array.from(pids);
|
|
2301
|
+
|
|
2265
2302
|
try {
|
|
2266
2303
|
const result = spawnSync('ps', ['-Ao', 'pid=,command='], {
|
|
2267
2304
|
encoding: 'utf8',
|
|
@@ -2491,7 +2528,7 @@ async function stopBridgeLaunchdService() {
|
|
|
2491
2528
|
async function resolveBridgeRuntimeConfig(flags, bridgeState) {
|
|
2492
2529
|
const explicitBrokerHttp = String(flags['broker-http'] || '').trim();
|
|
2493
2530
|
const explicitBrokerWs = String(flags['broker-ws'] || '').trim();
|
|
2494
|
-
const appUrl = String(flags['app-url'] || process.env.OOMI_APP_URL || '').trim();
|
|
2531
|
+
const appUrl = String(flags['app-url'] || process.env.OOMI_APP_URL || bridgeState.appUrl || '').trim();
|
|
2495
2532
|
|
|
2496
2533
|
let brokerHttp = String(explicitBrokerHttp || process.env.OOMI_CHAT_BROKER_HTTP_URL || bridgeState.brokerHttp || '').trim();
|
|
2497
2534
|
let brokerWs = String(explicitBrokerWs || process.env.OOMI_CHAT_BROKER_DEVICE_WS_URL || bridgeState.brokerWs || '').trim();
|
|
@@ -2534,6 +2571,7 @@ async function startOpenclawBridge(flags) {
|
|
|
2534
2571
|
}
|
|
2535
2572
|
|
|
2536
2573
|
const bridgeState = readBridgeState();
|
|
2574
|
+
if (bridgeState.openclawHome) process.env.OPENCLAW_HOME = bridgeState.openclawHome;
|
|
2537
2575
|
const runtimeConfig = await resolveBridgeRuntimeConfig(flags, bridgeState);
|
|
2538
2576
|
const brokerHttp = runtimeConfig.brokerHttp;
|
|
2539
2577
|
const brokerWs = runtimeConfig.brokerWs;
|
|
@@ -2574,14 +2612,17 @@ async function startOpenclawBridge(flags) {
|
|
|
2574
2612
|
console.log(`Claimed bridge device token for ${deviceId}.`);
|
|
2575
2613
|
}
|
|
2576
2614
|
|
|
2577
|
-
const
|
|
2578
|
-
|
|
2615
|
+
const localRuntime = bridgeState.localAgent
|
|
2616
|
+
? new LocalAgentRuntime(bridgeState.localAgent, path.dirname(resolveBridgeStatePath()), (health) => updateBridgeStatus(health))
|
|
2617
|
+
: null;
|
|
2618
|
+
const gateway = localRuntime ? { gatewayUrl: 'local-agent:', configPath: resolveBridgeStatePath() } : readOpenclawGatewayConfig();
|
|
2619
|
+
if (!localRuntime && !gateway.token && !gateway.password) {
|
|
2579
2620
|
throw new Error(`Gateway auth token/password not found in ${gateway.configPath}.`);
|
|
2580
2621
|
}
|
|
2581
|
-
const gatewayDeviceIdentity = loadGatewayDeviceIdentity();
|
|
2582
|
-
if (!gatewayDeviceIdentity) {
|
|
2622
|
+
const gatewayDeviceIdentity = localRuntime ? null : loadGatewayDeviceIdentity();
|
|
2623
|
+
if (!localRuntime && !gatewayDeviceIdentity) {
|
|
2583
2624
|
console.warn(
|
|
2584
|
-
`[bridge] OpenClaw device identity not found at ${
|
|
2625
|
+
`[bridge] OpenClaw device identity not found at ${resolveOpenclawIdentityPath()}; device-signed connect may fail on newer gateways.`
|
|
2585
2626
|
);
|
|
2586
2627
|
}
|
|
2587
2628
|
|
|
@@ -2599,10 +2640,16 @@ async function startOpenclawBridge(flags) {
|
|
|
2599
2640
|
gatewayUrl: gateway.gatewayUrl,
|
|
2600
2641
|
gatewayConfigPath: gateway.configPath,
|
|
2601
2642
|
});
|
|
2643
|
+
if (!localRuntime) {
|
|
2644
|
+
const result = await probeGateway({ gateway, deviceIdentity: gatewayDeviceIdentity, prepareFrame: prepareGatewayFrameForLocalGateway });
|
|
2645
|
+
gateway.protocol = result.protocol;
|
|
2646
|
+
}
|
|
2647
|
+
updateBridgeStatus({ runtime: bridgeState.runtime || 'openclaw', runtimeReady: Boolean(bridgeState.verifiedAt), verifiedAt: bridgeState.verifiedAt || null, gatewayProtocol: gateway.protocol || null });
|
|
2602
2648
|
} catch (err) {
|
|
2603
2649
|
const failure = classifyBridgeFailure({ err });
|
|
2604
2650
|
updateBridgeStatus({
|
|
2605
2651
|
status: 'error',
|
|
2652
|
+
runtimeReady: false,
|
|
2606
2653
|
deviceId,
|
|
2607
2654
|
brokerWs,
|
|
2608
2655
|
brokerHttp,
|
|
@@ -2632,7 +2679,16 @@ async function startOpenclawBridge(flags) {
|
|
|
2632
2679
|
startedAt: bridgeNowIso(),
|
|
2633
2680
|
});
|
|
2634
2681
|
|
|
2635
|
-
console.log(`Starting
|
|
2682
|
+
console.log(`Starting ${bridgeState.runtime || 'openclaw'} bridge: device=${deviceId}`);
|
|
2683
|
+
const stopChannelWorker = localRuntime ? startChannelWorker(bridgeState, localRuntime) : () => {};
|
|
2684
|
+
const stopIdentityWorker = startIdentityWorker({
|
|
2685
|
+
state: { ...bridgeState, brokerHttp, deviceToken },
|
|
2686
|
+
receiptPath: path.join(path.dirname(resolveBridgeStatePath()), 'oomi-identity-receipt.json'),
|
|
2687
|
+
// Applying a specific settings revision is idempotent, unlike a user turn.
|
|
2688
|
+
deliver: (turn) => localRuntime ? localRuntime.turn({ ...turn, retryInterrupted: true }) : probeGateway({ gateway, deviceIdentity: gatewayDeviceIdentity, prepareFrame: prepareGatewayFrameForLocalGateway, turn, timeoutMs: 180000 }),
|
|
2689
|
+
acknowledgeCommand: (identity) => `oomi identity acknowledge${bridgeConnectionName ? ` --connection ${bridgeConnectionName}` : ''} --identity-id ${identity.identityId} --revision ${identity.revision}`,
|
|
2690
|
+
onError: () => updateBridgeStatus({ identitySyncError: 'Identity notification could not be delivered. It will retry while the bridge is running.' }),
|
|
2691
|
+
});
|
|
2636
2692
|
console.log(`Local gateway: ${gateway.gatewayUrl}`);
|
|
2637
2693
|
console.log(`Broker WS: ${brokerWs}`);
|
|
2638
2694
|
bridgeDebugLog(
|
|
@@ -3316,6 +3372,7 @@ async function startOpenclawBridge(flags) {
|
|
|
3316
3372
|
}
|
|
3317
3373
|
}
|
|
3318
3374
|
} else if (requestMeta.method === 'connect' && !responseMeta.ok) {
|
|
3375
|
+
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
3376
|
const deferredFrames = Array.isArray(sessionBridge.waitingForConnect)
|
|
3320
3377
|
? sessionBridge.waitingForConnect.splice(0, sessionBridge.waitingForConnect.length)
|
|
3321
3378
|
: [];
|
|
@@ -3353,6 +3410,12 @@ async function startOpenclawBridge(flags) {
|
|
|
3353
3410
|
}
|
|
3354
3411
|
}
|
|
3355
3412
|
|
|
3413
|
+
if (gatewayPayload?.event === 'chat' && gatewayPayload.payload?.state === 'final') {
|
|
3414
|
+
updateBridgeStatus({ runtimeReady: true, lastReplyAt: bridgeNowIso() });
|
|
3415
|
+
} else if (gatewayPayload?.event === 'chat' && gatewayPayload.payload?.state === 'error') {
|
|
3416
|
+
updateBridgeStatus({ status: 'degraded', runtimeReady: false, lastErrorMessage: gatewayPayload.payload.errorMessage || 'The local agent failed to answer.' });
|
|
3417
|
+
}
|
|
3418
|
+
|
|
3356
3419
|
if (isGatewayRunStartedFrame(frame)) {
|
|
3357
3420
|
sendGatewayAck(brokerSocket, {
|
|
3358
3421
|
sessionId,
|
|
@@ -3445,7 +3508,7 @@ async function startOpenclawBridge(flags) {
|
|
|
3445
3508
|
const sessionBridge = ensureSessionBridge({
|
|
3446
3509
|
sessions: activeGatewaySockets,
|
|
3447
3510
|
sessionId,
|
|
3448
|
-
createSocket: () => new WebSocket(gateway.gatewayUrl),
|
|
3511
|
+
createSocket: () => localRuntime ? localRuntime.socket() : new WebSocket(gateway.gatewayUrl),
|
|
3449
3512
|
});
|
|
3450
3513
|
if (sessionBridge) setupGatewaySession(sessionId, sessionBridge);
|
|
3451
3514
|
return sessionBridge;
|
|
@@ -3798,8 +3861,11 @@ async function startOpenclawBridge(flags) {
|
|
|
3798
3861
|
}));
|
|
3799
3862
|
};
|
|
3800
3863
|
|
|
3801
|
-
const markStopped = (signal) => {
|
|
3864
|
+
const markStopped = async (signal) => {
|
|
3802
3865
|
reconnectState.stopped = true;
|
|
3866
|
+
stopChannelWorker();
|
|
3867
|
+
stopIdentityWorker();
|
|
3868
|
+
if (localRuntime) await localRuntime.queue;
|
|
3803
3869
|
if (reconnectState.timer) {
|
|
3804
3870
|
clearTimeout(reconnectState.timer);
|
|
3805
3871
|
reconnectState.timer = null;
|
|
@@ -3828,7 +3894,7 @@ async function startOpenclawBridge(flags) {
|
|
|
3828
3894
|
|
|
3829
3895
|
async function pairAndStartOpenclawBridge(flags) {
|
|
3830
3896
|
const bridgeState = readBridgeState();
|
|
3831
|
-
const appUrl = String(flags['app-url'] || process.env.OOMI_APP_URL || '
|
|
3897
|
+
const appUrl = String(flags['app-url'] || process.env.OOMI_APP_URL || bridgeState.appUrl || 'https://www.oomi.ai').trim();
|
|
3832
3898
|
const deviceId = resolveDeviceId(flags, bridgeState);
|
|
3833
3899
|
const label = String(flags.label || `${deviceId}-bridge`).trim();
|
|
3834
3900
|
const sessionKey = String(
|
|
@@ -3963,8 +4029,8 @@ async function createOpenclawInviteLink(flags) {
|
|
|
3963
4029
|
bridgeState.brokerHttp ||
|
|
3964
4030
|
''
|
|
3965
4031
|
).trim();
|
|
3966
|
-
const appUrl = String(flags['app-url'] || process.env.OOMI_APP_URL || '
|
|
3967
|
-
const configSessionKey = readOpenclawDefaultSessionKey();
|
|
4032
|
+
const appUrl = String(flags['app-url'] || process.env.OOMI_APP_URL || bridgeState.appUrl || 'https://www.oomi.ai').trim();
|
|
4033
|
+
const configSessionKey = bridgeConnectionName ? '' : readOpenclawDefaultSessionKey();
|
|
3968
4034
|
const sessionKey = String(
|
|
3969
4035
|
flags['session-key'] ||
|
|
3970
4036
|
process.env.OOMI_SESSION_KEY ||
|
|
@@ -4013,6 +4079,7 @@ async function createOpenclawInviteLink(flags) {
|
|
|
4013
4079
|
function printOpenclawBridgeStatus(flags) {
|
|
4014
4080
|
const bridgeState = readBridgeState();
|
|
4015
4081
|
const runtimeStatus = readBridgeStatus();
|
|
4082
|
+
if (runtimeStatus.pid && !isBridgeProcess(runtimeStatus.pid)) { runtimeStatus.status = 'stopped'; runtimeStatus.runtimeReady = false; }
|
|
4016
4083
|
const jsonOutput = isTruthyFlag(flags.json);
|
|
4017
4084
|
const redactToken = (value) => {
|
|
4018
4085
|
const text = String(value || '').trim();
|
|
@@ -4051,7 +4118,9 @@ function printOpenclawBridgeStatus(flags) {
|
|
|
4051
4118
|
console.log(`Device token: ${payload.bridgeState.deviceToken}`);
|
|
4052
4119
|
}
|
|
4053
4120
|
if (payload.runtime && typeof payload.runtime === 'object' && Object.keys(payload.runtime).length > 0) {
|
|
4054
|
-
console.log(`
|
|
4121
|
+
console.log(`Bridge state: ${String(payload.runtime.status || 'unknown')}`);
|
|
4122
|
+
console.log(`Agent runtime: ${bridgeState.runtime || 'openclaw'}${bridgeState.localAgent?.profile ? ` (profile ${bridgeState.localAgent.profile})` : ''}`);
|
|
4123
|
+
console.log(`Local reply verified: ${payload.runtime.runtimeReady ? 'yes' : 'no'}${payload.runtime.verifiedAt ? ` at ${payload.runtime.verifiedAt}` : ''}`);
|
|
4055
4124
|
if (payload.runtime.lastConnectedAt) {
|
|
4056
4125
|
console.log(`Last connected: ${payload.runtime.lastConnectedAt}`);
|
|
4057
4126
|
}
|
|
@@ -4329,6 +4398,98 @@ async function handleBridgeLifecycleCommand(flags = {}, actionRaw = '') {
|
|
|
4329
4398
|
);
|
|
4330
4399
|
}
|
|
4331
4400
|
|
|
4401
|
+
function connectionDirectory() {
|
|
4402
|
+
return path.join(process.env.OOMI_CONNECTIONS_DIR || path.join(os.homedir(), '.oomi', 'connections'), bridgeConnectionName);
|
|
4403
|
+
}
|
|
4404
|
+
|
|
4405
|
+
async function verifyLocalAgent(state) {
|
|
4406
|
+
if (state.openclawHome) process.env.OPENCLAW_HOME = state.openclawHome;
|
|
4407
|
+
let result;
|
|
4408
|
+
if (state.localAgent) {
|
|
4409
|
+
await runLocalAgent(state.localAgent, {
|
|
4410
|
+
sessionKey: `oomi:verify:${randomUUID()}`, idempotencyKey: randomUUID(), history: [],
|
|
4411
|
+
message: 'Oomi connection check. Reply with OOMI_OK only. Do not use tools or change anything.',
|
|
4412
|
+
});
|
|
4413
|
+
result = { verifiedAt: new Date().toISOString() };
|
|
4414
|
+
} else {
|
|
4415
|
+
result = await probeGateway({ gateway: readOpenclawGatewayConfig(), deviceIdentity: loadGatewayDeviceIdentity(), prepareFrame: prepareGatewayFrameForLocalGateway, verifyReply: true, timeoutMs: 180000 });
|
|
4416
|
+
}
|
|
4417
|
+
return { runtime: state.runtime || 'openclaw', ...result };
|
|
4418
|
+
}
|
|
4419
|
+
|
|
4420
|
+
async function connectLocalAgent(flags) {
|
|
4421
|
+
const agentName = flags['agent-name'] === undefined ? undefined : validateAgentName(flags['agent-name']);
|
|
4422
|
+
const runtime = String(flags.runtime || '');
|
|
4423
|
+
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.');
|
|
4424
|
+
const previous = readBridgeState();
|
|
4425
|
+
if (previous.runtime && previous.runtime !== runtime) throw new Error('This connection belongs to a different runtime. Choose a different --name.');
|
|
4426
|
+
const appUrl = String(flags['app-url'] || previous.appUrl || 'https://www.oomi.ai').replace(/\/$/, '');
|
|
4427
|
+
if (previous.appUrl && previous.appUrl !== appUrl) throw new Error('This connection belongs to another Oomi site. Choose a different --name.');
|
|
4428
|
+
const state = { ...previous, runtime, appUrl };
|
|
4429
|
+
if (runtime === 'openclaw') state.openclawHome = previous.openclawHome || resolveOpenclawHome();
|
|
4430
|
+
if (runtime !== 'openclaw') {
|
|
4431
|
+
const saved = previous.localAgent;
|
|
4432
|
+
state.localAgent = localAgentConfig({
|
|
4433
|
+
...flags,
|
|
4434
|
+
command: flags.command || (saved ? JSON.stringify(saved.command) : undefined),
|
|
4435
|
+
profile: flags.profile ?? saved?.profile,
|
|
4436
|
+
'hermes-home': saved?.hermesHome,
|
|
4437
|
+
resume: flags.resume ?? saved?.resume,
|
|
4438
|
+
cwd: flags.cwd ?? saved?.cwd,
|
|
4439
|
+
});
|
|
4440
|
+
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.');
|
|
4441
|
+
}
|
|
4442
|
+
console.log(`Verifying ${runtime}${state.localAgent?.profile ? ` profile ${state.localAgent.profile}` : ''} with a local test message...`);
|
|
4443
|
+
try {
|
|
4444
|
+
const verified = await verifyLocalAgent(state);
|
|
4445
|
+
state.verifiedAt = verified.verifiedAt;
|
|
4446
|
+
} catch (err) {
|
|
4447
|
+
updateBridgeStatus({ runtime, runtimeReady: false, status: 'degraded', lastErrorMessage: err.message });
|
|
4448
|
+
throw err;
|
|
4449
|
+
}
|
|
4450
|
+
if (!state.deviceToken) {
|
|
4451
|
+
const config = await fetchManagedGatewayConfig({ appUrl });
|
|
4452
|
+
state.brokerHttp = rewriteLoopbackBrokerUrlForAppHost(config.brokerHttpUrl, appUrl);
|
|
4453
|
+
state.brokerWs = rewriteLoopbackBrokerUrlForAppHost(config.brokerDeviceWsUrl, appUrl);
|
|
4454
|
+
const label = String(flags.label || agentName || flags.name || runtime);
|
|
4455
|
+
const pair = await requestManagedPairCode({ appUrl, label });
|
|
4456
|
+
state.deviceId = `${runtime}-${randomUUID()}`;
|
|
4457
|
+
const claim = await claimBridgeDeviceToken({ brokerHttp: state.brokerHttp, pairCode: pair.pairCode, deviceId: state.deviceId });
|
|
4458
|
+
state.deviceToken = claim.deviceToken;
|
|
4459
|
+
state.sessionKey = String(flags['session-key'] || (runtime === 'openclaw' ? readOpenclawDefaultSessionKey() : '') || 'agent:main:webchat:channel:oomi');
|
|
4460
|
+
state.claimedAt = new Date().toISOString();
|
|
4461
|
+
}
|
|
4462
|
+
writeBridgeState(state);
|
|
4463
|
+
try {
|
|
4464
|
+
await identityRequest(state, '/registration', { method: 'PUT', body: { runtime, ...(agentName ? { name: agentName } : {}) } });
|
|
4465
|
+
} catch (error) {
|
|
4466
|
+
if (error.status !== 404 || agentName) throw error;
|
|
4467
|
+
console.warn('This Oomi server does not support agent settings yet. Connecting without shared settings.');
|
|
4468
|
+
}
|
|
4469
|
+
// Stop this connection only; never leave a worker using an old adapter after a change.
|
|
4470
|
+
const supervised = !bridgeConnectionName && process.platform === 'darwin' && readBridgeLaunchdStatus().installed;
|
|
4471
|
+
const stopped = supervised ? await stopBridgeLaunchdService() : await stopBridgeProcesses();
|
|
4472
|
+
if (stopped.stillAlive?.length) throw new Error('The existing bridge did not stop. Resolve it before reconnecting.');
|
|
4473
|
+
writeBridgeState(state);
|
|
4474
|
+
updateBridgeStatus({ status: 'starting', runtime, runtimeReady: true, verifiedAt: state.verifiedAt, lastErrorMessage: '' });
|
|
4475
|
+
const child = supervised ? (startBridgeLaunchdService(), { pid: null }) : startBridgeDetachedProcess({ 'app-url': appUrl });
|
|
4476
|
+
const deadline = Date.now() + 20000;
|
|
4477
|
+
let connected = false;
|
|
4478
|
+
while (Date.now() < deadline) {
|
|
4479
|
+
const status = readBridgeStatus();
|
|
4480
|
+
if (status.pid && (!child.pid || status.pid === child.pid) && status.status === 'connected') { connected = true; break; }
|
|
4481
|
+
if ((!child.pid || status.pid === child.pid) && status.status === 'error') throw new Error(status.lastErrorMessage || 'Bridge startup failed.');
|
|
4482
|
+
if (child.pid && !isPidAlive(child.pid)) throw new Error(`Bridge exited. Check ${resolveBridgeLiveLogPath()}.`);
|
|
4483
|
+
await delay(200);
|
|
4484
|
+
}
|
|
4485
|
+
if (!connected) throw new Error('The local agent replied, but the Oomi bridge did not connect. Run oomi status for this connection.');
|
|
4486
|
+
const invite = await requestConnectInviteLink({ backendHttp: state.brokerHttp, appUrl, sessionKey: state.sessionKey, deviceToken: state.deviceToken });
|
|
4487
|
+
console.log(`Verified agent: ${runtime}${state.localAgent?.profile ? ` (${state.localAgent.profile})` : ''}`);
|
|
4488
|
+
console.log('Oomi Connect Ready');
|
|
4489
|
+
console.log(`Auth Link: ${invite.inviteUrl}`);
|
|
4490
|
+
console.log('Open the Auth Link to approve ownership, then send a message in Oomi to verify delivery end to end.');
|
|
4491
|
+
}
|
|
4492
|
+
|
|
4332
4493
|
async function main() {
|
|
4333
4494
|
const args = parseArgs(process.argv);
|
|
4334
4495
|
const command = args.command;
|
|
@@ -4339,6 +4500,30 @@ async function main() {
|
|
|
4339
4500
|
process.exit(0);
|
|
4340
4501
|
}
|
|
4341
4502
|
|
|
4503
|
+
const connection = args.flags.connection || args.flags.name || (command === 'connect' && args.flags.runtime !== 'openclaw' ? args.flags.runtime : '');
|
|
4504
|
+
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.');
|
|
4505
|
+
bridgeConnectionName = connection || '';
|
|
4506
|
+
|
|
4507
|
+
if (command === 'connect') return connectLocalAgent(args.flags);
|
|
4508
|
+
if (command === 'identity') return handleIdentityCommand(readBridgeState(), subcommand, args.flags);
|
|
4509
|
+
if (command === 'verify') {
|
|
4510
|
+
const state = readBridgeState();
|
|
4511
|
+
if (!state.deviceToken) throw new Error('No saved connection. Run oomi connect with an explicit runtime first.');
|
|
4512
|
+
try {
|
|
4513
|
+
const result = await verifyLocalAgent(state);
|
|
4514
|
+
writeBridgeState({ ...state, verifiedAt: result.verifiedAt });
|
|
4515
|
+
updateBridgeStatus({ runtimeReady: true, verifiedAt: result.verifiedAt, lastErrorMessage: '' });
|
|
4516
|
+
console.log(JSON.stringify(result, null, 2));
|
|
4517
|
+
} catch (err) {
|
|
4518
|
+
updateBridgeStatus({ runtimeReady: false, status: 'degraded', lastErrorMessage: err.message });
|
|
4519
|
+
throw err;
|
|
4520
|
+
}
|
|
4521
|
+
return;
|
|
4522
|
+
}
|
|
4523
|
+
if (command === 'status') return printOpenclawBridgeStatus(args.flags);
|
|
4524
|
+
if (command === 'invite') return createOpenclawInviteLink(args.flags);
|
|
4525
|
+
if (command === 'bridge') return handleBridgeLifecycleCommand(args.flags, subcommand || 'start');
|
|
4526
|
+
|
|
4342
4527
|
await maybeNotifyUpdate(command);
|
|
4343
4528
|
|
|
4344
4529
|
if (command === 'init') {
|
|
@@ -4363,6 +4548,7 @@ async function main() {
|
|
|
4363
4548
|
if (command === 'openclaw' && subcommand === 'bridge') {
|
|
4364
4549
|
const bridgeAction = String(args.positionals[0] || 'start').trim().toLowerCase();
|
|
4365
4550
|
if (bridgeAction === 'service') {
|
|
4551
|
+
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
4552
|
const serviceAction = args.positionals[1] || 'status';
|
|
4367
4553
|
await handleBridgeServiceCommand(serviceAction, args.flags);
|
|
4368
4554
|
return;
|
|
@@ -4429,6 +4615,12 @@ async function main() {
|
|
|
4429
4615
|
return;
|
|
4430
4616
|
}
|
|
4431
4617
|
|
|
4618
|
+
if (command === 'context' && subcommand === 'plugins') {
|
|
4619
|
+
const payload = await createCliPersonaApiClient(args.flags).getPluginContext({ plugin: args.flags.plugin });
|
|
4620
|
+
printStructuredResult(payload, isTruthyFlag(args.flags.json));
|
|
4621
|
+
return;
|
|
4622
|
+
}
|
|
4623
|
+
|
|
4432
4624
|
if (command === 'context' && subcommand === 'health') {
|
|
4433
4625
|
if (args.positionals[0] === 'sync') {
|
|
4434
4626
|
await handleContextHealthSyncCommand(args.flags);
|
|
@@ -4461,7 +4653,9 @@ async function main() {
|
|
|
4461
4653
|
|
|
4462
4654
|
const __currentFilePath = fileURLToPath(import.meta.url);
|
|
4463
4655
|
const __invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : '';
|
|
4464
|
-
const __isDirectExecution = Boolean(__invokedPath)
|
|
4656
|
+
const __isDirectExecution = Boolean(__invokedPath)
|
|
4657
|
+
&& fs.existsSync(__invokedPath)
|
|
4658
|
+
&& fs.realpathSync(__invokedPath) === fs.realpathSync(__currentFilePath);
|
|
4465
4659
|
|
|
4466
4660
|
if (__isDirectExecution) {
|
|
4467
4661
|
main().catch((err) => {
|