@brantrusnak/openclaw-omadeus 1.0.2 → 1.0.4

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.
Files changed (41) hide show
  1. package/README.md +15 -61
  2. package/dist/_virtual/_rolldown/runtime.js +4 -0
  3. package/dist/api.js +5 -0
  4. package/dist/index.js +14 -0
  5. package/dist/runtime-api.js +15 -0
  6. package/dist/setup-entry.js +7 -0
  7. package/dist/src/allowed-reaction-emojis.js +21 -0
  8. package/dist/src/api/auth.api.js +118 -0
  9. package/dist/src/api/channel.api.js +23 -0
  10. package/dist/src/api/message.api.js +76 -0
  11. package/dist/src/api/nugget.api.js +127 -0
  12. package/dist/src/auth.js +30 -0
  13. package/dist/src/channel.js +626 -0
  14. package/dist/src/config.js +52 -0
  15. package/dist/src/defaults.js +5 -0
  16. package/dist/src/inbound-policy.js +205 -0
  17. package/dist/src/inbound.js +97 -0
  18. package/dist/src/member-resolve.js +53 -0
  19. package/dist/src/message-handler.js +262 -0
  20. package/dist/src/nugget-lookup.js +140 -0
  21. package/dist/src/onboarding.js +357 -0
  22. package/dist/src/outbound.js +17 -0
  23. package/dist/src/reply-dispatcher.js +46 -0
  24. package/dist/src/runtime.js +5 -0
  25. package/dist/src/setup-core.js +46 -0
  26. package/dist/src/setup-surface.js +2 -0
  27. package/dist/src/socket/dolphin.socket.js +18 -0
  28. package/dist/src/socket/jaguar.socket.js +22 -0
  29. package/dist/src/socket/socket.js +153 -0
  30. package/dist/src/store.js +13 -0
  31. package/dist/src/token.js +84 -0
  32. package/dist/src/types.js +15 -0
  33. package/dist/src/utils/http.util.js +43 -0
  34. package/dist/src/utils/jwt.util.js +15 -0
  35. package/package.json +10 -3
  36. package/src/api/auth.api.ts +27 -7
  37. package/src/channel.ts +127 -238
  38. package/src/member-resolve.ts +1 -1
  39. package/src/onboarding.ts +117 -163
  40. package/src/setup-core.ts +10 -1
  41. package/src/socket/socket.ts +24 -11
