oomi-ai 0.2.1 → 0.2.2
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 +23 -19
- package/agent_instructions.md +10 -7
- package/bin/oomi-ai.js +99 -3
- package/openclaw.extension.js +247 -0
- package/openclaw.plugin.json +17 -0
- package/package.json +24 -2
- package/skills/oomi/agent_instructions.md +10 -7
package/README.md
CHANGED
|
@@ -14,31 +14,36 @@ 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.
|
|
@@ -56,9 +61,10 @@ Agent-intent mapping (recommended):
|
|
|
56
61
|
- Run:
|
|
57
62
|
- `curl -fsSL https://www.oomi.ai/install.sh | bash`
|
|
58
63
|
- 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
|
|
64
|
+
- `oomi openclaw pair --app-url https://www.oomi.ai --no-start`
|
|
65
|
+
- `openclaw plugins install oomi-ai@latest`
|
|
66
|
+
- `oomi openclaw plugin --show-secrets --backend-url https://api.oomi.ai`
|
|
67
|
+
- Apply `channels.oomi.accounts.default` config and restart OpenClaw.
|
|
62
68
|
|
|
63
69
|
Important distinction:
|
|
64
70
|
- `pairCode` is one-time and used internally by the pair/bootstrap flow.
|
|
@@ -79,9 +85,7 @@ Optional flags:
|
|
|
79
85
|
oomi init --workspace /path/to/openclaw/workspace
|
|
80
86
|
oomi init --agents-file /path/to/AGENTS.md
|
|
81
87
|
oomi openclaw install --skills-dir /path/to/openclaw/skills
|
|
82
|
-
oomi openclaw bridge --broker-ws wss://.../cable --device-token <token>
|
|
83
88
|
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
89
|
oomi openclaw pair --app-url https://your-oomi-app.vercel.app --json
|
|
86
90
|
oomi personas sync --root /path/to/oomi
|
|
87
91
|
oomi personas create creator --status active --chat-session agent:main:webchat:channel:oomi-creator
|
package/agent_instructions.md
CHANGED
|
@@ -19,14 +19,17 @@ 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
34
|
- `Code: ...`
|
|
32
35
|
- `Link: ...`
|
package/bin/oomi-ai.js
CHANGED
|
@@ -142,6 +142,9 @@ Commands:
|
|
|
142
142
|
openclaw pair
|
|
143
143
|
Pair this OpenClaw host with Oomi and start bridge (single command).
|
|
144
144
|
|
|
145
|
+
openclaw plugin
|
|
146
|
+
Print OpenClaw extension install/config guidance for Oomi channel plugin.
|
|
147
|
+
|
|
145
148
|
personas sync
|
|
146
149
|
Sync personas from the repo into the Oomi backend registry.
|
|
147
150
|
|
|
@@ -162,6 +165,7 @@ Common flags:
|
|
|
162
165
|
--no-start Pair and save token, but do not start bridge
|
|
163
166
|
--device-id ID Bridge device identifier (default: host name)
|
|
164
167
|
--device-token TOKEN Existing bridge device token
|
|
168
|
+
--show-secrets Print full token values in diagnostic output
|
|
165
169
|
--json Print pairing result as JSON (for automation)
|
|
166
170
|
--backend-url URL Override Oomi backend URL
|
|
167
171
|
--root PATH Override repo root path for persona discovery
|
|
@@ -614,6 +618,17 @@ function injectGatewayAuth(frameText, gatewayAuth) {
|
|
|
614
618
|
return frameText;
|
|
615
619
|
}
|
|
616
620
|
const params = frame.params && typeof frame.params === 'object' ? frame.params : {};
|
|
621
|
+
const existingScopes = Array.isArray(params.scopes)
|
|
622
|
+
? params.scopes.filter((value) => typeof value === 'string' && value.trim())
|
|
623
|
+
: [];
|
|
624
|
+
const requiredScopes = ['operator.read', 'operator.write'];
|
|
625
|
+
for (const scope of requiredScopes) {
|
|
626
|
+
if (!existingScopes.includes(scope)) {
|
|
627
|
+
existingScopes.push(scope);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
params.scopes = existingScopes;
|
|
631
|
+
|
|
617
632
|
const auth = {};
|
|
618
633
|
if (gatewayAuth.token) auth.token = gatewayAuth.token;
|
|
619
634
|
else if (gatewayAuth.password) auth.password = gatewayAuth.password;
|
|
@@ -818,10 +833,19 @@ async function startOpenclawBridge(flags) {
|
|
|
818
833
|
sendBrokerPayload(brokerSocket, { action: 'gateway_frame', type: 'gateway.frame', sessionId, frame });
|
|
819
834
|
});
|
|
820
835
|
|
|
821
|
-
gatewaySocket.on('close', () => {
|
|
822
|
-
|
|
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
|
+
);
|
|
823
841
|
activeGatewaySockets.delete(sessionId);
|
|
824
|
-
sendBrokerPayload(brokerSocket, {
|
|
842
|
+
sendBrokerPayload(brokerSocket, {
|
|
843
|
+
action: 'gateway_closed',
|
|
844
|
+
type: 'gateway.closed',
|
|
845
|
+
sessionId,
|
|
846
|
+
code,
|
|
847
|
+
reason: reasonText,
|
|
848
|
+
});
|
|
825
849
|
});
|
|
826
850
|
|
|
827
851
|
gatewaySocket.on('error', (err) => {
|
|
@@ -1009,6 +1033,73 @@ async function pairAndStartOpenclawBridge(flags) {
|
|
|
1009
1033
|
});
|
|
1010
1034
|
}
|
|
1011
1035
|
|
|
1036
|
+
function printOpenclawPluginSetup(flags) {
|
|
1037
|
+
const bridgeState = readBridgeState();
|
|
1038
|
+
const backendUrl = String(
|
|
1039
|
+
flags['backend-url'] ||
|
|
1040
|
+
process.env.OOMI_BACKEND_URL ||
|
|
1041
|
+
process.env.OOMI_CHAT_BROKER_HTTP_URL ||
|
|
1042
|
+
bridgeState.brokerHttp ||
|
|
1043
|
+
''
|
|
1044
|
+
).trim();
|
|
1045
|
+
const deviceToken = String(
|
|
1046
|
+
flags['device-token'] ||
|
|
1047
|
+
bridgeState.deviceToken ||
|
|
1048
|
+
''
|
|
1049
|
+
).trim();
|
|
1050
|
+
const showSecrets = isTruthyFlag(flags['show-secrets']);
|
|
1051
|
+
const redactToken = (value) => {
|
|
1052
|
+
if (!value) return '';
|
|
1053
|
+
if (showSecrets) return value;
|
|
1054
|
+
if (value.length <= 12) return '***';
|
|
1055
|
+
return `${value.slice(0, 6)}...${value.slice(-6)}`;
|
|
1056
|
+
};
|
|
1057
|
+
const defaultSessionKey = String(
|
|
1058
|
+
flags['session-key'] ||
|
|
1059
|
+
process.env.OOMI_SESSION_KEY ||
|
|
1060
|
+
'agent:main:webchat:channel:oomi'
|
|
1061
|
+
).trim();
|
|
1062
|
+
|
|
1063
|
+
console.log('OpenClaw Oomi Plugin Setup');
|
|
1064
|
+
console.log('--------------------------');
|
|
1065
|
+
console.log('1) Install extension package in OpenClaw:');
|
|
1066
|
+
console.log(' openclaw plugins install oomi-ai@latest');
|
|
1067
|
+
console.log('');
|
|
1068
|
+
console.log('2) Configure OpenClaw channel account (channels.oomi.accounts.default):');
|
|
1069
|
+
console.log(
|
|
1070
|
+
JSON.stringify(
|
|
1071
|
+
{
|
|
1072
|
+
channels: {
|
|
1073
|
+
oomi: {
|
|
1074
|
+
defaultAccountId: 'default',
|
|
1075
|
+
accounts: {
|
|
1076
|
+
default: {
|
|
1077
|
+
enabled: true,
|
|
1078
|
+
backendUrl,
|
|
1079
|
+
deviceToken: redactToken(deviceToken),
|
|
1080
|
+
defaultSessionKey,
|
|
1081
|
+
},
|
|
1082
|
+
},
|
|
1083
|
+
},
|
|
1084
|
+
},
|
|
1085
|
+
},
|
|
1086
|
+
null,
|
|
1087
|
+
2
|
|
1088
|
+
)
|
|
1089
|
+
);
|
|
1090
|
+
if (deviceToken && !showSecrets) {
|
|
1091
|
+
console.log('Token is redacted by default. Use --show-secrets to print full values.');
|
|
1092
|
+
console.log(`Bridge state file: ${resolveBridgeStatePath()}`);
|
|
1093
|
+
}
|
|
1094
|
+
console.log('');
|
|
1095
|
+
|
|
1096
|
+
if (!backendUrl || !deviceToken) {
|
|
1097
|
+
console.log('Missing backend/device credentials in local state.');
|
|
1098
|
+
console.log('Run: oomi openclaw pair --app-url https://www.oomi.ai --no-start');
|
|
1099
|
+
console.log('Then run: oomi openclaw plugin');
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1012
1103
|
async function main() {
|
|
1013
1104
|
const args = parseArgs(process.argv);
|
|
1014
1105
|
const command = args.command;
|
|
@@ -1050,6 +1141,11 @@ async function main() {
|
|
|
1050
1141
|
return;
|
|
1051
1142
|
}
|
|
1052
1143
|
|
|
1144
|
+
if (command === 'openclaw' && subcommand === 'plugin') {
|
|
1145
|
+
printOpenclawPluginSetup(args.flags);
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1053
1149
|
if (command === 'personas' && subcommand === 'sync') {
|
|
1054
1150
|
await syncPersonas({ backendUrl: args.flags['backend-url'], root: args.flags.root });
|
|
1055
1151
|
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.2",
|
|
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,14 +19,17 @@ 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
34
|
- `Code: ...`
|
|
32
35
|
- `Link: ...`
|