oomi-ai 0.3.5 → 0.3.7
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 +30 -6
- package/agent_instructions.md +28 -25
- package/bin/oomi-ai.js +129 -91
- package/lib/modelRelayClient.js +150 -0
- package/lib/openclawProfile.js +155 -1
- package/lib/personaApiClient.js +28 -0
- package/package.json +2 -1
- package/persona-app/README.md +2 -3
- package/persona-app/registry/v1.json +93 -40
- package/skills/oomi/SKILL.md +39 -32
- package/skills/oomi/agent_instructions.md +11 -21
package/README.md
CHANGED
|
@@ -145,12 +145,40 @@ Optional fields:
|
|
|
145
145
|
- `defaultSessionKey`
|
|
146
146
|
- `requestTimeoutMs`
|
|
147
147
|
|
|
148
|
+
Hosted Oomi-managed agents also include a `modelRelay` block under the default account. When enabled, `oomi-ai` applies an OpenClaw custom provider named `oomi-managed` that points at Oomi's OpenAI-compatible metered relay:
|
|
149
|
+
|
|
150
|
+
```json
|
|
151
|
+
{
|
|
152
|
+
"channels": {
|
|
153
|
+
"oomi": {
|
|
154
|
+
"accounts": {
|
|
155
|
+
"default": {
|
|
156
|
+
"modelRelay": {
|
|
157
|
+
"enabled": true,
|
|
158
|
+
"backendUrl": "https://api.oomi.ai",
|
|
159
|
+
"openaiBasePath": "/v1/model/relay/openai",
|
|
160
|
+
"openclawProviderId": "oomi-managed",
|
|
161
|
+
"openclawModelRef": "oomi-managed/google/gemini-2.5-flash",
|
|
162
|
+
"provider": "openrouter",
|
|
163
|
+
"model": "google/gemini-2.5-flash",
|
|
164
|
+
"userId": "usr_...",
|
|
165
|
+
"deviceId": "oomi-hosted-...",
|
|
166
|
+
"internalKeyEnv": "OOMI_HOSTED_BOOTSTRAP_INTERNAL_KEY"
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The config stores only the internal-key environment variable name. The secret value stays in the runtime environment.
|
|
176
|
+
|
|
148
177
|
## Persona App API Tools
|
|
149
178
|
|
|
150
179
|
Component-composed persona apps are the default Oomi mini-app path. Use these commands when the user asks about or updates an existing Oomi persona app such as Fitness Today:
|
|
151
180
|
|
|
152
181
|
```bash
|
|
153
|
-
oomi fitness show --json
|
|
154
182
|
oomi persona-apps list --json
|
|
155
183
|
oomi persona-apps show fitness-today --json
|
|
156
184
|
oomi persona-apps apply-action fitness-today --action fitness.complete_workout --payload-json '{"goalId":"easy-run","minutes":20}' --json
|
|
@@ -159,21 +187,17 @@ oomi persona-apps apply-action fitness-today --action fitness.complete_workout -
|
|
|
159
187
|
Rules for agents:
|
|
160
188
|
|
|
161
189
|
- Answer from returned `appState.bindings`, not from memory.
|
|
162
|
-
- For Fitness questions, prefer `oomi fitness show --json` because it returns the Fitness mini-app state and approved health context together.
|
|
163
190
|
- Apply updates only through approved `persona-apps apply-action` actions.
|
|
164
191
|
- If the persona app does not exist, tell the user to add it from the Oomi client first.
|
|
165
192
|
- Do not create local persona UI projects.
|
|
166
193
|
|
|
167
|
-
For the Fitness slice:
|
|
194
|
+
For the Fitness v0 slice:
|
|
168
195
|
|
|
169
196
|
```bash
|
|
170
|
-
oomi persona-apps apply-action fitness-today --action fitness.set_goal --payload-json '{"goalId":"move_more"}' --json
|
|
171
197
|
oomi persona-apps apply-action fitness-today --action fitness.complete_goal --payload-json '{"goalId":"easy-run"}' --json
|
|
172
198
|
oomi persona-apps apply-action fitness-today --action fitness.complete_workout --payload-json '{"goalId":"easy-run","minutes":20}' --json
|
|
173
199
|
```
|
|
174
200
|
|
|
175
|
-
Supported `fitness.set_goal` values are `move_more`, `sleep_better`, and `build_workout_habit`.
|
|
176
|
-
|
|
177
201
|
## Context Tools
|
|
178
202
|
|
|
179
203
|
Use context tools when the user asks a data-backed question that should come from Oomi-approved mobile permissions, not model memory. For health and fitness questions, read the latest backend HealthKit context first:
|
package/agent_instructions.md
CHANGED
|
@@ -12,7 +12,6 @@ Get OpenClaw into a state where:
|
|
|
12
12
|
- Oomi device-backed voice can start cleanly
|
|
13
13
|
- the bridge is supervised or restartable
|
|
14
14
|
- component-composed persona apps can be inspected and updated through Oomi API tools
|
|
15
|
-
- Fitness state can be inspected with one command that combines mini-app state and approved health context
|
|
16
15
|
- approved user context, such as HealthKit data synced by the Oomi mobile app, can be read through Oomi backend context tools
|
|
17
16
|
- the user never has to paste raw pair identifiers or gateway secrets manually when managed connect is available
|
|
18
17
|
|
|
@@ -20,27 +19,24 @@ Get OpenClaw into a state where:
|
|
|
20
19
|
|
|
21
20
|
If the user asks to connect OpenClaw to Oomi chat or voice:
|
|
22
21
|
|
|
23
|
-
1.
|
|
22
|
+
1. For a first-time install or missing local pairing state, run the bootstrap exactly once using the requested app URL:
|
|
24
23
|
- `curl -fsSL https://www.oomi.ai/install.sh | bash`
|
|
25
|
-
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
3. Pair the device without starting a legacy one-off flow:
|
|
29
|
-
- `oomi openclaw pair --app-url https://www.oomi.ai --no-start`
|
|
30
|
-
4. Install the OpenClaw plugin:
|
|
31
|
-
- `openclaw plugins install oomi-ai@latest`
|
|
32
|
-
5. Print the config block and secrets:
|
|
24
|
+
- The bootstrap installs or updates `oomi-ai`, performs the one initial pair, installs the plugin, and starts the saved bridge in detached mode.
|
|
25
|
+
- Do not pair again after the bootstrap finishes.
|
|
26
|
+
2. Print the config block and secrets:
|
|
33
27
|
- `oomi openclaw plugin --show-secrets --backend-url https://api.oomi.ai`
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
3. Apply the printed `channels.oomi.accounts.default` config.
|
|
29
|
+
4. Restart OpenClaw.
|
|
30
|
+
5. Start or repair the bridge from saved pairing state:
|
|
37
31
|
- `oomi openclaw bridge ensure --detach`
|
|
38
32
|
- if stale: `oomi openclaw bridge restart --detach`
|
|
39
33
|
- on macOS, prefer supervision: `oomi openclaw bridge service install`
|
|
34
|
+
6. If the Auth Link printed by bootstrap is missing or expired, create a replacement without re-pairing:
|
|
35
|
+
- `oomi openclaw invite --app-url https://www.oomi.ai`
|
|
40
36
|
|
|
41
|
-
If the
|
|
37
|
+
If the machine is already paired, do not rerun bootstrap. Use `invite` for a fresh Auth Link and `bridge ensure` or `bridge restart` for runtime recovery.
|
|
42
38
|
|
|
43
|
-
|
|
39
|
+
If the user provides a specific app URL, substitute it in both the bootstrap URL and the `invite --app-url` command.
|
|
44
40
|
|
|
45
41
|
## Required User-Facing Result
|
|
46
42
|
|
|
@@ -249,17 +245,8 @@ When a user asks about an existing persona app:
|
|
|
249
245
|
4. To update state, use a validated action: `oomi persona-apps apply-action <slug> --action <action-id> --payload-json '<json>' --json`.
|
|
250
246
|
5. If `persona-apps list` returns no app for the user's request, tell the user they can add that persona from the Oomi client so the client can collect the right permissions and data sources.
|
|
251
247
|
|
|
252
|
-
For
|
|
248
|
+
For the Fitness v0 slice:
|
|
253
249
|
|
|
254
|
-
```bash
|
|
255
|
-
oomi fitness show --json
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
It returns the linked Fitness persona app plus the latest approved health context in one payload.
|
|
259
|
-
|
|
260
|
-
For the Fitness slice:
|
|
261
|
-
|
|
262
|
-
- Use `fitness.set_goal` with payload `{"goalId":"move_more"}`, `{"goalId":"sleep_better"}`, or `{"goalId":"build_workout_habit"}` to set the active focus.
|
|
263
250
|
- Use `fitness.complete_goal` with payload `{"goalId":"easy-run"}` to mark a known goal complete.
|
|
264
251
|
- Use `fitness.complete_workout` with payload like `{"goalId":"easy-run","minutes":20}` when the user says they finished a workout.
|
|
265
252
|
|
|
@@ -292,3 +279,19 @@ Rules:
|
|
|
292
279
|
- Never present stale HealthKit values as current values.
|
|
293
280
|
- Do not infer unavailable health fields.
|
|
294
281
|
- Do not request HealthKit directly from the phone; the Oomi mobile app owns permission prompts and syncs approved data to the backend.
|
|
282
|
+
|
|
283
|
+
## Phone Notifications
|
|
284
|
+
|
|
285
|
+
To reach the user on their phone when they are not in the conversation, send a push notification:
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
oomi notify "Your 7-day plan is ready. Want to look now?" --title "Fitness" --json
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Rules:
|
|
292
|
+
|
|
293
|
+
- Notify only for things worth interrupting the user for: a reminder they asked for, a finished task, or something time-sensitive. Say everything else in chat.
|
|
294
|
+
- Keep the message short and specific. The title defaults to your agent name.
|
|
295
|
+
- Tapping the notification opens the Oomi app to the conversation, so follow up in chat.
|
|
296
|
+
- If `notification.status` is `skipped`, tell the user `notification.guidance` instead of retrying.
|
|
297
|
+
- If the request is rate limited, do not retry until `retryAfterSeconds` has passed.
|
package/bin/oomi-ai.js
CHANGED
|
@@ -233,15 +233,16 @@ Commands:
|
|
|
233
233
|
persona-apps apply-action <slug>
|
|
234
234
|
Apply an approved persona app action through the Oomi backend.
|
|
235
235
|
|
|
236
|
-
fitness show
|
|
237
|
-
Show the linked Fitness persona app plus approved health context.
|
|
238
|
-
|
|
239
236
|
context health
|
|
240
237
|
Show the latest account-linked health context available to this paired OpenClaw device.
|
|
241
238
|
context health sync [--wait]
|
|
242
239
|
Request the linked Oomi mobile app to sync fresh HealthKit context.
|
|
243
240
|
Use --wait to poll until fresh context is uploaded or the timeout expires.
|
|
244
241
|
|
|
242
|
+
notify "<message>" [--title TITLE]
|
|
243
|
+
Send the linked user a push notification on their phone. Use it for things worth
|
|
244
|
+
interrupting them for; say everything else in chat. Limited per hour.
|
|
245
|
+
|
|
245
246
|
Common flags:
|
|
246
247
|
--agents-file PATH Override AGENTS.md path
|
|
247
248
|
--workspace PATH Override OpenClaw workspace root
|
|
@@ -382,18 +383,6 @@ function findRepoRoot(startDir) {
|
|
|
382
383
|
return null;
|
|
383
384
|
}
|
|
384
385
|
|
|
385
|
-
function resolveRepoRoot(rootFlag) {
|
|
386
|
-
const explicitRoot =
|
|
387
|
-
typeof rootFlag === 'string' && rootFlag.trim()
|
|
388
|
-
? path.resolve(rootFlag.trim())
|
|
389
|
-
: '';
|
|
390
|
-
const repoRoot = explicitRoot || findRepoRoot(process.cwd()) || findRepoRoot(PACKAGE_ROOT);
|
|
391
|
-
if (!repoRoot) {
|
|
392
|
-
throw new Error('Could not locate repo root. Use --root <repo root>.');
|
|
393
|
-
}
|
|
394
|
-
return repoRoot;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
386
|
function resolveSkillSource(cliRoot) {
|
|
398
387
|
const packaged = path.join(PACKAGE_ROOT, 'skills', 'oomi');
|
|
399
388
|
if (fs.existsSync(packaged)) {
|
|
@@ -587,32 +576,18 @@ async function handleContextHealthCommand(flags = {}) {
|
|
|
587
576
|
printStructuredResult(payload, isTruthyFlag(flags.json));
|
|
588
577
|
}
|
|
589
578
|
|
|
590
|
-
async function
|
|
579
|
+
async function handleNotifyCommand(message, flags = {}) {
|
|
591
580
|
const client = createCliPersonaApiClient(flags);
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
const appPayload = await client.getPersonaApp({ slug: fitnessApp.slug });
|
|
602
|
-
let healthPayload = null;
|
|
603
|
-
let healthContextError = null;
|
|
604
|
-
try {
|
|
605
|
-
healthPayload = await client.getHealthContext();
|
|
606
|
-
} catch (err) {
|
|
607
|
-
healthContextError = err?.message || String(err);
|
|
608
|
-
}
|
|
581
|
+
const payload = await client.sendNotification({
|
|
582
|
+
body: message,
|
|
583
|
+
title: typeof flags.title === 'string' ? flags.title : undefined,
|
|
584
|
+
kind: typeof flags.kind === 'string' ? flags.kind : undefined,
|
|
585
|
+
});
|
|
586
|
+
printStructuredResult(payload, isTruthyFlag(flags.json));
|
|
587
|
+
}
|
|
609
588
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
personaApp: appPayload?.personaApp || null,
|
|
613
|
-
healthContext: healthPayload?.healthContext || null,
|
|
614
|
-
healthContextError,
|
|
615
|
-
}, isTruthyFlag(flags.json));
|
|
589
|
+
async function handleContextFinanceCommand(_flags = {}) {
|
|
590
|
+
throw new Error("oomi context finance is off until Gate A. Use the signed-in web Finance persona-app. Do not read bank balances from the connected OpenClaw shell.");
|
|
616
591
|
}
|
|
617
592
|
|
|
618
593
|
async function handleContextHealthSyncCommand(flags = {}) {
|
|
@@ -772,6 +747,28 @@ function readOpenclawGatewayConfig() {
|
|
|
772
747
|
};
|
|
773
748
|
}
|
|
774
749
|
|
|
750
|
+
function readOpenclawDefaultSessionKey() {
|
|
751
|
+
try {
|
|
752
|
+
const configPath = resolveOpenclawConfigPath();
|
|
753
|
+
if (!configPath || !fs.existsSync(configPath)) {
|
|
754
|
+
return '';
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const parsed = JSON.parse(readFile(configPath));
|
|
758
|
+
const channels = parsed.channels || {};
|
|
759
|
+
const oomi = channels.oomi || {};
|
|
760
|
+
const accounts = oomi.accounts || {};
|
|
761
|
+
const defaultAccount = accounts.default || {};
|
|
762
|
+
const defaultSessionKey = typeof defaultAccount.defaultSessionKey === 'string'
|
|
763
|
+
? defaultAccount.defaultSessionKey.trim()
|
|
764
|
+
: '';
|
|
765
|
+
|
|
766
|
+
return defaultSessionKey;
|
|
767
|
+
} catch {
|
|
768
|
+
return '';
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
775
772
|
function resolveBridgeStatePath() {
|
|
776
773
|
return resolveOpenclawBridgeStatePath();
|
|
777
774
|
}
|
|
@@ -1080,6 +1077,25 @@ async function requestManagedPairCode({ appUrl, label }) {
|
|
|
1080
1077
|
return payload;
|
|
1081
1078
|
}
|
|
1082
1079
|
|
|
1080
|
+
async function requestBrokerPairCode({ brokerHttp, adminKey, label }) {
|
|
1081
|
+
const response = await fetch(`${brokerHttp.replace(/\/$/, '')}/v1/pair/start`, {
|
|
1082
|
+
method: 'POST',
|
|
1083
|
+
headers: {
|
|
1084
|
+
'Content-Type': 'application/json',
|
|
1085
|
+
Authorization: `Bearer ${adminKey}`,
|
|
1086
|
+
},
|
|
1087
|
+
body: JSON.stringify({ label }),
|
|
1088
|
+
});
|
|
1089
|
+
const payload = await response.json().catch(() => ({}));
|
|
1090
|
+
if (!response.ok || !payload?.pairCode) {
|
|
1091
|
+
const message =
|
|
1092
|
+
(payload && typeof payload.error === 'string' && payload.error) ||
|
|
1093
|
+
`Broker pair start failed (${response.status})`;
|
|
1094
|
+
throw new Error(message);
|
|
1095
|
+
}
|
|
1096
|
+
return payload;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1083
1099
|
async function requestConnectInviteLink({ backendHttp, appUrl, sessionKey, deviceToken }) {
|
|
1084
1100
|
const response = await fetch(`${backendHttp.replace(/\/$/, '')}/v1/invite_links/start`, {
|
|
1085
1101
|
method: 'POST',
|
|
@@ -1115,6 +1131,35 @@ async function fetchManagedGatewayConfig({ appUrl }) {
|
|
|
1115
1131
|
return payload;
|
|
1116
1132
|
}
|
|
1117
1133
|
|
|
1134
|
+
function shouldRewriteLoopbackBrokerForAppHost(appHostname) {
|
|
1135
|
+
return String(appHostname || '').trim().toLowerCase() === 'host.docker.internal';
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function isLoopbackBrokerHostname(hostname) {
|
|
1139
|
+
const normalized = String(hostname || '').trim().toLowerCase();
|
|
1140
|
+
return normalized === '127.0.0.1' || normalized === 'localhost' || normalized === '::1';
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
function rewriteLoopbackBrokerUrlForAppHost(rawUrl, appUrl) {
|
|
1144
|
+
const value = String(rawUrl || '').trim();
|
|
1145
|
+
if (!value) return value;
|
|
1146
|
+
|
|
1147
|
+
try {
|
|
1148
|
+
const brokerUrl = new URL(value);
|
|
1149
|
+
const app = new URL(String(appUrl || '').trim());
|
|
1150
|
+
if (!isLoopbackBrokerHostname(brokerUrl.hostname)) {
|
|
1151
|
+
return value;
|
|
1152
|
+
}
|
|
1153
|
+
if (!shouldRewriteLoopbackBrokerForAppHost(app.hostname)) {
|
|
1154
|
+
return value;
|
|
1155
|
+
}
|
|
1156
|
+
brokerUrl.hostname = app.hostname;
|
|
1157
|
+
return brokerUrl.toString();
|
|
1158
|
+
} catch {
|
|
1159
|
+
return value;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1118
1163
|
function base64UrlEncode(value) {
|
|
1119
1164
|
return Buffer.from(value)
|
|
1120
1165
|
.toString('base64')
|
|
@@ -1773,48 +1818,6 @@ function printAssistantFinalDebugResult(result, asJson) {
|
|
|
1773
1818
|
}
|
|
1774
1819
|
}
|
|
1775
1820
|
|
|
1776
|
-
function resolveCommandFromPath(commandName) {
|
|
1777
|
-
const normalized = String(commandName || '').trim();
|
|
1778
|
-
if (!normalized) return '';
|
|
1779
|
-
try {
|
|
1780
|
-
const probe = spawnSync(process.platform === 'win32' ? 'where' : 'which', [normalized], {
|
|
1781
|
-
encoding: 'utf8',
|
|
1782
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1783
|
-
});
|
|
1784
|
-
if (probe.status !== 0) return '';
|
|
1785
|
-
const firstLine = String(probe.stdout || '')
|
|
1786
|
-
.split(/\r?\n/)
|
|
1787
|
-
.map((line) => line.trim())
|
|
1788
|
-
.find(Boolean);
|
|
1789
|
-
return firstLine || '';
|
|
1790
|
-
} catch {
|
|
1791
|
-
return '';
|
|
1792
|
-
}
|
|
1793
|
-
}
|
|
1794
|
-
|
|
1795
|
-
function resolveExecutable(candidates = []) {
|
|
1796
|
-
for (const candidate of candidates) {
|
|
1797
|
-
if (!candidate) continue;
|
|
1798
|
-
const value = String(candidate).trim();
|
|
1799
|
-
if (!value) continue;
|
|
1800
|
-
if (path.isAbsolute(value) && fs.existsSync(value)) {
|
|
1801
|
-
return value;
|
|
1802
|
-
}
|
|
1803
|
-
if (value.includes(path.sep) || value.includes('/')) {
|
|
1804
|
-
const resolved = path.resolve(value);
|
|
1805
|
-
if (fs.existsSync(resolved)) {
|
|
1806
|
-
return resolved;
|
|
1807
|
-
}
|
|
1808
|
-
continue;
|
|
1809
|
-
}
|
|
1810
|
-
const fromPath = resolveCommandFromPath(value);
|
|
1811
|
-
if (fromPath) {
|
|
1812
|
-
return fromPath;
|
|
1813
|
-
}
|
|
1814
|
-
}
|
|
1815
|
-
return '';
|
|
1816
|
-
}
|
|
1817
|
-
|
|
1818
1821
|
async function handleOpenclawDebugCommand(action, flags) {
|
|
1819
1822
|
const normalizedAction = String(action || '').trim().toLowerCase();
|
|
1820
1823
|
const frameFile =
|
|
@@ -2558,11 +2561,13 @@ async function startOpenclawBridge(flags) {
|
|
|
2558
2561
|
if (!deviceToken) {
|
|
2559
2562
|
throw new Error('Broker pair claim did not return deviceToken.');
|
|
2560
2563
|
}
|
|
2564
|
+
const configSessionKey = readOpenclawDefaultSessionKey();
|
|
2561
2565
|
writeBridgeState({
|
|
2562
2566
|
brokerHttp,
|
|
2563
2567
|
brokerWs,
|
|
2564
2568
|
deviceId,
|
|
2565
2569
|
deviceToken,
|
|
2570
|
+
sessionKey: configSessionKey || 'agent:main:webchat:channel:oomi',
|
|
2566
2571
|
claimedAt: new Date().toISOString(),
|
|
2567
2572
|
expiresAt: claimed.expiresAt || null,
|
|
2568
2573
|
});
|
|
@@ -3837,7 +3842,24 @@ async function pairAndStartOpenclawBridge(flags) {
|
|
|
3837
3842
|
|
|
3838
3843
|
console.log(`Pairing OpenClaw host with Oomi app: ${appUrl}`);
|
|
3839
3844
|
const managedConfig = await fetchManagedGatewayConfig({ appUrl });
|
|
3840
|
-
const
|
|
3845
|
+
const brokerHttp = rewriteLoopbackBrokerUrlForAppHost(managedConfig.brokerHttpUrl, appUrl);
|
|
3846
|
+
let pairStarted;
|
|
3847
|
+
try {
|
|
3848
|
+
pairStarted = await requestManagedPairCode({ appUrl, label });
|
|
3849
|
+
} catch (err) {
|
|
3850
|
+
const brokerAdminKey = String(process.env.OOMI_CHAT_BROKER_ADMIN_KEY || '').trim();
|
|
3851
|
+
if (!brokerAdminKey) {
|
|
3852
|
+
throw err;
|
|
3853
|
+
}
|
|
3854
|
+
console.warn(
|
|
3855
|
+
`[oomi] Managed pair start via app failed (${extractErrorMessage(err)}); falling back to direct broker pair/start.`
|
|
3856
|
+
);
|
|
3857
|
+
pairStarted = await requestBrokerPairCode({
|
|
3858
|
+
brokerHttp,
|
|
3859
|
+
adminKey: brokerAdminKey,
|
|
3860
|
+
label,
|
|
3861
|
+
});
|
|
3862
|
+
}
|
|
3841
3863
|
const pairCode = String(pairStarted.pairCode || '').trim().toUpperCase();
|
|
3842
3864
|
if (!pairCode) {
|
|
3843
3865
|
throw new Error('Pairing code missing from backend response.');
|
|
@@ -3845,7 +3867,7 @@ async function pairAndStartOpenclawBridge(flags) {
|
|
|
3845
3867
|
|
|
3846
3868
|
console.log(`Received pair code: ${pairCode}`);
|
|
3847
3869
|
const claimed = await claimBridgeDeviceToken({
|
|
3848
|
-
brokerHttp
|
|
3870
|
+
brokerHttp,
|
|
3849
3871
|
pairCode,
|
|
3850
3872
|
deviceId,
|
|
3851
3873
|
});
|
|
@@ -3854,9 +3876,11 @@ async function pairAndStartOpenclawBridge(flags) {
|
|
|
3854
3876
|
throw new Error('Broker pair claim did not return deviceToken.');
|
|
3855
3877
|
}
|
|
3856
3878
|
|
|
3857
|
-
const brokerWs = String(
|
|
3879
|
+
const brokerWs = String(
|
|
3880
|
+
flags['broker-ws'] || rewriteLoopbackBrokerUrlForAppHost(managedConfig.brokerDeviceWsUrl, appUrl)
|
|
3881
|
+
).trim();
|
|
3858
3882
|
writeBridgeState({
|
|
3859
|
-
brokerHttp
|
|
3883
|
+
brokerHttp,
|
|
3860
3884
|
brokerWs,
|
|
3861
3885
|
deviceId,
|
|
3862
3886
|
deviceToken,
|
|
@@ -3866,7 +3890,7 @@ async function pairAndStartOpenclawBridge(flags) {
|
|
|
3866
3890
|
});
|
|
3867
3891
|
|
|
3868
3892
|
const invite = await requestConnectInviteLink({
|
|
3869
|
-
backendHttp:
|
|
3893
|
+
backendHttp: brokerHttp,
|
|
3870
3894
|
appUrl,
|
|
3871
3895
|
sessionKey,
|
|
3872
3896
|
deviceToken,
|
|
@@ -3940,9 +3964,11 @@ async function createOpenclawInviteLink(flags) {
|
|
|
3940
3964
|
''
|
|
3941
3965
|
).trim();
|
|
3942
3966
|
const appUrl = String(flags['app-url'] || process.env.OOMI_APP_URL || 'http://127.0.0.1:3456').trim();
|
|
3967
|
+
const configSessionKey = readOpenclawDefaultSessionKey();
|
|
3943
3968
|
const sessionKey = String(
|
|
3944
3969
|
flags['session-key'] ||
|
|
3945
3970
|
process.env.OOMI_SESSION_KEY ||
|
|
3971
|
+
configSessionKey ||
|
|
3946
3972
|
bridgeState.sessionKey ||
|
|
3947
3973
|
'agent:main:webchat:channel:oomi'
|
|
3948
3974
|
).trim();
|
|
@@ -4403,11 +4429,6 @@ async function main() {
|
|
|
4403
4429
|
return;
|
|
4404
4430
|
}
|
|
4405
4431
|
|
|
4406
|
-
if (command === 'fitness' && subcommand === 'show') {
|
|
4407
|
-
await handleFitnessShowCommand(args.flags);
|
|
4408
|
-
return;
|
|
4409
|
-
}
|
|
4410
|
-
|
|
4411
4432
|
if (command === 'context' && subcommand === 'health') {
|
|
4412
4433
|
if (args.positionals[0] === 'sync') {
|
|
4413
4434
|
await handleContextHealthSyncCommand(args.flags);
|
|
@@ -4417,6 +4438,22 @@ async function main() {
|
|
|
4417
4438
|
return;
|
|
4418
4439
|
}
|
|
4419
4440
|
|
|
4441
|
+
if (command === 'context' && subcommand === 'finance') {
|
|
4442
|
+
await handleContextFinanceCommand(args.flags);
|
|
4443
|
+
return;
|
|
4444
|
+
}
|
|
4445
|
+
|
|
4446
|
+
if (command === 'notify') {
|
|
4447
|
+
// parseArgs reads the first word as a subcommand, so the message is everything after `notify`.
|
|
4448
|
+
const message = [subcommand, ...args.positionals].filter(Boolean).join(' ').trim()
|
|
4449
|
+
|| (typeof args.flags.body === 'string' ? args.flags.body.trim() : '');
|
|
4450
|
+
if (!message) {
|
|
4451
|
+
throw new Error('A message is required. Usage: oomi notify "<message>" [--title <title>]');
|
|
4452
|
+
}
|
|
4453
|
+
await handleNotifyCommand(message, args.flags);
|
|
4454
|
+
return;
|
|
4455
|
+
}
|
|
4456
|
+
|
|
4420
4457
|
console.error(`Unknown command: ${command} ${subcommand || ''}`.trim());
|
|
4421
4458
|
usage();
|
|
4422
4459
|
process.exit(1);
|
|
@@ -4457,4 +4494,5 @@ export {
|
|
|
4457
4494
|
isGatewayRunStartedFrame,
|
|
4458
4495
|
isBridgeWorkerCommand,
|
|
4459
4496
|
parsePositiveInteger,
|
|
4497
|
+
rewriteLoopbackBrokerUrlForAppHost,
|
|
4460
4498
|
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
const DEFAULT_ENDPOINT_PATH = '/v1/model/relay/chat';
|
|
2
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 60000;
|
|
3
|
+
const DEFAULT_REQUEST_ORIGIN = 'hosted_openclaw';
|
|
4
|
+
|
|
5
|
+
function toString(value, fallback = '') {
|
|
6
|
+
return typeof value === 'string' && value.trim() ? value.trim() : fallback;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function toPositiveInteger(value, fallback, { min = 1, max = Number.MAX_SAFE_INTEGER } = {}) {
|
|
10
|
+
const parsed = Number.parseInt(String(value || ''), 10);
|
|
11
|
+
if (!Number.isFinite(parsed) || parsed < min) return fallback;
|
|
12
|
+
return Math.min(parsed, max);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeMessages(messages) {
|
|
16
|
+
return Array.isArray(messages)
|
|
17
|
+
? messages
|
|
18
|
+
.map((message) => {
|
|
19
|
+
if (!message || typeof message !== 'object') return null;
|
|
20
|
+
const role = toString(message.role);
|
|
21
|
+
const content = toString(message.content);
|
|
22
|
+
if (!role || !content) return null;
|
|
23
|
+
return { role, content };
|
|
24
|
+
})
|
|
25
|
+
.filter(Boolean)
|
|
26
|
+
: [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function extractModelRelayConfig(cfg = {}, accountId = '') {
|
|
30
|
+
const channel = cfg?.channels?.oomi || cfg?.oomi || cfg;
|
|
31
|
+
const accounts = channel && typeof channel.accounts === 'object' ? channel.accounts : {};
|
|
32
|
+
const defaultAccountId = toString(accountId, toString(channel?.defaultAccountId, 'default'));
|
|
33
|
+
const account = accounts[defaultAccountId] || accounts.default || {};
|
|
34
|
+
const relay = account?.modelRelay || cfg?.modelRelay || {};
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
enabled: relay.enabled === true,
|
|
38
|
+
backendUrl: toString(relay.backendUrl, toString(account.backendUrl)),
|
|
39
|
+
endpointPath: toString(relay.endpointPath, DEFAULT_ENDPOINT_PATH),
|
|
40
|
+
provider: toString(relay.provider),
|
|
41
|
+
model: toString(relay.model),
|
|
42
|
+
requestOrigin: toString(relay.requestOrigin, DEFAULT_REQUEST_ORIGIN),
|
|
43
|
+
userId: toString(relay.userId),
|
|
44
|
+
deviceId: toString(relay.deviceId),
|
|
45
|
+
internalKeyEnv: toString(relay.internalKeyEnv),
|
|
46
|
+
requestTimeoutMs: toPositiveInteger(relay.requestTimeoutMs, DEFAULT_REQUEST_TIMEOUT_MS, {
|
|
47
|
+
min: 2000,
|
|
48
|
+
max: 120000,
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function buildRelayUrl(backendUrl, endpointPath) {
|
|
54
|
+
const base = toString(backendUrl).replace(/\/+$/, '');
|
|
55
|
+
const path = `/${toString(endpointPath, DEFAULT_ENDPOINT_PATH).replace(/^\/+/, '')}`;
|
|
56
|
+
return `${base}${path}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resolveInternalKey({ relayConfig, env = process.env, internalKey = '' }) {
|
|
60
|
+
const direct = toString(internalKey);
|
|
61
|
+
if (direct) return direct;
|
|
62
|
+
|
|
63
|
+
const envName = toString(relayConfig.internalKeyEnv);
|
|
64
|
+
if (!envName) return '';
|
|
65
|
+
|
|
66
|
+
return toString(env?.[envName]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function validateModelRelayConfig(relayConfig, { env = process.env, internalKey = '' } = {}) {
|
|
70
|
+
const config = relayConfig || {};
|
|
71
|
+
if (!config.enabled) {
|
|
72
|
+
return { ok: false, error: 'Oomi model relay is disabled.' };
|
|
73
|
+
}
|
|
74
|
+
if (!config.backendUrl) {
|
|
75
|
+
return { ok: false, error: 'Oomi model relay backendUrl is required.' };
|
|
76
|
+
}
|
|
77
|
+
if (!config.userId) {
|
|
78
|
+
return { ok: false, error: 'Oomi model relay userId is required.' };
|
|
79
|
+
}
|
|
80
|
+
if (!resolveInternalKey({ relayConfig: config, env, internalKey })) {
|
|
81
|
+
return { ok: false, error: 'Oomi model relay internal key is required.' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { ok: true };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function chatWithModelRelay({
|
|
88
|
+
cfg,
|
|
89
|
+
accountId = '',
|
|
90
|
+
messages,
|
|
91
|
+
maxOutputTokens,
|
|
92
|
+
temperature,
|
|
93
|
+
conversationKey = '',
|
|
94
|
+
requestOrigin = '',
|
|
95
|
+
fetchImpl = globalThis.fetch,
|
|
96
|
+
env = process.env,
|
|
97
|
+
internalKey = '',
|
|
98
|
+
} = {}) {
|
|
99
|
+
if (typeof fetchImpl !== 'function') {
|
|
100
|
+
throw new Error('A fetch implementation is required for Oomi model relay chat.');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const relayConfig = extractModelRelayConfig(cfg, accountId);
|
|
104
|
+
const validation = validateModelRelayConfig(relayConfig, { env, internalKey });
|
|
105
|
+
if (!validation.ok) {
|
|
106
|
+
throw new Error(validation.error);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const normalizedMessages = normalizeMessages(messages);
|
|
110
|
+
if (normalizedMessages.length === 0) {
|
|
111
|
+
throw new Error('At least one model relay message is required.');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const controller = new AbortController();
|
|
115
|
+
const timeout = setTimeout(() => controller.abort(), relayConfig.requestTimeoutMs);
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const body = {
|
|
119
|
+
provider: relayConfig.provider || undefined,
|
|
120
|
+
model: relayConfig.model || undefined,
|
|
121
|
+
messages: normalizedMessages,
|
|
122
|
+
requestOrigin: toString(requestOrigin, relayConfig.requestOrigin),
|
|
123
|
+
maxOutputTokens: toPositiveInteger(maxOutputTokens, undefined),
|
|
124
|
+
temperature,
|
|
125
|
+
deviceId: relayConfig.deviceId || undefined,
|
|
126
|
+
conversationKey: toString(conversationKey) || undefined,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const response = await fetchImpl(buildRelayUrl(relayConfig.backendUrl, relayConfig.endpointPath), {
|
|
130
|
+
method: 'POST',
|
|
131
|
+
headers: {
|
|
132
|
+
'Content-Type': 'application/json',
|
|
133
|
+
'X-Oomi-Internal-Key': resolveInternalKey({ relayConfig, env, internalKey }),
|
|
134
|
+
'X-Oomi-User-Id': relayConfig.userId,
|
|
135
|
+
},
|
|
136
|
+
body: JSON.stringify(body),
|
|
137
|
+
signal: controller.signal,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const payload = await response.json().catch(() => ({}));
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
const error = toString(payload.error, `Oomi model relay failed with status ${response.status}.`);
|
|
143
|
+
throw new Error(error);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return payload;
|
|
147
|
+
} finally {
|
|
148
|
+
clearTimeout(timeout);
|
|
149
|
+
}
|
|
150
|
+
}
|
package/lib/openclawProfile.js
CHANGED
|
@@ -8,6 +8,15 @@ const DEFAULT_SESSION_KEY = 'agent:main:webchat:channel:oomi';
|
|
|
8
8
|
const DEFAULT_GATEWAY_PORT = 18789;
|
|
9
9
|
const DEFAULT_PLUGIN_TRUST_MODE = 'auto-discovery';
|
|
10
10
|
const DEFAULT_MODEL_AUTH_MODE = 'oomi-managed';
|
|
11
|
+
const DEFAULT_MODEL_RELAY_PROVIDER = 'openrouter';
|
|
12
|
+
const DEFAULT_MODEL_RELAY_MODEL = 'google/gemini-2.5-flash';
|
|
13
|
+
const DEFAULT_MODEL_RELAY_ENDPOINT_PATH = '/v1/model/relay/chat';
|
|
14
|
+
const DEFAULT_MODEL_RELAY_OPENAI_BASE_PATH = '/v1/model/relay/openai';
|
|
15
|
+
const DEFAULT_MODEL_RELAY_REQUEST_ORIGIN = 'hosted_openclaw';
|
|
16
|
+
const DEFAULT_MODEL_RELAY_TIMEOUT_MS = 60000;
|
|
17
|
+
const DEFAULT_MODEL_RELAY_INTERNAL_KEY_ENV = 'OOMI_HOSTED_BOOTSTRAP_INTERNAL_KEY';
|
|
18
|
+
const DEFAULT_MODEL_RELAY_OPENCLAW_PROVIDER_ID = 'oomi-managed';
|
|
19
|
+
const DEFAULT_MODEL_RELAY_MAX_TOKENS = 1024;
|
|
11
20
|
|
|
12
21
|
function trimString(value) {
|
|
13
22
|
return typeof value === 'string' ? value.trim() : '';
|
|
@@ -62,6 +71,14 @@ function normalizeModelAuthMode(value) {
|
|
|
62
71
|
return trimString(value) === 'provider-env' ? 'provider-env' : DEFAULT_MODEL_AUTH_MODE;
|
|
63
72
|
}
|
|
64
73
|
|
|
74
|
+
function normalizeProviderId(value) {
|
|
75
|
+
const normalized = trimString(value)
|
|
76
|
+
.toLowerCase()
|
|
77
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
78
|
+
.replace(/^-+|-+$/g, '');
|
|
79
|
+
return normalized || DEFAULT_MODEL_RELAY_OPENCLAW_PROVIDER_ID;
|
|
80
|
+
}
|
|
81
|
+
|
|
65
82
|
function resolveConfigPath(openclawHome, explicitPath = '') {
|
|
66
83
|
const targetPath = trimString(explicitPath);
|
|
67
84
|
if (targetPath) return path.resolve(targetPath);
|
|
@@ -129,7 +146,7 @@ function applyOomiChannelConfig(config, oomiChannel) {
|
|
|
129
146
|
const accounts = typeof oomi.accounts === 'object' && oomi.accounts ? { ...oomi.accounts } : {};
|
|
130
147
|
const defaultAccount = typeof accounts.default === 'object' && accounts.default ? { ...accounts.default } : {};
|
|
131
148
|
|
|
132
|
-
|
|
149
|
+
const nextDefaultAccount = {
|
|
133
150
|
enabled: true,
|
|
134
151
|
requestTimeoutMs: normalizePositiveInteger(oomiChannel?.requestTimeoutMs, 15000),
|
|
135
152
|
...defaultAccount,
|
|
@@ -138,6 +155,16 @@ function applyOomiChannelConfig(config, oomiChannel) {
|
|
|
138
155
|
defaultSessionKey: trimString(oomiChannel?.defaultSessionKey) || DEFAULT_SESSION_KEY,
|
|
139
156
|
};
|
|
140
157
|
|
|
158
|
+
const modelRelay = buildModelRelayConfig(oomiChannel?.modelRelay, {
|
|
159
|
+
backendUrl: nextDefaultAccount.backendUrl,
|
|
160
|
+
});
|
|
161
|
+
if (modelRelay.enabled) {
|
|
162
|
+
nextDefaultAccount.modelRelay = modelRelay;
|
|
163
|
+
} else if (Object.prototype.hasOwnProperty.call(nextDefaultAccount, 'modelRelay')) {
|
|
164
|
+
delete nextDefaultAccount.modelRelay;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
accounts.default = nextDefaultAccount;
|
|
141
168
|
oomi.defaultAccountId = 'default';
|
|
142
169
|
oomi.accounts = accounts;
|
|
143
170
|
channels.oomi = oomi;
|
|
@@ -145,6 +172,110 @@ function applyOomiChannelConfig(config, oomiChannel) {
|
|
|
145
172
|
return nextConfig;
|
|
146
173
|
}
|
|
147
174
|
|
|
175
|
+
function buildModelRelayConfig(modelRelay, defaults = {}) {
|
|
176
|
+
const relay = modelRelay && typeof modelRelay === 'object' ? modelRelay : {};
|
|
177
|
+
const enabled = normalizeBoolean(relay.enabled, false);
|
|
178
|
+
const backendUrl = trimString(relay.backendUrl) || trimString(defaults.backendUrl);
|
|
179
|
+
const openclawProviderId = normalizeProviderId(relay.openclawProviderId);
|
|
180
|
+
const model = trimString(relay.model) || DEFAULT_MODEL_RELAY_MODEL;
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
enabled,
|
|
184
|
+
backendUrl,
|
|
185
|
+
endpointPath: trimString(relay.endpointPath) || DEFAULT_MODEL_RELAY_ENDPOINT_PATH,
|
|
186
|
+
openaiBasePath: trimString(relay.openaiBasePath) || DEFAULT_MODEL_RELAY_OPENAI_BASE_PATH,
|
|
187
|
+
openclawProviderId,
|
|
188
|
+
openclawModelRef: `${openclawProviderId}/${model}`,
|
|
189
|
+
provider: trimString(relay.provider) || DEFAULT_MODEL_RELAY_PROVIDER,
|
|
190
|
+
model,
|
|
191
|
+
requestOrigin: trimString(relay.requestOrigin) || DEFAULT_MODEL_RELAY_REQUEST_ORIGIN,
|
|
192
|
+
userId: trimString(relay.userId),
|
|
193
|
+
deviceId: trimString(relay.deviceId),
|
|
194
|
+
internalKeyEnv: trimString(relay.internalKeyEnv) || DEFAULT_MODEL_RELAY_INTERNAL_KEY_ENV,
|
|
195
|
+
requestTimeoutMs: normalizePositiveInteger(relay.requestTimeoutMs, DEFAULT_MODEL_RELAY_TIMEOUT_MS),
|
|
196
|
+
maxTokens: normalizePositiveInteger(relay.maxTokens, DEFAULT_MODEL_RELAY_MAX_TOKENS),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function joinUrl(baseUrl, pathValue) {
|
|
201
|
+
const base = trimString(baseUrl).replace(/\/+$/, '');
|
|
202
|
+
const suffix = `/${trimString(pathValue).replace(/^\/+/, '')}`;
|
|
203
|
+
return `${base}${suffix}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function applyOomiManagedModelProviderConfig(config, modelRelay) {
|
|
207
|
+
if (!modelRelay?.enabled || !trimString(modelRelay.backendUrl) || !trimString(modelRelay.userId)) {
|
|
208
|
+
return config;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const nextConfig = { ...(config || {}) };
|
|
212
|
+
const modelsRoot = nextConfig.models && typeof nextConfig.models === 'object' ? { ...nextConfig.models } : {};
|
|
213
|
+
const providers =
|
|
214
|
+
modelsRoot.providers && typeof modelsRoot.providers === 'object' ? { ...modelsRoot.providers } : {};
|
|
215
|
+
const providerId = normalizeProviderId(modelRelay.openclawProviderId);
|
|
216
|
+
const modelId = trimString(modelRelay.model) || DEFAULT_MODEL_RELAY_MODEL;
|
|
217
|
+
|
|
218
|
+
modelsRoot.mode = trimString(modelsRoot.mode) || 'merge';
|
|
219
|
+
providers[providerId] = {
|
|
220
|
+
...(providers[providerId] && typeof providers[providerId] === 'object' ? providers[providerId] : {}),
|
|
221
|
+
baseUrl: joinUrl(modelRelay.backendUrl, modelRelay.openaiBasePath),
|
|
222
|
+
api: 'openai-completions',
|
|
223
|
+
apiKey: `\${${modelRelay.internalKeyEnv}}`,
|
|
224
|
+
authHeader: true,
|
|
225
|
+
headers: {
|
|
226
|
+
'X-Oomi-User-Id': modelRelay.userId,
|
|
227
|
+
'X-Oomi-Device-Id': modelRelay.deviceId,
|
|
228
|
+
'X-Oomi-Relay-Provider': modelRelay.provider,
|
|
229
|
+
'X-Oomi-Request-Origin': modelRelay.requestOrigin,
|
|
230
|
+
},
|
|
231
|
+
models: [
|
|
232
|
+
{
|
|
233
|
+
id: modelId,
|
|
234
|
+
name: `Oomi Managed ${modelId}`,
|
|
235
|
+
reasoning: false,
|
|
236
|
+
input: ['text'],
|
|
237
|
+
cost: {
|
|
238
|
+
input: 0.0003,
|
|
239
|
+
output: 0.0025,
|
|
240
|
+
cacheRead: 0,
|
|
241
|
+
cacheWrite: 0,
|
|
242
|
+
},
|
|
243
|
+
contextWindow: 128000,
|
|
244
|
+
contextTokens: 96000,
|
|
245
|
+
maxTokens: normalizePositiveInteger(modelRelay.maxTokens, DEFAULT_MODEL_RELAY_MAX_TOKENS),
|
|
246
|
+
compat: {
|
|
247
|
+
requiresStringContent: true,
|
|
248
|
+
strictMessageKeys: true,
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
};
|
|
253
|
+
modelsRoot.providers = providers;
|
|
254
|
+
nextConfig.models = modelsRoot;
|
|
255
|
+
|
|
256
|
+
const agents = nextConfig.agents && typeof nextConfig.agents === 'object' ? { ...nextConfig.agents } : {};
|
|
257
|
+
const defaults = agents.defaults && typeof agents.defaults === 'object' ? { ...agents.defaults } : {};
|
|
258
|
+
const defaultModel =
|
|
259
|
+
defaults.model && typeof defaults.model === 'object' && !Array.isArray(defaults.model)
|
|
260
|
+
? { ...defaults.model }
|
|
261
|
+
: {};
|
|
262
|
+
const allowedModels =
|
|
263
|
+
defaults.models && typeof defaults.models === 'object' ? { ...defaults.models } : {};
|
|
264
|
+
const modelRef = `${providerId}/${modelId}`;
|
|
265
|
+
|
|
266
|
+
defaultModel.primary = modelRef;
|
|
267
|
+
allowedModels[modelRef] = {
|
|
268
|
+
...(allowedModels[modelRef] && typeof allowedModels[modelRef] === 'object' ? allowedModels[modelRef] : {}),
|
|
269
|
+
alias: 'Oomi Managed',
|
|
270
|
+
};
|
|
271
|
+
defaults.model = defaultModel;
|
|
272
|
+
defaults.models = allowedModels;
|
|
273
|
+
agents.defaults = defaults;
|
|
274
|
+
nextConfig.agents = agents;
|
|
275
|
+
|
|
276
|
+
return nextConfig;
|
|
277
|
+
}
|
|
278
|
+
|
|
148
279
|
function removeOomiChannelConfig(config) {
|
|
149
280
|
const nextConfig = { ...(config || {}) };
|
|
150
281
|
const channels =
|
|
@@ -193,6 +324,26 @@ export function buildOomiDevLocalProfile(options = {}) {
|
|
|
193
324
|
defaultSessionKey: trimString(options.defaultSessionKey) || DEFAULT_SESSION_KEY,
|
|
194
325
|
requestTimeoutMs: normalizePositiveInteger(options.requestTimeoutMs, 15000),
|
|
195
326
|
pluginTrustMode,
|
|
327
|
+
modelRelay: buildModelRelayConfig(
|
|
328
|
+
{
|
|
329
|
+
enabled: normalizeBoolean(options.modelRelayEnabled, false),
|
|
330
|
+
backendUrl: options.modelRelayBackendUrl || options.backendUrl,
|
|
331
|
+
endpointPath: options.modelRelayEndpointPath,
|
|
332
|
+
openaiBasePath: options.modelRelayOpenaiBasePath,
|
|
333
|
+
openclawProviderId: options.modelRelayOpenclawProviderId,
|
|
334
|
+
provider: options.modelRelayProvider,
|
|
335
|
+
model: options.modelRelayModel,
|
|
336
|
+
requestOrigin: options.modelRelayRequestOrigin,
|
|
337
|
+
userId: options.modelRelayUserId,
|
|
338
|
+
deviceId: options.modelRelayDeviceId || options.deviceId,
|
|
339
|
+
internalKeyEnv: options.modelRelayInternalKeyEnv,
|
|
340
|
+
requestTimeoutMs: options.modelRelayRequestTimeoutMs,
|
|
341
|
+
maxTokens: options.modelRelayMaxTokens,
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
backendUrl: options.backendUrl,
|
|
345
|
+
}
|
|
346
|
+
),
|
|
196
347
|
},
|
|
197
348
|
model: {
|
|
198
349
|
preset: trimString(options.modelPreset || 'openrouter-free'),
|
|
@@ -240,6 +391,9 @@ export function applyOpenclawProfile({
|
|
|
240
391
|
|
|
241
392
|
if (normalizeBoolean(profile?.oomiChannel?.enabled, false)) {
|
|
242
393
|
nextConfig = applyOomiChannelConfig(nextConfig, profile.oomiChannel);
|
|
394
|
+
if (normalizeModelAuthMode(profile?.model?.authMode) === 'oomi-managed') {
|
|
395
|
+
nextConfig = applyOomiManagedModelProviderConfig(nextConfig, profile.oomiChannel.modelRelay);
|
|
396
|
+
}
|
|
243
397
|
} else {
|
|
244
398
|
nextConfig = removeOomiChannelConfig(nextConfig);
|
|
245
399
|
}
|
package/lib/personaApiClient.js
CHANGED
|
@@ -149,6 +149,15 @@ export function createPersonaApiClient({
|
|
|
149
149
|
});
|
|
150
150
|
},
|
|
151
151
|
|
|
152
|
+
getFinanceContext() {
|
|
153
|
+
return getJson({
|
|
154
|
+
fetchImpl,
|
|
155
|
+
backendUrl: resolvedBackendUrl,
|
|
156
|
+
deviceToken: resolvedDeviceToken,
|
|
157
|
+
path: '/v1/context/finance',
|
|
158
|
+
});
|
|
159
|
+
},
|
|
160
|
+
|
|
152
161
|
requestHealthContextSync({
|
|
153
162
|
reason = 'agent_requested',
|
|
154
163
|
} = {}) {
|
|
@@ -162,5 +171,24 @@ export function createPersonaApiClient({
|
|
|
162
171
|
},
|
|
163
172
|
});
|
|
164
173
|
},
|
|
174
|
+
|
|
175
|
+
sendNotification({ body, title, kind } = {}) {
|
|
176
|
+
const safeBody = trimString(body);
|
|
177
|
+
if (!safeBody) {
|
|
178
|
+
throw new Error('Notification body is required.');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return postJson({
|
|
182
|
+
fetchImpl,
|
|
183
|
+
backendUrl: resolvedBackendUrl,
|
|
184
|
+
deviceToken: resolvedDeviceToken,
|
|
185
|
+
path: '/v1/notifications',
|
|
186
|
+
body: {
|
|
187
|
+
body: safeBody,
|
|
188
|
+
title: trimString(title) || undefined,
|
|
189
|
+
kind: trimString(kind) || undefined,
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
},
|
|
165
193
|
};
|
|
166
194
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oomi-ai",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"description": "Managed Oomi channel, bridge, voice, and persona app API tooling for OpenClaw",
|
|
5
5
|
"bin": {
|
|
6
6
|
"oomi": "bin/oomi-ai.js"
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"files": [
|
|
60
60
|
"bin/oomi-ai.js",
|
|
61
61
|
"bin/sessionBridgeState.js",
|
|
62
|
+
"lib/modelRelayClient.js",
|
|
62
63
|
"lib/openclawPaths.js",
|
|
63
64
|
"lib/openclawProfile.js",
|
|
64
65
|
"lib/personaApiClient.js",
|
package/persona-app/README.md
CHANGED
|
@@ -19,14 +19,11 @@ The current files are the first shared contract for the vertical slice:
|
|
|
19
19
|
OpenClaw agents should use the packaged CLI to inspect and update account-linked persona apps:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
oomi fitness show --json
|
|
23
22
|
oomi persona-apps list --json
|
|
24
23
|
oomi persona-apps show fitness-today --json
|
|
25
24
|
oomi persona-apps apply-action fitness-today --action fitness.complete_workout --payload-json '{"goalId":"easy-run","minutes":20}' --json
|
|
26
25
|
```
|
|
27
26
|
|
|
28
|
-
For Fitness, prefer `oomi fitness show --json` before answering because it returns the mini-app state and approved health context together.
|
|
29
|
-
|
|
30
27
|
When answering data-backed health or fitness questions, agents should read approved backend context first:
|
|
31
28
|
|
|
32
29
|
```bash
|
|
@@ -42,3 +39,5 @@ oomi context health sync --wait --json
|
|
|
42
39
|
Answer only after fresh context is uploaded. If the command times out, tell the user the phone sync is still pending.
|
|
43
40
|
|
|
44
41
|
If a persona app is missing, ask the user to add it from the Oomi client first so platform-specific permissions and hydration can run.
|
|
42
|
+
|
|
43
|
+
Finance balances are a signed-in web persona-app. `oomi context finance` / `finance.context.read` stay off the connected shell until Gate A.
|
|
@@ -1,27 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "persona-app.v1",
|
|
3
|
-
"registryVersion": "2026-04-
|
|
3
|
+
"registryVersion": "2026-04-24.1",
|
|
4
4
|
"contextTools": [
|
|
5
5
|
{
|
|
6
6
|
"id": "health.context.read",
|
|
7
7
|
"command": "oomi context health --json",
|
|
8
8
|
"endpoint": "/v1/context/health",
|
|
9
9
|
"description": "Reads the latest account-linked health context synced by Oomi mobile clients.",
|
|
10
|
-
"permissions": [
|
|
10
|
+
"permissions": [
|
|
11
|
+
"healthkit.read",
|
|
12
|
+
"health_connect.read"
|
|
13
|
+
],
|
|
11
14
|
"readOnly": true,
|
|
12
|
-
"targets": [
|
|
15
|
+
"targets": [
|
|
16
|
+
"ios",
|
|
17
|
+
"android",
|
|
18
|
+
"web"
|
|
19
|
+
]
|
|
13
20
|
}
|
|
14
21
|
],
|
|
15
22
|
"sharedComponents": [
|
|
16
23
|
{
|
|
17
24
|
"type": "shared.personaHero",
|
|
18
25
|
"description": "Domain hero with title, subtitle, and a primary metric.",
|
|
19
|
-
"targets": [
|
|
26
|
+
"targets": [
|
|
27
|
+
"ios",
|
|
28
|
+
"android",
|
|
29
|
+
"web",
|
|
30
|
+
"webspatial"
|
|
31
|
+
]
|
|
20
32
|
},
|
|
21
33
|
{
|
|
22
34
|
"type": "shared.oomiNote",
|
|
23
35
|
"description": "A primary-persona note with optional actions.",
|
|
24
|
-
"targets": [
|
|
36
|
+
"targets": [
|
|
37
|
+
"ios",
|
|
38
|
+
"android",
|
|
39
|
+
"web",
|
|
40
|
+
"webspatial"
|
|
41
|
+
]
|
|
25
42
|
}
|
|
26
43
|
],
|
|
27
44
|
"personaPacks": [
|
|
@@ -32,37 +49,38 @@
|
|
|
32
49
|
{
|
|
33
50
|
"type": "fitness.todaySummary",
|
|
34
51
|
"description": "Daily movement summary with progress, steps, and sleep context.",
|
|
35
|
-
"targets": [
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"targets": ["ios", "android", "web"]
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
"type": "fitness.dailyPlanCard",
|
|
44
|
-
"description": "Goal-aware daily plan generated from approved health context.",
|
|
45
|
-
"targets": ["ios", "android", "web"]
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
"type": "fitness.goalEditorCard",
|
|
49
|
-
"description": "Lets the user select one active fitness focus from approved Oomi goals.",
|
|
50
|
-
"targets": ["ios", "android", "web"]
|
|
52
|
+
"targets": [
|
|
53
|
+
"ios",
|
|
54
|
+
"android",
|
|
55
|
+
"web"
|
|
56
|
+
]
|
|
51
57
|
},
|
|
52
58
|
{
|
|
53
59
|
"type": "fitness.goalChecklist",
|
|
54
60
|
"description": "Checklist of suggested goals for today.",
|
|
55
|
-
"targets": [
|
|
61
|
+
"targets": [
|
|
62
|
+
"ios",
|
|
63
|
+
"android",
|
|
64
|
+
"web"
|
|
65
|
+
]
|
|
56
66
|
},
|
|
57
67
|
{
|
|
58
68
|
"type": "fitness.metricGrid",
|
|
59
69
|
"description": "Grid of approved HealthKit or Health Connect activity, sleep, cardio, and body metrics.",
|
|
60
|
-
"targets": [
|
|
70
|
+
"targets": [
|
|
71
|
+
"ios",
|
|
72
|
+
"android",
|
|
73
|
+
"web"
|
|
74
|
+
]
|
|
61
75
|
},
|
|
62
76
|
{
|
|
63
77
|
"type": "fitness.workoutList",
|
|
64
78
|
"description": "Recent workouts synced from the user's approved mobile health source.",
|
|
65
|
-
"targets": [
|
|
79
|
+
"targets": [
|
|
80
|
+
"ios",
|
|
81
|
+
"android",
|
|
82
|
+
"web"
|
|
83
|
+
]
|
|
66
84
|
}
|
|
67
85
|
],
|
|
68
86
|
"actions": [
|
|
@@ -71,9 +89,13 @@
|
|
|
71
89
|
"description": "Marks one fitness goal complete.",
|
|
72
90
|
"payloadSchema": {
|
|
73
91
|
"type": "object",
|
|
74
|
-
"required": [
|
|
92
|
+
"required": [
|
|
93
|
+
"goalId"
|
|
94
|
+
],
|
|
75
95
|
"properties": {
|
|
76
|
-
"goalId": {
|
|
96
|
+
"goalId": {
|
|
97
|
+
"type": "string"
|
|
98
|
+
}
|
|
77
99
|
}
|
|
78
100
|
}
|
|
79
101
|
},
|
|
@@ -82,22 +104,13 @@
|
|
|
82
104
|
"description": "Records a completed workout and updates today's movement state.",
|
|
83
105
|
"payloadSchema": {
|
|
84
106
|
"type": "object",
|
|
85
|
-
"properties": {
|
|
86
|
-
"goalId": { "type": "string" },
|
|
87
|
-
"minutes": { "type": "integer", "minimum": 1 }
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
"id": "fitness.set_goal",
|
|
93
|
-
"description": "Sets the user's active Fitness focus so Oomi can tune the mini-app and future nudges.",
|
|
94
|
-
"payloadSchema": {
|
|
95
|
-
"type": "object",
|
|
96
|
-
"required": ["goalId"],
|
|
97
107
|
"properties": {
|
|
98
108
|
"goalId": {
|
|
99
|
-
"type": "string"
|
|
100
|
-
|
|
109
|
+
"type": "string"
|
|
110
|
+
},
|
|
111
|
+
"minutes": {
|
|
112
|
+
"type": "integer",
|
|
113
|
+
"minimum": 1
|
|
101
114
|
}
|
|
102
115
|
}
|
|
103
116
|
}
|
|
@@ -109,6 +122,46 @@
|
|
|
109
122
|
"healthkit.read",
|
|
110
123
|
"health_connect.read"
|
|
111
124
|
]
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"personaType": "finance",
|
|
128
|
+
"displayName": "Finance",
|
|
129
|
+
"components": [
|
|
130
|
+
{
|
|
131
|
+
"type": "finance.accountList",
|
|
132
|
+
"description": "Linked depository and credit accounts with current/available balances.",
|
|
133
|
+
"targets": [
|
|
134
|
+
"web"
|
|
135
|
+
]
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"type": "finance.dataSource",
|
|
139
|
+
"description": "Plaid sandbox link status for the Finance persona-app.",
|
|
140
|
+
"targets": [
|
|
141
|
+
"web"
|
|
142
|
+
]
|
|
143
|
+
}
|
|
144
|
+
],
|
|
145
|
+
"actions": [
|
|
146
|
+
{
|
|
147
|
+
"id": "finance.sync_balances",
|
|
148
|
+
"description": "Refresh balances for linked depository and credit accounts.",
|
|
149
|
+
"payloadSchema": {
|
|
150
|
+
"type": "object"
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"id": "finance.reconnect",
|
|
155
|
+
"description": "Refresh Finance persona-app state after a Plaid update-mode reconnect.",
|
|
156
|
+
"payloadSchema": {
|
|
157
|
+
"type": "object"
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
],
|
|
161
|
+
"permissions": [
|
|
162
|
+
"finance.balances.read",
|
|
163
|
+
"plaid.link"
|
|
164
|
+
]
|
|
112
165
|
}
|
|
113
166
|
]
|
|
114
167
|
}
|
package/skills/oomi/SKILL.md
CHANGED
|
@@ -11,44 +11,25 @@ Use this skill when you need to:
|
|
|
11
11
|
- inspect managed chat or voice health
|
|
12
12
|
- inspect or update component-composed Oomi persona app state
|
|
13
13
|
- read approved Oomi backend context such as HealthKit summaries synced by the mobile app
|
|
14
|
+
- read permissioned Finance balances (no tokens, routing, or transactions)
|
|
14
15
|
- control the Oomi avatar with inline tags
|
|
15
16
|
|
|
16
17
|
## Primary Operator Workflow
|
|
17
18
|
|
|
18
19
|
If the user wants OpenClaw connected to Oomi:
|
|
19
20
|
|
|
20
|
-
1.
|
|
21
|
+
1. For a first-time install or missing local pairing state, run the bootstrap exactly once using the requested app URL:
|
|
21
22
|
```bash
|
|
22
|
-
|
|
23
|
-
```
|
|
24
|
-
Fallback:
|
|
25
|
-
```bash
|
|
26
|
-
npm install -g oomi-ai@latest
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
2. Pair the device:
|
|
30
|
-
```bash
|
|
31
|
-
oomi openclaw pair --app-url https://www.oomi.ai --no-start
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
3. Install the plugin:
|
|
35
|
-
```bash
|
|
36
|
-
openclaw plugins install oomi-ai@latest
|
|
23
|
+
curl -fsSL https://www.oomi.ai/install.sh | bash
|
|
37
24
|
```
|
|
25
|
+
The bootstrap installs or updates `oomi-ai`, performs the one initial pair, installs the plugin, and starts the saved bridge detached. Do not pair again after it finishes.
|
|
38
26
|
|
|
39
|
-
|
|
27
|
+
2. Print config guidance and apply the `channels.oomi.accounts.default` config:
|
|
40
28
|
```bash
|
|
41
29
|
oomi openclaw plugin --show-secrets --backend-url https://api.oomi.ai
|
|
42
30
|
```
|
|
43
31
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
6. Refresh the running bridge after package updates:
|
|
47
|
-
```bash
|
|
48
|
-
oomi openclaw refresh --skip-version-check
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
7. Start or repair the bridge:
|
|
32
|
+
3. Restart OpenClaw, then ensure the saved bridge is running:
|
|
52
33
|
```bash
|
|
53
34
|
oomi openclaw bridge ensure --detach
|
|
54
35
|
```
|
|
@@ -61,6 +42,20 @@ On macOS, prefer supervised mode:
|
|
|
61
42
|
oomi openclaw bridge service install
|
|
62
43
|
```
|
|
63
44
|
|
|
45
|
+
4. If the Auth Link printed by bootstrap is missing or expired, create a replacement without re-pairing:
|
|
46
|
+
```bash
|
|
47
|
+
oomi openclaw invite --app-url https://www.oomi.ai
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
5. If the machine is already paired, do not rerun bootstrap. Refresh the installed package and plugin without changing pairing state, then use `invite`, `bridge ensure`, or `bridge restart` as needed:
|
|
51
|
+
```bash
|
|
52
|
+
pnpm add -g oomi-ai@latest
|
|
53
|
+
openclaw plugins install oomi-ai@latest
|
|
54
|
+
oomi openclaw refresh --skip-version-check
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
If the user provides a specific app URL, substitute it in both the bootstrap URL and the `invite --app-url` command.
|
|
58
|
+
|
|
64
59
|
## Health Checks
|
|
65
60
|
|
|
66
61
|
Use these when chat or voice is failing:
|
|
@@ -101,25 +96,18 @@ Interpret bridge states like this:
|
|
|
101
96
|
Component-composed persona apps are the default Oomi mini-app path. Use these commands when the user asks about or updates an existing native/web Oomi persona app such as Fitness Today:
|
|
102
97
|
|
|
103
98
|
```bash
|
|
104
|
-
oomi fitness show --json
|
|
105
99
|
oomi persona-apps list --json
|
|
106
100
|
oomi persona-apps show fitness-today --json
|
|
107
101
|
oomi persona-apps apply-action fitness-today --action fitness.complete_workout --payload-json '{"goalId":"easy-run","minutes":20}' --json
|
|
108
102
|
```
|
|
109
103
|
|
|
110
104
|
Rules:
|
|
111
|
-
- prefer `oomi fitness show --json` for Fitness questions because it combines mini-app state and approved health context
|
|
112
105
|
- answer from returned `appState.bindings`, not from memory
|
|
113
106
|
- apply updates through approved `persona-apps apply-action` actions
|
|
114
107
|
- if the persona app does not exist, ask the user to add it from the Oomi client first so permissions and hydration can run
|
|
115
108
|
- do not create local persona UI projects from the OpenClaw machine
|
|
116
109
|
- do not execute persona UI jobs or start local persona app runtimes
|
|
117
110
|
|
|
118
|
-
Supported Fitness actions include:
|
|
119
|
-
- `fitness.set_goal` with `{"goalId":"move_more"}`, `{"goalId":"sleep_better"}`, or `{"goalId":"build_workout_habit"}`
|
|
120
|
-
- `fitness.complete_goal`
|
|
121
|
-
- `fitness.complete_workout`
|
|
122
|
-
|
|
123
111
|
## Permissioned Context Tools
|
|
124
112
|
|
|
125
113
|
Use context commands before answering personal data questions that should come from Oomi-approved permissions.
|
|
@@ -139,6 +127,21 @@ Rules:
|
|
|
139
127
|
- never present stale HealthKit values as current values
|
|
140
128
|
- do not ask HealthKit directly; the Oomi mobile app syncs approved HealthKit data to the backend
|
|
141
129
|
|
|
130
|
+
## Phone Notifications
|
|
131
|
+
|
|
132
|
+
To reach the user on their phone when they are not in the conversation:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
oomi notify "Your 7-day plan is ready. Want to look now?" --title "Fitness" --json
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Rules:
|
|
139
|
+
- notify only for things worth interrupting the user for (a reminder they asked for, a finished task, something time-sensitive); say everything else in chat
|
|
140
|
+
- keep it short and specific; the title defaults to your agent name
|
|
141
|
+
- tapping it opens the Oomi app to the conversation, so follow up in chat
|
|
142
|
+
- if `notification.status` is `skipped`, tell the user `notification.guidance` instead of retrying
|
|
143
|
+
- if rate limited, wait `retryAfterSeconds` before trying again
|
|
144
|
+
|
|
142
145
|
## Hidden Speech Payload
|
|
143
146
|
|
|
144
147
|
Managed voice can carry a hidden TTS-only speech sidecar alongside the normal assistant message.
|
|
@@ -199,3 +202,7 @@ Supported inline tags include:
|
|
|
199
202
|
- expressions: `[face:happy]`, `[face:sad]`, `[face:surprised]`, `[face:focused]`, `[face:gentle]`, `[face:thinking]`
|
|
200
203
|
- gestures: `[gesture:nod]`, `[gesture:think]`, `[gesture:shrug]`, `[gesture:wave]`, `[gesture:bow]`
|
|
201
204
|
- gaze: `[look:camera]`, `[look:left]`, `[look:right]`, `[look:up]`, `[look:down]`
|
|
205
|
+
|
|
206
|
+
## Permissioned Finance Context
|
|
207
|
+
|
|
208
|
+
Finance is a specialist persona-app on signed-in web. It is not a connected-chat tool. Do not run `oomi context finance`. There is no `finance.context.read` contextTool until Gate A. If the user asks about bank balances in live chat, send them to the web Finance persona-app. Do not answer from bank data.
|
|
@@ -15,26 +15,23 @@ 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 or chat:
|
|
17
17
|
|
|
18
|
-
1.
|
|
18
|
+
1. For a first-time install or missing local pairing state, run bootstrap exactly once using the requested app URL:
|
|
19
19
|
- `curl -fsSL https://www.oomi.ai/install.sh | bash`
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
|
|
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:
|
|
20
|
+
- Bootstrap installs or updates `oomi-ai`, performs the one initial pair, installs the plugin, and starts the saved bridge detached.
|
|
21
|
+
- Do not pair again after bootstrap.
|
|
22
|
+
2. Configure channel account (`channels.oomi.accounts.default`) from:
|
|
27
23
|
- `oomi openclaw plugin --show-secrets --backend-url https://api.oomi.ai`
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
3. Restart OpenClaw.
|
|
25
|
+
4. Ensure exactly one bridge worker is running (singleton handler):
|
|
30
26
|
- `oomi openclaw refresh --skip-version-check`
|
|
31
27
|
- `oomi openclaw bridge ensure --detach`
|
|
32
28
|
- If stale/disconnected: `oomi openclaw bridge restart --detach`
|
|
33
29
|
- On macOS, prefer supervised mode: `oomi openclaw bridge service install`
|
|
34
|
-
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
5. If the Auth Link printed by bootstrap is missing or expired, create a replacement without re-pairing:
|
|
31
|
+
- `oomi openclaw invite --app-url https://www.oomi.ai`
|
|
32
|
+
6. If the user provides an app URL, substitute it in both the bootstrap URL and the `invite --app-url` command.
|
|
33
|
+
7. If the machine is already paired, do not rerun bootstrap. Use `invite` for link recovery and `bridge ensure` or `bridge restart` for runtime recovery.
|
|
34
|
+
8. Return this exact result format to the user:
|
|
38
35
|
- `Oomi Connect Ready`
|
|
39
36
|
- `Auth Link: ...`
|
|
40
37
|
- No extra narrative text before or after those lines.
|
|
@@ -87,24 +84,17 @@ Oomi persona apps are component-composed client surfaces backed by approved temp
|
|
|
87
84
|
Persona UI creation and rendering happen inside Oomi-managed systems. Use the Oomi API tools instead:
|
|
88
85
|
|
|
89
86
|
```bash
|
|
90
|
-
oomi fitness show --json
|
|
91
87
|
oomi persona-apps list --json
|
|
92
88
|
oomi persona-apps show fitness-today --json
|
|
93
89
|
oomi persona-apps apply-action fitness-today --action fitness.complete_workout --payload-json '{"goalId":"easy-run","minutes":20}' --json
|
|
94
90
|
```
|
|
95
91
|
|
|
96
92
|
Rules:
|
|
97
|
-
- for Fitness questions, prefer `oomi fitness show --json` first because it combines mini-app state and approved health context
|
|
98
93
|
- answer from returned `appState.bindings`, not memory
|
|
99
94
|
- apply updates only through approved `persona-apps apply-action` actions
|
|
100
95
|
- if the persona app is missing, ask the user to add it from the Oomi client first so permissions and hydration can run
|
|
101
96
|
- do not execute persona UI jobs or start local persona app runtimes
|
|
102
97
|
|
|
103
|
-
Supported Fitness actions include:
|
|
104
|
-
- `fitness.set_goal` with `{"goalId":"move_more"}`, `{"goalId":"sleep_better"}`, or `{"goalId":"build_workout_habit"}`
|
|
105
|
-
- `fitness.complete_goal`
|
|
106
|
-
- `fitness.complete_workout`
|
|
107
|
-
|
|
108
98
|
## Permissioned Context Tools
|
|
109
99
|
|
|
110
100
|
Use Oomi context tools when the user asks about personal data that should come from mobile permissions instead of memory.
|