@masons/agent-network 0.1.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.
- package/LICENSE +21 -0
- package/README.md +53 -0
- package/dist/channel.d.ts +63 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +182 -0
- package/dist/cli-setup.d.ts +39 -0
- package/dist/cli-setup.d.ts.map +1 -0
- package/dist/cli-setup.js +146 -0
- package/dist/config-schema.d.ts +7 -0
- package/dist/config-schema.d.ts.map +1 -0
- package/dist/config-schema.js +9 -0
- package/dist/config.d.ts +105 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +245 -0
- package/dist/connector-client.d.ts +65 -0
- package/dist/connector-client.d.ts.map +1 -0
- package/dist/connector-client.js +288 -0
- package/dist/constants.d.ts +6 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +5 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/platform-client.d.ts +113 -0
- package/dist/platform-client.d.ts.map +1 -0
- package/dist/platform-client.js +163 -0
- package/dist/plugin.d.ts +27 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +46 -0
- package/dist/tools.d.ts +46 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +301 -0
- package/dist/types.d.ts +66 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +32 -0
- package/openclaw.plugin.json +16 -0
- package/package.json +78 -0
- package/skills/agent-network/SKILL.md +162 -0
- package/skills/agent-network/references/maintenance.md +61 -0
- package/skills/agent-network/references/troubleshooting.md +34 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config & runtime bridge — mediates shared state between channel.ts and tools.ts.
|
|
3
|
+
*
|
|
4
|
+
* Read/write separation architecture:
|
|
5
|
+
* - **Reads**: `initToolConfig(ctx.cfg)` injects Host-provided config into
|
|
6
|
+
* module-level variables. Tools read from these vars via guard functions.
|
|
7
|
+
* - **Writes**: 3 fs functions write to `openclaw.json`, signaling the Host
|
|
8
|
+
* (Gateway monitors file changes for hot-reload / restart).
|
|
9
|
+
* - **Runtime**: `initConnectorClient(client)` injects the live WebSocket
|
|
10
|
+
* client after connection is established. Conversation tools read it via
|
|
11
|
+
* `requireConnectorClient()`.
|
|
12
|
+
*
|
|
13
|
+
* `ctx.cfg` is the Host's authoritative value for this process lifecycle.
|
|
14
|
+
* Plugin does not re-parse the config file for reads.
|
|
15
|
+
*
|
|
16
|
+
*/
|
|
17
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
18
|
+
import { homedir } from "node:os";
|
|
19
|
+
import { dirname, join } from "node:path";
|
|
20
|
+
import { DEFAULT_API_HOST, } from "./platform-client.js";
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Module-level state (lifecycle-scoped — set by initToolConfig, read by tools)
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Initialize with defaults so setup tools work BEFORE startAccount() runs.
|
|
25
|
+
// On first install (no credentials), startAccount() never runs because
|
|
26
|
+
// listAccountIds() returns []. But the LLM setup tools (mstp_setup_init etc.)
|
|
27
|
+
// still need apiHost to call the Platform API. Without this default, tools
|
|
28
|
+
// throw "Channel must be started first" on first run.
|
|
29
|
+
// Config state (from Host's openclaw.json — set by initToolConfig)
|
|
30
|
+
let platformConfig = { apiHost: DEFAULT_API_HOST };
|
|
31
|
+
let storedApiKey = null;
|
|
32
|
+
let storedPendingTarget = null;
|
|
33
|
+
// Runtime state (live connection — set by startAccount, cleared by stopAccount)
|
|
34
|
+
let storedConnectorClient = null;
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Config navigation — shared between channel.ts and config.ts
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
/**
|
|
39
|
+
* Extract the `channels["agent-network"]` section from the full OpenClaw config.
|
|
40
|
+
*
|
|
41
|
+
* OpenClaw Gateway passes the ENTIRE config (all of openclaw.json) to
|
|
42
|
+
* channel adapter methods and startAccount(). This helper navigates to
|
|
43
|
+
* the MSTP-specific section.
|
|
44
|
+
*
|
|
45
|
+
*/
|
|
46
|
+
export function extractMstpConfig(cfg) {
|
|
47
|
+
const channels = cfg.channels;
|
|
48
|
+
if (!channels)
|
|
49
|
+
return null;
|
|
50
|
+
const section = channels["agent-network"];
|
|
51
|
+
if (typeof section !== "object" || section === null)
|
|
52
|
+
return null;
|
|
53
|
+
return section;
|
|
54
|
+
}
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Config injection (called by startAccount() in channel.ts)
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
/**
|
|
59
|
+
* Inject Host-provided config into module-level variables.
|
|
60
|
+
*
|
|
61
|
+
* Called by `startAccount()` each time Gateway starts or hot-reloads.
|
|
62
|
+
* Ensures tools always read the Host's current config values.
|
|
63
|
+
*/
|
|
64
|
+
export function initToolConfig(cfg) {
|
|
65
|
+
const mstpCfg = extractMstpConfig(cfg);
|
|
66
|
+
platformConfig = {
|
|
67
|
+
apiHost: typeof mstpCfg?.apiHost === "string" ? mstpCfg.apiHost : DEFAULT_API_HOST,
|
|
68
|
+
};
|
|
69
|
+
// Extract API key from channels["agent-network"].accounts.default.token
|
|
70
|
+
const accounts = mstpCfg?.accounts;
|
|
71
|
+
const defaultAccount = accounts?.default;
|
|
72
|
+
storedApiKey =
|
|
73
|
+
typeof defaultAccount?.token === "string" ? defaultAccount.token : null;
|
|
74
|
+
storedPendingTarget =
|
|
75
|
+
typeof mstpCfg?.pendingTarget === "string" ? mstpCfg.pendingTarget : null;
|
|
76
|
+
}
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// ConnectorClient injection (called by startAccount() after connect)
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
/**
|
|
81
|
+
* Inject the live ConnectorClient after WebSocket connection is established.
|
|
82
|
+
*
|
|
83
|
+
* Called by `startAccount()` AFTER `client.connect()` resolves — ensures
|
|
84
|
+
* tools never receive a client with a null WebSocket.
|
|
85
|
+
*
|
|
86
|
+
* Phase 1 supports a single account. The double-init guard makes this
|
|
87
|
+
* assumption explicit: a second call without `clearConnectorClient()` throws.
|
|
88
|
+
*/
|
|
89
|
+
export function initConnectorClient(client) {
|
|
90
|
+
if (storedConnectorClient !== null) {
|
|
91
|
+
throw new Error("ConnectorClient already initialized. Multi-account not yet supported.");
|
|
92
|
+
}
|
|
93
|
+
storedConnectorClient = client;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Clear the stored ConnectorClient reference.
|
|
97
|
+
*
|
|
98
|
+
* Called by `stopAccount()` before disconnecting — prevents tools from
|
|
99
|
+
* operating on a disconnected client.
|
|
100
|
+
*/
|
|
101
|
+
export function clearConnectorClient() {
|
|
102
|
+
storedConnectorClient = null;
|
|
103
|
+
}
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Guard functions (used by tools — fail-fast if not initialized)
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
/**
|
|
108
|
+
* Get platform config. Always returns a valid config — defaults are set
|
|
109
|
+
* at module load time, and overwritten by `initToolConfig()` when
|
|
110
|
+
* `startAccount()` runs.
|
|
111
|
+
*
|
|
112
|
+
* This ensures setup tools work on first install (before credentials
|
|
113
|
+
* exist and before `startAccount()` has run).
|
|
114
|
+
*/
|
|
115
|
+
export function requirePlatformConfig() {
|
|
116
|
+
return platformConfig;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Get stored API key. Throws if no credentials are configured.
|
|
120
|
+
*/
|
|
121
|
+
export function requireApiKey() {
|
|
122
|
+
if (!storedApiKey) {
|
|
123
|
+
throw new Error("No API key configured. Complete MSTP setup first.");
|
|
124
|
+
}
|
|
125
|
+
return storedApiKey;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Get pending connection target handle, or null if none.
|
|
129
|
+
*/
|
|
130
|
+
export function getPendingTarget() {
|
|
131
|
+
return storedPendingTarget;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Get the live ConnectorClient. Throws if not initialized.
|
|
135
|
+
*
|
|
136
|
+
* Note: The returned client may be in a reconnecting state (WebSocket
|
|
137
|
+
* temporarily null after a drop). Callers must handle `false` returns
|
|
138
|
+
* from send methods (`sendMessage`, `createSession`, `endSession`).
|
|
139
|
+
*/
|
|
140
|
+
export function requireConnectorClient() {
|
|
141
|
+
if (!storedConnectorClient) {
|
|
142
|
+
throw new Error("Not connected to the agent network. Wait for the connection to be established.");
|
|
143
|
+
}
|
|
144
|
+
return storedConnectorClient;
|
|
145
|
+
}
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Config path resolution
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
function getConfigPath() {
|
|
150
|
+
const base = process.env.OPENCLAW_HOME || join(homedir(), ".openclaw");
|
|
151
|
+
return join(base, "openclaw.json");
|
|
152
|
+
}
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Internal read/write helpers
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
async function readConfig() {
|
|
157
|
+
try {
|
|
158
|
+
const raw = await readFile(getConfigPath(), "utf-8");
|
|
159
|
+
return JSON.parse(raw);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return {}; // File doesn't exist yet
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async function persistConfig(config) {
|
|
166
|
+
const configPath = getConfigPath();
|
|
167
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
168
|
+
await writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Get or create the `channels["agent-network"]` section in config.
|
|
172
|
+
*/
|
|
173
|
+
function ensureMstpSection(config) {
|
|
174
|
+
if (typeof config.channels !== "object" || config.channels === null) {
|
|
175
|
+
config.channels = {};
|
|
176
|
+
}
|
|
177
|
+
const channels = config.channels;
|
|
178
|
+
if (typeof channels["agent-network"] !== "object" ||
|
|
179
|
+
channels["agent-network"] === null) {
|
|
180
|
+
channels["agent-network"] = {};
|
|
181
|
+
}
|
|
182
|
+
return channels["agent-network"];
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Write credentials + apiHost to `openclaw.json`.
|
|
186
|
+
*
|
|
187
|
+
* Atomic: credentials and apiHost are written in a single operation
|
|
188
|
+
* to avoid partial-write race conditions.
|
|
189
|
+
*/
|
|
190
|
+
export async function writeCredentials(creds, apiHost) {
|
|
191
|
+
const config = await readConfig();
|
|
192
|
+
const mstp = ensureMstpSection(config);
|
|
193
|
+
mstp.enabled = true;
|
|
194
|
+
// Write accounts.default
|
|
195
|
+
if (typeof mstp.accounts !== "object" || mstp.accounts === null) {
|
|
196
|
+
mstp.accounts = {};
|
|
197
|
+
}
|
|
198
|
+
const accounts = mstp.accounts;
|
|
199
|
+
accounts.default = {
|
|
200
|
+
connectorUrl: creds.connectorUrl,
|
|
201
|
+
token: creds.token,
|
|
202
|
+
};
|
|
203
|
+
// Write apiHost if provided
|
|
204
|
+
if (apiHost) {
|
|
205
|
+
mstp.apiHost = apiHost;
|
|
206
|
+
}
|
|
207
|
+
await persistConfig(config);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Write pending connection target handle.
|
|
211
|
+
*
|
|
212
|
+
* Reserved for future use: web-initiated connection flows where a target
|
|
213
|
+
* handle is pre-set before the plugin starts.
|
|
214
|
+
*/
|
|
215
|
+
export async function writeTargetHandle(handle) {
|
|
216
|
+
const config = await readConfig();
|
|
217
|
+
const mstp = ensureMstpSection(config);
|
|
218
|
+
mstp.pendingTarget = handle;
|
|
219
|
+
await persistConfig(config);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Clear pending connection target from config file AND module-level variable.
|
|
223
|
+
*
|
|
224
|
+
* This is a deliberate exception to the read/write separation pattern:
|
|
225
|
+
* we also update `storedPendingTarget` in memory to avoid stale reads
|
|
226
|
+
* within the same session (since `initToolConfig()` only runs at startup).
|
|
227
|
+
*/
|
|
228
|
+
export async function clearTargetHandle() {
|
|
229
|
+
const config = await readConfig();
|
|
230
|
+
const mstp = ensureMstpSection(config);
|
|
231
|
+
delete mstp.pendingTarget;
|
|
232
|
+
await persistConfig(config);
|
|
233
|
+
// Update module-level var — deliberate exception to read/write separation
|
|
234
|
+
storedPendingTarget = null;
|
|
235
|
+
}
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// Test-only reset (prefix _ = internal)
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
/** @internal Reset module state for test isolation. */
|
|
240
|
+
export function _resetForTesting() {
|
|
241
|
+
platformConfig = { apiHost: DEFAULT_API_HOST };
|
|
242
|
+
storedApiKey = null;
|
|
243
|
+
storedPendingTarget = null;
|
|
244
|
+
storedConnectorClient = null;
|
|
245
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { type MessageReceivedEvent, type SessionCreatedEvent, type SessionEndedEvent } from "./types.js";
|
|
3
|
+
type ConnectorClientEvents = {
|
|
4
|
+
session_created: (event: SessionCreatedEvent) => void;
|
|
5
|
+
message_received: (event: MessageReceivedEvent) => void;
|
|
6
|
+
session_ended: (event: SessionEndedEvent) => void;
|
|
7
|
+
session_error: (sessionId: string, message: string) => void;
|
|
8
|
+
error: (error: Error) => void;
|
|
9
|
+
connected: () => void;
|
|
10
|
+
disconnected: () => void;
|
|
11
|
+
};
|
|
12
|
+
export declare class ConnectorClient extends EventEmitter {
|
|
13
|
+
private readonly url;
|
|
14
|
+
private readonly token;
|
|
15
|
+
private ws;
|
|
16
|
+
private intentionalClose;
|
|
17
|
+
private backoffMs;
|
|
18
|
+
private alreadyConnectedRetries;
|
|
19
|
+
private reconnectTimer;
|
|
20
|
+
private registerResolve;
|
|
21
|
+
private registerReject;
|
|
22
|
+
private registerTimer;
|
|
23
|
+
constructor(url: string, token: string);
|
|
24
|
+
connect(): Promise<void>;
|
|
25
|
+
disconnect(): void;
|
|
26
|
+
createSession(target: string, opts?: {
|
|
27
|
+
secret?: string;
|
|
28
|
+
metadata?: Record<string, unknown>;
|
|
29
|
+
}): {
|
|
30
|
+
requestId: string;
|
|
31
|
+
sent: boolean;
|
|
32
|
+
};
|
|
33
|
+
sendMessage(sessionId: string, content: string, opts?: {
|
|
34
|
+
contentType?: string;
|
|
35
|
+
metadata?: Record<string, unknown>;
|
|
36
|
+
}): boolean;
|
|
37
|
+
endSession(sessionId: string, reason?: string): boolean;
|
|
38
|
+
on<K extends keyof ConnectorClientEvents>(event: K, listener: ConnectorClientEvents[K]): this;
|
|
39
|
+
on(event: string | symbol, listener: (...args: unknown[]) => void): this;
|
|
40
|
+
once<K extends keyof ConnectorClientEvents>(event: K, listener: ConnectorClientEvents[K]): this;
|
|
41
|
+
once(event: string | symbol, listener: (...args: unknown[]) => void): this;
|
|
42
|
+
off<K extends keyof ConnectorClientEvents>(event: K, listener: ConnectorClientEvents[K]): this;
|
|
43
|
+
off(event: string | symbol, listener: (...args: unknown[]) => void): this;
|
|
44
|
+
emit<K extends keyof ConnectorClientEvents>(event: K, ...args: Parameters<ConnectorClientEvents[K]>): boolean;
|
|
45
|
+
emit(event: string | symbol, ...args: unknown[]): boolean;
|
|
46
|
+
private doConnect;
|
|
47
|
+
private sendRegister;
|
|
48
|
+
private startRegisterTimeout;
|
|
49
|
+
private resolveRegister;
|
|
50
|
+
private rejectRegister;
|
|
51
|
+
private handleMessage;
|
|
52
|
+
private handleRegisterAck;
|
|
53
|
+
private handleSessionCreated;
|
|
54
|
+
private handleErrorEvent;
|
|
55
|
+
private handleClose;
|
|
56
|
+
private handleError;
|
|
57
|
+
private scheduleReconnect;
|
|
58
|
+
private calculateBackoff;
|
|
59
|
+
private nextExponentialBackoff;
|
|
60
|
+
private clearReconnectTimer;
|
|
61
|
+
private cleanupConnection;
|
|
62
|
+
private send;
|
|
63
|
+
}
|
|
64
|
+
export {};
|
|
65
|
+
//# sourceMappingURL=connector-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connector-client.d.ts","sourceRoot":"","sources":["../src/connector-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import WebSocket from "ws";
|
|
4
|
+
import { CURRENT_PROTOCOL_VERSION, isErrorEvent, isMessageReceived, isRegisterAck, isSessionCreated, isSessionEnded, REGISTER_ACK_TIMEOUT_MS, } from "./types.js";
|
|
5
|
+
// --- Reconnection constants ---
|
|
6
|
+
const BACKOFF_INITIAL_MS = 1_000;
|
|
7
|
+
const BACKOFF_MAX_MS = 30_000;
|
|
8
|
+
const ALREADY_CONNECTED_RETRY_MS = 2_000;
|
|
9
|
+
const ALREADY_CONNECTED_MAX_RETRIES = 30;
|
|
10
|
+
// --- ConnectorClient ---
|
|
11
|
+
export class ConnectorClient extends EventEmitter {
|
|
12
|
+
url;
|
|
13
|
+
token;
|
|
14
|
+
ws = null;
|
|
15
|
+
intentionalClose = false;
|
|
16
|
+
backoffMs = BACKOFF_INITIAL_MS;
|
|
17
|
+
alreadyConnectedRetries = 0;
|
|
18
|
+
reconnectTimer = null;
|
|
19
|
+
registerResolve = null;
|
|
20
|
+
registerReject = null;
|
|
21
|
+
registerTimer = null;
|
|
22
|
+
constructor(url, token) {
|
|
23
|
+
super();
|
|
24
|
+
this.url = url;
|
|
25
|
+
this.token = token;
|
|
26
|
+
}
|
|
27
|
+
// --- Public API ---
|
|
28
|
+
connect() {
|
|
29
|
+
if (this.ws) {
|
|
30
|
+
return Promise.reject(new Error("Already connecting or connected"));
|
|
31
|
+
}
|
|
32
|
+
this.intentionalClose = false;
|
|
33
|
+
return this.doConnect();
|
|
34
|
+
}
|
|
35
|
+
disconnect() {
|
|
36
|
+
this.intentionalClose = true;
|
|
37
|
+
this.clearReconnectTimer();
|
|
38
|
+
const wsToClose = this.ws;
|
|
39
|
+
this.cleanupConnection();
|
|
40
|
+
if (wsToClose &&
|
|
41
|
+
(wsToClose.readyState === WebSocket.OPEN ||
|
|
42
|
+
wsToClose.readyState === WebSocket.CONNECTING)) {
|
|
43
|
+
wsToClose.close(1000, "Client disconnect");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
createSession(target, opts) {
|
|
47
|
+
const requestId = randomUUID();
|
|
48
|
+
const event = {
|
|
49
|
+
event: "CREATE_SESSION",
|
|
50
|
+
requestId,
|
|
51
|
+
target,
|
|
52
|
+
secret: opts?.secret,
|
|
53
|
+
metadata: opts?.metadata,
|
|
54
|
+
};
|
|
55
|
+
const sent = this.send(event);
|
|
56
|
+
return { requestId, sent };
|
|
57
|
+
}
|
|
58
|
+
sendMessage(sessionId, content, opts) {
|
|
59
|
+
const event = {
|
|
60
|
+
event: "SEND_MESSAGE",
|
|
61
|
+
sessionId,
|
|
62
|
+
content,
|
|
63
|
+
contentType: opts?.contentType,
|
|
64
|
+
metadata: opts?.metadata,
|
|
65
|
+
};
|
|
66
|
+
return this.send(event);
|
|
67
|
+
}
|
|
68
|
+
endSession(sessionId, reason) {
|
|
69
|
+
const event = {
|
|
70
|
+
event: "END_SESSION",
|
|
71
|
+
sessionId,
|
|
72
|
+
reason,
|
|
73
|
+
};
|
|
74
|
+
return this.send(event);
|
|
75
|
+
}
|
|
76
|
+
on(event, listener) {
|
|
77
|
+
return super.on(event, listener);
|
|
78
|
+
}
|
|
79
|
+
once(event, listener) {
|
|
80
|
+
return super.once(event, listener);
|
|
81
|
+
}
|
|
82
|
+
off(event, listener) {
|
|
83
|
+
return super.off(event, listener);
|
|
84
|
+
}
|
|
85
|
+
emit(event, ...args) {
|
|
86
|
+
return super.emit(event, ...args);
|
|
87
|
+
}
|
|
88
|
+
// --- Connection lifecycle ---
|
|
89
|
+
doConnect() {
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
this.registerResolve = resolve;
|
|
92
|
+
this.registerReject = reject;
|
|
93
|
+
const ws = new WebSocket(this.url, { handshakeTimeout: 10_000 });
|
|
94
|
+
this.ws = ws;
|
|
95
|
+
ws.on("open", () => {
|
|
96
|
+
this.sendRegister();
|
|
97
|
+
this.startRegisterTimeout();
|
|
98
|
+
});
|
|
99
|
+
ws.on("message", this.handleMessage);
|
|
100
|
+
ws.on("close", this.handleClose);
|
|
101
|
+
ws.on("error", this.handleError);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
sendRegister() {
|
|
105
|
+
const event = {
|
|
106
|
+
event: "REGISTER",
|
|
107
|
+
token: this.token,
|
|
108
|
+
protocolVersion: CURRENT_PROTOCOL_VERSION,
|
|
109
|
+
};
|
|
110
|
+
this.send(event);
|
|
111
|
+
}
|
|
112
|
+
startRegisterTimeout() {
|
|
113
|
+
this.registerTimer = setTimeout(() => {
|
|
114
|
+
this.rejectRegister(new Error("REGISTER_ACK timeout"));
|
|
115
|
+
this.ws?.close(4000, "Register timeout");
|
|
116
|
+
}, REGISTER_ACK_TIMEOUT_MS);
|
|
117
|
+
}
|
|
118
|
+
resolveRegister() {
|
|
119
|
+
if (this.registerTimer) {
|
|
120
|
+
clearTimeout(this.registerTimer);
|
|
121
|
+
this.registerTimer = null;
|
|
122
|
+
}
|
|
123
|
+
if (this.registerResolve) {
|
|
124
|
+
this.registerResolve();
|
|
125
|
+
this.registerResolve = null;
|
|
126
|
+
this.registerReject = null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
rejectRegister(err) {
|
|
130
|
+
if (this.registerTimer) {
|
|
131
|
+
clearTimeout(this.registerTimer);
|
|
132
|
+
this.registerTimer = null;
|
|
133
|
+
}
|
|
134
|
+
if (this.registerReject) {
|
|
135
|
+
this.registerReject(err);
|
|
136
|
+
this.registerResolve = null;
|
|
137
|
+
this.registerReject = null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// --- Message handling ---
|
|
141
|
+
handleMessage = (data) => {
|
|
142
|
+
let parsed;
|
|
143
|
+
try {
|
|
144
|
+
parsed = JSON.parse(data.toString());
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
this.emit("error", new Error("Received non-JSON message from Connector"));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (isRegisterAck(parsed)) {
|
|
151
|
+
this.handleRegisterAck(parsed);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (isSessionCreated(parsed)) {
|
|
155
|
+
this.handleSessionCreated(parsed);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (isMessageReceived(parsed)) {
|
|
159
|
+
this.emit("message_received", parsed);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (isSessionEnded(parsed)) {
|
|
163
|
+
this.emit("session_ended", parsed);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (isErrorEvent(parsed)) {
|
|
167
|
+
this.handleErrorEvent(parsed);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
// Unknown event type — silently ignore (forward compatibility)
|
|
171
|
+
};
|
|
172
|
+
handleRegisterAck(ack) {
|
|
173
|
+
if (ack.status === "ok") {
|
|
174
|
+
this.backoffMs = BACKOFF_INITIAL_MS;
|
|
175
|
+
this.alreadyConnectedRetries = 0;
|
|
176
|
+
this.resolveRegister();
|
|
177
|
+
this.emit("connected");
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
const reason = ack.reason ?? "Unknown registration error";
|
|
181
|
+
// "Unsupported protocol version" — do not reconnect
|
|
182
|
+
if (reason === "Unsupported protocol version") {
|
|
183
|
+
this.intentionalClose = true;
|
|
184
|
+
this.rejectRegister(new Error(reason));
|
|
185
|
+
this.emit("error", new Error(reason));
|
|
186
|
+
this.ws?.close(4001, reason);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// "Already connected" — track retries for short backoff
|
|
190
|
+
if (reason === "Already connected") {
|
|
191
|
+
this.alreadyConnectedRetries++;
|
|
192
|
+
}
|
|
193
|
+
this.rejectRegister(new Error(reason));
|
|
194
|
+
this.ws?.close(4001, reason);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
handleSessionCreated(event) {
|
|
198
|
+
this.emit("session_created", event);
|
|
199
|
+
}
|
|
200
|
+
handleErrorEvent(event) {
|
|
201
|
+
// (1) Route by sessionId — session-scoped error
|
|
202
|
+
if (event.sessionId) {
|
|
203
|
+
this.emit("session_error", event.sessionId, event.message);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// (2) Global error (includes requestId-only errors from failed CREATE_SESSION)
|
|
207
|
+
this.emit("error", new Error(event.message));
|
|
208
|
+
}
|
|
209
|
+
// --- Connection close ---
|
|
210
|
+
handleClose = () => {
|
|
211
|
+
const wasRegistering = this.registerReject !== null;
|
|
212
|
+
this.cleanupConnection();
|
|
213
|
+
if (wasRegistering) {
|
|
214
|
+
// Connection closed during REGISTER — check for special retry logic
|
|
215
|
+
// (handled by rejectRegister which was already called if ACK error came first)
|
|
216
|
+
if (this.registerReject) {
|
|
217
|
+
this.rejectRegister(new Error("Connection closed during registration"));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// "disconnected" is emitted on every close, including intermediate closes
|
|
221
|
+
// during reconnect cycles. This is intentional — channel adapter uses this
|
|
222
|
+
// to clear stale session state before a fresh REGISTER re-creates sessions.
|
|
223
|
+
this.emit("disconnected");
|
|
224
|
+
if (!this.intentionalClose) {
|
|
225
|
+
this.scheduleReconnect();
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
handleError = (err) => {
|
|
229
|
+
// WebSocket errors are followed by a close event, so just emit
|
|
230
|
+
this.emit("error", err);
|
|
231
|
+
};
|
|
232
|
+
// --- Reconnection ---
|
|
233
|
+
scheduleReconnect() {
|
|
234
|
+
this.clearReconnectTimer();
|
|
235
|
+
const delay = this.calculateBackoff();
|
|
236
|
+
this.reconnectTimer = setTimeout(() => {
|
|
237
|
+
this.reconnectTimer = null;
|
|
238
|
+
this.doConnect().catch(() => {
|
|
239
|
+
// connect rejection is expected during reconnect — handled by close event
|
|
240
|
+
});
|
|
241
|
+
}, delay);
|
|
242
|
+
}
|
|
243
|
+
calculateBackoff() {
|
|
244
|
+
// "Already connected" — short fixed retry with limit
|
|
245
|
+
if (this.alreadyConnectedRetries > 0) {
|
|
246
|
+
if (this.alreadyConnectedRetries >= ALREADY_CONNECTED_MAX_RETRIES) {
|
|
247
|
+
// Exceeded max retries, fall back to normal exponential backoff
|
|
248
|
+
this.alreadyConnectedRetries = 0;
|
|
249
|
+
return this.nextExponentialBackoff();
|
|
250
|
+
}
|
|
251
|
+
return ALREADY_CONNECTED_RETRY_MS;
|
|
252
|
+
}
|
|
253
|
+
return this.nextExponentialBackoff();
|
|
254
|
+
}
|
|
255
|
+
nextExponentialBackoff() {
|
|
256
|
+
const jitter = 1 + Math.random() * 0.3;
|
|
257
|
+
const delay = Math.min(this.backoffMs * jitter, BACKOFF_MAX_MS);
|
|
258
|
+
this.backoffMs = Math.min(this.backoffMs * 2, BACKOFF_MAX_MS);
|
|
259
|
+
return delay;
|
|
260
|
+
}
|
|
261
|
+
clearReconnectTimer() {
|
|
262
|
+
if (this.reconnectTimer) {
|
|
263
|
+
clearTimeout(this.reconnectTimer);
|
|
264
|
+
this.reconnectTimer = null;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// --- Cleanup ---
|
|
268
|
+
cleanupConnection() {
|
|
269
|
+
if (this.ws) {
|
|
270
|
+
this.ws.off("message", this.handleMessage);
|
|
271
|
+
this.ws.off("close", this.handleClose);
|
|
272
|
+
this.ws.off("error", this.handleError);
|
|
273
|
+
this.ws = null;
|
|
274
|
+
}
|
|
275
|
+
if (this.registerTimer) {
|
|
276
|
+
clearTimeout(this.registerTimer);
|
|
277
|
+
this.registerTimer = null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// --- Helpers ---
|
|
281
|
+
send(event) {
|
|
282
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
283
|
+
this.ws.send(JSON.stringify(event));
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Unambiguous characters: A-H J-N P-Z 2-9 (excludes I/O/1/0) */
|
|
2
|
+
export declare const SETUP_CODE_CHARSET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
3
|
+
export declare const SETUP_CODE_LENGTH = 8;
|
|
4
|
+
export declare const SETUP_CODE_TTL_MS: number;
|
|
5
|
+
export declare const AUTHORIZED_TOKEN_TTL_MS: number;
|
|
6
|
+
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,eAAO,MAAM,kBAAkB,qCAAqC,CAAC;AACrE,eAAO,MAAM,iBAAiB,IAAI,CAAC;AACnC,eAAO,MAAM,iBAAiB,QAAiB,CAAC;AAChD,eAAO,MAAM,uBAAuB,QAAiB,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Unambiguous characters: A-H J-N P-Z 2-9 (excludes I/O/1/0) */
|
|
2
|
+
export const SETUP_CODE_CHARSET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
3
|
+
export const SETUP_CODE_LENGTH = 8;
|
|
4
|
+
export const SETUP_CODE_TTL_MS = 15 * 60 * 1000; // 15 minutes
|
|
5
|
+
export const AUTHORIZED_TOKEN_TTL_MS = 60 * 60 * 1000; // 1 hour
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { AUTHORIZED_TOKEN_TTL_MS, SETUP_CODE_CHARSET, SETUP_CODE_LENGTH, SETUP_CODE_TTL_MS, } from "./constants.js";
|
|
2
|
+
export type { MstpAccount } from "./config-schema.js";
|
|
3
|
+
export type { ErrorEvent, MessageReceivedEvent, RegisterAckEvent, SessionCreatedEvent, SessionEndedEvent, } from "./types.js";
|
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAWA,OAAO,EACL,uBAAuB,EACvB,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,gBAAgB,CAAC;AAIxB,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,YAAY,EACV,UAAU,EACV,oBAAoB,EACpB,gBAAgB,EAChB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Main entry point for @masons/agent-network.
|
|
2
|
+
//
|
|
3
|
+
// This file MUST NOT import modules that depend on Node.js-only packages (ws,
|
|
4
|
+
// etc.) because some consumers import from here in Next.js builds that include
|
|
5
|
+
// client bundles.
|
|
6
|
+
//
|
|
7
|
+
// The OpenClaw Plugin entry is in ./plugin.ts (loaded via openclaw.extensions).
|
|
8
|
+
// The ConnectorClient and protocol types are available via dedicated sub-exports.
|
|
9
|
+
// --- Shared constants ---
|
|
10
|
+
export { AUTHORIZED_TOKEN_TTL_MS, SETUP_CODE_CHARSET, SETUP_CODE_LENGTH, SETUP_CODE_TTL_MS, } from "./constants.js";
|