oomi-ai 0.2.2 → 0.2.4
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 +8 -5
- package/agent_instructions.md +7 -2
- package/bin/oomi-ai.js +161 -73
- package/bin/sessionBridgeState.js +51 -0
- package/openclaw.extension.js +21 -9
- package/openclaw.plugin.json +1 -1
- package/package.json +4 -2
- package/skills/oomi/agent_instructions.md +7 -2
package/README.md
CHANGED
|
@@ -49,12 +49,15 @@ oomi openclaw pair --app-url https://your-oomi-app.vercel.app --device-id my-ope
|
|
|
49
49
|
`--app-url` must be reachable from the OpenClaw host. If OpenClaw runs on a different machine/network, do not use `localhost` unless tunneled.
|
|
50
50
|
|
|
51
51
|
This prints:
|
|
52
|
-
- `
|
|
53
|
-
- `Connect URL: https://.../?connect=managed://<device-id>&sessionKey=...`
|
|
52
|
+
- `Auth invite URL: https://.../connect/<single-use-token>`
|
|
54
53
|
- A copy-ready block for the user:
|
|
55
54
|
- `Oomi Connect Ready`
|
|
56
|
-
- `
|
|
57
|
-
|
|
55
|
+
- `Auth Link: ...`
|
|
56
|
+
|
|
57
|
+
If you need a fresh auth link later (without re-pairing), run:
|
|
58
|
+
```
|
|
59
|
+
oomi openclaw invite --app-url https://your-oomi-app.vercel.app
|
|
60
|
+
```
|
|
58
61
|
|
|
59
62
|
Agent-intent mapping (recommended):
|
|
60
63
|
- If user says `Connect yourself to Oomi. Use app URL https://www.oomi.ai.`
|
|
@@ -68,7 +71,7 @@ Agent-intent mapping (recommended):
|
|
|
68
71
|
|
|
69
72
|
Important distinction:
|
|
70
73
|
- `pairCode` is one-time and used internally by the pair/bootstrap flow.
|
|
71
|
-
-
|
|
74
|
+
- Invite auth links are the required user flow.
|
|
72
75
|
|
|
73
76
|
Sync personas from the repo into the backend registry:
|
|
74
77
|
```
|
package/agent_instructions.md
CHANGED
|
@@ -31,7 +31,12 @@ If the user asks to connect OpenClaw to Oomi voice/web:
|
|
|
31
31
|
- Example: `oomi openclaw pair --app-url <URL> --no-start`
|
|
32
32
|
8. Return this exact result format to the user:
|
|
33
33
|
- `Oomi Connect Ready`
|
|
34
|
-
- `
|
|
35
|
-
-
|
|
34
|
+
- `Auth Link: ...`
|
|
35
|
+
- No extra narrative text before or after those lines.
|
|
36
|
+
|
|
37
|
+
Primary UX requirement:
|
|
38
|
+
- Never ask the user to enter raw pairing identifiers.
|
|
39
|
+
- The user should open the auth link, complete sign-in, and land directly in connected chat.
|
|
40
|
+
- Return `/connect/<invite-token>` auth links only.
|
|
36
41
|
|
|
37
42
|
Do not ask users to paste gateway IP/token/password when managed connect is available.
|
package/bin/oomi-ai.js
CHANGED
|
@@ -5,6 +5,7 @@ import path from 'path';
|
|
|
5
5
|
import { spawn } from 'child_process';
|
|
6
6
|
import { randomUUID } from 'crypto';
|
|
7
7
|
import WebSocket from 'ws';
|
|
8
|
+
import { ensureSessionBridge, forwardFrameToSession, flushSessionQueue } from './sessionBridgeState.js';
|
|
8
9
|
|
|
9
10
|
const MARKER_START = '<oomi-agent-instructions>';
|
|
10
11
|
const MARKER_END = '</oomi-agent-instructions>';
|
|
@@ -142,6 +143,9 @@ Commands:
|
|
|
142
143
|
openclaw pair
|
|
143
144
|
Pair this OpenClaw host with Oomi and start bridge (single command).
|
|
144
145
|
|
|
146
|
+
openclaw invite
|
|
147
|
+
Create a single-use auth invite link for the paired OpenClaw device.
|
|
148
|
+
|
|
145
149
|
openclaw plugin
|
|
146
150
|
Print OpenClaw extension install/config guidance for Oomi channel plugin.
|
|
147
151
|
|
|
@@ -158,7 +162,7 @@ Common flags:
|
|
|
158
162
|
--broker-http URL Managed broker HTTPS URL (for pair claim)
|
|
159
163
|
--broker-ws URL Managed broker device WS URL (wss://.../cable)
|
|
160
164
|
--pair-code CODE One-time pairing code from Oomi
|
|
161
|
-
--app-url URL Oomi app URL used for pairing APIs (default: http://127.0.0.1:
|
|
165
|
+
--app-url URL Oomi app URL used for pairing APIs (default: http://127.0.0.1:3456)
|
|
162
166
|
--label TEXT Pairing label shown in broker logs
|
|
163
167
|
--session-key KEY Session key used in generated connect URL
|
|
164
168
|
--detach Start bridge in background and exit
|
|
@@ -595,6 +599,25 @@ async function requestManagedPairCode({ appUrl, label }) {
|
|
|
595
599
|
return payload;
|
|
596
600
|
}
|
|
597
601
|
|
|
602
|
+
async function requestConnectInviteLink({ backendHttp, appUrl, sessionKey, deviceToken }) {
|
|
603
|
+
const response = await fetch(`${backendHttp.replace(/\/$/, '')}/v1/invite_links/start`, {
|
|
604
|
+
method: 'POST',
|
|
605
|
+
headers: {
|
|
606
|
+
'Content-Type': 'application/json',
|
|
607
|
+
Authorization: `Bearer ${deviceToken}`,
|
|
608
|
+
},
|
|
609
|
+
body: JSON.stringify({ appUrl, sessionKey }),
|
|
610
|
+
});
|
|
611
|
+
const payload = await response.json().catch(() => ({}));
|
|
612
|
+
if (!response.ok || !payload?.inviteUrl) {
|
|
613
|
+
const message =
|
|
614
|
+
(payload && typeof payload.error === 'string' && payload.error) ||
|
|
615
|
+
`Invite link start failed (${response.status})`;
|
|
616
|
+
throw new Error(message);
|
|
617
|
+
}
|
|
618
|
+
return payload;
|
|
619
|
+
}
|
|
620
|
+
|
|
598
621
|
async function fetchManagedGatewayConfig({ appUrl }) {
|
|
599
622
|
const baseUrl = appUrl.replace(/\/$/, '');
|
|
600
623
|
const response = await fetch(`${baseUrl}/api/gateway/managed/config`, {
|
|
@@ -768,6 +791,58 @@ async function startOpenclawBridge(flags) {
|
|
|
768
791
|
|
|
769
792
|
const brokerSocket = new WebSocket(wsUrl.toString());
|
|
770
793
|
let actionCableHeartbeat = null;
|
|
794
|
+
const setupGatewaySession = (sessionId, sessionBridge) => {
|
|
795
|
+
if (!sessionBridge || !sessionBridge.socket) return;
|
|
796
|
+
const gatewaySocket = sessionBridge.socket;
|
|
797
|
+
|
|
798
|
+
gatewaySocket.on('open', () => {
|
|
799
|
+
console.log(`[bridge] gateway.open ${sessionId}`);
|
|
800
|
+
flushSessionQueue(sessionBridge);
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
gatewaySocket.on('message', (gatewayRaw) => {
|
|
804
|
+
const frame = typeof gatewayRaw === 'string' ? gatewayRaw : gatewayRaw.toString();
|
|
805
|
+
sendBrokerPayload(brokerSocket, { action: 'gateway_frame', type: 'gateway.frame', sessionId, frame });
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
gatewaySocket.on('close', (code, reason) => {
|
|
809
|
+
const reasonText = reason ? reason.toString() : '';
|
|
810
|
+
console.log(
|
|
811
|
+
`[bridge] gateway.close ${sessionId} code=${String(code)}${reasonText ? ` reason=${reasonText}` : ''}`
|
|
812
|
+
);
|
|
813
|
+
activeGatewaySockets.delete(sessionId);
|
|
814
|
+
sendBrokerPayload(brokerSocket, {
|
|
815
|
+
action: 'gateway_closed',
|
|
816
|
+
type: 'gateway.closed',
|
|
817
|
+
sessionId,
|
|
818
|
+
code,
|
|
819
|
+
reason: reasonText,
|
|
820
|
+
});
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
gatewaySocket.on('error', (err) => {
|
|
824
|
+
console.error(`[bridge] gateway.error ${sessionId}: ${String(err)}`);
|
|
825
|
+
sendBrokerPayload(brokerSocket, {
|
|
826
|
+
action: 'log',
|
|
827
|
+
type: 'log',
|
|
828
|
+
sessionId,
|
|
829
|
+
level: 'error',
|
|
830
|
+
message: `Gateway socket error (${sessionId}): ${String(err)}`,
|
|
831
|
+
});
|
|
832
|
+
});
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
const getOrCreateGatewaySession = (sessionId) => {
|
|
836
|
+
const existing = activeGatewaySockets.get(sessionId);
|
|
837
|
+
if (existing) return existing;
|
|
838
|
+
const sessionBridge = ensureSessionBridge({
|
|
839
|
+
sessions: activeGatewaySockets,
|
|
840
|
+
sessionId,
|
|
841
|
+
createSocket: () => new WebSocket(gateway.gatewayUrl),
|
|
842
|
+
});
|
|
843
|
+
if (sessionBridge) setupGatewaySession(sessionId, sessionBridge);
|
|
844
|
+
return sessionBridge;
|
|
845
|
+
};
|
|
771
846
|
|
|
772
847
|
brokerSocket.on('open', () => {
|
|
773
848
|
console.log('[bridge] Connected to managed broker.');
|
|
@@ -809,55 +884,9 @@ async function startOpenclawBridge(flags) {
|
|
|
809
884
|
|
|
810
885
|
if (payload.type === 'client.open') {
|
|
811
886
|
const sessionId = String(payload.sessionId || '').trim();
|
|
812
|
-
if (!sessionId
|
|
887
|
+
if (!sessionId) return;
|
|
813
888
|
console.log(`[bridge] client.open ${sessionId}`);
|
|
814
|
-
|
|
815
|
-
const sessionBridge = {
|
|
816
|
-
socket: gatewaySocket,
|
|
817
|
-
queue: [],
|
|
818
|
-
};
|
|
819
|
-
activeGatewaySockets.set(sessionId, sessionBridge);
|
|
820
|
-
|
|
821
|
-
gatewaySocket.on('open', () => {
|
|
822
|
-
console.log(`[bridge] gateway.open ${sessionId}`);
|
|
823
|
-
while (sessionBridge.queue.length > 0 && gatewaySocket.readyState === WebSocket.OPEN) {
|
|
824
|
-
const nextFrame = sessionBridge.queue.shift();
|
|
825
|
-
if (typeof nextFrame === 'string' && nextFrame) {
|
|
826
|
-
gatewaySocket.send(nextFrame);
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
});
|
|
830
|
-
|
|
831
|
-
gatewaySocket.on('message', (gatewayRaw) => {
|
|
832
|
-
const frame = typeof gatewayRaw === 'string' ? gatewayRaw : gatewayRaw.toString();
|
|
833
|
-
sendBrokerPayload(brokerSocket, { action: 'gateway_frame', type: 'gateway.frame', sessionId, frame });
|
|
834
|
-
});
|
|
835
|
-
|
|
836
|
-
gatewaySocket.on('close', (code, reason) => {
|
|
837
|
-
const reasonText = reason ? reason.toString() : '';
|
|
838
|
-
console.log(
|
|
839
|
-
`[bridge] gateway.close ${sessionId} code=${String(code)}${reasonText ? ` reason=${reasonText}` : ''}`
|
|
840
|
-
);
|
|
841
|
-
activeGatewaySockets.delete(sessionId);
|
|
842
|
-
sendBrokerPayload(brokerSocket, {
|
|
843
|
-
action: 'gateway_closed',
|
|
844
|
-
type: 'gateway.closed',
|
|
845
|
-
sessionId,
|
|
846
|
-
code,
|
|
847
|
-
reason: reasonText,
|
|
848
|
-
});
|
|
849
|
-
});
|
|
850
|
-
|
|
851
|
-
gatewaySocket.on('error', (err) => {
|
|
852
|
-
console.error(`[bridge] gateway.error ${sessionId}: ${String(err)}`);
|
|
853
|
-
sendBrokerPayload(brokerSocket, {
|
|
854
|
-
action: 'log',
|
|
855
|
-
type: 'log',
|
|
856
|
-
sessionId,
|
|
857
|
-
level: 'error',
|
|
858
|
-
message: `Gateway socket error (${sessionId}): ${String(err)}`,
|
|
859
|
-
});
|
|
860
|
-
});
|
|
889
|
+
getOrCreateGatewaySession(sessionId);
|
|
861
890
|
return;
|
|
862
891
|
}
|
|
863
892
|
|
|
@@ -866,19 +895,13 @@ async function startOpenclawBridge(flags) {
|
|
|
866
895
|
const frame = typeof payload.frame === 'string' ? payload.frame : '';
|
|
867
896
|
if (!sessionId || !frame) return;
|
|
868
897
|
console.log(`[bridge] client.frame ${sessionId}`);
|
|
869
|
-
const sessionBridge =
|
|
870
|
-
if (!sessionBridge
|
|
871
|
-
console.log(`[bridge] client.frame dropped (no session) ${sessionId}`);
|
|
872
|
-
return;
|
|
873
|
-
}
|
|
874
|
-
const gatewaySocket = sessionBridge.socket;
|
|
898
|
+
const sessionBridge = getOrCreateGatewaySession(sessionId);
|
|
899
|
+
if (!sessionBridge) return;
|
|
875
900
|
const frameWithAuth = injectGatewayAuth(frame, gateway);
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
} else if (gatewaySocket.readyState === WebSocket.CONNECTING) {
|
|
901
|
+
const result = forwardFrameToSession(sessionBridge, frameWithAuth);
|
|
902
|
+
if (result === 'queued') {
|
|
879
903
|
console.log(`[bridge] client.frame queued ${sessionId}`);
|
|
880
|
-
|
|
881
|
-
} else {
|
|
904
|
+
} else if (result === 'dropped') {
|
|
882
905
|
console.log(`[bridge] client.frame dropped (socket not open) ${sessionId}`);
|
|
883
906
|
}
|
|
884
907
|
return;
|
|
@@ -924,7 +947,7 @@ async function startOpenclawBridge(flags) {
|
|
|
924
947
|
|
|
925
948
|
async function pairAndStartOpenclawBridge(flags) {
|
|
926
949
|
const bridgeState = readBridgeState();
|
|
927
|
-
const appUrl = String(flags['app-url'] || process.env.OOMI_APP_URL || 'http://127.0.0.1:
|
|
950
|
+
const appUrl = String(flags['app-url'] || process.env.OOMI_APP_URL || 'http://127.0.0.1:3456').trim();
|
|
928
951
|
const deviceId = resolveDeviceId(flags, bridgeState);
|
|
929
952
|
const label = String(flags.label || `${deviceId}-bridge`).trim();
|
|
930
953
|
const sessionKey = String(
|
|
@@ -961,19 +984,24 @@ async function pairAndStartOpenclawBridge(flags) {
|
|
|
961
984
|
brokerWs,
|
|
962
985
|
deviceId,
|
|
963
986
|
deviceToken,
|
|
987
|
+
sessionKey,
|
|
964
988
|
claimedAt: new Date().toISOString(),
|
|
965
989
|
expiresAt: claimed.expiresAt || null,
|
|
966
990
|
});
|
|
967
991
|
|
|
968
|
-
const
|
|
969
|
-
|
|
970
|
-
|
|
992
|
+
const invite = await requestConnectInviteLink({
|
|
993
|
+
backendHttp: managedConfig.brokerHttpUrl,
|
|
994
|
+
appUrl,
|
|
995
|
+
sessionKey,
|
|
996
|
+
deviceToken,
|
|
997
|
+
});
|
|
998
|
+
const inviteUrl = String(invite.inviteUrl || '').trim();
|
|
999
|
+
const inviteExpiresAt = String(invite.expiresAt || '').trim();
|
|
971
1000
|
|
|
972
1001
|
const pairSummary = {
|
|
973
1002
|
deviceId,
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
connectUrl,
|
|
1003
|
+
inviteUrl,
|
|
1004
|
+
inviteExpiresAt,
|
|
977
1005
|
brokerHttpUrl: managedConfig.brokerHttpUrl,
|
|
978
1006
|
brokerDeviceWsUrl: brokerWs,
|
|
979
1007
|
bridgeMode: shouldStart ? (detach ? 'detached' : 'attached') : 'skipped',
|
|
@@ -984,15 +1012,15 @@ async function pairAndStartOpenclawBridge(flags) {
|
|
|
984
1012
|
console.log(JSON.stringify(pairSummary, null, 2));
|
|
985
1013
|
} else {
|
|
986
1014
|
console.log(`Paired device: ${deviceId}`);
|
|
987
|
-
console.log(`
|
|
988
|
-
|
|
989
|
-
|
|
1015
|
+
console.log(`Auth invite URL: ${inviteUrl}`);
|
|
1016
|
+
if (inviteExpiresAt) {
|
|
1017
|
+
console.log(`Invite expires at: ${inviteExpiresAt}`);
|
|
1018
|
+
}
|
|
990
1019
|
console.log('');
|
|
991
|
-
console.log('Share this with the user:');
|
|
1020
|
+
console.log('Share this exact response with the user (no extra text):');
|
|
992
1021
|
console.log('----------------------------------------');
|
|
993
1022
|
console.log('Oomi Connect Ready');
|
|
994
|
-
console.log(`
|
|
995
|
-
console.log(`Link: ${connectUrl}`);
|
|
1023
|
+
console.log(`Auth Link: ${inviteUrl}`);
|
|
996
1024
|
console.log('----------------------------------------');
|
|
997
1025
|
}
|
|
998
1026
|
|
|
@@ -1033,6 +1061,61 @@ async function pairAndStartOpenclawBridge(flags) {
|
|
|
1033
1061
|
});
|
|
1034
1062
|
}
|
|
1035
1063
|
|
|
1064
|
+
async function createOpenclawInviteLink(flags) {
|
|
1065
|
+
const bridgeState = readBridgeState();
|
|
1066
|
+
const backendHttp = String(
|
|
1067
|
+
flags['backend-url'] ||
|
|
1068
|
+
flags['broker-http'] ||
|
|
1069
|
+
process.env.OOMI_BACKEND_URL ||
|
|
1070
|
+
process.env.OOMI_CHAT_BROKER_HTTP_URL ||
|
|
1071
|
+
bridgeState.brokerHttp ||
|
|
1072
|
+
''
|
|
1073
|
+
).trim();
|
|
1074
|
+
const appUrl = String(flags['app-url'] || process.env.OOMI_APP_URL || 'http://127.0.0.1:3456').trim();
|
|
1075
|
+
const sessionKey = String(
|
|
1076
|
+
flags['session-key'] ||
|
|
1077
|
+
process.env.OOMI_SESSION_KEY ||
|
|
1078
|
+
bridgeState.sessionKey ||
|
|
1079
|
+
'agent:main:webchat:channel:oomi'
|
|
1080
|
+
).trim();
|
|
1081
|
+
const deviceToken = String(flags['device-token'] || bridgeState.deviceToken || '').trim();
|
|
1082
|
+
const jsonOutput = isTruthyFlag(flags.json);
|
|
1083
|
+
|
|
1084
|
+
if (!backendHttp) {
|
|
1085
|
+
throw new Error('Missing backend URL. Set --backend-url (or --broker-http) or pair first.');
|
|
1086
|
+
}
|
|
1087
|
+
if (!deviceToken) {
|
|
1088
|
+
throw new Error('Missing device token in bridge state. Run: oomi openclaw pair --app-url https://www.oomi.ai --no-start');
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
const invite = await requestConnectInviteLink({
|
|
1092
|
+
backendHttp,
|
|
1093
|
+
appUrl,
|
|
1094
|
+
sessionKey,
|
|
1095
|
+
deviceToken,
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
const summary = {
|
|
1099
|
+
appUrl,
|
|
1100
|
+
backendHttp,
|
|
1101
|
+
inviteUrl: invite.inviteUrl,
|
|
1102
|
+
expiresAt: invite.expiresAt || null,
|
|
1103
|
+
sessionKey,
|
|
1104
|
+
};
|
|
1105
|
+
|
|
1106
|
+
if (jsonOutput) {
|
|
1107
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
console.log('Oomi Auth Invite Ready');
|
|
1112
|
+
console.log('----------------------');
|
|
1113
|
+
console.log(`Auth Link: ${summary.inviteUrl}`);
|
|
1114
|
+
if (summary.expiresAt) {
|
|
1115
|
+
console.log(`Expires: ${summary.expiresAt}`);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1036
1119
|
function printOpenclawPluginSetup(flags) {
|
|
1037
1120
|
const bridgeState = readBridgeState();
|
|
1038
1121
|
const backendUrl = String(
|
|
@@ -1141,6 +1224,11 @@ async function main() {
|
|
|
1141
1224
|
return;
|
|
1142
1225
|
}
|
|
1143
1226
|
|
|
1227
|
+
if (command === 'openclaw' && subcommand === 'invite') {
|
|
1228
|
+
await createOpenclawInviteLink(args.flags);
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1144
1232
|
if (command === 'openclaw' && subcommand === 'plugin') {
|
|
1145
1233
|
printOpenclawPluginSetup(args.flags);
|
|
1146
1234
|
return;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const WS_CONNECTING = 0;
|
|
2
|
+
const WS_OPEN = 1;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Ensure session state exists so client frames can be buffered before client.open arrives.
|
|
6
|
+
*/
|
|
7
|
+
export function ensureSessionBridge({ sessions, sessionId, createSocket }) {
|
|
8
|
+
const id = String(sessionId || '').trim();
|
|
9
|
+
if (!id) return null;
|
|
10
|
+
|
|
11
|
+
const existing = sessions.get(id);
|
|
12
|
+
if (existing) return existing;
|
|
13
|
+
|
|
14
|
+
const socket = createSocket(id);
|
|
15
|
+
const next = { socket, queue: [] };
|
|
16
|
+
sessions.set(id, next);
|
|
17
|
+
return next;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Forward a frame to the gateway socket or queue it while connecting.
|
|
22
|
+
*/
|
|
23
|
+
export function forwardFrameToSession(sessionBridge, frameText) {
|
|
24
|
+
if (!sessionBridge || !sessionBridge.socket || typeof frameText !== 'string' || !frameText) {
|
|
25
|
+
return 'dropped';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const { socket } = sessionBridge;
|
|
29
|
+
if (socket.readyState === WS_OPEN) {
|
|
30
|
+
socket.send(frameText);
|
|
31
|
+
return 'sent';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (socket.readyState === WS_CONNECTING) {
|
|
35
|
+
sessionBridge.queue.push(frameText);
|
|
36
|
+
return 'queued';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return 'dropped';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function flushSessionQueue(sessionBridge) {
|
|
43
|
+
if (!sessionBridge || !sessionBridge.socket) return;
|
|
44
|
+
const socket = sessionBridge.socket;
|
|
45
|
+
while (sessionBridge.queue.length > 0 && socket.readyState === WS_OPEN) {
|
|
46
|
+
const nextFrame = sessionBridge.queue.shift();
|
|
47
|
+
if (typeof nextFrame === 'string' && nextFrame) {
|
|
48
|
+
socket.send(nextFrame);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
package/openclaw.extension.js
CHANGED
|
@@ -154,7 +154,12 @@ async function postJson({ url, token, body, timeoutMs }) {
|
|
|
154
154
|
const oomiChannelPlugin = {
|
|
155
155
|
id: CHANNEL_ID,
|
|
156
156
|
meta: {
|
|
157
|
-
|
|
157
|
+
label: 'Oomi',
|
|
158
|
+
selectionLabel: 'Oomi (Managed)',
|
|
159
|
+
docsPath: '/channels/oomi',
|
|
160
|
+
docsLabel: 'oomi',
|
|
161
|
+
blurb: 'Managed channel transport for Oomi chat.',
|
|
162
|
+
aliases: ['oomi-ai'],
|
|
158
163
|
description: 'Managed Oomi channel plugin.',
|
|
159
164
|
},
|
|
160
165
|
capabilities: {
|
|
@@ -167,15 +172,22 @@ const oomiChannelPlugin = {
|
|
|
167
172
|
threads: true,
|
|
168
173
|
},
|
|
169
174
|
|
|
170
|
-
config
|
|
171
|
-
|
|
172
|
-
|
|
175
|
+
config: {
|
|
176
|
+
listAccountIds(cfg) {
|
|
177
|
+
const normalized = normalizeConfig(cfg);
|
|
178
|
+
return Object.entries(normalized.accounts)
|
|
179
|
+
.filter(([, account]) => account.enabled !== false)
|
|
180
|
+
.map(([accountId]) => accountId);
|
|
181
|
+
},
|
|
173
182
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
183
|
+
resolveAccount(cfg, accountId) {
|
|
184
|
+
const { accountId: resolvedAccountId, account } = resolveAccount(cfg, accountId);
|
|
185
|
+
if (!account) return null;
|
|
186
|
+
return {
|
|
187
|
+
id: resolvedAccountId,
|
|
188
|
+
...account,
|
|
189
|
+
};
|
|
190
|
+
},
|
|
179
191
|
},
|
|
180
192
|
|
|
181
193
|
outbound: {
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oomi-ai",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Oomi CLI for OpenClaw setup",
|
|
5
5
|
"bin": {
|
|
6
6
|
"oomi": "bin/oomi-ai.js"
|
|
@@ -46,7 +46,8 @@
|
|
|
46
46
|
"url": "https://github.com/crispcode-io/oomi/issues"
|
|
47
47
|
},
|
|
48
48
|
"scripts": {
|
|
49
|
-
"check": "node --check bin/oomi-ai.js"
|
|
49
|
+
"check": "node --check bin/oomi-ai.js",
|
|
50
|
+
"test": "node --test test/sessionBridgeState.test.mjs"
|
|
50
51
|
},
|
|
51
52
|
"dependencies": {
|
|
52
53
|
"ws": "^8.19.0"
|
|
@@ -54,6 +55,7 @@
|
|
|
54
55
|
"license": "MIT",
|
|
55
56
|
"files": [
|
|
56
57
|
"bin/oomi-ai.js",
|
|
58
|
+
"bin/sessionBridgeState.js",
|
|
57
59
|
"openclaw.plugin.json",
|
|
58
60
|
"openclaw.extension.js",
|
|
59
61
|
"agent_instructions.md",
|
|
@@ -31,7 +31,12 @@ If the user asks to connect OpenClaw to Oomi voice/web:
|
|
|
31
31
|
- Example: `oomi openclaw pair --app-url <URL> --no-start`
|
|
32
32
|
8. Return this exact result format to the user:
|
|
33
33
|
- `Oomi Connect Ready`
|
|
34
|
-
- `
|
|
35
|
-
-
|
|
34
|
+
- `Auth Link: ...`
|
|
35
|
+
- No extra narrative text before or after those lines.
|
|
36
|
+
|
|
37
|
+
Primary UX requirement:
|
|
38
|
+
- Never ask the user to enter raw pairing identifiers.
|
|
39
|
+
- The user should open the auth link, complete sign-in, and land directly in connected chat.
|
|
40
|
+
- Return `/connect/<invite-token>` auth links only.
|
|
36
41
|
|
|
37
42
|
Do not ask users to paste gateway IP/token/password when managed connect is available.
|