oomi-ai 0.3.4 → 0.3.6
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 +29 -0
- package/agent_instructions.md +11 -14
- package/bin/oomi-ai.js +108 -59
- package/lib/modelRelayClient.js +150 -0
- package/lib/openclawProfile.js +155 -1
- package/lib/personaApiClient.js +9 -0
- package/package.json +2 -1
- package/persona-app/README.md +2 -0
- package/persona-app/registry/v1.json +94 -12
- package/skills/oomi/SKILL.md +24 -25
- package/skills/oomi/agent_instructions.md +11 -14
package/README.md
CHANGED
|
@@ -145,6 +145,35 @@ 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:
|
package/agent_instructions.md
CHANGED
|
@@ -19,27 +19,24 @@ Get OpenClaw into a state where:
|
|
|
19
19
|
|
|
20
20
|
If the user asks to connect OpenClaw to Oomi chat or voice:
|
|
21
21
|
|
|
22
|
-
1.
|
|
22
|
+
1. For a first-time install or missing local pairing state, run the bootstrap exactly once using the requested app URL:
|
|
23
23
|
- `curl -fsSL https://www.oomi.ai/install.sh | bash`
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
3. Pair the device without starting a legacy one-off flow:
|
|
28
|
-
- `oomi openclaw pair --app-url https://www.oomi.ai --no-start`
|
|
29
|
-
4. Install the OpenClaw plugin:
|
|
30
|
-
- `openclaw plugins install oomi-ai@latest`
|
|
31
|
-
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:
|
|
32
27
|
- `oomi openclaw plugin --show-secrets --backend-url https://api.oomi.ai`
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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:
|
|
36
31
|
- `oomi openclaw bridge ensure --detach`
|
|
37
32
|
- if stale: `oomi openclaw bridge restart --detach`
|
|
38
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`
|
|
39
36
|
|
|
40
|
-
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.
|
|
41
38
|
|
|
42
|
-
|
|
39
|
+
If the user provides a specific app URL, substitute it in both the bootstrap URL and the `invite --app-url` command.
|
|
43
40
|
|
|
44
41
|
## Required User-Facing Result
|
|
45
42
|
|
package/bin/oomi-ai.js
CHANGED
|
@@ -379,18 +379,6 @@ function findRepoRoot(startDir) {
|
|
|
379
379
|
return null;
|
|
380
380
|
}
|
|
381
381
|
|
|
382
|
-
function resolveRepoRoot(rootFlag) {
|
|
383
|
-
const explicitRoot =
|
|
384
|
-
typeof rootFlag === 'string' && rootFlag.trim()
|
|
385
|
-
? path.resolve(rootFlag.trim())
|
|
386
|
-
: '';
|
|
387
|
-
const repoRoot = explicitRoot || findRepoRoot(process.cwd()) || findRepoRoot(PACKAGE_ROOT);
|
|
388
|
-
if (!repoRoot) {
|
|
389
|
-
throw new Error('Could not locate repo root. Use --root <repo root>.');
|
|
390
|
-
}
|
|
391
|
-
return repoRoot;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
382
|
function resolveSkillSource(cliRoot) {
|
|
395
383
|
const packaged = path.join(PACKAGE_ROOT, 'skills', 'oomi');
|
|
396
384
|
if (fs.existsSync(packaged)) {
|
|
@@ -584,6 +572,10 @@ async function handleContextHealthCommand(flags = {}) {
|
|
|
584
572
|
printStructuredResult(payload, isTruthyFlag(flags.json));
|
|
585
573
|
}
|
|
586
574
|
|
|
575
|
+
async function handleContextFinanceCommand(_flags = {}) {
|
|
576
|
+
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.");
|
|
577
|
+
}
|
|
578
|
+
|
|
587
579
|
async function handleContextHealthSyncCommand(flags = {}) {
|
|
588
580
|
const client = createCliPersonaApiClient(flags);
|
|
589
581
|
const payload = await client.requestHealthContextSync({
|
|
@@ -741,6 +733,28 @@ function readOpenclawGatewayConfig() {
|
|
|
741
733
|
};
|
|
742
734
|
}
|
|
743
735
|
|
|
736
|
+
function readOpenclawDefaultSessionKey() {
|
|
737
|
+
try {
|
|
738
|
+
const configPath = resolveOpenclawConfigPath();
|
|
739
|
+
if (!configPath || !fs.existsSync(configPath)) {
|
|
740
|
+
return '';
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const parsed = JSON.parse(readFile(configPath));
|
|
744
|
+
const channels = parsed.channels || {};
|
|
745
|
+
const oomi = channels.oomi || {};
|
|
746
|
+
const accounts = oomi.accounts || {};
|
|
747
|
+
const defaultAccount = accounts.default || {};
|
|
748
|
+
const defaultSessionKey = typeof defaultAccount.defaultSessionKey === 'string'
|
|
749
|
+
? defaultAccount.defaultSessionKey.trim()
|
|
750
|
+
: '';
|
|
751
|
+
|
|
752
|
+
return defaultSessionKey;
|
|
753
|
+
} catch {
|
|
754
|
+
return '';
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
744
758
|
function resolveBridgeStatePath() {
|
|
745
759
|
return resolveOpenclawBridgeStatePath();
|
|
746
760
|
}
|
|
@@ -1049,6 +1063,25 @@ async function requestManagedPairCode({ appUrl, label }) {
|
|
|
1049
1063
|
return payload;
|
|
1050
1064
|
}
|
|
1051
1065
|
|
|
1066
|
+
async function requestBrokerPairCode({ brokerHttp, adminKey, label }) {
|
|
1067
|
+
const response = await fetch(`${brokerHttp.replace(/\/$/, '')}/v1/pair/start`, {
|
|
1068
|
+
method: 'POST',
|
|
1069
|
+
headers: {
|
|
1070
|
+
'Content-Type': 'application/json',
|
|
1071
|
+
Authorization: `Bearer ${adminKey}`,
|
|
1072
|
+
},
|
|
1073
|
+
body: JSON.stringify({ label }),
|
|
1074
|
+
});
|
|
1075
|
+
const payload = await response.json().catch(() => ({}));
|
|
1076
|
+
if (!response.ok || !payload?.pairCode) {
|
|
1077
|
+
const message =
|
|
1078
|
+
(payload && typeof payload.error === 'string' && payload.error) ||
|
|
1079
|
+
`Broker pair start failed (${response.status})`;
|
|
1080
|
+
throw new Error(message);
|
|
1081
|
+
}
|
|
1082
|
+
return payload;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1052
1085
|
async function requestConnectInviteLink({ backendHttp, appUrl, sessionKey, deviceToken }) {
|
|
1053
1086
|
const response = await fetch(`${backendHttp.replace(/\/$/, '')}/v1/invite_links/start`, {
|
|
1054
1087
|
method: 'POST',
|
|
@@ -1084,6 +1117,35 @@ async function fetchManagedGatewayConfig({ appUrl }) {
|
|
|
1084
1117
|
return payload;
|
|
1085
1118
|
}
|
|
1086
1119
|
|
|
1120
|
+
function shouldRewriteLoopbackBrokerForAppHost(appHostname) {
|
|
1121
|
+
return String(appHostname || '').trim().toLowerCase() === 'host.docker.internal';
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
function isLoopbackBrokerHostname(hostname) {
|
|
1125
|
+
const normalized = String(hostname || '').trim().toLowerCase();
|
|
1126
|
+
return normalized === '127.0.0.1' || normalized === 'localhost' || normalized === '::1';
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function rewriteLoopbackBrokerUrlForAppHost(rawUrl, appUrl) {
|
|
1130
|
+
const value = String(rawUrl || '').trim();
|
|
1131
|
+
if (!value) return value;
|
|
1132
|
+
|
|
1133
|
+
try {
|
|
1134
|
+
const brokerUrl = new URL(value);
|
|
1135
|
+
const app = new URL(String(appUrl || '').trim());
|
|
1136
|
+
if (!isLoopbackBrokerHostname(brokerUrl.hostname)) {
|
|
1137
|
+
return value;
|
|
1138
|
+
}
|
|
1139
|
+
if (!shouldRewriteLoopbackBrokerForAppHost(app.hostname)) {
|
|
1140
|
+
return value;
|
|
1141
|
+
}
|
|
1142
|
+
brokerUrl.hostname = app.hostname;
|
|
1143
|
+
return brokerUrl.toString();
|
|
1144
|
+
} catch {
|
|
1145
|
+
return value;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1087
1149
|
function base64UrlEncode(value) {
|
|
1088
1150
|
return Buffer.from(value)
|
|
1089
1151
|
.toString('base64')
|
|
@@ -1742,48 +1804,6 @@ function printAssistantFinalDebugResult(result, asJson) {
|
|
|
1742
1804
|
}
|
|
1743
1805
|
}
|
|
1744
1806
|
|
|
1745
|
-
function resolveCommandFromPath(commandName) {
|
|
1746
|
-
const normalized = String(commandName || '').trim();
|
|
1747
|
-
if (!normalized) return '';
|
|
1748
|
-
try {
|
|
1749
|
-
const probe = spawnSync(process.platform === 'win32' ? 'where' : 'which', [normalized], {
|
|
1750
|
-
encoding: 'utf8',
|
|
1751
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1752
|
-
});
|
|
1753
|
-
if (probe.status !== 0) return '';
|
|
1754
|
-
const firstLine = String(probe.stdout || '')
|
|
1755
|
-
.split(/\r?\n/)
|
|
1756
|
-
.map((line) => line.trim())
|
|
1757
|
-
.find(Boolean);
|
|
1758
|
-
return firstLine || '';
|
|
1759
|
-
} catch {
|
|
1760
|
-
return '';
|
|
1761
|
-
}
|
|
1762
|
-
}
|
|
1763
|
-
|
|
1764
|
-
function resolveExecutable(candidates = []) {
|
|
1765
|
-
for (const candidate of candidates) {
|
|
1766
|
-
if (!candidate) continue;
|
|
1767
|
-
const value = String(candidate).trim();
|
|
1768
|
-
if (!value) continue;
|
|
1769
|
-
if (path.isAbsolute(value) && fs.existsSync(value)) {
|
|
1770
|
-
return value;
|
|
1771
|
-
}
|
|
1772
|
-
if (value.includes(path.sep) || value.includes('/')) {
|
|
1773
|
-
const resolved = path.resolve(value);
|
|
1774
|
-
if (fs.existsSync(resolved)) {
|
|
1775
|
-
return resolved;
|
|
1776
|
-
}
|
|
1777
|
-
continue;
|
|
1778
|
-
}
|
|
1779
|
-
const fromPath = resolveCommandFromPath(value);
|
|
1780
|
-
if (fromPath) {
|
|
1781
|
-
return fromPath;
|
|
1782
|
-
}
|
|
1783
|
-
}
|
|
1784
|
-
return '';
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
1807
|
async function handleOpenclawDebugCommand(action, flags) {
|
|
1788
1808
|
const normalizedAction = String(action || '').trim().toLowerCase();
|
|
1789
1809
|
const frameFile =
|
|
@@ -2527,11 +2547,13 @@ async function startOpenclawBridge(flags) {
|
|
|
2527
2547
|
if (!deviceToken) {
|
|
2528
2548
|
throw new Error('Broker pair claim did not return deviceToken.');
|
|
2529
2549
|
}
|
|
2550
|
+
const configSessionKey = readOpenclawDefaultSessionKey();
|
|
2530
2551
|
writeBridgeState({
|
|
2531
2552
|
brokerHttp,
|
|
2532
2553
|
brokerWs,
|
|
2533
2554
|
deviceId,
|
|
2534
2555
|
deviceToken,
|
|
2556
|
+
sessionKey: configSessionKey || 'agent:main:webchat:channel:oomi',
|
|
2535
2557
|
claimedAt: new Date().toISOString(),
|
|
2536
2558
|
expiresAt: claimed.expiresAt || null,
|
|
2537
2559
|
});
|
|
@@ -3806,7 +3828,24 @@ async function pairAndStartOpenclawBridge(flags) {
|
|
|
3806
3828
|
|
|
3807
3829
|
console.log(`Pairing OpenClaw host with Oomi app: ${appUrl}`);
|
|
3808
3830
|
const managedConfig = await fetchManagedGatewayConfig({ appUrl });
|
|
3809
|
-
const
|
|
3831
|
+
const brokerHttp = rewriteLoopbackBrokerUrlForAppHost(managedConfig.brokerHttpUrl, appUrl);
|
|
3832
|
+
let pairStarted;
|
|
3833
|
+
try {
|
|
3834
|
+
pairStarted = await requestManagedPairCode({ appUrl, label });
|
|
3835
|
+
} catch (err) {
|
|
3836
|
+
const brokerAdminKey = String(process.env.OOMI_CHAT_BROKER_ADMIN_KEY || '').trim();
|
|
3837
|
+
if (!brokerAdminKey) {
|
|
3838
|
+
throw err;
|
|
3839
|
+
}
|
|
3840
|
+
console.warn(
|
|
3841
|
+
`[oomi] Managed pair start via app failed (${extractErrorMessage(err)}); falling back to direct broker pair/start.`
|
|
3842
|
+
);
|
|
3843
|
+
pairStarted = await requestBrokerPairCode({
|
|
3844
|
+
brokerHttp,
|
|
3845
|
+
adminKey: brokerAdminKey,
|
|
3846
|
+
label,
|
|
3847
|
+
});
|
|
3848
|
+
}
|
|
3810
3849
|
const pairCode = String(pairStarted.pairCode || '').trim().toUpperCase();
|
|
3811
3850
|
if (!pairCode) {
|
|
3812
3851
|
throw new Error('Pairing code missing from backend response.');
|
|
@@ -3814,7 +3853,7 @@ async function pairAndStartOpenclawBridge(flags) {
|
|
|
3814
3853
|
|
|
3815
3854
|
console.log(`Received pair code: ${pairCode}`);
|
|
3816
3855
|
const claimed = await claimBridgeDeviceToken({
|
|
3817
|
-
brokerHttp
|
|
3856
|
+
brokerHttp,
|
|
3818
3857
|
pairCode,
|
|
3819
3858
|
deviceId,
|
|
3820
3859
|
});
|
|
@@ -3823,9 +3862,11 @@ async function pairAndStartOpenclawBridge(flags) {
|
|
|
3823
3862
|
throw new Error('Broker pair claim did not return deviceToken.');
|
|
3824
3863
|
}
|
|
3825
3864
|
|
|
3826
|
-
const brokerWs = String(
|
|
3865
|
+
const brokerWs = String(
|
|
3866
|
+
flags['broker-ws'] || rewriteLoopbackBrokerUrlForAppHost(managedConfig.brokerDeviceWsUrl, appUrl)
|
|
3867
|
+
).trim();
|
|
3827
3868
|
writeBridgeState({
|
|
3828
|
-
brokerHttp
|
|
3869
|
+
brokerHttp,
|
|
3829
3870
|
brokerWs,
|
|
3830
3871
|
deviceId,
|
|
3831
3872
|
deviceToken,
|
|
@@ -3835,7 +3876,7 @@ async function pairAndStartOpenclawBridge(flags) {
|
|
|
3835
3876
|
});
|
|
3836
3877
|
|
|
3837
3878
|
const invite = await requestConnectInviteLink({
|
|
3838
|
-
backendHttp:
|
|
3879
|
+
backendHttp: brokerHttp,
|
|
3839
3880
|
appUrl,
|
|
3840
3881
|
sessionKey,
|
|
3841
3882
|
deviceToken,
|
|
@@ -3909,9 +3950,11 @@ async function createOpenclawInviteLink(flags) {
|
|
|
3909
3950
|
''
|
|
3910
3951
|
).trim();
|
|
3911
3952
|
const appUrl = String(flags['app-url'] || process.env.OOMI_APP_URL || 'http://127.0.0.1:3456').trim();
|
|
3953
|
+
const configSessionKey = readOpenclawDefaultSessionKey();
|
|
3912
3954
|
const sessionKey = String(
|
|
3913
3955
|
flags['session-key'] ||
|
|
3914
3956
|
process.env.OOMI_SESSION_KEY ||
|
|
3957
|
+
configSessionKey ||
|
|
3915
3958
|
bridgeState.sessionKey ||
|
|
3916
3959
|
'agent:main:webchat:channel:oomi'
|
|
3917
3960
|
).trim();
|
|
@@ -4381,6 +4424,11 @@ async function main() {
|
|
|
4381
4424
|
return;
|
|
4382
4425
|
}
|
|
4383
4426
|
|
|
4427
|
+
if (command === 'context' && subcommand === 'finance') {
|
|
4428
|
+
await handleContextFinanceCommand(args.flags);
|
|
4429
|
+
return;
|
|
4430
|
+
}
|
|
4431
|
+
|
|
4384
4432
|
console.error(`Unknown command: ${command} ${subcommand || ''}`.trim());
|
|
4385
4433
|
usage();
|
|
4386
4434
|
process.exit(1);
|
|
@@ -4421,4 +4469,5 @@ export {
|
|
|
4421
4469
|
isGatewayRunStartedFrame,
|
|
4422
4470
|
isBridgeWorkerCommand,
|
|
4423
4471
|
parsePositiveInteger,
|
|
4472
|
+
rewriteLoopbackBrokerUrlForAppHost,
|
|
4424
4473
|
};
|
|
@@ -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
|
} = {}) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oomi-ai",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
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
|
@@ -39,3 +39,5 @@ oomi context health sync --wait --json
|
|
|
39
39
|
Answer only after fresh context is uploaded. If the command times out, tell the user the phone sync is still pending.
|
|
40
40
|
|
|
41
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.
|
|
@@ -7,21 +7,38 @@
|
|
|
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,22 +49,38 @@
|
|
|
32
49
|
{
|
|
33
50
|
"type": "fitness.todaySummary",
|
|
34
51
|
"description": "Daily movement summary with progress, steps, and sleep context.",
|
|
35
|
-
"targets": [
|
|
52
|
+
"targets": [
|
|
53
|
+
"ios",
|
|
54
|
+
"android",
|
|
55
|
+
"web"
|
|
56
|
+
]
|
|
36
57
|
},
|
|
37
58
|
{
|
|
38
59
|
"type": "fitness.goalChecklist",
|
|
39
60
|
"description": "Checklist of suggested goals for today.",
|
|
40
|
-
"targets": [
|
|
61
|
+
"targets": [
|
|
62
|
+
"ios",
|
|
63
|
+
"android",
|
|
64
|
+
"web"
|
|
65
|
+
]
|
|
41
66
|
},
|
|
42
67
|
{
|
|
43
68
|
"type": "fitness.metricGrid",
|
|
44
69
|
"description": "Grid of approved HealthKit or Health Connect activity, sleep, cardio, and body metrics.",
|
|
45
|
-
"targets": [
|
|
70
|
+
"targets": [
|
|
71
|
+
"ios",
|
|
72
|
+
"android",
|
|
73
|
+
"web"
|
|
74
|
+
]
|
|
46
75
|
},
|
|
47
76
|
{
|
|
48
77
|
"type": "fitness.workoutList",
|
|
49
78
|
"description": "Recent workouts synced from the user's approved mobile health source.",
|
|
50
|
-
"targets": [
|
|
79
|
+
"targets": [
|
|
80
|
+
"ios",
|
|
81
|
+
"android",
|
|
82
|
+
"web"
|
|
83
|
+
]
|
|
51
84
|
}
|
|
52
85
|
],
|
|
53
86
|
"actions": [
|
|
@@ -56,9 +89,13 @@
|
|
|
56
89
|
"description": "Marks one fitness goal complete.",
|
|
57
90
|
"payloadSchema": {
|
|
58
91
|
"type": "object",
|
|
59
|
-
"required": [
|
|
92
|
+
"required": [
|
|
93
|
+
"goalId"
|
|
94
|
+
],
|
|
60
95
|
"properties": {
|
|
61
|
-
"goalId": {
|
|
96
|
+
"goalId": {
|
|
97
|
+
"type": "string"
|
|
98
|
+
}
|
|
62
99
|
}
|
|
63
100
|
}
|
|
64
101
|
},
|
|
@@ -68,8 +105,13 @@
|
|
|
68
105
|
"payloadSchema": {
|
|
69
106
|
"type": "object",
|
|
70
107
|
"properties": {
|
|
71
|
-
"goalId": {
|
|
72
|
-
|
|
108
|
+
"goalId": {
|
|
109
|
+
"type": "string"
|
|
110
|
+
},
|
|
111
|
+
"minutes": {
|
|
112
|
+
"type": "integer",
|
|
113
|
+
"minimum": 1
|
|
114
|
+
}
|
|
73
115
|
}
|
|
74
116
|
}
|
|
75
117
|
}
|
|
@@ -80,6 +122,46 @@
|
|
|
80
122
|
"healthkit.read",
|
|
81
123
|
"health_connect.read"
|
|
82
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
|
+
]
|
|
83
165
|
}
|
|
84
166
|
]
|
|
85
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:
|
|
@@ -192,3 +187,7 @@ Supported inline tags include:
|
|
|
192
187
|
- expressions: `[face:happy]`, `[face:sad]`, `[face:surprised]`, `[face:focused]`, `[face:gentle]`, `[face:thinking]`
|
|
193
188
|
- gestures: `[gesture:nod]`, `[gesture:think]`, `[gesture:shrug]`, `[gesture:wave]`, `[gesture:bow]`
|
|
194
189
|
- gaze: `[look:camera]`, `[look:left]`, `[look:right]`, `[look:up]`, `[look:down]`
|
|
190
|
+
|
|
191
|
+
## Permissioned Finance Context
|
|
192
|
+
|
|
193
|
+
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.
|