@brantrusnak/openclaw-omadeus 1.0.0 → 1.0.2

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.
@@ -1,3 +1,7 @@
1
+ import { readNuggetNumber } from "./api/nugget.api.js";
2
+ import { mergePeopleIntoNuggetAgentPayload } from "./member-resolve.js";
3
+ import type { OmadeusApiOptions } from "./utils/http.util.js";
4
+
1
5
  export type NuggetLookupIntent = {
2
6
  /** Display nugget number from `N###` (maps to API `number`, not internal `id`). */
3
7
  nuggetNumber: number;
@@ -150,6 +154,13 @@ const NUGGET_FIELDS_FOR_AGENT = [
150
154
  "releaseNumber",
151
155
  "publicRoomId",
152
156
  "privateRoomId",
157
+ "memberReferenceId",
158
+ "assigneeReferenceId",
159
+ "ownerReferenceId",
160
+ "memberFirstName",
161
+ "memberLastName",
162
+ "assigneeFirstName",
163
+ "assigneeLastName",
153
164
  ] as const;
154
165
 
155
166
  export function pickNuggetFieldsForAgent(record: Record<string, unknown>): Record<string, unknown> {
@@ -162,16 +173,31 @@ export function pickNuggetFieldsForAgent(record: Record<string, unknown>): Recor
162
173
  return out;
163
174
  }
164
175
 
176
+ /**
177
+ * Picked Dolphin fields + `people` map (referenceId → display name) for the organization.
178
+ */
179
+ export async function buildNuggetAgentDataPayload(
180
+ apiOpts: OmadeusApiOptions,
181
+ fullRecord: Record<string, unknown> | null,
182
+ ): Promise<Record<string, unknown> | null> {
183
+ if (!fullRecord) {
184
+ return null;
185
+ }
186
+ const base = pickNuggetFieldsForAgent(fullRecord);
187
+ return mergePeopleIntoNuggetAgentPayload(apiOpts, fullRecord, base);
188
+ }
189
+
165
190
  /**
166
191
  * Augments the user message so the agent receives Dolphin nugget/task data and can reply with a summary.
167
192
  * On miss or API error, the agent still gets instructions to respond helpfully.
168
193
  */
169
- export function appendNuggetLookupContextForAgent(
194
+ export async function appendNuggetLookupContextForAgent(
170
195
  rawBody: string,
171
196
  nuggetNumber: number,
172
197
  record: Record<string, unknown> | null,
198
+ apiOpts: OmadeusApiOptions,
173
199
  fetchError?: string,
174
- ): string {
200
+ ): Promise<string> {
175
201
  const header = `[Omadeus nugget/task N${nuggetNumber}]`;
176
202
 
177
203
  if (fetchError) {
@@ -188,9 +214,46 @@ export function appendNuggetLookupContextForAgent(
188
214
  );
189
215
  }
190
216
 
191
- const payload = pickNuggetFieldsForAgent(record);
217
+ const payload = (await buildNuggetAgentDataPayload(apiOpts, record)) ?? pickNuggetFieldsForAgent(record);
218
+ return (
219
+ `${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` +
220
+ `${JSON.stringify(payload, null, 2)}`
221
+ );
222
+ }
223
+
224
+ /**
225
+ * Enriches a Task or Nugget **Jaguar room** with Dolphin data matched by this chat's `roomId`, so the
226
+ * agent can answer "status" without a bare `N###` in the message.
227
+ */
228
+ export async function appendNuggetContextForTaskOrNuggetRoom(
229
+ rawBody: string,
230
+ roomId: number,
231
+ roomName: string | null,
232
+ record: Record<string, unknown> | null,
233
+ apiOpts: OmadeusApiOptions,
234
+ fetchError?: string,
235
+ ): Promise<string> {
236
+ const roomLabel = roomName?.trim() ? `room ${roomId} ("${roomName.trim()}")` : `room ${roomId}`;
237
+
238
+ if (fetchError) {
239
+ return (
240
+ `${rawBody}\n\n[Omadeus, this task/nugget ${roomLabel}] Lookup failed: ${fetchError}.\n` +
241
+ `Answer from this thread. Do not tell the user to "use the Omadeus platform" or similar — give a direct reply or a concrete next step.`
242
+ );
243
+ }
244
+
245
+ if (!record) {
246
+ return (
247
+ `${rawBody}\n\n[Omadeus, this task/nugget ${roomLabel}] No Dolphin row matched this room id in search yet.\n` +
248
+ `Answer from the conversation; be honest if you cannot see live fields. Do not hand-wave to "the platform".`
249
+ );
250
+ }
251
+
252
+ const n = readNuggetNumber(record);
253
+ const nLabel = n !== undefined ? `N${n}` : "nugget";
254
+ const payload = (await buildNuggetAgentDataPayload(apiOpts, record)) ?? pickNuggetFieldsForAgent(record);
192
255
  return (
193
- `${rawBody}\n\n${header} Data from Omadeus (summarize for someone tracking this work — status, ownership, timeline, project; plain language):\n` +
256
+ `${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` +
194
257
  `${JSON.stringify(payload, null, 2)}`
195
258
  );
196
259
  }
package/src/onboarding.ts CHANGED
@@ -1,25 +1,58 @@
1
1
  import type { ChannelSetupWizard, OpenClawConfig, WizardPrompter } from "openclaw/plugin-sdk/setup";
2
- import { DEFAULT_ACCOUNT_ID, formatDocsLink } from "openclaw/plugin-sdk/setup";
2
+ import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
3
3
  import {
4
- listMemberChannelViews,
5
4
  listOrganizationMembers,
6
5
  listOrganizations,
7
6
  } from "./api/auth.api.js";
7
+ import { listMemberChannelViews } from "./api/channel.api.js";
8
8
  import { authenticate } from "./auth.js";
9
9
  import { getOmadeusChannelConfig, resolveOmadeusAccount } from "./config.js";
10
10
  import { OMADEUS_CAS_URL, OMADEUS_MAESTRO_URL } from "./defaults.js";
11
11
  import type {
12
12
  OmadeusChannelConfig,
13
13
  OmadeusChannelView,
14
+ OmadeusInboundEntityKind,
14
15
  OmadeusOrganizationMember,
15
16
  } from "./types.js";
16
17
 
17
18
  const channel = "omadeus" as const;
19
+ const DONE = "__done__";
20
+ const ALL_ENTITY_KINDS: OmadeusInboundEntityKind[] = [
21
+ "task",
22
+ "nugget",
23
+ "project",
24
+ "release",
25
+ "sprint",
26
+ "summary",
27
+ "client",
28
+ "folder",
29
+ ];
18
30
 
19
31
  type CoreConfig = OpenClawConfig & {
20
32
  channels?: { omadeus?: OmadeusChannelConfig };
21
33
  };
22
34
 
35
+ type SelectOption = {
36
+ value: string;
37
+ label: string;
38
+ hint?: string;
39
+ };
40
+
41
+ type MultiSelectPrompter = {
42
+ multiSelect?: (args: {
43
+ message: string;
44
+ options: SelectOption[];
45
+ initialValues?: string[];
46
+ initialValue?: string[];
47
+ }) => Promise<string[]>;
48
+ multiselect?: (args: {
49
+ message: string;
50
+ options: SelectOption[];
51
+ initialValues?: string[];
52
+ initialValue?: string[];
53
+ }) => Promise<string[]>;
54
+ };
55
+
23
56
  function getOmadeusSection(cfg: OpenClawConfig): OmadeusChannelConfig | undefined {
24
57
  return getOmadeusChannelConfig(cfg as CoreConfig);
25
58
  }
@@ -34,7 +67,6 @@ async function noteOmadeusAuthHelp(prompter: WizardPrompter): Promise<void> {
34
67
  `CAS URL: ${OMADEUS_CAS_URL}`,
35
68
  `Maestro URL: ${OMADEUS_MAESTRO_URL}`,
36
69
  "Env vars supported: OMADEUS_EMAIL, OMADEUS_PASSWORD, OMADEUS_ORGANIZATION_ID.",
37
- `Docs: ${formatDocsLink("/channels/omadeus", "omadeus")}`,
38
70
  ].join("\n"),
39
71
  "Omadeus setup",
40
72
  );
@@ -97,14 +129,9 @@ async function promptChannelSelection(params: {
97
129
  maestroUrl: string;
98
130
  sessionToken: string;
99
131
  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;
132
+ existingChannelViewIds?: number[];
133
+ }): Promise<OmadeusChannelView[]> {
134
+ const { prompter, maestroUrl, sessionToken, memberReferenceId, existingChannelViewIds } = params;
108
135
  const channels = await listMemberChannelViews({
109
136
  maestroUrl,
110
137
  sessionToken,
@@ -115,31 +142,26 @@ async function promptChannelSelection(params: {
115
142
  if (channels.length === 0) {
116
143
  throw new Error("No channels found for this account.");
117
144
  }
118
- const selected = await prompter.select({
119
- message: "Which channel to use?",
145
+ const selected = await promptMultiSelect({
146
+ prompter,
147
+ message: "Which channels should OpenClaw listen to?",
120
148
  options: channels.map((item) => ({
121
149
  value: String(item.id),
122
150
  label: item.title || `Channel ${item.id}`,
151
+ hint: [item.privateRoomId ? `private:${item.privateRoomId}` : undefined, item.publicRoomId ? `public:${item.publicRoomId}` : undefined]
152
+ .filter(Boolean)
153
+ .join(" | "),
123
154
  })),
124
- initialValue:
125
- existing?.selectedChannelViewId !== undefined
126
- ? String(existing.selectedChannelViewId)
127
- : String(channels[0]!.id),
155
+ initialValues:
156
+ existingChannelViewIds && existingChannelViewIds.length > 0
157
+ ? existingChannelViewIds.map(String)
158
+ : [String(channels[0]!.id)],
128
159
  });
129
- const chosen = channels.find((item) => String(item.id) === String(selected));
130
- if (!chosen) {
131
- throw new Error("Selected channel was not found.");
160
+ const chosen = channels.filter((item) => selected.includes(String(item.id)));
161
+ if (chosen.length === 0) {
162
+ throw new Error("At least one channel must be selected.");
132
163
  }
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
- };
164
+ return chosen;
143
165
  }
144
166
 
145
167
  function memberLabel(member: OmadeusOrganizationMember): string {
@@ -162,93 +184,129 @@ function memberHint(member: OmadeusOrganizationMember): string | undefined {
162
184
  return parts.length > 0 ? parts.join(" | ") : undefined;
163
185
  }
164
186
 
165
- function filterMembersByQuery(
166
- members: OmadeusOrganizationMember[],
167
- query: string,
168
- ): OmadeusOrganizationMember[] {
169
- const q = query.trim().toLowerCase();
170
- if (!q) {
171
- return members;
187
+ async function promptMultiSelect(params: {
188
+ prompter: WizardPrompter;
189
+ message: string;
190
+ options: SelectOption[];
191
+ initialValues?: string[];
192
+ }): Promise<string[]> {
193
+ const multi = params.prompter as unknown as MultiSelectPrompter;
194
+ const runMulti = multi.multiSelect ?? multi.multiselect;
195
+ if (runMulti) {
196
+ return runMulti({
197
+ message: params.message,
198
+ options: params.options,
199
+ initialValues: params.initialValues,
200
+ initialValue: params.initialValues,
201
+ });
202
+ }
203
+
204
+ const selected = new Set(params.initialValues ?? []);
205
+ while (true) {
206
+ const next = await params.prompter.select({
207
+ message: `${params.message} (${selected.size} selected)`,
208
+ options: [
209
+ { value: DONE, label: selected.size > 0 ? "Done" : "Done (select none)" },
210
+ ...params.options.map((option) => ({
211
+ ...option,
212
+ label: selected.has(option.value) ? `[selected] ${option.label}` : option.label,
213
+ })),
214
+ ],
215
+ initialValue: DONE,
216
+ });
217
+ const value = String(next);
218
+ if (value === DONE) {
219
+ return [...selected];
220
+ }
221
+ if (selected.has(value)) {
222
+ selected.delete(value);
223
+ } else {
224
+ selected.add(value);
225
+ }
172
226
  }
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
227
  }
184
228
 
185
- async function promptMemberSelection(params: {
186
- prompter: WizardPrompter;
229
+ async function loadSelectableMembers(params: {
187
230
  maestroUrl: string;
188
231
  sessionToken: string;
189
232
  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 = (
233
+ excludeReferenceIds?: number[];
234
+ }): Promise<OmadeusOrganizationMember[]> {
235
+ const excluded = new Set(params.excludeReferenceIds ?? []);
236
+ return (
203
237
  await listOrganizationMembers({
204
- maestroUrl,
205
- sessionToken,
206
- organizationId,
238
+ maestroUrl: params.maestroUrl,
239
+ sessionToken: params.sessionToken,
240
+ organizationId: params.organizationId,
207
241
  })
208
242
  )
209
- .filter((member) => member.isSystem !== true)
243
+ .filter((member) => member.isSystem !== true && !excluded.has(member.referenceId))
210
244
  .sort((a, b) => memberLabel(a).localeCompare(memberLabel(b)));
245
+ }
211
246
 
247
+ function memberOptions(members: OmadeusOrganizationMember[]): SelectOption[] {
248
+ return members.map((member) => ({
249
+ value: String(member.referenceId),
250
+ label: memberLabel(member),
251
+ hint: memberHint(member),
252
+ }));
253
+ }
254
+
255
+ function readReferenceIds(values: string[]): number[] {
256
+ return values
257
+ .map((value) => Number(value))
258
+ .filter((value) => Number.isInteger(value) && value > 0);
259
+ }
260
+
261
+ async function promptSenderAllowlist(params: {
262
+ prompter: WizardPrompter;
263
+ message: string;
264
+ members: OmadeusOrganizationMember[];
265
+ existingReferenceIds?: number[];
266
+ }): Promise<number[] | undefined> {
267
+ const { prompter, message, members, existingReferenceIds } = params;
212
268
  if (members.length === 0) {
213
269
  throw new Error("No organization members found.");
214
270
  }
215
271
 
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
- };
272
+ const mode = await prompter.select({
273
+ message,
274
+ options: [
275
+ { value: "all", label: "All users", hint: "No sender allowlist" },
276
+ { value: "specific", label: "Specific users", hint: "Select one or more users" },
277
+ ],
278
+ initialValue: existingReferenceIds && existingReferenceIds.length > 0 ? "specific" : "all",
279
+ });
280
+ if (String(mode) === "all") {
281
+ return undefined;
251
282
  }
283
+
284
+ const selected = await promptMultiSelect({
285
+ prompter,
286
+ message: `${message} (specific users)`,
287
+ options: memberOptions(members),
288
+ initialValues: existingReferenceIds?.map(String),
289
+ });
290
+ return readReferenceIds(selected);
291
+ }
292
+
293
+ async function promptEntityKindSelection(params: {
294
+ prompter: WizardPrompter;
295
+ existingKinds?: OmadeusInboundEntityKind[];
296
+ }): Promise<OmadeusInboundEntityKind[]> {
297
+ const selected = await promptMultiSelect({
298
+ prompter: params.prompter,
299
+ message: "Which entity room types should OpenClaw listen to?",
300
+ options: ALL_ENTITY_KINDS.map((kind) => ({
301
+ value: kind,
302
+ label: kind,
303
+ })),
304
+ initialValues:
305
+ params.existingKinds && params.existingKinds.length > 0
306
+ ? params.existingKinds
307
+ : ALL_ENTITY_KINDS,
308
+ });
309
+ return ALL_ENTITY_KINDS.filter((kind) => selected.includes(kind));
252
310
  }
253
311
 
254
312
  export const omadeusSetupWizard: ChannelSetupWizard = {
@@ -372,26 +430,79 @@ export const omadeusSetupWizard: ChannelSetupWizard = {
372
430
  throw new Error("Authentication is required to list channels.");
373
431
  }
374
432
 
375
- const selectedMember = await promptMemberSelection({
376
- prompter,
433
+ if (typeof selfReferenceId !== "number") {
434
+ throw new Error("Authentication did not return an Omadeus member reference ID.");
435
+ }
436
+
437
+ const members = await loadSelectableMembers({
377
438
  maestroUrl,
378
439
  sessionToken,
379
440
  organizationId,
380
- existingMemberReferenceId: section.selectedMemberReferenceId,
381
- fallbackMemberReferenceId: selfReferenceId,
441
+ excludeReferenceIds: [selfReferenceId],
442
+ });
443
+ const existingInbound = section.inbound;
444
+
445
+ const directSenderIds = await promptSenderAllowlist({
446
+ prompter,
447
+ message: "Which users can DM OpenClaw directly?",
448
+ members,
449
+ existingReferenceIds: existingInbound?.direct?.allowedSenderReferenceIds,
382
450
  });
383
451
 
384
- const selectedChannel = await promptChannelSelection({
452
+ const selectedChannels = await promptChannelSelection({
385
453
  prompter,
386
454
  maestroUrl,
387
455
  sessionToken,
388
- memberReferenceId: selectedMember.memberReferenceId,
389
- existing: section,
456
+ memberReferenceId: selfReferenceId,
457
+ existingChannelViewIds: existingInbound?.channels?.allowedChannelViewIds,
390
458
  });
391
459
 
460
+ const channelSenderIds = await promptSenderAllowlist({
461
+ prompter,
462
+ message: "Which users can trigger OpenClaw from allowed channels?",
463
+ members,
464
+ existingReferenceIds: existingInbound?.channels?.allowedSenderReferenceIds,
465
+ });
466
+
467
+ const entityKinds = await promptEntityKindSelection({
468
+ prompter,
469
+ existingKinds: existingInbound?.entities?.allowedKinds,
470
+ });
471
+
472
+ const entitySenderIds =
473
+ entityKinds.length > 0
474
+ ? await promptSenderAllowlist({
475
+ prompter,
476
+ message: "Which users can trigger OpenClaw from entity rooms?",
477
+ members,
478
+ existingReferenceIds: existingInbound?.entities?.allowedSenderReferenceIds,
479
+ })
480
+ : undefined;
481
+
482
+ const channelRoomIds = selectedChannels
483
+ .flatMap((selectedChannel) => [
484
+ selectedChannel.publicRoomId,
485
+ selectedChannel.privateRoomId,
486
+ ])
487
+ .filter((id): id is number => typeof id === "number");
488
+ const channelViewIds = selectedChannels.map((selectedChannel) => selectedChannel.id);
489
+ const channelTitles = selectedChannels
490
+ .map((selectedChannel) => selectedChannel.title || `Channel ${selectedChannel.id}`)
491
+ .join(", ");
492
+
493
+ const senderSummary = (ids: number[] | undefined) =>
494
+ ids && ids.length > 0 ? ids.join(", ") : "all users";
495
+ const entityKindSummary =
496
+ entityKinds.length > 0 ? entityKinds.join(", ") : "none (entity rooms disabled)";
497
+
392
498
  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",
499
+ [
500
+ `Inbound policy (Jaguar chat):`,
501
+ `- Direct messages: enabled for ${senderSummary(directSenderIds)} (no @mention required).`,
502
+ `- Channels "${channelTitles}": rooms ${channelRoomIds.join(", ") || "(no room ids)"} from ${senderSummary(channelSenderIds)}; @mention not required in those rooms.`,
503
+ `- Entity rooms (${entityKindSummary}): ${senderSummary(entitySenderIds)}; @mention required.`,
504
+ ].join("\n"),
505
+ "Omadeus inbound policy",
395
506
  );
396
507
 
397
508
  next = {
@@ -399,7 +510,6 @@ export const omadeusSetupWizard: ChannelSetupWizard = {
399
510
  channels: {
400
511
  ...next.channels,
401
512
  omadeus: {
402
- ...getOmadeusSection(next),
403
513
  enabled: true,
404
514
  casUrl,
405
515
  maestroUrl,
@@ -407,15 +517,27 @@ export const omadeusSetupWizard: ChannelSetupWizard = {
407
517
  password,
408
518
  organizationId,
409
519
  ...(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
- : {}),
520
+ inbound: {
521
+ version: 1,
522
+ direct: {
523
+ enabled: true,
524
+ ...(directSenderIds ? { allowedSenderReferenceIds: directSenderIds } : {}),
525
+ requireMention: "never",
526
+ },
527
+ channels: {
528
+ enabled: true,
529
+ allowedRoomIds: channelRoomIds,
530
+ allowedChannelViewIds: channelViewIds,
531
+ ...(channelSenderIds ? { allowedSenderReferenceIds: channelSenderIds } : {}),
532
+ requireMention: "outsideAllowlist",
533
+ },
534
+ entities: {
535
+ enabled: entityKinds.length > 0,
536
+ allowedKinds: entityKinds,
537
+ ...(entitySenderIds ? { allowedSenderReferenceIds: entitySenderIds } : {}),
538
+ requireMention: "always",
539
+ },
540
+ },
419
541
  },
420
542
  },
421
543
  };
package/src/types.ts CHANGED
@@ -2,6 +2,63 @@
2
2
  // Omadeus config shape (stored under channels.omadeus in OpenClaw config)
3
3
  // ---------------------------------------------------------------------------
4
4
 
5
+ export type OmadeusInboundMentionPolicy = "never" | "always" | "outsideAllowlist";
6
+
7
+ export type OmadeusInboundDirectPolicy = {
8
+ enabled: boolean;
9
+ allowedSenderReferenceIds?: number[];
10
+ requireMention?: "never" | "always";
11
+ };
12
+
13
+ export type OmadeusInboundChannelsPolicy = {
14
+ enabled: boolean;
15
+ allowedRoomIds?: number[];
16
+ allowedChannelViewIds?: number[];
17
+ allowedSenderReferenceIds?: number[];
18
+ requireMention?: OmadeusInboundMentionPolicy;
19
+ };
20
+
21
+ /** Jaguar `subscribableKind` values treated as entity chat (not DM, not channel). */
22
+ export type OmadeusInboundEntityKind =
23
+ | "task"
24
+ | "nugget"
25
+ | "project"
26
+ | "release"
27
+ | "sprint"
28
+ | "summary"
29
+ | "client"
30
+ | "folder";
31
+
32
+ /** Jaguar entity chats (`subscribableKind`) — DMs and channel rooms use other values. */
33
+ export const OMADEUS_INBOUND_ENTITY_KINDS: readonly OmadeusInboundEntityKind[] = [
34
+ "task",
35
+ "nugget",
36
+ "project",
37
+ "release",
38
+ "sprint",
39
+ "summary",
40
+ "client",
41
+ "folder",
42
+ ];
43
+
44
+ export const OMADEUS_INBOUND_ENTITY_KIND_SET: ReadonlySet<string> = new Set(OMADEUS_INBOUND_ENTITY_KINDS);
45
+
46
+ export type OmadeusInboundEntitiesPolicy = {
47
+ enabled: boolean;
48
+ allowedKinds?: OmadeusInboundEntityKind[];
49
+ allowedRoomIds?: number[];
50
+ allowedSenderReferenceIds?: number[];
51
+ requireMention?: OmadeusInboundMentionPolicy;
52
+ };
53
+
54
+ /** Jaguar chat ingress policy (DMs, channel rooms, entity rooms). */
55
+ export type OmadeusInboundPolicy = {
56
+ version?: number;
57
+ direct?: OmadeusInboundDirectPolicy;
58
+ channels?: OmadeusInboundChannelsPolicy;
59
+ entities?: OmadeusInboundEntitiesPolicy;
60
+ };
61
+
5
62
  export type OmadeusChannelConfig = {
6
63
  enabled?: boolean;
7
64
  casUrl?: string;
@@ -11,13 +68,8 @@ export type OmadeusChannelConfig = {
11
68
  organizationId?: number;
12
69
  /** Cached Omadeus session JWT obtained during onboarding/startup. */
13
70
  sessionToken?: string;
14
- /** Selected member reference ID (account) used by onboarding. */
15
- selectedMemberReferenceId?: number;
16
- /** Selected channel metadata used for inbound filtering. */
17
- selectedChannelViewId?: number;
18
- selectedChannelTitle?: string;
19
- selectedChannelPrivateRoomId?: number;
20
- selectedChannelPublicRoomId?: number;
71
+ /** Jaguar chat ingress allowlists and mention rules. */
72
+ inbound?: OmadeusInboundPolicy;
21
73
  };
22
74
 
23
75
  export type ResolvedOmadeusAccount = {
@@ -101,6 +153,13 @@ export type OmadeusJwtPayload = {
101
153
  // Jaguar socket message (chat — DMs, nugget rooms, task rooms, etc.)
102
154
  // ---------------------------------------------------------------------------
103
155
 
156
+ /**
157
+ * Omadeus subscribable **type** on Jaguar chat payloads (room context).
158
+ *
159
+ * - **direct** — DM from a user.
160
+ * - **channel** — message in a channel room.
161
+ * - **nugget** … **folder** — entity-associated chat (tasks, work items, hierarchy).
162
+ */
104
163
  export type OmadeusSubscribableType =
105
164
  | "direct"
106
165
  | "channel"
@@ -112,6 +171,15 @@ export type OmadeusSubscribableType =
112
171
  | "client"
113
172
  | "folder"
114
173
  | (string & {});
174
+
175
+ /**
176
+ * Omadeus subscribable **kind** on Jaguar chat payloads (same coarse buckets as `OmadeusSubscribableType`;
177
+ * routing uses `subscribableKind` in the plugin).
178
+ *
179
+ * - **direct** — DM from a user.
180
+ * - **channel** — message in a channel room.
181
+ * - **task**, **nugget**, **project**, **release**, **sprint**, **summary**, **client**, **folder** — entity chat.
182
+ */
115
183
  export type OmadeusSubscribableKind =
116
184
  | "task"
117
185
  | "direct"
@@ -181,6 +249,8 @@ export type OmadeusInboundMessage = {
181
249
  roomName: string | null;
182
250
  subscribableType: OmadeusSubscribableType;
183
251
  subscribableKind: OmadeusSubscribableKind;
252
+ /** When present, used with `inbound.channels.allowedChannelViewIds`. */
253
+ channelViewId?: number;
184
254
  isMention: boolean;
185
255
  timestamp: number;
186
256
  };