@clawchatsai/connector 0.0.1

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/shim.d.ts ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * HTTP Shim — translates DataChannel RPC messages into fake req/res
3
+ * objects compatible with server.js handleRequest().
4
+ *
5
+ * Per spec section 6.3.1:
6
+ * - Fake req: Readable stream with .url, .method, .headers
7
+ * - Fake res: Writable stream (required for pipe() in handleServeFile)
8
+ */
9
+ import { Readable, Writable } from 'node:stream';
10
+ import type { IncomingHttpHeaders } from 'node:http';
11
+ export interface RpcRequest {
12
+ id: string;
13
+ method: string;
14
+ url: string;
15
+ headers?: Record<string, string>;
16
+ body?: string;
17
+ }
18
+ export interface RpcResponse {
19
+ id: string;
20
+ status: number;
21
+ headers: Record<string, string>;
22
+ body: string;
23
+ }
24
+ type HandleRequestFn = (req: FakeReq, res: FakeRes) => void | Promise<void>;
25
+ /**
26
+ * Fake IncomingMessage — a Readable stream with HTTP-like properties.
27
+ */
28
+ declare class FakeReq extends Readable {
29
+ url: string;
30
+ method: string;
31
+ headers: IncomingHttpHeaders;
32
+ constructor(rpc: RpcRequest);
33
+ _read(): void;
34
+ }
35
+ /**
36
+ * Fake ServerResponse — a Writable stream that buffers output.
37
+ *
38
+ * Extends Writable so that fs.createReadStream(...).pipe(res) works
39
+ * (handleServeFile uses pipe()).
40
+ */
41
+ declare class FakeRes extends Writable {
42
+ statusCode: number;
43
+ private _headers;
44
+ private _chunks;
45
+ private _resolvePromise;
46
+ readonly finished: Promise<{
47
+ status: number;
48
+ headers: Record<string, string>;
49
+ body: Buffer;
50
+ }>;
51
+ constructor();
52
+ _write(chunk: Buffer | string, _encoding: string, callback: (error?: Error | null) => void): void;
53
+ setHeader(name: string, value: string | number): void;
54
+ writeHead(statusCode: number, headers?: Record<string, string | number>): this;
55
+ end(chunk?: unknown, encoding?: unknown, _cb?: unknown): this;
56
+ }
57
+ /**
58
+ * Dispatch an RPC request through handleRequest and return the response.
59
+ */
60
+ export declare function dispatchRpc(rpc: RpcRequest, handleRequest: HandleRequestFn): Promise<RpcResponse>;
61
+ export {};
package/dist/shim.js ADDED
@@ -0,0 +1,154 @@
1
+ /**
2
+ * HTTP Shim — translates DataChannel RPC messages into fake req/res
3
+ * objects compatible with server.js handleRequest().
4
+ *
5
+ * Per spec section 6.3.1:
6
+ * - Fake req: Readable stream with .url, .method, .headers
7
+ * - Fake res: Writable stream (required for pipe() in handleServeFile)
8
+ */
9
+ import { Readable, Writable } from 'node:stream';
10
+ /**
11
+ * Fake IncomingMessage — a Readable stream with HTTP-like properties.
12
+ */
13
+ class FakeReq extends Readable {
14
+ url;
15
+ method;
16
+ headers;
17
+ constructor(rpc) {
18
+ super();
19
+ this.url = rpc.url;
20
+ this.method = rpc.method;
21
+ // Lowercase header keys to match Node's IncomingHttpHeaders convention.
22
+ // Browsers send 'Authorization' but Node expects 'authorization'.
23
+ const raw = rpc.headers ?? {};
24
+ const lowered = {};
25
+ for (const [k, v] of Object.entries(raw))
26
+ lowered[k.toLowerCase()] = v;
27
+ this.headers = lowered;
28
+ // Push body (if any) and signal end — reconstruct binary data from
29
+ // _blob / _multipart envelope sent by the browser's transport.js
30
+ if (rpc.body) {
31
+ let parsed = null;
32
+ try {
33
+ parsed = JSON.parse(rpc.body);
34
+ }
35
+ catch { /* not JSON — treat as raw string */ }
36
+ if (parsed && parsed['_blob']) {
37
+ // Binary blob: { _blob: true, contentType: "audio/webm", data: "<base64>" }
38
+ const buf = Buffer.from(parsed['data'], 'base64');
39
+ this.headers['content-type'] = parsed['contentType'] || 'application/octet-stream';
40
+ this.headers['content-length'] = String(buf.length);
41
+ this.push(buf);
42
+ }
43
+ else if (parsed && parsed['_text']) {
44
+ // Raw text string (e.g. file content) — push as-is without JSON wrapping
45
+ this.push(parsed['data']);
46
+ }
47
+ else if (parsed && parsed['_multipart']) {
48
+ // Multipart form data: { _multipart: true, fields: { key: string | { filename, contentType, data } } }
49
+ const boundary = '----ShellChatBoundary' + Date.now();
50
+ this.headers['content-type'] = `multipart/form-data; boundary=${boundary}`;
51
+ const fields = parsed['fields'] || {};
52
+ const parts = [];
53
+ for (const [key, value] of Object.entries(fields)) {
54
+ if (value && typeof value === 'object' && value['filename']) {
55
+ const f = value;
56
+ parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="${key}"; filename="${f.filename}"\r\nContent-Type: ${f.contentType}\r\n\r\n`));
57
+ parts.push(Buffer.from(f.data, 'base64'));
58
+ parts.push(Buffer.from('\r\n'));
59
+ }
60
+ else {
61
+ parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="${key}"\r\n\r\n${String(value)}\r\n`));
62
+ }
63
+ }
64
+ parts.push(Buffer.from(`--${boundary}--\r\n`));
65
+ const multipartBuf = Buffer.concat(parts);
66
+ this.headers['content-length'] = String(multipartBuf.length);
67
+ this.push(multipartBuf);
68
+ }
69
+ else {
70
+ this.push(rpc.body);
71
+ }
72
+ }
73
+ this.push(null);
74
+ }
75
+ _read() {
76
+ // No-op — data already pushed in constructor
77
+ }
78
+ }
79
+ /**
80
+ * Fake ServerResponse — a Writable stream that buffers output.
81
+ *
82
+ * Extends Writable so that fs.createReadStream(...).pipe(res) works
83
+ * (handleServeFile uses pipe()).
84
+ */
85
+ class FakeRes extends Writable {
86
+ statusCode = 200;
87
+ _headers = {};
88
+ _chunks = [];
89
+ _resolvePromise = null;
90
+ finished;
91
+ constructor() {
92
+ super();
93
+ this.finished = new Promise((resolve) => {
94
+ this._resolvePromise = resolve;
95
+ });
96
+ // When the Writable stream finishes, resolve the promise
97
+ this.on('finish', () => {
98
+ this._resolvePromise?.({
99
+ status: this.statusCode,
100
+ headers: { ...this._headers },
101
+ body: Buffer.concat(this._chunks),
102
+ });
103
+ });
104
+ }
105
+ // Required by Writable
106
+ _write(chunk, _encoding, callback) {
107
+ this._chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
108
+ callback();
109
+ }
110
+ setHeader(name, value) {
111
+ this._headers[name.toLowerCase()] = String(value);
112
+ }
113
+ writeHead(statusCode, headers) {
114
+ this.statusCode = statusCode;
115
+ if (headers) {
116
+ for (const [k, v] of Object.entries(headers)) {
117
+ this._headers[k.toLowerCase()] = String(v);
118
+ }
119
+ }
120
+ return this;
121
+ }
122
+ // Override end() to handle the (data, encoding) signature used by send()
123
+ end(chunk, encoding, _cb) {
124
+ if (chunk != null && typeof chunk !== 'function') {
125
+ this._chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
126
+ }
127
+ // Signal Writable stream finish
128
+ super.end();
129
+ return this;
130
+ }
131
+ }
132
+ function tryJsonParse(buf) {
133
+ try {
134
+ return buf.toString('utf8');
135
+ }
136
+ catch {
137
+ return buf.toString('base64');
138
+ }
139
+ }
140
+ /**
141
+ * Dispatch an RPC request through handleRequest and return the response.
142
+ */
143
+ export async function dispatchRpc(rpc, handleRequest) {
144
+ const req = new FakeReq(rpc);
145
+ const res = new FakeRes();
146
+ await handleRequest(req, res);
147
+ const result = await res.finished;
148
+ return {
149
+ id: rpc.id,
150
+ status: result.status,
151
+ headers: result.headers,
152
+ body: tryJsonParse(result.body),
153
+ };
154
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * SignalingClient — persistent WSS connection to the ShellChat signaling server.
3
+ *
4
+ * Responsibilities:
5
+ * - Authenticate with the signaling server on connect (gateway-auth)
6
+ * - Relay ICE offers/answers for WebRTC negotiation
7
+ * - Report active DataChannel connection count
8
+ * - Handle server push messages (force-update, account-suspended, device-limit-updated)
9
+ * - Reconnect with exponential backoff on unintentional disconnects
10
+ *
11
+ * Per spec: signaling server sends WS pings every 30s; the `ws` library
12
+ * handles pong automatically. If no ping is received for 90s (3 missed),
13
+ * the connection is considered dead and we reconnect.
14
+ */
15
+ import { EventEmitter } from 'node:events';
16
+ export declare class SignalingClient extends EventEmitter {
17
+ private readonly serverUrl;
18
+ private readonly userId;
19
+ private readonly apiKey;
20
+ private ws;
21
+ /** True only after gateway-auth-ok has been received. */
22
+ private _connected;
23
+ /**
24
+ * Set to true by disconnect() or after an auth rejection.
25
+ * Prevents reconnect loops when the close is intentional.
26
+ */
27
+ private intentionalClose;
28
+ /** Number of consecutive reconnect attempts (resets on successful auth). */
29
+ private reconnectAttempts;
30
+ /** Timer handle for the scheduled reconnect. */
31
+ private reconnectTimer;
32
+ /** Timer handle for ping-timeout watchdog. */
33
+ private pingWatchdog;
34
+ constructor(serverUrl: string, userId: string, apiKey: string);
35
+ /** Returns true when gateway-auth-ok has been received on the current socket. */
36
+ get isConnected(): boolean;
37
+ /**
38
+ * Open the WebSocket connection and perform the gateway-auth handshake.
39
+ * Resolves once the socket is open (not necessarily authenticated yet).
40
+ * Authentication outcome is signalled via 'connected' / 'auth-rejected' /
41
+ * 'version-rejected' events.
42
+ */
43
+ connect(): Promise<void>;
44
+ /**
45
+ * Intentionally close the connection. Suppresses reconnection.
46
+ */
47
+ disconnect(): void;
48
+ /**
49
+ * Send a connection-count report to the signaling server.
50
+ * Call whenever a DataChannel opens or closes.
51
+ */
52
+ reportConnectionCount(active: number): void;
53
+ /**
54
+ * Send an ICE answer back to the signaling server in response to an
55
+ * ice-offer that was forwarded to us by the server.
56
+ */
57
+ sendIceAnswer(connectionId: string, sdp: string, candidates: unknown[]): void;
58
+ /**
59
+ * Send a trickle ICE candidate from the plugin to a browser via signaling.
60
+ */
61
+ sendIceCandidate(connectionId: string, candidate: unknown): void;
62
+ private _openSocket;
63
+ private _handleMessage;
64
+ private _send;
65
+ private _scheduleReconnect;
66
+ private _clearReconnectTimer;
67
+ /**
68
+ * Restart the 90-second watchdog timer. Called on gateway-auth-ok and on
69
+ * every received ping frame. If the timer fires, the connection is dead
70
+ * and we force a reconnect.
71
+ */
72
+ private _resetPingWatchdog;
73
+ private _clearPingWatchdog;
74
+ }
@@ -0,0 +1,322 @@
1
+ /**
2
+ * SignalingClient — persistent WSS connection to the ShellChat signaling server.
3
+ *
4
+ * Responsibilities:
5
+ * - Authenticate with the signaling server on connect (gateway-auth)
6
+ * - Relay ICE offers/answers for WebRTC negotiation
7
+ * - Report active DataChannel connection count
8
+ * - Handle server push messages (force-update, account-suspended, device-limit-updated)
9
+ * - Reconnect with exponential backoff on unintentional disconnects
10
+ *
11
+ * Per spec: signaling server sends WS pings every 30s; the `ws` library
12
+ * handles pong automatically. If no ping is received for 90s (3 missed),
13
+ * the connection is considered dead and we reconnect.
14
+ */
15
+ import { EventEmitter } from 'node:events';
16
+ import { WebSocket } from 'ws';
17
+ import { PLUGIN_VERSION } from './index.js';
18
+ // ---------------------------------------------------------------------------
19
+ // Constants
20
+ // ---------------------------------------------------------------------------
21
+ /** Backoff delays in ms: 1s, 2s, 4s, 8s, 16s, capped at 30s */
22
+ const BACKOFF_BASE_MS = 1_000;
23
+ const BACKOFF_MAX_MS = 30_000;
24
+ /**
25
+ * How long to wait without receiving a WS ping before declaring the
26
+ * connection dead. Signaling server pings every 30s; three missed = 90s.
27
+ */
28
+ const PING_TIMEOUT_MS = 90_000;
29
+ // ---------------------------------------------------------------------------
30
+ // SignalingClient
31
+ // ---------------------------------------------------------------------------
32
+ export class SignalingClient extends EventEmitter {
33
+ serverUrl;
34
+ userId;
35
+ apiKey;
36
+ ws = null;
37
+ /** True only after gateway-auth-ok has been received. */
38
+ _connected = false;
39
+ /**
40
+ * Set to true by disconnect() or after an auth rejection.
41
+ * Prevents reconnect loops when the close is intentional.
42
+ */
43
+ intentionalClose = false;
44
+ /** Number of consecutive reconnect attempts (resets on successful auth). */
45
+ reconnectAttempts = 0;
46
+ /** Timer handle for the scheduled reconnect. */
47
+ reconnectTimer = null;
48
+ /** Timer handle for ping-timeout watchdog. */
49
+ pingWatchdog = null;
50
+ constructor(serverUrl, userId, apiKey) {
51
+ super();
52
+ this.serverUrl = serverUrl;
53
+ this.userId = userId;
54
+ this.apiKey = apiKey;
55
+ }
56
+ // -------------------------------------------------------------------------
57
+ // Public API
58
+ // -------------------------------------------------------------------------
59
+ /** Returns true when gateway-auth-ok has been received on the current socket. */
60
+ get isConnected() {
61
+ return this._connected;
62
+ }
63
+ /**
64
+ * Open the WebSocket connection and perform the gateway-auth handshake.
65
+ * Resolves once the socket is open (not necessarily authenticated yet).
66
+ * Authentication outcome is signalled via 'connected' / 'auth-rejected' /
67
+ * 'version-rejected' events.
68
+ */
69
+ connect() {
70
+ this.intentionalClose = false;
71
+ return this._openSocket();
72
+ }
73
+ /**
74
+ * Intentionally close the connection. Suppresses reconnection.
75
+ */
76
+ disconnect() {
77
+ this.intentionalClose = true;
78
+ this._clearReconnectTimer();
79
+ this._clearPingWatchdog();
80
+ this._connected = false;
81
+ if (this.ws) {
82
+ this.ws.close();
83
+ this.ws = null;
84
+ }
85
+ }
86
+ /**
87
+ * Send a connection-count report to the signaling server.
88
+ * Call whenever a DataChannel opens or closes.
89
+ */
90
+ reportConnectionCount(active) {
91
+ this._send({ type: 'connection-count', active });
92
+ }
93
+ /**
94
+ * Send an ICE answer back to the signaling server in response to an
95
+ * ice-offer that was forwarded to us by the server.
96
+ */
97
+ sendIceAnswer(connectionId, sdp, candidates) {
98
+ this._send({ type: 'ice-answer', connectionId, sdp, candidates });
99
+ }
100
+ /**
101
+ * Send a trickle ICE candidate from the plugin to a browser via signaling.
102
+ */
103
+ sendIceCandidate(connectionId, candidate) {
104
+ this._send({ type: 'ice-candidate', connectionId, candidate });
105
+ }
106
+ // -------------------------------------------------------------------------
107
+ // Internal — socket lifecycle
108
+ // -------------------------------------------------------------------------
109
+ _openSocket() {
110
+ return new Promise((resolve, reject) => {
111
+ // Guard: never open two sockets simultaneously
112
+ if (this.ws) {
113
+ this.ws.removeAllListeners();
114
+ this.ws.close();
115
+ this.ws = null;
116
+ }
117
+ let settled = false;
118
+ const settle = (err) => {
119
+ if (settled)
120
+ return;
121
+ settled = true;
122
+ if (err)
123
+ reject(err);
124
+ else
125
+ resolve();
126
+ };
127
+ const socket = new WebSocket(this.serverUrl);
128
+ this.ws = socket;
129
+ socket.on('open', () => {
130
+ // Send gateway-auth as the first message
131
+ this._send({
132
+ type: 'gateway-auth',
133
+ userId: this.userId,
134
+ apiKey: this.apiKey,
135
+ pluginVersion: PLUGIN_VERSION,
136
+ });
137
+ // Resolve the connect() promise: the socket is open and auth is in flight
138
+ settle();
139
+ });
140
+ socket.on('ping', () => {
141
+ // ws library handles sending the pong automatically.
142
+ // We just need to reset our watchdog timer.
143
+ this._resetPingWatchdog();
144
+ });
145
+ socket.on('message', (raw) => {
146
+ this._handleMessage(raw);
147
+ });
148
+ socket.on('error', (err) => {
149
+ // Reject connect() if we haven't resolved yet; otherwise log only
150
+ if (!settled) {
151
+ settle(err);
152
+ }
153
+ // The 'close' event will fire after 'error', so reconnection is
154
+ // handled there — no duplicate reconnect scheduling needed here.
155
+ });
156
+ socket.on('close', (_code, _reason) => {
157
+ this._connected = false;
158
+ this._clearPingWatchdog();
159
+ this.ws = null;
160
+ this.emit('disconnected');
161
+ if (!settled) {
162
+ // connect() is still pending — reject it
163
+ settle(new Error('WebSocket closed before open'));
164
+ return;
165
+ }
166
+ if (!this.intentionalClose) {
167
+ this._scheduleReconnect();
168
+ }
169
+ });
170
+ });
171
+ }
172
+ // -------------------------------------------------------------------------
173
+ // Internal — message handling
174
+ // -------------------------------------------------------------------------
175
+ _handleMessage(raw) {
176
+ let msg;
177
+ try {
178
+ msg = JSON.parse(raw.toString());
179
+ }
180
+ catch {
181
+ console.error('[SignalingClient] Received non-JSON message, ignoring');
182
+ return;
183
+ }
184
+ const type = msg['type'];
185
+ switch (type) {
186
+ case 'gateway-auth-ok': {
187
+ this._connected = true;
188
+ this.reconnectAttempts = 0;
189
+ this._resetPingWatchdog();
190
+ this.emit('connected');
191
+ break;
192
+ }
193
+ case 'gateway-auth-rejected': {
194
+ const reason = msg['reason'] ?? 'unknown';
195
+ console.error(`[SignalingClient] Auth rejected: ${reason}`);
196
+ // Do not reconnect after auth rejection — the API key is invalid
197
+ this.intentionalClose = true;
198
+ this.emit('auth-rejected', reason);
199
+ break;
200
+ }
201
+ case 'version-rejected': {
202
+ const current = msg['current'] ?? PLUGIN_VERSION;
203
+ const minimum = msg['minimum'] ?? '';
204
+ console.error(`[SignalingClient] Version rejected: current=${current}, minimum=${minimum}`);
205
+ // Do not reconnect — must upgrade first; auto-update logic is in updater.ts
206
+ this.intentionalClose = true;
207
+ this.emit('version-rejected', current, minimum);
208
+ break;
209
+ }
210
+ case 'ice-offer': {
211
+ const offer = {
212
+ connectionId: msg['connectionId'] ?? '',
213
+ sdp: msg['sdp'] ?? '',
214
+ candidates: msg['candidates'] ?? [],
215
+ };
216
+ this.emit('ice-offer', offer);
217
+ break;
218
+ }
219
+ case 'ice-servers': {
220
+ // ICE server config (STUN/TURN) arrives before the offer for a connection.
221
+ const connectionId = msg['connectionId'] ?? '';
222
+ const iceServers = msg['iceServers'] ?? [];
223
+ this.emit('ice-servers', { connectionId, iceServers });
224
+ break;
225
+ }
226
+ case 'ice-candidate': {
227
+ // Trickle ICE candidate from browser, relayed by signaling server.
228
+ const connectionId = msg['connectionId'] ?? '';
229
+ const candidate = msg['candidate'] ?? null;
230
+ this.emit('ice-candidate', { connectionId, candidate });
231
+ break;
232
+ }
233
+ case 'force-update': {
234
+ const targetVersion = msg['targetVersion'] ?? '';
235
+ const reason = msg['reason'] ?? '';
236
+ this.emit('force-update', targetVersion, reason);
237
+ break;
238
+ }
239
+ case 'account-suspended': {
240
+ const reason = msg['reason'] ?? '';
241
+ // Stop reconnecting — account is suspended
242
+ this.intentionalClose = true;
243
+ this.emit('account-suspended', reason);
244
+ break;
245
+ }
246
+ case 'device-limit-updated': {
247
+ const deviceLimit = msg['deviceLimit'] ?? 0;
248
+ this.emit('device-limit-updated', deviceLimit);
249
+ break;
250
+ }
251
+ default: {
252
+ // Unknown messages are silently ignored to allow forward compatibility
253
+ break;
254
+ }
255
+ }
256
+ }
257
+ // -------------------------------------------------------------------------
258
+ // Internal — send helper
259
+ // -------------------------------------------------------------------------
260
+ _send(payload) {
261
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
262
+ return;
263
+ }
264
+ try {
265
+ this.ws.send(JSON.stringify(payload));
266
+ }
267
+ catch (err) {
268
+ console.error('[SignalingClient] Send error:', err);
269
+ }
270
+ }
271
+ // -------------------------------------------------------------------------
272
+ // Internal — reconnection
273
+ // -------------------------------------------------------------------------
274
+ _scheduleReconnect() {
275
+ if (this.intentionalClose)
276
+ return;
277
+ // Exponential backoff: 1s * 2^attempt, capped at BACKOFF_MAX_MS
278
+ const delay = Math.min(BACKOFF_BASE_MS * Math.pow(2, this.reconnectAttempts), BACKOFF_MAX_MS);
279
+ this.reconnectAttempts++;
280
+ console.log(`[SignalingClient] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
281
+ this.reconnectTimer = setTimeout(() => {
282
+ this.reconnectTimer = null;
283
+ if (this.intentionalClose)
284
+ return;
285
+ this._openSocket().catch((err) => {
286
+ console.error('[SignalingClient] Reconnect failed:', err);
287
+ // The 'close' event will have already scheduled the next attempt
288
+ });
289
+ }, delay);
290
+ }
291
+ _clearReconnectTimer() {
292
+ if (this.reconnectTimer !== null) {
293
+ clearTimeout(this.reconnectTimer);
294
+ this.reconnectTimer = null;
295
+ }
296
+ }
297
+ // -------------------------------------------------------------------------
298
+ // Internal — ping watchdog
299
+ // -------------------------------------------------------------------------
300
+ /**
301
+ * Restart the 90-second watchdog timer. Called on gateway-auth-ok and on
302
+ * every received ping frame. If the timer fires, the connection is dead
303
+ * and we force a reconnect.
304
+ */
305
+ _resetPingWatchdog() {
306
+ this._clearPingWatchdog();
307
+ this.pingWatchdog = setTimeout(() => {
308
+ console.warn('[SignalingClient] No ping received for 90s — connection presumed dead, reconnecting');
309
+ // Force-close the socket; the 'close' handler will schedule reconnect
310
+ if (this.ws) {
311
+ this.ws.terminate();
312
+ this.ws = null;
313
+ }
314
+ }, PING_TIMEOUT_MS);
315
+ }
316
+ _clearPingWatchdog() {
317
+ if (this.pingWatchdog !== null) {
318
+ clearTimeout(this.pingWatchdog);
319
+ this.pingWatchdog = null;
320
+ }
321
+ }
322
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Auto-update checker for the @clawchatsai/connector plugin.
3
+ *
4
+ * Checks the npm registry for newer versions and can trigger
5
+ * an in-place update via the OpenClaw plugin CLI.
6
+ */
7
+ export interface UpdateInfo {
8
+ current: string;
9
+ latest: string;
10
+ }
11
+ /**
12
+ * Check npm registry for updates.
13
+ * Returns UpdateInfo if a newer version is available, null otherwise.
14
+ * Silently returns null on any network or parse error.
15
+ */
16
+ export declare function checkForUpdates(): Promise<UpdateInfo | null>;
17
+ /**
18
+ * Run the OpenClaw plugin update command.
19
+ * Throws an Error if the command exits with a non-zero code or times out.
20
+ */
21
+ export declare function performUpdate(): Promise<void>;