@nordbyte/nordrelay 0.3.1 → 0.4.0

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 (48) hide show
  1. package/.env.example +45 -2
  2. package/README.md +204 -30
  3. package/dist/agent-activity.js +300 -0
  4. package/dist/agent-adapter.js +17 -30
  5. package/dist/agent-factory.js +27 -0
  6. package/dist/agent.js +123 -9
  7. package/dist/artifacts.js +1 -1
  8. package/dist/audit-log.js +1 -1
  9. package/dist/bot-ui.js +1 -1
  10. package/dist/bot.js +328 -159
  11. package/dist/claude-code-auth.js +121 -0
  12. package/dist/claude-code-cli.js +19 -0
  13. package/dist/claude-code-launch.js +73 -0
  14. package/dist/claude-code-session.js +660 -0
  15. package/dist/claude-code-state.js +590 -0
  16. package/dist/codex-session.js +12 -1
  17. package/dist/config.js +113 -9
  18. package/dist/hermes-api.js +150 -0
  19. package/dist/hermes-auth.js +96 -0
  20. package/dist/hermes-cli.js +19 -0
  21. package/dist/hermes-launch.js +57 -0
  22. package/dist/hermes-session.js +477 -0
  23. package/dist/hermes-state.js +609 -0
  24. package/dist/index.js +51 -8
  25. package/dist/openclaw-auth.js +27 -0
  26. package/dist/openclaw-cli.js +19 -0
  27. package/dist/openclaw-gateway.js +285 -0
  28. package/dist/openclaw-launch.js +65 -0
  29. package/dist/openclaw-session.js +549 -0
  30. package/dist/openclaw-state.js +409 -0
  31. package/dist/operations.js +83 -2
  32. package/dist/pi-auth.js +59 -0
  33. package/dist/pi-launch.js +61 -0
  34. package/dist/pi-rpc.js +18 -0
  35. package/dist/pi-session.js +103 -15
  36. package/dist/pi-state.js +253 -0
  37. package/dist/relay-runtime.js +673 -51
  38. package/dist/session-format.js +28 -18
  39. package/dist/session-registry.js +40 -15
  40. package/dist/settings-service.js +35 -4
  41. package/dist/web-dashboard-ui.js +18 -0
  42. package/dist/web-dashboard.js +329 -47
  43. package/package.json +8 -3
  44. package/plugins/nordrelay/.codex-plugin/plugin.json +7 -4
  45. package/plugins/nordrelay/commands/remote.md +2 -2
  46. package/plugins/nordrelay/scripts/nordrelay.mjs +131 -3
  47. package/plugins/nordrelay/skills/telegram-remote/SKILL.md +2 -2
  48. package/CHANGELOG.md +0 -26
