nexus-channel 1.6.5

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.
@@ -0,0 +1,249 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.WSClient = void 0;
40
+ const ws_1 = __importDefault(require("ws"));
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ const frames_1 = require("./frames");
44
+ const reconnect_1 = require("./reconnect");
45
+ const resume_store_1 = require("./resume-store");
46
+ class WSClient {
47
+ constructor(config) {
48
+ this.ws = null;
49
+ this.reconnectTimer = null;
50
+ this.config = config;
51
+ this.reconnectState = (0, reconnect_1.createReconnectState)();
52
+ this.state = { connected: false };
53
+ }
54
+ connect() {
55
+ const myConnId = (0, reconnect_1.incrementConnId)(this.reconnectState);
56
+ if (this.ws) {
57
+ try {
58
+ this.ws.close();
59
+ }
60
+ catch { }
61
+ this.ws = null;
62
+ }
63
+ console.log(`[nexus] WS connecting: ${this.config.hub2dUrl} rooms=${this.config.rooms.join(',')}`);
64
+ this.ws = new ws_1.default(this.config.hub2dUrl);
65
+ this.ws.on('open', () => {
66
+ this.sendConnectFrame();
67
+ });
68
+ this.ws.on('message', (data) => {
69
+ this.handleMessage(data);
70
+ });
71
+ this.ws.on('close', (code, reason) => {
72
+ this.handleClose(code, reason, myConnId);
73
+ });
74
+ this.ws.on('error', (error) => {
75
+ console.error(`[nexus] WS error: ${error.message}`);
76
+ });
77
+ }
78
+ sendConnectFrame() {
79
+ const frame = (0, frames_1.buildConnectFrame)({
80
+ node: this.config.agentName,
81
+ rooms: this.config.rooms,
82
+ resumeToken: (0, resume_store_1.getResumeTokens)(),
83
+ token: this.config.token,
84
+ openclawVersion: process.env.OPENCLAW_SERVICE_VERSION || process.env.OPENCLAW_VERSION || 'unknown',
85
+ pluginVersion: this.readPluginVersion(),
86
+ });
87
+ this.ws?.send(JSON.stringify(frame));
88
+ console.log('[nexus] WS open, connect frame sent');
89
+ }
90
+ readPluginVersion() {
91
+ try {
92
+ // Walk up from __dirname to find the plugin's package.json
93
+ // dist/src/transport -> dist/src -> dist -> <plugin-root>
94
+ let dir = path.resolve(__dirname, '..');
95
+ for (let i = 0; i < 5 && dir; i++) {
96
+ const pkgPath = path.join(dir, 'package.json');
97
+ if (fs.existsSync(pkgPath)) {
98
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
99
+ if (pkg.name === 'nexus-channel' || pkg.name?.includes('nexus')) {
100
+ return pkg.version || 'unknown';
101
+ }
102
+ }
103
+ dir = path.dirname(dir);
104
+ }
105
+ return 'unknown';
106
+ }
107
+ catch {
108
+ return 'unknown';
109
+ }
110
+ }
111
+ handleMessage(data) {
112
+ let frame;
113
+ try {
114
+ frame = JSON.parse(typeof data === 'string' ? data : data.toString());
115
+ }
116
+ catch {
117
+ console.error('[nexus] WS bad JSON, ignoring');
118
+ return;
119
+ }
120
+ switch (frame.type) {
121
+ case 'connected': {
122
+ const connectedFrame = frame;
123
+ (0, reconnect_1.resetReconnectDelay)(this.reconnectState);
124
+ this.state.connected = true;
125
+ this.state.pendingApproval = false;
126
+ if (typeof connectedFrame.user_id === 'number') {
127
+ this.state.userId = connectedFrame.user_id;
128
+ }
129
+ console.log(`[nexus] WS connected: user_id=${this.state.userId} backlog_count=${JSON.stringify(connectedFrame.backlog_count || {})}`);
130
+ this.config.onConnected?.(connectedFrame);
131
+ break;
132
+ }
133
+ case 'event':
134
+ this.config.onEvent?.(frame);
135
+ break;
136
+ case 'ping':
137
+ this.ws?.send(JSON.stringify({ type: 'pong', ts: Date.now() }));
138
+ break;
139
+ case 'error': {
140
+ const errorFrame = frame;
141
+ this.state.lastError = {
142
+ code: errorFrame.code || 'UNKNOWN',
143
+ message: errorFrame.message || JSON.stringify(frame),
144
+ ts: Date.now(),
145
+ };
146
+ if (errorFrame.code === 'PENDING_APPROVAL') {
147
+ this.state.pendingApproval = true;
148
+ console.warn(`[nexus] Agent pending approval: node=${this.config.agentName}. ` +
149
+ 'An admin must approve this agent in the Nexus UI before connecting. ' +
150
+ 'Will retry with reduced frequency.');
151
+ }
152
+ console.error(`[nexus] Server error: code=${this.state.lastError.code} msg=${this.state.lastError.message}`);
153
+ this.config.onError?.(errorFrame);
154
+ break;
155
+ }
156
+ default:
157
+ break;
158
+ }
159
+ }
160
+ handleClose(code, reason, myConnId) {
161
+ console.log(`[nexus] WS closed: code=${code} reason=${reason.toString() || 'none'} connId=${myConnId}/${this.reconnectState.connId}`);
162
+ this.state.connected = false;
163
+ (0, resume_store_1.flushResumeTokens)();
164
+ if (!(0, reconnect_1.isStaleConnection)(this.reconnectState, myConnId)) {
165
+ this.scheduleReconnect();
166
+ }
167
+ else {
168
+ console.log('[nexus] WS close from stale connection, skipping reconnect');
169
+ }
170
+ this.config.onClose?.();
171
+ }
172
+ scheduleReconnect() {
173
+ if (this.reconnectTimer) {
174
+ clearTimeout(this.reconnectTimer);
175
+ }
176
+ // If pending approval, use a longer delay (60s base, 5min max)
177
+ const PENDING_BASE_DELAY = 60000;
178
+ const PENDING_MAX_DELAY = 300000;
179
+ if (this.state.pendingApproval) {
180
+ const jitter = Math.floor(Math.random() * 5000);
181
+ const delay = Math.min(Math.max(this.reconnectState.currentDelay, PENDING_BASE_DELAY) + jitter, PENDING_MAX_DELAY);
182
+ this.reconnectTimer = setTimeout(() => { this.connect(); }, delay);
183
+ console.log(`[nexus] Reconnecting in ${Math.round(delay / 1000)}s (pending approval)`);
184
+ }
185
+ else {
186
+ const { delay, timer } = (0, reconnect_1.scheduleReconnect)(this.reconnectState, () => {
187
+ this.connect();
188
+ });
189
+ this.reconnectTimer = timer;
190
+ console.log(`[nexus] Reconnecting in ${delay}ms`);
191
+ }
192
+ }
193
+ sendAck(roomId, eventId) {
194
+ if (!this.ws || this.ws.readyState !== ws_1.default.OPEN)
195
+ return;
196
+ this.ws.send(JSON.stringify((0, frames_1.buildAckFrame)(roomId, eventId)));
197
+ }
198
+ sendReply(eventId, roomId, to, text, status) {
199
+ if (!this.ws || this.ws.readyState !== ws_1.default.OPEN) {
200
+ console.error(`[nexus] Cannot send reply, WS not open (event_id=${eventId})`);
201
+ return;
202
+ }
203
+ this.ws.send(JSON.stringify((0, frames_1.buildReplyFrame)({
204
+ eventId,
205
+ roomId,
206
+ from: this.config.agentName,
207
+ to,
208
+ text,
209
+ status,
210
+ })));
211
+ }
212
+ sendMessage(roomId, text, mentions, clientMsgId) {
213
+ if (!this.ws || this.ws.readyState !== ws_1.default.OPEN) {
214
+ console.error('[nexus] Cannot send message, WS not connected');
215
+ return;
216
+ }
217
+ this.ws.send(JSON.stringify((0, frames_1.buildSendFrame)({
218
+ roomId,
219
+ text,
220
+ mentions,
221
+ clientMsgId,
222
+ })));
223
+ }
224
+ getState() {
225
+ return { ...this.state };
226
+ }
227
+ isConnected() {
228
+ return this.state.connected;
229
+ }
230
+ getUserId() {
231
+ return this.state.userId;
232
+ }
233
+ close() {
234
+ if (this.reconnectTimer) {
235
+ clearTimeout(this.reconnectTimer);
236
+ this.reconnectTimer = null;
237
+ }
238
+ if (this.ws) {
239
+ try {
240
+ this.ws.close();
241
+ }
242
+ catch { }
243
+ this.ws = null;
244
+ }
245
+ (0, resume_store_1.flushResumeTokens)();
246
+ this.state.connected = false;
247
+ }
248
+ }
249
+ exports.WSClient = WSClient;
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_test_1 = __importDefault(require("node:test"));
7
+ const strict_1 = __importDefault(require("node:assert/strict"));
8
+ const resolve_config_1 = require("../../src/config/resolve-config");
9
+ (0, node_test_1.default)('resolveNexusConfig reads channels.nexus path', () => {
10
+ const result = (0, resolve_config_1.resolveNexusConfig)({
11
+ channels: {
12
+ 'nexus-channel': {
13
+ hub2dUrl: 'ws://127.0.0.1:3001',
14
+ roomId: 'general',
15
+ agentName: 'dot',
16
+ },
17
+ },
18
+ });
19
+ strict_1.default.equal(result.hub2dUrl, 'ws://127.0.0.1:3001');
20
+ strict_1.default.equal(result.roomId, 'general');
21
+ strict_1.default.equal(result.agentName, 'dot');
22
+ strict_1.default.equal(result.__nexusMeta.configured, true);
23
+ strict_1.default.equal(result.__nexusMeta.configSource, 'channels.nexus-channel');
24
+ });
25
+ (0, node_test_1.default)('validateNexusConfig reports missing required fields', () => {
26
+ const result = (0, resolve_config_1.validateNexusConfig)({});
27
+ strict_1.default.equal(result.valid, false);
28
+ strict_1.default.ok(result.errors.includes('hub2dUrl is required'));
29
+ strict_1.default.ok(result.errors.includes('roomId is required'));
30
+ strict_1.default.ok(result.errors.includes('agentName is required'));
31
+ });
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_test_1 = __importDefault(require("node:test"));
7
+ const strict_1 = __importDefault(require("node:assert/strict"));
8
+ const event_handler_1 = require("../../src/runtime/event-handler");
9
+ (0, node_test_1.default)('getEventTimestamp parses stream id prefix', () => {
10
+ strict_1.default.equal((0, event_handler_1.getEventTimestamp)('1710000000000-1'), 1710000000000);
11
+ });
12
+ (0, node_test_1.default)('getEventDecision skips own messages', () => {
13
+ const decision = (0, event_handler_1.getEventDecision)({
14
+ event: { event_id: '1710000000000-1', room_id: 'general', from: 'dot', text: 'hello' },
15
+ config: { agentName: 'dot' },
16
+ isRecent: () => false,
17
+ });
18
+ strict_1.default.equal(decision, 'skip-own');
19
+ });
20
+ (0, node_test_1.default)('buildEventPrompt prepends sender identity', () => {
21
+ const prompt = (0, event_handler_1.buildEventPrompt)({ event_id: '1-1', room_id: 'general', from: 'boss', text: 'hello' }, { longTextThreshold: 4000 });
22
+ strict_1.default.equal(prompt, '[from: boss] hello');
23
+ });
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_test_1 = __importDefault(require("node:test"));
7
+ const strict_1 = __importDefault(require("node:assert/strict"));
8
+ const frames_1 = require("../../src/transport/frames");
9
+ (0, node_test_1.default)('buildConnectFrame fills required fields', () => {
10
+ const frame = (0, frames_1.buildConnectFrame)({ node: 'dot', rooms: ['general'] });
11
+ strict_1.default.equal(frame.type, 'connect');
12
+ strict_1.default.equal(frame.node, 'dot');
13
+ strict_1.default.deepEqual(frame.rooms, ['general']);
14
+ strict_1.default.deepEqual(frame.resume_token, {});
15
+ });
16
+ (0, node_test_1.default)('buildAckFrame returns ack shape', () => {
17
+ strict_1.default.deepEqual((0, frames_1.buildAckFrame)('general', '1-1'), {
18
+ type: 'ack',
19
+ room_id: 'general',
20
+ event_id: '1-1',
21
+ });
22
+ });
23
+ (0, node_test_1.default)('buildReplyFrame mirrors text to content', () => {
24
+ const frame = (0, frames_1.buildReplyFrame)({ eventId: '1-1', roomId: 'general', from: 'dot', to: 'boss', text: 'ok', status: 'done' });
25
+ strict_1.default.equal(frame.text, 'ok');
26
+ strict_1.default.equal(frame.content, 'ok');
27
+ });
28
+ (0, node_test_1.default)('buildSendFrame includes mentions when provided', () => {
29
+ const frame = (0, frames_1.buildSendFrame)({ roomId: 'general', text: 'hello', mentions: ['dot'] });
30
+ strict_1.default.deepEqual(frame.mentions, ['dot']);
31
+ });
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_test_1 = __importDefault(require("node:test"));
7
+ const strict_1 = __importDefault(require("node:assert/strict"));
8
+ const reconnect_1 = require("../../src/transport/reconnect");
9
+ (0, node_test_1.default)('reconnect state starts with defaults', () => {
10
+ const state = (0, reconnect_1.createReconnectState)();
11
+ strict_1.default.equal(state.currentDelay, 1000);
12
+ strict_1.default.equal(state.connId, 0);
13
+ });
14
+ (0, node_test_1.default)('incrementConnId advances connection id', () => {
15
+ const state = (0, reconnect_1.createReconnectState)();
16
+ strict_1.default.equal((0, reconnect_1.incrementConnId)(state), 1);
17
+ strict_1.default.equal((0, reconnect_1.incrementConnId)(state), 2);
18
+ });
19
+ (0, node_test_1.default)('isStaleConnection detects stale ids', () => {
20
+ const state = (0, reconnect_1.createReconnectState)();
21
+ (0, reconnect_1.incrementConnId)(state);
22
+ strict_1.default.equal((0, reconnect_1.isStaleConnection)(state, 1), false);
23
+ strict_1.default.equal((0, reconnect_1.isStaleConnection)(state, 0), true);
24
+ (0, reconnect_1.resetReconnectDelay)(state);
25
+ strict_1.default.equal(state.currentDelay, 1000);
26
+ });
@@ -0,0 +1,43 @@
1
+ {
2
+ "id": "nexus",
3
+ "name": "Nexus Hub Channel",
4
+ "description": "Nexus Hub 2.0 channel plugin for multi-agent collaboration via WebSocket",
5
+ "kind": "channel",
6
+ "channels": ["nexus"],
7
+ "configSchema": {
8
+ "type": "object",
9
+ "additionalProperties": false,
10
+ "properties": {}
11
+ },
12
+ "channelConfigs": {
13
+ "nexus-channel": {
14
+ "schema": {
15
+ "type": "object",
16
+ "additionalProperties": false,
17
+ "properties": {
18
+ "enabled": { "type": "boolean" },
19
+ "hub2dUrl": { "type": "string" },
20
+ "roomId": { "type": "string" },
21
+ "agentName": { "type": "string" },
22
+ "token": { "type": "string" },
23
+ "nexusApiKey": { "type": "string" },
24
+ "apiPort": { "type": "integer" },
25
+ "gatewayPort": { "type": "integer" },
26
+ "gatewayToken": { "type": "string" },
27
+ "gatewayTimeoutMs": { "type": "integer", "minimum": 5000 },
28
+ "longTextThreshold": { "type": "integer", "minimum": 500 },
29
+ "contextInjection": { "type": "string", "enum": ["P0", "L0", "L1", "L2", "OFF"] }
30
+ }
31
+ },
32
+ "uiHints": {
33
+ "hub2dUrl": { "label": "Hub2d WS URL", "placeholder": "wss://your-hub.example.com/ws" },
34
+ "roomId": { "label": "Room IDs (comma-sep)", "placeholder": "general,boss" },
35
+ "agentName": { "label": "Agent Name", "placeholder": "serina" },
36
+ "token": { "label": "Agent Token", "placeholder": "nagt_...", "sensitive": true },
37
+ "nexusApiKey": { "label": "Nexus API Key", "placeholder": "nxk-...", "sensitive": true },
38
+ "gatewayTimeoutMs": { "label": "Gateway Timeout (ms)", "placeholder": "120000" },
39
+ "contextInjection": { "label": "Context Inject Level", "placeholder": "P0" }
40
+ }
41
+ }
42
+ }
43
+ }
package/package.json ADDED
@@ -0,0 +1,83 @@
1
+ {
2
+ "name": "nexus-channel",
3
+ "version": "1.6.5",
4
+ "description": "Nexus Hub 2.0 channel plugin for OpenClaw — enables agents to connect to Nexus Hub as a channel, with A2A dispatch, room summary, and Control Plane management.",
5
+ "type": "commonjs",
6
+ "main": "dist/index.js",
7
+ "files": [
8
+ "dist/",
9
+ "openclaw.plugin.json"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "typecheck": "tsc --noEmit",
14
+ "test": "node --test dist/tests/unit/*.test.js",
15
+ "test:src": "npm run build && npm run test",
16
+ "prepublishOnly": "npm run build"
17
+ },
18
+ "dependencies": {
19
+ "ws": "^8.19.0"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^20.11.0",
23
+ "@types/ws": "^8.5.12",
24
+ "typescript": "^5.3.3"
25
+ },
26
+ "peerDependencies": {
27
+ "openclaw": "*"
28
+ },
29
+ "openclaw": {
30
+ "extensions": [
31
+ "./dist/index.js"
32
+ ],
33
+ "setupEntry": "./dist/setup-entry.js",
34
+ "compat": {
35
+ "pluginApi": ">=2026.3.0",
36
+ "minGatewayVersion": "2026.3.0"
37
+ },
38
+ "build": {
39
+ "openclawVersion": "2026.3.0",
40
+ "pluginSdkVersion": "2026.3.0"
41
+ },
42
+ "channel": {
43
+ "id": "nexus-channel",
44
+ "label": "Nexus Hub 2.0",
45
+ "selectionLabel": "Nexus Hub (WebSocket)",
46
+ "detailLabel": "Nexus Hub 2.0 Channel",
47
+ "docsPath": "/channels/nexus-channel",
48
+ "docsLabel": "nexus-channel",
49
+ "blurb": "Connect to your Nexus Hub 2.0 workspace — A2A dispatch, room summaries, and shared agent memory across rooms.",
50
+ "systemImage": "network",
51
+ "markdownCapable": true
52
+ },
53
+ "install": {
54
+ "npmSpec": "nexus-channel",
55
+ "localPath": "extensions/nexus-channel",
56
+ "defaultChoice": "npm",
57
+ "minHostVersion": ">=2026.3.0"
58
+ },
59
+ "bundle": {
60
+ "stageRuntimeDependencies": false
61
+ },
62
+ "release": {
63
+ "publishToNpm": true
64
+ }
65
+ },
66
+ "engines": {
67
+ "node": ">=18.0.0"
68
+ },
69
+ "author": "Your Org <your@email.com>",
70
+ "license": "MIT",
71
+ "repository": {
72
+ "type": "git",
73
+ "url": "git+https://github.com/yourorg/openclaw-nexus.git"
74
+ },
75
+ "keywords": [
76
+ "openclaw",
77
+ "openclaw-plugin",
78
+ "nexus",
79
+ "nexus-hub",
80
+ "channel",
81
+ "multi-agent"
82
+ ]
83
+ }