oomi-ai 0.2.3 → 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/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>';
@@ -790,6 +791,58 @@ async function startOpenclawBridge(flags) {
790
791
 
791
792
  const brokerSocket = new WebSocket(wsUrl.toString());
792
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
+ };
793
846
 
794
847
  brokerSocket.on('open', () => {
795
848
  console.log('[bridge] Connected to managed broker.');
@@ -831,55 +884,9 @@ async function startOpenclawBridge(flags) {
831
884
 
832
885
  if (payload.type === 'client.open') {
833
886
  const sessionId = String(payload.sessionId || '').trim();
834
- if (!sessionId || activeGatewaySockets.has(sessionId)) return;
887
+ if (!sessionId) return;
835
888
  console.log(`[bridge] client.open ${sessionId}`);
836
- const gatewaySocket = new WebSocket(gateway.gatewayUrl);
837
- const sessionBridge = {
838
- socket: gatewaySocket,
839
- queue: [],
840
- };
841
- activeGatewaySockets.set(sessionId, sessionBridge);
842
-
843
- gatewaySocket.on('open', () => {
844
- console.log(`[bridge] gateway.open ${sessionId}`);
845
- while (sessionBridge.queue.length > 0 && gatewaySocket.readyState === WebSocket.OPEN) {
846
- const nextFrame = sessionBridge.queue.shift();
847
- if (typeof nextFrame === 'string' && nextFrame) {
848
- gatewaySocket.send(nextFrame);
849
- }
850
- }
851
- });
852
-
853
- gatewaySocket.on('message', (gatewayRaw) => {
854
- const frame = typeof gatewayRaw === 'string' ? gatewayRaw : gatewayRaw.toString();
855
- sendBrokerPayload(brokerSocket, { action: 'gateway_frame', type: 'gateway.frame', sessionId, frame });
856
- });
857
-
858
- gatewaySocket.on('close', (code, reason) => {
859
- const reasonText = reason ? reason.toString() : '';
860
- console.log(
861
- `[bridge] gateway.close ${sessionId} code=${String(code)}${reasonText ? ` reason=${reasonText}` : ''}`
862
- );
863
- activeGatewaySockets.delete(sessionId);
864
- sendBrokerPayload(brokerSocket, {
865
- action: 'gateway_closed',
866
- type: 'gateway.closed',
867
- sessionId,
868
- code,
869
- reason: reasonText,
870
- });
871
- });
872
-
873
- gatewaySocket.on('error', (err) => {
874
- console.error(`[bridge] gateway.error ${sessionId}: ${String(err)}`);
875
- sendBrokerPayload(brokerSocket, {
876
- action: 'log',
877
- type: 'log',
878
- sessionId,
879
- level: 'error',
880
- message: `Gateway socket error (${sessionId}): ${String(err)}`,
881
- });
882
- });
889
+ getOrCreateGatewaySession(sessionId);
883
890
  return;
884
891
  }
885
892
 
@@ -888,19 +895,13 @@ async function startOpenclawBridge(flags) {
888
895
  const frame = typeof payload.frame === 'string' ? payload.frame : '';
889
896
  if (!sessionId || !frame) return;
890
897
  console.log(`[bridge] client.frame ${sessionId}`);
891
- const sessionBridge = activeGatewaySockets.get(sessionId);
892
- if (!sessionBridge || !sessionBridge.socket) {
893
- console.log(`[bridge] client.frame dropped (no session) ${sessionId}`);
894
- return;
895
- }
896
- const gatewaySocket = sessionBridge.socket;
898
+ const sessionBridge = getOrCreateGatewaySession(sessionId);
899
+ if (!sessionBridge) return;
897
900
  const frameWithAuth = injectGatewayAuth(frame, gateway);
898
- if (gatewaySocket.readyState === WebSocket.OPEN) {
899
- gatewaySocket.send(frameWithAuth);
900
- } else if (gatewaySocket.readyState === WebSocket.CONNECTING) {
901
+ const result = forwardFrameToSession(sessionBridge, frameWithAuth);
902
+ if (result === 'queued') {
901
903
  console.log(`[bridge] client.frame queued ${sessionId}`);
902
- sessionBridge.queue.push(frameWithAuth);
903
- } else {
904
+ } else if (result === 'dropped') {
904
905
  console.log(`[bridge] client.frame dropped (socket not open) ${sessionId}`);
905
906
  }
906
907
  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.3",
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",