@brantrusnak/openclaw-omadeus 1.0.2 → 1.0.4
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 +15 -61
- 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 +118 -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 +357 -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/api/auth.api.ts +27 -7
- package/src/channel.ts +127 -238
- package/src/member-resolve.ts +1 -1
- package/src/onboarding.ts +117 -163
- package/src/setup-core.ts +10 -1
- package/src/socket/socket.ts +24 -11
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { readNuggetNumber } from "./api/nugget.api.js";
|
|
2
|
+
import { mergePeopleIntoNuggetAgentPayload } from "./member-resolve.js";
|
|
3
|
+
//#region src/nugget-lookup.ts
|
|
4
|
+
function parseNuggetLookupIntent(rawBody) {
|
|
5
|
+
const body = rawBody.trim();
|
|
6
|
+
if (!body) return null;
|
|
7
|
+
const idMatch = /\bN(\d+)\b/i.exec(body);
|
|
8
|
+
if (!idMatch) return null;
|
|
9
|
+
const nuggetNumber = Number(idMatch[1]);
|
|
10
|
+
if (!Number.isFinite(nuggetNumber)) return null;
|
|
11
|
+
if (/^N\d+\??$/i.test(body)) return { nuggetNumber };
|
|
12
|
+
if (/\bnugget\s+N?\d+\b/i.test(body)) return { nuggetNumber };
|
|
13
|
+
if (/\b(get|show|lookup|find|search|status|detail|info)\b/i.test(body)) return { nuggetNumber };
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
function parseTaskChannelTargetIntent(rawInput) {
|
|
17
|
+
const trimmed = rawInput.trim();
|
|
18
|
+
const match = /^([nt])(\d+)$/i.exec(trimmed);
|
|
19
|
+
if (!match) return null;
|
|
20
|
+
const nuggetNumber = Number(match[2]);
|
|
21
|
+
if (!Number.isFinite(nuggetNumber)) return null;
|
|
22
|
+
return {
|
|
23
|
+
nuggetNumber,
|
|
24
|
+
rawPrefix: match[1].toLowerCase()
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function resolvePriorityFromText(text) {
|
|
28
|
+
const lowered = text.toLowerCase();
|
|
29
|
+
if (/\b(urgent|asap|critical|p0)\b/.test(lowered)) return "urgent";
|
|
30
|
+
if (/\b(high|important|p1)\b/.test(lowered)) return "high";
|
|
31
|
+
if (/\b(medium|normal|p2)\b/.test(lowered)) return "medium";
|
|
32
|
+
return "low";
|
|
33
|
+
}
|
|
34
|
+
function parseChannelTaskCreateIntent(rawBody) {
|
|
35
|
+
const body = rawBody.trim();
|
|
36
|
+
if (!body) return null;
|
|
37
|
+
const lower = body.toLowerCase();
|
|
38
|
+
const isCreateVerb = /\b(create|open|add|spawn|start)\b/.test(lower);
|
|
39
|
+
const hasTaskWord = /\b(task|nugget)\b/.test(lower);
|
|
40
|
+
if (!isCreateVerb || !hasTaskWord) return null;
|
|
41
|
+
const kind = /\bnugget\b/.test(lower) ? "nugget" : "task";
|
|
42
|
+
return {
|
|
43
|
+
kind,
|
|
44
|
+
title: body.replace(/^\s*(please\s+)?(create|open|add|spawn|start)\s+(a\s+|an\s+)?(new\s+)?/i, "").replace(/^(task|nugget)\s*/i, "").trim() || `${kind === "task" ? "Task" : "Nugget"} from channel request`,
|
|
45
|
+
description: body,
|
|
46
|
+
priority: resolvePriorityFromText(body)
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function parseRecurringScheduleIntent(rawBody) {
|
|
50
|
+
const lowered = rawBody.toLowerCase();
|
|
51
|
+
const minuteMatch = /\bevery\s+(\d+)\s*(m|min|mins|minute|minutes)\b/.exec(lowered);
|
|
52
|
+
if (minuteMatch) {
|
|
53
|
+
const everyMinutes = Number(minuteMatch[1]);
|
|
54
|
+
if (Number.isFinite(everyMinutes) && everyMinutes > 0) return { everyMinutes: Math.min(60, everyMinutes) };
|
|
55
|
+
}
|
|
56
|
+
if (/\bevery\s+hour\b|\bhourly\b/.test(lowered)) return { everyMinutes: 60 };
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
/** Fields from Dolphin nuggetviews that are useful for an agent summary (avoids huge payloads). */
|
|
60
|
+
const NUGGET_FIELDS_FOR_AGENT = [
|
|
61
|
+
"number",
|
|
62
|
+
"id",
|
|
63
|
+
"title",
|
|
64
|
+
"description",
|
|
65
|
+
"status",
|
|
66
|
+
"stage",
|
|
67
|
+
"leadPhaseTitle",
|
|
68
|
+
"priority",
|
|
69
|
+
"priorityValue",
|
|
70
|
+
"dueDate",
|
|
71
|
+
"kind",
|
|
72
|
+
"entityType",
|
|
73
|
+
"tempo",
|
|
74
|
+
"projectTitle",
|
|
75
|
+
"projectStatus",
|
|
76
|
+
"projectNumber",
|
|
77
|
+
"projectManagerFirstName",
|
|
78
|
+
"projectManagerLastName",
|
|
79
|
+
"projectManagerTitle",
|
|
80
|
+
"projectManagerReferenceId",
|
|
81
|
+
"clientTitle",
|
|
82
|
+
"folderTitle",
|
|
83
|
+
"createdAt",
|
|
84
|
+
"autoModifiedAt",
|
|
85
|
+
"lastMovingTime",
|
|
86
|
+
"responseTimestamp",
|
|
87
|
+
"assignmentLevel",
|
|
88
|
+
"estimated",
|
|
89
|
+
"sprintName",
|
|
90
|
+
"sprintNumber",
|
|
91
|
+
"releaseTitle",
|
|
92
|
+
"releaseNumber",
|
|
93
|
+
"publicRoomId",
|
|
94
|
+
"privateRoomId",
|
|
95
|
+
"memberReferenceId",
|
|
96
|
+
"assigneeReferenceId",
|
|
97
|
+
"ownerReferenceId",
|
|
98
|
+
"memberFirstName",
|
|
99
|
+
"memberLastName",
|
|
100
|
+
"assigneeFirstName",
|
|
101
|
+
"assigneeLastName"
|
|
102
|
+
];
|
|
103
|
+
function pickNuggetFieldsForAgent(record) {
|
|
104
|
+
const out = {};
|
|
105
|
+
for (const key of NUGGET_FIELDS_FOR_AGENT) if (key in record) out[key] = record[key];
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Picked Dolphin fields + `people` map (referenceId → display name) for the organization.
|
|
110
|
+
*/
|
|
111
|
+
async function buildNuggetAgentDataPayload(apiOpts, fullRecord) {
|
|
112
|
+
if (!fullRecord) return null;
|
|
113
|
+
return mergePeopleIntoNuggetAgentPayload(apiOpts, fullRecord, pickNuggetFieldsForAgent(fullRecord));
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Augments the user message so the agent receives Dolphin nugget/task data and can reply with a summary.
|
|
117
|
+
* On miss or API error, the agent still gets instructions to respond helpfully.
|
|
118
|
+
*/
|
|
119
|
+
async function appendNuggetLookupContextForAgent(rawBody, nuggetNumber, record, apiOpts, fetchError) {
|
|
120
|
+
const header = `[Omadeus nugget/task N${nuggetNumber}]`;
|
|
121
|
+
if (fetchError) return `${rawBody}\n\n${header} Lookup failed: ${fetchError}\nBriefly explain the error to the user and suggest they try again or check permissions.`;
|
|
122
|
+
if (!record) return `${rawBody}\n\n${header} No row matched display number ${nuggetNumber} (field \`number\`) in search results.\nTell the user succinctly that this nugget/task was not found.`;
|
|
123
|
+
const payload = await buildNuggetAgentDataPayload(apiOpts, record) ?? pickNuggetFieldsForAgent(record);
|
|
124
|
+
return `${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${JSON.stringify(payload, null, 2)}`;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Enriches a Task or Nugget **Jaguar room** with Dolphin data matched by this chat's `roomId`, so the
|
|
128
|
+
* agent can answer "status" without a bare `N###` in the message.
|
|
129
|
+
*/
|
|
130
|
+
async function appendNuggetContextForTaskOrNuggetRoom(rawBody, roomId, roomName, record, apiOpts, fetchError) {
|
|
131
|
+
const roomLabel = roomName?.trim() ? `room ${roomId} ("${roomName.trim()}")` : `room ${roomId}`;
|
|
132
|
+
if (fetchError) return `${rawBody}\n\n[Omadeus, this task/nugget ${roomLabel}] Lookup failed: ${fetchError}.\nAnswer from this thread. Do not tell the user to "use the Omadeus platform" or similar — give a direct reply or a concrete next step.`;
|
|
133
|
+
if (!record) return `${rawBody}\n\n[Omadeus, this task/nugget ${roomLabel}] No Dolphin row matched this room id in search yet.\nAnswer from the conversation; be honest if you cannot see live fields. Do not hand-wave to "the platform".`;
|
|
134
|
+
const n = readNuggetNumber(record);
|
|
135
|
+
const nLabel = n !== void 0 ? `N${n}` : "nugget";
|
|
136
|
+
const payload = await buildNuggetAgentDataPayload(apiOpts, record) ?? pickNuggetFieldsForAgent(record);
|
|
137
|
+
return `${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${JSON.stringify(payload, null, 2)}`;
|
|
138
|
+
}
|
|
139
|
+
//#endregion
|
|
140
|
+
export { appendNuggetContextForTaskOrNuggetRoom, appendNuggetLookupContextForAgent, parseChannelTaskCreateIntent, parseNuggetLookupIntent, parseRecurringScheduleIntent, parseTaskChannelTargetIntent };
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { OMADEUS_CAS_URL, OMADEUS_MAESTRO_URL } from "./defaults.js";
|
|
2
|
+
import { getOmadeusChannelConfig, resolveOmadeusAccount } from "./config.js";
|
|
3
|
+
import { listOrganizationMembers, listOrganizations } from "./api/auth.api.js";
|
|
4
|
+
import { formatMemberLabel } from "./member-resolve.js";
|
|
5
|
+
import { OMADEUS_INBOUND_ENTITY_KINDS } from "./types.js";
|
|
6
|
+
import { listMemberChannelViews } from "./api/channel.api.js";
|
|
7
|
+
import { authenticate } from "./auth.js";
|
|
8
|
+
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
|
|
9
|
+
//#region src/onboarding.ts
|
|
10
|
+
const channel = "omadeus";
|
|
11
|
+
function formatAuthError(err) {
|
|
12
|
+
if (!(err instanceof Error)) return String(err);
|
|
13
|
+
const parts = [err.message];
|
|
14
|
+
const { cause } = err;
|
|
15
|
+
if (cause instanceof Error) {
|
|
16
|
+
parts.push(cause.message);
|
|
17
|
+
const code = cause.code;
|
|
18
|
+
if (typeof code === "string" && code) parts.push(`(${code})`);
|
|
19
|
+
} else if (typeof cause === "string" && cause.trim()) parts.push(cause);
|
|
20
|
+
return parts.join(" — ");
|
|
21
|
+
}
|
|
22
|
+
async function noteOmadeusAuthHelp(prompter) {
|
|
23
|
+
await prompter.note([
|
|
24
|
+
"Omadeus authenticates via CAS + Maestro (email + password + organization).",
|
|
25
|
+
"You need:",
|
|
26
|
+
" - Email + password",
|
|
27
|
+
" - Organization ID (we can look it up for you)",
|
|
28
|
+
`CAS URL: ${OMADEUS_CAS_URL}`,
|
|
29
|
+
`Maestro URL: ${OMADEUS_MAESTRO_URL}`,
|
|
30
|
+
"Env vars supported: OMADEUS_EMAIL, OMADEUS_PASSWORD, OMADEUS_ORGANIZATION_ID."
|
|
31
|
+
].join("\n"), "Omadeus setup");
|
|
32
|
+
}
|
|
33
|
+
async function promptOrganizationId(params) {
|
|
34
|
+
const { prompter, maestroUrl, email, existing } = params;
|
|
35
|
+
try {
|
|
36
|
+
const orgs = await listOrganizations({
|
|
37
|
+
maestroUrl,
|
|
38
|
+
email
|
|
39
|
+
});
|
|
40
|
+
if (orgs.length > 0) {
|
|
41
|
+
if (orgs.length === 1) {
|
|
42
|
+
await prompter.note(`Found organization: ${orgs[0].title} (${orgs[0].id})`, "Omadeus organization");
|
|
43
|
+
return orgs[0].id;
|
|
44
|
+
}
|
|
45
|
+
const choice = await prompter.select({
|
|
46
|
+
message: "Select organization",
|
|
47
|
+
options: orgs.map((org) => ({
|
|
48
|
+
value: String(org.id),
|
|
49
|
+
label: `${org.title} (${org.membersCount} members)`,
|
|
50
|
+
hint: `ID: ${org.id}`
|
|
51
|
+
})),
|
|
52
|
+
initialValue: existing ? String(existing) : String(orgs[0].id)
|
|
53
|
+
});
|
|
54
|
+
return Number(choice);
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
await prompter.note("Could not fetch organizations from the API. Enter the ID manually.", "Omadeus organization");
|
|
58
|
+
}
|
|
59
|
+
const raw = await prompter.text({
|
|
60
|
+
message: "Organization ID (number)",
|
|
61
|
+
initialValue: existing ? String(existing) : void 0,
|
|
62
|
+
validate: (value) => {
|
|
63
|
+
const trimmed = String(value ?? "").trim();
|
|
64
|
+
if (!trimmed) return "Required";
|
|
65
|
+
if (!/^\d+$/.test(trimmed)) return "Must be a number";
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
return Number(String(raw).trim());
|
|
69
|
+
}
|
|
70
|
+
async function promptChannelSelection(params) {
|
|
71
|
+
const { prompter, maestroUrl, sessionToken, memberReferenceId, existingChannelViewIds } = params;
|
|
72
|
+
const channels = await listMemberChannelViews({
|
|
73
|
+
maestroUrl,
|
|
74
|
+
sessionToken,
|
|
75
|
+
memberReferenceId,
|
|
76
|
+
skip: 0,
|
|
77
|
+
take: 100
|
|
78
|
+
});
|
|
79
|
+
if (channels.length === 0) {
|
|
80
|
+
await prompter.note("No channels found for this account. Channel listening will stay disabled.", "Omadeus channels");
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
if (!await prompter.confirm({
|
|
84
|
+
message: "Listen for messages in Omadeus channels?",
|
|
85
|
+
initialValue: (existingChannelViewIds?.length ?? 0) > 0
|
|
86
|
+
})) return [];
|
|
87
|
+
const selected = await promptMultiSelect({
|
|
88
|
+
prompter,
|
|
89
|
+
message: "Which channels should OpenClaw listen to?",
|
|
90
|
+
options: channels.map((item) => ({
|
|
91
|
+
value: String(item.id),
|
|
92
|
+
label: item.title || `Channel ${item.id}`,
|
|
93
|
+
hint: [item.privateRoomId ? `private:${item.privateRoomId}` : void 0, item.publicRoomId ? `public:${item.publicRoomId}` : void 0].filter(Boolean).join(" | ")
|
|
94
|
+
})),
|
|
95
|
+
initialValues: existingChannelViewIds && existingChannelViewIds.length > 0 ? existingChannelViewIds.map(String) : void 0
|
|
96
|
+
});
|
|
97
|
+
return channels.filter((item) => selected.includes(String(item.id)));
|
|
98
|
+
}
|
|
99
|
+
function memberHint(member) {
|
|
100
|
+
const parts = [
|
|
101
|
+
`${member.firstName ?? ""} ${member.lastName ?? ""}`.trim(),
|
|
102
|
+
member.email?.trim(),
|
|
103
|
+
`ref:${member.referenceId}`
|
|
104
|
+
].filter(Boolean);
|
|
105
|
+
return parts.length > 0 ? parts.join(" | ") : void 0;
|
|
106
|
+
}
|
|
107
|
+
async function promptMultiSelect(params) {
|
|
108
|
+
return params.prompter.multiselect({
|
|
109
|
+
message: params.message,
|
|
110
|
+
options: params.options,
|
|
111
|
+
initialValues: params.initialValues
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
async function loadSelectableMembers(params) {
|
|
115
|
+
const excluded = new Set(params.excludeReferenceIds ?? []);
|
|
116
|
+
return (await listOrganizationMembers({
|
|
117
|
+
maestroUrl: params.maestroUrl,
|
|
118
|
+
sessionToken: params.sessionToken,
|
|
119
|
+
organizationId: params.organizationId
|
|
120
|
+
})).filter((member) => member.isSystem !== true && !excluded.has(member.referenceId)).sort((a, b) => formatMemberLabel(a).localeCompare(formatMemberLabel(b)));
|
|
121
|
+
}
|
|
122
|
+
function memberOptions(members) {
|
|
123
|
+
return members.map((member) => ({
|
|
124
|
+
value: String(member.referenceId),
|
|
125
|
+
label: formatMemberLabel(member),
|
|
126
|
+
hint: memberHint(member)
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
function readReferenceIds(values) {
|
|
130
|
+
return values.map((value) => Number(value)).filter((value) => Number.isInteger(value) && value > 0);
|
|
131
|
+
}
|
|
132
|
+
async function promptCredentials(prompter, existing) {
|
|
133
|
+
return {
|
|
134
|
+
email: String(await prompter.text({
|
|
135
|
+
message: "Omadeus username (email)",
|
|
136
|
+
initialValue: existing.email,
|
|
137
|
+
validate: (value) => String(value ?? "").trim() ? void 0 : "Required"
|
|
138
|
+
})).trim(),
|
|
139
|
+
password: String(await prompter.text({
|
|
140
|
+
message: "Omadeus password",
|
|
141
|
+
sensitive: true,
|
|
142
|
+
validate: (value) => String(value ?? "").trim() ? void 0 : "Required"
|
|
143
|
+
})).trim()
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
async function promptSenderAllowlist(params) {
|
|
147
|
+
const { prompter, message, members, existingReferenceIds } = params;
|
|
148
|
+
if (members.length === 0) throw new Error("No organization members found.");
|
|
149
|
+
if (await prompter.select({
|
|
150
|
+
message,
|
|
151
|
+
options: [{
|
|
152
|
+
value: "all",
|
|
153
|
+
label: "All users",
|
|
154
|
+
hint: "No sender allowlist"
|
|
155
|
+
}, {
|
|
156
|
+
value: "specific",
|
|
157
|
+
label: "Specific users",
|
|
158
|
+
hint: "Select one or more users"
|
|
159
|
+
}],
|
|
160
|
+
initialValue: existingReferenceIds && existingReferenceIds.length > 0 ? "specific" : "all"
|
|
161
|
+
}) === "all") return;
|
|
162
|
+
return readReferenceIds(await promptMultiSelect({
|
|
163
|
+
prompter,
|
|
164
|
+
message: `${message} (specific users)`,
|
|
165
|
+
options: memberOptions(members),
|
|
166
|
+
initialValues: existingReferenceIds?.map(String)
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
async function promptEntityKindSelection(params) {
|
|
170
|
+
const selected = await promptMultiSelect({
|
|
171
|
+
prompter: params.prompter,
|
|
172
|
+
message: "Which entity room types should OpenClaw listen to?",
|
|
173
|
+
options: OMADEUS_INBOUND_ENTITY_KINDS.map((kind) => ({
|
|
174
|
+
value: kind,
|
|
175
|
+
label: kind
|
|
176
|
+
})),
|
|
177
|
+
initialValues: params.existingKinds && params.existingKinds.length > 0 ? params.existingKinds : [...OMADEUS_INBOUND_ENTITY_KINDS]
|
|
178
|
+
});
|
|
179
|
+
const selectedSet = new Set(selected);
|
|
180
|
+
return OMADEUS_INBOUND_ENTITY_KINDS.filter((kind) => selectedSet.has(kind));
|
|
181
|
+
}
|
|
182
|
+
const omadeusSetupWizard = {
|
|
183
|
+
channel,
|
|
184
|
+
resolveAccountIdForConfigure: () => DEFAULT_ACCOUNT_ID,
|
|
185
|
+
resolveShouldPromptAccountIds: () => false,
|
|
186
|
+
status: {
|
|
187
|
+
configuredLabel: "configured",
|
|
188
|
+
unconfiguredLabel: "needs credentials",
|
|
189
|
+
configuredHint: "configured",
|
|
190
|
+
unconfiguredHint: "needs credentials",
|
|
191
|
+
configuredScore: 2,
|
|
192
|
+
unconfiguredScore: 0,
|
|
193
|
+
resolveConfigured: ({ cfg }) => {
|
|
194
|
+
return resolveOmadeusAccount({ cfg }).credentialSource !== "none";
|
|
195
|
+
},
|
|
196
|
+
resolveStatusLines: ({ cfg }) => {
|
|
197
|
+
return [`Omadeus: ${resolveOmadeusAccount({ cfg }).credentialSource !== "none" ? "configured" : "needs email, password, and organization ID"}`];
|
|
198
|
+
},
|
|
199
|
+
resolveSelectionHint: ({ cfg }) => {
|
|
200
|
+
return resolveOmadeusAccount({ cfg }).credentialSource !== "none" ? "configured" : "needs credentials";
|
|
201
|
+
},
|
|
202
|
+
resolveQuickstartScore: ({ cfg }) => {
|
|
203
|
+
return resolveOmadeusAccount({ cfg }).credentialSource !== "none" ? 2 : 0;
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
credentials: [],
|
|
207
|
+
finalize: async ({ cfg, prompter }) => {
|
|
208
|
+
const account = resolveOmadeusAccount({ cfg });
|
|
209
|
+
const section = getOmadeusChannelConfig(cfg) ?? {};
|
|
210
|
+
let next = cfg;
|
|
211
|
+
if (account.credentialSource === "none") await noteOmadeusAuthHelp(prompter);
|
|
212
|
+
const envEmail = process.env.OMADEUS_EMAIL?.trim();
|
|
213
|
+
const envPassword = process.env.OMADEUS_PASSWORD?.trim();
|
|
214
|
+
const casUrl = OMADEUS_CAS_URL;
|
|
215
|
+
const maestroUrl = OMADEUS_MAESTRO_URL;
|
|
216
|
+
let { email, password } = await promptCredentials(prompter, {
|
|
217
|
+
email: section.email ?? envEmail,
|
|
218
|
+
password: section.password ?? envPassword
|
|
219
|
+
});
|
|
220
|
+
const organizationId = await promptOrganizationId({
|
|
221
|
+
prompter,
|
|
222
|
+
maestroUrl,
|
|
223
|
+
email,
|
|
224
|
+
existing: section.organizationId
|
|
225
|
+
});
|
|
226
|
+
let sessionToken;
|
|
227
|
+
let selfReferenceId;
|
|
228
|
+
while (true) try {
|
|
229
|
+
const { dolphinToken, payload } = await authenticate({
|
|
230
|
+
casUrl,
|
|
231
|
+
maestroUrl,
|
|
232
|
+
email,
|
|
233
|
+
password,
|
|
234
|
+
organizationId
|
|
235
|
+
});
|
|
236
|
+
sessionToken = dolphinToken;
|
|
237
|
+
selfReferenceId = payload.referenceId;
|
|
238
|
+
await prompter.note(`Authenticated as ${payload.email}`, "Omadeus authentication");
|
|
239
|
+
break;
|
|
240
|
+
} catch (err) {
|
|
241
|
+
await prompter.note(`Authentication failed: ${formatAuthError(err)}`, "Omadeus authentication");
|
|
242
|
+
if (!await prompter.confirm({
|
|
243
|
+
message: "Re-enter email/password and try again?",
|
|
244
|
+
initialValue: true
|
|
245
|
+
})) {
|
|
246
|
+
await prompter.note("Saving config without verifying credentials. The gateway may fail to connect.", "Omadeus authentication");
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
({email, password} = await promptCredentials(prompter, {
|
|
250
|
+
email,
|
|
251
|
+
password
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
254
|
+
if (!sessionToken) throw new Error("Authentication is required to list channels.");
|
|
255
|
+
if (typeof selfReferenceId !== "number") throw new Error("Authentication did not return an Omadeus member reference ID.");
|
|
256
|
+
const members = await loadSelectableMembers({
|
|
257
|
+
maestroUrl,
|
|
258
|
+
sessionToken,
|
|
259
|
+
organizationId,
|
|
260
|
+
excludeReferenceIds: [selfReferenceId]
|
|
261
|
+
});
|
|
262
|
+
const existingInbound = section.inbound;
|
|
263
|
+
const directSenderIds = await promptSenderAllowlist({
|
|
264
|
+
prompter,
|
|
265
|
+
message: "Which users can DM OpenClaw directly?",
|
|
266
|
+
members,
|
|
267
|
+
existingReferenceIds: existingInbound?.direct?.allowedSenderReferenceIds
|
|
268
|
+
});
|
|
269
|
+
const selectedChannels = await promptChannelSelection({
|
|
270
|
+
prompter,
|
|
271
|
+
maestroUrl,
|
|
272
|
+
sessionToken,
|
|
273
|
+
memberReferenceId: selfReferenceId,
|
|
274
|
+
existingChannelViewIds: existingInbound?.channels?.allowedChannelViewIds
|
|
275
|
+
});
|
|
276
|
+
const channelSenderIds = selectedChannels.length > 0 ? await promptSenderAllowlist({
|
|
277
|
+
prompter,
|
|
278
|
+
message: "Which users can trigger OpenClaw from allowed channels?",
|
|
279
|
+
members,
|
|
280
|
+
existingReferenceIds: existingInbound?.channels?.allowedSenderReferenceIds
|
|
281
|
+
}) : void 0;
|
|
282
|
+
const entityKinds = await promptEntityKindSelection({
|
|
283
|
+
prompter,
|
|
284
|
+
existingKinds: existingInbound?.entities?.allowedKinds
|
|
285
|
+
});
|
|
286
|
+
const entitySenderIds = entityKinds.length > 0 ? await promptSenderAllowlist({
|
|
287
|
+
prompter,
|
|
288
|
+
message: "Which users can trigger OpenClaw from entity rooms?",
|
|
289
|
+
members,
|
|
290
|
+
existingReferenceIds: existingInbound?.entities?.allowedSenderReferenceIds
|
|
291
|
+
}) : void 0;
|
|
292
|
+
const channelRoomIds = selectedChannels.flatMap((selectedChannel) => [selectedChannel.publicRoomId, selectedChannel.privateRoomId]).filter((id) => typeof id === "number");
|
|
293
|
+
const channelViewIds = selectedChannels.map((selectedChannel) => selectedChannel.id);
|
|
294
|
+
const channelTitles = selectedChannels.map((selectedChannel) => selectedChannel.title || `Channel ${selectedChannel.id}`).join(", ");
|
|
295
|
+
const senderSummary = (ids) => ids && ids.length > 0 ? ids.join(", ") : "all users";
|
|
296
|
+
const entityKindSummary = entityKinds.length > 0 ? entityKinds.join(", ") : "none (entity rooms disabled)";
|
|
297
|
+
const channelSummary = selectedChannels.length > 0 ? `- Channels "${channelTitles}": rooms ${channelRoomIds.join(", ") || "(no room ids)"} from ${senderSummary(channelSenderIds)}; @mention not required in those rooms.` : "- Channels: disabled (none selected).";
|
|
298
|
+
await prompter.note([
|
|
299
|
+
`Inbound policy (Jaguar chat):`,
|
|
300
|
+
`- Direct messages: enabled for ${senderSummary(directSenderIds)} (no @mention required).`,
|
|
301
|
+
channelSummary,
|
|
302
|
+
`- Entity rooms (${entityKindSummary}): ${senderSummary(entitySenderIds)}; @mention required.`
|
|
303
|
+
].join("\n"), "Omadeus inbound policy");
|
|
304
|
+
next = {
|
|
305
|
+
...next,
|
|
306
|
+
channels: {
|
|
307
|
+
...next.channels,
|
|
308
|
+
omadeus: {
|
|
309
|
+
enabled: true,
|
|
310
|
+
casUrl,
|
|
311
|
+
maestroUrl,
|
|
312
|
+
email,
|
|
313
|
+
password,
|
|
314
|
+
organizationId,
|
|
315
|
+
sessionToken,
|
|
316
|
+
inbound: {
|
|
317
|
+
version: 1,
|
|
318
|
+
direct: {
|
|
319
|
+
enabled: true,
|
|
320
|
+
...directSenderIds ? { allowedSenderReferenceIds: directSenderIds } : {},
|
|
321
|
+
requireMention: "never"
|
|
322
|
+
},
|
|
323
|
+
channels: {
|
|
324
|
+
enabled: selectedChannels.length > 0,
|
|
325
|
+
allowedRoomIds: channelRoomIds,
|
|
326
|
+
allowedChannelViewIds: channelViewIds,
|
|
327
|
+
...channelSenderIds ? { allowedSenderReferenceIds: channelSenderIds } : {},
|
|
328
|
+
requireMention: "outsideAllowlist"
|
|
329
|
+
},
|
|
330
|
+
entities: {
|
|
331
|
+
enabled: entityKinds.length > 0,
|
|
332
|
+
allowedKinds: entityKinds,
|
|
333
|
+
...entitySenderIds ? { allowedSenderReferenceIds: entitySenderIds } : {},
|
|
334
|
+
requireMention: "always"
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
return {
|
|
341
|
+
cfg: next,
|
|
342
|
+
accountId: DEFAULT_ACCOUNT_ID
|
|
343
|
+
};
|
|
344
|
+
},
|
|
345
|
+
disable: (cfg) => ({
|
|
346
|
+
...cfg,
|
|
347
|
+
channels: {
|
|
348
|
+
...cfg.channels,
|
|
349
|
+
omadeus: {
|
|
350
|
+
...getOmadeusChannelConfig(cfg),
|
|
351
|
+
enabled: false
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
})
|
|
355
|
+
};
|
|
356
|
+
//#endregion
|
|
357
|
+
export { omadeusSetupWizard };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { sendRoomMessage } from "./api/message.api.js";
|
|
2
|
+
//#region src/outbound.ts
|
|
3
|
+
async function sendOmadeusMessage(deps, params) {
|
|
4
|
+
const { to, text } = params;
|
|
5
|
+
const result = await sendRoomMessage(deps.apiOpts, {
|
|
6
|
+
roomId: to,
|
|
7
|
+
body: text
|
|
8
|
+
});
|
|
9
|
+
if (!result.ok) throw new Error(`Omadeus send failed: ${result.error}`);
|
|
10
|
+
return {
|
|
11
|
+
channel: "omadeus",
|
|
12
|
+
messageId: String(result.message?.id ?? ""),
|
|
13
|
+
chatId: to
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { sendOmadeusMessage };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createReplyPrefixContext } from "../runtime-api.js";
|
|
2
|
+
import { sendOmadeusMessage } from "./outbound.js";
|
|
3
|
+
import { getOmadeusRuntime } from "./runtime.js";
|
|
4
|
+
//#region src/reply-dispatcher.ts
|
|
5
|
+
function createOmadeusReplyDispatcher(params) {
|
|
6
|
+
const core = getOmadeusRuntime();
|
|
7
|
+
const { cfg, agentId, roomId, accountId } = params;
|
|
8
|
+
const prefixContext = createReplyPrefixContext({
|
|
9
|
+
cfg,
|
|
10
|
+
agentId
|
|
11
|
+
});
|
|
12
|
+
const textChunkLimit = core.channel.text.resolveTextChunkLimit(cfg, "omadeus", accountId, { fallbackLimit: 4e3 });
|
|
13
|
+
const chunkMode = core.channel.text.resolveChunkMode(cfg, "omadeus");
|
|
14
|
+
const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({
|
|
15
|
+
responsePrefix: prefixContext.responsePrefix,
|
|
16
|
+
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
|
|
17
|
+
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, agentId),
|
|
18
|
+
deliver: async (payload) => {
|
|
19
|
+
const text = payload.text ?? "";
|
|
20
|
+
if (!text.trim()) return;
|
|
21
|
+
const chunks = core.channel.text.chunkTextWithMode(text, textChunkLimit, chunkMode);
|
|
22
|
+
for (const chunk of chunks) await sendOmadeusMessage(params.outboundDeps, {
|
|
23
|
+
to: String(roomId),
|
|
24
|
+
text: chunk
|
|
25
|
+
});
|
|
26
|
+
},
|
|
27
|
+
onError: (error, info) => {
|
|
28
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
29
|
+
params.runtime.error?.(`omadeus ${info.kind} reply failed: ${errMsg}`);
|
|
30
|
+
params.log.error("reply failed", {
|
|
31
|
+
kind: info.kind,
|
|
32
|
+
error: errMsg
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
dispatcher,
|
|
38
|
+
replyOptions: {
|
|
39
|
+
...replyOptions,
|
|
40
|
+
onModelSelected: prefixContext.onModelSelected
|
|
41
|
+
},
|
|
42
|
+
markDispatchIdle
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
export { createOmadeusReplyDispatcher };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
2
|
+
//#region src/runtime.ts
|
|
3
|
+
const { setRuntime: setOmadeusRuntime, getRuntime: getOmadeusRuntime } = createPluginRuntimeStore("Omadeus runtime not initialized");
|
|
4
|
+
//#endregion
|
|
5
|
+
export { getOmadeusRuntime, setOmadeusRuntime };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//#region src/setup-core.ts
|
|
2
|
+
function readSetupStringField(input, key) {
|
|
3
|
+
const value = input[key];
|
|
4
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
5
|
+
}
|
|
6
|
+
function readSetupNumberField(input, key) {
|
|
7
|
+
const value = input[key];
|
|
8
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
9
|
+
if (typeof value === "string" && value.trim()) {
|
|
10
|
+
const parsed = Number(value.trim());
|
|
11
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const omadeusSetupAdapter = {
|
|
15
|
+
validateInput: ({ input }) => {
|
|
16
|
+
if (!readSetupStringField(input, "email") && !input.useEnv) return "Omadeus requires --email (or use OMADEUS_EMAIL env var).";
|
|
17
|
+
return null;
|
|
18
|
+
},
|
|
19
|
+
applyAccountConfig: ({ cfg, input }) => {
|
|
20
|
+
const rawInput = input;
|
|
21
|
+
const casUrl = input.httpUrl?.trim() || void 0;
|
|
22
|
+
const maestroUrl = input.url?.trim() || void 0;
|
|
23
|
+
const email = readSetupStringField(rawInput, "email");
|
|
24
|
+
const password = input.password?.trim() || void 0;
|
|
25
|
+
const organizationId = readSetupNumberField(rawInput, "organizationId");
|
|
26
|
+
const omadeusExisting = cfg.channels?.["omadeus"];
|
|
27
|
+
const omadeusPrevious = omadeusExisting !== null && typeof omadeusExisting === "object" && !Array.isArray(omadeusExisting) ? omadeusExisting : {};
|
|
28
|
+
return {
|
|
29
|
+
...cfg,
|
|
30
|
+
channels: {
|
|
31
|
+
...cfg.channels,
|
|
32
|
+
omadeus: {
|
|
33
|
+
...omadeusPrevious,
|
|
34
|
+
enabled: true,
|
|
35
|
+
...casUrl ? { casUrl } : {},
|
|
36
|
+
...maestroUrl ? { maestroUrl } : {},
|
|
37
|
+
...email ? { email } : {},
|
|
38
|
+
...password ? { password } : {},
|
|
39
|
+
...organizationId ? { organizationId } : {}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
//#endregion
|
|
46
|
+
export { omadeusSetupAdapter };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createOmadeusSocketClient } from "./socket.js";
|
|
2
|
+
//#region src/socket/dolphin.socket.ts
|
|
3
|
+
function createDolphinSocketClient(opts) {
|
|
4
|
+
const { maestroUrl, tokenManager, onEvent, onConnect, onDisconnect, onError, log } = opts;
|
|
5
|
+
return createOmadeusSocketClient({
|
|
6
|
+
maestroUrl,
|
|
7
|
+
tokenManager,
|
|
8
|
+
pathSuffix: "dolphin-ws",
|
|
9
|
+
logPrefix: "[dolphin]",
|
|
10
|
+
onEvent,
|
|
11
|
+
onConnect,
|
|
12
|
+
onDisconnect,
|
|
13
|
+
onError,
|
|
14
|
+
log
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { createDolphinSocketClient };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { isOmadeusMessage } from "../inbound.js";
|
|
2
|
+
import { createOmadeusSocketClient } from "./socket.js";
|
|
3
|
+
//#region src/socket/jaguar.socket.ts
|
|
4
|
+
function createJaguarSocketClient(opts) {
|
|
5
|
+
const { maestroUrl, tokenManager, onMessage, onOtherEvent, onConnect, onDisconnect, onError, log } = opts;
|
|
6
|
+
return createOmadeusSocketClient({
|
|
7
|
+
maestroUrl,
|
|
8
|
+
tokenManager,
|
|
9
|
+
pathSuffix: "ws",
|
|
10
|
+
logPrefix: "[jaguar]",
|
|
11
|
+
onEvent: (data) => {
|
|
12
|
+
if (isOmadeusMessage(data)) onMessage?.(data);
|
|
13
|
+
else onOtherEvent?.(data);
|
|
14
|
+
},
|
|
15
|
+
onConnect,
|
|
16
|
+
onDisconnect,
|
|
17
|
+
onError,
|
|
18
|
+
log
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
export { createJaguarSocketClient };
|