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 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
- - `Connect code: <device-id>`
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
- - `Code: ...`
57
- - `Link: ...`
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
- - `connect code` is the stable device id used by browser connect (`managed://<device-id>`).
74
+ - Invite auth links are the required user flow.
72
75
 
73
76
  Sync personas from the repo into the backend registry:
74
77
  ```
@@ -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
- - `Code: ...`
35
- - `Link: ...`
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:3000)
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 || activeGatewaySockets.has(sessionId)) return;
887
+ if (!sessionId) return;
813
888
  console.log(`[bridge] client.open ${sessionId}`);
814
- const gatewaySocket = new WebSocket(gateway.gatewayUrl);
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 = activeGatewaySockets.get(sessionId);
870
- if (!sessionBridge || !sessionBridge.socket) {
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
- if (gatewaySocket.readyState === WebSocket.OPEN) {
877
- gatewaySocket.send(frameWithAuth);
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
- sessionBridge.queue.push(frameWithAuth);
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:3000').trim();
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 connectCode = deviceId;
969
- const gatewayValue = `managed://${deviceId}`;
970
- const connectUrl = `${appUrl.replace(/\/$/, '')}/?connect=${encodeURIComponent(gatewayValue)}&sessionKey=${encodeURIComponent(sessionKey)}`;
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
- connectCode,
975
- gatewayValue,
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(`Connect code: ${connectCode}`);
988
- console.log(`Gateway value: ${gatewayValue}`);
989
- console.log(`Connect URL: ${connectUrl}`);
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(`Code: ${connectCode}`);
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
+ }
@@ -154,7 +154,12 @@ async function postJson({ url, token, body, timeoutMs }) {
154
154
  const oomiChannelPlugin = {
155
155
  id: CHANNEL_ID,
156
156
  meta: {
157
- name: 'Oomi',
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(cfg) {
171
- return normalizeConfig(cfg);
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
- listAccountIds(cfg) {
175
- const normalized = normalizeConfig(cfg);
176
- return Object.entries(normalized.accounts)
177
- .filter(([, account]) => account.enabled !== false)
178
- .map(([accountId]) => accountId);
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: {
@@ -2,7 +2,7 @@
2
2
  "id": "oomi-ai",
3
3
  "name": "Oomi Channel Plugin",
4
4
  "description": "Managed Oomi channel integration for OpenClaw.",
5
- "version": "0.2.1",
5
+ "version": "0.2.4",
6
6
  "author": "Oomi",
7
7
  "license": "MIT",
8
8
  "openclawVersion": ">=0.5.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oomi-ai",
3
- "version": "0.2.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
- - `Code: ...`
35
- - `Link: ...`
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.