oomi-ai 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,7 +28,12 @@ The current model is:
28
28
  - Files: `persona-app/*`, `lib/personaApiClient.js`, `bin/oomi-ai.js`
29
29
  - Purpose: inspect account-linked component-composed persona apps and apply approved backend actions.
30
30
 
31
- 4. Agent/operator instructions
31
+ 4. Permissioned context tools
32
+
33
+ - Files: `lib/personaApiClient.js`, `bin/oomi-ai.js`
34
+ - Purpose: read user-approved Oomi backend context, such as HealthKit summaries synced by the mobile app.
35
+
36
+ 5. Agent/operator instructions
32
37
 
33
38
  - Files: `agent_instructions.md`, `skills/oomi/*`
34
39
  - Purpose: tell OpenClaw agents how to connect, repair, and use Oomi without local UI code generation.
@@ -164,6 +169,22 @@ oomi persona-apps apply-action fitness-today --action fitness.complete_goal --pa
164
169
  oomi persona-apps apply-action fitness-today --action fitness.complete_workout --payload-json '{"goalId":"easy-run","minutes":20}' --json
165
170
  ```
166
171
 
172
+ ## Context Tools
173
+
174
+ 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:
175
+
176
+ ```bash
177
+ oomi context health --json
178
+ ```
179
+
180
+ Rules for agents:
181
+
182
+ - Answer from `healthContext.summary` and `healthContext.derived`.
183
+ - Respect `healthContext.agentGuidance.canAnswerFromContext`.
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
+ - If context is `stale`, mention freshness when timing matters.
186
+ - Do not ask the phone for HealthKit directly; the Oomi mobile app syncs HealthKit to the backend.
187
+
167
188
  ## Voice Contract
168
189
 
169
190
  Managed voice uses the same Oomi plugin and bridge layer as managed chat.
@@ -12,6 +12,7 @@ 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
+ - approved user context, such as HealthKit data synced by the Oomi mobile app, can be read through Oomi backend context tools
15
16
  - the user never has to paste raw pair identifiers or gateway secrets manually when managed connect is available
16
17
 
17
18
  ## Standard Connect Flow
@@ -260,3 +261,22 @@ Strict rules:
260
261
  - Do not install app framework dependencies for persona UI work.
261
262
  - Do not present local URLs as persona app URLs.
262
263
  - If a persona app needs to be created, tell the user to add it from the Oomi client first so permissions and hydration run correctly.
264
+
265
+ ## Permissioned Context Tools
266
+
267
+ Use Oomi context tools when the user asks about personal data that should come from mobile permissions instead of model memory.
268
+
269
+ For health, fitness, sleep, workout, cardio, or movement questions:
270
+
271
+ ```bash
272
+ oomi context health --json
273
+ ```
274
+
275
+ Rules:
276
+
277
+ - Answer only from `healthContext.summary` and `healthContext.derived`.
278
+ - Respect `healthContext.agentGuidance.canAnswerFromContext`.
279
+ - If `canAnswerFromContext` is false, tell the user the relevant `healthContext.repair.label` or `healthContext.repair.reason`.
280
+ - If `healthContext.status` is `stale`, mention that the latest HealthKit sync may be out of date when timing matters.
281
+ - Do not infer unavailable health fields.
282
+ - 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
@@ -227,6 +227,9 @@ Commands:
227
227
  persona-apps apply-action <slug>
228
228
  Apply an approved persona app action through the Oomi backend.
229
229
 
230
+ context health
231
+ Show the latest account-linked health context available to this paired OpenClaw device.
232
+
230
233
  Common flags:
231
234
  --agents-file PATH Override AGENTS.md path
232
235
  --workspace PATH Override OpenClaw workspace root
@@ -566,6 +569,12 @@ async function handlePersonaAppApplyActionCommand(slug, flags = {}) {
566
569
  printStructuredResult(result, isTruthyFlag(flags.json));
567
570
  }
568
571
 
572
+ async function handleContextHealthCommand(flags = {}) {
573
+ const client = createCliPersonaApiClient(flags);
574
+ const payload = await client.getHealthContext();
575
+ printStructuredResult(payload, isTruthyFlag(flags.json));
576
+ }
577
+
569
578
  async function refreshBridgeForUpdate(flags = {}, options = {}) {
570
579
  const logger = options.logger || null;
571
580
  if (process.platform === 'darwin') {
@@ -1346,6 +1355,127 @@ function extractTextFromGatewayMessage(message) {
1346
1355
  .join(' ');
1347
1356
  }
1348
1357
 
1358
+ function normalizeWhitespace(value) {
1359
+ return String(value || '')
1360
+ .replace(/[ \t]+\n/g, '\n')
1361
+ .replace(/\n{3,}/g, '\n\n')
1362
+ .replace(/[ \t]{2,}/g, ' ')
1363
+ .trim();
1364
+ }
1365
+
1366
+ function sanitizeUserVisibleText(value) {
1367
+ let text = String(value || '');
1368
+ if (!text.trim()) return '';
1369
+
1370
+ text = text.replace(/<think>[\s\S]*?<\/think>/gi, ' ');
1371
+ if (/<think>/i.test(text)) {
1372
+ text = text.replace(/<think>[\s\S]*$/i, ' ');
1373
+ }
1374
+ text = text.replace(/<\/think>/gi, ' ');
1375
+
1376
+ text = text.replace(/(?:^|\n)System \(untrusted\):[\s\S]*?(?=\n\n|$)/gi, '\n');
1377
+ text = text.replace(
1378
+ /(?:^|\n)An async command you ran earlier has completed\.[\s\S]*?(?:\nCurrent time:[^\n]*(?:\n|$)|$)/gi,
1379
+ '\n'
1380
+ );
1381
+
1382
+ return normalizeWhitespace(text);
1383
+ }
1384
+
1385
+ function shouldSuppressOomiVisibleText(value) {
1386
+ const text = String(value || '').trim();
1387
+ if (!text) return true;
1388
+
1389
+ if (/System \(untrusted\):/i.test(text)) return true;
1390
+ if (/An async command you ran earlier has completed\./i.test(text)) return true;
1391
+ if (/Handle the result internally\. Do not relay it to the user/i.test(text)) return true;
1392
+ if (/Exec (failed|completed) \([^)]+\) ::/i.test(text)) return true;
1393
+ if (/^\s*<think>[\s\S]*$/i.test(text) && !/<\/think>/i.test(text)) return true;
1394
+
1395
+ return !sanitizeUserVisibleText(text);
1396
+ }
1397
+
1398
+ function sanitizeGatewayMessageForOomi(message) {
1399
+ if (!message || typeof message !== 'object') return null;
1400
+
1401
+ const role = typeof message.role === 'string' ? message.role.trim().toLowerCase() : '';
1402
+ if (role && !['assistant', 'user'].includes(role)) {
1403
+ return null;
1404
+ }
1405
+
1406
+ const rawText = extractTextFromGatewayMessage(message);
1407
+ if (shouldSuppressOomiVisibleText(rawText)) {
1408
+ return null;
1409
+ }
1410
+
1411
+ const content = sanitizeUserVisibleText(rawText);
1412
+ if (!content) return null;
1413
+
1414
+ return {
1415
+ ...message,
1416
+ role: role || 'assistant',
1417
+ content,
1418
+ };
1419
+ }
1420
+
1421
+ function sanitizeGatewayFrameForOomiClient(frameText) {
1422
+ const frame = parseJsonPayload(frameText);
1423
+ if (!frame || typeof frame !== 'object') {
1424
+ return { frameText, changed: false, suppressed: false };
1425
+ }
1426
+
1427
+ if (frame.type === 'res' && frame.payload && typeof frame.payload === 'object' && Array.isArray(frame.payload.messages)) {
1428
+ const nextMessages = frame.payload.messages
1429
+ .map((message) => sanitizeGatewayMessageForOomi(message))
1430
+ .filter(Boolean);
1431
+ const nextFrame = {
1432
+ ...frame,
1433
+ payload: {
1434
+ ...frame.payload,
1435
+ messages: nextMessages,
1436
+ },
1437
+ };
1438
+ const nextFrameText = JSON.stringify(nextFrame);
1439
+ return {
1440
+ frameText: nextFrameText,
1441
+ changed: nextFrameText !== frameText,
1442
+ suppressed: nextMessages.length < frame.payload.messages.length,
1443
+ };
1444
+ }
1445
+
1446
+ if (frame.type === 'event' && frame.event === 'chat') {
1447
+ const payload = frame.payload && typeof frame.payload === 'object' ? frame.payload : null;
1448
+ const message = payload?.message && typeof payload.message === 'object' ? payload.message : null;
1449
+ if (!payload || !message) {
1450
+ return { frameText, changed: false, suppressed: false };
1451
+ }
1452
+
1453
+ const sanitizedMessage = sanitizeGatewayMessageForOomi(message);
1454
+ const nextFrame = {
1455
+ ...frame,
1456
+ payload: {
1457
+ ...payload,
1458
+ message: sanitizedMessage || {
1459
+ ...message,
1460
+ content: '',
1461
+ metadata: {
1462
+ ...(message.metadata && typeof message.metadata === 'object' && !Array.isArray(message.metadata) ? message.metadata : {}),
1463
+ oomiSuppressed: true,
1464
+ },
1465
+ },
1466
+ },
1467
+ };
1468
+ const nextFrameText = JSON.stringify(nextFrame);
1469
+ return {
1470
+ frameText: nextFrameText,
1471
+ changed: nextFrameText !== frameText,
1472
+ suppressed: !sanitizedMessage,
1473
+ };
1474
+ }
1475
+
1476
+ return { frameText, changed: false, suppressed: false };
1477
+ }
1478
+
1349
1479
  function summarizeVoiceFrameContract(frameText) {
1350
1480
  const frame = parseJsonPayload(frameText);
1351
1481
  if (!frame || typeof frame !== 'object') {
@@ -2988,6 +3118,13 @@ async function startOpenclawBridge(flags) {
2988
3118
 
2989
3119
  gatewaySocket.on('message', runBridgeCallbackSafely((gatewayRaw) => {
2990
3120
  let frame = typeof gatewayRaw === 'string' ? gatewayRaw : gatewayRaw.toString();
3121
+ const visibleSanitized = sanitizeGatewayFrameForOomiClient(frame);
3122
+ if (visibleSanitized.changed) {
3123
+ frame = visibleSanitized.frameText;
3124
+ bridgeDebugLog(
3125
+ `[bridge] oomi.visible_sanitized ${sessionId} suppressed=${visibleSanitized.suppressed ? 'yes' : 'no'}`
3126
+ );
3127
+ }
2991
3128
  const spokenNormalized = normalizeAssistantGatewayFrame(sessionId, frame);
2992
3129
  if (spokenNormalized.changed) {
2993
3130
  frame = spokenNormalized.frameText;
@@ -4181,6 +4318,11 @@ async function main() {
4181
4318
  return;
4182
4319
  }
4183
4320
 
4321
+ if (command === 'context' && subcommand === 'health') {
4322
+ await handleContextHealthCommand(args.flags);
4323
+ return;
4324
+ }
4325
+
4184
4326
  console.error(`Unknown command: ${command} ${subcommand || ''}`.trim());
4185
4327
  usage();
4186
4328
  process.exit(1);
@@ -4199,6 +4341,9 @@ if (__isDirectExecution) {
4199
4341
 
4200
4342
  export {
4201
4343
  prepareGatewayFrameForLocalGateway,
4344
+ sanitizeGatewayFrameForOomiClient,
4345
+ sanitizeUserVisibleText,
4346
+ shouldSuppressOomiVisibleText,
4202
4347
  ensureAssistantSpokenMetadata,
4203
4348
  normalizeAssistantGatewayFrame,
4204
4349
  runAssistantFinalDebugCheck,
@@ -139,5 +139,14 @@ export function createPersonaApiClient({
139
139
  },
140
140
  });
141
141
  },
142
+
143
+ getHealthContext() {
144
+ return getJson({
145
+ fetchImpl,
146
+ backendUrl: resolvedBackendUrl,
147
+ deviceToken: resolvedDeviceToken,
148
+ path: '/v1/context/health',
149
+ });
150
+ },
142
151
  };
143
152
  }
@@ -107,6 +107,46 @@ 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
+
128
+ text = text.replace(/(?:^|\n)System \(untrusted\):[\s\S]*?(?=\n\n|$)/gi, '\n');
129
+ text = text.replace(
130
+ /(?:^|\n)An async command you ran earlier has completed\.[\s\S]*?(?:\nCurrent time:[^\n]*(?:\n|$)|$)/gi,
131
+ '\n'
132
+ );
133
+
134
+ return normalizeWhitespace(text);
135
+ }
136
+
137
+ function shouldSuppressOomiVisibleText(value) {
138
+ const text = String(value || '').trim();
139
+ if (!text) return true;
140
+
141
+ if (/System \(untrusted\):/i.test(text)) return true;
142
+ if (/An async command you ran earlier has completed\./i.test(text)) return true;
143
+ if (/Handle the result internally\. Do not relay it to the user/i.test(text)) return true;
144
+ if (/Exec (failed|completed) \([^)]+\) ::/i.test(text)) return true;
145
+ if (/^\s*<think>[\s\S]*$/i.test(text) && !/<\/think>/i.test(text)) return true;
146
+
147
+ return !sanitizeUserVisibleText(text);
148
+ }
149
+
110
150
  function extractConversationKey(payload) {
111
151
  const candidates = [
112
152
  payload?.conversationKey,
@@ -290,7 +330,16 @@ const oomiChannelPlugin = {
290
330
  };
291
331
  }
292
332
 
293
- const content = extractText(payload);
333
+ const rawContent = extractText(payload);
334
+ if (shouldSuppressOomiVisibleText(rawContent)) {
335
+ return {
336
+ ok: true,
337
+ suppressed: true,
338
+ reason: 'internal_content',
339
+ };
340
+ }
341
+
342
+ const content = sanitizeUserVisibleText(rawContent);
294
343
  if (!content) {
295
344
  return {
296
345
  ok: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oomi-ai",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
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"
@@ -24,4 +24,10 @@ oomi persona-apps show fitness-today --json
24
24
  oomi persona-apps apply-action fitness-today --action fitness.complete_workout --payload-json '{"goalId":"easy-run","minutes":20}' --json
25
25
  ```
