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 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
@@ -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 is never rewritten by the package
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,
@@ -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
  }
@@ -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 content = extractText(payload);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oomi-ai",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
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"
@@ -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.
@@ -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 is never rewritten by the plugin
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