@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
package/src/onboarding.ts CHANGED
@@ -8,29 +8,17 @@ 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
+ import { formatMemberLabel } from "./member-resolve.js";
11
12
  import type {
12
13
  OmadeusChannelConfig,
13
14
  OmadeusChannelView,
14
15
  OmadeusInboundEntityKind,
15
16
  OmadeusOrganizationMember,
16
17
  } from "./types.js";
18
+ import { OMADEUS_INBOUND_ENTITY_KINDS } from "./types.js";
17
19
 
18
20
  const channel = "omadeus" as const;
19
21
  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
- ];
30
-
31
- type CoreConfig = OpenClawConfig & {
32
- channels?: { omadeus?: OmadeusChannelConfig };
33
- };
34
22
 
35
23
  type SelectOption = {
36
24
  value: string;
@@ -38,25 +26,18 @@ type SelectOption = {
38
26
  hint?: string;
39
27
  };
40
28
 
29
+ type MultiSelectFn = (args: {
30
+ message: string;
31
+ options: SelectOption[];
32
+ initialValues?: string[];
33
+ initialValue?: string[];
34
+ }) => Promise<string[]>;
35
+
41
36
  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[]>;
37
+ multiSelect?: MultiSelectFn;
38
+ multiselect?: MultiSelectFn;
54
39
  };
55
40
 
