oomi-ai 0.2.1 → 0.2.3
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 +31 -24
- package/agent_instructions.md +17 -9
- package/bin/oomi-ai.js +200 -17
- package/openclaw.extension.js +247 -0
- package/openclaw.plugin.json +17 -0
- package/package.json +24 -2
- package/skills/oomi/agent_instructions.md +17 -9
package/README.md
CHANGED
|
@@ -14,55 +14,64 @@ npm install -g oomi-ai
|
|
|
14
14
|
|
|
15
15
|
## Usage
|
|
16
16
|
|
|
17
|
-
Install
|
|
17
|
+
Install as an OpenClaw channel extension (preferred architecture):
|
|
18
18
|
```
|
|
19
|
-
oomi
|
|
19
|
+
openclaw plugins install oomi-ai@latest
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
This package now ships an OpenClaw channel plugin (`openclaw.plugin.json`) with channel id `oomi`.
|
|
23
|
+
Channel account config fields (`channels.oomi.accounts.<accountId>`):
|
|
24
|
+
- `backendUrl`
|
|
25
|
+
- `deviceToken`
|
|
26
|
+
- `defaultSessionKey` (optional, default `agent:main:webchat:channel:oomi`)
|
|
27
|
+
- `requestTimeoutMs` (optional)
|
|
28
|
+
|
|
29
|
+
Print plugin install/config guidance from local pair state:
|
|
23
30
|
```
|
|
24
|
-
oomi openclaw
|
|
31
|
+
oomi openclaw plugin
|
|
25
32
|
```
|
|
26
33
|
|
|
27
|
-
|
|
34
|
+
Install agent instructions only:
|
|
28
35
|
```
|
|
29
|
-
oomi
|
|
30
|
-
--broker-http https://your-signaling-service.example.com \
|
|
31
|
-
--broker-ws wss://your-signaling-service.example.com/cable \
|
|
32
|
-
--pair-code ABCD2345
|
|
36
|
+
oomi init
|
|
33
37
|
```
|
|
34
38
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
Install agent instructions + Oomi skill:
|
|
40
|
+
```
|
|
41
|
+
oomi openclaw install
|
|
42
|
+
```
|
|
38
43
|
|
|
39
|
-
Pair
|
|
44
|
+
Pair and provision device token from Oomi web backend:
|
|
40
45
|
```
|
|
41
|
-
oomi openclaw pair --app-url https://your-oomi-app.vercel.app --device-id my-openclaw-mac --
|
|
46
|
+
oomi openclaw pair --app-url https://your-oomi-app.vercel.app --device-id my-openclaw-mac --no-start
|
|
42
47
|
```
|
|
43
48
|
|
|
44
49
|
`--app-url` must be reachable from the OpenClaw host. If OpenClaw runs on a different machine/network, do not use `localhost` unless tunneled.
|
|
45
50
|
|
|
46
51
|
This prints:
|
|
47
|
-
- `
|
|
48
|
-
- `Connect URL: https://.../?connect=managed://<device-id>&sessionKey=...`
|
|
52
|
+
- `Auth invite URL: https://.../connect/<single-use-token>`
|
|
49
53
|
- A copy-ready block for the user:
|
|
50
54
|
- `Oomi Connect Ready`
|
|
51
|
-
- `
|
|
52
|
-
|
|
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
|
+
```
|
|
53
61
|
|
|
54
62
|
Agent-intent mapping (recommended):
|
|
55
63
|
- If user says `Connect yourself to Oomi. Use app URL https://www.oomi.ai.`
|
|
56
64
|
- Run:
|
|
57
65
|
- `curl -fsSL https://www.oomi.ai/install.sh | bash`
|
|
58
66
|
- or `pnpm add -g oomi-ai@latest` (`npm install -g oomi-ai@latest` fallback)
|
|
59
|
-
- `oomi openclaw pair --app-url https://www.oomi.ai --
|
|
60
|
-
-
|
|
61
|
-
- `oomi openclaw
|
|
67
|
+
- `oomi openclaw pair --app-url https://www.oomi.ai --no-start`
|
|
68
|
+
- `openclaw plugins install oomi-ai@latest`
|
|
69
|
+
- `oomi openclaw plugin --show-secrets --backend-url https://api.oomi.ai`
|
|
70
|
+
- Apply `channels.oomi.accounts.default` config and restart OpenClaw.
|
|
62
71
|
|
|
63
72
|
Important distinction:
|
|
64
73
|
- `pairCode` is one-time and used internally by the pair/bootstrap flow.
|
|
65
|
-
-
|
|
74
|
+
- Invite auth links are the required user flow.
|
|
66
75
|
|
|
67
76
|
Sync personas from the repo into the backend registry:
|
|
68
77
|
```
|
|
@@ -79,9 +88,7 @@ Optional flags:
|
|
|
79
88
|
oomi init --workspace /path/to/openclaw/workspace
|
|
80
89
|
oomi init --agents-file /path/to/AGENTS.md
|
|
81
90
|
oomi openclaw install --skills-dir /path/to/openclaw/skills
|
|
82
|
-
oomi openclaw bridge --broker-ws wss://.../cable --device-token <token>
|
|
83
91
|
oomi openclaw pair --app-url https://your-oomi-app.vercel.app --no-start
|
|
84
|
-
oomi openclaw pair --app-url https://your-oomi-app.vercel.app --detach
|
|
85
92
|
oomi openclaw pair --app-url https://your-oomi-app.vercel.app --json
|
|
86
93
|
oomi personas sync --root /path/to/oomi
|
|
87
94
|
oomi personas create creator --status active --chat-session agent:main:webchat:channel:oomi-creator
|
package/agent_instructions.md
CHANGED
|
@@ -19,16 +19,24 @@ If the user asks to connect OpenClaw to Oomi voice/web:
|
|
|
19
19
|
- `curl -fsSL https://www.oomi.ai/install.sh | bash`
|
|
20
20
|
2. Ensure `oomi-ai` is installed (`pnpm add -g oomi-ai@latest`).
|
|
21
21
|
- Fallback: `npm install -g oomi-ai@latest`
|
|
22
|
-
3.
|
|
23
|
-
- `oomi openclaw pair --app-url https://www.oomi.ai --
|
|
24
|
-
4.
|
|
25
|
-
- `
|
|
26
|
-
5.
|
|
22
|
+
3. Pair device token without starting legacy bridge:
|
|
23
|
+
- `oomi openclaw pair --app-url https://www.oomi.ai --no-start`
|
|
24
|
+
4. Install OpenClaw plugin:
|
|
25
|
+
- `openclaw plugins install oomi-ai@latest`
|
|
26
|
+
5. Configure channel account (`channels.oomi.accounts.default`) from:
|
|
27
|
+
- `oomi openclaw plugin --show-secrets --backend-url https://api.oomi.ai`
|
|
28
|
+
6. Restart OpenClaw.
|
|
29
|
+
7. If user provides an app URL (for example "Connect yourself to Oomi. Use app URL <URL>."):
|
|
27
30
|
- Use that URL directly in the pair command.
|
|
28
|
-
- Example: `oomi openclaw pair --app-url <URL> --
|
|
29
|
-
|
|
31
|
+
- Example: `oomi openclaw pair --app-url <URL> --no-start`
|
|
32
|
+
8. Return this exact result format to the user:
|
|
30
33
|
- `Oomi Connect Ready`
|
|
31
|
-
- `
|
|
32
|
-
-
|
|
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.
|
|
33
41
|
|
|
34
42
|
Do not ask users to paste gateway IP/token/password when managed connect is available.
|
package/bin/oomi-ai.js
CHANGED
|
@@ -142,6 +142,12 @@ Commands:
|
|
|
142
142
|
openclaw pair
|
|
143
143
|
Pair this OpenClaw host with Oomi and start bridge (single command).
|
|
144
144
|
|
|
145
|
+
openclaw invite
|
|
146
|
+
Create a single-use auth invite link for the paired OpenClaw device.
|
|
147
|
+
|
|
148
|
+
openclaw plugin
|
|
149
|
+
Print OpenClaw extension install/config guidance for Oomi channel plugin.
|
|
150
|
+
|
|
145
151
|
personas sync
|
|
146
152
|
Sync personas from the repo into the Oomi backend registry.
|
|
147
153
|
|
|
@@ -155,13 +161,14 @@ Common flags:
|
|
|
155
161
|
--broker-http URL Managed broker HTTPS URL (for pair claim)
|
|
156
162
|
--broker-ws URL Managed broker device WS URL (wss://.../cable)
|
|
157
163
|
--pair-code CODE One-time pairing code from Oomi
|
|
158
|
-
--app-url URL Oomi app URL used for pairing APIs (default: http://127.0.0.1:
|
|
164
|
+
--app-url URL Oomi app URL used for pairing APIs (default: http://127.0.0.1:3456)
|
|
159
165
|
--label TEXT Pairing label shown in broker logs
|
|
160
166
|
--session-key KEY Session key used in generated connect URL
|
|
161
167
|
--detach Start bridge in background and exit
|
|
162
168
|
--no-start Pair and save token, but do not start bridge
|
|
163
169
|
--device-id ID Bridge device identifier (default: host name)
|
|
164
170
|
--device-token TOKEN Existing bridge device token
|
|
171
|
+
--show-secrets Print full token values in diagnostic output
|
|
165
172
|
--json Print pairing result as JSON (for automation)
|
|
166
173
|
--backend-url URL Override Oomi backend URL
|
|
167
174
|
--root PATH Override repo root path for persona discovery
|
|
@@ -591,6 +598,25 @@ async function requestManagedPairCode({ appUrl, label }) {
|
|
|
591
598
|
return payload;
|
|
592
599
|
}
|
|
593
600
|
|
|
601
|
+
async function requestConnectInviteLink({ backendHttp, appUrl, sessionKey, deviceToken }) {
|
|
602
|
+
const response = await fetch(`${backendHttp.replace(/\/$/, '')}/v1/invite_links/start`, {
|
|
603
|
+
method: 'POST',
|
|
604
|
+
headers: {
|
|
605
|
+
'Content-Type': 'application/json',
|
|
606
|
+
Authorization: `Bearer ${deviceToken}`,
|
|
607
|
+
},
|
|
608
|
+
body: JSON.stringify({ appUrl, sessionKey }),
|
|
609
|
+
});
|
|
610
|
+
const payload = await response.json().catch(() => ({}));
|
|
611
|
+
if (!response.ok || !payload?.inviteUrl) {
|
|
612
|
+
const message =
|
|
613
|
+
(payload && typeof payload.error === 'string' && payload.error) ||
|
|
614
|
+
`Invite link start failed (${response.status})`;
|
|
615
|
+
throw new Error(message);
|
|
616
|
+
}
|
|
617
|
+
return payload;
|
|
618
|
+
}
|
|
619
|
+
|
|
594
620
|
async function fetchManagedGatewayConfig({ appUrl }) {
|
|
595
621
|
const baseUrl = appUrl.replace(/\/$/, '');
|
|
596
622
|
const response = await fetch(`${baseUrl}/api/gateway/managed/config`, {
|
|
@@ -614,6 +640,17 @@ function injectGatewayAuth(frameText, gatewayAuth) {
|
|
|
614
640
|
return frameText;
|
|
615
641
|
}
|
|
616
642
|
const params = frame.params && typeof frame.params === 'object' ? frame.params : {};
|
|
643
|
+
const existingScopes = Array.isArray(params.scopes)
|
|
644
|
+
? params.scopes.filter((value) => typeof value === 'string' && value.trim())
|
|
645
|
+
: [];
|
|
646
|
+
const requiredScopes = ['operator.read', 'operator.write'];
|
|
647
|
+
for (const scope of requiredScopes) {
|
|
648
|
+
if (!existingScopes.includes(scope)) {
|
|
649
|
+
existingScopes.push(scope);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
params.scopes = existingScopes;
|
|
653
|
+
|
|
617
654
|
const auth = {};
|
|
618
655
|
if (gatewayAuth.token) auth.token = gatewayAuth.token;
|
|
619
656
|
else if (gatewayAuth.password) auth.password = gatewayAuth.password;
|
|
@@ -818,10 +855,19 @@ async function startOpenclawBridge(flags) {
|
|
|
818
855
|
sendBrokerPayload(brokerSocket, { action: 'gateway_frame', type: 'gateway.frame', sessionId, frame });
|
|
819
856
|
});
|
|
820
857
|
|
|
821
|
-
gatewaySocket.on('close', () => {
|
|
822
|
-
|
|
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
|
+
);
|
|
823
863
|
activeGatewaySockets.delete(sessionId);
|
|
824
|
-
sendBrokerPayload(brokerSocket, {
|
|
864
|
+
sendBrokerPayload(brokerSocket, {
|
|
865
|
+
action: 'gateway_closed',
|
|
866
|
+
type: 'gateway.closed',
|
|
867
|
+
sessionId,
|
|
868
|
+
code,
|
|
869
|
+
reason: reasonText,
|
|
870
|
+
});
|
|
825
871
|
});
|
|
826
872
|
|
|
827
873
|
gatewaySocket.on('error', (err) => {
|
|
@@ -900,7 +946,7 @@ async function startOpenclawBridge(flags) {
|
|
|
900
946
|
|
|
901
947
|
async function pairAndStartOpenclawBridge(flags) {
|
|
902
948
|
const bridgeState = readBridgeState();
|
|
903
|
-
const appUrl = String(flags['app-url'] || process.env.OOMI_APP_URL || 'http://127.0.0.1:
|
|
949
|
+
const appUrl = String(flags['app-url'] || process.env.OOMI_APP_URL || 'http://127.0.0.1:3456').trim();
|
|
904
950
|
const deviceId = resolveDeviceId(flags, bridgeState);
|
|
905
951
|
const label = String(flags.label || `${deviceId}-bridge`).trim();
|
|
906
952
|
const sessionKey = String(
|
|
@@ -937,19 +983,24 @@ async function pairAndStartOpenclawBridge(flags) {
|
|
|
937
983
|
brokerWs,
|
|
938
984
|
deviceId,
|
|
939
985
|
deviceToken,
|
|
986
|
+
sessionKey,
|
|
940
987
|
claimedAt: new Date().toISOString(),
|
|
941
988
|
expiresAt: claimed.expiresAt || null,
|
|
942
989
|
});
|
|
943
990
|
|
|
944
|
-
const
|
|
945
|
-
|
|
946
|
-
|
|
991
|
+
const invite = await requestConnectInviteLink({
|
|
992
|
+
backendHttp: managedConfig.brokerHttpUrl,
|
|
993
|
+
appUrl,
|
|
994
|
+
sessionKey,
|
|
995
|
+
deviceToken,
|
|
996
|
+
});
|
|
997
|
+
const inviteUrl = String(invite.inviteUrl || '').trim();
|
|
998
|
+
const inviteExpiresAt = String(invite.expiresAt || '').trim();
|
|
947
999
|
|
|
948
1000
|
const pairSummary = {
|
|
949
1001
|
deviceId,
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
connectUrl,
|
|
1002
|
+
inviteUrl,
|
|
1003
|
+
inviteExpiresAt,
|
|
953
1004
|
brokerHttpUrl: managedConfig.brokerHttpUrl,
|
|
954
1005
|
brokerDeviceWsUrl: brokerWs,
|
|
955
1006
|
bridgeMode: shouldStart ? (detach ? 'detached' : 'attached') : 'skipped',
|
|
@@ -960,15 +1011,15 @@ async function pairAndStartOpenclawBridge(flags) {
|
|
|
960
1011
|
console.log(JSON.stringify(pairSummary, null, 2));
|
|
961
1012
|
} else {
|
|
962
1013
|
console.log(`Paired device: ${deviceId}`);
|
|
963
|
-
console.log(`
|
|
964
|
-
|
|
965
|
-
|
|
1014
|
+
console.log(`Auth invite URL: ${inviteUrl}`);
|
|
1015
|
+
if (inviteExpiresAt) {
|
|
1016
|
+
console.log(`Invite expires at: ${inviteExpiresAt}`);
|
|
1017
|
+
}
|
|
966
1018
|
console.log('');
|
|
967
|
-
console.log('Share this with the user:');
|
|
1019
|
+
console.log('Share this exact response with the user (no extra text):');
|
|
968
1020
|
console.log('----------------------------------------');
|
|
969
1021
|
console.log('Oomi Connect Ready');
|
|
970
|
-
console.log(`
|
|
971
|
-
console.log(`Link: ${connectUrl}`);
|
|
1022
|
+
console.log(`Auth Link: ${inviteUrl}`);
|
|
972
1023
|
console.log('----------------------------------------');
|
|
973
1024
|
}
|
|
974
1025
|
|
|
@@ -1009,6 +1060,128 @@ async function pairAndStartOpenclawBridge(flags) {
|
|
|
1009
1060
|
});
|
|
1010
1061
|
}
|
|
1011
1062
|
|
|
1063
|
+
async function createOpenclawInviteLink(flags) {
|
|
1064
|
+
const bridgeState = readBridgeState();
|
|
1065
|
+
const backendHttp = String(
|
|
1066
|
+
flags['backend-url'] ||
|
|
1067
|
+
flags['broker-http'] ||
|
|
1068
|
+
process.env.OOMI_BACKEND_URL ||
|
|
1069
|
+
process.env.OOMI_CHAT_BROKER_HTTP_URL ||
|
|
1070
|
+
bridgeState.brokerHttp ||
|
|
1071
|
+
''
|
|
1072
|
+
).trim();
|
|
1073
|
+
const appUrl = String(flags['app-url'] || process.env.OOMI_APP_URL || 'http://127.0.0.1:3456').trim();
|
|
1074
|
+
const sessionKey = String(
|
|
1075
|
+
flags['session-key'] ||
|
|
1076
|
+
process.env.OOMI_SESSION_KEY ||
|
|
1077
|
+
bridgeState.sessionKey ||
|
|
1078
|
+
'agent:main:webchat:channel:oomi'
|
|
1079
|
+
).trim();
|
|
1080
|
+
const deviceToken = String(flags['device-token'] || bridgeState.deviceToken || '').trim();
|
|
1081
|
+
const jsonOutput = isTruthyFlag(flags.json);
|
|
1082
|
+
|
|
1083
|
+
if (!backendHttp) {
|
|
1084
|
+
throw new Error('Missing backend URL. Set --backend-url (or --broker-http) or pair first.');
|
|
1085
|
+
}
|
|
1086
|
+
if (!deviceToken) {
|
|
1087
|
+
throw new Error('Missing device token in bridge state. Run: oomi openclaw pair --app-url https://www.oomi.ai --no-start');
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
const invite = await requestConnectInviteLink({
|
|
1091
|
+
backendHttp,
|
|
1092
|
+
appUrl,
|
|
1093
|
+
sessionKey,
|
|
1094
|
+
deviceToken,
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
const summary = {
|
|
1098
|
+
appUrl,
|
|
1099
|
+
backendHttp,
|
|
1100
|
+
inviteUrl: invite.inviteUrl,
|
|
1101
|
+
expiresAt: invite.expiresAt || null,
|
|
1102
|
+
sessionKey,
|
|
1103
|
+
};
|
|
1104
|
+
|
|
1105
|
+
if (jsonOutput) {
|
|
1106
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
console.log('Oomi Auth Invite Ready');
|
|
1111
|
+
console.log('----------------------');
|
|
1112
|
+
console.log(`Auth Link: ${summary.inviteUrl}`);
|
|
1113
|
+
if (summary.expiresAt) {
|
|
1114
|
+
console.log(`Expires: ${summary.expiresAt}`);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function printOpenclawPluginSetup(flags) {
|
|
1119
|
+
const bridgeState = readBridgeState();
|
|
1120
|
+
const backendUrl = String(
|
|
1121
|
+
flags['backend-url'] ||
|
|
1122
|
+
process.env.OOMI_BACKEND_URL ||
|
|
1123
|
+
process.env.OOMI_CHAT_BROKER_HTTP_URL ||
|
|
1124
|
+
bridgeState.brokerHttp ||
|
|
1125
|
+
''
|
|
1126
|
+
).trim();
|
|
1127
|
+
const deviceToken = String(
|
|
1128
|
+
flags['device-token'] ||
|
|
1129
|
+
bridgeState.deviceToken ||
|
|
1130
|
+
''
|
|
1131
|
+
).trim();
|
|
1132
|
+
const showSecrets = isTruthyFlag(flags['show-secrets']);
|
|
1133
|
+
const redactToken = (value) => {
|
|
1134
|
+
if (!value) return '';
|
|
1135
|
+
if (showSecrets) return value;
|
|
1136
|
+
if (value.length <= 12) return '***';
|
|
1137
|
+
return `${value.slice(0, 6)}...${value.slice(-6)}`;
|
|
1138
|
+
};
|
|
1139
|
+
const defaultSessionKey = String(
|
|
1140
|
+
flags['session-key'] ||
|
|
1141
|
+
process.env.OOMI_SESSION_KEY ||
|
|
1142
|
+
'agent:main:webchat:channel:oomi'
|
|
1143
|
+
).trim();
|
|
1144
|
+
|
|
1145
|
+
console.log('OpenClaw Oomi Plugin Setup');
|
|
1146
|
+
console.log('--------------------------');
|
|
1147
|
+
console.log('1) Install extension package in OpenClaw:');
|
|
1148
|
+
console.log(' openclaw plugins install oomi-ai@latest');
|
|
1149
|
+
console.log('');
|
|
1150
|
+
console.log('2) Configure OpenClaw channel account (channels.oomi.accounts.default):');
|
|
1151
|
+
console.log(
|
|
1152
|
+
JSON.stringify(
|
|
1153
|
+
{
|
|
1154
|
+
channels: {
|
|
1155
|
+
oomi: {
|
|
1156
|
+
defaultAccountId: 'default',
|
|
1157
|
+
accounts: {
|
|
1158
|
+
default: {
|
|
1159
|
+
enabled: true,
|
|
1160
|
+
backendUrl,
|
|
1161
|
+
deviceToken: redactToken(deviceToken),
|
|
1162
|
+
defaultSessionKey,
|
|
1163
|
+
},
|
|
1164
|
+
},
|
|
1165
|
+
},
|
|
1166
|
+
},
|
|
1167
|
+
},
|
|
1168
|
+
null,
|
|
1169
|
+
2
|
|
1170
|
+
)
|
|
1171
|
+
);
|
|
1172
|
+
if (deviceToken && !showSecrets) {
|
|
1173
|
+
console.log('Token is redacted by default. Use --show-secrets to print full values.');
|
|
1174
|
+
console.log(`Bridge state file: ${resolveBridgeStatePath()}`);
|
|
1175
|
+
}
|
|
1176
|
+
console.log('');
|
|
1177
|
+
|
|
1178
|
+
if (!backendUrl || !deviceToken) {
|
|
1179
|
+
console.log('Missing backend/device credentials in local state.');
|
|
1180
|
+
console.log('Run: oomi openclaw pair --app-url https://www.oomi.ai --no-start');
|
|
1181
|
+
console.log('Then run: oomi openclaw plugin');
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1012
1185
|
async function main() {
|
|
1013
1186
|
const args = parseArgs(process.argv);
|
|
1014
1187
|
const command = args.command;
|
|
@@ -1050,6 +1223,16 @@ async function main() {
|
|
|
1050
1223
|
return;
|
|
1051
1224
|
}
|
|
1052
1225
|
|
|
1226
|
+
if (command === 'openclaw' && subcommand === 'invite') {
|
|
1227
|
+
await createOpenclawInviteLink(args.flags);
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
if (command === 'openclaw' && subcommand === 'plugin') {
|
|
1232
|
+
printOpenclawPluginSetup(args.flags);
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1053
1236
|
if (command === 'personas' && subcommand === 'sync') {
|
|
1054
1237
|
await syncPersonas({ backendUrl: args.flags['backend-url'], root: args.flags.root });
|
|
1055
1238
|
return;
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
const CHANNEL_ID = 'oomi';
|
|
2
|
+
const DEFAULT_SESSION_KEY = 'agent:main:webchat:channel:oomi';
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 15000;
|
|
4
|
+
|
|
5
|
+
function toString(value, fallback = '') {
|
|
6
|
+
return typeof value === 'string' && value.trim() ? value.trim() : fallback;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function toNumber(value, fallback, { min = 1, max = Number.MAX_SAFE_INTEGER } = {}) {
|
|
10
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return fallback;
|
|
11
|
+
const normalized = Math.floor(value);
|
|
12
|
+
if (normalized < min) return fallback;
|
|
13
|
+
if (normalized > max) return max;
|
|
14
|
+
return normalized;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseAccounts(rawAccounts) {
|
|
18
|
+
if (!rawAccounts || typeof rawAccounts !== 'object') return {};
|
|
19
|
+
const accounts = {};
|
|
20
|
+
|
|
21
|
+
for (const [accountId, raw] of Object.entries(rawAccounts)) {
|
|
22
|
+
if (!raw || typeof raw !== 'object') continue;
|
|
23
|
+
accounts[accountId] = {
|
|
24
|
+
enabled: raw.enabled !== false,
|
|
25
|
+
backendUrl: toString(raw.backendUrl),
|
|
26
|
+
deviceToken: toString(raw.deviceToken),
|
|
27
|
+
defaultSessionKey: toString(raw.defaultSessionKey, DEFAULT_SESSION_KEY),
|
|
28
|
+
requestTimeoutMs: toNumber(raw.requestTimeoutMs, DEFAULT_TIMEOUT_MS, { min: 2000, max: 120000 }),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return accounts;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeConfig(cfg = {}) {
|
|
36
|
+
const channelConfig = cfg?.channels?.[CHANNEL_ID] || {};
|
|
37
|
+
const configuredAccounts = parseAccounts(channelConfig.accounts);
|
|
38
|
+
const accountIds = Object.keys(configuredAccounts);
|
|
39
|
+
const defaultAccountId = toString(channelConfig.defaultAccountId, accountIds[0] || 'default');
|
|
40
|
+
|
|
41
|
+
if (!configuredAccounts[defaultAccountId]) {
|
|
42
|
+
configuredAccounts[defaultAccountId] = {
|
|
43
|
+
enabled: true,
|
|
44
|
+
backendUrl: '',
|
|
45
|
+
deviceToken: '',
|
|
46
|
+
defaultSessionKey: DEFAULT_SESSION_KEY,
|
|
47
|
+
requestTimeoutMs: DEFAULT_TIMEOUT_MS,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
defaultAccountId,
|
|
53
|
+
accounts: configuredAccounts,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function resolveAccount(cfg, accountId) {
|
|
58
|
+
const normalized = normalizeConfig(cfg);
|
|
59
|
+
const resolvedId = toString(accountId, normalized.defaultAccountId);
|
|
60
|
+
const account = normalized.accounts[resolvedId];
|
|
61
|
+
if (!account) {
|
|
62
|
+
return {
|
|
63
|
+
accountId: resolvedId,
|
|
64
|
+
account: null,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
accountId: resolvedId,
|
|
70
|
+
account,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function extractText(payload) {
|
|
75
|
+
if (!payload) return '';
|
|
76
|
+
if (typeof payload === 'string') return payload.trim();
|
|
77
|
+
|
|
78
|
+
const direct = [payload.text, payload.message, payload.content, payload.body];
|
|
79
|
+
for (const value of direct) {
|
|
80
|
+
if (typeof value === 'string' && value.trim()) return value.trim();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (Array.isArray(payload.content)) {
|
|
84
|
+
return payload.content
|
|
85
|
+
.filter((part) => part && typeof part === 'object' && part.type === 'text' && typeof part.text === 'string')
|
|
86
|
+
.map((part) => part.text.trim())
|
|
87
|
+
.filter(Boolean)
|
|
88
|
+
.join('\n');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return '';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function extractConversationKey(payload) {
|
|
95
|
+
const candidates = [
|
|
96
|
+
payload?.conversationKey,
|
|
97
|
+
payload?.threadId,
|
|
98
|
+
payload?.target?.conversationKey,
|
|
99
|
+
payload?.target?.threadId,
|
|
100
|
+
payload?.target?.id,
|
|
101
|
+
payload?.metadata?.conversationKey,
|
|
102
|
+
payload?.metadata?.threadId,
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
for (const candidate of candidates) {
|
|
106
|
+
const value = toString(candidate);
|
|
107
|
+
if (value) return value;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return '';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function extractUserId(payload) {
|
|
114
|
+
const candidates = [
|
|
115
|
+
payload?.userId,
|
|
116
|
+
payload?.target?.userId,
|
|
117
|
+
payload?.metadata?.userId,
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
for (const candidate of candidates) {
|
|
121
|
+
const value = toString(candidate);
|
|
122
|
+
if (value) return value;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return '';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function postJson({ url, token, body, timeoutMs }) {
|
|
129
|
+
const controller = new AbortController();
|
|
130
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const response = await fetch(url, {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
headers: {
|
|
136
|
+
'Content-Type': 'application/json',
|
|
137
|
+
Authorization: `Bearer ${token}`,
|
|
138
|
+
},
|
|
139
|
+
body: JSON.stringify(body),
|
|
140
|
+
signal: controller.signal,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const payload = await response.json().catch(() => ({}));
|
|
144
|
+
return {
|
|
145
|
+
ok: response.ok,
|
|
146
|
+
status: response.status,
|
|
147
|
+
payload,
|
|
148
|
+
};
|
|
149
|
+
} finally {
|
|
150
|
+
clearTimeout(timeout);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const oomiChannelPlugin = {
|
|
155
|
+
id: CHANNEL_ID,
|
|
156
|
+
meta: {
|
|
157
|
+
name: 'Oomi',
|
|
158
|
+
description: 'Managed Oomi channel plugin.',
|
|
159
|
+
},
|
|
160
|
+
capabilities: {
|
|
161
|
+
chatTypes: ['direct'],
|
|
162
|
+
media: {
|
|
163
|
+
images: false,
|
|
164
|
+
audio: false,
|
|
165
|
+
files: false,
|
|
166
|
+
},
|
|
167
|
+
threads: true,
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
config(cfg) {
|
|
171
|
+
return normalizeConfig(cfg);
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
listAccountIds(cfg) {
|
|
175
|
+
const normalized = normalizeConfig(cfg);
|
|
176
|
+
return Object.entries(normalized.accounts)
|
|
177
|
+
.filter(([, account]) => account.enabled !== false)
|
|
178
|
+
.map(([accountId]) => accountId);
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
outbound: {
|
|
182
|
+
deliveryMode: 'direct',
|
|
183
|
+
|
|
184
|
+
async sendText(payload = {}) {
|
|
185
|
+
const { cfg, accountId } = payload;
|
|
186
|
+
const { accountId: resolvedAccountId, account } = resolveAccount(cfg, accountId);
|
|
187
|
+
|
|
188
|
+
if (!account || account.enabled === false) {
|
|
189
|
+
return {
|
|
190
|
+
ok: false,
|
|
191
|
+
error: `oomi account is disabled or missing (${resolvedAccountId})`,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
if (!account.backendUrl || !account.deviceToken) {
|
|
195
|
+
return {
|
|
196
|
+
ok: false,
|
|
197
|
+
error: `oomi account is missing backendUrl/deviceToken (${resolvedAccountId})`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const content = extractText(payload);
|
|
202
|
+
if (!content) {
|
|
203
|
+
return {
|
|
204
|
+
ok: false,
|
|
205
|
+
error: 'oomi outbound message content is empty',
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const conversationKey = extractConversationKey(payload);
|
|
210
|
+
const userId = extractUserId(payload);
|
|
211
|
+
const sessionKey = toString(payload?.sessionKey || payload?.metadata?.sessionKey, account.defaultSessionKey);
|
|
212
|
+
|
|
213
|
+
const response = await postJson({
|
|
214
|
+
url: `${account.backendUrl}/v1/channel/plugin/messages`,
|
|
215
|
+
token: account.deviceToken,
|
|
216
|
+
timeoutMs: account.requestTimeoutMs,
|
|
217
|
+
body: {
|
|
218
|
+
conversationKey,
|
|
219
|
+
userId,
|
|
220
|
+
sessionKey,
|
|
221
|
+
content,
|
|
222
|
+
source: 'openclaw.channel',
|
|
223
|
+
metadata: {
|
|
224
|
+
accountId: resolvedAccountId,
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
if (!response.ok) {
|
|
230
|
+
const reason = toString(response.payload?.error, `status ${response.status}`);
|
|
231
|
+
return {
|
|
232
|
+
ok: false,
|
|
233
|
+
error: `oomi plugin message publish failed: ${reason}`,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
ok: true,
|
|
239
|
+
providerMessageId: toString(response.payload?.message?.messageId),
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
export default function register(api) {
|
|
246
|
+
api.registerChannel({ plugin: oomiChannelPlugin });
|
|
247
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "oomi-ai",
|
|
3
|
+
"name": "Oomi Channel Plugin",
|
|
4
|
+
"description": "Managed Oomi channel integration for OpenClaw.",
|
|
5
|
+
"version": "0.2.1",
|
|
6
|
+
"author": "Oomi",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"openclawVersion": ">=0.5.0",
|
|
9
|
+
"channels": [
|
|
10
|
+
"oomi"
|
|
11
|
+
],
|
|
12
|
+
"configSchema": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"additionalProperties": false,
|
|
15
|
+
"properties": {}
|
|
16
|
+
}
|
|
17
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oomi-ai",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Oomi CLI for OpenClaw setup",
|
|
5
5
|
"bin": {
|
|
6
6
|
"oomi": "bin/oomi-ai.js"
|
|
@@ -9,6 +9,26 @@
|
|
|
9
9
|
"engines": {
|
|
10
10
|
"node": ">=18"
|
|
11
11
|
},
|
|
12
|
+
"openclaw": {
|
|
13
|
+
"extensions": [
|
|
14
|
+
"./openclaw.extension.js"
|
|
15
|
+
],
|
|
16
|
+
"channel": {
|
|
17
|
+
"id": "oomi",
|
|
18
|
+
"label": "Oomi",
|
|
19
|
+
"selectionLabel": "Oomi (Managed)",
|
|
20
|
+
"docsPath": "/channels/oomi",
|
|
21
|
+
"docsLabel": "oomi",
|
|
22
|
+
"blurb": "Managed channel transport for Oomi chat.",
|
|
23
|
+
"aliases": [
|
|
24
|
+
"oomi-ai"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"install": {
|
|
28
|
+
"npmSpec": "oomi-ai",
|
|
29
|
+
"defaultChoice": "npm"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
12
32
|
"keywords": [
|
|
13
33
|
"oomi",
|
|
14
34
|
"openclaw",
|
|
@@ -19,7 +39,7 @@
|
|
|
19
39
|
"homepage": "https://oomi.ai",
|
|
20
40
|
"repository": {
|
|
21
41
|
"type": "git",
|
|
22
|
-
"url": "https://github.com/crispcode-io/oomi.git",
|
|
42
|
+
"url": "git+https://github.com/crispcode-io/oomi.git",
|
|
23
43
|
"directory": "packages/oomi-ai"
|
|
24
44
|
},
|
|
25
45
|
"bugs": {
|
|
@@ -34,6 +54,8 @@
|
|
|
34
54
|
"license": "MIT",
|
|
35
55
|
"files": [
|
|
36
56
|
"bin/oomi-ai.js",
|
|
57
|
+
"openclaw.plugin.json",
|
|
58
|
+
"openclaw.extension.js",
|
|
37
59
|
"agent_instructions.md",
|
|
38
60
|
"README.md",
|
|
39
61
|
"skills/oomi"
|
|
@@ -19,16 +19,24 @@ If the user asks to connect OpenClaw to Oomi voice/web:
|
|
|
19
19
|
- `curl -fsSL https://www.oomi.ai/install.sh | bash`
|
|
20
20
|
2. Ensure `oomi-ai` is installed (`pnpm add -g oomi-ai@latest`).
|
|
21
21
|
- Fallback: `npm install -g oomi-ai@latest`
|
|
22
|
-
3.
|
|
23
|
-
- `oomi openclaw pair --app-url https://www.oomi.ai --
|
|
24
|
-
4.
|
|
25
|
-
- `
|
|
26
|
-
5.
|
|
22
|
+
3. Pair device token without starting legacy bridge:
|
|
23
|
+
- `oomi openclaw pair --app-url https://www.oomi.ai --no-start`
|
|
24
|
+
4. Install OpenClaw plugin:
|
|
25
|
+
- `openclaw plugins install oomi-ai@latest`
|
|
26
|
+
5. Configure channel account (`channels.oomi.accounts.default`) from:
|
|
27
|
+
- `oomi openclaw plugin --show-secrets --backend-url https://api.oomi.ai`
|
|
28
|
+
6. Restart OpenClaw.
|
|
29
|
+
7. If user provides an app URL (for example "Connect yourself to Oomi. Use app URL <URL>."):
|
|
27
30
|
- Use that URL directly in the pair command.
|
|
28
|
-
- Example: `oomi openclaw pair --app-url <URL> --
|
|
29
|
-
|
|
31
|
+
- Example: `oomi openclaw pair --app-url <URL> --no-start`
|
|
32
|
+
8. Return this exact result format to the user:
|
|
30
33
|
- `Oomi Connect Ready`
|
|
31
|
-
- `
|
|
32
|
-
-
|
|
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.
|
|
33
41
|
|
|
34
42
|
Do not ask users to paste gateway IP/token/password when managed connect is available.
|