26
26
 
27
+ When answering data-backed health or fitness questions, agents should read approved backend context first:
28
+
29
+ ```bash
30
+ oomi context health --json
31
+ ```
32
+
27
33
  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.
@@ -1,6 +1,17 @@
1
1
  {
2
2
  "schemaVersion": "persona-app.v1",
3
- "registryVersion": "2026-04-23.1",
3
+ "registryVersion": "2026-04-24.1",
4
+ "contextTools": [
5
+ {
6
+ "id": "health.context.read",
7
+ "command": "oomi context health --json",
8
+ "endpoint": "/v1/context/health",
9
+ "description": "Reads the latest account-linked health context synced by Oomi mobile clients.",
10
+ "permissions": ["healthkit.read", "health_connect.read"],
11
+ "readOnly": true,
12
+ "targets": ["ios", "android", "web"]
13
+ }
14
+ ],
4
15
  "sharedComponents": [
5
16
  {
6
17
  "type": "shared.personaHero",
@@ -27,6 +38,16 @@
27
38
  "type": "fitness.goalChecklist",
28
39
  "description": "Checklist of suggested goals for today.",
29
40
  "targets": ["ios", "android", "web"]
41
+ },
42
+ {
43
+ "type": "fitness.metricGrid",
44
+ "description": "Grid of approved HealthKit or Health Connect activity, sleep, cardio, and body metrics.",
45
+ "targets": ["ios", "android", "web"]
46
+ },
47
+ {
48
+ "type": "fitness.workoutList",
49
+ "description": "Recent workouts synced from the user's approved mobile health source.",
50
+ "targets": ["ios", "android", "web"]
30
51
  }
31
52
  ],
32
53
  "actions": [
@@ -55,9 +76,10 @@
55
76
  ],
56
77
  "permissions": [
57
78
  "fitness.state.read",
58
- "fitness.state.write"
79
+ "fitness.state.write",
80
+ "healthkit.read",
81
+ "health_connect.read"
59
82
  ]
60
83
  }