56
- function getOmadeusSection(cfg: OpenClawConfig): OmadeusChannelConfig | undefined {
57
- return getOmadeusChannelConfig(cfg as CoreConfig);
58
- }
59
-
60
41
  async function noteOmadeusAuthHelp(prompter: WizardPrompter): Promise<void> {
61
42
  await prompter.note(
62
43
  [
@@ -80,35 +61,32 @@ async function promptOrganizationId(params: {
80
61
  }): Promise<number> {
81
62
  const { prompter, maestroUrl, email, existing } = params;
82
63
 
83
- // Try to list organizations from the API
84
- if (maestroUrl && email) {
85
- try {
86
- const orgs = await listOrganizations({ maestroUrl, email });
87
- if (orgs.length > 0) {
88
- if (orgs.length === 1) {
89
- await prompter.note(
90
- `Found organization: ${orgs[0]!.title} (${orgs[0]!.id})`,
91
- "Omadeus organization",
92
- );
93
- return orgs[0]!.id;
94
- }
95
- const choice = await prompter.select({
96
- message: "Select organization",
97
- options: orgs.map((org) => ({
98
- value: String(org.id),
99
- label: `${org.title} (${org.membersCount} members)`,
100
- hint: `ID: ${org.id}`,
101
- })),
102
- initialValue: existing ? String(existing) : String(orgs[0]!.id),
103
- });
104
- return Number(choice);
64
+ try {
65
+ const orgs = await listOrganizations({ maestroUrl, email });
66
+ if (orgs.length > 0) {
67
+ if (orgs.length === 1) {
68
+ await prompter.note(
69
+ `Found organization: ${orgs[0]!.title} (${orgs[0]!.id})`,
70
+ "Omadeus organization",
71
+ );
72
+ return orgs[0]!.id;
105
73
  }
106
- } catch {
107
- await prompter.note(
108
- "Could not fetch organizations from the API. Enter the ID manually.",
109
- "Omadeus organization",
110
- );
74
+ const choice = await prompter.select({
75
+ message: "Select organization",
76
+ options: orgs.map((org) => ({
77
+ value: String(org.id),
78
+ label: `${org.title} (${org.membersCount} members)`,
79
+ hint: `ID: ${org.id}`,
80
+ })),
81
+ initialValue: existing ? String(existing) : String(orgs[0]!.id),
82
+ });
83
+ return Number(choice);
111
84
  }
85
+ } catch {
86
+ await prompter.note(
87
+ "Could not fetch organizations from the API. Enter the ID manually.",
88
+ "Omadeus organization",
89
+ );
112
90
  }
113
91
 
114
92
  const raw = await prompter.text({
@@ -164,20 +142,6 @@ async function promptChannelSelection(params: {
164
142
  return chosen;
165
143
  }
166
144
 
167
- function memberLabel(member: OmadeusOrganizationMember): string {
168
- const fullName = `${member.firstName ?? ""} ${member.lastName ?? ""}`.trim();
169
- if (member.title?.trim()) {
170
- return member.title.trim();
171
- }
172
- if (fullName) {
173
- return fullName;
174
- }
175
- if (member.email?.trim()) {
176
- return member.email.trim();
177
- }
178
- return `Member ${member.referenceId}`;
179
- }
180
-
181
145
  function memberHint(member: OmadeusOrganizationMember): string | undefined {
182
146
  const fullName = `${member.firstName ?? ""} ${member.lastName ?? ""}`.trim();
183
147
  const parts = [fullName, member.email?.trim(), `ref:${member.referenceId}`].filter(Boolean);
@@ -241,13 +205,13 @@ async function loadSelectableMembers(params: {
241
205
  })
242
206
  )
243
207
  .filter((member) => member.isSystem !== true && !excluded.has(member.referenceId))
244
- .sort((a, b) => memberLabel(a).localeCompare(memberLabel(b)));
208
+ .sort((a, b) => formatMemberLabel(a).localeCompare(formatMemberLabel(b)));
245
209
  }
246
210
 
247
211
  function memberOptions(members: OmadeusOrganizationMember[]): SelectOption[] {
248
212
  return members.map((member) => ({
249
213
  value: String(member.referenceId),
250
- label: memberLabel(member),
214
+ label: formatMemberLabel(member),
251
215
  hint: memberHint(member),
252
216
  }));
253
217
  }
@@ -258,6 +222,27 @@ function readReferenceIds(values: string[]): number[] {
258
222
  .filter((value) => Number.isInteger(value) && value > 0);
259
223
  }
260
224
 
225
+ async function promptCredentials(
226
+ prompter: WizardPrompter,
227
+ existing: { email?: string; password?: string },
228
+ ): Promise<{ email: string; password: string }> {
229
+ const email = String(
230
+ await prompter.text({
231
+ message: "Omadeus username (email)",
232
+ initialValue: existing.email,
233
+ validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
234
+ }),
235
+ ).trim();
236
+ const password = String(
237
+ await prompter.text({
238
+ message: "Omadeus password",
239
+ sensitive: true,
240
+ validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
241
+ }),
242
+ ).trim();
243
+ return { email, password };
244
+ }
245
+
261
246
  async function promptSenderAllowlist(params: {
262
247
  prompter: WizardPrompter;
263
248
  message: string;
@@ -277,7 +262,7 @@ async function promptSenderAllowlist(params: {
277
262
  ],
278
263
  initialValue: existingReferenceIds && existingReferenceIds.length > 0 ? "specific" : "all",
279
264
  });
280
- if (String(mode) === "all") {
265
+ if (mode === "all") {
281
266
  return undefined;
282
267
  }
283
268
 
@@ -297,16 +282,17 @@ async function promptEntityKindSelection(params: {
297
282
  const selected = await promptMultiSelect({
298
283
  prompter: params.prompter,
299
284
  message: "Which entity room types should OpenClaw listen to?",
300
- options: ALL_ENTITY_KINDS.map((kind) => ({
285
+ options: OMADEUS_INBOUND_ENTITY_KINDS.map((kind) => ({
301
286
  value: kind,
302
287
  label: kind,
303
288
  })),
304
289
  initialValues:
305
290
  params.existingKinds && params.existingKinds.length > 0
306
291
  ? params.existingKinds
307
- : ALL_ENTITY_KINDS,
292
+ : [...OMADEUS_INBOUND_ENTITY_KINDS],
308
293
  });
309
- return ALL_ENTITY_KINDS.filter((kind) => selected.includes(kind));
294
+ const selectedSet = new Set(selected);
295
+ return OMADEUS_INBOUND_ENTITY_KINDS.filter((kind) => selectedSet.has(kind));
310
296
  }
311
297
 
312
298
  export const omadeusSetupWizard: ChannelSetupWizard = {
@@ -343,7 +329,7 @@ export const omadeusSetupWizard: ChannelSetupWizard = {
343
329
  credentials: [],
344
330
  finalize: async ({ cfg, prompter }) => {
345
331
  const account = resolveOmadeusAccount({ cfg });
346
- const section = getOmadeusSection(cfg) ?? {};
332
+ const section = getOmadeusChannelConfig(cfg) ?? {};
347
333
  let next = cfg;
348
334
 
349
335
  if (account.credentialSource === "none") {
@@ -356,21 +342,10 @@ export const omadeusSetupWizard: ChannelSetupWizard = {
356
342
  const casUrl = OMADEUS_CAS_URL;
357
343
  const maestroUrl = OMADEUS_MAESTRO_URL;
358
344
 
359
- let email = String(
360
- await prompter.text({
361
- message: "Omadeus username (email)",
362
- initialValue: section.email ?? envEmail,
363
- validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
364
- }),
365
- ).trim();
366
-
367
- let password = String(
368
- await prompter.text({
369
- message: "Omadeus password",
370
- initialValue: section.password ?? envPassword,
371
- validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
372
- }),
373
- ).trim();
345
+ let { email, password } = await promptCredentials(prompter, {
346
+ email: section.email ?? envEmail,
347
+ password: section.password ?? envPassword,
348
+ });
374
349
 
375
350
  const organizationId = await promptOrganizationId({
376
351
  prompter,
@@ -379,7 +354,6 @@ export const omadeusSetupWizard: ChannelSetupWizard = {
379
354
  existing: section.organizationId,
380
355
  });
381
356
 
382
- // Verify the full auth flow before saving
383
357
  let sessionToken: string | undefined;
384
358
  let selfReferenceId: number | undefined;
385
359
  while (true) {
@@ -409,20 +383,7 @@ export const omadeusSetupWizard: ChannelSetupWizard = {
409
383
  );
410
384
  break;
411
385
  }
412
- email = String(
413
- await prompter.text({
414
- message: "Omadeus email",
415
- initialValue: email,
416
- validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
417
- }),
418
- ).trim();
419
- password = String(
420
- await prompter.text({
421
- message: "Omadeus password",
422
- initialValue: password,
423
- validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
424
- }),
425
- ).trim();
386
+ ({ email, password } = await promptCredentials(prompter, { email, password }));
426
387
  }
427
388
  }
428
389
 
@@ -516,7 +477,7 @@ export const omadeusSetupWizard: ChannelSetupWizard = {
516
477
  email,
517
478
  password,
518
479
  organizationId,
519
- ...(sessionToken ? { sessionToken } : {}),
480
+ sessionToken,
520
481
  inbound: {
521
482
  version: 1,
522
483
  direct: {
@@ -548,7 +509,7 @@ export const omadeusSetupWizard: ChannelSetupWizard = {
548
509
  ...cfg,
549
510
  channels: {
550
511
  ...cfg.channels,
551
- omadeus: { ...getOmadeusSection(cfg), enabled: false },
512
+ omadeus: { ...getOmadeusChannelConfig(cfg), enabled: false },
552
513
  },
553
514
  }),
554
515
  };
package/src/setup-core.ts CHANGED
@@ -35,12 +35,21 @@ export const omadeusSetupAdapter: ChannelSetupAdapter = {
35
35
  const password = input.password?.trim() || undefined;
36
36
  const organizationId = readSetupNumberField(rawInput, "organizationId");
37
37
 
38
+ const channelsRecord = cfg.channels as Record<string, unknown> | undefined;
39
+ const omadeusExisting = channelsRecord?.["omadeus"];
40
+ const omadeusPrevious =
41
+ omadeusExisting !== null &&
42
+ typeof omadeusExisting === "object" &&
43
+ !Array.isArray(omadeusExisting)
44
+ ? (omadeusExisting as Record<string, unknown>)
45
+ : {};
46
+
38
47
  return {
39
48
  ...cfg,
40
49
  channels: {
41
50
  ...cfg.channels,
42
51
  omadeus: {
43
- ...(cfg.channels as Record<string, unknown>)?.["omadeus"],
52
+ ...omadeusPrevious,
44
53
  enabled: true,
45
54
  ...(casUrl ? { casUrl } : {}),
46
55
  ...(maestroUrl ? { maestroUrl } : {}),
@@ -31,10 +31,12 @@ const HEARTBEAT_MISSED_MAX = 5;
31
31
  const KEEP_ALIVE_CONTENT = "keep-alive";
32
32
  const KEEP_ALIVE_ACTION = "answer";
33
33
 
34
- function isKeepAliveMessage(data: Record<string, unknown>): boolean {
35
- const content = (data as { content?: unknown }).content;
36
- const payloadData = (data as { data?: unknown }).data;
37
- return content === KEEP_ALIVE_CONTENT || payloadData === KEEP_ALIVE_CONTENT;
34
+ function isServerKeepAlive(data: Record<string, unknown>): boolean {
35
+ return (data as { content?: unknown }).content === KEEP_ALIVE_CONTENT;
36
+ }
37
+
38
+ function isClientKeepAlive(data: Record<string, unknown>): boolean {
39
+ return (data as { data?: unknown }).data === KEEP_ALIVE_CONTENT;
38
40
  }
39
41
 
40
42
  export function createOmadeusSocketClient(opts: OmadeusSocketOptions): OmadeusSocketClient {
@@ -87,7 +89,7 @@ export function createOmadeusSocketClient(opts: OmadeusSocketOptions): OmadeusSo
87
89
  return;
88
90
  }
89
91
  heartbeatMissCount += 1;
90
- ws.send(JSON.stringify({ data: KEEP_ALIVE_CONTENT, action: KEEP_ALIVE_ACTION }));
92
+ sendKeepAliveFrame();
91
93
 
92
94
  if (heartbeatMissCount >= HEARTBEAT_MISSED_MAX) {
93
95
  log?.warn(
@@ -104,6 +106,12 @@ export function createOmadeusSocketClient(opts: OmadeusSocketOptions): OmadeusSo
104
106
  }, HEARTBEAT_INTERVAL_MS);
105
107
  }
106
108
 
109
+ function sendKeepAliveFrame() {
110
+ if (ws?.readyState === WebSocket.OPEN) {
111
+ ws.send(JSON.stringify({ data: KEEP_ALIVE_CONTENT, action: KEEP_ALIVE_ACTION }));
112
+ }
113
+ }
114
+
107
115
  function connect() {
108
116
  if (ws) {
109
117
  ws.removeAllListeners();
@@ -144,19 +152,24 @@ export function createOmadeusSocketClient(opts: OmadeusSocketOptions): OmadeusSo
144
152
  const data = JSON.parse(String(raw)) as Record<string, unknown>;
145
153
 
146
154
  const action = (data as { action?: unknown }).action;
147
- if (isKeepAliveMessage(data) && action === KEEP_ALIVE_ACTION) {
155
+ if (isServerKeepAlive(data) && action === KEEP_ALIVE_ACTION) {
148
156
  resetHeartbeat();
149
157
  return;
150
158
  }
151
159
 
152
- // If backend sends heartbeat pings, answer them immediately.
153
- if (isKeepAliveMessage(data) && action === "heartbeat") {
154
- if (ws?.readyState === WebSocket.OPEN) {
155
- ws.send(JSON.stringify({ data: KEEP_ALIVE_CONTENT, action: KEEP_ALIVE_ACTION }));
156
- }
160
+ if (isClientKeepAlive(data) && action === KEEP_ALIVE_ACTION) {
161
+ resetHeartbeat();
162
+ return;
163
+ }
164
+
165
+ // If backend sends a heartbeat ping, answer it immediately.
166
+ if (isServerKeepAlive(data) && action === "heartbeat") {
167
+ resetHeartbeat();
168
+ sendKeepAliveFrame();
157
169
  return;
158
170
  }
159
171
 
172
+ resetHeartbeat();
160
173
  onEvent?.(data);
161
174
  } catch {
162
175
  log?.warn(`${logPrefix} unparseable message: ${String(raw).slice(0, 200)}`);