@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.
@@ -0,0 +1,196 @@
1
+ export type NuggetLookupIntent = {
2
+ /** Display nugget number from `N###` (maps to API `number`, not internal `id`). */
3
+ nuggetNumber: number;
4
+ };
5
+
6
+ export type TaskChannelTargetIntent = {
7
+ nuggetNumber: number;
8
+ rawPrefix: "n" | "t";
9
+ };
10
+
11
+ export function parseNuggetLookupIntent(rawBody: string): NuggetLookupIntent | null {
12
+ const body = rawBody.trim();
13
+ if (!body) {
14
+ return null;
15
+ }
16
+ const idMatch = /\bN(\d+)\b/i.exec(body);
17
+ if (!idMatch) {
18
+ return null;
19
+ }
20
+ const nuggetNumber = Number(idMatch[1]);
21
+ if (!Number.isFinite(nuggetNumber)) {
22
+ return null;
23
+ }
24
+
25
+ if (/^N\d+\??$/i.test(body)) {
26
+ return { nuggetNumber };
27
+ }
28
+ if (/\bnugget\s+N?\d+\b/i.test(body)) {
29
+ return { nuggetNumber };
30
+ }
31
+ if (/\b(get|show|lookup|find|search|status|detail|info)\b/i.test(body)) {
32
+ return { nuggetNumber };
33
+ }
34
+ return null;
35
+ }
36
+
37
+ export function parseTaskChannelTargetIntent(rawInput: string): TaskChannelTargetIntent | null {
38
+ const trimmed = rawInput.trim();
39
+ const match = /^([nt])(\d+)$/i.exec(trimmed);
40
+ if (!match) {
41
+ return null;
42
+ }
43
+ const nuggetNumber = Number(match[2]);
44
+ if (!Number.isFinite(nuggetNumber)) {
45
+ return null;
46
+ }
47
+ return {
48
+ nuggetNumber,
49
+ rawPrefix: match[1]!.toLowerCase() as "n" | "t",
50
+ };
51
+ }
52
+
53
+ export type ChannelTaskCreateIntent = {
54
+ kind: "task" | "nugget";
55
+ title: string;
56
+ description: string;
57
+ priority: "low" | "medium" | "high" | "urgent";
58
+ };
59
+
60
+ export type RecurringScheduleIntent = {
61
+ everyMinutes: number;
62
+ };
63
+
64
+ function resolvePriorityFromText(text: string): ChannelTaskCreateIntent["priority"] {
65
+ const lowered = text.toLowerCase();
66
+ if (/\b(urgent|asap|critical|p0)\b/.test(lowered)) return "urgent";
67
+ if (/\b(high|important|p1)\b/.test(lowered)) return "high";
68
+ if (/\b(medium|normal|p2)\b/.test(lowered)) return "medium";
69
+ return "low";
70
+ }
71
+
72
+ export function parseChannelTaskCreateIntent(rawBody: string): ChannelTaskCreateIntent | null {
73
+ const body = rawBody.trim();
74
+ if (!body) {
75
+ return null;
76
+ }
77
+ const lower = body.toLowerCase();
78
+ const isCreateVerb = /\b(create|open|add|spawn|start)\b/.test(lower);
79
+ const hasTaskWord = /\b(task|nugget)\b/.test(lower);
80
+ if (!isCreateVerb || !hasTaskWord) {
81
+ return null;
82
+ }
83
+
84
+ const kind: "task" | "nugget" = /\bnugget\b/.test(lower) ? "nugget" : "task";
85
+ // Trim obvious command prefixes to leave a natural title candidate.
86
+ const candidate = body
87
+ .replace(/^\s*(please\s+)?(create|open|add|spawn|start)\s+(a\s+|an\s+)?(new\s+)?/i, "")
88
+ .replace(/^(task|nugget)\s*/i, "")
89
+ .trim();
90
+ const title = candidate || `${kind === "task" ? "Task" : "Nugget"} from channel request`;
91
+ const description = body;
92
+ return {
93
+ kind,
94
+ title,
95
+ description,
96
+ priority: resolvePriorityFromText(body),
97
+ };
98
+ }
99
+
100
+ export function parseRecurringScheduleIntent(rawBody: string): RecurringScheduleIntent | null {
101
+ const lowered = rawBody.toLowerCase();
102
+ // every 5 min / every 5 mins / every 5 minutes
103
+ const minuteMatch = /\bevery\s+(\d+)\s*(m|min|mins|minute|minutes)\b/.exec(lowered);
104
+ if (minuteMatch) {
105
+ const everyMinutes = Number(minuteMatch[1]);
106
+ if (Number.isFinite(everyMinutes) && everyMinutes > 0) {
107
+ return { everyMinutes: Math.min(60, everyMinutes) };
108
+ }
109
+ }
110
+ // every hour / hourly
111
+ if (/\bevery\s+hour\b|\bhourly\b/.test(lowered)) {
112
+ return { everyMinutes: 60 };
113
+ }
114
+ return null;
115
+ }
116
+
117
+ /** Fields from Dolphin nuggetviews that are useful for an agent summary (avoids huge payloads). */
118
+ const NUGGET_FIELDS_FOR_AGENT = [
119
+ "number",
120
+ "id",
121
+ "title",
122
+ "description",
123
+ "status",
124
+ "stage",
125
+ "leadPhaseTitle",
126
+ "priority",
127
+ "priorityValue",
128
+ "dueDate",
129
+ "kind",
130
+ "entityType",
131
+ "tempo",
132
+ "projectTitle",
133
+ "projectStatus",
134
+ "projectNumber",
135
+ "projectManagerFirstName",
136
+ "projectManagerLastName",
137
+ "projectManagerTitle",
138
+ "projectManagerReferenceId",
139
+ "clientTitle",
140
+ "folderTitle",
141
+ "createdAt",
142
+ "autoModifiedAt",
143
+ "lastMovingTime",
144
+ "responseTimestamp",
145
+ "assignmentLevel",
146
+ "estimated",
147
+ "sprintName",
148
+ "sprintNumber",
149
+ "releaseTitle",
150
+ "releaseNumber",
151
+ "publicRoomId",
152
+ "privateRoomId",
153
+ ] as const;
154
+
155
+ export function pickNuggetFieldsForAgent(record: Record<string, unknown>): Record<string, unknown> {
156
+ const out: Record<string, unknown> = {};
157
+ for (const key of NUGGET_FIELDS_FOR_AGENT) {
158
+ if (key in record) {
159
+ out[key] = record[key];
160
+ }
161
+ }
162
+ return out;
163
+ }
164
+
165
+ /**
166
+ * Augments the user message so the agent receives Dolphin nugget/task data and can reply with a summary.
167
+ * On miss or API error, the agent still gets instructions to respond helpfully.
168
+ */
169
+ export function appendNuggetLookupContextForAgent(
170
+ rawBody: string,
171
+ nuggetNumber: number,
172
+ record: Record<string, unknown> | null,
173
+ fetchError?: string,
174
+ ): string {
175
+ const header = `[Omadeus nugget/task N${nuggetNumber}]`;
176
+
177
+ if (fetchError) {
178
+ return (
179
+ `${rawBody}\n\n${header} Lookup failed: ${fetchError}\n` +
180
+ `Briefly explain the error to the user and suggest they try again or check permissions.`
181
+ );
182
+ }
183
+
184
+ if (!record) {
185
+ return (
186
+ `${rawBody}\n\n${header} No row matched display number ${nuggetNumber} (field \`number\`) in search results.\n` +
187
+ `Tell the user succinctly that this nugget/task was not found.`
188
+ );
189
+ }
190
+
191
+ const payload = pickNuggetFieldsForAgent(record);
192
+ return (
193
+ `${rawBody}\n\n${header} Data from Omadeus (summarize for someone tracking this work — status, ownership, timeline, project; plain language):\n` +
194
+ `${JSON.stringify(payload, null, 2)}`
195
+ );
196
+ }
@@ -0,0 +1,434 @@
1
+ import type { ChannelSetupWizard, OpenClawConfig, WizardPrompter } from "openclaw/plugin-sdk/setup";
2
+ import { DEFAULT_ACCOUNT_ID, formatDocsLink } from "openclaw/plugin-sdk/setup";
3
+ import {
4
+ listMemberChannelViews,
5
+ listOrganizationMembers,
6
+ listOrganizations,
7
+ } from "./api/auth.api.js";
8
+ import { authenticate } from "./auth.js";
9
+ import { getOmadeusChannelConfig, resolveOmadeusAccount } from "./config.js";
10
+ import { OMADEUS_CAS_URL, OMADEUS_MAESTRO_URL } from "./defaults.js";
11
+ import type {
12
+ OmadeusChannelConfig,
13
+ OmadeusChannelView,
14
+ OmadeusOrganizationMember,
15
+ } from "./types.js";
16
+
17
+ const channel = "omadeus" as const;
18
+
19
+ type CoreConfig = OpenClawConfig & {
20
+ channels?: { omadeus?: OmadeusChannelConfig };
21
+ };
22
+
23
+ function getOmadeusSection(cfg: OpenClawConfig): OmadeusChannelConfig | undefined {
24
+ return getOmadeusChannelConfig(cfg as CoreConfig);
25
+ }
26
+
27
+ async function noteOmadeusAuthHelp(prompter: WizardPrompter): Promise<void> {
28
+ await prompter.note(
29
+ [
30
+ "Omadeus authenticates via CAS + Maestro (email + password + organization).",
31
+ "You need:",
32
+ " - Email + password",
33
+ " - Organization ID (we can look it up for you)",
34
+ `CAS URL: ${OMADEUS_CAS_URL}`,
35
+ `Maestro URL: ${OMADEUS_MAESTRO_URL}`,
36
+ "Env vars supported: OMADEUS_EMAIL, OMADEUS_PASSWORD, OMADEUS_ORGANIZATION_ID.",
37
+ `Docs: ${formatDocsLink("/channels/omadeus", "omadeus")}`,
38
+ ].join("\n"),
39
+ "Omadeus setup",
40
+ );
41
+ }
42
+
43
+ async function promptOrganizationId(params: {
44
+ prompter: WizardPrompter;
45
+ maestroUrl: string;
46
+ email: string;
47
+ existing?: number;
48
+ }): Promise<number> {
49
+ const { prompter, maestroUrl, email, existing } = params;
50
+
51
+ // Try to list organizations from the API
52
+ if (maestroUrl && email) {
53
+ try {
54
+ const orgs = await listOrganizations({ maestroUrl, email });
55
+ if (orgs.length > 0) {
56
+ if (orgs.length === 1) {
57
+ await prompter.note(
58
+ `Found organization: ${orgs[0]!.title} (${orgs[0]!.id})`,
59
+ "Omadeus organization",
60
+ );
61
+ return orgs[0]!.id;
62
+ }
63
+ const choice = await prompter.select({
64
+ message: "Select organization",
65
+ options: orgs.map((org) => ({
66
+ value: String(org.id),
67
+ label: `${org.title} (${org.membersCount} members)`,
68
+ hint: `ID: ${org.id}`,
69
+ })),
70
+ initialValue: existing ? String(existing) : String(orgs[0]!.id),
71
+ });
72
+ return Number(choice);
73
+ }
74
+ } catch {
75
+ await prompter.note(
76
+ "Could not fetch organizations from the API. Enter the ID manually.",
77
+ "Omadeus organization",
78
+ );
79
+ }
80
+ }
81
+
82
+ const raw = await prompter.text({
83
+ message: "Organization ID (number)",
84
+ initialValue: existing ? String(existing) : undefined,
85
+ validate: (value) => {
86
+ const trimmed = String(value ?? "").trim();
87
+ if (!trimmed) return "Required";
88
+ if (!/^\d+$/.test(trimmed)) return "Must be a number";
89
+ return undefined;
90
+ },
91
+ });
92
+ return Number(String(raw).trim());
93
+ }
94
+
95
+ async function promptChannelSelection(params: {
96
+ prompter: WizardPrompter;
97
+ maestroUrl: string;
98
+ sessionToken: string;
99
+ memberReferenceId: number;
100
+ existing?: OmadeusChannelConfig;
101
+ }): Promise<{
102
+ selectedChannelViewId: number;
103
+ selectedChannelTitle: string;
104
+ selectedChannelPrivateRoomId?: number;
105
+ selectedChannelPublicRoomId?: number;
106
+ }> {
107
+ const { prompter, maestroUrl, sessionToken, memberReferenceId, existing } = params;
108
+ const channels = await listMemberChannelViews({
109
+ maestroUrl,
110
+ sessionToken,
111
+ memberReferenceId,
112
+ skip: 0,
113
+ take: 100,
114
+ });
115
+ if (channels.length === 0) {
116
+ throw new Error("No channels found for this account.");
117
+ }
118
+ const selected = await prompter.select({
119
+ message: "Which channel to use?",
120
+ options: channels.map((item) => ({
121
+ value: String(item.id),
122
+ label: item.title || `Channel ${item.id}`,
123
+ })),
124
+ initialValue:
125
+ existing?.selectedChannelViewId !== undefined
126
+ ? String(existing.selectedChannelViewId)
127
+ : String(channels[0]!.id),
128
+ });
129
+ const chosen = channels.find((item) => String(item.id) === String(selected));
130
+ if (!chosen) {
131
+ throw new Error("Selected channel was not found.");
132
+ }
133
+ return {
134
+ selectedChannelViewId: chosen.id,
135
+ selectedChannelTitle: chosen.title,
136
+ ...(typeof chosen.privateRoomId === "number"
137
+ ? { selectedChannelPrivateRoomId: chosen.privateRoomId }
138
+ : {}),
139
+ ...(typeof chosen.publicRoomId === "number"
140
+ ? { selectedChannelPublicRoomId: chosen.publicRoomId }
141
+ : {}),
142
+ };
143
+ }
144
+
145
+ function memberLabel(member: OmadeusOrganizationMember): string {
146
+ const fullName = `${member.firstName ?? ""} ${member.lastName ?? ""}`.trim();
147
+ if (member.title?.trim()) {
148
+ return member.title.trim();
149
+ }
150
+ if (fullName) {
151
+ return fullName;
152
+ }
153
+ if (member.email?.trim()) {
154
+ return member.email.trim();
155
+ }
156
+ return `Member ${member.referenceId}`;
157
+ }
158
+
159
+ function memberHint(member: OmadeusOrganizationMember): string | undefined {
160
+ const fullName = `${member.firstName ?? ""} ${member.lastName ?? ""}`.trim();
161
+ const parts = [fullName, member.email?.trim(), `ref:${member.referenceId}`].filter(Boolean);
162
+ return parts.length > 0 ? parts.join(" | ") : undefined;
163
+ }
164
+
165
+ function filterMembersByQuery(
166
+ members: OmadeusOrganizationMember[],
167
+ query: string,
168
+ ): OmadeusOrganizationMember[] {
169
+ const q = query.trim().toLowerCase();
170
+ if (!q) {
171
+ return members;
172
+ }
173
+ return members.filter((member) => {
174
+ const fields = [
175
+ String(member.referenceId),
176
+ member.title ?? "",
177
+ member.firstName ?? "",
178
+ member.lastName ?? "",
179
+ member.email ?? "",
180
+ ].map((value) => value.toLowerCase());
181
+ return fields.some((value) => value.includes(q));
182
+ });
183
+ }
184
+
185
+ async function promptMemberSelection(params: {
186
+ prompter: WizardPrompter;
187
+ maestroUrl: string;
188
+ sessionToken: string;
189
+ organizationId: number;
190
+ existingMemberReferenceId?: number;
191
+ fallbackMemberReferenceId?: number;
192
+ }): Promise<{ memberReferenceId: number; memberTitle: string }> {
193
+ const {
194
+ prompter,
195
+ maestroUrl,
196
+ sessionToken,
197
+ organizationId,
198
+ existingMemberReferenceId,
199
+ fallbackMemberReferenceId,
200
+ } = params;
201
+
202
+ const members = (
203
+ await listOrganizationMembers({
204
+ maestroUrl,
205
+ sessionToken,
206
+ organizationId,
207
+ })
208
+ )
209
+ .filter((member) => member.isSystem !== true)
210
+ .sort((a, b) => memberLabel(a).localeCompare(memberLabel(b)));
211
+
212
+ if (members.length === 0) {
213
+ throw new Error("No organization members found.");
214
+ }
215
+
216
+ while (true) {
217
+ const query = String(
218
+ await prompter.text({
219
+ message: "Search member to listen to (name/title/email/referenceId, optional)",
220
+ placeholder: "e.g. John Doe",
221
+ }),
222
+ );
223
+ const filtered = filterMembersByQuery(members, query).slice(0, 100);
224
+ if (filtered.length === 0) {
225
+ await prompter.note("No members matched that search. Try another query.", "Omadeus member");
226
+ continue;
227
+ }
228
+
229
+ const defaultRef = existingMemberReferenceId ?? fallbackMemberReferenceId;
230
+ const selected = await prompter.select({
231
+ message: "Which member should OpenClaw listen to?",
232
+ options: filtered.map((member) => ({
233
+ value: String(member.referenceId),
234
+ label: memberLabel(member),
235
+ hint: memberHint(member),
236
+ })),
237
+ initialValue:
238
+ defaultRef !== undefined && filtered.some((member) => member.referenceId === defaultRef)
239
+ ? String(defaultRef)
240
+ : String(filtered[0]!.referenceId),
241
+ });
242
+ const chosen = filtered.find((member) => String(member.referenceId) === String(selected));
243
+ if (!chosen) {
244
+ await prompter.note("Could not resolve selected member. Please retry.", "Omadeus member");
245
+ continue;
246
+ }
247
+ return {
248
+ memberReferenceId: chosen.referenceId,
249
+ memberTitle: memberLabel(chosen),
250
+ };
251
+ }
252
+ }
253
+
254
+ export const omadeusSetupWizard: ChannelSetupWizard = {
255
+ channel,
256
+ resolveAccountIdForConfigure: () => DEFAULT_ACCOUNT_ID,
257
+ resolveShouldPromptAccountIds: () => false,
258
+ status: {
259
+ configuredLabel: "configured",
260
+ unconfiguredLabel: "needs credentials",
261
+ configuredHint: "configured",
262
+ unconfiguredHint: "needs credentials",
263
+ configuredScore: 2,
264
+ unconfiguredScore: 0,
265
+ resolveConfigured: ({ cfg }) => {
266
+ const account = resolveOmadeusAccount({ cfg });
267
+ return account.credentialSource !== "none";
268
+ },
269
+ resolveStatusLines: ({ cfg }) => {
270
+ const account = resolveOmadeusAccount({ cfg });
271
+ const configured = account.credentialSource !== "none";
272
+ return [
273
+ `Omadeus: ${configured ? "configured" : "needs email, password, and organization ID"}`,
274
+ ];
275
+ },
276
+ resolveSelectionHint: ({ cfg }) => {
277
+ const account = resolveOmadeusAccount({ cfg });
278
+ return account.credentialSource !== "none" ? "configured" : "needs credentials";
279
+ },
280
+ resolveQuickstartScore: ({ cfg }) => {
281
+ const account = resolveOmadeusAccount({ cfg });
282
+ return account.credentialSource !== "none" ? 2 : 0;
283
+ },
284
+ },
285
+ credentials: [],
286
+ finalize: async ({ cfg, prompter }) => {
287
+ const account = resolveOmadeusAccount({ cfg });
288
+ const section = getOmadeusSection(cfg) ?? {};
289
+ let next = cfg;
290
+
291
+ if (account.credentialSource === "none") {
292
+ await noteOmadeusAuthHelp(prompter);
293
+ }
294
+
295
+ const envEmail = process.env.OMADEUS_EMAIL?.trim();
296
+ const envPassword = process.env.OMADEUS_PASSWORD?.trim();
297
+
298
+ const casUrl = OMADEUS_CAS_URL;
299
+ const maestroUrl = OMADEUS_MAESTRO_URL;
300
+
301
+ let email = String(
302
+ await prompter.text({
303
+ message: "Omadeus username (email)",
304
+ initialValue: section.email ?? envEmail,
305
+ validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
306
+ }),
307
+ ).trim();
308
+
309
+ let password = String(
310
+ await prompter.text({
311
+ message: "Omadeus password",
312
+ initialValue: section.password ?? envPassword,
313
+ validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
314
+ }),
315
+ ).trim();
316
+
317
+ const organizationId = await promptOrganizationId({
318
+ prompter,
319
+ maestroUrl,
320
+ email,
321
+ existing: section.organizationId,
322
+ });
323
+
324
+ // Verify the full auth flow before saving
325
+ let sessionToken: string | undefined;
326
+ let selfReferenceId: number | undefined;
327
+ while (true) {
328
+ try {
329
+ const { dolphinToken, payload } = await authenticate({
330
+ casUrl,
331
+ maestroUrl,
332
+ email,
333
+ password,
334
+ organizationId,
335
+ });
336
+ sessionToken = dolphinToken;
337
+ selfReferenceId = payload.referenceId;
338
+ await prompter.note(`Authenticated as ${payload.email}`, "Omadeus authentication");
339
+ break;
340
+ } catch (err) {
341
+ const msg = err instanceof Error ? err.message : String(err);
342
+ await prompter.note(`Authentication failed: ${msg}`, "Omadeus authentication");
343
+ const retry = await prompter.confirm({
344
+ message: "Re-enter email/password and try again?",
345
+ initialValue: true,
346
+ });
347
+ if (!retry) {
348
+ await prompter.note(
349
+ "Saving config without verifying credentials. The gateway may fail to connect.",
350
+ "Omadeus authentication",
351
+ );
352
+ break;
353
+ }
354
+ email = String(
355
+ await prompter.text({
356
+ message: "Omadeus email",
357
+ initialValue: email,
358
+ validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
359
+ }),
360
+ ).trim();
361
+ password = String(
362
+ await prompter.text({
363
+ message: "Omadeus password",
364
+ initialValue: password,
365
+ validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
366
+ }),
367
+ ).trim();
368
+ }
369
+ }
370
+
371
+ if (!sessionToken) {
372
+ throw new Error("Authentication is required to list channels.");
373
+ }
374
+
375
+ const selectedMember = await promptMemberSelection({
376
+ prompter,
377
+ maestroUrl,
378
+ sessionToken,
379
+ organizationId,
380
+ existingMemberReferenceId: section.selectedMemberReferenceId,
381
+ fallbackMemberReferenceId: selfReferenceId,
382
+ });
383
+
384
+ const selectedChannel = await promptChannelSelection({
385
+ prompter,
386
+ maestroUrl,
387
+ sessionToken,
388
+ memberReferenceId: selectedMember.memberReferenceId,
389
+ existing: section,
390
+ });
391
+
392
+ await prompter.note(
393
+ `Omadeus will process only "${selectedChannel.selectedChannelTitle}" for member ${selectedMember.memberTitle} (${selectedMember.memberReferenceId}), plus task private-chat mentions.`,
394
+ "Omadeus channel scope",
395
+ );
396
+
397
+ next = {
398
+ ...next,
399
+ channels: {
400
+ ...next.channels,
401
+ omadeus: {
402
+ ...getOmadeusSection(next),
403
+ enabled: true,
404
+ casUrl,
405
+ maestroUrl,
406
+ email,
407
+ password,
408
+ organizationId,
409
+ ...(sessionToken ? { sessionToken } : {}),
410
+ selectedMemberReferenceId: selectedMember.memberReferenceId,
411
+ selectedChannelViewId: selectedChannel.selectedChannelViewId,
412
+ selectedChannelTitle: selectedChannel.selectedChannelTitle,
413
+ ...(selectedChannel.selectedChannelPrivateRoomId !== undefined
414
+ ? { selectedChannelPrivateRoomId: selectedChannel.selectedChannelPrivateRoomId }
415
+ : {}),
416
+ ...(selectedChannel.selectedChannelPublicRoomId !== undefined
417
+ ? { selectedChannelPublicRoomId: selectedChannel.selectedChannelPublicRoomId }
418
+ : {}),
419
+ },
420
+ },
421
+ };
422
+
423
+ return { cfg: next, accountId: DEFAULT_ACCOUNT_ID };
424
+ },
425
+ disable: (cfg) => ({
426
+ ...cfg,
427
+ channels: {
428
+ ...cfg.channels,
429
+ omadeus: { ...getOmadeusSection(cfg), enabled: false },
430
+ },
431
+ }),
432
+ };
433
+
434
+ export const omadeusOnboardingAdapter = omadeusSetupWizard;
@@ -0,0 +1,26 @@
1
+ import { sendRoomMessage } from "./api/message.api.js";
2
+ import type { JaguarSocketClient } from "./socket/jaguar.socket.js";
3
+ import type { OmadeusApiOptions } from "./utils/http.util.js";
4
+
5
+ export type OutboundDeps = {
6
+ apiOpts: OmadeusApiOptions;
7
+ jaguarSocket: JaguarSocketClient;
8
+ };
9
+
10
+ export async function sendOmadeusMessage(
11
+ deps: OutboundDeps,
12
+ params: { to: string; text: string },
13
+ ): Promise<{ channel: string; messageId: string; chatId: string }> {
14
+ const { to, text } = params;
15
+
16
+ const result = await sendRoomMessage(deps.apiOpts, { roomId: to, body: text });
17
+ if (!result.ok) {
18
+ throw new Error(`Omadeus send failed: ${result.error}`);
19
+ }
20
+
21
+ return {
22
+ channel: "omadeus",
23
+ messageId: String(result.message?.id ?? ""),
24
+ chatId: to,
25
+ };
26
+ }