@@ -0,0 +1,27 @@
1
+ import { OpenClawGatewayClient } from "./openclaw-gateway.js";
2
+ export async function checkOpenClawAuthStatus(options) {
3
+ const client = new OpenClawGatewayClient({
4
+ url: options.gatewayUrl,
5
+ token: options.token,
6
+ password: options.password,
7
+ timeoutMs: 5_000,
8
+ });
9
+ try {
10
+ await client.health();
11
+ return {
12
+ authenticated: true,
13
+ method: options.token || options.password ? "api-key" : "cli",
14
+ detail: `OpenClaw Gateway reachable at ${options.gatewayUrl}`,
15
+ };
16
+ }
17
+ catch (error) {
18
+ return {
19
+ authenticated: false,
20
+ method: options.token || options.password ? "api-key" : "cli",
21
+ detail: error instanceof Error ? error.message : String(error),
22
+ };
23
+ }
24
+ finally {
25
+ client.close();
26
+ }
27
+ }
@@ -0,0 +1,19 @@
1
+ import { findExecutableOnPath } from "./codex-cli.js";
2
+ export function resolveOpenClawCli(env = process.env, explicitPath) {
3
+ const configuredPath = optionalString(explicitPath) ?? optionalString(env.OPENCLAW_CLI_PATH);
4
+ if (configuredPath) {
5
+ return { path: configuredPath, source: "env" };
6
+ }
7
+ const pathMatch = findExecutableOnPath("openclaw", env.PATH);
8
+ return pathMatch ? { path: pathMatch, source: "path" } : { source: "missing" };
9
+ }
10
+ export function describeOpenClawCli(resolution) {
11
+ if (resolution.path) {
12
+ return `${resolution.source} (${resolution.path})`;
13
+ }
14
+ return "missing";
15
+ }
16
+ function optionalString(value) {
17
+ const trimmed = value?.trim();
18
+ return trimmed ? trimmed : undefined;
19
+ }
@@ -0,0 +1,285 @@
1
+ import { randomUUID } from "node:crypto";
2
+ export class OpenClawGatewayClient {
3
+ options;
4
+ socket = null;
5
+ connected = false;
6
+ pending = new Map();
7
+ listeners = new Set();
8
+ timeoutMs;
9
+ constructor(options) {
10
+ this.options = options;
11
+ this.timeoutMs = options.timeoutMs ?? 15_000;
12
+ }
13
+ async connect() {
14
+ if (this.socket && this.connected) {
15
+ return {};
16
+ }
17
+ const WebSocketClass = this.options.webSocketFactory ?? getGlobalWebSocket();
18
+ const socket = new WebSocketClass(this.options.url);
19
+ this.socket = socket;
20
+ socket.addEventListener("message", (event) => this.handleMessage(event));
21
+ socket.addEventListener("close", () => this.rejectAll(new Error("OpenClaw Gateway connection closed")));
22
+ socket.addEventListener("error", () => this.rejectAll(new Error("OpenClaw Gateway connection failed")));
23
+ await waitForOpen(socket, this.timeoutMs);
24
+ const hello = await this.sendConnect();
25
+ this.connected = true;
26
+ return hello;
27
+ }
28
+ async health() {
29
+ await this.connect();
30
+ return this.request("health", {}, { timeoutMs: 10_000 }).catch(() => this.request("status", {}, { timeoutMs: 10_000 }));
31
+ }
32
+ async listSessions(params = {}) {
33
+ await this.connect();
34
+ return this.request("sessions.list", params, { timeoutMs: 10_000 });
35
+ }
36
+ async listModels(params = {}) {
37
+ await this.connect();
38
+ return this.request("models.list", params, { timeoutMs: 10_000 });
39
+ }
40
+ async runAgent(request, onEvent, signal) {
41
+ await this.connect();
42
+ const runState = { runId: null };
43
+ const off = this.onEvent((event) => {
44
+ if (eventMatchesRun(event, runState.runId, request.sessionId)) {
45
+ onEvent(event);
46
+ }
47
+ });
48
+ const abort = () => {
49
+ if (runState.runId) {
50
+ void this.cancelRun(runState.runId).catch(() => { });
51
+ }
52
+ };
53
+ signal?.addEventListener("abort", abort, { once: true });
54
+ try {
55
+ const payload = await this.request("agent", buildAgentParams(request), {
56
+ expectFinal: true,
57
+ timeoutMs: 0,
58
+ onAccepted: (accepted) => {
59
+ runState.runId = stringValue(accepted.runId) ?? stringValue(accepted.run_id) ?? stringValue(accepted.id);
60
+ if (runState.runId) {
61
+ request.onRunId?.(runState.runId);
62
+ }
63
+ },
64
+ });
65
+ const text = extractOpenClawOutputText(payload);
66
+ return {
67
+ runId: runState.runId ?? stringValue(payload.runId) ?? stringValue(payload.run_id),
68
+ status: stringValue(payload.status) ?? "ok",
69
+ text,
70
+ usage: payload.usage,
71
+ payload,
72
+ };
73
+ }
74
+ finally {
75
+ signal?.removeEventListener("abort", abort);
76
+ off();
77
+ }
78
+ }
79
+ async cancelRun(runId) {
80
+ await this.connect();
81
+ await this.request("agent.cancel", { runId }, { timeoutMs: 5_000 }).catch(() => this.request("tasks.cancel", { runId }, { timeoutMs: 5_000 })).catch(() => { });
82
+ }
83
+ onEvent(listener) {
84
+ this.listeners.add(listener);
85
+ return () => this.listeners.delete(listener);
86
+ }
87
+ close() {
88
+ this.connected = false;
89
+ this.rejectAll(new Error("OpenClaw Gateway connection closed"));
90
+ this.socket?.close();
91
+ this.socket = null;
92
+ }
93
+ sendConnect() {
94
+ const params = {
95
+ client: {
96
+ name: this.options.clientName ?? "NordRelay",
97
+ deviceFamily: "nordrelay",
98
+ },
99
+ role: "operator",
100
+ subscribe: ["agent", "session.message", "session.tool", "sessions.changed", "health"],
101
+ };
102
+ const auth = {};
103
+ if (this.options.token)
104
+ auth.token = this.options.token;
105
+ if (this.options.password)
106
+ auth.password = this.options.password;
107
+ if (Object.keys(auth).length > 0) {
108
+ params.auth = auth;
109
+ }
110
+ return this.sendFrameAndWait("connect", "connect", params, { timeoutMs: this.timeoutMs });
111
+ }
112
+ request(method, params, options = {}) {
113
+ return this.sendFrameAndWait("req", method, params, options);
114
+ }
115
+ sendFrameAndWait(type, method, params, options = {}) {
116
+ const socket = this.socket;
117
+ if (!socket) {
118
+ return Promise.reject(new Error("OpenClaw Gateway is not connected"));
119
+ }
120
+ const id = randomUUID();
121
+ const timeoutMs = options.timeoutMs ?? this.timeoutMs;
122
+ const frame = type === "connect"
123
+ ? { type, id, params }
124
+ : { type, id, method, params, idempotencyKey: randomUUID() };
125
+ return new Promise((resolve, reject) => {
126
+ const timeout = timeoutMs > 0
127
+ ? setTimeout(() => {
128
+ this.pending.delete(id);
129
+ reject(new Error(`OpenClaw Gateway ${method} timed out after ${timeoutMs}ms`));
130
+ }, timeoutMs)
131
+ : undefined;
132
+ if (timeout?.unref)
133
+ timeout.unref();
134
+ this.pending.set(id, {
135
+ method,
136
+ expectFinal: Boolean(options.expectFinal),
137
+ resolve,
138
+ reject,
139
+ onAccepted: options.onAccepted,
140
+ timeout,
141
+ });
142
+ socket.send(JSON.stringify(frame));
143
+ });
144
+ }
145
+ handleMessage(event) {
146
+ const data = event.data;
147
+ const raw = typeof data === "string" ? data : data instanceof Buffer ? data.toString("utf8") : String(data ?? "");
148
+ const frame = parseFrame(raw);
149
+ if (!frame) {
150
+ return;
151
+ }
152
+ if (frame.type === "event") {
153
+ for (const listener of this.listeners) {
154
+ try {
155
+ listener(frame);
156
+ }
157
+ catch {
158
+ this.listeners.delete(listener);
159
+ }
160
+ }
161
+ return;
162
+ }
163
+ const id = stringValue(frame.id);
164
+ if (!id) {
165
+ return;
166
+ }
167
+ const pending = this.pending.get(id);
168
+ if (!pending) {
169
+ return;
170
+ }
171
+ if (frame.ok === false || frame.error) {
172
+ this.pending.delete(id);
173
+ clearTimeout(pending.timeout);
174
+ pending.reject(new Error(formatGatewayError(frame.error ?? frame.payload ?? frame)));
175
+ return;
176
+ }
177
+ const payload = objectValue(frame.payload) ?? objectValue(frame.result) ?? frame;
178
+ const status = stringValue(payload.status);
179
+ if (pending.expectFinal && status === "accepted") {
180
+ pending.onAccepted?.(payload);
181
+ return;
182
+ }
183
+ this.pending.delete(id);
184
+ clearTimeout(pending.timeout);
185
+ pending.resolve(payload);
186
+ }
187
+ rejectAll(error) {
188
+ for (const [id, pending] of this.pending.entries()) {
189
+ clearTimeout(pending.timeout);
190
+ pending.reject(error);
191
+ this.pending.delete(id);
192
+ }
193
+ }
194
+ }
195
+ export function extractOpenClawOutputText(payload) {
196
+ const object = objectValue(payload);
197
+ if (!object) {
198
+ return typeof payload === "string" && payload.trim() ? payload : null;
199
+ }
200
+ const direct = stringValue(object.text)
201
+ ?? stringValue(object.output)
202
+ ?? stringValue(object.summary)
203
+ ?? stringValue(object.message)
204
+ ?? stringValue(object.content);
205
+ if (direct) {
206
+ return direct;
207
+ }
208
+ const result = objectValue(object.result);
209
+ if (result) {
210
+ return extractOpenClawOutputText(result);
211
+ }
212
+ const payloads = Array.isArray(object.payloads) ? object.payloads : [];
213
+ const textParts = payloads
214
+ .map((entry) => stringValue(objectValue(entry)?.text) ?? stringValue(entry))
215
+ .filter((entry) => Boolean(entry));
216
+ return textParts.length > 0 ? textParts.join("\n\n") : null;
217
+ }
218
+ function buildAgentParams(request) {
219
+ return {
220
+ message: request.message,
221
+ sessionId: request.sessionId,
222
+ session_id: request.sessionId,
223
+ agent: request.agentId,
224
+ agentId: request.agentId,
225
+ model: request.model,
226
+ thinking: request.thinking,
227
+ workspace: request.workspace,
228
+ local: request.local,
229
+ deliver: request.deliver,
230
+ instructions: request.instructions,
231
+ attachments: request.attachments,
232
+ };
233
+ }
234
+ function eventMatchesRun(event, runId, sessionId) {
235
+ const payload = objectValue(event.payload) ?? event;
236
+ const eventRunId = stringValue(payload.runId) ?? stringValue(payload.run_id) ?? stringValue(payload.id);
237
+ const eventSessionId = stringValue(payload.sessionId) ?? stringValue(payload.session_id) ?? stringValue(payload.sessionKey);
238
+ if (runId && eventRunId) {
239
+ return eventRunId === runId;
240
+ }
241
+ return !eventSessionId || eventSessionId === sessionId;
242
+ }
243
+ function getGlobalWebSocket() {
244
+ const WebSocketClass = globalThis.WebSocket;
245
+ if (!WebSocketClass) {
246
+ throw new Error("OpenClaw Gateway requires a WebSocket-capable Node runtime.");
247
+ }
248
+ return WebSocketClass;
249
+ }
250
+ function waitForOpen(socket, timeoutMs) {
251
+ return new Promise((resolve, reject) => {
252
+ const timeout = setTimeout(() => reject(new Error(`OpenClaw Gateway connection timed out after ${timeoutMs}ms`)), timeoutMs);
253
+ timeout.unref?.();
254
+ socket.addEventListener("open", () => {
255
+ clearTimeout(timeout);
256
+ resolve();
257
+ }, { once: true });
258
+ socket.addEventListener("error", () => {
259
+ clearTimeout(timeout);
260
+ reject(new Error("OpenClaw Gateway connection failed"));
261
+ }, { once: true });
262
+ });
263
+ }
264
+ function parseFrame(raw) {
265
+ try {
266
+ const parsed = JSON.parse(raw);
267
+ return objectValue(parsed);
268
+ }
269
+ catch {
270
+ return null;
271
+ }
272
+ }
273
+ function formatGatewayError(value) {
274
+ const object = objectValue(value);
275
+ if (!object) {
276
+ return typeof value === "string" ? value : "OpenClaw Gateway request failed";
277
+ }
278
+ return stringValue(object.message) ?? stringValue(object.error) ?? JSON.stringify(object);
279
+ }
280
+ function objectValue(value) {
281
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
282
+ }
283
+ function stringValue(value) {
284
+ return typeof value === "string" && value.trim() ? value : null;
285
+ }
@@ -0,0 +1,65 @@
1
+ import { createLaunchProfile } from "./codex-launch.js";
2
+ export const OPENCLAW_LAUNCH_PROFILES = [
3
+ {
4
+ id: "default",
5
+ label: "Default",
6
+ behavior: "OpenClaw Gateway defaults",
7
+ unsafe: false,
8
+ local: false,
9
+ deliver: false,
10
+ },
11
+ {
12
+ id: "safe",
13
+ label: "Safe",
14
+ behavior: "non-destructive intent / Gateway defaults",
15
+ unsafe: false,
16
+ local: false,
17
+ deliver: false,
18
+ instructions: "Do not perform destructive operations. If a risky action is required, stop and explain the required approval.",
19
+ },
20
+ {
21
+ id: "readonly",
22
+ label: "Read Only",
23
+ behavior: "inspect-only intent / no file changes",
24
+ unsafe: false,
25
+ local: false,
26
+ deliver: false,
27
+ instructions: "Treat this run as read-only. Inspect, explain, and plan, but do not modify files or execute destructive commands.",
28
+ },
29
+ {
30
+ id: "local",
31
+ label: "Local",
32
+ behavior: "force OpenClaw embedded local run",
33
+ unsafe: false,
34
+ local: true,
35
+ deliver: false,
36
+ },
37
+ {
38
+ id: "deliver",
39
+ label: "Deliver",
40
+ behavior: "OpenClaw Gateway delivery enabled",
41
+ unsafe: false,
42
+ local: false,
43
+ deliver: true,
44
+ },
45
+ ];
46
+ export function listOpenClawLaunchProfiles() {
47
+ return OPENCLAW_LAUNCH_PROFILES.map((profile) => ({
48
+ id: profile.id,
49
+ label: profile.label,
50
+ behavior: profile.behavior,
51
+ unsafe: profile.unsafe,
52
+ }));
53
+ }
54
+ export function findOpenClawLaunchProfile(profileId) {
55
+ const profile = OPENCLAW_LAUNCH_PROFILES.find((candidate) => candidate.id === profileId);
56
+ return profile ?? OPENCLAW_LAUNCH_PROFILES[0];
57
+ }
58
+ export function openClawProfileAsLaunchProfile(profile) {
59
+ return createLaunchProfile({
60
+ id: profile.id,
61
+ label: profile.label,
62
+ sandboxMode: profile.unsafe ? "danger-full-access" : "workspace-write",
63
+ approvalPolicy: "never",
64
+ });
65
+ }