61
84
  ]
62
85
  }
63
-
@@ -10,6 +10,7 @@ Use this skill when you need to:
10
10
  - repair the Oomi plugin or bridge on a machine
11
11
  - inspect managed chat or voice health
12
12
  - inspect or update component-composed Oomi persona app state
13
+ - read approved Oomi backend context such as HealthKit summaries synced by the mobile app
13
14
  - control the Oomi avatar with inline tags
14
15
 
15
16
  ## Primary Operator Workflow
@@ -112,6 +113,22 @@ Rules:
112
113
  - do not create local persona UI projects from the OpenClaw machine
113
114
  - do not execute persona UI jobs or start local persona app runtimes
114
115
 
116
+ ## Permissioned Context Tools
117
+
118
+ Use context commands before answering personal data questions that should come from Oomi-approved permissions.
119
+
120
+ For health, fitness, sleep, workout, cardio, or movement questions:
121
+
122
+ ```bash
123
+ oomi context health --json
124
+ ```
125
+
126
+ Rules:
127
+ - answer only from `healthContext.summary` and `healthContext.derived`
128
+ - respect `healthContext.agentGuidance.canAnswerFromContext`
129
+ - if context is missing, stale, denied, disabled, or unavailable, follow `healthContext.repair`
130
+ - do not ask HealthKit directly; the Oomi mobile app syncs approved HealthKit data to the backend
131
+
115
132
  ## Hidden Speech Payload
