@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.
- package/README.md +4 -1
- package/dist/_virtual/_rolldown/runtime.js +4 -0
- package/dist/api.js +5 -0
- package/dist/index.js +14 -0
- package/dist/runtime-api.js +15 -0
- package/dist/setup-entry.js +7 -0
- package/dist/src/allowed-reaction-emojis.js +21 -0
- package/dist/src/api/auth.api.js +115 -0
- package/dist/src/api/channel.api.js +23 -0
- package/dist/src/api/message.api.js +76 -0
- package/dist/src/api/nugget.api.js +127 -0
- package/dist/src/auth.js +30 -0
- package/dist/src/channel.js +626 -0
- package/dist/src/config.js +52 -0
- package/dist/src/defaults.js +5 -0
- package/dist/src/inbound-policy.js +205 -0
- package/dist/src/inbound.js +97 -0
- package/dist/src/member-resolve.js +53 -0
- package/dist/src/message-handler.js +262 -0
- package/dist/src/nugget-lookup.js +140 -0
- package/dist/src/onboarding.js +363 -0
- package/dist/src/outbound.js +17 -0
- package/dist/src/reply-dispatcher.js +46 -0
- package/dist/src/runtime.js +5 -0
- package/dist/src/setup-core.js +46 -0
- package/dist/src/setup-surface.js +2 -0
- package/dist/src/socket/dolphin.socket.js +18 -0
- package/dist/src/socket/jaguar.socket.js +22 -0
- package/dist/src/socket/socket.js +153 -0
- package/dist/src/store.js +13 -0
- package/dist/src/token.js +84 -0
- package/dist/src/types.js +15 -0
- package/dist/src/utils/http.util.js +43 -0
- package/dist/src/utils/jwt.util.js +15 -0
- package/package.json +10 -3
- package/src/channel.ts +127 -238
- package/src/member-resolve.ts +1 -1
- package/src/onboarding.ts +71 -110
- package/src/setup-core.ts +10 -1
- package/src/socket/socket.ts +24 -11
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { WebSocket } from "ws";
|
|
2
|
+
//#region src/socket/socket.ts
|
|
3
|
+
const RECONNECT_BASE_MS = 2e3;
|
|
4
|
+
const RECONNECT_MAX_MS = 6e4;
|
|
5
|
+
const HEARTBEAT_INTERVAL_MS = 3e4;
|
|
6
|
+
const HEARTBEAT_MISSED_MAX = 5;
|
|
7
|
+
const KEEP_ALIVE_CONTENT = "keep-alive";
|
|
8
|
+
const KEEP_ALIVE_ACTION = "answer";
|
|
9
|
+
function isServerKeepAlive(data) {
|
|
10
|
+
return data.content === KEEP_ALIVE_CONTENT;
|
|
11
|
+
}
|
|
12
|
+
function isClientKeepAlive(data) {
|
|
13
|
+
return data.data === KEEP_ALIVE_CONTENT;
|
|
14
|
+
}
|
|
15
|
+
function createOmadeusSocketClient(opts) {
|
|
16
|
+
const { maestroUrl, tokenManager, pathSuffix, logPrefix, onEvent, onConnect, onDisconnect, onError, log } = opts;
|
|
17
|
+
let ws = null;
|
|
18
|
+
let reconnectAttempt = 0;
|
|
19
|
+
let reconnectTimer = null;
|
|
20
|
+
let heartbeatTimer = null;
|
|
21
|
+
let heartbeatMissCount = 0;
|
|
22
|
+
let intentionalClose = false;
|
|
23
|
+
function buildWsUrl() {
|
|
24
|
+
const base = maestroUrl.replace(/^http/, "ws");
|
|
25
|
+
const token = tokenManager.getToken();
|
|
26
|
+
return `${base}/${pathSuffix}?token=${encodeURIComponent(token)}`;
|
|
27
|
+
}
|
|
28
|
+
function scheduleReconnect() {
|
|
29
|
+
if (intentionalClose) return;
|
|
30
|
+
const delayMs = Math.min(RECONNECT_BASE_MS * 2 ** reconnectAttempt, RECONNECT_MAX_MS);
|
|
31
|
+
reconnectAttempt++;
|
|
32
|
+
log?.info(`${logPrefix} reconnecting in ${delayMs}ms (attempt ${reconnectAttempt})`);
|
|
33
|
+
reconnectTimer = setTimeout(() => connect(), delayMs);
|
|
34
|
+
}
|
|
35
|
+
function stopHeartbeat() {
|
|
36
|
+
if (heartbeatTimer) {
|
|
37
|
+
clearInterval(heartbeatTimer);
|
|
38
|
+
heartbeatTimer = null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function resetHeartbeat() {
|
|
42
|
+
heartbeatMissCount = 0;
|
|
43
|
+
}
|
|
44
|
+
function sendKeepAlive() {
|
|
45
|
+
if (ws?.readyState !== WebSocket.OPEN) return;
|
|
46
|
+
heartbeatMissCount += 1;
|
|
47
|
+
sendKeepAliveFrame();
|
|
48
|
+
if (heartbeatMissCount >= HEARTBEAT_MISSED_MAX) {
|
|
49
|
+
log?.warn(`${logPrefix} heartbeat unanswered ${heartbeatMissCount} times; reconnecting socket`);
|
|
50
|
+
ws.close();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function startHeartbeat() {
|
|
54
|
+
stopHeartbeat();
|
|
55
|
+
heartbeatTimer = setInterval(() => {
|
|
56
|
+
sendKeepAlive();
|
|
57
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
58
|
+
}
|
|
59
|
+
function sendKeepAliveFrame() {
|
|
60
|
+
if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify({
|
|
61
|
+
data: KEEP_ALIVE_CONTENT,
|
|
62
|
+
action: KEEP_ALIVE_ACTION
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
function connect() {
|
|
66
|
+
if (ws) {
|
|
67
|
+
ws.removeAllListeners();
|
|
68
|
+
ws.close();
|
|
69
|
+
ws = null;
|
|
70
|
+
}
|
|
71
|
+
intentionalClose = false;
|
|
72
|
+
stopHeartbeat();
|
|
73
|
+
resetHeartbeat();
|
|
74
|
+
if (tokenManager.needsRefresh()) {
|
|
75
|
+
tokenManager.refresh().then(() => connect()).catch((err) => {
|
|
76
|
+
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
77
|
+
scheduleReconnect();
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const url = buildWsUrl();
|
|
82
|
+
log?.info(`${logPrefix} connecting...`);
|
|
83
|
+
ws = new WebSocket(url);
|
|
84
|
+
ws.on("open", () => {
|
|
85
|
+
reconnectAttempt = 0;
|
|
86
|
+
log?.info(`${logPrefix} connected`);
|
|
87
|
+
onConnect?.();
|
|
88
|
+
resetHeartbeat();
|
|
89
|
+
sendKeepAlive();
|
|
90
|
+
startHeartbeat();
|
|
91
|
+
});
|
|
92
|
+
ws.on("message", (raw) => {
|
|
93
|
+
try {
|
|
94
|
+
const data = JSON.parse(String(raw));
|
|
95
|
+
const action = data.action;
|
|
96
|
+
if (isServerKeepAlive(data) && action === KEEP_ALIVE_ACTION) {
|
|
97
|
+
resetHeartbeat();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (isClientKeepAlive(data) && action === KEEP_ALIVE_ACTION) {
|
|
101
|
+
resetHeartbeat();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (isServerKeepAlive(data) && action === "heartbeat") {
|
|
105
|
+
resetHeartbeat();
|
|
106
|
+
sendKeepAliveFrame();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
resetHeartbeat();
|
|
110
|
+
onEvent?.(data);
|
|
111
|
+
} catch {
|
|
112
|
+
log?.warn(`${logPrefix} unparseable message: ${String(raw).slice(0, 200)}`);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
ws.on("close", (code, reason) => {
|
|
116
|
+
const msg = `code=${code} reason=${String(reason)}`;
|
|
117
|
+
log?.info(`${logPrefix} disconnected: ${msg}`);
|
|
118
|
+
onDisconnect?.(msg);
|
|
119
|
+
ws = null;
|
|
120
|
+
stopHeartbeat();
|
|
121
|
+
resetHeartbeat();
|
|
122
|
+
scheduleReconnect();
|
|
123
|
+
});
|
|
124
|
+
ws.on("error", (err) => {
|
|
125
|
+
log?.error(`${logPrefix} error: ${err.message}`);
|
|
126
|
+
onError?.(err);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function disconnect() {
|
|
130
|
+
intentionalClose = true;
|
|
131
|
+
if (reconnectTimer) {
|
|
132
|
+
clearTimeout(reconnectTimer);
|
|
133
|
+
reconnectTimer = null;
|
|
134
|
+
}
|
|
135
|
+
stopHeartbeat();
|
|
136
|
+
resetHeartbeat();
|
|
137
|
+
if (ws) {
|
|
138
|
+
ws.removeAllListeners();
|
|
139
|
+
ws.close();
|
|
140
|
+
ws = null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
connect,
|
|
145
|
+
disconnect,
|
|
146
|
+
isConnected: () => ws?.readyState === WebSocket.OPEN,
|
|
147
|
+
send: (data) => {
|
|
148
|
+
if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify(data));
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
//#endregion
|
|
153
|
+
export { createOmadeusSocketClient };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/store.ts
|
|
2
|
+
let currentSession = null;
|
|
3
|
+
function setCasSession(session) {
|
|
4
|
+
currentSession = session;
|
|
5
|
+
}
|
|
6
|
+
function getCasSession() {
|
|
7
|
+
return currentSession;
|
|
8
|
+
}
|
|
9
|
+
function clearCasSession() {
|
|
10
|
+
currentSession = null;
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { clearCasSession, getCasSession, setCasSession };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { decodeJwtPayload, tokenExpiresInMs } from "./utils/jwt.util.js";
|
|
2
|
+
import { authenticate } from "./auth.js";
|
|
3
|
+
//#region src/token.ts
|
|
4
|
+
const TOKEN_REFRESH_MARGIN_MS = 300 * 1e3;
|
|
5
|
+
const MAX_TIMEOUT_MS = 2147483647;
|
|
6
|
+
/** Whether the token should be refreshed now (within safety margin). */
|
|
7
|
+
function shouldRefreshToken(token) {
|
|
8
|
+
return tokenExpiresInMs(token) < TOKEN_REFRESH_MARGIN_MS;
|
|
9
|
+
}
|
|
10
|
+
function createTokenManager(params) {
|
|
11
|
+
const { casUrl, maestroUrl, email, password, organizationId, initialToken, onRefresh, onError } = params;
|
|
12
|
+
let currentToken = "";
|
|
13
|
+
let currentPayload = null;
|
|
14
|
+
if (initialToken) try {
|
|
15
|
+
const payload = decodeJwtPayload(initialToken);
|
|
16
|
+
currentToken = initialToken;
|
|
17
|
+
currentPayload = payload;
|
|
18
|
+
} catch (err) {
|
|
19
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
20
|
+
onError?.(error);
|
|
21
|
+
}
|
|
22
|
+
let refreshTimer = null;
|
|
23
|
+
const refresh = async () => {
|
|
24
|
+
if (currentToken && !shouldRefreshToken(currentToken)) return;
|
|
25
|
+
const { dolphinToken, payload } = await authenticate({
|
|
26
|
+
casUrl,
|
|
27
|
+
maestroUrl,
|
|
28
|
+
email,
|
|
29
|
+
password,
|
|
30
|
+
organizationId
|
|
31
|
+
});
|
|
32
|
+
currentToken = dolphinToken;
|
|
33
|
+
currentPayload = payload;
|
|
34
|
+
onRefresh?.(dolphinToken);
|
|
35
|
+
};
|
|
36
|
+
const scheduleNextRefresh = () => {
|
|
37
|
+
if (refreshTimer) {
|
|
38
|
+
clearTimeout(refreshTimer);
|
|
39
|
+
refreshTimer = null;
|
|
40
|
+
}
|
|
41
|
+
if (!currentToken) return;
|
|
42
|
+
const desiredDelayMs = tokenExpiresInMs(currentToken) - TOKEN_REFRESH_MARGIN_MS;
|
|
43
|
+
refreshTimer = setTimeout(async () => {
|
|
44
|
+
try {
|
|
45
|
+
await refresh();
|
|
46
|
+
scheduleNextRefresh();
|
|
47
|
+
} catch (err) {
|
|
48
|
+
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
49
|
+
refreshTimer = setTimeout(() => void scheduleNextRefresh(), 3e4);
|
|
50
|
+
}
|
|
51
|
+
}, Math.min(Math.max(desiredDelayMs, 1e4), MAX_TIMEOUT_MS));
|
|
52
|
+
};
|
|
53
|
+
return {
|
|
54
|
+
getToken() {
|
|
55
|
+
return currentToken;
|
|
56
|
+
},
|
|
57
|
+
getPayload() {
|
|
58
|
+
if (!currentPayload) throw new Error("Omadeus: not authenticated");
|
|
59
|
+
return currentPayload;
|
|
60
|
+
},
|
|
61
|
+
async refresh() {
|
|
62
|
+
try {
|
|
63
|
+
await refresh();
|
|
64
|
+
} catch (err) {
|
|
65
|
+
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
startAutoRefresh() {
|
|
70
|
+
scheduleNextRefresh();
|
|
71
|
+
},
|
|
72
|
+
stopAutoRefresh() {
|
|
73
|
+
if (refreshTimer) {
|
|
74
|
+
clearTimeout(refreshTimer);
|
|
75
|
+
refreshTimer = null;
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
needsRefresh() {
|
|
79
|
+
return !currentToken || shouldRefreshToken(currentToken);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
84
|
+
export { createTokenManager };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/types.ts
|
|
2
|
+
/** Jaguar entity chats (`subscribableKind`) — DMs and channel rooms use other values. */
|
|
3
|
+
const OMADEUS_INBOUND_ENTITY_KINDS = [
|
|
4
|
+
"task",
|
|
5
|
+
"nugget",
|
|
6
|
+
"project",
|
|
7
|
+
"release",
|
|
8
|
+
"sprint",
|
|
9
|
+
"summary",
|
|
10
|
+
"client",
|
|
11
|
+
"folder"
|
|
12
|
+
];
|
|
13
|
+
const OMADEUS_INBOUND_ENTITY_KIND_SET = new Set(OMADEUS_INBOUND_ENTITY_KINDS);
|
|
14
|
+
//#endregion
|
|
15
|
+
export { OMADEUS_INBOUND_ENTITY_KINDS, OMADEUS_INBOUND_ENTITY_KIND_SET };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
//#region src/utils/http.util.ts
|
|
3
|
+
function authHeaders(token) {
|
|
4
|
+
return {
|
|
5
|
+
Authorization: `Bearer ${token}`,
|
|
6
|
+
"Content-Type": "application/json"
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
async function apiFetch(opts, path, init) {
|
|
10
|
+
const token = opts.tokenManager.getToken();
|
|
11
|
+
if (!token) throw new Error("Omadeus: not authenticated");
|
|
12
|
+
const url = `${opts.maestroUrl}${path}`;
|
|
13
|
+
try {
|
|
14
|
+
return await fetch(url, {
|
|
15
|
+
...init,
|
|
16
|
+
headers: {
|
|
17
|
+
...authHeaders(token),
|
|
18
|
+
...init?.headers
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
} catch (err) {
|
|
22
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
23
|
+
throw new Error(`Omadeus API request to ${url} failed: ${message}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function withApiPrefix(prefix, path) {
|
|
27
|
+
if (!path) return prefix;
|
|
28
|
+
if (path.startsWith("/")) return `${prefix}${path}`;
|
|
29
|
+
return `${prefix}/${path}`;
|
|
30
|
+
}
|
|
31
|
+
const JAGUAR_PREFIX = "/jaguar/apiv1";
|
|
32
|
+
const DOLPHIN_PREFIX = "/dolphin/apiv1";
|
|
33
|
+
async function jaguarFetch(opts, path, init) {
|
|
34
|
+
return apiFetch(opts, withApiPrefix(JAGUAR_PREFIX, path), init);
|
|
35
|
+
}
|
|
36
|
+
async function dolphinFetch(opts, path, init) {
|
|
37
|
+
return apiFetch(opts, withApiPrefix(DOLPHIN_PREFIX, path), init);
|
|
38
|
+
}
|
|
39
|
+
function generateTemporaryId() {
|
|
40
|
+
return `_${randomUUID().replace(/-/g, "").slice(0, 10)}`;
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { dolphinFetch, generateTemporaryId, jaguarFetch };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/utils/jwt.util.ts
|
|
2
|
+
/** Decode the payload portion of a JWT without verifying the signature. */
|
|
3
|
+
function decodeJwtPayload(token) {
|
|
4
|
+
const parts = token.split(".");
|
|
5
|
+
if (parts.length !== 3) throw new Error("Invalid JWT: expected 3 parts");
|
|
6
|
+
const payload = Buffer.from(parts[1], "base64url").toString("utf-8");
|
|
7
|
+
return JSON.parse(payload);
|
|
8
|
+
}
|
|
9
|
+
/** Returns ms until the token expires (negative = already expired). */
|
|
10
|
+
function tokenExpiresInMs(token) {
|
|
11
|
+
const { exp } = decodeJwtPayload(token);
|
|
12
|
+
return exp * 1e3 - Date.now();
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
export { decodeJwtPayload, tokenExpiresInMs };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brantrusnak/openclaw-omadeus",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "OpenClaw Omadeus project management channel plugin",
|
|
6
6
|
"homepage": "https://github.com/brantrusnak/openclaw-omadeus-plugin#readme",
|
|
@@ -14,15 +14,17 @@
|
|
|
14
14
|
"license": "ISC",
|
|
15
15
|
"author": "Brant Rusnak",
|
|
16
16
|
"type": "module",
|
|
17
|
-
"main": "./index.
|
|
17
|
+
"main": "./dist/index.js",
|
|
18
18
|
"scripts": {
|
|
19
|
+
"build": "rolldown -c rolldown.config.mjs",
|
|
19
20
|
"test": "vitest run",
|
|
20
|
-
"prepack": "node ./scripts/verify-npm-files.mjs"
|
|
21
|
+
"prepack": "npm run build && node ./scripts/verify-npm-files.mjs"
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|
|
23
24
|
"ws": "^8.20.0"
|
|
24
25
|
},
|
|
25
26
|
"files": [
|
|
27
|
+
"dist",
|
|
26
28
|
"index.ts",
|
|
27
29
|
"setup-entry.ts",
|
|
28
30
|
"api.ts",
|
|
@@ -33,6 +35,7 @@
|
|
|
33
35
|
],
|
|
34
36
|
"devDependencies": {
|
|
35
37
|
"openclaw": ">=2026.4.10",
|
|
38
|
+
"rolldown": "1.0.0-rc.17",
|
|
36
39
|
"vitest": "^4.1.5"
|
|
37
40
|
},
|
|
38
41
|
"peerDependencies": {
|
|
@@ -50,7 +53,11 @@
|
|
|
50
53
|
"extensions": [
|
|
51
54
|
"./index.ts"
|
|
52
55
|
],
|
|
56
|
+
"runtimeExtensions": [
|
|
57
|
+
"./dist/index.js"
|
|
58
|
+
],
|
|
53
59
|
"setupEntry": "./setup-entry.ts",
|
|
60
|
+
"runtimeSetupEntry": "./dist/setup-entry.js",
|
|
54
61
|
"channel": {
|
|
55
62
|
"id": "omadeus",
|
|
56
63
|
"label": "Omadeus",
|