@brantrusnak/openclaw-omadeus 1.0.2 → 1.0.3

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 (40) hide show
  1. package/README.md +4 -1
  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 +115 -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 +363 -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/channel.ts +127 -238
  37. package/src/member-resolve.ts +1 -1
  38. package/src/onboarding.ts +71 -110
  39. package/src/setup-core.ts +10 -1
  40. 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,363 @@
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
+ const DONE = "__done__";
12
+ async function noteOmadeusAuthHelp(prompter) {
13
+ await prompter.note([
14
+ "Omadeus authenticates via CAS + Maestro (email + password + organization).",
15
+ "You need:",
16
+ " - Email + password",
17
+ " - Organization ID (we can look it up for you)",
18
+ `CAS URL: ${OMADEUS_CAS_URL}`,
19
+ `Maestro URL: ${OMADEUS_MAESTRO_URL}`,
20
+ "Env vars supported: OMADEUS_EMAIL, OMADEUS_PASSWORD, OMADEUS_ORGANIZATION_ID."
21
+ ].join("\n"), "Omadeus setup");
22
+ }
23
+ async function promptOrganizationId(params) {
24
+ const { prompter, maestroUrl, email, existing } = params;
25
+ try {
26
+ const orgs = await listOrganizations({
27
+ maestroUrl,
28
+ email
29
+ });
30
+ if (orgs.length > 0) {
31
+ if (orgs.length === 1) {
32
+ await prompter.note(`Found organization: ${orgs[0].title} (${orgs[0].id})`, "Omadeus organization");
33
+ return orgs[0].id;
34
+ }
35
+ const choice = await prompter.select({
36
+ message: "Select organization",
37
+ options: orgs.map((org) => ({
38
+ value: String(org.id),
39
+ label: `${org.title} (${org.membersCount} members)`,
40
+ hint: `ID: ${org.id}`
41
+ })),
42
+ initialValue: existing ? String(existing) : String(orgs[0].id)
43
+ });
44
+ return Number(choice);
45
+ }
46
+ } catch {
47
+ await prompter.note("Could not fetch organizations from the API. Enter the ID manually.", "Omadeus organization");
48
+ }
49
+ const raw = await prompter.text({
50
+ message: "Organization ID (number)",
51
+ initialValue: existing ? String(existing) : void 0,
52
+ validate: (value) => {
53
+ const trimmed = String(value ?? "").trim();
54
+ if (!trimmed) return "Required";
55
+ if (!/^\d+$/.test(trimmed)) return "Must be a number";
56
+ }
57
+ });
58
+ return Number(String(raw).trim());
59
+ }
60
+ async function promptChannelSelection(params) {
61
+ const { prompter, maestroUrl, sessionToken, memberReferenceId, existingChannelViewIds } = params;
62
+ const channels = await listMemberChannelViews({
63
+ maestroUrl,
64
+ sessionToken,
65
+ memberReferenceId,
66
+ skip: 0,
67
+ take: 100
68
+ });
69
+ if (channels.length === 0) throw new Error("No channels found for this account.");
70
+ const selected = await promptMultiSelect({
71
+ prompter,
72
+ message: "Which channels should OpenClaw listen to?",
73
+ options: channels.map((item) => ({
74
+ value: String(item.id),
75
+ label: item.title || `Channel ${item.id}`,
76
+ hint: [item.privateRoomId ? `private:${item.privateRoomId}` : void 0, item.publicRoomId ? `public:${item.publicRoomId}` : void 0].filter(Boolean).join(" | ")
77
+ })),
78
+ initialValues: existingChannelViewIds && existingChannelViewIds.length > 0 ? existingChannelViewIds.map(String) : [String(channels[0].id)]
79
+ });
80
+ const chosen = channels.filter((item) => selected.includes(String(item.id)));
81
+ if (chosen.length === 0) throw new Error("At least one channel must be selected.");
82
+ return chosen;
83
+ }
84
+ function memberHint(member) {
85
+ const parts = [
86
+ `${member.firstName ?? ""} ${member.lastName ?? ""}`.trim(),
87
+ member.email?.trim(),
88
+ `ref:${member.referenceId}`
89
+ ].filter(Boolean);
90
+ return parts.length > 0 ? parts.join(" | ") : void 0;
91
+ }
92
+ async function promptMultiSelect(params) {
93
+ const multi = params.prompter;
94
+ const runMulti = multi.multiSelect ?? multi.multiselect;
95
+ if (runMulti) return runMulti({
96
+ message: params.message,
97
+ options: params.options,
98
+ initialValues: params.initialValues,
99
+ initialValue: params.initialValues
100
+ });
101
+ const selected = new Set(params.initialValues ?? []);
102
+ while (true) {
103
+ const next = await params.prompter.select({
104
+ message: `${params.message} (${selected.size} selected)`,
105
+ options: [{
106
+ value: DONE,
107
+ label: selected.size > 0 ? "Done" : "Done (select none)"
108
+ }, ...params.options.map((option) => ({
109
+ ...option,
110
+ label: selected.has(option.value) ? `[selected] ${option.label}` : option.label
111
+ }))],
112
+ initialValue: DONE
113
+ });
114
+ const value = String(next);
115
+ if (value === DONE) return [...selected];
116
+ if (selected.has(value)) selected.delete(value);
117
+ else selected.add(value);
118
+ }
119
+ }
120
+ async function loadSelectableMembers(params) {
121
+ const excluded = new Set(params.excludeReferenceIds ?? []);
122
+ return (await listOrganizationMembers({
123
+ maestroUrl: params.maestroUrl,
124
+ sessionToken: params.sessionToken,
125
+ organizationId: params.organizationId
126
+ })).filter((member) => member.isSystem !== true && !excluded.has(member.referenceId)).sort((a, b) => formatMemberLabel(a).localeCompare(formatMemberLabel(b)));
127
+ }
128
+ function memberOptions(members) {
129
+ return members.map((member) => ({
130
+ value: String(member.referenceId),
131
+ label: formatMemberLabel(member),
132
+ hint: memberHint(member)
133
+ }));
134
+ }
135
+ function readReferenceIds(values) {
136
+ return values.map((value) => Number(value)).filter((value) => Number.isInteger(value) && value > 0);
137
+ }
138
+ async function promptCredentials(prompter, existing) {
139
+ return {
140
+ email: String(await prompter.text({
141
+ message: "Omadeus username (email)",
142
+ initialValue: existing.email,
143
+ validate: (value) => String(value ?? "").trim() ? void 0 : "Required"
144
+ })).trim(),
145
+ password: String(await prompter.text({
146
+ message: "Omadeus password",
147
+ sensitive: true,
148
+ validate: (value) => String(value ?? "").trim() ? void 0 : "Required"
149
+ })).trim()
150
+ };
151
+ }
152
+ async function promptSenderAllowlist(params) {
153
+ const { prompter, message, members, existingReferenceIds } = params;
154
+ if (members.length === 0) throw new Error("No organization members found.");
155
+ if (await prompter.select({
156
+ message,
157
+ options: [{
158
+ value: "all",
159
+ label: "All users",
160
+ hint: "No sender allowlist"
161
+ }, {
162
+ value: "specific",
163
+ label: "Specific users",
164
+ hint: "Select one or more users"
165
+ }],
166
+ initialValue: existingReferenceIds && existingReferenceIds.length > 0 ? "specific" : "all"
167
+ }) === "all") return;
168
+ return readReferenceIds(await promptMultiSelect({
169
+ prompter,
170
+ message: `${message} (specific users)`,
171
+ options: memberOptions(members),
172
+ initialValues: existingReferenceIds?.map(String)
173
+ }));
174
+ }
175
+ async function promptEntityKindSelection(params) {
176
+ const selected = await promptMultiSelect({
177
+ prompter: params.prompter,
178
+ message: "Which entity room types should OpenClaw listen to?",
179
+ options: OMADEUS_INBOUND_ENTITY_KINDS.map((kind) => ({
180
+ value: kind,
181
+ label: kind
182
+ })),
183
+ initialValues: params.existingKinds && params.existingKinds.length > 0 ? params.existingKinds : [...OMADEUS_INBOUND_ENTITY_KINDS]
184
+ });
185
+ const selectedSet = new Set(selected);
186
+ return OMADEUS_INBOUND_ENTITY_KINDS.filter((kind) => selectedSet.has(kind));
187
+ }
188
+ const omadeusSetupWizard = {
189
+ channel,
190
+ resolveAccountIdForConfigure: () => DEFAULT_ACCOUNT_ID,
191
+ resolveShouldPromptAccountIds: () => false,
192
+ status: {
193
+ configuredLabel: "configured",
194
+ unconfiguredLabel: "needs credentials",
195
+ configuredHint: "configured",
196
+ unconfiguredHint: "needs credentials",
197
+ configuredScore: 2,
198
+ unconfiguredScore: 0,
199
+ resolveConfigured: ({ cfg }) => {
200
+ return resolveOmadeusAccount({ cfg }).credentialSource !== "none";
201
+ },
202
+ resolveStatusLines: ({ cfg }) => {
203
+ return [`Omadeus: ${resolveOmadeusAccount({ cfg }).credentialSource !== "none" ? "configured" : "needs email, password, and organization ID"}`];
204
+ },
205
+ resolveSelectionHint: ({ cfg }) => {
206
+ return resolveOmadeusAccount({ cfg }).credentialSource !== "none" ? "configured" : "needs credentials";
207
+ },
208
+ resolveQuickstartScore: ({ cfg }) => {
209
+ return resolveOmadeusAccount({ cfg }).credentialSource !== "none" ? 2 : 0;
210
+ }
211
+ },
212
+ credentials: [],
213
+ finalize: async ({ cfg, prompter }) => {
214
+ const account = resolveOmadeusAccount({ cfg });
215
+ const section = getOmadeusChannelConfig(cfg) ?? {};
216
+ let next = cfg;
217
+ if (account.credentialSource === "none") await noteOmadeusAuthHelp(prompter);
218
+ const envEmail = process.env.OMADEUS_EMAIL?.trim();
219
+ const envPassword = process.env.OMADEUS_PASSWORD?.trim();
220
+ const casUrl = OMADEUS_CAS_URL;
221
+ const maestroUrl = OMADEUS_MAESTRO_URL;
222
+ let { email, password } = await promptCredentials(prompter, {
223
+ email: section.email ?? envEmail,
224
+ password: section.password ?? envPassword
225
+ });
226
+ const organizationId = await promptOrganizationId({
227
+ prompter,
228
+ maestroUrl,
229
+ email,
230
+ existing: section.organizationId
231
+ });
232
+ let sessionToken;
233
+ let selfReferenceId;
234
+ while (true) try {
235
+ const { dolphinToken, payload } = await authenticate({
236
+ casUrl,
237
+ maestroUrl,
238
+ email,
239
+ password,
240
+ organizationId
241
+ });
242
+ sessionToken = dolphinToken;
243
+ selfReferenceId = payload.referenceId;
244
+ await prompter.note(`Authenticated as ${payload.email}`, "Omadeus authentication");
245
+ break;
246
+ } catch (err) {
247
+ const msg = err instanceof Error ? err.message : String(err);
248
+ await prompter.note(`Authentication failed: ${msg}`, "Omadeus authentication");
249
+ if (!await prompter.confirm({
250
+ message: "Re-enter email/password and try again?",
251
+ initialValue: true
252
+ })) {
253
+ await prompter.note("Saving config without verifying credentials. The gateway may fail to connect.", "Omadeus authentication");
254
+ break;
255
+ }
256
+ ({email, password} = await promptCredentials(prompter, {
257
+ email,
258
+ password
259
+ }));
260
+ }
261
+ if (!sessionToken) throw new Error("Authentication is required to list channels.");
262
+ if (typeof selfReferenceId !== "number") throw new Error("Authentication did not return an Omadeus member reference ID.");
263
+ const members = await loadSelectableMembers({
264
+ maestroUrl,
265
+ sessionToken,
266
+ organizationId,
267
+ excludeReferenceIds: [selfReferenceId]
268
+ });
269
+ const existingInbound = section.inbound;
270
+ const directSenderIds = await promptSenderAllowlist({
271
+ prompter,
272
+ message: "Which users can DM OpenClaw directly?",
273
+ members,
274
+ existingReferenceIds: existingInbound?.direct?.allowedSenderReferenceIds
275
+ });
276
+ const selectedChannels = await promptChannelSelection({
277
+ prompter,
278
+ maestroUrl,
279
+ sessionToken,
280
+ memberReferenceId: selfReferenceId,
281
+ existingChannelViewIds: existingInbound?.channels?.allowedChannelViewIds
282
+ });
283
+ const channelSenderIds = await promptSenderAllowlist({
284
+ prompter,
285
+ message: "Which users can trigger OpenClaw from allowed channels?",
286
+ members,
287
+ existingReferenceIds: existingInbound?.channels?.allowedSenderReferenceIds
288
+ });
289
+ const entityKinds = await promptEntityKindSelection({
290
+ prompter,
291
+ existingKinds: existingInbound?.entities?.allowedKinds
292
+ });
293
+ const entitySenderIds = entityKinds.length > 0 ? await promptSenderAllowlist({
294
+ prompter,
295
+ message: "Which users can trigger OpenClaw from entity rooms?",
296
+ members,
297
+ existingReferenceIds: existingInbound?.entities?.allowedSenderReferenceIds
298
+ }) : void 0;
299
+ const channelRoomIds = selectedChannels.flatMap((selectedChannel) => [selectedChannel.publicRoomId, selectedChannel.privateRoomId]).filter((id) => typeof id === "number");
300
+ const channelViewIds = selectedChannels.map((selectedChannel) => selectedChannel.id);
301
+ const channelTitles = selectedChannels.map((selectedChannel) => selectedChannel.title || `Channel ${selectedChannel.id}`).join(", ");
302
+ const senderSummary = (ids) => ids && ids.length > 0 ? ids.join(", ") : "all users";
303
+ const entityKindSummary = entityKinds.length > 0 ? entityKinds.join(", ") : "none (entity rooms disabled)";
304
+ await prompter.note([
305
+ `Inbound policy (Jaguar chat):`,
306
+ `- Direct messages: enabled for ${senderSummary(directSenderIds)} (no @mention required).`,
307
+ `- Channels "${channelTitles}": rooms ${channelRoomIds.join(", ") || "(no room ids)"} from ${senderSummary(channelSenderIds)}; @mention not required in those rooms.`,
308
+ `- Entity rooms (${entityKindSummary}): ${senderSummary(entitySenderIds)}; @mention required.`
309
+ ].join("\n"), "Omadeus inbound policy");
310
+ next = {
311
+ ...next,
312
+ channels: {
313
+ ...next.channels,
314
+ omadeus: {
315
+ enabled: true,
316
+ casUrl,
317
+ maestroUrl,
318
+ email,
319
+ password,
320
+ organizationId,
321
+ sessionToken,
322
+ inbound: {
323
+ version: 1,
324
+ direct: {
325
+ enabled: true,
326
+ ...directSenderIds ? { allowedSenderReferenceIds: directSenderIds } : {},
327
+ requireMention: "never"
328
+ },
329
+ channels: {
330
+ enabled: true,
331
+ allowedRoomIds: channelRoomIds,
332
+ allowedChannelViewIds: channelViewIds,
333
+ ...channelSenderIds ? { allowedSenderReferenceIds: channelSenderIds } : {},
334
+ requireMention: "outsideAllowlist"
335
+ },
336
+ entities: {
337
+ enabled: entityKinds.length > 0,
338
+ allowedKinds: entityKinds,
339
+ ...entitySenderIds ? { allowedSenderReferenceIds: entitySenderIds } : {},
340
+ requireMention: "always"
341
+ }
342
+ }
343
+ }
344
+ }
345
+ };
346
+ return {
347
+ cfg: next,
348
+ accountId: DEFAULT_ACCOUNT_ID
349
+ };
350
+ },
351
+ disable: (cfg) => ({
352
+ ...cfg,
353
+ channels: {
354
+ ...cfg.channels,
355
+ omadeus: {
356
+ ...getOmadeusChannelConfig(cfg),
357
+ enabled: false
358
+ }
359
+ }
360
+ })
361
+ };
362
+ //#endregion
363
+ 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 };