oomi-ai 0.7.0 → 0.7.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/agent_instructions.md +10 -1
- package/bin/oomi-ai.js +7 -4
- package/lib/localAgent.js +64 -5
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/oomi/SKILL.md +10 -1
- package/skills/oomi/agent_instructions.md +10 -1
package/agent_instructions.md
CHANGED
|
@@ -271,12 +271,15 @@ To reach the user on their phone when they are not in the conversation, send a p
|
|
|
271
271
|
oomi notify "Your 7-day plan is ready. Want to look now?" --title "Fitness" --json
|
|
272
272
|
```
|
|
273
273
|
|
|
274
|
+
Add `--connection NAME` when you run it outside an Oomi conversation (for example from a scheduled job). Turns Oomi starts already set `OOMI_CONNECTION`. Omit it for the default OpenClaw connection.
|
|
275
|
+
|
|
274
276
|
Rules:
|
|
275
277
|
|
|
276
278
|
- Notify only for things worth interrupting the user for: a reminder they asked for, a finished task, or something time-sensitive. Say everything else in chat.
|
|
277
279
|
- Keep the message short and specific. The title defaults to your agent name.
|
|
278
280
|
- Tapping the notification opens the Oomi app to the conversation, where the message also appears.
|
|
279
|
-
-
|
|
281
|
+
- Replies to the user's own Oomi messages, and OpenClaw sends on the Oomi channel, already notify their phone. Do not also run `oomi notify` for the same message.
|
|
282
|
+
- Anything you start on your own, such as a reminder or finished background work, needs `oomi notify`. Hermes agents have no other way to reach the phone.
|
|
280
283
|
- If `notification.status` is `skipped`, tell the user `notification.guidance` instead of retrying.
|
|
281
284
|
- If the request is rate limited, do not retry until `retryAfterSeconds` has passed.
|
|
282
285
|
|
|
@@ -338,3 +341,9 @@ Use `oomi routines pause|resume|end <routine-id> --file action.json` with expect
|
|
|
338
341
|
For instructions in another chat, the user must first grant this connection routineManagementEnabled through Oomi Agent settings or `routines settings` using an authenticated Oomi sourceMessageId. Then routine/response mutations can substitute externalInstruction:{channel,messageId,instruction} for sourceMessageId. Record the actual message reference and instruction. Oomi labels this runtime-attested, not independently authenticated. Never turn on the permission yourself or invent external approval. Ownership, data permissions, and primary selection remain separate.
|
|
339
342
|
|
|
340
343
|
The running bridge receives routine.approved, routine.due, routine.changed, routine.response_recorded, and primary_agent.changed. Ordinary events sync quietly without a model call; explicitly enabled routine wakes are bounded and are not retried after an uncertain model dispatch. Refresh canonical state before acting on a delayed event. Due events already have notificationHandled:true; do not send another phone alert or message for every event. Persist useful context for the next conversation. If a follow-up is explicitly linked to an event, include relatedEventId in the notification API so Oomi suppresses an extra push. Keep Oomi credentials private; all /v1/routines, /v1/agent_preferences, /v1/routine_occurrences, and /v1/agent_events requests use the saved device bearer token, never a user ID supplied by the agent.
|
|
344
|
+
|
|
345
|
+
### Reply first, then share routine cards
|
|
346
|
+
|
|
347
|
+
Always answer the person in your own conversational voice after a routine or Space tool succeeds. Explain what you recommend or what changed, and invite a useful next step when appropriate. A tool result or review card is not the reply. Do not return only routine names, IDs, JSON, or a stock list of chips. Mention the routines that actually belong to this request; do not automatically recreate or list three routines on unrelated requests. Oomi places newly created routine cards beneath the completed reply in that creation turn. Later approvals update the original card in place. Keep talking normally after a user starts or changes a routine; do not require another UI click when their instruction is already clear.
|
|
348
|
+
|
|
349
|
+
Oomi renders simple Markdown in chat: short paragraphs, bullet or numbered lists, and **bold**. Keep replies brief and scannable, and put long plans in a Space note instead of the chat. A Space link in your reply appears as a card, so share it once rather than repeating the URL or listing IDs.
|
package/bin/oomi-ai.js
CHANGED
|
@@ -286,9 +286,10 @@ Commands:
|
|
|
286
286
|
Save user-requested routines and settings. Use the current sourceMessageId,
|
|
287
287
|
expectedRevision, and a stable idempotencyKey. No extra approval button is needed.
|
|
288
288
|
|
|
289
|
-
notify "<message>" [--title TITLE]
|
|
289
|
+
notify "<message>" [--title TITLE] [--connection NAME]
|
|
290
290
|
Send the linked user a push notification on their phone. Use it for things worth
|
|
291
|
-
interrupting them for; say everything else in chat. Limited per hour.
|
|
291
|
+
interrupting them for; say everything else in chat. Limited per hour. Turns started
|
|
292
|
+
by an Oomi bridge already use their own connection (OOMI_CONNECTION).
|
|
292
293
|
|
|
293
294
|
Common flags:
|
|
294
295
|
--agents-file PATH Override AGENTS.md path
|
|
@@ -2641,7 +2642,7 @@ async function startOpenclawBridge(flags) {
|
|
|
2641
2642
|
}
|
|
2642
2643
|
|
|
2643
2644
|
const localRuntime = bridgeState.localAgent
|
|
2644
|
-
? new LocalAgentRuntime(bridgeState.localAgent, path.dirname(resolveBridgeStatePath()), (health) => updateBridgeStatus(health))
|
|
2645
|
+
? new LocalAgentRuntime({ ...bridgeState.localAgent, connection: bridgeConnectionName }, path.dirname(resolveBridgeStatePath()), (health) => updateBridgeStatus(health))
|
|
2645
2646
|
: null;
|
|
2646
2647
|
const gateway = localRuntime ? { gatewayUrl: 'local-agent:', configPath: resolveBridgeStatePath() } : readOpenclawGatewayConfig();
|
|
2647
2648
|
if (!localRuntime && !gateway.token && !gateway.password) {
|
|
@@ -4546,7 +4547,8 @@ async function main() {
|
|
|
4546
4547
|
process.exit(0);
|
|
4547
4548
|
}
|
|
4548
4549
|
|
|
4549
|
-
|
|
4550
|
+
// Agents started by an Oomi bridge inherit OOMI_CONNECTION, so their commands reach their own account.
|
|
4551
|
+
const connection = args.flags.connection || args.flags.name || (command === 'connect' ? (args.flags.runtime !== 'openclaw' ? args.flags.runtime : '') : String(process.env.OOMI_CONNECTION || '').trim());
|
|
4550
4552
|
if (connection && !/^[a-z0-9][a-z0-9_-]{0,63}$/.test(connection)) throw new Error('Connection name must use lowercase letters, numbers, underscores, or hyphens.');
|
|
4551
4553
|
bridgeConnectionName = connection || '';
|
|
4552
4554
|
|
|
@@ -4556,6 +4558,7 @@ async function main() {
|
|
|
4556
4558
|
spaces: { catalog: 'oomi spaces catalog', list: 'oomi spaces list', create: 'oomi spaces create --file space.json', update: 'oomi spaces update ID --file change.json', revisions: 'oomi spaces revisions ID', guide: 'oomi instructions' },
|
|
4557
4559
|
health: { steps: 'oomi context health --window week --metric steps --json', calendarWeek: 'oomi context health --window range --from YYYY-MM-DD --to YYYY-MM-DD --metric steps --json', largeResults: '--output new-file.json', plugins: 'oomi context plugins', missing: 'unknown, never zero', week: 'rolling seven days; use explicit range for calendar week' },
|
|
4558
4560
|
routines: 'oomi routines list; oomi routines context', identity: 'oomi identity get', updates: 'oomi updates',
|
|
4561
|
+
notify: { send: 'oomi notify "<message>" --title TITLE --json', when: 'reminders the user asked for, finished work, time-sensitive news; everything else in chat', skipped: 'tell the user notification.guidance', rateLimited: 'wait retryAfterSeconds' },
|
|
4559
4562
|
costs: { passiveEvents: 'no model turn', wakeAgent: 'routine.agentWakeEnabled defaults false; enable only on explicit user request', maximumRoutineWakeAttemptsPerHour: 6, retries: 'uncertain event turns are not automatically rerun', connectionCheck: 'first connect or explicit oomi verify makes one model call; reconnect reuses verification', identity: 'a changed identity revision is delivered once to keep names consistent' }
|
|
4560
4563
|
}, null, 2)); return;
|
|
4561
4564
|
}
|
package/lib/localAgent.js
CHANGED
|
@@ -5,6 +5,56 @@ import { spawn } from 'node:child_process';
|
|
|
5
5
|
import { EventEmitter } from 'node:events';
|
|
6
6
|
import { createHash, randomUUID } from 'node:crypto';
|
|
7
7
|
|
|
8
|
+
const IMAGE_EXTENSIONS = { 'image/jpeg': '.jpg', 'image/png': '.png', 'image/webp': '.webp', 'image/gif': '.gif' };
|
|
9
|
+
const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Chat photos arrive inline as base64 (the shape OpenClaw's gateway also accepts), with an
|
|
13
|
+
* optional `storagePath` in the owner's Oomi storage so the conversation can show it again
|
|
14
|
+
* later. Hermes looks at one image per query.
|
|
15
|
+
*/
|
|
16
|
+
export function readImageAttachments(attachments) {
|
|
17
|
+
if (!Array.isArray(attachments) || attachments.length === 0) return [];
|
|
18
|
+
if (attachments.length > 1) throw new Error('Send one photo at a time.');
|
|
19
|
+
return attachments.map((attachment) => {
|
|
20
|
+
const mimeType = String(attachment?.mimeType || '').toLowerCase();
|
|
21
|
+
if (attachment?.type !== 'image' || !IMAGE_EXTENSIONS[mimeType]) throw new Error('Only JPEG, PNG, WebP, or GIF photos can be sent.');
|
|
22
|
+
const bytes = Buffer.from(typeof attachment.content === 'string' ? attachment.content : '', 'base64');
|
|
23
|
+
if (!bytes.length) throw new Error('The photo arrived empty. Try sending it again.');
|
|
24
|
+
if (bytes.length > MAX_IMAGE_BYTES) throw new Error('That photo is too large. Photos can be up to 10 MB.');
|
|
25
|
+
const dimension = (value) => (Number.isFinite(value) && value > 0 ? Math.round(value) : undefined);
|
|
26
|
+
return {
|
|
27
|
+
mimeType,
|
|
28
|
+
bytes,
|
|
29
|
+
storagePath: typeof attachment.storagePath === 'string' && attachment.storagePath.trim() ? attachment.storagePath.trim() : undefined,
|
|
30
|
+
width: dimension(attachment.width),
|
|
31
|
+
height: dimension(attachment.height),
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Private files for one turn; the caller removes the folder when the turn ends.
|
|
37
|
+
function writeTurnImages(images) {
|
|
38
|
+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'oomi-turn-'));
|
|
39
|
+
fs.chmodSync(directory, 0o700);
|
|
40
|
+
const paths = images.map((image, index) => {
|
|
41
|
+
const file = path.join(directory, `photo-${index + 1}${IMAGE_EXTENSIONS[image.mimeType]}`);
|
|
42
|
+
fs.writeFileSync(file, image.bytes, { mode: 0o600 });
|
|
43
|
+
return file;
|
|
44
|
+
});
|
|
45
|
+
return { directory, paths };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// What the conversation remembers: the words, plus where the owner's photo is stored (never the bytes).
|
|
49
|
+
function userContent(text, images) {
|
|
50
|
+
const stored = images.filter((image) => image.storagePath);
|
|
51
|
+
if (!stored.length) return text;
|
|
52
|
+
return [
|
|
53
|
+
...(text.trim() ? [{ type: 'text', text }] : []),
|
|
54
|
+
...stored.map(({ mimeType, storagePath, width, height }) => ({ type: 'image', mimeType, storagePath, ...(width ? { width } : {}), ...(height ? { height } : {}) })),
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
|
|
8
58
|
export function writePrivateJson(file, data) {
|
|
9
59
|
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
10
60
|
const temporary = `${file}.${process.pid}.tmp`;
|
|
@@ -55,8 +105,11 @@ export function runLocalAgent(config, request, { signal, timeoutMs = 180000 } =
|
|
|
55
105
|
if (config.profile) args.push('--profile', config.profile);
|
|
56
106
|
args.push('chat', '--quiet', '--query-file', '-');
|
|
57
107
|
if (request.runtimeSessionId) args.push('--resume', request.runtimeSessionId);
|
|
108
|
+
if (request.imagePaths?.[0]) args.push('--image', request.imagePaths[0]);
|
|
58
109
|
}
|
|
59
|
-
|
|
110
|
+
// The agent's own `oomi` commands (notify, routines, spaces) use this connection by default.
|
|
111
|
+
const env = { ...process.env, ...(config.hermesHome ? { HERMES_HOME: config.hermesHome } : {}), ...(config.connection ? { OOMI_CONNECTION: config.connection } : {}) };
|
|
112
|
+
const child = spawn(config.command[0], args, { env, cwd: config.cwd, shell: false, detached: process.platform !== 'win32', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
60
113
|
let output = '';
|
|
61
114
|
let diagnostics = '';
|
|
62
115
|
child.stdout.setEncoding('utf8');
|
|
@@ -116,8 +169,11 @@ export class LocalAgentRuntime {
|
|
|
116
169
|
file(sessionKey) { return path.join(this.directory, 'conversations', createHash('sha256').update(sessionKey).digest('hex') + '.json'); }
|
|
117
170
|
history(sessionKey) { return readJson(this.file(sessionKey), { messages: [], receipts: {} }); }
|
|
118
171
|
async turn({ sessionKey, message, idempotencyKey, attachments = [], retryInterrupted = false }) {
|
|
119
|
-
|
|
120
|
-
|
|
172
|
+
const images = readImageAttachments(attachments);
|
|
173
|
+
const text = typeof message === 'string' ? message : '';
|
|
174
|
+
if (typeof sessionKey !== 'string' || !sessionKey || typeof idempotencyKey !== 'string' || !idempotencyKey || (!text.trim() && !images.length)) throw new Error('A session, message, and idempotency key are required.');
|
|
175
|
+
const prompt = text.trim() ? text : 'Here is a photo.';
|
|
176
|
+
message = text;
|
|
121
177
|
const key = `${sessionKey}\0${idempotencyKey}`;
|
|
122
178
|
if (this.active.has(key)) {
|
|
123
179
|
const active = this.active.get(key);
|
|
@@ -136,15 +192,18 @@ export class LocalAgentRuntime {
|
|
|
136
192
|
if (controller.signal.aborted) throw new Error('Agent turn cancelled.');
|
|
137
193
|
state.receipts[receiptKey] = { message, status: 'running' };
|
|
138
194
|
writePrivateJson(this.file(sessionKey), state);
|
|
195
|
+
let scratch;
|
|
139
196
|
try {
|
|
140
|
-
|
|
197
|
+
scratch = images.length ? writeTurnImages(images) : null;
|
|
198
|
+
const result = await runLocalAgent(this.config, { sessionKey, message: prompt, idempotencyKey, history: state.messages, runtimeSessionId: state.runtimeSessionId || this.config.resume, imagePaths: scratch?.paths || [] }, { signal: controller.signal });
|
|
141
199
|
state.runtimeSessionId = result.sessionId;
|
|
142
|
-
state.messages.push({ role: 'user', content:
|
|
200
|
+
state.messages.push({ role: 'user', content: userContent(text, images), timestamp: Date.now() }, { role: 'assistant', content: [{ type: 'text', text: result.text }], timestamp: Date.now() });
|
|
143
201
|
state.receipts[receiptKey] = { message, result };
|
|
144
202
|
writePrivateJson(this.file(sessionKey), state);
|
|
145
203
|
this.onHealth({ runtimeReady: true, lastReplyAt: new Date().toISOString(), lastErrorMessage: '' });
|
|
146
204
|
return result;
|
|
147
205
|
} catch (err) { this.onHealth({ runtimeReady: false, status: 'degraded', lastErrorMessage: err.message }); throw err; }
|
|
206
|
+
finally { if (scratch) fs.rmSync(scratch.directory, { recursive: true, force: true }); }
|
|
148
207
|
});
|
|
149
208
|
this.queue = promise.catch(() => {});
|
|
150
209
|
this.active.set(key, { promise, controller, sessionKey, message, runId: idempotencyKey });
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "oomi-ai",
|
|
3
3
|
"name": "Oomi Channel Plugin",
|
|
4
4
|
"description": "Oomi connection, Spaces, health context and routines. Run oomi capabilities and oomi instructions; ordinary state updates do not invoke a model.",
|
|
5
|
-
"version": "0.7.
|
|
5
|
+
"version": "0.7.1",
|
|
6
6
|
"author": "Oomi",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"openclawVersion": ">=0.5.0",
|
package/package.json
CHANGED
package/skills/oomi/SKILL.md
CHANGED
|
@@ -205,11 +205,14 @@ To reach the user on their phone when they are not in the conversation:
|
|
|
205
205
|
oomi notify "Your 7-day plan is ready. Want to look now?" --title "Fitness" --json
|
|
206
206
|
```
|
|
207
207
|
|
|
208
|
+
Add `--connection NAME` when you run it outside an Oomi conversation (for example from a scheduled job); turns Oomi starts already set `OOMI_CONNECTION`. Omit it for the default OpenClaw connection.
|
|
209
|
+
|
|
208
210
|
Rules:
|
|
209
211
|
- notify only for things worth interrupting the user for (a reminder they asked for, a finished task, something time-sensitive); say everything else in chat
|
|
210
212
|
- keep it short and specific; the title defaults to your agent name
|
|
211
213
|
- tapping it opens the Oomi app to the conversation, where the message also appears
|
|
212
|
-
- messages
|
|
214
|
+
- replies to the user's own Oomi messages and OpenClaw sends on the Oomi channel already notify the phone; do not also run `oomi notify` for the same message
|
|
215
|
+
- anything you start on your own (a reminder, finished background work) needs `oomi notify`; Hermes agents have no other way to reach the phone
|
|
213
216
|
- if `notification.status` is `skipped`, tell the user `notification.guidance` instead of retrying
|
|
214
217
|
- if rate limited, wait `retryAfterSeconds` before trying again
|
|
215
218
|
|
|
@@ -336,3 +339,9 @@ Use `oomi routines pause|resume|end <routine-id> --file action.json` with expect
|
|
|
336
339
|
For instructions in another chat, the user must first grant this connection routineManagementEnabled through Oomi Agent settings or `routines settings` using an authenticated Oomi sourceMessageId. Then routine/response mutations can substitute externalInstruction:{channel,messageId,instruction} for sourceMessageId. Record the actual message reference and instruction. Oomi labels this runtime-attested, not independently authenticated. Never turn on the permission yourself or invent external approval. Ownership, data permissions, and primary selection remain separate.
|
|
337
340
|
|
|
338
341
|
The running bridge receives routine.approved, routine.due, routine.changed, routine.response_recorded, and primary_agent.changed. It acknowledges after the runtime accepts each event and retries failures. Refresh canonical state before acting on a delayed event. Due events already have notificationHandled:true; do not send another phone alert or message for every event. Persist useful context for the next conversation. If a follow-up is explicitly linked to an event, include relatedEventId in the notification API so Oomi suppresses an extra push. Keep Oomi credentials private; all /v1/routines, /v1/agent_preferences, /v1/routine_occurrences, and /v1/agent_events requests use the saved device bearer token, never a user ID supplied by the agent.
|
|
342
|
+
|
|
343
|
+
### Reply first, then share routine cards
|
|
344
|
+
|
|
345
|
+
Always answer the person in your own conversational voice after a routine or Space tool succeeds. Explain what you recommend or what changed, and invite a useful next step when appropriate. A tool result or review card is not the reply. Do not return only routine names, IDs, JSON, or a stock list of chips. Mention the routines that actually belong to this request; do not automatically recreate or list three routines on unrelated requests. Oomi places newly created routine cards beneath the completed reply in that creation turn. Later approvals update the original card in place. Keep talking normally after a user starts or changes a routine; do not require another UI click when their instruction is already clear.
|
|
346
|
+
|
|
347
|
+
Oomi renders simple Markdown in chat: short paragraphs, bullet or numbered lists, and **bold**. Keep replies brief and scannable, and put long plans in a Space note instead of the chat. A Space link in your reply appears as a card, so share it once rather than repeating the URL or listing IDs.
|
|
@@ -280,12 +280,15 @@ To reach the user on their phone when they are not in the conversation, send a p
|
|
|
280
280
|
oomi notify "Your 7-day plan is ready. Want to look now?" --title "Fitness" --json
|
|
281
281
|
```
|
|
282
282
|
|
|
283
|
+
Add `--connection NAME` when you run it outside an Oomi conversation (for example from a scheduled job). Turns Oomi starts already set `OOMI_CONNECTION`. Omit it for the default OpenClaw connection.
|
|
284
|
+
|
|
283
285
|
Rules:
|
|
284
286
|
|
|
285
287
|
- Notify only for things worth interrupting the user for: a reminder they asked for, a finished task, or something time-sensitive. Say everything else in chat.
|
|
286
288
|
- Keep the message short and specific. The title defaults to your agent name.
|
|
287
289
|
- Tapping the notification opens the Oomi app to the conversation, where the message also appears.
|
|
288
|
-
-
|
|
290
|
+
- Replies to the user's own Oomi messages, and OpenClaw sends on the Oomi channel, already notify their phone. Do not also run `oomi notify` for the same message.
|
|
291
|
+
- Anything you start on your own, such as a reminder or finished background work, needs `oomi notify`. Hermes agents have no other way to reach the phone.
|
|
289
292
|
- If `notification.status` is `skipped`, tell the user `notification.guidance` instead of retrying.
|
|
290
293
|
- If the request is rate limited, do not retry until `retryAfterSeconds` has passed.
|
|
291
294
|
|
|
@@ -341,3 +344,9 @@ Use `oomi routines pause|resume|end <routine-id> --file action.json` with expect
|
|
|
341
344
|
For instructions in another chat, the user must first grant this connection routineManagementEnabled through Oomi Agent settings or `routines settings` using an authenticated Oomi sourceMessageId. Then routine/response mutations can substitute externalInstruction:{channel,messageId,instruction} for sourceMessageId. Record the actual message reference and instruction. Oomi labels this runtime-attested, not independently authenticated. Never turn on the permission yourself or invent external approval. Ownership, data permissions, and primary selection remain separate.
|
|
342
345
|
|
|
343
346
|
The running bridge receives routine.approved, routine.due, routine.changed, routine.response_recorded, and primary_agent.changed. Ordinary events sync quietly without a model call; explicitly enabled routine wakes are bounded and are not retried after an uncertain model dispatch. Refresh canonical state before acting on a delayed event. Due events already have notificationHandled:true; do not send another phone alert or message for every event. Persist useful context for the next conversation. If a follow-up is explicitly linked to an event, include relatedEventId in the notification API so Oomi suppresses an extra push. Keep Oomi credentials private; all /v1/routines, /v1/agent_preferences, /v1/routine_occurrences, and /v1/agent_events requests use the saved device bearer token, never a user ID supplied by the agent.
|
|
347
|
+
|
|
348
|
+
### Reply first, then share routine cards
|
|
349
|
+
|
|
350
|
+
Always answer the person in your own conversational voice after a routine or Space tool succeeds. Explain what you recommend or what changed, and invite a useful next step when appropriate. A tool result or review card is not the reply. Do not return only routine names, IDs, JSON, or a stock list of chips. Mention the routines that actually belong to this request; do not automatically recreate or list three routines on unrelated requests. Oomi places newly created routine cards beneath the completed reply in that creation turn. Later approvals update the original card in place. Keep talking normally after a user starts or changes a routine; do not require another UI click when their instruction is already clear.
|
|
351
|
+
|
|
352
|
+
Oomi renders simple Markdown in chat: short paragraphs, bullet or numbered lists, and **bold**. Keep replies brief and scannable, and put long plans in a Space note instead of the chat. A Space link in your reply appears as a card, so share it once rather than repeating the URL or listing IDs.
|