@brantrusnak/openclaw-omadeus 1.0.1 → 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 (49) hide show
  1. package/README.md +5 -2
  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/openclaw.plugin.json +144 -28
  36. package/package.json +12 -7
  37. package/src/api/auth.api.ts +0 -29
  38. package/src/api/channel.api.ts +29 -0
  39. package/src/api/nugget.api.ts +81 -10
  40. package/src/channel.ts +136 -247
  41. package/src/inbound-policy.ts +250 -0
  42. package/src/inbound.ts +20 -0
  43. package/src/member-resolve.ts +84 -0
  44. package/src/message-handler.ts +99 -53
  45. package/src/nugget-lookup.ts +67 -4
  46. package/src/onboarding.ts +283 -200
  47. package/src/setup-core.ts +10 -1
  48. package/src/socket/socket.ts +24 -11
  49. package/src/types.ts +77 -7
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # OpenClaw Omadeus Plugin
2
2
 
3
- Omadeus channel plugin for [OpenClaw](https://www.npmjs.com/package/openclaw).
3
+ [Omadeus](https://omadeus.com) plugin for [OpenClaw](https://www.npmjs.com/package/openclaw).
4
4
 
5
5
  This plugin connects OpenClaw to Omadeus over WebSocket so OpenClaw can listen
6
6
  for Omadeus messages and reply through the selected Omadeus channel.
@@ -88,15 +88,18 @@ openclaw plugins doctor
88
88
 
89
89
  ## Local Development
90
90
 
91
- From this repository, you can link the local plugin into OpenClaw:
91
+ Build the runtime files before linking a local checkout into OpenClaw:
92
92
 
93
93
  ```bash
94
+ npm install
95
+ npm run build
94
96
  openclaw plugins install . --link
95
97
  ```
96
98
 
97
99
  Before publishing, inspect the npm package contents:
98
100
 
99
101
  ```bash
102
+ npm run prepack
100
103
  npm pack --dry-run
101
104
  ```
102
105
 
@@ -0,0 +1,4 @@
1
+ import "node:module";
2
+ import.meta.url;
3
+ //#endregion
4
+ export {};
package/dist/api.js ADDED
@@ -0,0 +1,5 @@
1
+ import "./_virtual/_rolldown/runtime.js";
2
+ import { omadeusSetupAdapter } from "./src/setup-core.js";
3
+ import { omadeusSetupWizard } from "./src/onboarding.js";
4
+ import "./src/setup-surface.js";
5
+ export { omadeusSetupAdapter, omadeusSetupWizard };
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ import "./_virtual/_rolldown/runtime.js";
2
+ import { setOmadeusRuntime } from "./src/runtime.js";
3
+ import { omadeusPlugin } from "./src/channel.js";
4
+ import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
5
+ //#region index.ts
6
+ var openclaw_omadeus_plugin_default = defineChannelPluginEntry({
7
+ id: "omadeus",
8
+ name: "Omadeus",
9
+ description: "Omadeus project management channel plugin",
10
+ plugin: omadeusPlugin,
11
+ setRuntime: setOmadeusRuntime
12
+ });
13
+ //#endregion
14
+ export { openclaw_omadeus_plugin_default as default, omadeusPlugin, setOmadeusRuntime };
@@ -0,0 +1,15 @@
1
+ import "./_virtual/_rolldown/runtime.js";
2
+ import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/core";
3
+ import { createReplyPrefixContext } from "openclaw/plugin-sdk/channel-runtime";
4
+ import { logInboundDrop } from "openclaw/plugin-sdk/channel-inbound";
5
+ import { resolveControlCommandGate } from "openclaw/plugin-sdk/command-auth";
6
+ import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
7
+ import { readStoreAllowFromForDmPolicy, resolveDmGroupAccessWithLists } from "openclaw/plugin-sdk/channel-policy";
8
+ import { addWildcardAllowFrom, formatDocsLink, mergeAllowFromEntries } from "openclaw/plugin-sdk/setup";
9
+ //#region runtime-api.ts
10
+ function missingTargetError(provider, hint) {
11
+ const normalizedHint = hint?.trim();
12
+ return /* @__PURE__ */ new Error(`Delivering to ${provider} requires target${normalizedHint ? ` ${normalizedHint}` : ""}`);
13
+ }
14
+ //#endregion
15
+ export { DEFAULT_ACCOUNT_ID, addWildcardAllowFrom, createChannelPairingController, createReplyPrefixContext, formatDocsLink, logInboundDrop, mergeAllowFromEntries, missingTargetError, readStoreAllowFromForDmPolicy, resolveControlCommandGate, resolveDmGroupAccessWithLists };
@@ -0,0 +1,7 @@
1
+ import "./_virtual/_rolldown/runtime.js";
2
+ import { omadeusPlugin } from "./src/channel.js";
3
+ import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core";
4
+ //#region setup-entry.ts
5
+ var setup_entry_default = defineSetupPluginEntry(omadeusPlugin);
6
+ //#endregion
7
+ export { setup_entry_default as default };
@@ -0,0 +1,21 @@
1
+ //#region src/allowed-reaction-emojis.ts
2
+ /**
3
+ * Omadeus only accepts these exact reaction strings; any other value is ignored (no API call).
4
+ */
5
+ const ALLOWED_OMADEUS_REACTION_EMOJI_LIST = [
6
+ "👍",
7
+ "👎",
8
+ "❤️",
9
+ "😂",
10
+ "😮",
11
+ "😢",
12
+ "🙏"
13
+ ];
14
+ const ALLOWED = new Set(ALLOWED_OMADEUS_REACTION_EMOJI_LIST);
15
+ function isAllowedOmadeusReactionEmoji(raw) {
16
+ const trimmed = raw.trim();
17
+ if (!trimmed) return false;
18
+ return ALLOWED.has(trimmed);
19
+ }
20
+ //#endregion
21
+ export { ALLOWED_OMADEUS_REACTION_EMOJI_LIST, isAllowedOmadeusReactionEmoji };
@@ -0,0 +1,115 @@
1
+ import { getCasSession, setCasSession } from "../store.js";
2
+ //#region src/api/auth.api.ts
3
+ const CAS_APPLICATION_ID = 1;
4
+ const CAS_SCOPES = "title,email,avatar,firstName,lastName,birth,phone,countryCode";
5
+ async function createCasToken(params) {
6
+ const { casUrl, email, password } = params;
7
+ const url = `${casUrl}/apiv1/tokens`;
8
+ const jsonBody = JSON.stringify({
9
+ email,
10
+ password
11
+ });
12
+ const res = await fetch(url, {
13
+ method: "CREATE",
14
+ headers: {
15
+ "Content-Type": "application/json;charset=UTF-8",
16
+ "Content-Length": String(jsonBody.length)
17
+ },
18
+ body: jsonBody
19
+ });
20
+ if (!res.ok) {
21
+ const text = await res.text().catch(() => "");
22
+ throw new Error(`CAS token request failed (${res.status}): ${text}`);
23
+ }
24
+ const body = await res.json();
25
+ const refreshCookie = res.headers.get("set-cookie") ?? "";
26
+ setCasSession({
27
+ token: body.token,
28
+ refreshCookie
29
+ });
30
+ return {
31
+ token: body.token,
32
+ refreshCookie
33
+ };
34
+ }
35
+ async function createAuthorizationCode(params) {
36
+ const { casUrl, token, email, redirectUri } = params;
37
+ const casSession = getCasSession();
38
+ const qs = new URLSearchParams({
39
+ applicationId: String(CAS_APPLICATION_ID),
40
+ scopes: CAS_SCOPES,
41
+ state: email,
42
+ redirectUri: redirectUri ?? ""
43
+ });
44
+ if (redirectUri) qs.set("redirectUri", redirectUri);
45
+ const url = `${casUrl}/apiv1/authorizationcodes?${qs}`;
46
+ const body = "";
47
+ const headers = {
48
+ Authorization: `Bearer ${token}`,
49
+ ...casSession?.refreshCookie ? { Cookie: casSession.refreshCookie } : {}
50
+ };
51
+ const res = await fetch(url, {
52
+ method: "CREATE",
53
+ body,
54
+ headers
55
+ });
56
+ if (!res.ok) {
57
+ const text = await res.text().catch(() => "");
58
+ throw new Error(`CAS authorization code request failed (${res.status}): ${text}`);
59
+ }
60
+ const jsonResponse = await res.json();
61
+ const code = jsonResponse.authorizationCode ?? jsonResponse.code;
62
+ if (!code) throw new Error("CAS authorization code response missing code");
63
+ return code;
64
+ }
65
+ async function obtainSessionToken(params) {
66
+ const { maestroUrl, authorizationCode, organizationId } = params;
67
+ const url = `${maestroUrl}/dolphin/apiv1/oauth2/tokens`;
68
+ const res = await fetch(url, {
69
+ method: "OBTAIN",
70
+ headers: { "Content-Type": "application/json;charset=UTF-8" },
71
+ body: JSON.stringify({
72
+ authorizationCode,
73
+ organizationId
74
+ })
75
+ });
76
+ if (!res.ok) {
77
+ const text = await res.text().catch(() => "");
78
+ throw new Error(`Omadeus session token request failed (${res.status}): ${text}`);
79
+ }
80
+ const body = await res.json();
81
+ if (!body.token) throw new Error("Omadeus session token response missing token");
82
+ return body.token;
83
+ }
84
+ async function listOrganizations(params) {
85
+ const { maestroUrl, email } = params;
86
+ const url = `${maestroUrl}/dolphin/apiv1/organizations`;
87
+ const res = await fetch(url, {
88
+ method: "LIST",
89
+ headers: { "Content-Type": "application/json;charset=UTF-8" },
90
+ body: JSON.stringify({ email })
91
+ });
92
+ if (!res.ok) {
93
+ const text = await res.text().catch(() => "");
94
+ throw new Error(`Omadeus list organizations failed (${res.status}): ${text}`);
95
+ }
96
+ return await res.json();
97
+ }
98
+ async function listOrganizationMembers(params) {
99
+ const { maestroUrl, sessionToken, organizationId } = params;
100
+ const url = `${maestroUrl}/dolphin/apiv1/organizations/${organizationId}/members`;
101
+ const res = await fetch(url, {
102
+ method: "LIST",
103
+ headers: {
104
+ Authorization: `Bearer ${sessionToken}`,
105
+ "Content-Type": "application/json;charset=UTF-8"
106
+ }
107
+ });
108
+ if (!res.ok) {
109
+ const text = await res.text().catch(() => "");
110
+ throw new Error(`Omadeus list organization members failed (${res.status}): ${text}`);
111
+ }
112
+ return await res.json();
113
+ }
114
+ //#endregion
115
+ export { createAuthorizationCode, createCasToken, listOrganizationMembers, listOrganizations, obtainSessionToken };
@@ -0,0 +1,23 @@
1
+ //#region src/api/channel.api.ts
2
+ async function listMemberChannelViews(params) {
3
+ const { maestroUrl, sessionToken, memberReferenceId, skip = 0, take = 100 } = params;
4
+ const url = `${maestroUrl}/jaguar/apiv1/members/${memberReferenceId}/channelviews?${new URLSearchParams({
5
+ skip: String(skip),
6
+ take: String(take),
7
+ sort: "-recentMessageAt"
8
+ }).toString()}`;
9
+ const res = await fetch(url, {
10
+ method: "LIST",
11
+ headers: {
12
+ Authorization: `Bearer ${sessionToken}`,
13
+ "Content-Type": "application/json;charset=UTF-8"
14
+ }
15
+ });
16
+ if (!res.ok) {
17
+ const text = await res.text().catch(() => "");
18
+ throw new Error(`Omadeus list channel views failed (${res.status}): ${text}`);
19
+ }
20
+ return await res.json();
21
+ }
22
+ //#endregion
23
+ export { listMemberChannelViews };
@@ -0,0 +1,76 @@
1
+ import { generateTemporaryId, jaguarFetch } from "../utils/http.util.js";
2
+ //#region src/api/message.api.ts
3
+ async function readJsonOrEmpty(res) {
4
+ if (res.status === 204) return;
5
+ const trimmed = (await res.text()).trim();
6
+ if (!trimmed) return;
7
+ try {
8
+ return JSON.parse(trimmed);
9
+ } catch {
10
+ return trimmed;
11
+ }
12
+ }
13
+ async function sendRoomMessage(opts, params) {
14
+ try {
15
+ const res = await jaguarFetch(opts, `/rooms/${params.roomId}/messages`, {
16
+ method: "SEND",
17
+ body: JSON.stringify({
18
+ body: params.body,
19
+ temporaryId: generateTemporaryId(),
20
+ links: "[]"
21
+ })
22
+ });
23
+ if (!res.ok) {
24
+ const text = await res.text().catch(() => "");
25
+ return {
26
+ ok: false,
27
+ error: `${res.status}: ${text.slice(0, 200)}`
28
+ };
29
+ }
30
+ return {
31
+ ok: true,
32
+ message: await res.json()
33
+ };
34
+ } catch (err) {
35
+ return {
36
+ ok: false,
37
+ error: (err instanceof Error ? err.message : String(err)).slice(0, 300)
38
+ };
39
+ }
40
+ }
41
+ async function editMessage(opts, params) {
42
+ const res = await jaguarFetch(opts, `/messages/${params.messageId}`, {
43
+ method: "EDIT",
44
+ body: JSON.stringify({
45
+ body: params.body,
46
+ ...params.temporaryId ? { temporaryId: params.temporaryId } : {},
47
+ ...params.links ? { links: JSON.stringify(params.links) } : {}
48
+ })
49
+ });
50
+ if (!res.ok) {
51
+ const text = await res.text().catch(() => "");
52
+ throw new Error(`Omadeus edit message failed (${res.status}): ${text.slice(0, 200)}`);
53
+ }
54
+ return await res.json();
55
+ }
56
+ async function deleteMessage(opts, params) {
57
+ const res = await jaguarFetch(opts, `/messages/${params.messageId}`, { method: "DELETE" });
58
+ if (!res.ok) {
59
+ const text = await res.text().catch(() => "");
60
+ throw new Error(`Omadeus delete message failed (${res.status}): ${text.slice(0, 200)}`);
61
+ }
62
+ return await res.json();
63
+ }
64
+ async function addMessageReaction(opts, params) {
65
+ const res = await jaguarFetch(opts, `/messages/${params.messageId}/reactions`, {
66
+ method: "ADD",
67
+ body: JSON.stringify({ emoji: params.emoji })
68
+ });
69
+ if (!res.ok) {
70
+ const text = await res.text().catch(() => "");
71
+ throw new Error(`Omadeus add reaction failed (${res.status}): ${text.slice(0, 200)}`);
72
+ }
73
+ return readJsonOrEmpty(res);
74
+ }
75
+ //#endregion
76
+ export { addMessageReaction, deleteMessage, editMessage, sendRoomMessage };
@@ -0,0 +1,127 @@
1
+ import { dolphinFetch } from "../utils/http.util.js";
2
+ //#region src/api/nugget.api.ts
3
+ /** Omadeus nugget/task display number (`N###` in UI maps to this field). */
4
+ function readNuggetNumber(record) {
5
+ const value = record["number"];
6
+ if (typeof value === "number" && Number.isFinite(value)) return value;
7
+ if (typeof value === "string" && /^\d+$/.test(value.trim())) return Number(value.trim());
8
+ }
9
+ function readNumberField(record, key) {
10
+ const value = record[key];
11
+ if (typeof value === "number" && Number.isFinite(value)) return value;
12
+ if (typeof value === "string" && /^\d+$/.test(value.trim())) return Number(value.trim());
13
+ }
14
+ function findNuggetRowByNumber(rows, nuggetNumber) {
15
+ return rows.find((row) => readNuggetNumber(row) === nuggetNumber);
16
+ }
17
+ function resolveTaskChannelRoomId(record) {
18
+ return readNumberField(record, "privateRoomId") ?? readNumberField(record, "publicRoomId") ?? readNumberField(record, "sharedRoomId");
19
+ }
20
+ function extractRows(payload) {
21
+ if (Array.isArray(payload)) return payload.filter((entry) => !!entry && typeof entry === "object");
22
+ if (!payload || typeof payload !== "object") return [];
23
+ const envelope = payload;
24
+ for (const key of [
25
+ "data",
26
+ "results",
27
+ "items",
28
+ "rows"
29
+ ]) {
30
+ const value = envelope[key];
31
+ if (Array.isArray(value)) return value.filter((entry) => !!entry && typeof entry === "object");
32
+ }
33
+ return [];
34
+ }
35
+ /**
36
+ * Dolphin SEARCH on nuggetviews — arbitrary text query (e.g. N###, task title, or room id string).
37
+ * Prefer filtering results with `findNuggetRowByNumber` or `findNuggetRowByRoomId`.
38
+ */
39
+ async function searchNuggetRowsByTextQuery(opts, params) {
40
+ const take = params.take ?? 100;
41
+ const q = params.query.trim();
42
+ if (!q) return [];
43
+ const search = new URLSearchParams();
44
+ search.set("take", String(take));
45
+ const res = await dolphinFetch(opts, `/nuggetviews?${search.toString()}`, {
46
+ method: "SEARCH",
47
+ body: JSON.stringify({ query: q }),
48
+ signal: params.signal
49
+ });
50
+ if (!res.ok) {
51
+ const text = await res.text().catch(() => "");
52
+ throw new Error(`Omadeus nugget search failed (${res.status}): ${text.slice(0, 200)}`);
53
+ }
54
+ return extractRows(await res.json());
55
+ }
56
+ /**
57
+ * Picks a row whose private/public/shared task room id matches a Jaguar `roomId`.
58
+ */
59
+ function findNuggetRowByRoomId(rows, roomId) {
60
+ for (const row of rows) for (const key of [
61
+ "privateRoomId",
62
+ "publicRoomId",
63
+ "sharedRoomId"
64
+ ]) if (readNumberField(row, key) === roomId) return row;
65
+ }
66
+ /**
67
+ * Resolve the nugget/task row for a Jaguar Task or Nugget **chat room** by matching
68
+ * `privateRoomId` / `publicRoomId` / `sharedRoomId` to `roomId` in Dolphin `nuggetviews` search results.
69
+ * Tries search by `roomName` first (usually matches the task title), then by the numeric `roomId` as text.
70
+ */
71
+ async function findNuggetByTaskChannelRoom(opts, params) {
72
+ const { roomId, roomName, signal } = params;
73
+ const tryQueries = [];
74
+ if (typeof roomName === "string" && roomName.trim()) tryQueries.push(roomName.trim());
75
+ tryQueries.push(String(roomId));
76
+ const tried = /* @__PURE__ */ new Set();
77
+ for (const query of tryQueries) {
78
+ if (tried.has(query)) continue;
79
+ tried.add(query);
80
+ const match = findNuggetRowByRoomId(await searchNuggetRowsByTextQuery(opts, {
81
+ query,
82
+ take: 100,
83
+ signal
84
+ }), roomId);
85
+ if (match) return match;
86
+ }
87
+ return null;
88
+ }
89
+ /**
90
+ * Dolphin SEARCH on nuggetviews returns an array of nugget/task rows.
91
+ * User-facing `N111` corresponds to `number: 111` on each row (not `id`).
92
+ */
93
+ async function searchNuggetByNumber(opts, params) {
94
+ return findNuggetRowByNumber(await searchNuggetRowsByTextQuery(opts, {
95
+ query: `N${params.nuggetNumber}`,
96
+ take: 100,
97
+ signal: params.signal
98
+ }), params.nuggetNumber) ?? null;
99
+ }
100
+ async function resolveTaskRoomIdByNumber(opts, params) {
101
+ const row = await searchNuggetByNumber(opts, params);
102
+ if (!row) return null;
103
+ return resolveTaskChannelRoomId(row) ?? null;
104
+ }
105
+ async function createNugget(opts, params) {
106
+ const res = await dolphinFetch(opts, "/nuggets", {
107
+ method: "CREATE",
108
+ body: JSON.stringify({
109
+ title: params.title,
110
+ stage: params.stage,
111
+ description: params.description,
112
+ kind: params.kind,
113
+ priority: params.priority,
114
+ memberReferenceId: params.memberReferenceId,
115
+ clientId: params.clientId,
116
+ folderId: params.folderId
117
+ }),
118
+ signal: params.signal
119
+ });
120
+ if (!res.ok) {
121
+ const text = await res.text().catch(() => "");
122
+ throw new Error(`Omadeus nugget create failed (${res.status}): ${text.slice(0, 200)}`);
123
+ }
124
+ return await res.json();
125
+ }
126
+ //#endregion
127
+ export { createNugget, findNuggetByTaskChannelRoom, readNuggetNumber, resolveTaskChannelRoomId, resolveTaskRoomIdByNumber, searchNuggetByNumber };
@@ -0,0 +1,30 @@
1
+ import { clearCasSession } from "./store.js";
2
+ import { createAuthorizationCode, createCasToken, obtainSessionToken } from "./api/auth.api.js";
3
+ import { decodeJwtPayload } from "./utils/jwt.util.js";
4
+ //#region src/auth.ts
5
+ async function authenticate(params) {
6
+ const { casUrl, maestroUrl, email, password, organizationId } = params;
7
+ const { token } = await createCasToken({
8
+ casUrl,
9
+ email,
10
+ password
11
+ });
12
+ const authorizationCode = await createAuthorizationCode({
13
+ casUrl,
14
+ token,
15
+ email,
16
+ redirectUri: maestroUrl
17
+ });
18
+ clearCasSession();
19
+ const dolphinToken = await obtainSessionToken({
20
+ maestroUrl,
21
+ authorizationCode,
22
+ organizationId
23
+ });
24
+ return {
25
+ dolphinToken,
26
+ payload: decodeJwtPayload(dolphinToken)
27
+ };
28
+ }
29
+ //#endregion
30
+ export { authenticate };