@brantrusnak/openclaw-omadeus 1.0.0

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/src/config.ts ADDED
@@ -0,0 +1,80 @@
1
+ import { DEFAULT_ACCOUNT_ID, type OpenClawConfig } from "../runtime-api.js";
2
+ import { OMADEUS_CAS_URL, OMADEUS_MAESTRO_URL } from "./defaults.js";
3
+ import type { OmadeusChannelConfig, ResolvedOmadeusAccount } from "./types.js";
4
+
5
+ export function getOmadeusChannelConfig(cfg: OpenClawConfig): OmadeusChannelConfig | undefined {
6
+ return (cfg.channels as Record<string, unknown> | undefined)?.["omadeus"] as
7
+ | OmadeusChannelConfig
8
+ | undefined;
9
+ }
10
+
11
+ export function listOmadeusAccountIds(cfg: OpenClawConfig): string[] {
12
+ const section = getOmadeusChannelConfig(cfg);
13
+ if (!section && !resolveOmadeusEnvCredentials()) return [];
14
+ return [DEFAULT_ACCOUNT_ID];
15
+ }
16
+
17
+ export function resolveDefaultOmadeusAccountId(_cfg: OpenClawConfig): string {
18
+ return DEFAULT_ACCOUNT_ID;
19
+ }
20
+
21
+ export function resolveOmadeusAccount(params: {
22
+ cfg: OpenClawConfig;
23
+ accountId?: string | null;
24
+ }): ResolvedOmadeusAccount {
25
+ const { cfg } = params;
26
+ const section = getOmadeusChannelConfig(cfg) ?? {};
27
+ const envCredentials = resolveOmadeusEnvCredentials();
28
+ const email = section.email?.trim() || envCredentials?.email || "";
29
+ const password = section.password?.trim() || envCredentials?.password || "";
30
+ const orgId = section.organizationId ?? envCredentials?.organizationId;
31
+ const sessionToken = section.sessionToken?.trim() ?? "";
32
+ const hasCredentials = Boolean(email && password && orgId);
33
+ const hasSessionToken = Boolean(sessionToken);
34
+ const hasConfigCredentials = Boolean(
35
+ section.email?.trim() && section.password?.trim() && section.organizationId,
36
+ );
37
+ const credentialSource = hasConfigCredentials
38
+ ? "config"
39
+ : hasCredentials
40
+ ? "env"
41
+ : hasSessionToken
42
+ ? "session"
43
+ : "none";
44
+
45
+ return {
46
+ accountId: DEFAULT_ACCOUNT_ID,
47
+ name: "Omadeus",
48
+ enabled: section.enabled !== false,
49
+ config: section,
50
+ casUrl: section.casUrl?.trim() || OMADEUS_CAS_URL,
51
+ maestroUrl: section.maestroUrl?.trim() || OMADEUS_MAESTRO_URL,
52
+ email,
53
+ password,
54
+ organizationId: orgId ?? 0,
55
+ ...(hasSessionToken ? { sessionToken } : {}),
56
+ credentialSource,
57
+ };
58
+ }
59
+
60
+ function resolveOmadeusEnvCredentials():
61
+ | {
62
+ email: string;
63
+ password: string;
64
+ organizationId: number;
65
+ }
66
+ | undefined {
67
+ const email = process.env.OMADEUS_EMAIL?.trim();
68
+ const password = process.env.OMADEUS_PASSWORD?.trim();
69
+ const organizationIdRaw = process.env.OMADEUS_ORGANIZATION_ID?.trim();
70
+ if (!email || !password || !organizationIdRaw || !/^\d+$/.test(organizationIdRaw)) {
71
+ return undefined;
72
+ }
73
+ return {
74
+ email,
75
+ password,
76
+ organizationId: Number(organizationIdRaw),
77
+ };
78
+ }
79
+
80
+ /** Whether messages from the authenticated user should be ignored. */
@@ -0,0 +1,2 @@
1
+ export const OMADEUS_CAS_URL = "https://dev1-cas.rouztech.com";
2
+ export const OMADEUS_MAESTRO_URL = "https://dev3-maestro.rouztech.com";
package/src/inbound.ts ADDED
@@ -0,0 +1,112 @@
1
+ import type {
2
+ OmadeusInboundMessage,
3
+ OmadeusMessage,
4
+ OmadeusMessageDetails,
5
+ } from "./types.js";
6
+
7
+ type Log = {
8
+ info: (msg: string) => void;
9
+ warn: (msg: string) => void;
10
+ debug?: (msg: string) => void;
11
+ };
12
+
13
+ const USER_REF_PATTERN = /\{user_reference_id:(\d+)\}/g;
14
+ const BOLD_MENTION_PREFIX_PATTERN = /^\*\*@[^*]+\*\*/;
15
+
16
+ /**
17
+ * Parse the `details` JSON string to extract the rawMessage with mention
18
+ * template tokens like `{user_reference_id:87}`.
19
+ */
20
+ function parseDetails(raw: string | null): OmadeusMessageDetails | null {
21
+ if (!raw) return null;
22
+ try {
23
+ return JSON.parse(raw) as OmadeusMessageDetails;
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Check whether the bot user (by referenceId) is @-mentioned in the message.
31
+ * Omadeus encodes mentions as `{user_reference_id:N}` in details.rawMessage.
32
+ */
33
+ function isBotMentioned(details: OmadeusMessageDetails | null, selfReferenceId: number): boolean {
34
+ const raw = details?.rawMessage;
35
+ if (!raw) return false;
36
+ for (const match of raw.matchAll(USER_REF_PATTERN)) {
37
+ if (Number(match[1]) === selfReferenceId) return true;
38
+ }
39
+ return false;
40
+ }
41
+
42
+ /**
43
+ * Fallback mention detection when details.rawMessage is absent.
44
+ * Omadeus often prefixes mentioned messages with `**@Display Name** ...`.
45
+ */
46
+ function hasMentionPrefixInBody(body: string): boolean {
47
+ return BOLD_MENTION_PREFIX_PATTERN.test(body.trim());
48
+ }
49
+
50
+ /**
51
+ * Strip the formatted @mention from the body so the agent sees clean text.
52
+ * The body contains `**@Display Name** actual text`; this strips the bold
53
+ * mention prefix when it appears at the start.
54
+ */
55
+ function stripLeadingMention(body: string): string {
56
+ return body.replace(/^\*\*@[^*]+\*\*\s*/, "").trim();
57
+ }
58
+
59
+ /**
60
+ * Determine whether a raw Jaguar socket payload is an OmadeusMessage.
61
+ */
62
+ export function isOmadeusMessage(data: unknown): data is OmadeusMessage {
63
+ if (typeof data !== "object" || data === null) return false;
64
+ const obj = data as Record<string, unknown>;
65
+ return obj.type === "message" && typeof obj.roomId === "number" && typeof obj.body === "string";
66
+ }
67
+
68
+ /**
69
+ * Parse a Jaguar socket message into an OpenClaw inbound message.
70
+ *
71
+ * Returns null when:
72
+ * - The event is not a chat message
73
+ * - The body is empty or the message was removed
74
+ */
75
+ export function parseJaguarMessage(
76
+ msg: OmadeusMessage,
77
+ opts: {
78
+ selfReferenceId: number;
79
+ },
80
+ log?: Log,
81
+ ): OmadeusInboundMessage | null {
82
+ if (msg.type !== "message") {
83
+ log?.debug?.(`[jaguar-inbound] ignoring type: ${msg.type}`);
84
+ return null;
85
+ }
86
+
87
+ if (msg.removedAt) return null;
88
+
89
+ const body = (msg.body ?? "").trim();
90
+ if (!body) return null;
91
+
92
+ const details = parseDetails(msg.details);
93
+ const mentioned = isBotMentioned(details, opts.selfReferenceId) || hasMentionPrefixInBody(body);
94
+ const content = mentioned ? stripLeadingMention(body) : body;
95
+
96
+ if (!content) return null;
97
+
98
+ return {
99
+ messageId: msg.id,
100
+ from: String(msg.senderReferenceId),
101
+ fromReferenceId: msg.senderReferenceId,
102
+ content,
103
+ roomId: msg.roomId,
104
+ roomName: msg.roomName,
105
+ subscribableType: msg.subscribableType,
106
+ subscribableKind: msg.subscribableKind,
107
+ isMention: mentioned,
108
+ timestamp: msg.createdAtTimestamp
109
+ ? Math.floor(msg.createdAtTimestamp * 1000)
110
+ : Date.now(),
111
+ };
112
+ }
@@ -0,0 +1,357 @@
1
+ import {
2
+ DEFAULT_ACCOUNT_ID,
3
+ logInboundDrop,
4
+ resolveControlCommandGate,
5
+ type OpenClawConfig,
6
+ type RuntimeEnv,
7
+ } from "../runtime-api.js";
8
+ import { createNugget, resolveTaskChannelRoomId, searchNuggetByNumber } from "./api/nugget.api.js";
9
+ import {
10
+ appendNuggetLookupContextForAgent,
11
+ parseChannelTaskCreateIntent,
12
+ parseNuggetLookupIntent,
13
+ parseRecurringScheduleIntent,
14
+ } from "./nugget-lookup.js";
15
+ import type { OutboundDeps } from "./outbound.js";
16
+ import { createOmadeusReplyDispatcher } from "./reply-dispatcher.js";
17
+ import { getOmadeusChannelConfig } from "./config.js";
18
+ import { getOmadeusRuntime } from "./runtime.js";
19
+ import type { OmadeusInboundMessage } from "./types.js";
20
+
21
+ type Log = {
22
+ info: (msg: string, extra?: Record<string, unknown>) => void;
23
+ warn: (msg: string, extra?: Record<string, unknown>) => void;
24
+ error: (msg: string, extra?: Record<string, unknown>) => void;
25
+ debug?: (msg: string, extra?: Record<string, unknown>) => void;
26
+ };
27
+
28
+ export type OmadeusMessageHandlerDeps = {
29
+ cfg: OpenClawConfig;
30
+ runtime: RuntimeEnv;
31
+ log: Log;
32
+ outboundDeps: OutboundDeps;
33
+ };
34
+
35
+ export function createOmadeusMessageHandler(deps: OmadeusMessageHandlerDeps) {
36
+ const { cfg, runtime, log, outboundDeps } = deps;
37
+ const core = getOmadeusRuntime();
38
+ const omadeusCfg = getOmadeusChannelConfig(cfg);
39
+
40
+ const inboundDebounceMs = core.channel.debounce.resolveInboundDebounceMs({
41
+ cfg,
42
+ channel: "omadeus",
43
+ });
44
+
45
+ const handleMessageNow = async (inbound: OmadeusInboundMessage) => {
46
+ const isDirectMessage = inbound.subscribableKind === "direct";
47
+ const senderId = String(inbound.fromReferenceId);
48
+ const senderName = inbound.from;
49
+ const roomId = String(inbound.roomId);
50
+ const rawBody = inbound.content;
51
+
52
+ if (!rawBody.trim()) {
53
+ log.debug?.("skipping empty message");
54
+ return;
55
+ }
56
+
57
+ const selectedPublicRoomId = omadeusCfg?.selectedChannelPublicRoomId;
58
+ const selectedPrivateRoomId = omadeusCfg?.selectedChannelPrivateRoomId;
59
+ const selectedMemberReferenceId = omadeusCfg?.selectedMemberReferenceId;
60
+ const hasSelectedScope =
61
+ typeof selectedPublicRoomId === "number" || typeof selectedPrivateRoomId === "number";
62
+ let inSelectedChannelRoom = false;
63
+ let isSelectedMemberTaskPrivate = false;
64
+ if (hasSelectedScope) {
65
+ inSelectedChannelRoom =
66
+ inbound.roomId === selectedPublicRoomId || inbound.roomId === selectedPrivateRoomId;
67
+ const isSelectedMember =
68
+ typeof selectedMemberReferenceId !== "number" ||
69
+ inbound.fromReferenceId === selectedMemberReferenceId;
70
+ isSelectedMemberTaskPrivate =
71
+ inbound.subscribableKind === "task" && inbound.isMention && isSelectedMember;
72
+ const allowSelectedChannelMessage = inSelectedChannelRoom && isSelectedMember;
73
+ if (!allowSelectedChannelMessage && !isSelectedMemberTaskPrivate) {
74
+ log.info("omadeus: dropped message outside selected channel scope", {
75
+ roomId: inbound.roomId,
76
+ roomName: inbound.roomName,
77
+ selectedPublicRoomId,
78
+ selectedPrivateRoomId,
79
+ selectedMemberReferenceId,
80
+ kind: inbound.subscribableKind,
81
+ fromReferenceId: inbound.fromReferenceId,
82
+ isMention: inbound.isMention,
83
+ selectedMemberMatched: isSelectedMember,
84
+ });
85
+ return;
86
+ }
87
+ }
88
+
89
+ const useAccessGroups =
90
+ (cfg.commands as Record<string, unknown> | undefined)?.useAccessGroups !== false;
91
+
92
+ // For group messages, only respond when mentioned (unless groupPolicy is open)
93
+ const bypassMentionGate = hasSelectedScope && (inSelectedChannelRoom || isSelectedMemberTaskPrivate);
94
+ if (!isDirectMessage && !inbound.isMention && !bypassMentionGate) {
95
+ log.debug?.("skipping group message (not mentioned)");
96
+ return;
97
+ }
98
+ if (!isDirectMessage && !inbound.isMention && bypassMentionGate) {
99
+ log.info("omadeus: processing selected-scope group message without mention", {
100
+ roomId: inbound.roomId,
101
+ roomName: inbound.roomName,
102
+ kind: inbound.subscribableKind,
103
+ });
104
+ }
105
+
106
+ const hasControlCommand = core.channel.text.hasControlCommand(rawBody, cfg);
107
+ const commandGate = resolveControlCommandGate({
108
+ useAccessGroups,
109
+ authorizers: [],
110
+ allowTextCommands: true,
111
+ hasControlCommand,
112
+ });
113
+
114
+ if (commandGate.shouldBlock) {
115
+ logInboundDrop({
116
+ log: (msg) => log.debug?.(msg),
117
+ channel: "omadeus",
118
+ reason: "control command (unauthorized)",
119
+ target: senderId,
120
+ });
121
+ return;
122
+ }
123
+
124
+ let bodyForAgent = rawBody;
125
+ const createIntent = parseChannelTaskCreateIntent(rawBody);
126
+ if (createIntent) {
127
+ try {
128
+ const memberReferenceId =
129
+ typeof selectedMemberReferenceId === "number"
130
+ ? selectedMemberReferenceId
131
+ : inbound.fromReferenceId;
132
+ const created = await createNugget(outboundDeps.apiOpts, {
133
+ title: createIntent.title,
134
+ description: createIntent.description,
135
+ kind: createIntent.kind,
136
+ priority: createIntent.priority,
137
+ stage: "Triage",
138
+ memberReferenceId,
139
+ clientId: 1,
140
+ folderId: 1,
141
+ });
142
+ const createdLabel =
143
+ typeof created["number"] === "number"
144
+ ? `N${created["number"]}`
145
+ : String(created["id"] ?? "created");
146
+ const recurring = parseRecurringScheduleIntent(rawBody);
147
+ if (recurring && typeof created["number"] === "number") {
148
+ const cronExpr = recurring.everyMinutes === 60 ? "0 * * * *" : `*/${recurring.everyMinutes} * * * *`;
149
+ const taskRoomId = resolveTaskChannelRoomId(created);
150
+ const taskTarget = taskRoomId ? `room:${taskRoomId}` : `N${created["number"]}`;
151
+ bodyForAgent =
152
+ `${rawBody}\n\n` +
153
+ `[Omadeus create] Created ${createIntent.kind} ${createdLabel}.\n` +
154
+ `[Scheduling required] The user asked for recurring execution.\n` +
155
+ `You MUST use the cron tool now (no simulation) to add a job with:\n` +
156
+ `- schedule.kind: "cron"\n` +
157
+ `- schedule.expr: "${cronExpr}"\n` +
158
+ `- payload.kind: "agentTurn"\n` +
159
+ `- payload.message: "${createIntent.description}"\n` +
160
+ `- payload.deliver: true\n` +
161
+ `- payload.channel: "omadeus"\n` +
162
+ `- payload.to: "${taskTarget}"\n` +
163
+ `- sessionTarget: "isolated"\n` +
164
+ `- delivery.mode: "announce"\n` +
165
+ `- delivery.channel: "omadeus"\n` +
166
+ `- delivery.to: "${taskTarget}"\n` +
167
+ `Do NOT deliver to the current selected channel; delivery must go only to the created task private channel target above.\n` +
168
+ `Then confirm cron job creation to the user.`;
169
+ } else {
170
+ bodyForAgent = `${rawBody}\n\n[Omadeus create] Created ${createIntent.kind} ${createdLabel}.`;
171
+ }
172
+ } catch (err) {
173
+ const errorMessage = err instanceof Error ? err.message : String(err);
174
+ runtime.error?.(`omadeus channel-triggered task create failed: ${errorMessage}`);
175
+ }
176
+ }
177
+
178
+ const nuggetIntent = parseNuggetLookupIntent(rawBody);
179
+ if (nuggetIntent) {
180
+ try {
181
+ const nugget = await searchNuggetByNumber(outboundDeps.apiOpts, {
182
+ nuggetNumber: nuggetIntent.nuggetNumber,
183
+ });
184
+ bodyForAgent = appendNuggetLookupContextForAgent(
185
+ rawBody,
186
+ nuggetIntent.nuggetNumber,
187
+ nugget,
188
+ );
189
+ } catch (err) {
190
+ const errorMessage = err instanceof Error ? err.message : String(err);
191
+ runtime.error?.(`omadeus nugget lookup failed: ${errorMessage}`);
192
+ bodyForAgent = appendNuggetLookupContextForAgent(
193
+ rawBody,
194
+ nuggetIntent.nuggetNumber,
195
+ null,
196
+ errorMessage,
197
+ );
198
+ }
199
+ }
200
+
201
+ const omadeusFrom = isDirectMessage ? `omadeus:${senderId}` : `omadeus:group:${roomId}`;
202
+ const omadeusTo = isDirectMessage ? `room:${roomId}` : `room:${roomId}`;
203
+
204
+ const route = core.channel.routing.resolveAgentRoute({
205
+ cfg,
206
+ channel: "omadeus",
207
+ peer: {
208
+ kind: isDirectMessage ? "direct" : "group",
209
+ id: isDirectMessage ? senderId : roomId,
210
+ },
211
+ });
212
+
213
+ const preview = rawBody.replace(/\s+/g, " ").slice(0, 160);
214
+ const inboundLabel = isDirectMessage
215
+ ? `Omadeus DM from ${senderName}`
216
+ : `Omadeus message in ${inbound.subscribableKind}/${inbound.roomName ?? roomId} from ${senderName}`;
217
+
218
+ core.system.enqueueSystemEvent(`${inboundLabel}: ${preview}`, {
219
+ sessionKey: route.sessionKey,
220
+ contextKey: `omadeus:message:${roomId}:${inbound.timestamp}`,
221
+ });
222
+
223
+ const envelopeFrom = isDirectMessage ? senderName : (inbound.roomName ?? roomId);
224
+ const storePath = core.channel.session.resolveStorePath(
225
+ (cfg.session as Record<string, unknown> | undefined)?.store as string | undefined,
226
+ { agentId: route.agentId },
227
+ );
228
+ const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
229
+ const timestamp = inbound.timestamp ? new Date(inbound.timestamp) : undefined;
230
+ const previousTimestamp = core.channel.session.readSessionUpdatedAt({
231
+ storePath,
232
+ sessionKey: route.sessionKey,
233
+ });
234
+
235
+ const body = core.channel.reply.formatAgentEnvelope({
236
+ channel: "Omadeus",
237
+ from: envelopeFrom,
238
+ timestamp,
239
+ previousTimestamp,
240
+ envelope: envelopeOptions,
241
+ body: rawBody,
242
+ });
243
+
244
+ const ctxPayload = core.channel.reply.finalizeInboundContext({
245
+ Body: body,
246
+ BodyForAgent: bodyForAgent,
247
+ RawBody: rawBody,
248
+ CommandBody: rawBody.trim(),
249
+ BodyForCommands: rawBody.trim(),
250
+ /** Lets the message tool default `react` / `edit` to this Jaguar message id. */
251
+ MessageSid: String(inbound.messageId),
252
+ From: omadeusFrom,
253
+ To: omadeusTo,
254
+ SessionKey: route.sessionKey,
255
+ AccountId: route.accountId,
256
+ ChatType: isDirectMessage ? "direct" : "group",
257
+ ConversationLabel: envelopeFrom,
258
+ GroupSubject: !isDirectMessage ? (inbound.roomName ?? inbound.subscribableKind) : undefined,
259
+ SenderName: senderName,
260
+ SenderId: senderId,
261
+ Provider: "omadeus" as const,
262
+ Surface: "omadeus" as const,
263
+ Timestamp: inbound.timestamp ?? Date.now(),
264
+ WasMentioned: isDirectMessage || inbound.isMention,
265
+ CommandAuthorized: commandGate.commandAuthorized,
266
+ OriginatingChannel: "omadeus" as const,
267
+ OriginatingTo: omadeusTo,
268
+ });
269
+
270
+ await core.channel.session.recordInboundSession({
271
+ storePath,
272
+ sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
273
+ ctx: ctxPayload,
274
+ onRecordError: (err) => {
275
+ log.debug?.(`omadeus: failed updating session meta: ${String(err)}`);
276
+ },
277
+ });
278
+
279
+ const { dispatcher, replyOptions, markDispatchIdle } = createOmadeusReplyDispatcher({
280
+ cfg,
281
+ agentId: route.agentId,
282
+ accountId: route.accountId,
283
+ runtime,
284
+ log,
285
+ outboundDeps,
286
+ roomId,
287
+ });
288
+
289
+ log.info("dispatching to agent", { sessionKey: route.sessionKey });
290
+ try {
291
+ const { queuedFinal, counts } = await core.channel.reply.withReplyDispatcher({
292
+ dispatcher,
293
+ onSettled: () => {
294
+ markDispatchIdle();
295
+ },
296
+ run: () =>
297
+ core.channel.reply.dispatchReplyFromConfig({
298
+ ctx: ctxPayload,
299
+ cfg,
300
+ dispatcher,
301
+ replyOptions,
302
+ }),
303
+ });
304
+
305
+ log.info("dispatch complete", { queuedFinal, counts });
306
+ const finalCount = counts.final;
307
+ if (queuedFinal) {
308
+ log.debug?.(
309
+ `omadeus: delivered ${finalCount} repl${finalCount === 1 ? "y" : "ies"} to room ${roomId}`,
310
+ );
311
+ }
312
+ } catch (err) {
313
+ log.error("dispatch failed", { error: String(err) });
314
+ runtime.error?.(`omadeus dispatch failed: ${String(err)}`);
315
+ }
316
+ };
317
+
318
+ const inboundDebouncer = core.channel.debounce.createInboundDebouncer<OmadeusInboundMessage>({
319
+ debounceMs: inboundDebounceMs,
320
+ buildKey: (entry) => {
321
+ return `omadeus:${entry.roomId}:${entry.fromReferenceId}`;
322
+ },
323
+ shouldDebounce: (entry) => {
324
+ if (!entry.content.trim()) return false;
325
+ return !core.channel.text.hasControlCommand(entry.content, cfg);
326
+ },
327
+ onFlush: async (entries) => {
328
+ const last = entries.at(-1);
329
+ if (!last) return;
330
+
331
+ if (entries.length === 1) {
332
+ await handleMessageNow(last);
333
+ return;
334
+ }
335
+
336
+ // Combine debounced messages into a single inbound
337
+ const combinedContent = entries
338
+ .map((e) => e.content)
339
+ .filter(Boolean)
340
+ .join("\n");
341
+ if (!combinedContent.trim()) return;
342
+
343
+ await handleMessageNow({
344
+ ...last,
345
+ content: combinedContent,
346
+ isMention: entries.some((e) => e.isMention),
347
+ });
348
+ },
349
+ onError: (err) => {
350
+ runtime.error?.(`omadeus debounce flush failed: ${String(err)}`);
351
+ },
352
+ });
353
+
354
+ return async function handleOmadeusMessage(inbound: OmadeusInboundMessage) {
355
+ await inboundDebouncer.enqueue(inbound);
356
+ };
357
+ }