@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/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
@@ -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";