@brantrusnak/openclaw-omadeus 1.0.4 → 1.0.6

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.
@@ -16,7 +16,7 @@ async function sendRoomMessage(opts, params) {
16
16
  method: "SEND",
17
17
  body: JSON.stringify({
18
18
  body: params.body,
19
- temporaryId: generateTemporaryId(),
19
+ temporaryId: params.temporaryId ?? generateTemporaryId(),
20
20
  links: "[]"
21
21
  })
22
22
  });
@@ -38,6 +38,17 @@ async function sendRoomMessage(opts, params) {
38
38
  };
39
39
  }
40
40
  }
41
+ async function seeMessage(opts, params) {
42
+ const res = await jaguarFetch(opts, `/messages/${params.messageId}`, {
43
+ method: "SEE",
44
+ body: "{}"
45
+ });
46
+ if (!res.ok) {
47
+ const text = await res.text().catch(() => "");
48
+ throw new Error(`Omadeus see message failed (${res.status}): ${text.slice(0, 200)}`);
49
+ }
50
+ return await res.json();
51
+ }
41
52
  async function editMessage(opts, params) {
42
53
  const res = await jaguarFetch(opts, `/messages/${params.messageId}`, {
43
54
  method: "EDIT",
@@ -73,4 +84,4 @@ async function addMessageReaction(opts, params) {
73
84
  return readJsonOrEmpty(res);
74
85
  }
75
86
  //#endregion
76
- export { addMessageReaction, deleteMessage, editMessage, sendRoomMessage };
87
+ export { addMessageReaction, deleteMessage, editMessage, seeMessage, sendRoomMessage };
@@ -1,5 +1,6 @@
1
1
  import { DEFAULT_ACCOUNT_ID, missingTargetError } from "../runtime-api.js";
2
2
  import { ALLOWED_OMADEUS_REACTION_EMOJI_LIST, isAllowedOmadeusReactionEmoji } from "./allowed-reaction-emojis.js";
3
+ import { generateTemporaryId } from "./utils/http.util.js";
3
4
  import { createNugget, resolveTaskRoomIdByNumber } from "./api/nugget.api.js";
4
5
  import { addMessageReaction, deleteMessage, editMessage } from "./api/message.api.js";
5
6
  import { getOmadeusChannelConfig, listOmadeusAccountIds, resolveDefaultOmadeusAccountId, resolveOmadeusAccount } from "./config.js";
@@ -8,6 +9,7 @@ import { parseTaskChannelTargetIntent } from "./nugget-lookup.js";
8
9
  import { sendOmadeusMessage } from "./outbound.js";
9
10
  import { getOmadeusRuntime } from "./runtime.js";
10
11
  import { createOmadeusMessageHandler } from "./message-handler.js";
12
+ import { SentMessageTracker } from "./sent-message-tracker.js";
11
13
  import { omadeusSetupAdapter } from "./setup-core.js";
12
14
  import { omadeusSetupWizard } from "./onboarding.js";
13
15
  import "./setup-surface.js";
@@ -23,14 +25,16 @@ const CHANNEL_ID = "omadeus";
23
25
  const gatewayState = {
24
26
  tokenManager: null,
25
27
  dolphin: null,
26
- jaguar: null
28
+ jaguar: null,
29
+ sentTracker: null
27
30
  };
28
31
  const isUnconfigured = (account) => account.credentialSource === "none";
29
32
  let lastPersistedToken = null;
30
- async function persistSessionToken(token) {
33
+ async function persistSessionToken(token, environment) {
31
34
  if (lastPersistedToken === token) return;
32
35
  const runtime = getOmadeusRuntime();
33
- if ((getOmadeusChannelConfig(runtime.config.current()) ?? {}).sessionToken === token) {
36
+ const section = getOmadeusChannelConfig(runtime.config.current()) ?? {};
37
+ if (section.sessionToken === token && section.sessionTokenEnvironment === environment) {
34
38
  lastPersistedToken = token;
35
39
  return;
36
40
  }
@@ -41,7 +45,8 @@ async function persistSessionToken(token) {
41
45
  ...draft.channels ?? {},
42
46
  omadeus: {
43
47
  ...getOmadeusChannelConfig(draft) ?? {},
44
- sessionToken: token
48
+ sessionToken: token,
49
+ sessionTokenEnvironment: environment
45
50
  }
46
51
  };
47
52
  }
@@ -82,12 +87,12 @@ const omadeusConfigAdapter = createTopLevelChannelConfigAdapter({
82
87
  defaultAccountId: resolveDefaultOmadeusAccountId,
83
88
  deleteMode: "clear-fields",
84
89
  clearBaseFields: [
85
- "casUrl",
86
- "maestroUrl",
90
+ "environment",
87
91
  "email",
88
92
  "password",
89
93
  "organizationId",
90
94
  "sessionToken",
95
+ "sessionTokenEnvironment",
91
96
  "inbound"
92
97
  ],
93
98
  resolveAllowFrom: () => [],
@@ -249,10 +254,17 @@ const omadeusPlugin = {
249
254
  if (messageId == null) return actionError("Omadeus edit requires `messageId` (Jaguar message id) or current inbound MessageSid.", "Missing messageId for edit.");
250
255
  if (!body) return actionError("Omadeus edit requires new text in `message`, `text`, or `content`.", "Missing body for edit.");
251
256
  try {
252
- await editMessage(apiOpts(), {
253
- messageId,
257
+ const temporaryId = generateTemporaryId();
258
+ gatewayState.sentTracker?.trackOutbound({
259
+ temporaryId,
254
260
  body
255
261
  });
262
+ const edited = await editMessage(apiOpts(), {
263
+ messageId,
264
+ body,
265
+ temporaryId
266
+ });
267
+ if (typeof edited?.id === "number") gatewayState.sentTracker?.trackId(edited.id);
256
268
  } catch (err) {
257
269
  return actionError(err instanceof Error ? err.message : String(err));
258
270
  }
@@ -374,7 +386,8 @@ const omadeusPlugin = {
374
386
  maestroUrl: resolveOmadeusAccount({ cfg }).maestroUrl,
375
387
  tokenManager: gatewayState.tokenManager
376
388
  },
377
- jaguarSocket: gatewayState.jaguar
389
+ jaguarSocket: gatewayState.jaguar,
390
+ sentTracker: gatewayState.sentTracker ?? void 0
378
391
  }, {
379
392
  to,
380
393
  text
@@ -473,7 +486,7 @@ const omadeusPlugin = {
473
486
  initialToken: account.sessionToken,
474
487
  onRefresh: (token) => {
475
488
  log.info("[omadeus] token refreshed");
476
- persistSessionToken(token).catch((err) => log.warn(`[omadeus] failed to persist session token: ${String(err)}`));
489
+ persistSessionToken(token, account.environment).catch((err) => log.warn(`[omadeus] failed to persist session token: ${String(err)}`));
477
490
  },
478
491
  onError: (err) => {
479
492
  log.error(`[omadeus] token refresh failed: ${err.message}`);
@@ -498,12 +511,15 @@ const omadeusPlugin = {
498
511
  tokenManager.startAutoRefresh();
499
512
  gatewayState.tokenManager = tokenManager;
500
513
  const selfReferenceId = tokenManager.getPayload().referenceId;
514
+ const sentTracker = new SentMessageTracker();
515
+ gatewayState.sentTracker = sentTracker;
501
516
  const outboundDeps = {
502
517
  apiOpts: {
503
518
  maestroUrl: account.maestroUrl,
504
519
  tokenManager
505
520
  },
506
- jaguarSocket: null
521
+ jaguarSocket: null,
522
+ sentTracker
507
523
  };
508
524
  const handleMessage = createOmadeusMessageHandler({
509
525
  cfg,
@@ -519,6 +535,16 @@ const omadeusPlugin = {
519
535
  onMessage: (msg) => {
520
536
  const label = msg.subscribableKind === "direct" ? `DM from ${msg.senderReferenceId}` : `${msg.subscribableKind}/${msg.roomName ?? msg.roomId} from ${msg.senderReferenceId}`;
521
537
  log.info(`[jaguar] ${label}: ${msg.body.slice(0, 80)}`);
538
+ if (sentTracker.isEcho({
539
+ id: msg.id,
540
+ temporaryId: msg.temporaryId,
541
+ body: msg.body,
542
+ roomId: msg.roomId,
543
+ fromSelf: msg.senderReferenceId === selfReferenceId
544
+ })) {
545
+ log.debug?.(`[jaguar] suppressed self-echo id=${msg.id}`);
546
+ return;
547
+ }
522
548
  const inbound = parseJaguarMessage(msg, { selfReferenceId }, log);
523
549
  if (inbound) {
524
550
  log.info(`[jaguar] inbound: ${inbound.subscribableKind} room=${inbound.roomId} from=${inbound.from} mention=${inbound.isMention}`);
@@ -605,6 +631,7 @@ const omadeusPlugin = {
605
631
  gatewayState.tokenManager = null;
606
632
  gatewayState.jaguar = null;
607
633
  gatewayState.dolphin = null;
634
+ gatewayState.sentTracker = null;
608
635
  lastPersistedToken = null;
609
636
  ctx.setStatus({
610
637
  accountId: account.accountId,
@@ -1,5 +1,5 @@
1
1
  import { DEFAULT_ACCOUNT_ID } from "../runtime-api.js";
2
- import "./defaults.js";
2
+ import { getOmadeusEnvironmentUrls, resolveOmadeusEnvironment } from "./defaults.js";
3
3
  //#region src/config.ts
4
4
  function getOmadeusChannelConfig(cfg) {
5
5
  return cfg.channels?.["omadeus"];
@@ -14,11 +14,15 @@ function resolveDefaultOmadeusAccountId(_cfg) {
14
14
  function resolveOmadeusAccount(params) {
15
15
  const { cfg } = params;
16
16
  const section = getOmadeusChannelConfig(cfg) ?? {};
17
+ const environment = resolveOmadeusEnvironment(section.environment);
18
+ const { casUrl, maestroUrl } = getOmadeusEnvironmentUrls(environment);
17
19
  const envCredentials = resolveOmadeusEnvCredentials();
18
20
  const email = section.email?.trim() || envCredentials?.email || "";
19
21
  const password = section.password?.trim() || envCredentials?.password || "";
20
22
  const orgId = section.organizationId ?? envCredentials?.organizationId;
21
- const sessionToken = section.sessionToken?.trim() ?? "";
23
+ const rawSessionToken = section.sessionToken?.trim() ?? "";
24
+ const sessionTokenEnvironment = resolveOmadeusEnvironment(section.sessionTokenEnvironment);
25
+ const sessionToken = Boolean(rawSessionToken) && sessionTokenEnvironment === environment ? rawSessionToken : "";
22
26
  const hasCredentials = Boolean(email && password && orgId);
23
27
  const hasSessionToken = Boolean(sessionToken);
24
28
  const credentialSource = Boolean(section.email?.trim() && section.password?.trim() && section.organizationId) ? "config" : hasCredentials ? "env" : hasSessionToken ? "session" : "none";
@@ -27,8 +31,9 @@ function resolveOmadeusAccount(params) {
27
31
  name: "Omadeus",
28
32
  enabled: section.enabled !== false,
29
33
  config: section,
30
- casUrl: section.casUrl?.trim() || "https://dev1-cas.rouztech.com",
31
- maestroUrl: section.maestroUrl?.trim() || "https://dev3-maestro.rouztech.com",
34
+ environment,
35
+ casUrl,
36
+ maestroUrl,
32
37
  email,
33
38
  password,
34
39
  organizationId: orgId ?? 0,
@@ -1,5 +1,31 @@
1
- //#region src/defaults.ts
2
- const OMADEUS_CAS_URL = "https://dev1-cas.rouztech.com";
3
- const OMADEUS_MAESTRO_URL = "https://dev3-maestro.rouztech.com";
1
+ const OMADEUS_ENVIRONMENTS = {
2
+ production: {
3
+ label: "Production",
4
+ casUrl: "https://xas.xeba.tech",
5
+ maestroUrl: "https://maestro.xeba.tech"
6
+ },
7
+ staging: {
8
+ label: "Staging",
9
+ casUrl: "https://staging-xas.xeba.tech",
10
+ maestroUrl: "https://staging.xeba.tech"
11
+ },
12
+ dev: {
13
+ label: "Dev",
14
+ casUrl: "https://dev1-cas.rouztech.com",
15
+ maestroUrl: "https://dev1-maestro.rouztech.com"
16
+ }
17
+ };
18
+ const OMADEUS_ENVIRONMENT_SET = new Set(Object.keys(OMADEUS_ENVIRONMENTS));
19
+ function resolveOmadeusEnvironment(value) {
20
+ if (typeof value === "string" && OMADEUS_ENVIRONMENT_SET.has(value)) return value;
21
+ return "dev";
22
+ }
23
+ function getOmadeusEnvironmentUrls(env) {
24
+ const config = OMADEUS_ENVIRONMENTS[env];
25
+ return {
26
+ casUrl: config.casUrl,
27
+ maestroUrl: config.maestroUrl
28
+ };
29
+ }
4
30
  //#endregion
5
- export { OMADEUS_CAS_URL, OMADEUS_MAESTRO_URL };
31
+ export { OMADEUS_ENVIRONMENTS, getOmadeusEnvironmentUrls, resolveOmadeusEnvironment };
@@ -42,7 +42,8 @@ function surfaceForKind(kind) {
42
42
  if (kind === "channel") return "channel";
43
43
  return "entity";
44
44
  }
45
- function senderAllowed(allowed, fromReferenceId) {
45
+ function senderAllowed(allowed, fromReferenceId, selfReferenceId) {
46
+ if (fromReferenceId === selfReferenceId) return true;
46
47
  if (!allowed || allowed.length === 0) return true;
47
48
  return allowed.includes(fromReferenceId);
48
49
  }
@@ -85,18 +86,14 @@ function mentionRequired(params) {
85
86
  }
86
87
  /**
87
88
  * Evaluate whether a normalized Jaguar inbound should be dispatched to OpenClaw.
88
- * Callers must drop self-authored messages separately if they prefer logging there.
89
+ *
90
+ * The logged-in user (`selfReferenceId`) is always treated as an allowed sender
91
+ * so they can message their own OpenClaw even if the stored allowlist predates
92
+ * them. Self-authored *echoes* (the reply loop) are filtered earlier, at socket
93
+ * ingestion, by the {@link SentMessageTracker} — not here.
89
94
  */
90
95
  function evaluateOmadeusInboundPolicy(params) {
91
96
  const { inbound, omadeusCfg, selfReferenceId } = params;
92
- if (inbound.fromReferenceId === selfReferenceId) return {
93
- allow: false,
94
- reason: "self_message",
95
- details: {
96
- fromReferenceId: inbound.fromReferenceId,
97
- selfReferenceId
98
- }
99
- };
100
97
  const policy = mergePolicy(omadeusCfg);
101
98
  const surface = surfaceForKind(inbound.subscribableKind);
102
99
  if (surface === "direct") {
@@ -105,7 +102,7 @@ function evaluateOmadeusInboundPolicy(params) {
105
102
  reason: "direct_disabled",
106
103
  details: { surface }
107
104
  };
108
- if (!senderAllowed(policy.direct.allowedSenderReferenceIds, inbound.fromReferenceId)) return {
105
+ if (!senderAllowed(policy.direct.allowedSenderReferenceIds, inbound.fromReferenceId, selfReferenceId)) return {
109
106
  allow: false,
110
107
  reason: "direct_sender_not_allowed",
111
108
  details: { fromReferenceId: inbound.fromReferenceId }
@@ -128,7 +125,7 @@ function evaluateOmadeusInboundPolicy(params) {
128
125
  reason: "channels_disabled",
129
126
  details: { surface }
130
127
  };
131
- if (!senderAllowed(policy.channels.allowedSenderReferenceIds, inbound.fromReferenceId)) return {
128
+ if (!senderAllowed(policy.channels.allowedSenderReferenceIds, inbound.fromReferenceId, selfReferenceId)) return {
132
129
  allow: false,
133
130
  reason: "channel_sender_not_allowed",
134
131
  details: { fromReferenceId: inbound.fromReferenceId }
@@ -139,7 +136,7 @@ function evaluateOmadeusInboundPolicy(params) {
139
136
  allowedRoomIds: policy.channels.allowedRoomIds,
140
137
  allowedChannelViewIds: policy.channels.allowedChannelViewIds
141
138
  });
142
- const senderInList = !policy.channels.allowedSenderReferenceIds || policy.channels.allowedSenderReferenceIds.length === 0 || policy.channels.allowedSenderReferenceIds.includes(inbound.fromReferenceId);
139
+ const senderInList = inbound.fromReferenceId === selfReferenceId || !policy.channels.allowedSenderReferenceIds || policy.channels.allowedSenderReferenceIds.length === 0 || policy.channels.allowedSenderReferenceIds.includes(inbound.fromReferenceId);
143
140
  const inAllowlist = rv.geoInAllowlist && senderInList;
144
141
  const channelMention = policy.channels.requireMention ?? DEFAULT_INBOUND_POLICY.channels.requireMention;
145
142
  if (mentionRequired({
@@ -170,7 +167,7 @@ function evaluateOmadeusInboundPolicy(params) {
170
167
  allowedKinds: policy.entities.allowedKinds
171
168
  }
172
169
  };
173
- if (!senderAllowed(policy.entities.allowedSenderReferenceIds, inbound.fromReferenceId)) return {
170
+ if (!senderAllowed(policy.entities.allowedSenderReferenceIds, inbound.fromReferenceId, selfReferenceId)) return {
174
171
  allow: false,
175
172
  reason: "entity_sender_not_allowed",
176
173
  details: { fromReferenceId: inbound.fromReferenceId }
@@ -1,5 +1,6 @@
1
1
  import { logInboundDrop, resolveControlCommandGate } from "../runtime-api.js";
2
2
  import { createNugget, findNuggetByTaskChannelRoom, resolveTaskChannelRoomId, searchNuggetByNumber } from "./api/nugget.api.js";
3
+ import { seeMessage } from "./api/message.api.js";
3
4
  import { getOmadeusChannelConfig } from "./config.js";
4
5
  import { appendNuggetContextForTaskOrNuggetRoom, appendNuggetLookupContextForAgent, parseChannelTaskCreateIntent, parseNuggetLookupIntent, parseRecurringScheduleIntent } from "./nugget-lookup.js";
5
6
  import { getOmadeusRuntime } from "./runtime.js";
@@ -36,7 +37,17 @@ function createOmadeusMessageHandler(deps) {
36
37
  cfg,
37
38
  channel: "omadeus"
38
39
  });
39
- const handleMessageNow = async (inbound) => {
40
+ /** Mark inbound messages as seen in Omadeus (fire-and-forget). */
41
+ const markMessagesSeen = (messageIds) => {
42
+ for (const messageId of messageIds) {
43
+ if (!Number.isFinite(messageId)) continue;
44
+ log.info(`omadeus: marking message ${messageId} seen`);
45
+ seeMessage(outboundDeps.apiOpts, { messageId }).then(() => log.debug?.(`omadeus: marked message ${messageId} seen`)).catch((err) => {
46
+ log.warn(`omadeus: failed to mark message ${messageId} seen: ${err instanceof Error ? err.message : String(err)}`);
47
+ });
48
+ }
49
+ };
50
+ const handleMessageNow = async (inbound, ackMessageIds = [inbound.messageId]) => {
40
51
  const isDirectMessage = inbound.subscribableKind === "direct";
41
52
  const senderId = String(inbound.fromReferenceId);
42
53
  const senderName = inbound.from;
@@ -77,6 +88,7 @@ function createOmadeusMessageHandler(deps) {
77
88
  });
78
89
  return;
79
90
  }
91
+ if (inbound.fromReferenceId !== selfReferenceId) markMessagesSeen(ackMessageIds);
80
92
  let bodyForAgent = rawBody;
81
93
  const createIntent = parseChannelTaskCreateIntent(rawBody);
82
94
  if (createIntent) try {
@@ -248,7 +260,7 @@ function createOmadeusMessageHandler(deps) {
248
260
  ...last,
249
261
  content: combinedContent,
250
262
  isMention: entries.some((e) => e.isMention)
251
- });
263
+ }, entries.map((e) => e.messageId));
252
264
  },
253
265
  onError: (err) => {
254
266
  runtime.error?.(`omadeus debounce flush failed: ${String(err)}`);
@@ -1,4 +1,4 @@
1
- import { OMADEUS_CAS_URL, OMADEUS_MAESTRO_URL } from "./defaults.js";
1
+ import { OMADEUS_ENVIRONMENTS, getOmadeusEnvironmentUrls, resolveOmadeusEnvironment } from "./defaults.js";
2
2
  import { getOmadeusChannelConfig, resolveOmadeusAccount } from "./config.js";
3
3
  import { listOrganizationMembers, listOrganizations } from "./api/auth.api.js";
4
4
  import { formatMemberLabel } from "./member-resolve.js";
@@ -19,17 +19,26 @@ function formatAuthError(err) {
19
19
  } else if (typeof cause === "string" && cause.trim()) parts.push(cause);
20
20
  return parts.join(" — ");
21
21
  }
22
- async function noteOmadeusAuthHelp(prompter) {
22
+ async function noteOmadeusAuthHelp(prompter, environment) {
23
+ const envLabel = OMADEUS_ENVIRONMENTS[environment].label;
23
24
  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."
25
+ `Connect OpenClaw to Omadeus (${envLabel}).`,
26
+ "",
27
+ "We'll ask for your email and password, then show the organizations on your account so you can pick one."
31
28
  ].join("\n"), "Omadeus setup");
32
29
  }
30
+ async function promptEnvironment(prompter, existing) {
31
+ const initial = existing ?? "dev";
32
+ return resolveOmadeusEnvironment(await prompter.select({
33
+ message: "Select Omadeus environment",
34
+ options: Object.keys(OMADEUS_ENVIRONMENTS).map((env) => ({
35
+ value: env,
36
+ label: OMADEUS_ENVIRONMENTS[env].label,
37
+ hint: getOmadeusEnvironmentUrls(env).maestroUrl
38
+ })),
39
+ initialValue: initial
40
+ }));
41
+ }
33
42
  async function promptOrganizationId(params) {
34
43
  const { prompter, maestroUrl, email, existing } = params;
35
44
  try {
@@ -143,28 +152,43 @@ async function promptCredentials(prompter, existing) {
143
152
  })).trim()
144
153
  };
145
154
  }
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
- }));
155
+ /**
156
+ * Prompt for the set of users allowed to message this OpenClaw instance.
157
+ *
158
+ * This single allowlist governs direct messages, channels, and entity rooms.
159
+ * There is no "all users" option: only whitelisted members may message
160
+ * OpenClaw. The logged-in user is always added
161
+ * to the allowlist (and is excluded from the selectable list by the caller) so
162
+ * they can interact with their own OpenClaw. Self-authored echoes (the reply
163
+ * loop) are filtered earlier, at socket ingestion, by the SentMessageTracker —
164
+ * not by the inbound policy — so allowing yourself here cannot cause a loop.
165
+ */
166
+ async function promptMessagingAllowlist(params) {
167
+ const { prompter, members, selfReferenceId, existingReferenceIds } = params;
168
+ let selected = [];
169
+ if (members.length === 0) await prompter.note("No other organization members found. Only you will be able to message OpenClaw.", "Omadeus messaging allowlist");
170
+ else {
171
+ const memberReferenceIds = new Set(members.map((member) => member.referenceId));
172
+ const initialValues = (existingReferenceIds ?? []).filter((id) => id !== selfReferenceId && memberReferenceIds.has(id)).map(String);
173
+ selected = readReferenceIds(await promptMultiSelect({
174
+ prompter,
175
+ message: "Which users do you want to be able to message this OpenClaw instance? (You are always allowed.)",
176
+ options: memberOptions(members),
177
+ initialValues
178
+ }));
179
+ }
180
+ return Array.from(new Set([selfReferenceId, ...selected]));
181
+ }
182
+ /**
183
+ * Ask whether an @mention is required to trigger OpenClaw in a given surface
184
+ * (channels or entity rooms). DMs never use this — you can't @mention in a DM.
185
+ */
186
+ async function promptRequireMention(params) {
187
+ if (params.existing === "outsideAllowlist") return "outsideAllowlist";
188
+ return await params.prompter.confirm({
189
+ message: `Require an @mention to trigger OpenClaw in ${params.surfaceLabel}?`,
190
+ initialValue: params.existing ? params.existing !== "never" : true
191
+ }) ? "always" : "never";
168
192
  }
169
193
  async function promptEntityKindSelection(params) {
170
194
  const selected = await promptMultiSelect({
@@ -208,11 +232,11 @@ const omadeusSetupWizard = {
208
232
  const account = resolveOmadeusAccount({ cfg });
209
233
  const section = getOmadeusChannelConfig(cfg) ?? {};
210
234
  let next = cfg;
211
- if (account.credentialSource === "none") await noteOmadeusAuthHelp(prompter);
235
+ const environment = await promptEnvironment(prompter, section.environment ? resolveOmadeusEnvironment(section.environment) : void 0);
236
+ const { casUrl, maestroUrl } = getOmadeusEnvironmentUrls(environment);
237
+ if (account.credentialSource === "none") await noteOmadeusAuthHelp(prompter, environment);
212
238
  const envEmail = process.env.OMADEUS_EMAIL?.trim();
213
239
  const envPassword = process.env.OMADEUS_PASSWORD?.trim();
214
- const casUrl = OMADEUS_CAS_URL;
215
- const maestroUrl = OMADEUS_MAESTRO_URL;
216
240
  let { email, password } = await promptCredentials(prompter, {
217
241
  email: section.email ?? envEmail,
218
242
  password: section.password ?? envPassword
@@ -260,11 +284,15 @@ const omadeusSetupWizard = {
260
284
  excludeReferenceIds: [selfReferenceId]
261
285
  });
262
286
  const existingInbound = section.inbound;
263
- const directSenderIds = await promptSenderAllowlist({
287
+ const allowedUserReferenceIds = await promptMessagingAllowlist({
264
288
  prompter,
265
- message: "Which users can DM OpenClaw directly?",
266
289
  members,
267
- existingReferenceIds: existingInbound?.direct?.allowedSenderReferenceIds
290
+ selfReferenceId,
291
+ existingReferenceIds: Array.from(new Set([
292
+ ...existingInbound?.direct?.allowedSenderReferenceIds ?? [],
293
+ ...existingInbound?.channels?.allowedSenderReferenceIds ?? [],
294
+ ...existingInbound?.entities?.allowedSenderReferenceIds ?? []
295
+ ]))
268
296
  });
269
297
  const selectedChannels = await promptChannelSelection({
270
298
  prompter,
@@ -273,33 +301,34 @@ const omadeusSetupWizard = {
273
301
  memberReferenceId: selfReferenceId,
274
302
  existingChannelViewIds: existingInbound?.channels?.allowedChannelViewIds
275
303
  });
276
- const channelSenderIds = selectedChannels.length > 0 ? await promptSenderAllowlist({
304
+ const channelSenderIds = selectedChannels.length > 0 ? allowedUserReferenceIds : void 0;
305
+ const channelRequireMention = selectedChannels.length > 0 ? await promptRequireMention({
277
306
  prompter,
278
- message: "Which users can trigger OpenClaw from allowed channels?",
279
- members,
280
- existingReferenceIds: existingInbound?.channels?.allowedSenderReferenceIds
281
- }) : void 0;
307
+ surfaceLabel: "allowed channels",
308
+ existing: existingInbound?.channels?.requireMention
309
+ }) : "never";
282
310
  const entityKinds = await promptEntityKindSelection({
283
311
  prompter,
284
312
  existingKinds: existingInbound?.entities?.allowedKinds
285
313
  });
286
- const entitySenderIds = entityKinds.length > 0 ? await promptSenderAllowlist({
314
+ const entitySenderIds = entityKinds.length > 0 ? allowedUserReferenceIds : void 0;
315
+ const entityRequireMention = entityKinds.length > 0 ? await promptRequireMention({
287
316
  prompter,
288
- message: "Which users can trigger OpenClaw from entity rooms?",
289
- members,
290
- existingReferenceIds: existingInbound?.entities?.allowedSenderReferenceIds
291
- }) : void 0;
317
+ surfaceLabel: "entity rooms",
318
+ existing: existingInbound?.entities?.requireMention
319
+ }) : "never";
292
320
  const channelRoomIds = selectedChannels.flatMap((selectedChannel) => [selectedChannel.publicRoomId, selectedChannel.privateRoomId]).filter((id) => typeof id === "number");
293
321
  const channelViewIds = selectedChannels.map((selectedChannel) => selectedChannel.id);
294
322
  const channelTitles = selectedChannels.map((selectedChannel) => selectedChannel.title || `Channel ${selectedChannel.id}`).join(", ");
295
323
  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).";
324
+ const mentionSummary = (require) => require === "never" ? "no @mention required" : require === "outsideAllowlist" ? "@mention required outside the allowlist" : "@mention required";
325
+ const channelSummary = selectedChannels.length > 0 ? `- Channels "${channelTitles}": rooms ${channelRoomIds.join(", ") || "(no room ids)"} from ${senderSummary(channelSenderIds)}; ${mentionSummary(channelRequireMention)}.` : "- Channels: disabled (none selected).";
326
+ const entitySummary = entityKinds.length > 0 ? `- Entity rooms (${entityKinds.join(", ")}): ${senderSummary(entitySenderIds)}; ${mentionSummary(entityRequireMention)}.` : "- Entity rooms: disabled (no room types selected).";
298
327
  await prompter.note([
299
328
  `Inbound policy (Jaguar chat):`,
300
- `- Direct messages: enabled for ${senderSummary(directSenderIds)} (no @mention required).`,
329
+ `- Direct messages: enabled for ${senderSummary(allowedUserReferenceIds)}.`,
301
330
  channelSummary,
302
- `- Entity rooms (${entityKindSummary}): ${senderSummary(entitySenderIds)}; @mention required.`
331
+ entitySummary
303
332
  ].join("\n"), "Omadeus inbound policy");
304
333
  next = {
305
334
  ...next,
@@ -307,17 +336,17 @@ const omadeusSetupWizard = {
307
336
  ...next.channels,
308
337
  omadeus: {
309
338
  enabled: true,
310
- casUrl,
311
- maestroUrl,
339
+ environment,
312
340
  email,
313
341
  password,
314
342
  organizationId,
315
343
  sessionToken,
344
+ sessionTokenEnvironment: environment,
316
345
  inbound: {
317
346
  version: 1,
318
347
  direct: {
319
348
  enabled: true,
320
- ...directSenderIds ? { allowedSenderReferenceIds: directSenderIds } : {},
349
+ allowedSenderReferenceIds: allowedUserReferenceIds,
321
350
  requireMention: "never"
322
351
  },
323
352
  channels: {
@@ -325,13 +354,13 @@ const omadeusSetupWizard = {
325
354
  allowedRoomIds: channelRoomIds,
326
355
  allowedChannelViewIds: channelViewIds,
327
356
  ...channelSenderIds ? { allowedSenderReferenceIds: channelSenderIds } : {},
328
- requireMention: "outsideAllowlist"
357
+ requireMention: channelRequireMention
329
358
  },
330
359
  entities: {
331
360
  enabled: entityKinds.length > 0,
332
361
  allowedKinds: entityKinds,
333
362
  ...entitySenderIds ? { allowedSenderReferenceIds: entitySenderIds } : {},
334
- requireMention: "always"
363
+ requireMention: entityRequireMention
335
364
  }
336
365
  }
337
366
  }
@@ -1,12 +1,21 @@
1
+ import { generateTemporaryId } from "./utils/http.util.js";
1
2
  import { sendRoomMessage } from "./api/message.api.js";
2
3
  //#region src/outbound.ts
3
4
  async function sendOmadeusMessage(deps, params) {
4
5
  const { to, text } = params;
6
+ const temporaryId = generateTemporaryId();
7
+ deps.sentTracker?.trackOutbound({
8
+ temporaryId,
9
+ body: text,
10
+ roomId: to
11
+ });
5
12
  const result = await sendRoomMessage(deps.apiOpts, {
6
13
  roomId: to,
7
- body: text
14
+ body: text,
15
+ temporaryId
8
16
  });
9
17
  if (!result.ok) throw new Error(`Omadeus send failed: ${result.error}`);
18
+ if (typeof result.message?.id === "number") deps.sentTracker?.trackId(result.message.id);
10
19
  return {
11
20
  channel: "omadeus",
12
21
  messageId: String(result.message?.id ?? ""),