oomi-ai 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/agent_instructions.md +2 -1
- package/bin/oomi-ai.js +148 -0
- package/lib/personaApiClient.js +14 -0
- package/openclaw.extension.js +53 -1
- package/package.json +1 -1
- package/persona-app/README.md +8 -0
- package/skills/oomi/SKILL.md +1 -0
- package/skills/oomi/agent_instructions.md +2 -1
package/README.md
CHANGED
|
@@ -183,6 +183,7 @@ Rules for agents:
|
|
|
183
183
|
- Respect `healthContext.agentGuidance.canAnswerFromContext`.
|
|
184
184
|
- If context is `missing`, `needs_sync`, `permission_denied`, `disabled_by_user`, or `unavailable`, tell the user what repair action is needed instead of guessing.
|
|
185
185
|
- If context is `stale`, mention freshness when timing matters.
|
|
186
|
+
- To request a fresh phone-side HealthKit sync, run `oomi context health sync --json`. The iPhone app must open or refresh before it can read Apple Health and fulfill the pending request.
|
|
186
187
|
- Do not ask the phone for HealthKit directly; the Oomi mobile app syncs HealthKit to the backend.
|
|
187
188
|
|
|
188
189
|
## Voice Contract
|
package/agent_instructions.md
CHANGED
|
@@ -213,7 +213,7 @@ Rules:
|
|
|
213
213
|
- `metadata.spoken.language` should use the backend-supported language value for the active provider; use `English` only when no more specific locale is supplied
|
|
214
214
|
- `metadata.spoken.segments` can carry bounded per-segment prosody for pace, pitch, volume, and pause timing
|
|
215
215
|
- `metadata.spoken.instructions` should be natural-language guidance, not raw bracket tags
|
|
216
|
-
- visible chat text
|
|
216
|
+
- visible chat text should be clean before sending; the package may strip private runtime wrappers and avatar command tags as a safety guardrail
|
|
217
217
|
|
|
218
218
|
## Avatar Commands
|
|
219
219
|
|
|
@@ -278,5 +278,6 @@ Rules:
|
|
|
278
278
|
- Respect `healthContext.agentGuidance.canAnswerFromContext`.
|
|
279
279
|
- If `canAnswerFromContext` is false, tell the user the relevant `healthContext.repair.label` or `healthContext.repair.reason`.
|
|
280
280
|
- If `healthContext.status` is `stale`, mention that the latest HealthKit sync may be out of date when timing matters.
|
|
281
|
+
- If the user asks you to refresh stale HealthKit context, run `oomi context health sync --json`; tell the user the iPhone app must be opened for Oomi to read Apple Health and fulfill the pending sync.
|
|
281
282
|
- Do not infer unavailable health fields.
|
|
282
283
|
- Do not request HealthKit directly from the phone; the Oomi mobile app owns permission prompts and syncs approved data to the backend.
|
package/bin/oomi-ai.js
CHANGED
|
@@ -229,6 +229,8 @@ Commands:
|
|
|
229
229
|
|
|
230
230
|
context health
|
|
231
231
|
Show the latest account-linked health context available to this paired OpenClaw device.
|
|
232
|
+
context health sync
|
|
233
|
+
Request the linked Oomi mobile app to sync fresh HealthKit context.
|
|
232
234
|
|
|
233
235
|
Common flags:
|
|
234
236
|
--agents-file PATH Override AGENTS.md path
|
|
@@ -575,6 +577,14 @@ async function handleContextHealthCommand(flags = {}) {
|
|
|
575
577
|
printStructuredResult(payload, isTruthyFlag(flags.json));
|
|
576
578
|
}
|
|
577
579
|
|
|
580
|
+
async function handleContextHealthSyncCommand(flags = {}) {
|
|
581
|
+
const client = createCliPersonaApiClient(flags);
|
|
582
|
+
const payload = await client.requestHealthContextSync({
|
|
583
|
+
reason: flags.reason || 'agent_requested',
|
|
584
|
+
});
|
|
585
|
+
printStructuredResult(payload, isTruthyFlag(flags.json));
|
|
586
|
+
}
|
|
587
|
+
|
|
578
588
|
async function refreshBridgeForUpdate(flags = {}, options = {}) {
|
|
579
589
|
const logger = options.logger || null;
|
|
580
590
|
if (process.platform === 'darwin') {
|
|
@@ -1355,6 +1365,130 @@ function extractTextFromGatewayMessage(message) {
|
|
|
1355
1365
|
.join(' ');
|
|
1356
1366
|
}
|
|
1357
1367
|
|
|
1368
|
+
function normalizeWhitespace(value) {
|
|
1369
|
+
return String(value || '')
|
|
1370
|
+
.replace(/[ \t]+\n/g, '\n')
|
|
1371
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
1372
|
+
.replace(/[ \t]{2,}/g, ' ')
|
|
1373
|
+
.trim();
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
function sanitizeUserVisibleText(value) {
|
|
1377
|
+
let text = String(value || '');
|
|
1378
|
+
if (!text.trim()) return '';
|
|
1379
|
+
|
|
1380
|
+
text = text.replace(/<think>[\s\S]*?<\/think>/gi, ' ');
|
|
1381
|
+
if (/<think>/i.test(text)) {
|
|
1382
|
+
text = text.replace(/<think>[\s\S]*$/i, ' ');
|
|
1383
|
+
}
|
|
1384
|
+
text = text.replace(/<\/think>/gi, ' ');
|
|
1385
|
+
text = text.replace(/<\/?final>/gi, ' ');
|
|
1386
|
+
text = text.replace(/\[(?:anim|face|gesture|look):[^\]\n]{1,80}\]/gi, ' ');
|
|
1387
|
+
|
|
1388
|
+
text = text.replace(/(?:^|\n)System \(untrusted\):[\s\S]*?(?=\n\n|$)/gi, '\n');
|
|
1389
|
+
text = text.replace(
|
|
1390
|
+
/(?:^|\n)An async command you ran earlier has completed\.[\s\S]*?(?:\nCurrent time:[^\n]*(?:\n|$)|$)/gi,
|
|
1391
|
+
'\n'
|
|
1392
|
+
);
|
|
1393
|
+
|
|
1394
|
+
return normalizeWhitespace(text);
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
function shouldSuppressOomiVisibleText(value) {
|
|
1398
|
+
const text = String(value || '').trim();
|
|
1399
|
+
if (!text) return true;
|
|
1400
|
+
|
|
1401
|
+
if (/System \(untrusted\):/i.test(text)) return true;
|
|
1402
|
+
if (/An async command you ran earlier has completed\./i.test(text)) return true;
|
|
1403
|
+
if (/Handle the result internally\. Do not relay it to the user/i.test(text)) return true;
|
|
1404
|
+
if (/Exec (failed|completed) \([^)]+\) ::/i.test(text)) return true;
|
|
1405
|
+
if (/^\s*<think>[\s\S]*$/i.test(text) && !/<\/think>/i.test(text)) return true;
|
|
1406
|
+
if (/^NO_REPLY$/i.test(text)) return true;
|
|
1407
|
+
|
|
1408
|
+
return !sanitizeUserVisibleText(text);
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
function sanitizeGatewayMessageForOomi(message) {
|
|
1412
|
+
if (!message || typeof message !== 'object') return null;
|
|
1413
|
+
|
|
1414
|
+
const role = typeof message.role === 'string' ? message.role.trim().toLowerCase() : '';
|
|
1415
|
+
if (role && !['assistant', 'user'].includes(role)) {
|
|
1416
|
+
return null;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
const rawText = extractTextFromGatewayMessage(message);
|
|
1420
|
+
if (shouldSuppressOomiVisibleText(rawText)) {
|
|
1421
|
+
return null;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
const content = sanitizeUserVisibleText(rawText);
|
|
1425
|
+
if (!content) return null;
|
|
1426
|
+
|
|
1427
|
+
return {
|
|
1428
|
+
...message,
|
|
1429
|
+
role: role || 'assistant',
|
|
1430
|
+
content,
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
function sanitizeGatewayFrameForOomiClient(frameText) {
|
|
1435
|
+
const frame = parseJsonPayload(frameText);
|
|
1436
|
+
if (!frame || typeof frame !== 'object') {
|
|
1437
|
+
return { frameText, changed: false, suppressed: false };
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
if (frame.type === 'res' && frame.payload && typeof frame.payload === 'object' && Array.isArray(frame.payload.messages)) {
|
|
1441
|
+
const nextMessages = frame.payload.messages
|
|
1442
|
+
.map((message) => sanitizeGatewayMessageForOomi(message))
|
|
1443
|
+
.filter(Boolean);
|
|
1444
|
+
const nextFrame = {
|
|
1445
|
+
...frame,
|
|
1446
|
+
payload: {
|
|
1447
|
+
...frame.payload,
|
|
1448
|
+
messages: nextMessages,
|
|
1449
|
+
},
|
|
1450
|
+
};
|
|
1451
|
+
const nextFrameText = JSON.stringify(nextFrame);
|
|
1452
|
+
return {
|
|
1453
|
+
frameText: nextFrameText,
|
|
1454
|
+
changed: nextFrameText !== frameText,
|
|
1455
|
+
suppressed: nextMessages.length < frame.payload.messages.length,
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
if (frame.type === 'event' && frame.event === 'chat') {
|
|
1460
|
+
const payload = frame.payload && typeof frame.payload === 'object' ? frame.payload : null;
|
|
1461
|
+
const message = payload?.message && typeof payload.message === 'object' ? payload.message : null;
|
|
1462
|
+
if (!payload || !message) {
|
|
1463
|
+
return { frameText, changed: false, suppressed: false };
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
const sanitizedMessage = sanitizeGatewayMessageForOomi(message);
|
|
1467
|
+
const nextFrame = {
|
|
1468
|
+
...frame,
|
|
1469
|
+
payload: {
|
|
1470
|
+
...payload,
|
|
1471
|
+
message: sanitizedMessage || {
|
|
1472
|
+
...message,
|
|
1473
|
+
content: '',
|
|
1474
|
+
metadata: {
|
|
1475
|
+
...(message.metadata && typeof message.metadata === 'object' && !Array.isArray(message.metadata) ? message.metadata : {}),
|
|
1476
|
+
oomiSuppressed: true,
|
|
1477
|
+
},
|
|
1478
|
+
},
|
|
1479
|
+
},
|
|
1480
|
+
};
|
|
1481
|
+
const nextFrameText = JSON.stringify(nextFrame);
|
|
1482
|
+
return {
|
|
1483
|
+
frameText: nextFrameText,
|
|
1484
|
+
changed: nextFrameText !== frameText,
|
|
1485
|
+
suppressed: !sanitizedMessage,
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
return { frameText, changed: false, suppressed: false };
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1358
1492
|
function summarizeVoiceFrameContract(frameText) {
|
|
1359
1493
|
const frame = parseJsonPayload(frameText);
|
|
1360
1494
|
if (!frame || typeof frame !== 'object') {
|
|
@@ -2997,6 +3131,13 @@ async function startOpenclawBridge(flags) {
|
|
|
2997
3131
|
|
|
2998
3132
|
gatewaySocket.on('message', runBridgeCallbackSafely((gatewayRaw) => {
|
|
2999
3133
|
let frame = typeof gatewayRaw === 'string' ? gatewayRaw : gatewayRaw.toString();
|
|
3134
|
+
const visibleSanitized = sanitizeGatewayFrameForOomiClient(frame);
|
|
3135
|
+
if (visibleSanitized.changed) {
|
|
3136
|
+
frame = visibleSanitized.frameText;
|
|
3137
|
+
bridgeDebugLog(
|
|
3138
|
+
`[bridge] oomi.visible_sanitized ${sessionId} suppressed=${visibleSanitized.suppressed ? 'yes' : 'no'}`
|
|
3139
|
+
);
|
|
3140
|
+
}
|
|
3000
3141
|
const spokenNormalized = normalizeAssistantGatewayFrame(sessionId, frame);
|
|
3001
3142
|
if (spokenNormalized.changed) {
|
|
3002
3143
|
frame = spokenNormalized.frameText;
|
|
@@ -4191,6 +4332,10 @@ async function main() {
|
|
|
4191
4332
|
}
|
|
4192
4333
|
|
|
4193
4334
|
if (command === 'context' && subcommand === 'health') {
|
|
4335
|
+
if (args.positionals[0] === 'sync') {
|
|
4336
|
+
await handleContextHealthSyncCommand(args.flags);
|
|
4337
|
+
return;
|
|
4338
|
+
}
|
|
4194
4339
|
await handleContextHealthCommand(args.flags);
|
|
4195
4340
|
return;
|
|
4196
4341
|
}
|
|
@@ -4213,6 +4358,9 @@ if (__isDirectExecution) {
|
|
|
4213
4358
|
|
|
4214
4359
|
export {
|
|
4215
4360
|
prepareGatewayFrameForLocalGateway,
|
|
4361
|
+
sanitizeGatewayFrameForOomiClient,
|
|
4362
|
+
sanitizeUserVisibleText,
|
|
4363
|
+
shouldSuppressOomiVisibleText,
|
|
4216
4364
|
ensureAssistantSpokenMetadata,
|
|
4217
4365
|
normalizeAssistantGatewayFrame,
|
|
4218
4366
|
runAssistantFinalDebugCheck,
|
package/lib/personaApiClient.js
CHANGED
|
@@ -148,5 +148,19 @@ export function createPersonaApiClient({
|
|
|
148
148
|
path: '/v1/context/health',
|
|
149
149
|
});
|
|
150
150
|
},
|
|
151
|
+
|
|
152
|
+
requestHealthContextSync({
|
|
153
|
+
reason = 'agent_requested',
|
|
154
|
+
} = {}) {
|
|
155
|
+
return postJson({
|
|
156
|
+
fetchImpl,
|
|
157
|
+
backendUrl: resolvedBackendUrl,
|
|
158
|
+
deviceToken: resolvedDeviceToken,
|
|
159
|
+
path: '/v1/context/health/sync',
|
|
160
|
+
body: {
|
|
161
|
+
reason: trimString(reason) || 'agent_requested',
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
},
|
|
151
165
|
};
|
|
152
166
|
}
|
package/openclaw.extension.js
CHANGED
|
@@ -107,6 +107,49 @@ function extractText(payload) {
|
|
|
107
107
|
return '';
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
function normalizeWhitespace(value) {
|
|
111
|
+
return String(value || '')
|
|
112
|
+
.replace(/[ \t]+\n/g, '\n')
|
|
113
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
114
|
+
.replace(/[ \t]{2,}/g, ' ')
|
|
115
|
+
.trim();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function sanitizeUserVisibleText(value) {
|
|
119
|
+
let text = String(value || '');
|
|
120
|
+
if (!text.trim()) return '';
|
|
121
|
+
|
|
122
|
+
text = text.replace(/<think>[\s\S]*?<\/think>/gi, ' ');
|
|
123
|
+
if (/<think>/i.test(text)) {
|
|
124
|
+
text = text.replace(/<think>[\s\S]*$/i, ' ');
|
|
125
|
+
}
|
|
126
|
+
text = text.replace(/<\/think>/gi, ' ');
|
|
127
|
+
text = text.replace(/<\/?final>/gi, ' ');
|
|
128
|
+
text = text.replace(/\[(?:anim|face|gesture|look):[^\]\n]{1,80}\]/gi, ' ');
|
|
129
|
+
|
|
130
|
+
text = text.replace(/(?:^|\n)System \(untrusted\):[\s\S]*?(?=\n\n|$)/gi, '\n');
|
|
131
|
+
text = text.replace(
|
|
132
|
+
/(?:^|\n)An async command you ran earlier has completed\.[\s\S]*?(?:\nCurrent time:[^\n]*(?:\n|$)|$)/gi,
|
|
133
|
+
'\n'
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
return normalizeWhitespace(text);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function shouldSuppressOomiVisibleText(value) {
|
|
140
|
+
const text = String(value || '').trim();
|
|
141
|
+
if (!text) return true;
|
|
142
|
+
|
|
143
|
+
if (/System \(untrusted\):/i.test(text)) return true;
|
|
144
|
+
if (/An async command you ran earlier has completed\./i.test(text)) return true;
|
|
145
|
+
if (/Handle the result internally\. Do not relay it to the user/i.test(text)) return true;
|
|
146
|
+
if (/Exec (failed|completed) \([^)]+\) ::/i.test(text)) return true;
|
|
147
|
+
if (/^\s*<think>[\s\S]*$/i.test(text) && !/<\/think>/i.test(text)) return true;
|
|
148
|
+
if (/^NO_REPLY$/i.test(text)) return true;
|
|
149
|
+
|
|
150
|
+
return !sanitizeUserVisibleText(text);
|
|
151
|
+
}
|
|
152
|
+
|
|
110
153
|
function extractConversationKey(payload) {
|
|
111
154
|
const candidates = [
|
|
112
155
|
payload?.conversationKey,
|
|
@@ -290,7 +333,16 @@ const oomiChannelPlugin = {
|
|
|
290
333
|
};
|
|
291
334
|
}
|
|
292
335
|
|
|
293
|
-
const
|
|
336
|
+
const rawContent = extractText(payload);
|
|
337
|
+
if (shouldSuppressOomiVisibleText(rawContent)) {
|
|
338
|
+
return {
|
|
339
|
+
ok: true,
|
|
340
|
+
suppressed: true,
|
|
341
|
+
reason: 'internal_content',
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const content = sanitizeUserVisibleText(rawContent);
|
|
294
346
|
if (!content) {
|
|
295
347
|
return {
|
|
296
348
|
ok: false,
|
package/package.json
CHANGED
package/persona-app/README.md
CHANGED
|
@@ -30,4 +30,12 @@ When answering data-backed health or fitness questions, agents should read appro
|
|
|
30
30
|
oomi context health --json
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
If the user asks you to refresh stale HealthKit context, run:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
oomi context health sync --json
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The iPhone app must open or refresh before it can read Apple Health and fulfill the pending request.
|
|
40
|
+
|
|
33
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.
|
package/skills/oomi/SKILL.md
CHANGED
|
@@ -127,6 +127,7 @@ Rules:
|
|
|
127
127
|
- answer only from `healthContext.summary` and `healthContext.derived`
|
|
128
128
|
- respect `healthContext.agentGuidance.canAnswerFromContext`
|
|
129
129
|
- if context is missing, stale, denied, disabled, or unavailable, follow `healthContext.repair`
|
|
130
|
+
- if the user asks you to refresh stale HealthKit context, run `oomi context health sync --json`; tell the user the iPhone app must be opened for Oomi to read Apple Health and fulfill the pending sync
|
|
130
131
|
- do not ask HealthKit directly; the Oomi mobile app syncs approved HealthKit data to the backend
|
|
131
132
|
|
|
132
133
|
## Hidden Speech Payload
|
|
@@ -78,7 +78,7 @@ Rules:
|
|
|
78
78
|
- `metadata.spoken.style` is optional metadata for debugging or future mapping
|
|
79
79
|
- if no hidden speech sidecar exists, Oomi falls back to speaking the visible assistant text
|
|
80
80
|
- if you omit `metadata.spoken`, the plugin now synthesizes a bounded hidden fallback from visible assistant text
|
|
81
|
-
- visible chat text
|
|
81
|
+
- visible chat text should be clean before sending; the plugin may strip private runtime wrappers and avatar command tags as a safety guardrail
|
|
82
82
|
|
|
83
83
|
## Persona App API Tools
|
|
84
84
|
|
|
@@ -113,5 +113,6 @@ Rules:
|
|
|
113
113
|
- respect `healthContext.agentGuidance.canAnswerFromContext`
|
|
114
114
|
- if `canAnswerFromContext` is false, tell the user the relevant `healthContext.repair.label` or `healthContext.repair.reason`
|
|
115
115
|
- if `healthContext.status` is `stale`, mention that the latest HealthKit sync may be out of date when timing matters
|
|
116
|
+
- if the user asks you to refresh stale HealthKit context, run `oomi context health sync --json`; tell the user the iPhone app must be opened for Oomi to read Apple Health and fulfill the pending sync
|
|
116
117
|
- do not infer unavailable health fields
|
|
117
118
|
- do not request HealthKit directly from the phone; the Oomi mobile app owns permission prompts and syncs approved data to the backend
|