@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/channel.ts CHANGED
@@ -28,6 +28,7 @@ import {
28
28
  } from "./api/nugget.api.js";
29
29
  import { addMessageReaction, deleteMessage, editMessage } from "./api/message.api.js";
30
30
  import {
31
+ getOmadeusChannelConfig,
31
32
  listOmadeusAccountIds,
32
33
  resolveDefaultOmadeusAccountId,
33
34
  resolveOmadeusAccount,
@@ -44,30 +45,55 @@ import { createJaguarSocketClient, type JaguarSocketClient } from "./socket/jagu
44
45
  import { createTokenManager, type OmadeusTokenManager } from "./token.js";
45
46
  import type { ResolvedOmadeusAccount as Account } from "./types.js";
46
47
 
47
- // Gateway-scoped state for the running account
48
- let activeTokenManager: OmadeusTokenManager | null = null;
49
- let activeDolphin: DolphinSocketClient | null = null;
50
- let activeJaguar: JaguarSocketClient | null = null;
48
+ const CHANNEL_ID = "omadeus" as const;
49
+
50
+ const gatewayState: {
51
+ tokenManager: OmadeusTokenManager | null;
52
+ dolphin: DolphinSocketClient | null;
53
+ jaguar: JaguarSocketClient | null;
54
+ } = { tokenManager: null, dolphin: null, jaguar: null };
55
+
56
+ const isUnconfigured = (account: Account) => account.credentialSource === "none";
57
+
58
+ let lastPersistedToken: string | null = null;
51
59
 
52
60
  async function persistSessionToken(token: string): Promise<void> {
61
+ if (lastPersistedToken === token) return;
53
62
  const runtime = getOmadeusRuntime();
54
- const cfg = runtime.config.loadConfig();
55
- const section = ((cfg.channels as Record<string, unknown> | undefined)?.["omadeus"] ??
56
- {}) as Record<string, unknown>;
57
- if (section["sessionToken"] === token) {
63
+ const cfg = runtime.config.current() as OpenClawConfig;
64
+ const section = getOmadeusChannelConfig(cfg) ?? {};
65
+ if (section.sessionToken === token) {
66
+ lastPersistedToken = token;
58
67
  return;
59
68
  }
60
-
61
- await runtime.config.writeConfigFile({
62
- ...cfg,
63
- channels: {
64
- ...cfg.channels,
65
- omadeus: {
66
- ...section,
67
- sessionToken: token,
68
- },
69
+ await runtime.config.mutateConfigFile({
70
+ afterWrite: { mode: "auto" },
71
+ mutate: (draft) => {
72
+ draft.channels = {
73
+ ...(draft.channels ?? {}),
74
+ omadeus: {
75
+ ...(getOmadeusChannelConfig(draft) ?? {}),
76
+ sessionToken: token,
77
+ },
78
+ };
69
79
  },
70
- } as OpenClawConfig);
80
+ });
81
+ lastPersistedToken = token;
82
+ }
83
+
84
+ function actionError(text: string, error = text) {
85
+ return {
86
+ isError: true as const,
87
+ content: [{ type: "text" as const, text }],
88
+ details: { error },
89
+ };
90
+ }
91
+
92
+ function actionOk(payload: Record<string, unknown>) {
93
+ return {
94
+ content: [{ type: "text" as const, text: JSON.stringify({ ok: true, channel: CHANNEL_ID, ...payload }) }],
95
+ details: { ok: true, channel: CHANNEL_ID, ...payload },
96
+ };
71
97
  }
72
98
 
73
99
  const omadeusConfigAdapter = createTopLevelChannelConfigAdapter<Account>({
@@ -212,7 +238,7 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
212
238
  describeMessageTool: ({ cfg }) => {
213
239
  const enabled =
214
240
  cfg.channels?.omadeus?.enabled !== false &&
215
- resolveOmadeusAccount({ cfg }).credentialSource !== "none";
241
+ !isUnconfigured(resolveOmadeusAccount({ cfg }));
216
242
  return {
217
243
  actions: enabled ? ["send", "edit", "delete", "react"] : [],
218
244
  capabilities: [],
@@ -220,28 +246,19 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
220
246
  };
221
247
  },
222
248
  handleAction: async (ctx) => {
223
- const apiOptsForAccount = () => {
224
- const account = resolveOmadeusAccount({ cfg: ctx.cfg });
225
- if (!activeTokenManager) {
249
+ const account = resolveOmadeusAccount({ cfg: ctx.cfg });
250
+ const apiOpts = () => {
251
+ if (!gatewayState.tokenManager) {
226
252
  throw new Error("Omadeus: not connected; gateway must be running with Omadeus enabled.");
227
253
  }
228
- return { maestroUrl: account.maestroUrl, tokenManager: activeTokenManager };
254
+ return { maestroUrl: account.maestroUrl, tokenManager: gatewayState.tokenManager };
229
255
  };
230
256
 
231
257
  if (ctx.action === "send" && isCreateNuggetRequest(ctx.params)) {
232
258
  const title = readStringParam(ctx.params, ["title", "subject", "name"]);
233
259
  const description = readStringParam(ctx.params, ["description", "details", "body"]);
234
260
  if (!title || !description) {
235
- return {
236
- isError: true,
237
- content: [
238
- {
239
- type: "text" as const,
240
- text: "Omadeus create task/nugget requires `title` and `description`.",
241
- },
242
- ],
243
- details: { error: "Missing title/description." },
244
- };
261
+ return actionError("Omadeus create task/nugget requires `title` and `description`.", "Missing title/description.");
245
262
  }
246
263
 
247
264
  const kind = readNuggetKind(ctx.params);
@@ -249,25 +266,19 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
249
266
  const stage = readStringParam(ctx.params, ["stage"]) ?? "Triage";
250
267
  const memberReferenceId =
251
268
  readNumberParam(ctx.params, ["memberReferenceId", "assigneeReferenceId"]) ??
252
- activeTokenManager?.getPayload().referenceId;
269
+ gatewayState.tokenManager?.getPayload().referenceId;
253
270
  const clientId = readNumberParam(ctx.params, ["clientId"]) ?? 1;
254
271
  const folderId = readNumberParam(ctx.params, ["folderId"]) ?? 1;
255
272
 
256
273
  if (!memberReferenceId) {
257
- return {
258
- isError: true,
259
- content: [
260
- {
261
- type: "text" as const,
262
- text: "Omadeus create task/nugget needs `memberReferenceId` or an active authenticated user.",
263
- },
264
- ],
265
- details: { error: "Missing memberReferenceId." },
266
- };
274
+ return actionError(
275
+ "Omadeus create task/nugget needs `memberReferenceId` or an active authenticated user.",
276
+ "Missing memberReferenceId.",
277
+ );
267
278
  }
268
279
 
269
280
  try {
270
- const created = await createNugget(apiOptsForAccount(), {
281
+ const created = await createNugget(apiOpts(), {
271
282
  title,
272
283
  description,
273
284
  stage,
@@ -277,39 +288,10 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
277
288
  clientId,
278
289
  folderId,
279
290
  });
280
- const number = created["number"];
281
- const id = created["id"];
282
- return {
283
- content: [
284
- {
285
- type: "text" as const,
286
- text: JSON.stringify({
287
- ok: true,
288
- channel: "omadeus",
289
- action: "create",
290
- kind,
291
- number,
292
- id,
293
- title,
294
- }),
295
- },
296
- ],
297
- details: {
298
- ok: true,
299
- channel: "omadeus",
300
- action: "create",
301
- kind,
302
- number,
303
- id,
304
- },
305
- };
291
+ return actionOk({ action: "create", kind, number: created["number"], id: created["id"], title });
306
292
  } catch (err) {
307
293
  const msg = err instanceof Error ? err.message : String(err);
308
- return {
309
- isError: true,
310
- content: [{ type: "text" as const, text: msg }],
311
- details: { error: msg },
312
- };
294
+ return actionError(msg);
313
295
  }
314
296
  }
315
297
 
@@ -321,93 +303,47 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
321
303
  (typeof ctx.params.content === "string" && ctx.params.content.trim()) ||
322
304
  "";
323
305
  if (messageId == null) {
324
- return {
325
- isError: true,
326
- content: [
327
- {
328
- type: "text" as const,
329
- text: "Omadeus edit requires `messageId` (Jaguar message id) or current inbound MessageSid.",
330
- },
331
- ],
332
- details: { error: "Missing messageId for edit." },
333
- };
306
+ return actionError(
307
+ "Omadeus edit requires `messageId` (Jaguar message id) or current inbound MessageSid.",
308
+ "Missing messageId for edit.",
309
+ );
334
310
  }
335
311
  if (!body) {
336
- return {
337
- isError: true,
338
- content: [
339
- {
340
- type: "text" as const,
341
- text: "Omadeus edit requires new text in `message`, `text`, or `content`.",
342
- },
343
- ],
344
- details: { error: "Missing body for edit." },
345
- };
312
+ return actionError(
313
+ "Omadeus edit requires new text in `message`, `text`, or `content`.",
314
+ "Missing body for edit.",
315
+ );
346
316
  }
347
317
  try {
348
- await editMessage(apiOptsForAccount(), { messageId, body });
318
+ await editMessage(apiOpts(), { messageId, body });
349
319
  } catch (err) {
350
320
  const msg = err instanceof Error ? err.message : String(err);
351
- return {
352
- isError: true,
353
- content: [{ type: "text" as const, text: msg }],
354
- details: { error: msg },
355
- };
321
+ return actionError(msg);
356
322
  }
357
- return {
358
- content: [
359
- {
360
- type: "text" as const,
361
- text: JSON.stringify({ ok: true, channel: "omadeus", action: "edit", messageId }),
362
- },
363
- ],
364
- details: { ok: true, channel: "omadeus", messageId },
365
- };
323
+ return actionOk({ action: "edit", messageId });
366
324
  }
367
325
 
368
326
  if (ctx.action === "delete") {
369
327
  const messageId = readReactionMessageId(ctx.params, ctx.toolContext);
370
328
  if (messageId == null) {
371
- return {
372
- isError: true,
373
- content: [
374
- {
375
- type: "text" as const,
376
- text: "Omadeus delete requires `messageId` (Jaguar message id) or current inbound MessageSid.",
377
- },
378
- ],
379
- details: { error: "Missing messageId for delete." },
380
- };
329
+ return actionError(
330
+ "Omadeus delete requires `messageId` (Jaguar message id) or current inbound MessageSid.",
331
+ "Missing messageId for delete.",
332
+ );
381
333
  }
382
334
  try {
383
- await deleteMessage(apiOptsForAccount(), { messageId });
335
+ await deleteMessage(apiOpts(), { messageId });
384
336
  } catch (err) {
385
337
  const msg = err instanceof Error ? err.message : String(err);
386
- return {
387
- isError: true,
388
- content: [{ type: "text" as const, text: msg }],
389
- details: { error: msg },
390
- };
338
+ return actionError(msg);
391
339
  }
392
- return {
393
- content: [
394
- {
395
- type: "text" as const,
396
- text: JSON.stringify({ ok: true, channel: "omadeus", action: "delete", messageId }),
397
- },
398
- ],
399
- details: { ok: true, channel: "omadeus", messageId },
400
- };
340
+ return actionOk({ action: "delete", messageId });
401
341
  }
402
342
 
403
343
  if (ctx.action === "react") {
404
344
  const emoji = typeof ctx.params.emoji === "string" ? ctx.params.emoji.trim() : "";
405
345
  if (!emoji) {
406
- return {
407
- isError: true,
408
- content: [{ type: "text" as const, text: "Omadeus react requires `emoji`." }],
409
- details: { error: "Omadeus react requires emoji." },
410
- };
346
+ return actionError("Omadeus react requires `emoji`.", "Omadeus react requires emoji.");
411
347
  }
412
348
  if (!isAllowedOmadeusReactionEmoji(emoji)) {
413
349
  return {
@@ -416,7 +352,7 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
416
352
  type: "text" as const,
417
353
  text: JSON.stringify({
418
354
  ok: true,
419
- channel: "omadeus",
355
+ channel: CHANNEL_ID,
420
356
  ignored: true,
421
357
  reason: "unsupported_emoji",
422
358
  emoji,
@@ -424,84 +360,42 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
424
360
  }),
425
361
  },
426
362
  ],
427
- details: {
428
- ok: true,
429
- ignored: true,
430
- channel: "omadeus",
431
- },
363
+ details: { ok: true, ignored: true, channel: CHANNEL_ID },
432
364
  };
433
365
  }
434
366
  const messageId = readReactionMessageId(ctx.params, ctx.toolContext);
435
367
  if (messageId == null) {
436
- return {
437
- isError: true,
438
- content: [
439
- {
440
- type: "text" as const,
441
- text: "Omadeus react requires `messageId` or a current inbound message id (MessageSid).",
442
- },
443
- ],
444
- details: { error: "Missing messageId for reaction." },
445
- };
446
- }
447
- if (!activeTokenManager) {
448
- return {
449
- isError: true,
450
- content: [
451
- {
452
- type: "text" as const,
453
- text: "Omadeus is not connected; cannot react (gateway must be running).",
454
- },
455
- ],
456
- details: { error: "Omadeus not connected." },
457
- };
368
+ return actionError(
369
+ "Omadeus react requires `messageId` or a current inbound message id (MessageSid).",
370
+ "Missing messageId for reaction.",
371
+ );
458
372
  }
459
373
  try {
460
- await addMessageReaction(apiOptsForAccount(), { messageId, emoji });
374
+ await addMessageReaction(apiOpts(), { messageId, emoji });
461
375
  } catch (err) {
462
376
  const msg = err instanceof Error ? err.message : String(err);
463
- return {
464
- isError: true,
465
- content: [{ type: "text" as const, text: msg }],
466
- details: { error: msg },
467
- };
377
+ return actionError(msg);
468
378
  }
469
- return {
470
- content: [
471
- {
472
- type: "text" as const,
473
- text: JSON.stringify({
474
- ok: true,
475
- channel: "omadeus",
476
- messageId,
477
- emoji,
478
- }),
479
- },
480
- ],
481
- details: { ok: true, channel: "omadeus", messageId, emoji },
482
- };
379
+ return actionOk({ messageId, emoji });
483
380
  }
484
- // Return null to fall through to default handler
485
- return null as never;
381
+
382
+ throw new Error(`Unhandled Omadeus action: ${String(ctx.action)}`);
486
383
  },
487
384
  },
488
385
  reload: { configPrefixes: ["channels.omadeus"] },
489
386
  setup: omadeusSetupAdapter,
490
387
  setupWizard: omadeusSetupWizard,
491
388
 
492
- // -------------------------------------------------------------------------
493
- // Config adapter
494
- // -------------------------------------------------------------------------
495
389
  config: {
496
390
  ...omadeusConfigAdapter,
497
- isConfigured: (account) => account.credentialSource !== "none",
391
+ isConfigured: (account) => !isUnconfigured(account),
498
392
  unconfiguredReason: () =>
499
393
  "Omadeus requires email, password, and organizationId. Run: openclaw setup omadeus",
500
394
  describeAccount: (account) => ({
501
395
  accountId: account.accountId,
502
396
  name: account.name,
503
397
  enabled: account.enabled,
504
- configured: account.credentialSource !== "none",
398
+ configured: !isUnconfigured(account),
505
399
  credentialSource: account.credentialSource,
506
400
  baseUrl: account.maestroUrl,
507
401
  }),
@@ -519,13 +413,13 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
519
413
  const id = normalizeOmadeusRoomId(input);
520
414
  if (!id) {
521
415
  const taskIntent = parseTaskChannelTargetIntent(input);
522
- if (!taskIntent || !activeTokenManager) {
416
+ if (!taskIntent || !gatewayState.tokenManager) {
523
417
  return null;
524
418
  }
525
419
  const roomId = await resolveTaskRoomIdByNumber(
526
420
  {
527
421
  maestroUrl: resolveOmadeusAccount({ cfg }).maestroUrl,
528
- tokenManager: activeTokenManager,
422
+ tokenManager: gatewayState.tokenManager,
529
423
  },
530
424
  { nuggetNumber: taskIntent.nuggetNumber },
531
425
  );
@@ -549,18 +443,15 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
549
443
  },
550
444
  },
551
445
 
552
- // -------------------------------------------------------------------------
553
- // Outbound adapter
554
- // -------------------------------------------------------------------------
555
446
  outbound: {
556
447
  deliveryMode: "direct",
557
448
  textChunkLimit: 4000,
558
449
  chunker: (text, limit) => getOmadeusRuntime().channel.text.chunkMarkdownText(text, limit),
559
450
  chunkerMode: "markdown",
560
451
  ...createAttachedChannelResultAdapter({
561
- channel: "omadeus",
452
+ channel: CHANNEL_ID,
562
453
  sendText: async ({ cfg, to, text }) => {
563
- if (!activeJaguar || !activeTokenManager) {
454
+ if (!gatewayState.jaguar || !gatewayState.tokenManager) {
564
455
  throw new Error("Omadeus: not connected. Is the gateway running with Omadeus enabled?");
565
456
  }
566
457
  const deps: OutboundDeps = {
@@ -568,9 +459,9 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
568
459
  maestroUrl: resolveOmadeusAccount({
569
460
  cfg,
570
461
  }).maestroUrl,
571
- tokenManager: activeTokenManager,
462
+ tokenManager: gatewayState.tokenManager,
572
463
  },
573
- jaguarSocket: activeJaguar,
464
+ jaguarSocket: gatewayState.jaguar,
574
465
  };
575
466
  return await sendOmadeusMessage(deps, { to, text });
576
467
  },
@@ -592,9 +483,6 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
592
483
  },
593
484
  },
594
485
 
595
- // -------------------------------------------------------------------------
596
- // Status adapter
597
- // -------------------------------------------------------------------------
598
486
  status: {
599
487
  defaultRuntime: defaultRuntimeState,
600
488
  collectStatusIssues: (accounts): ChannelStatusIssue[] =>
@@ -602,7 +490,7 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
602
490
  const issues: ChannelStatusIssue[] = [];
603
491
  if (entry.enabled !== false && entry.configured !== true) {
604
492
  issues.push({
605
- channel: "omadeus",
493
+ channel: CHANNEL_ID,
606
494
  accountId: String(entry.accountId ?? DEFAULT_ACCOUNT_ID),
607
495
  kind: "config",
608
496
  message: "Omadeus credentials are missing.",
@@ -625,7 +513,7 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
625
513
  accountId: account.accountId,
626
514
  name: account.name,
627
515
  enabled: account.enabled,
628
- configured: account.credentialSource !== "none",
516
+ configured: !isUnconfigured(account),
629
517
  runtime,
630
518
  }),
631
519
  baseUrl: account.maestroUrl,
@@ -635,15 +523,12 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
635
523
  }),
636
524
  },
637
525
 
638
- // -------------------------------------------------------------------------
639
- // Gateway adapter — starts sockets on gateway boot
640
- // -------------------------------------------------------------------------
641
526
  gateway: {
642
527
  startAccount: async (ctx) => {
643
528
  const { account, cfg, abortSignal } = ctx;
644
529
  ctx.log?.info(`[omadeus] starting for org ${account.organizationId}`);
645
530
 
646
- if (account.credentialSource === "none") {
531
+ if (isUnconfigured(account)) {
647
532
  ctx.log?.warn("[omadeus] skipping start: credentials not configured");
648
533
  ctx.setStatus({
649
534
  accountId: account.accountId,
@@ -665,8 +550,8 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
665
550
  }
666
551
 
667
552
  const log = ctx.log ?? { info: () => {}, warn: () => {}, error: () => {} };
553
+ let isConnected = false;
668
554
 
669
- // Auth
670
555
  const tokenManager = createTokenManager({
671
556
  casUrl: account.casUrl,
672
557
  maestroUrl: account.maestroUrl,
@@ -696,7 +581,7 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
696
581
  }
697
582
 
698
583
  tokenManager.startAutoRefresh();
699
- activeTokenManager = tokenManager;
584
+ gatewayState.tokenManager = tokenManager;
700
585
 
701
586
  const selfReferenceId = tokenManager.getPayload().referenceId;
702
587
 
@@ -713,7 +598,6 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
713
598
  selfReferenceId,
714
599
  });
715
600
 
716
- // Jaguar socket (chat — DMs, nugget/task/project rooms)
717
601
  const jaguar = createJaguarSocketClient({
718
602
  maestroUrl: account.maestroUrl,
719
603
  tokenManager,
@@ -742,17 +626,19 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
742
626
  onOtherEvent: (data) => {
743
627
  log.info(`[jaguar] non-message event: ${JSON.stringify(data).slice(0, 120)}`);
744
628
  },
745
- onConnect: () =>
746
- ctx.setStatus({
747
- accountId: account.accountId,
748
- connected: true,
749
- lastConnectedAt: Date.now(),
750
- }),
751
- onDisconnect: () => ctx.setStatus({ accountId: account.accountId, connected: false }),
629
+ onConnect: () => {
630
+ if (!isConnected) {
631
+ isConnected = true;
632
+ ctx.setStatus({ accountId: account.accountId, connected: true, lastConnectedAt: Date.now() });
633
+ }
634
+ },
635
+ onDisconnect: () => {
636
+ isConnected = false;
637
+ ctx.setStatus({ accountId: account.accountId, connected: false });
638
+ },
752
639
  onError: (err) => ctx.setStatus({ accountId: account.accountId, lastError: err.message }),
753
640
  });
754
641
 
755
- // Dolphin socket (data — tasks, projects, sprints, releases)
756
642
  const dolphin = createDolphinSocketClient({
757
643
  maestroUrl: account.maestroUrl,
758
644
  tokenManager,
@@ -761,13 +647,16 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
761
647
  log.info(`[dolphin] event: ${JSON.stringify(data).slice(0, 120)}`);
762
648
  // TODO: handle task assignment/update events as they are discovered
763
649
  },
764
- onConnect: () =>
765
- ctx.setStatus({
766
- accountId: account.accountId,
767
- connected: true,
768
- lastConnectedAt: Date.now(),
769
- }),
770
- onDisconnect: () => ctx.setStatus({ accountId: account.accountId, connected: false }),
650
+ onConnect: () => {
651
+ if (!isConnected) {
652
+ isConnected = true;
653
+ ctx.setStatus({ accountId: account.accountId, connected: true, lastConnectedAt: Date.now() });
654
+ }
655
+ },
656
+ onDisconnect: () => {
657
+ isConnected = false;
658
+ ctx.setStatus({ accountId: account.accountId, connected: false });
659
+ },
771
660
  onError: (err) => ctx.setStatus({ accountId: account.accountId, lastError: err.message }),
772
661
  });
773
662
 
@@ -776,8 +665,8 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
776
665
 
777
666
  jaguar.connect();
778
667
  dolphin.connect();
779
- activeJaguar = jaguar;
780
- activeDolphin = dolphin;
668
+ gatewayState.jaguar = jaguar;
669
+ gatewayState.dolphin = dolphin;
781
670
 
782
671
  ctx.setStatus({
783
672
  accountId: account.accountId,
@@ -792,9 +681,10 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
792
681
  tokenManager.stopAutoRefresh();
793
682
  jaguar.disconnect();
794
683
  dolphin.disconnect();
795
- activeTokenManager = null;
796
- activeJaguar = null;
797
- activeDolphin = null;
684
+ gatewayState.tokenManager = null;
685
+ gatewayState.jaguar = null;
686
+ gatewayState.dolphin = null;
687
+ lastPersistedToken = null;
798
688
  ctx.setStatus({
799
689
  accountId: account.accountId,
800
690
  running: false,
@@ -802,7 +692,6 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
802
692
  });
803
693
  };
804
694
 
805
- // Keep this account runner alive until the gateway aborts it.
806
695
  await new Promise<void>((resolve) => {
807
696
  if (abortSignal.aborted) {
808
697
  resolve();
@@ -2,7 +2,7 @@ import { listOrganizationMembers } from "./api/auth.api.js";
2
2
  import type { OmadeusOrganizationMember } from "./types.js";
3
3
  import type { OmadeusApiOptions } from "./utils/http.util.js";
4
4
 
5
- function formatMemberLabel(m: OmadeusOrganizationMember): string {
5
+ export function formatMemberLabel(m: OmadeusOrganizationMember): string {
6
6
  const fullName = `${m.firstName ?? ""} ${m.lastName ?? ""}`.trim();
7
7
  if (fullName) {
8
8
  return fullName;