oomi-ai 0.2.0 → 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 +46 -13
- package/agent_instructions.md +15 -4
- package/bin/oomi-ai.js +366 -27
- package/openclaw.extension.js +247 -0
- package/openclaw.plugin.json +17 -0
- package/package.json +29 -7
- package/skills/oomi/agent_instructions.md +15 -4
package/README.md
CHANGED
|
@@ -14,7 +14,24 @@ npm install -g oomi-ai
|
|
|
14
14
|
|
|
15
15
|
## Usage
|
|
16
16
|
|
|
17
|
-
Install
|
|
17
|
+
Install as an OpenClaw channel extension (preferred architecture):
|
|
18
|
+
```
|
|
19
|
+
openclaw plugins install oomi-ai@latest
|
|
20
|
+
```
|
|
21
|
+
|
|
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:
|
|
30
|
+
```
|
|
31
|
+
oomi openclaw plugin
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Install agent instructions only:
|
|
18
35
|
```
|
|
19
36
|
oomi init
|
|
20
37
|
```
|
|
@@ -24,18 +41,12 @@ Install agent instructions + Oomi skill:
|
|
|
24
41
|
oomi openclaw install
|
|
25
42
|
```
|
|
26
43
|
|
|
27
|
-
|
|
44
|
+
Pair and provision device token from Oomi web backend:
|
|
28
45
|
```
|
|
29
|
-
oomi openclaw
|
|
30
|
-
--broker-http https://your-broker.modal.run \
|
|
31
|
-
--broker-ws wss://your-broker.modal.run/ws/device \
|
|
32
|
-
--pair-code ABCD2345
|
|
46
|
+
oomi openclaw pair --app-url https://your-oomi-app.vercel.app --device-id my-openclaw-mac --no-start
|
|
33
47
|
```
|
|
34
48
|
|
|
35
|
-
|
|
36
|
-
```
|
|
37
|
-
oomi openclaw pair --app-url https://your-oomi-app.vercel.app --device-id my-openclaw-mac --detach
|
|
38
|
-
```
|
|
49
|
+
`--app-url` must be reachable from the OpenClaw host. If OpenClaw runs on a different machine/network, do not use `localhost` unless tunneled.
|
|
39
50
|
|
|
40
51
|
This prints:
|
|
41
52
|
- `Connect code: <device-id>`
|
|
@@ -45,6 +56,20 @@ This prints:
|
|
|
45
56
|
- `Code: ...`
|
|
46
57
|
- `Link: ...`
|
|
47
58
|
|
|
59
|
+
Agent-intent mapping (recommended):
|
|
60
|
+
- If user says `Connect yourself to Oomi. Use app URL https://www.oomi.ai.`
|
|
61
|
+
- Run:
|
|
62
|
+
- `curl -fsSL https://www.oomi.ai/install.sh | bash`
|
|
63
|
+
- or `pnpm add -g oomi-ai@latest` (`npm install -g oomi-ai@latest` fallback)
|
|
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.
|
|
68
|
+
|
|
69
|
+
Important distinction:
|
|
70
|
+
- `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>`).
|
|
72
|
+
|
|
48
73
|
Sync personas from the repo into the backend registry:
|
|
49
74
|
```
|
|
50
75
|
oomi personas sync --backend-url http://localhost:3001
|
|
@@ -60,9 +85,7 @@ Optional flags:
|
|
|
60
85
|
oomi init --workspace /path/to/openclaw/workspace
|
|
61
86
|
oomi init --agents-file /path/to/AGENTS.md
|
|
62
87
|
oomi openclaw install --skills-dir /path/to/openclaw/skills
|
|
63
|
-
oomi openclaw bridge --broker-ws wss://.../ws/device --device-token <token>
|
|
64
88
|
oomi openclaw pair --app-url https://your-oomi-app.vercel.app --no-start
|
|
65
|
-
oomi openclaw pair --app-url https://your-oomi-app.vercel.app --detach
|
|
66
89
|
oomi openclaw pair --app-url https://your-oomi-app.vercel.app --json
|
|
67
90
|
oomi personas sync --root /path/to/oomi
|
|
68
91
|
oomi personas create creator --status active --chat-session agent:main:webchat:channel:oomi-creator
|
|
@@ -74,9 +97,19 @@ Defaults:
|
|
|
74
97
|
|
|
75
98
|
Restart OpenClaw after running `oomi init` or `oomi openclaw install`.
|
|
76
99
|
|
|
100
|
+
## Update Notifications
|
|
101
|
+
- `oomi` checks npm for a newer `oomi-ai` version on normal commands (cached, best-effort).
|
|
102
|
+
- When an update is available it prints:
|
|
103
|
+
- `pnpm add -g oomi-ai@latest`
|
|
104
|
+
- fallback: `npm install -g oomi-ai@latest`
|
|
105
|
+
- Optional env controls:
|
|
106
|
+
- `OOMI_SKIP_UPDATE_CHECK=1` disables checks
|
|
107
|
+
- `OOMI_UPDATE_CHECK_INTERVAL_MS=<ms>` changes check interval
|
|
108
|
+
- `OOMI_UPDATE_CHECK_TIMEOUT_MS=<ms>` changes network timeout
|
|
109
|
+
|
|
77
110
|
## Package Audit + Publish (pnpm)
|
|
78
111
|
```
|
|
79
|
-
cd
|
|
112
|
+
cd <repo-root>/packages/oomi-ai
|
|
80
113
|
pnpm check
|
|
81
114
|
pnpm publish --dry-run --no-git-checks --access public
|
|
82
115
|
pnpm publish --access public
|
package/agent_instructions.md
CHANGED
|
@@ -15,10 +15,21 @@ Aliases allowed (if needed): wave -> Waving, walk -> Walking, idle -> Idle, sit/
|
|
|
15
15
|
|
|
16
16
|
If the user asks to connect OpenClaw to Oomi voice/web:
|
|
17
17
|
|
|
18
|
-
1.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
1. Refresh bootstrap/update first:
|
|
19
|
+
- `curl -fsSL https://www.oomi.ai/install.sh | bash`
|
|
20
|
+
2. Ensure `oomi-ai` is installed (`pnpm add -g oomi-ai@latest`).
|
|
21
|
+
- Fallback: `npm install -g oomi-ai@latest`
|
|
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>."):
|
|
30
|
+
- Use that URL directly in the pair command.
|
|
31
|
+
- Example: `oomi openclaw pair --app-url <URL> --no-start`
|
|
32
|
+
8. Return this exact result format to the user:
|
|
22
33
|
- `Oomi Connect Ready`
|
|
23
34
|
- `Code: ...`
|
|
24
35
|
- `Link: ...`
|
package/bin/oomi-ai.js
CHANGED
|
@@ -10,6 +10,121 @@ const MARKER_START = '<oomi-agent-instructions>';
|
|
|
10
10
|
const MARKER_END = '</oomi-agent-instructions>';
|
|
11
11
|
|
|
12
12
|
const PACKAGE_ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
|
|
13
|
+
const UPDATE_STATE_FILE = path.join(os.homedir(), '.openclaw', 'oomi-ai-update-check.json');
|
|
14
|
+
const DEFAULT_UPDATE_CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000;
|
|
15
|
+
const DEFAULT_UPDATE_CHECK_TIMEOUT_MS = 1200;
|
|
16
|
+
|
|
17
|
+
function parsePositiveInteger(value, fallback) {
|
|
18
|
+
const num = Number(value);
|
|
19
|
+
if (!Number.isFinite(num) || num <= 0) return fallback;
|
|
20
|
+
return Math.floor(num);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readJsonSafe(filePath) {
|
|
24
|
+
if (!fs.existsSync(filePath)) return null;
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(readFile(filePath));
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function writeJsonSafe(filePath, value) {
|
|
33
|
+
try {
|
|
34
|
+
ensureDir(path.dirname(filePath));
|
|
35
|
+
writeFile(filePath, JSON.stringify(value, null, 2) + '\n');
|
|
36
|
+
} catch {
|
|
37
|
+
// best-effort cache write
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function currentPackageVersion() {
|
|
42
|
+
const packageJsonPath = path.join(PACKAGE_ROOT, 'package.json');
|
|
43
|
+
const packageJson = readJsonSafe(packageJsonPath);
|
|
44
|
+
const version = typeof packageJson?.version === 'string' ? packageJson.version.trim() : '';
|
|
45
|
+
return version;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseVersionTuple(version) {
|
|
49
|
+
if (typeof version !== 'string') return null;
|
|
50
|
+
const cleaned = version.trim().replace(/^v/i, '').split('-')[0];
|
|
51
|
+
const parts = cleaned.split('.');
|
|
52
|
+
if (parts.length < 3) return null;
|
|
53
|
+
const major = Number(parts[0]);
|
|
54
|
+
const minor = Number(parts[1]);
|
|
55
|
+
const patch = Number(parts[2]);
|
|
56
|
+
if (![major, minor, patch].every((n) => Number.isInteger(n) && n >= 0)) return null;
|
|
57
|
+
return [major, minor, patch];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function compareVersions(a, b) {
|
|
61
|
+
const av = parseVersionTuple(a);
|
|
62
|
+
const bv = parseVersionTuple(b);
|
|
63
|
+
if (!av || !bv) return 0;
|
|
64
|
+
for (let i = 0; i < 3; i += 1) {
|
|
65
|
+
if (av[i] < bv[i]) return -1;
|
|
66
|
+
if (av[i] > bv[i]) return 1;
|
|
67
|
+
}
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function fetchLatestPublishedVersion(pkgName) {
|
|
72
|
+
const timeoutMs = parsePositiveInteger(
|
|
73
|
+
process.env.OOMI_UPDATE_CHECK_TIMEOUT_MS,
|
|
74
|
+
DEFAULT_UPDATE_CHECK_TIMEOUT_MS
|
|
75
|
+
);
|
|
76
|
+
const controller = new AbortController();
|
|
77
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
78
|
+
try {
|
|
79
|
+
const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(pkgName)}/latest`, {
|
|
80
|
+
method: 'GET',
|
|
81
|
+
headers: {
|
|
82
|
+
Accept: 'application/json',
|
|
83
|
+
},
|
|
84
|
+
signal: controller.signal,
|
|
85
|
+
});
|
|
86
|
+
if (!response.ok) return '';
|
|
87
|
+
const payload = await response.json().catch(() => ({}));
|
|
88
|
+
const version = typeof payload?.version === 'string' ? payload.version.trim() : '';
|
|
89
|
+
return version;
|
|
90
|
+
} catch {
|
|
91
|
+
return '';
|
|
92
|
+
} finally {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function maybeNotifyUpdate(command) {
|
|
98
|
+
if (isTruthyFlag(process.env.OOMI_SKIP_UPDATE_CHECK)) return;
|
|
99
|
+
if (!command || command === 'help' || command === '--help') return;
|
|
100
|
+
|
|
101
|
+
const currentVersion = currentPackageVersion();
|
|
102
|
+
if (!currentVersion) return;
|
|
103
|
+
|
|
104
|
+
const intervalMs = parsePositiveInteger(
|
|
105
|
+
process.env.OOMI_UPDATE_CHECK_INTERVAL_MS,
|
|
106
|
+
DEFAULT_UPDATE_CHECK_INTERVAL_MS
|
|
107
|
+
);
|
|
108
|
+
const now = Date.now();
|
|
109
|
+
const state = readJsonSafe(UPDATE_STATE_FILE) || {};
|
|
110
|
+
const lastCheckedAt = Number(state.lastCheckedAt || 0);
|
|
111
|
+
if (Number.isFinite(lastCheckedAt) && lastCheckedAt > 0 && now - lastCheckedAt < intervalMs) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const latestVersion = await fetchLatestPublishedVersion('oomi-ai');
|
|
116
|
+
writeJsonSafe(UPDATE_STATE_FILE, {
|
|
117
|
+
lastCheckedAt: now,
|
|
118
|
+
latestVersion: latestVersion || String(state.latestVersion || ''),
|
|
119
|
+
});
|
|
120
|
+
if (!latestVersion) return;
|
|
121
|
+
|
|
122
|
+
if (compareVersions(currentVersion, latestVersion) < 0) {
|
|
123
|
+
console.warn(`[oomi] Update available: oomi-ai ${currentVersion} -> ${latestVersion}`);
|
|
124
|
+
console.warn('[oomi] Update command: pnpm add -g oomi-ai@latest');
|
|
125
|
+
console.warn('[oomi] Fallback update command: npm install -g oomi-ai@latest');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
13
128
|
|
|
14
129
|
function usage() {
|
|
15
130
|
console.log(`oomi <command>
|
|
@@ -27,6 +142,9 @@ Commands:
|
|
|
27
142
|
openclaw pair
|
|
28
143
|
Pair this OpenClaw host with Oomi and start bridge (single command).
|
|
29
144
|
|
|
145
|
+
openclaw plugin
|
|
146
|
+
Print OpenClaw extension install/config guidance for Oomi channel plugin.
|
|
147
|
+
|
|
30
148
|
personas sync
|
|
31
149
|
Sync personas from the repo into the Oomi backend registry.
|
|
32
150
|
|
|
@@ -38,7 +156,7 @@ Common flags:
|
|
|
38
156
|
--workspace PATH Override OpenClaw workspace root
|
|
39
157
|
--skills-dir PATH Override skills install dir
|
|
40
158
|
--broker-http URL Managed broker HTTPS URL (for pair claim)
|
|
41
|
-
--broker-ws URL Managed broker device WS URL (wss://.../
|
|
159
|
+
--broker-ws URL Managed broker device WS URL (wss://.../cable)
|
|
42
160
|
--pair-code CODE One-time pairing code from Oomi
|
|
43
161
|
--app-url URL Oomi app URL used for pairing APIs (default: http://127.0.0.1:3000)
|
|
44
162
|
--label TEXT Pairing label shown in broker logs
|
|
@@ -47,6 +165,7 @@ Common flags:
|
|
|
47
165
|
--no-start Pair and save token, but do not start bridge
|
|
48
166
|
--device-id ID Bridge device identifier (default: host name)
|
|
49
167
|
--device-token TOKEN Existing bridge device token
|
|
168
|
+
--show-secrets Print full token values in diagnostic output
|
|
50
169
|
--json Print pairing result as JSON (for automation)
|
|
51
170
|
--backend-url URL Override Oomi backend URL
|
|
52
171
|
--root PATH Override repo root path for persona discovery
|
|
@@ -499,6 +618,17 @@ function injectGatewayAuth(frameText, gatewayAuth) {
|
|
|
499
618
|
return frameText;
|
|
500
619
|
}
|
|
501
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
|
+
|
|
502
632
|
const auth = {};
|
|
503
633
|
if (gatewayAuth.token) auth.token = gatewayAuth.token;
|
|
504
634
|
else if (gatewayAuth.password) auth.password = gatewayAuth.password;
|
|
@@ -523,19 +653,39 @@ function parseJsonPayload(raw) {
|
|
|
523
653
|
|
|
524
654
|
async function startOpenclawBridge(flags) {
|
|
525
655
|
const bridgeState = readBridgeState();
|
|
526
|
-
const brokerHttp = String(
|
|
527
|
-
|
|
656
|
+
const brokerHttp = String(
|
|
657
|
+
flags['broker-http'] ||
|
|
658
|
+
process.env.OOMI_CHAT_BROKER_HTTP_URL ||
|
|
659
|
+
bridgeState.brokerHttp ||
|
|
660
|
+
''
|
|
661
|
+
).trim();
|
|
662
|
+
const brokerWs = String(
|
|
663
|
+
flags['broker-ws'] ||
|
|
664
|
+
process.env.OOMI_CHAT_BROKER_DEVICE_WS_URL ||
|
|
665
|
+
bridgeState.brokerWs ||
|
|
666
|
+
''
|
|
667
|
+
).trim();
|
|
528
668
|
const deviceId = resolveDeviceId(flags, bridgeState);
|
|
529
669
|
const pairCode = String(flags['pair-code'] || '').trim().toUpperCase();
|
|
530
|
-
|
|
670
|
+
const explicitDeviceToken = String(flags['device-token'] || '').trim();
|
|
671
|
+
const canReuseStateToken =
|
|
672
|
+
String(bridgeState.deviceId || '').trim() === deviceId &&
|
|
673
|
+
String(bridgeState.brokerWs || '').trim() === brokerWs &&
|
|
674
|
+
String(bridgeState.brokerHttp || '').trim() === brokerHttp;
|
|
675
|
+
let deviceToken = explicitDeviceToken;
|
|
676
|
+
if (!deviceToken && canReuseStateToken) {
|
|
677
|
+
deviceToken = String(bridgeState.deviceToken || '').trim();
|
|
678
|
+
}
|
|
531
679
|
|
|
532
680
|
if (!brokerWs) {
|
|
533
|
-
throw new Error('Missing broker device websocket URL. Set --broker-ws or
|
|
681
|
+
throw new Error('Missing broker device websocket URL. Set --broker-ws or OOMI_CHAT_BROKER_DEVICE_WS_URL.');
|
|
534
682
|
}
|
|
535
683
|
|
|
536
684
|
if (!deviceToken) {
|
|
537
685
|
if (!brokerHttp || !pairCode) {
|
|
538
|
-
throw new Error(
|
|
686
|
+
throw new Error(
|
|
687
|
+
'No valid saved device token for this device/broker. Provide --pair-code and --broker-http to claim one.'
|
|
688
|
+
);
|
|
539
689
|
}
|
|
540
690
|
const claimed = await claimBridgeDeviceToken({ brokerHttp, pairCode, deviceId });
|
|
541
691
|
deviceToken = String(claimed.deviceToken || '').trim();
|
|
@@ -563,22 +713,95 @@ async function startOpenclawBridge(flags) {
|
|
|
563
713
|
console.log(`Broker WS: ${brokerWs}`);
|
|
564
714
|
|
|
565
715
|
const activeGatewaySockets = new Map();
|
|
716
|
+
const brokerPath = (() => {
|
|
717
|
+
try {
|
|
718
|
+
return new URL(brokerWs).pathname || '';
|
|
719
|
+
} catch {
|
|
720
|
+
return '';
|
|
721
|
+
}
|
|
722
|
+
})();
|
|
723
|
+
const actionCableMode = brokerPath.endsWith('/cable');
|
|
724
|
+
const deviceChannelIdentifier = JSON.stringify({ channel: 'DeviceChannel' });
|
|
725
|
+
|
|
726
|
+
const sendBrokerPayload = (brokerSocket, payload) => {
|
|
727
|
+
if (brokerSocket.readyState !== WebSocket.OPEN) return;
|
|
728
|
+
if (!actionCableMode) {
|
|
729
|
+
brokerSocket.send(JSON.stringify(payload));
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
brokerSocket.send(
|
|
733
|
+
JSON.stringify({
|
|
734
|
+
command: 'message',
|
|
735
|
+
identifier: deviceChannelIdentifier,
|
|
736
|
+
data: JSON.stringify(payload),
|
|
737
|
+
})
|
|
738
|
+
);
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
const parseBrokerEnvelope = (raw) => {
|
|
742
|
+
const payload = parseJsonPayload(raw);
|
|
743
|
+
if (!payload) return null;
|
|
744
|
+
if (!actionCableMode) return payload;
|
|
745
|
+
|
|
746
|
+
if (payload.type === 'welcome' || payload.type === 'ping') return null;
|
|
747
|
+
if (payload.type === 'confirm_subscription') return { type: 'device.subscribed' };
|
|
748
|
+
if (payload.type === 'disconnect') {
|
|
749
|
+
return {
|
|
750
|
+
type: 'broker.disconnect',
|
|
751
|
+
reason: String(payload.reason || ''),
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
if (payload.type === 'reject_subscription') {
|
|
755
|
+
return {
|
|
756
|
+
type: 'broker.reject_subscription',
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
if (payload.message && typeof payload.message === 'object') {
|
|
760
|
+
return payload.message;
|
|
761
|
+
}
|
|
762
|
+
return null;
|
|
763
|
+
};
|
|
566
764
|
|
|
567
765
|
const connectBroker = () => {
|
|
568
766
|
const wsUrl = new URL(brokerWs);
|
|
569
767
|
wsUrl.searchParams.set('token', deviceToken);
|
|
570
768
|
|
|
571
769
|
const brokerSocket = new WebSocket(wsUrl.toString());
|
|
770
|
+
let actionCableHeartbeat = null;
|
|
572
771
|
|
|
573
772
|
brokerSocket.on('open', () => {
|
|
574
773
|
console.log('[bridge] Connected to managed broker.');
|
|
774
|
+
if (!actionCableMode) return;
|
|
775
|
+
brokerSocket.send(
|
|
776
|
+
JSON.stringify({
|
|
777
|
+
command: 'subscribe',
|
|
778
|
+
identifier: deviceChannelIdentifier,
|
|
779
|
+
})
|
|
780
|
+
);
|
|
781
|
+
actionCableHeartbeat = setInterval(() => {
|
|
782
|
+
sendBrokerPayload(brokerSocket, { action: 'heartbeat' });
|
|
783
|
+
}, 15000);
|
|
575
784
|
});
|
|
576
785
|
|
|
577
786
|
brokerSocket.on('message', (rawData) => {
|
|
578
787
|
const text = typeof rawData === 'string' ? rawData : rawData.toString();
|
|
579
|
-
const payload =
|
|
788
|
+
const payload = parseBrokerEnvelope(text);
|
|
580
789
|
if (!payload || typeof payload.type !== 'string') return;
|
|
581
790
|
|
|
791
|
+
if (payload.type === 'device.subscribed') {
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
if (payload.type === 'broker.disconnect') {
|
|
796
|
+
console.error(`[bridge] Broker rejected connection: ${String(payload.reason || 'unauthorized')}`);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (payload.type === 'broker.reject_subscription') {
|
|
801
|
+
console.error('[bridge] Broker rejected DeviceChannel subscription.');
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
|
|
582
805
|
if (payload.type === 'device.ready') {
|
|
583
806
|
console.log(`[bridge] Broker ready for device ${payload.deviceId || deviceId}.`);
|
|
584
807
|
return;
|
|
@@ -587,27 +810,53 @@ async function startOpenclawBridge(flags) {
|
|
|
587
810
|
if (payload.type === 'client.open') {
|
|
588
811
|
const sessionId = String(payload.sessionId || '').trim();
|
|
589
812
|
if (!sessionId || activeGatewaySockets.has(sessionId)) return;
|
|
813
|
+
console.log(`[bridge] client.open ${sessionId}`);
|
|
590
814
|
const gatewaySocket = new WebSocket(gateway.gatewayUrl);
|
|
591
|
-
|
|
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
|
+
});
|
|
592
830
|
|
|
593
831
|
gatewaySocket.on('message', (gatewayRaw) => {
|
|
594
832
|
const frame = typeof gatewayRaw === 'string' ? gatewayRaw : gatewayRaw.toString();
|
|
595
|
-
|
|
596
|
-
brokerSocket.send(JSON.stringify({ type: 'gateway.frame', sessionId, frame }));
|
|
597
|
-
}
|
|
833
|
+
sendBrokerPayload(brokerSocket, { action: 'gateway_frame', type: 'gateway.frame', sessionId, frame });
|
|
598
834
|
});
|
|
599
835
|
|
|
600
|
-
gatewaySocket.on('close', () => {
|
|
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
|
+
);
|
|
601
841
|
activeGatewaySockets.delete(sessionId);
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
842
|
+
sendBrokerPayload(brokerSocket, {
|
|
843
|
+
action: 'gateway_closed',
|
|
844
|
+
type: 'gateway.closed',
|
|
845
|
+
sessionId,
|
|
846
|
+
code,
|
|
847
|
+
reason: reasonText,
|
|
848
|
+
});
|
|
605
849
|
});
|
|
606
850
|
|
|
607
851
|
gatewaySocket.on('error', (err) => {
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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
|
+
});
|
|
611
860
|
});
|
|
612
861
|
return;
|
|
613
862
|
}
|
|
@@ -616,30 +865,47 @@ async function startOpenclawBridge(flags) {
|
|
|
616
865
|
const sessionId = String(payload.sessionId || '').trim();
|
|
617
866
|
const frame = typeof payload.frame === 'string' ? payload.frame : '';
|
|
618
867
|
if (!sessionId || !frame) return;
|
|
619
|
-
|
|
620
|
-
|
|
868
|
+
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;
|
|
621
875
|
const frameWithAuth = injectGatewayAuth(frame, gateway);
|
|
622
|
-
gatewaySocket.
|
|
876
|
+
if (gatewaySocket.readyState === WebSocket.OPEN) {
|
|
877
|
+
gatewaySocket.send(frameWithAuth);
|
|
878
|
+
} else if (gatewaySocket.readyState === WebSocket.CONNECTING) {
|
|
879
|
+
console.log(`[bridge] client.frame queued ${sessionId}`);
|
|
880
|
+
sessionBridge.queue.push(frameWithAuth);
|
|
881
|
+
} else {
|
|
882
|
+
console.log(`[bridge] client.frame dropped (socket not open) ${sessionId}`);
|
|
883
|
+
}
|
|
623
884
|
return;
|
|
624
885
|
}
|
|
625
886
|
|
|
626
887
|
if (payload.type === 'client.close') {
|
|
627
888
|
const sessionId = String(payload.sessionId || '').trim();
|
|
628
|
-
|
|
629
|
-
|
|
889
|
+
console.log(`[bridge] client.close ${sessionId}`);
|
|
890
|
+
const sessionBridge = activeGatewaySockets.get(sessionId);
|
|
891
|
+
if (sessionBridge && sessionBridge.socket) {
|
|
630
892
|
activeGatewaySockets.delete(sessionId);
|
|
631
|
-
|
|
893
|
+
sessionBridge.socket.close(1000, 'client_closed');
|
|
632
894
|
}
|
|
633
895
|
return;
|
|
634
896
|
}
|
|
635
897
|
});
|
|
636
898
|
|
|
637
899
|
brokerSocket.on('close', (code, reason) => {
|
|
900
|
+
if (actionCableHeartbeat) {
|
|
901
|
+
clearInterval(actionCableHeartbeat);
|
|
902
|
+
actionCableHeartbeat = null;
|
|
903
|
+
}
|
|
638
904
|
console.log(`[bridge] Broker disconnected (${code}) ${reason.toString()}`);
|
|
639
|
-
for (const [sessionId,
|
|
905
|
+
for (const [sessionId, sessionBridge] of activeGatewaySockets.entries()) {
|
|
640
906
|
activeGatewaySockets.delete(sessionId);
|
|
641
907
|
try {
|
|
642
|
-
|
|
908
|
+
sessionBridge.socket.close(1001, 'broker_disconnected');
|
|
643
909
|
} catch {
|
|
644
910
|
// no-op
|
|
645
911
|
}
|
|
@@ -664,7 +930,6 @@ async function pairAndStartOpenclawBridge(flags) {
|
|
|
664
930
|
const sessionKey = String(
|
|
665
931
|
flags['session-key'] ||
|
|
666
932
|
process.env.OOMI_SESSION_KEY ||
|
|
667
|
-
process.env.NEXT_PUBLIC_SESSION_KEY ||
|
|
668
933
|
'agent:main:webchat:channel:oomi'
|
|
669
934
|
).trim();
|
|
670
935
|
const detach = Boolean(flags.detach);
|
|
@@ -768,6 +1033,73 @@ async function pairAndStartOpenclawBridge(flags) {
|
|
|
768
1033
|
});
|
|
769
1034
|
}
|
|
770
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
|
+
|
|
771
1103
|
async function main() {
|
|
772
1104
|
const args = parseArgs(process.argv);
|
|
773
1105
|
const command = args.command;
|
|
@@ -778,6 +1110,8 @@ async function main() {
|
|
|
778
1110
|
process.exit(0);
|
|
779
1111
|
}
|
|
780
1112
|
|
|
1113
|
+
await maybeNotifyUpdate(command);
|
|
1114
|
+
|
|
781
1115
|
if (command === 'init') {
|
|
782
1116
|
const agentsPath = resolveAgentsFile(args.flags['agents-file'], args.flags.workspace);
|
|
783
1117
|
installInstructions(agentsPath);
|
|
@@ -807,6 +1141,11 @@ async function main() {
|
|
|
807
1141
|
return;
|
|
808
1142
|
}
|
|
809
1143
|
|
|
1144
|
+
if (command === 'openclaw' && subcommand === 'plugin') {
|
|
1145
|
+
printOpenclawPluginSetup(args.flags);
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
810
1149
|
if (command === 'personas' && subcommand === 'sync') {
|
|
811
1150
|
await syncPersonas({ backendUrl: args.flags['backend-url'], root: args.flags.root });
|
|
812
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,23 +39,25 @@
|
|
|
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": {
|
|
26
46
|
"url": "https://github.com/crispcode-io/oomi/issues"
|
|
27
47
|
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"check": "node --check bin/oomi-ai.js"
|
|
50
|
+
},
|
|
28
51
|
"dependencies": {
|
|
29
52
|
"ws": "^8.19.0"
|
|
30
53
|
},
|
|
31
54
|
"license": "MIT",
|
|
32
55
|
"files": [
|
|
33
56
|
"bin/oomi-ai.js",
|
|
57
|
+
"openclaw.plugin.json",
|
|
58
|
+
"openclaw.extension.js",
|
|
34
59
|
"agent_instructions.md",
|
|
35
60
|
"README.md",
|
|
36
61
|
"skills/oomi"
|
|
37
|
-
]
|
|
38
|
-
|
|
39
|
-
"check": "node --check bin/oomi-ai.js"
|
|
40
|
-
}
|
|
41
|
-
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
@@ -15,10 +15,21 @@ Aliases allowed (if needed): wave -> Waving, walk -> Walking, idle -> Idle, sit/
|
|
|
15
15
|
|
|
16
16
|
If the user asks to connect OpenClaw to Oomi voice/web:
|
|
17
17
|
|
|
18
|
-
1.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
1. Refresh bootstrap/update first:
|
|
19
|
+
- `curl -fsSL https://www.oomi.ai/install.sh | bash`
|
|
20
|
+
2. Ensure `oomi-ai` is installed (`pnpm add -g oomi-ai@latest`).
|
|
21
|
+
- Fallback: `npm install -g oomi-ai@latest`
|
|
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>."):
|
|
30
|
+
- Use that URL directly in the pair command.
|
|
31
|
+
- Example: `oomi openclaw pair --app-url <URL> --no-start`
|
|
32
|
+
8. Return this exact result format to the user:
|
|
22
33
|
- `Oomi Connect Ready`
|
|
23
34
|
- `Code: ...`
|
|
24
35
|
- `Link: ...`
|