@@ -0,0 +1,140 @@
1
+ import { readNuggetNumber } from "./api/nugget.api.js";
2
+ import { mergePeopleIntoNuggetAgentPayload } from "./member-resolve.js";
3
+ //#region src/nugget-lookup.ts
4
+ function parseNuggetLookupIntent(rawBody) {
5
+ const body = rawBody.trim();
6
+ if (!body) return null;
7
+ const idMatch = /\bN(\d+)\b/i.exec(body);
8
+ if (!idMatch) return null;
9
+ const nuggetNumber = Number(idMatch[1]);
10
+ if (!Number.isFinite(nuggetNumber)) return null;
11
+ if (/^N\d+\??$/i.test(body)) return { nuggetNumber };
12
+ if (/\bnugget\s+N?\d+\b/i.test(body)) return { nuggetNumber };
13
+ if (/\b(get|show|lookup|find|search|status|detail|info)\b/i.test(body)) return { nuggetNumber };
14
+ return null;
15
+ }
16
+ function parseTaskChannelTargetIntent(rawInput) {
17
+ const trimmed = rawInput.trim();
18
+ const match = /^([nt])(\d+)$/i.exec(trimmed);
19
+ if (!match) return null;
20
+ const nuggetNumber = Number(match[2]);
21
+ if (!Number.isFinite(nuggetNumber)) return null;
22
+ return {
23
+ nuggetNumber,
24
+ rawPrefix: match[1].toLowerCase()
25
+ };
26
+ }
27
+ function resolvePriorityFromText(text) {
28
+ const lowered = text.toLowerCase();
29
+ if (/\b(urgent|asap|critical|p0)\b/.test(lowered)) return "urgent";
30
+ if (/\b(high|important|p1)\b/.test(lowered)) return "high";
31
+ if (/\b(medium|normal|p2)\b/.test(lowered)) return "medium";
32
+ return "low";
33
+ }
34
+ function parseChannelTaskCreateIntent(rawBody) {
35
+ const body = rawBody.trim();
36
+ if (!body) return null;
37
+ const lower = body.toLowerCase();
38
+ const isCreateVerb = /\b(create|open|add|spawn|start)\b/.test(lower);
39
+ const hasTaskWord = /\b(task|nugget)\b/.test(lower);
40
+ if (!isCreateVerb || !hasTaskWord) return null;
41
+ const kind = /\bnugget\b/.test(lower) ? "nugget" : "task";
42
+ return {
43
+ kind,
44
+ title: body.replace(/^\s*(please\s+)?(create|open|add|spawn|start)\s+(a\s+|an\s+)?(new\s+)?/i, "").replace(/^(task|nugget)\s*/i, "").trim() || `${kind === "task" ? "Task" : "Nugget"} from channel request`,
45
+ description: body,
46
+ priority: resolvePriorityFromText(body)
47
+ };
48
+ }
49
+ function parseRecurringScheduleIntent(rawBody) {
50
+ const lowered = rawBody.toLowerCase();
51
+ const minuteMatch = /\bevery\s+(\d+)\s*(m|min|mins|minute|minutes)\b/.exec(lowered);
52
+ if (minuteMatch) {
53
+ const everyMinutes = Number(minuteMatch[1]);
54
+ if (Number.isFinite(everyMinutes) && everyMinutes > 0) return { everyMinutes: Math.min(60, everyMinutes) };
55
+ }
56
+ if (/\bevery\s+hour\b|\bhourly\b/.test(lowered)) return { everyMinutes: 60 };
57
+ return null;
58
+ }
59
+ /** Fields from Dolphin nuggetviews that are useful for an agent summary (avoids huge payloads). */
60
+ const NUGGET_FIELDS_FOR_AGENT = [
61
+ "number",
62
+ "id",
63
+ "title",
64
+ "description",
65
+ "status",
66
+ "stage",
67
+ "leadPhaseTitle",
68
+ "priority",
69
+ "priorityValue",
70
+ "dueDate",
71
+ "kind",
72
+ "entityType",
73
+ "tempo",
74
+ "projectTitle",
75
+ "projectStatus",
76
+ "projectNumber",
77
+ "projectManagerFirstName",
78
+ "projectManagerLastName",
79
+ "projectManagerTitle",
80
+ "projectManagerReferenceId",
81
+ "clientTitle",
82
+ "folderTitle",
83
+ "createdAt",
84
+ "autoModifiedAt",
85
+ "lastMovingTime",
86
+ "responseTimestamp",
87
+ "assignmentLevel",
88
+ "estimated",
89
+ "sprintName",
90
+ "sprintNumber",
91
+ "releaseTitle",
92
+ "releaseNumber",
93
+ "publicRoomId",
94
+ "privateRoomId",
95
+ "memberReferenceId",
96
+ "assigneeReferenceId",
97
+ "ownerReferenceId",
98
+ "memberFirstName",
99
+ "memberLastName",
100
+ "assigneeFirstName",
101
+ "assigneeLastName"
102
+ ];
103
+ function pickNuggetFieldsForAgent(record) {
104
+ const out = {};
105
+ for (const key of NUGGET_FIELDS_FOR_AGENT) if (key in record) out[key] = record[key];
106
+ return out;
107
+ }
108
+ /**
109
+ * Picked Dolphin fields + `people` map (referenceId → display name) for the organization.
110
+ */
111
+ async function buildNuggetAgentDataPayload(apiOpts, fullRecord) {
112
+ if (!fullRecord) return null;
113
+ return mergePeopleIntoNuggetAgentPayload(apiOpts, fullRecord, pickNuggetFieldsForAgent(fullRecord));
114
+ }
115
+ /**
116
+ * Augments the user message so the agent receives Dolphin nugget/task data and can reply with a summary.
117
+ * On miss or API error, the agent still gets instructions to respond helpfully.
118
+ */
119
+ async function appendNuggetLookupContextForAgent(rawBody, nuggetNumber, record, apiOpts, fetchError) {
120
+ const header = `[Omadeus nugget/task N${nuggetNumber}]`;
121
+ if (fetchError) return `${rawBody}\n\n${header} Lookup failed: ${fetchError}\nBriefly explain the error to the user and suggest they try again or check permissions.`;
122
+ if (!record) return `${rawBody}\n\n${header} No row matched display number ${nuggetNumber} (field \`number\`) in search results.\nTell the user succinctly that this nugget/task was not found.`;
123
+ const payload = await buildNuggetAgentDataPayload(apiOpts, record) ?? pickNuggetFieldsForAgent(record);
124
+ return `${rawBody}\n\n${header} Data from Omadeus (summarize for someone tracking this work — status, ownership, timeline, project; plain language. **For assignees and anyone in \`people\` / \`*FirstName\` fields, use those names; never read raw *ReferenceId numbers to the user as a person.**):\n${JSON.stringify(payload, null, 2)}`;
125
+ }
126
+ /**
127
+ * Enriches a Task or Nugget **Jaguar room** with Dolphin data matched by this chat's `roomId`, so the
128
+ * agent can answer "status" without a bare `N###` in the message.
129
+ */
130
+ async function appendNuggetContextForTaskOrNuggetRoom(rawBody, roomId, roomName, record, apiOpts, fetchError) {
131
+ const roomLabel = roomName?.trim() ? `room ${roomId} ("${roomName.trim()}")` : `room ${roomId}`;
132
+ if (fetchError) return `${rawBody}\n\n[Omadeus, this task/nugget ${roomLabel}] Lookup failed: ${fetchError}.\nAnswer from this thread. Do not tell the user to "use the Omadeus platform" or similar — give a direct reply or a concrete next step.`;
133
+ if (!record) return `${rawBody}\n\n[Omadeus, this task/nugget ${roomLabel}] No Dolphin row matched this room id in search yet.\nAnswer from the conversation; be honest if you cannot see live fields. Do not hand-wave to "the platform".`;
134
+ const n = readNuggetNumber(record);
135
+ const nLabel = n !== void 0 ? `N${n}` : "nugget";
136
+ const payload = await buildNuggetAgentDataPayload(apiOpts, record) ?? pickNuggetFieldsForAgent(record);
137
+ return `${rawBody}\n\n[Omadeus ${nLabel} for this chat room] The following is live task/nugget data — **answer the user with this** (stage, status, title, who, due date; plain language. **For assignee/owner, use \`people\` and name fields; never recite *ReferenceId numbers (e.g. 210) as a person's name.**). \`task/...\` in the UI is not an OpenClaw session key.\n${JSON.stringify(payload, null, 2)}`;
138
+ }
139
+ //#endregion
140
+ export { appendNuggetContextForTaskOrNuggetRoom, appendNuggetLookupContextForAgent, parseChannelTaskCreateIntent, parseNuggetLookupIntent, parseRecurringScheduleIntent, parseTaskChannelTargetIntent };
@@ -0,0 +1,357 @@
1
+ import { OMADEUS_CAS_URL, OMADEUS_MAESTRO_URL } from "./defaults.js";
2
+ import { getOmadeusChannelConfig, resolveOmadeusAccount } from "./config.js";
3
+ import { listOrganizationMembers, listOrganizations } from "./api/auth.api.js";
4
+ import { formatMemberLabel } from "./member-resolve.js";
5
+ import { OMADEUS_INBOUND_ENTITY_KINDS } from "./types.js";
6
+ import { listMemberChannelViews } from "./api/channel.api.js";
7
+ import { authenticate } from "./auth.js";
8
+ import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
9
+ //#region src/onboarding.ts
10
+ const channel = "omadeus";
11
+ function formatAuthError(err) {
12
+ if (!(err instanceof Error)) return String(err);
13
+ const parts = [err.message];
14
+ const { cause } = err;
15
+ if (cause instanceof Error) {
16
+ parts.push(cause.message);
17
+ const code = cause.code;
18
+ if (typeof code === "string" && code) parts.push(`(${code})`);
19
+ } else if (typeof cause === "string" && cause.trim()) parts.push(cause);
20
+ return parts.join(" — ");
21
+ }
22
+ async function noteOmadeusAuthHelp(prompter) {
23
+ await prompter.note([
24
+ "Omadeus authenticates via CAS + Maestro (email + password + organization).",
25
+ "You need:",
26
+ " - Email + password",
27
+ " - Organization ID (we can look it up for you)",
28
+ `CAS URL: ${OMADEUS_CAS_URL}`,
29
+ `Maestro URL: ${OMADEUS_MAESTRO_URL}`,
30
+ "Env vars supported: OMADEUS_EMAIL, OMADEUS_PASSWORD, OMADEUS_ORGANIZATION_ID."
31
+ ].join("\n"), "Omadeus setup");
32
+ }
33
+ async function promptOrganizationId(params) {
34
+ const { prompter, maestroUrl, email, existing } = params;
35
+ try {
36
+ const orgs = await listOrganizations({
37
+ maestroUrl,
38
+ email
39
+ });
40
+ if (orgs.length > 0) {
41
+ if (orgs.length === 1) {
42
+ await prompter.note(`Found organization: ${orgs[0].title} (${orgs[0].id})`, "Omadeus organization");
43
+ return orgs[0].id;
44
+ }
45
+ const choice = await prompter.select({
46
+ message: "Select organization",
47
+ options: orgs.map((org) => ({
48
+ value: String(org.id),
49
+ label: `${org.title} (${org.membersCount} members)`,
50
+ hint: `ID: ${org.id}`
51
+ })),
52
+ initialValue: existing ? String(existing) : String(orgs[0].id)
53
+ });
54
+ return Number(choice);
55
+ }
56
+ } catch {
57
+ await prompter.note("Could not fetch organizations from the API. Enter the ID manually.", "Omadeus organization");
58
+ }
59
+ const raw = await prompter.text({
60
+ message: "Organization ID (number)",
61
+ initialValue: existing ? String(existing) : void 0,
62
+ validate: (value) => {
63
+ const trimmed = String(value ?? "").trim();
64
+ if (!trimmed) return "Required";
65
+ if (!/^\d+$/.test(trimmed)) return "Must be a number";
66
+ }
67
+ });
68
+ return Number(String(raw).trim());
69
+ }
70
+ async function promptChannelSelection(params) {
71
+ const { prompter, maestroUrl, sessionToken, memberReferenceId, existingChannelViewIds } = params;
72
+ const channels = await listMemberChannelViews({
73
+ maestroUrl,
74
+ sessionToken,
75
+ memberReferenceId,
76
+ skip: 0,
77
+ take: 100
78
+ });
79
+ if (channels.length === 0) {
80
+ await prompter.note("No channels found for this account. Channel listening will stay disabled.", "Omadeus channels");
81
+ return [];
82
+ }
83
+ if (!await prompter.confirm({
84
+ message: "Listen for messages in Omadeus channels?",
85
+ initialValue: (existingChannelViewIds?.length ?? 0) > 0
86
+ })) return [];
87
+ const selected = await promptMultiSelect({
88
+ prompter,
89
+ message: "Which channels should OpenClaw listen to?",
90
+ options: channels.map((item) => ({
91
+ value: String(item.id),
92
+ label: item.title || `Channel ${item.id}`,
93
+ hint: [item.privateRoomId ? `private:${item.privateRoomId}` : void 0, item.publicRoomId ? `public:${item.publicRoomId}` : void 0].filter(Boolean).join(" | ")
94
+ })),
95
+ initialValues: existingChannelViewIds && existingChannelViewIds.length > 0 ? existingChannelViewIds.map(String) : void 0
96
+ });
97
+ return channels.filter((item) => selected.includes(String(item.id)));
98
+ }
99
+ function memberHint(member) {
100
+ const parts = [
101
+ `${member.firstName ?? ""} ${member.lastName ?? ""}`.trim(),
102
+ member.email?.trim(),
103
+ `ref:${member.referenceId}`
104
+ ].filter(Boolean);
105
+ return parts.length > 0 ? parts.join(" | ") : void 0;
106
+ }
107
+ async function promptMultiSelect(params) {
108
+ return params.prompter.multiselect({
109
+ message: params.message,
110
+ options: params.options,
111
+ initialValues: params.initialValues
112
+ });
113
+ }
114
+ async function loadSelectableMembers(params) {
115
+ const excluded = new Set(params.excludeReferenceIds ?? []);
116
+ return (await listOrganizationMembers({
117
+ maestroUrl: params.maestroUrl,
118
+ sessionToken: params.sessionToken,
119
+ organizationId: params.organizationId
120
+ })).filter((member) => member.isSystem !== true && !excluded.has(member.referenceId)).sort((a, b) => formatMemberLabel(a).localeCompare(formatMemberLabel(b)));
121
+ }
122
+ function memberOptions(members) {
123
+ return members.map((member) => ({
124
+ value: String(member.referenceId),
125
+ label: formatMemberLabel(member),
126
+ hint: memberHint(member)
127
+ }));
128
+ }
129
+ function readReferenceIds(values) {
130
+ return values.map((value) => Number(value)).filter((value) => Number.isInteger(value) && value > 0);
131
+ }
132
+ async function promptCredentials(prompter, existing) {
133
+ return {
134
+ email: String(await prompter.text({
135
+ message: "Omadeus username (email)",
136
+ initialValue: existing.email,
137
+ validate: (value) => String(value ?? "").trim() ? void 0 : "Required"
138
+ })).trim(),
139
+ password: String(await prompter.text({
140
+ message: "Omadeus password",
141
+ sensitive: true,
142
+ validate: (value) => String(value ?? "").trim() ? void 0 : "Required"
143
+ })).trim()
144
+ };
145
+ }
146
+ async function promptSenderAllowlist(params) {
147
+ const { prompter, message, members, existingReferenceIds } = params;
148
+ if (members.length === 0) throw new Error("No organization members found.");
149
+ if (await prompter.select({
150
+ message,
151
+ options: [{
152
+ value: "all",
153
+ label: "All users",
154
+ hint: "No sender allowlist"
155
+ }, {
156
+ value: "specific",
157
+ label: "Specific users",
158
+ hint: "Select one or more users"
159
+ }],
160
+ initialValue: existingReferenceIds && existingReferenceIds.length > 0 ? "specific" : "all"
161
+ }) === "all") return;
162
+ return readReferenceIds(await promptMultiSelect({
163
+ prompter,
164
+ message: `${message} (specific users)`,
165
+ options: memberOptions(members),
166
+ initialValues: existingReferenceIds?.map(String)
167
+ }));
168
+ }
169
+ async function promptEntityKindSelection(params) {
170
+ const selected = await promptMultiSelect({
171
+ prompter: params.prompter,
172
+ message: "Which entity room types should OpenClaw listen to?",
173
+ options: OMADEUS_INBOUND_ENTITY_KINDS.map((kind) => ({
174
+ value: kind,
175
+ label: kind
176
+ })),
177
+ initialValues: params.existingKinds && params.existingKinds.length > 0 ? params.existingKinds : [...OMADEUS_INBOUND_ENTITY_KINDS]
178
+ });
179
+ const selectedSet = new Set(selected);
180
+ return OMADEUS_INBOUND_ENTITY_KINDS.filter((kind) => selectedSet.has(kind));
181
+ }
182
+ const omadeusSetupWizard = {
183
+ channel,
184
+ resolveAccountIdForConfigure: () => DEFAULT_ACCOUNT_ID,
185
+ resolveShouldPromptAccountIds: () => false,
186
+ status: {
187
+ configuredLabel: "configured",
188
+ unconfiguredLabel: "needs credentials",
189
+ configuredHint: "configured",
190
+ unconfiguredHint: "needs credentials",
191
+ configuredScore: 2,
192
+ unconfiguredScore: 0,
193
+ resolveConfigured: ({ cfg }) => {
194
+ return resolveOmadeusAccount({ cfg }).credentialSource !== "none";
195
+ },
196
+ resolveStatusLines: ({ cfg }) => {
197
+ return [`Omadeus: ${resolveOmadeusAccount({ cfg }).credentialSource !== "none" ? "configured" : "needs email, password, and organization ID"}`];
198
+ },
199
+ resolveSelectionHint: ({ cfg }) => {
200
+ return resolveOmadeusAccount({ cfg }).credentialSource !== "none" ? "configured" : "needs credentials";
201
+ },
202
+ resolveQuickstartScore: ({ cfg }) => {
203
+ return resolveOmadeusAccount({ cfg }).credentialSource !== "none" ? 2 : 0;
204
+ }
205
+ },
206
+ credentials: [],
207
+ finalize: async ({ cfg, prompter }) => {
208
+ const account = resolveOmadeusAccount({ cfg });
209
+ const section = getOmadeusChannelConfig(cfg) ?? {};
210
+ let next = cfg;
211
+ if (account.credentialSource === "none") await noteOmadeusAuthHelp(prompter);
212
+ const envEmail = process.env.OMADEUS_EMAIL?.trim();
213
+ const envPassword = process.env.OMADEUS_PASSWORD?.trim();
214
+ const casUrl = OMADEUS_CAS_URL;
215
+ const maestroUrl = OMADEUS_MAESTRO_URL;
216
+ let { email, password } = await promptCredentials(prompter, {
217
+ email: section.email ?? envEmail,
218
+ password: section.password ?? envPassword
219
+ });
220
+ const organizationId = await promptOrganizationId({
221
+ prompter,
222
+ maestroUrl,
223
+ email,
224
+ existing: section.organizationId
225
+ });
226
+ let sessionToken;
227
+ let selfReferenceId;
228
+ while (true) try {
229
+ const { dolphinToken, payload } = await authenticate({
230
+ casUrl,
231
+ maestroUrl,
232
+ email,
233
+ password,
234
+ organizationId
235
+ });
236
+ sessionToken = dolphinToken;
237
+ selfReferenceId = payload.referenceId;
238
+ await prompter.note(`Authenticated as ${payload.email}`, "Omadeus authentication");
239
+ break;
240
+ } catch (err) {
241
+ await prompter.note(`Authentication failed: ${formatAuthError(err)}`, "Omadeus authentication");
242
+ if (!await prompter.confirm({
243
+ message: "Re-enter email/password and try again?",
244
+ initialValue: true
245
+ })) {
246
+ await prompter.note("Saving config without verifying credentials. The gateway may fail to connect.", "Omadeus authentication");
247
+ break;
248
+ }
249
+ ({email, password} = await promptCredentials(prompter, {
250
+ email,
251
+ password
252
+ }));
253
+ }
254
+ if (!sessionToken) throw new Error("Authentication is required to list channels.");
255
+ if (typeof selfReferenceId !== "number") throw new Error("Authentication did not return an Omadeus member reference ID.");
256
+ const members = await loadSelectableMembers({
257
+ maestroUrl,
258
+ sessionToken,
259
+ organizationId,
260
+ excludeReferenceIds: [selfReferenceId]
261
+ });
262
+ const existingInbound = section.inbound;
263
+ const directSenderIds = await promptSenderAllowlist({
264
+ prompter,
265
+ message: "Which users can DM OpenClaw directly?",
266
+ members,
267
+ existingReferenceIds: existingInbound?.direct?.allowedSenderReferenceIds
268
+ });
269
+ const selectedChannels = await promptChannelSelection({
270
+ prompter,
271
+ maestroUrl,
272
+ sessionToken,
273
+ memberReferenceId: selfReferenceId,
274
+ existingChannelViewIds: existingInbound?.channels?.allowedChannelViewIds
275
+ });
276
+ const channelSenderIds = selectedChannels.length > 0 ? await promptSenderAllowlist({
277
+ prompter,
278
+ message: "Which users can trigger OpenClaw from allowed channels?",
279
+ members,
280
+ existingReferenceIds: existingInbound?.channels?.allowedSenderReferenceIds
281
+ }) : void 0;
282
+ const entityKinds = await promptEntityKindSelection({
283
+ prompter,
284
+ existingKinds: existingInbound?.entities?.allowedKinds
285
+ });
286
+ const entitySenderIds = entityKinds.length > 0 ? await promptSenderAllowlist({
287
+ prompter,
288
+ message: "Which users can trigger OpenClaw from entity rooms?",
289
+ members,
290
+ existingReferenceIds: existingInbound?.entities?.allowedSenderReferenceIds
291
+ }) : void 0;
292
+ const channelRoomIds = selectedChannels.flatMap((selectedChannel) => [selectedChannel.publicRoomId, selectedChannel.privateRoomId]).filter((id) => typeof id === "number");
293
+ const channelViewIds = selectedChannels.map((selectedChannel) => selectedChannel.id);
294
+ const channelTitles = selectedChannels.map((selectedChannel) => selectedChannel.title || `Channel ${selectedChannel.id}`).join(", ");
295
+ const senderSummary = (ids) => ids && ids.length > 0 ? ids.join(", ") : "all users";
296
+ const entityKindSummary = entityKinds.length > 0 ? entityKinds.join(", ") : "none (entity rooms disabled)";
297
+ const channelSummary = selectedChannels.length > 0 ? `- Channels "${channelTitles}": rooms ${channelRoomIds.join(", ") || "(no room ids)"} from ${senderSummary(channelSenderIds)}; @mention not required in those rooms.` : "- Channels: disabled (none selected).";
298
+ await prompter.note([
299
+ `Inbound policy (Jaguar chat):`,
300
+ `- Direct messages: enabled for ${senderSummary(directSenderIds)} (no @mention required).`,
301
+ channelSummary,
302
+ `- Entity rooms (${entityKindSummary}): ${senderSummary(entitySenderIds)}; @mention required.`
303
+ ].join("\n"), "Omadeus inbound policy");
304
+ next = {
305
+ ...next,
306
+ channels: {
307
+ ...next.channels,
308
+ omadeus: {
309
+ enabled: true,
310
+ casUrl,
311
+ maestroUrl,
312
+ email,
313
+ password,
314
+ organizationId,
315
+ sessionToken,
316
+ inbound: {
317
+ version: 1,
318
+ direct: {
319
+ enabled: true,
320
+ ...directSenderIds ? { allowedSenderReferenceIds: directSenderIds } : {},
321
+ requireMention: "never"
322
+ },
323
+ channels: {
324
+ enabled: selectedChannels.length > 0,
325
+ allowedRoomIds: channelRoomIds,
326
+ allowedChannelViewIds: channelViewIds,
327
+ ...channelSenderIds ? { allowedSenderReferenceIds: channelSenderIds } : {},
328
+ requireMention: "outsideAllowlist"
329
+ },
330
+ entities: {
331
+ enabled: entityKinds.length > 0,
332
+ allowedKinds: entityKinds,
333
+ ...entitySenderIds ? { allowedSenderReferenceIds: entitySenderIds } : {},
334
+ requireMention: "always"
335
+ }
336
+ }
337
+ }
338
+ }
339
+ };
340
+ return {
341
+ cfg: next,
342
+ accountId: DEFAULT_ACCOUNT_ID
343
+ };
344
+ },
345
+ disable: (cfg) => ({
346
+ ...cfg,
347
+ channels: {
348
+ ...cfg.channels,
349
+ omadeus: {
350
+ ...getOmadeusChannelConfig(cfg),
351
+ enabled: false
352
+ }
353
+ }
354
+ })
355
+ };
356
+ //#endregion
357
+ export { omadeusSetupWizard };
@@ -0,0 +1,17 @@
1
+ import { sendRoomMessage } from "./api/message.api.js";
2
+ //#region src/outbound.ts
3
+ async function sendOmadeusMessage(deps, params) {
4
+ const { to, text } = params;
5
+ const result = await sendRoomMessage(deps.apiOpts, {
6
+ roomId: to,
7
+ body: text
8
+ });
9
+ if (!result.ok) throw new Error(`Omadeus send failed: ${result.error}`);
10
+ return {
11
+ channel: "omadeus",
12
+ messageId: String(result.message?.id ?? ""),
13
+ chatId: to
14
+ };
15
+ }
16
+ //#endregion
17
+ export { sendOmadeusMessage };
@@ -0,0 +1,46 @@
1
+ import { createReplyPrefixContext } from "../runtime-api.js";
2
+ import { sendOmadeusMessage } from "./outbound.js";
3
+ import { getOmadeusRuntime } from "./runtime.js";
4
+ //#region src/reply-dispatcher.ts
5
+ function createOmadeusReplyDispatcher(params) {
6
+ const core = getOmadeusRuntime();
7
+ const { cfg, agentId, roomId, accountId } = params;
8
+ const prefixContext = createReplyPrefixContext({
9
+ cfg,
10
+ agentId
11
+ });
12
+ const textChunkLimit = core.channel.text.resolveTextChunkLimit(cfg, "omadeus", accountId, { fallbackLimit: 4e3 });
13
+ const chunkMode = core.channel.text.resolveChunkMode(cfg, "omadeus");
14
+ const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({
15
+ responsePrefix: prefixContext.responsePrefix,
16
+ responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
17
+ humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, agentId),
18
+ deliver: async (payload) => {
19
+ const text = payload.text ?? "";
20
+ if (!text.trim()) return;
21
+ const chunks = core.channel.text.chunkTextWithMode(text, textChunkLimit, chunkMode);
22
+ for (const chunk of chunks) await sendOmadeusMessage(params.outboundDeps, {
23
+ to: String(roomId),
24
+ text: chunk
25
+ });
26
+ },
27
+ onError: (error, info) => {
28
+ const errMsg = error instanceof Error ? error.message : String(error);
29
+ params.runtime.error?.(`omadeus ${info.kind} reply failed: ${errMsg}`);
30
+ params.log.error("reply failed", {
31
+ kind: info.kind,
32
+ error: errMsg
33
+ });
34
+ }
35
+ });
36
+ return {
37
+ dispatcher,
38
+ replyOptions: {
39
+ ...replyOptions,
40
+ onModelSelected: prefixContext.onModelSelected
41
+ },
42
+ markDispatchIdle
43
+ };
44
+ }
45
+ //#endregion
46
+ export { createOmadeusReplyDispatcher };
@@ -0,0 +1,5 @@
1
+ import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
2
+ //#region src/runtime.ts
3
+ const { setRuntime: setOmadeusRuntime, getRuntime: getOmadeusRuntime } = createPluginRuntimeStore("Omadeus runtime not initialized");
4
+ //#endregion
5
+ export { getOmadeusRuntime, setOmadeusRuntime };
@@ -0,0 +1,46 @@
1
+ //#region src/setup-core.ts
2
+ function readSetupStringField(input, key) {
3
+ const value = input[key];
4
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
5
+ }
6
+ function readSetupNumberField(input, key) {
7
+ const value = input[key];
8
+ if (typeof value === "number" && Number.isFinite(value)) return value;
9
+ if (typeof value === "string" && value.trim()) {
10
+ const parsed = Number(value.trim());
11
+ return Number.isFinite(parsed) ? parsed : void 0;
12
+ }
13
+ }
14
+ const omadeusSetupAdapter = {
15
+ validateInput: ({ input }) => {
16
+ if (!readSetupStringField(input, "email") && !input.useEnv) return "Omadeus requires --email (or use OMADEUS_EMAIL env var).";
17
+ return null;
18
+ },
19
+ applyAccountConfig: ({ cfg, input }) => {
20
+ const rawInput = input;
21
+ const casUrl = input.httpUrl?.trim() || void 0;
22
+ const maestroUrl = input.url?.trim() || void 0;
23
+ const email = readSetupStringField(rawInput, "email");
24
+ const password = input.password?.trim() || void 0;
25
+ const organizationId = readSetupNumberField(rawInput, "organizationId");
26
+ const omadeusExisting = cfg.channels?.["omadeus"];
27
+ const omadeusPrevious = omadeusExisting !== null && typeof omadeusExisting === "object" && !Array.isArray(omadeusExisting) ? omadeusExisting : {};
28
+ return {
29
+ ...cfg,
30
+ channels: {
31
+ ...cfg.channels,
32
+ omadeus: {
33
+ ...omadeusPrevious,
34
+ enabled: true,
35
+ ...casUrl ? { casUrl } : {},
36
+ ...maestroUrl ? { maestroUrl } : {},
37
+ ...email ? { email } : {},
38
+ ...password ? { password } : {},
39
+ ...organizationId ? { organizationId } : {}
40
+ }
41
+ }
42
+ };
43
+ }
44
+ };
45
+ //#endregion
46
+ export { omadeusSetupAdapter };
@@ -0,0 +1,2 @@
1
+ import "./onboarding.js";
2
+ export {};
@@ -0,0 +1,18 @@
1
+ import { createOmadeusSocketClient } from "./socket.js";
2
+ //#region src/socket/dolphin.socket.ts
3
+ function createDolphinSocketClient(opts) {
4
+ const { maestroUrl, tokenManager, onEvent, onConnect, onDisconnect, onError, log } = opts;
5
+ return createOmadeusSocketClient({
6
+ maestroUrl,
7
+ tokenManager,
8
+ pathSuffix: "dolphin-ws",
9
+ logPrefix: "[dolphin]",
10
+ onEvent,
11
+ onConnect,
12
+ onDisconnect,
13
+ onError,
14
+ log
15
+ });
16
+ }
17
+ //#endregion
18
+ export { createDolphinSocketClient };
@@ -0,0 +1,22 @@
1
+ import { isOmadeusMessage } from "../inbound.js";
2
+ import { createOmadeusSocketClient } from "./socket.js";
3
+ //#region src/socket/jaguar.socket.ts
4
+ function createJaguarSocketClient(opts) {
5
+ const { maestroUrl, tokenManager, onMessage, onOtherEvent, onConnect, onDisconnect, onError, log } = opts;
6
+ return createOmadeusSocketClient({
7
+ maestroUrl,
8
+ tokenManager,
9
+ pathSuffix: "ws",
10
+ logPrefix: "[jaguar]",
11
+ onEvent: (data) => {
12
+ if (isOmadeusMessage(data)) onMessage?.(data);
13
+ else onOtherEvent?.(data);
14
+ },
15
+ onConnect,
16
+ onDisconnect,
17
+ onError,
18
+ log
19
+ });
20
+ }
21
+ //#endregion
22
+ export { createJaguarSocketClient };