116
133
 
117
134
  Managed voice can carry a hidden TTS-only speech sidecar alongside the normal assistant message.
@@ -97,3 +97,21 @@ Rules:
97
97
  - apply updates only through approved `persona-apps apply-action` actions
98
98
  - if the persona app is missing, ask the user to add it from the Oomi client first so permissions and hydration can run
99
99
  - do not execute persona UI jobs or start local persona app runtimes
100
+
101
+ ## Permissioned Context Tools
102
+
103
+ Use Oomi context tools when the user asks about personal data that should come from mobile permissions instead of memory.
104
+
105
+ For health, fitness, sleep, workout, cardio, or movement questions:
106
+
107
+ ```bash
108
+ oomi context health --json
109
+ ```
110
+
111
+ Rules:
112
+ - answer only from `healthContext.summary` and `healthContext.derived`
113
+ - respect `healthContext.agentGuidance.canAnswerFromContext`
114
+ - if `canAnswerFromContext` is false, tell the user the relevant `healthContext.repair.label` or `healthContext.repair.reason`
115
+ - if `healthContext.status` is `stale`, mention that the latest HealthKit sync may be out of date when timing matters
116
+ - do not infer unavailable health fields
117
+ - do not request HealthKit directly from the phone; the Oomi mobile app owns permission prompts and syncs approved data to the backend