@clawchatsai/connector 0.0.12 → 0.0.14
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/gateway-bridge.d.ts +1 -0
- package/dist/gateway-bridge.js +75 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +35 -35
- package/dist/migrate.d.ts +1 -1
- package/dist/migrate.js +1 -1
- package/dist/shim.js +1 -1
- package/dist/signaling-client.d.ts +1 -1
- package/dist/signaling-client.js +1 -1
- package/dist/webrtc-peer.d.ts +1 -1
- package/dist/webrtc-peer.js +1 -1
- package/openclaw.plugin.json +2 -2
- package/package.json +2 -2
- package/server.js +94 -33
package/dist/gateway-bridge.d.ts
CHANGED
package/dist/gateway-bridge.js
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
* 3. Reconnect with exponential backoff on disconnect.
|
|
10
10
|
*/
|
|
11
11
|
import { EventEmitter } from 'node:events';
|
|
12
|
+
import * as crypto from 'node:crypto';
|
|
13
|
+
import * as fs from 'node:fs';
|
|
14
|
+
import * as path from 'node:path';
|
|
12
15
|
import { WebSocket } from 'ws';
|
|
13
16
|
import { PLUGIN_VERSION } from './index.js';
|
|
14
17
|
// Reconnect backoff constants (milliseconds)
|
|
@@ -19,6 +22,62 @@ const CLIENT_ID = 'gateway-client';
|
|
|
19
22
|
const CLIENT_MODE = 'backend';
|
|
20
23
|
const ROLE = 'operator';
|
|
21
24
|
const SCOPES = ['operator.read', 'operator.write', 'operator.admin'];
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Device Identity — ed25519 key generation + connect payload signing
|
|
27
|
+
// Required by OpenClaw ≥2.15 to prevent scope clearing on connect.
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
|
|
30
|
+
function _derivePublicKeyRaw(publicKeyPem) {
|
|
31
|
+
const spki = crypto.createPublicKey(publicKeyPem).export({ type: 'spki', format: 'der' });
|
|
32
|
+
if (spki.length === ED25519_SPKI_PREFIX.length + 32 &&
|
|
33
|
+
spki.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)) {
|
|
34
|
+
return spki.subarray(ED25519_SPKI_PREFIX.length);
|
|
35
|
+
}
|
|
36
|
+
return spki;
|
|
37
|
+
}
|
|
38
|
+
function _fingerprintPublicKey(publicKeyPem) {
|
|
39
|
+
return crypto.createHash('sha256').update(_derivePublicKeyRaw(publicKeyPem)).digest('hex');
|
|
40
|
+
}
|
|
41
|
+
function _base64UrlEncode(buf) {
|
|
42
|
+
return buf.toString('base64').replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/g, '');
|
|
43
|
+
}
|
|
44
|
+
function _loadOrCreateDeviceIdentity(identityPath) {
|
|
45
|
+
try {
|
|
46
|
+
if (fs.existsSync(identityPath)) {
|
|
47
|
+
const parsed = JSON.parse(fs.readFileSync(identityPath, 'utf8'));
|
|
48
|
+
if (parsed?.version === 1 && parsed.deviceId && parsed.publicKeyPem && parsed.privateKeyPem) {
|
|
49
|
+
return parsed;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch { /* regenerate */ }
|
|
54
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
|
|
55
|
+
const publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' }).toString();
|
|
56
|
+
const privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' }).toString();
|
|
57
|
+
const identity = {
|
|
58
|
+
version: 1,
|
|
59
|
+
deviceId: _fingerprintPublicKey(publicKeyPem),
|
|
60
|
+
publicKeyPem,
|
|
61
|
+
privateKeyPem,
|
|
62
|
+
createdAtMs: Date.now(),
|
|
63
|
+
};
|
|
64
|
+
fs.mkdirSync(path.dirname(identityPath), { recursive: true });
|
|
65
|
+
fs.writeFileSync(identityPath, JSON.stringify(identity, null, 2) + '\n', { mode: 0o600 });
|
|
66
|
+
return identity;
|
|
67
|
+
}
|
|
68
|
+
function _buildDeviceAuth(identity, params) {
|
|
69
|
+
const signedAt = Date.now();
|
|
70
|
+
const payload = [
|
|
71
|
+
'v2', identity.deviceId, params.clientId, params.clientMode,
|
|
72
|
+
params.role, params.scopes.join(','), String(signedAt),
|
|
73
|
+
params.token || '', params.nonce,
|
|
74
|
+
].join('|');
|
|
75
|
+
const privateKey = crypto.createPrivateKey(identity.privateKeyPem);
|
|
76
|
+
const signature = _base64UrlEncode(crypto.sign(null, Buffer.from(payload, 'utf8'), privateKey));
|
|
77
|
+
const publicKeyB64Url = _base64UrlEncode(_derivePublicKeyRaw(identity.publicKeyPem));
|
|
78
|
+
return { id: identity.deviceId, publicKey: publicKeyB64Url, signature, signedAt, nonce: params.nonce };
|
|
79
|
+
}
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
22
81
|
export class GatewayBridge extends EventEmitter {
|
|
23
82
|
gatewayUrl;
|
|
24
83
|
config;
|
|
@@ -121,9 +180,11 @@ export class GatewayBridge extends EventEmitter {
|
|
|
121
180
|
}
|
|
122
181
|
const type = msg['type'];
|
|
123
182
|
const event = msg['event'];
|
|
124
|
-
// connect.challenge → send
|
|
183
|
+
// connect.challenge → send signed connect request with device identity
|
|
125
184
|
if (type === 'event' && event === 'connect.challenge') {
|
|
126
|
-
|
|
185
|
+
const payload = msg['payload'];
|
|
186
|
+
const nonce = payload?.['nonce'] ?? '';
|
|
187
|
+
this._sendConnect(nonce);
|
|
127
188
|
return;
|
|
128
189
|
}
|
|
129
190
|
// hello-ok → handshake complete
|
|
@@ -138,7 +199,17 @@ export class GatewayBridge extends EventEmitter {
|
|
|
138
199
|
}
|
|
139
200
|
}
|
|
140
201
|
}
|
|
141
|
-
_sendConnect() {
|
|
202
|
+
_sendConnect(nonce) {
|
|
203
|
+
const identityPath = path.join(this.config.dataDir, 'device-identity.json');
|
|
204
|
+
const identity = _loadOrCreateDeviceIdentity(identityPath);
|
|
205
|
+
const device = _buildDeviceAuth(identity, {
|
|
206
|
+
clientId: CLIENT_ID,
|
|
207
|
+
clientMode: CLIENT_MODE,
|
|
208
|
+
role: ROLE,
|
|
209
|
+
scopes: SCOPES,
|
|
210
|
+
token: this.config.gatewayToken,
|
|
211
|
+
nonce,
|
|
212
|
+
});
|
|
142
213
|
this.send(JSON.stringify({
|
|
143
214
|
type: 'req',
|
|
144
215
|
id: 'gw-connect-1',
|
|
@@ -154,6 +225,7 @@ export class GatewayBridge extends EventEmitter {
|
|
|
154
225
|
},
|
|
155
226
|
role: ROLE,
|
|
156
227
|
scopes: SCOPES,
|
|
228
|
+
device,
|
|
157
229
|
auth: { token: this.config.gatewayToken },
|
|
158
230
|
caps: ['tool-events'],
|
|
159
231
|
},
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @
|
|
2
|
+
* @clawchatsai/connector — OpenClaw plugin entry point
|
|
3
3
|
*
|
|
4
|
-
* Registers
|
|
4
|
+
* Registers ClawChats as a gateway plugin, providing:
|
|
5
5
|
* - Local HTTP API bridge via createApp()
|
|
6
6
|
* - WebRTC DataChannel for browser connections
|
|
7
7
|
* - Signaling client for NAT traversal
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* Spec: specs/multitenant-p2p.md sections 6.1-6.2
|
|
11
11
|
*/
|
|
12
12
|
export declare const PLUGIN_ID = "connector";
|
|
13
|
-
export declare const PLUGIN_VERSION = "0.0.
|
|
13
|
+
export declare const PLUGIN_VERSION = "0.0.13";
|
|
14
14
|
interface PluginServiceContext {
|
|
15
15
|
stateDir: string;
|
|
16
16
|
logger: {
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @
|
|
2
|
+
* @clawchatsai/connector — OpenClaw plugin entry point
|
|
3
3
|
*
|
|
4
|
-
* Registers
|
|
4
|
+
* Registers ClawChats as a gateway plugin, providing:
|
|
5
5
|
* - Local HTTP API bridge via createApp()
|
|
6
6
|
* - WebRTC DataChannel for browser connections
|
|
7
7
|
* - Signaling client for NAT traversal
|
|
@@ -19,7 +19,7 @@ import { checkForUpdates, performUpdate } from './updater.js';
|
|
|
19
19
|
// Inline from shared/api-version.ts to avoid rootDir conflict
|
|
20
20
|
const CURRENT_API_VERSION = 1;
|
|
21
21
|
export const PLUGIN_ID = 'connector';
|
|
22
|
-
export const PLUGIN_VERSION = '0.0.
|
|
22
|
+
export const PLUGIN_VERSION = '0.0.13';
|
|
23
23
|
/** Max DataChannel message size (~256KB, leave room for envelope) */
|
|
24
24
|
const MAX_DC_MESSAGE_SIZE = 256 * 1024;
|
|
25
25
|
/** Active DataChannel connections: connectionId → send function */
|
|
@@ -32,7 +32,7 @@ let _stopRequested = false;
|
|
|
32
32
|
// ---------------------------------------------------------------------------
|
|
33
33
|
// Config helpers
|
|
34
34
|
// ---------------------------------------------------------------------------
|
|
35
|
-
const CONFIG_DIR = path.join(process.env.HOME || '/root', '.openclaw', '
|
|
35
|
+
const CONFIG_DIR = path.join(process.env.HOME || '/root', '.openclaw', 'clawchats');
|
|
36
36
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
37
37
|
const RUNTIME_FILE = path.join(CONFIG_DIR, 'runtime.json');
|
|
38
38
|
function loadConfig() {
|
|
@@ -73,11 +73,11 @@ async function ensureNativeModules(ctx) {
|
|
|
73
73
|
ctx.logger.error('Try running manually: cd ~/.openclaw/extensions/connector && npm rebuild better-sqlite3');
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
|
-
async function
|
|
76
|
+
async function startClawChats(ctx, api) {
|
|
77
77
|
_stopRequested = false;
|
|
78
78
|
let config = loadConfig();
|
|
79
79
|
if (!config) {
|
|
80
|
-
ctx.logger.info('
|
|
80
|
+
ctx.logger.info('ClawChats not configured. Waiting for setup...');
|
|
81
81
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
82
82
|
while (!config && !_stopRequested) {
|
|
83
83
|
await new Promise(r => setTimeout(r, 2000));
|
|
@@ -85,7 +85,7 @@ async function startShellChat(ctx, api) {
|
|
|
85
85
|
}
|
|
86
86
|
if (_stopRequested || !config)
|
|
87
87
|
return;
|
|
88
|
-
ctx.logger.info('Setup detected — connecting to
|
|
88
|
+
ctx.logger.info('Setup detected — connecting to ClawChats...');
|
|
89
89
|
}
|
|
90
90
|
// 1. Check for updates
|
|
91
91
|
const update = await checkForUpdates();
|
|
@@ -95,7 +95,7 @@ async function startShellChat(ctx, api) {
|
|
|
95
95
|
try {
|
|
96
96
|
await performUpdate();
|
|
97
97
|
ctx.logger.info(`Updated to ${update.latest}. Requesting graceful restart...`);
|
|
98
|
-
api.runtime.requestRestart?.('
|
|
98
|
+
api.runtime.requestRestart?.('clawchats update');
|
|
99
99
|
return; // will restart with new version
|
|
100
100
|
}
|
|
101
101
|
catch (e) {
|
|
@@ -108,14 +108,14 @@ async function startShellChat(ctx, api) {
|
|
|
108
108
|
const gwAuth = gwCfg?.['gateway']?.['auth'];
|
|
109
109
|
const gatewayToken = gwAuth?.['token'] || config.gatewayToken || '';
|
|
110
110
|
if (!gatewayToken) {
|
|
111
|
-
ctx.logger.error('No gateway token available. Re-run: openclaw
|
|
111
|
+
ctx.logger.error('No gateway token available. Re-run: openclaw clawchats setup <token>');
|
|
112
112
|
return;
|
|
113
113
|
}
|
|
114
114
|
// 3. Ensure native modules are built (OpenClaw installs with --ignore-scripts)
|
|
115
115
|
await ensureNativeModules(ctx);
|
|
116
116
|
// 4. Import server.js and create app instance with plugin paths
|
|
117
|
-
const dataDir = path.join(ctx.stateDir, '
|
|
118
|
-
const uploadsDir = path.join(ctx.stateDir, '
|
|
117
|
+
const dataDir = path.join(ctx.stateDir, 'clawchats', 'data');
|
|
118
|
+
const uploadsDir = path.join(ctx.stateDir, 'clawchats', 'uploads');
|
|
119
119
|
// Dynamic import of server.js (plain JS, no type declarations)
|
|
120
120
|
// @ts-expect-error — server.js is plain JS with no .d.ts
|
|
121
121
|
const serverModule = await import('../server.js');
|
|
@@ -228,11 +228,11 @@ async function startShellChat(ctx, api) {
|
|
|
228
228
|
}, null, 2), { mode: 0o600 });
|
|
229
229
|
ctx.logger.info(`Health endpoint on 127.0.0.1:${addr.port}`);
|
|
230
230
|
});
|
|
231
|
-
ctx.logger.info('
|
|
231
|
+
ctx.logger.info('ClawChats service started');
|
|
232
232
|
}
|
|
233
|
-
async function
|
|
233
|
+
async function stopClawChats(ctx) {
|
|
234
234
|
_stopRequested = true;
|
|
235
|
-
ctx.logger.info('
|
|
235
|
+
ctx.logger.info('ClawChats service stopping...');
|
|
236
236
|
// 0. Tear down health endpoint
|
|
237
237
|
if (healthServer) {
|
|
238
238
|
healthServer.close();
|
|
@@ -259,7 +259,7 @@ async function stopShellChat(ctx) {
|
|
|
259
259
|
// 4. Close SQLite databases
|
|
260
260
|
app?.shutdown();
|
|
261
261
|
app = null;
|
|
262
|
-
ctx.logger.info('
|
|
262
|
+
ctx.logger.info('ClawChats service stopped');
|
|
263
263
|
}
|
|
264
264
|
// ---------------------------------------------------------------------------
|
|
265
265
|
// DataChannel message handler (spec section 6.4)
|
|
@@ -386,7 +386,7 @@ function broadcastToClients(msg) {
|
|
|
386
386
|
// ---------------------------------------------------------------------------
|
|
387
387
|
function formatStatus() {
|
|
388
388
|
const lines = [];
|
|
389
|
-
lines.push(`
|
|
389
|
+
lines.push(`ClawChats Plugin v${PLUGIN_VERSION}`);
|
|
390
390
|
lines.push(`Gateway: ${app?.gatewayClient?.connected ? 'connected' : 'disconnected'}`);
|
|
391
391
|
lines.push(`Signaling: ${signaling?.isConnected ? 'connected' : 'disconnected'}`);
|
|
392
392
|
lines.push(`Clients: ${connectedClients.size}`);
|
|
@@ -410,7 +410,7 @@ async function handleSetup(token) {
|
|
|
410
410
|
console.error('Setup token has expired. Generate a new one from clawchats.ai.');
|
|
411
411
|
return;
|
|
412
412
|
}
|
|
413
|
-
console.log('Setting up
|
|
413
|
+
console.log('Setting up ClawChats...');
|
|
414
414
|
console.log(` Server: ${tokenData.serverUrl}`);
|
|
415
415
|
// Generate API key for signaling server auth
|
|
416
416
|
const { randomBytes } = await import('node:crypto');
|
|
@@ -467,7 +467,7 @@ async function handleSetup(token) {
|
|
|
467
467
|
const uploadsDir = path.join(CONFIG_DIR, 'uploads');
|
|
468
468
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
469
469
|
fs.mkdirSync(uploadsDir, { recursive: true });
|
|
470
|
-
console.log('
|
|
470
|
+
console.log(' ClawChats is ready!');
|
|
471
471
|
console.log(' Open clawchats.ai in your browser to start chatting.');
|
|
472
472
|
ws.close();
|
|
473
473
|
resolve();
|
|
@@ -497,7 +497,7 @@ async function handleStatus() {
|
|
|
497
497
|
runtime = JSON.parse(fs.readFileSync(RUNTIME_FILE, 'utf8'));
|
|
498
498
|
}
|
|
499
499
|
catch {
|
|
500
|
-
console.log('
|
|
500
|
+
console.log('ClawChats: offline (service not running)');
|
|
501
501
|
return;
|
|
502
502
|
}
|
|
503
503
|
// Verify PID is alive
|
|
@@ -505,7 +505,7 @@ async function handleStatus() {
|
|
|
505
505
|
process.kill(runtime.pid, 0);
|
|
506
506
|
}
|
|
507
507
|
catch {
|
|
508
|
-
console.log('
|
|
508
|
+
console.log('ClawChats: offline (stale runtime file)');
|
|
509
509
|
try {
|
|
510
510
|
fs.unlinkSync(RUNTIME_FILE);
|
|
511
511
|
}
|
|
@@ -524,20 +524,20 @@ async function handleStatus() {
|
|
|
524
524
|
req.setTimeout(3000, () => { req.destroy(); reject(new Error('timeout')); });
|
|
525
525
|
});
|
|
526
526
|
const status = JSON.parse(body);
|
|
527
|
-
console.log(`
|
|
527
|
+
console.log(`ClawChats Plugin v${status.version}`);
|
|
528
528
|
console.log(`Uptime: ${Math.floor(status.uptime)}s`);
|
|
529
529
|
console.log(`Gateway: ${status.gateway.connected ? 'connected' : 'disconnected'}`);
|
|
530
530
|
console.log(`Signaling: ${status.signaling.connected ? 'connected' : 'disconnected'}`);
|
|
531
531
|
console.log(`Clients: ${status.clients.active}`);
|
|
532
532
|
}
|
|
533
533
|
catch {
|
|
534
|
-
console.log('
|
|
534
|
+
console.log('ClawChats: offline (could not reach service)');
|
|
535
535
|
}
|
|
536
536
|
}
|
|
537
537
|
async function handleReset() {
|
|
538
538
|
try {
|
|
539
539
|
fs.rmSync(CONFIG_DIR, { recursive: true, force: true });
|
|
540
|
-
console.log('
|
|
540
|
+
console.log('ClawChats data removed. Plugin disconnected.');
|
|
541
541
|
}
|
|
542
542
|
catch (e) {
|
|
543
543
|
console.error(`Reset failed: ${e.message}`);
|
|
@@ -554,7 +554,7 @@ async function handleImport(sourcePath) {
|
|
|
554
554
|
console.error(`Source must be a directory: ${resolvedSource}`);
|
|
555
555
|
return;
|
|
556
556
|
}
|
|
557
|
-
// Destination: ~/.openclaw/
|
|
557
|
+
// Destination: ~/.openclaw/clawchats/data/
|
|
558
558
|
const destDataDir = path.join(CONFIG_DIR, 'data');
|
|
559
559
|
fs.mkdirSync(destDataDir, { recursive: true });
|
|
560
560
|
// Import .db files
|
|
@@ -586,7 +586,7 @@ async function handleImport(sourcePath) {
|
|
|
586
586
|
}
|
|
587
587
|
}
|
|
588
588
|
// Also try to migrate config.json from the parent directory
|
|
589
|
-
// e.g. if source is ~/.openclaw/
|
|
589
|
+
// e.g. if source is ~/.openclaw/clawchats/data/, config is at ~/.openclaw/clawchats/config.json
|
|
590
590
|
const parentConfigPath = path.join(path.dirname(resolvedSource), 'config.json');
|
|
591
591
|
if (fs.existsSync(parentConfigPath)) {
|
|
592
592
|
try {
|
|
@@ -605,26 +605,26 @@ async function handleImport(sourcePath) {
|
|
|
605
605
|
// ---------------------------------------------------------------------------
|
|
606
606
|
const plugin = {
|
|
607
607
|
id: PLUGIN_ID,
|
|
608
|
-
name: '
|
|
609
|
-
description: 'Connects your gateway to
|
|
608
|
+
name: 'ClawChats',
|
|
609
|
+
description: 'Connects your gateway to ClawChats via WebRTC P2P',
|
|
610
610
|
register(api) {
|
|
611
611
|
// Background service: signaling + gateway bridge + future WebRTC
|
|
612
612
|
api.registerService({
|
|
613
613
|
id: 'connector-service',
|
|
614
|
-
start: (ctx) =>
|
|
615
|
-
stop: (ctx) =>
|
|
614
|
+
start: (ctx) => startClawChats(ctx, api),
|
|
615
|
+
stop: (ctx) => stopClawChats(ctx),
|
|
616
616
|
});
|
|
617
617
|
// CLI commands
|
|
618
618
|
api.registerCli((ctx) => {
|
|
619
|
-
const cmd = ctx.program.command('
|
|
619
|
+
const cmd = ctx.program.command('clawchats');
|
|
620
620
|
cmd.command('setup <token>')
|
|
621
|
-
.description('Set up
|
|
621
|
+
.description('Set up ClawChats with a setup token')
|
|
622
622
|
.action((token) => handleSetup(String(token)));
|
|
623
623
|
cmd.command('status')
|
|
624
|
-
.description('Show
|
|
624
|
+
.description('Show ClawChats connection status')
|
|
625
625
|
.action(() => handleStatus());
|
|
626
626
|
cmd.command('reset')
|
|
627
|
-
.description('Disconnect and remove all
|
|
627
|
+
.description('Disconnect and remove all ClawChats data')
|
|
628
628
|
.action(() => handleReset());
|
|
629
629
|
cmd.command('import <path>')
|
|
630
630
|
.description('Import databases and config from a folder (e.g. migrate from old data directory)')
|
|
@@ -632,8 +632,8 @@ const plugin = {
|
|
|
632
632
|
});
|
|
633
633
|
// Slash command for status from any channel
|
|
634
634
|
api.registerCommand({
|
|
635
|
-
name: '
|
|
636
|
-
description: 'Show
|
|
635
|
+
name: 'clawchats',
|
|
636
|
+
description: 'Show ClawChats tunnel status',
|
|
637
637
|
handler: () => ({ text: formatStatus() }),
|
|
638
638
|
});
|
|
639
639
|
},
|
package/dist/migrate.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Schema migration runner for the
|
|
2
|
+
* Schema migration runner for the ClawChats plugin SQLite database.
|
|
3
3
|
*
|
|
4
4
|
* - Tracks schema version in a `_schema_version` table
|
|
5
5
|
* - Runs migrations sequentially from current version to target
|
package/dist/migrate.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Schema migration runner for the
|
|
2
|
+
* Schema migration runner for the ClawChats plugin SQLite database.
|
|
3
3
|
*
|
|
4
4
|
* - Tracks schema version in a `_schema_version` table
|
|
5
5
|
* - Runs migrations sequentially from current version to target
|
package/dist/shim.js
CHANGED
|
@@ -46,7 +46,7 @@ class FakeReq extends Readable {
|
|
|
46
46
|
}
|
|
47
47
|
else if (parsed && parsed['_multipart']) {
|
|
48
48
|
// Multipart form data: { _multipart: true, fields: { key: string | { filename, contentType, data } } }
|
|
49
|
-
const boundary = '----
|
|
49
|
+
const boundary = '----ClawChatsBoundary' + Date.now();
|
|
50
50
|
this.headers['content-type'] = `multipart/form-data; boundary=${boundary}`;
|
|
51
51
|
const fields = parsed['fields'] || {};
|
|
52
52
|
const parts = [];
|
package/dist/signaling-client.js
CHANGED
package/dist/webrtc-peer.d.ts
CHANGED
|
@@ -109,7 +109,7 @@ export declare class WebRTCPeerManager extends EventEmitter {
|
|
|
109
109
|
handleIceCandidate(connectionId: string, candidate: unknown): void;
|
|
110
110
|
/**
|
|
111
111
|
* Close all active peer connections and DataChannels.
|
|
112
|
-
* Called during plugin shutdown (
|
|
112
|
+
* Called during plugin shutdown (stopClawChats).
|
|
113
113
|
*/
|
|
114
114
|
closeAll(): void;
|
|
115
115
|
/**
|
package/dist/webrtc-peer.js
CHANGED
|
@@ -194,7 +194,7 @@ export class WebRTCPeerManager extends EventEmitter {
|
|
|
194
194
|
}
|
|
195
195
|
/**
|
|
196
196
|
* Close all active peer connections and DataChannels.
|
|
197
|
-
* Called during plugin shutdown (
|
|
197
|
+
* Called during plugin shutdown (stopClawChats).
|
|
198
198
|
*/
|
|
199
199
|
closeAll() {
|
|
200
200
|
console.log(`[WebRTCPeerManager] Closing all connections (${this.peerConnections.size} peers, ${this.activeChannels.size} channels)`);
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "connector",
|
|
3
|
-
"name": "
|
|
4
|
-
"description": "Connects your OpenClaw gateway to
|
|
3
|
+
"name": "ClawChats",
|
|
4
|
+
"description": "Connects your OpenClaw gateway to ClawChats via WebRTC P2P",
|
|
5
5
|
"kind": "integration",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clawchatsai/connector",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "ClawChats OpenClaw plugin — P2P tunnel + local API bridge",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"files": [
|
package/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// ClawChats Backend Server
|
|
2
2
|
// Single-file Node.js HTTP server with SQLite storage
|
|
3
3
|
// See specs/backend-session-architecture.md for full spec
|
|
4
4
|
|
|
@@ -16,9 +16,55 @@ import { WebSocket as WS, WebSocketServer } from 'ws';
|
|
|
16
16
|
const __filename = fileURLToPath(import.meta.url);
|
|
17
17
|
const __dirname = path.dirname(__filename);
|
|
18
18
|
|
|
19
|
+
// ─── Device Identity (ed25519 signing for OpenClaw ≥2.15 scope preservation) ─
|
|
20
|
+
|
|
21
|
+
const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
|
|
22
|
+
|
|
23
|
+
function _derivePublicKeyRaw(publicKeyPem) {
|
|
24
|
+
const spki = crypto.createPublicKey(publicKeyPem).export({ type: 'spki', format: 'der' });
|
|
25
|
+
if (spki.length === ED25519_SPKI_PREFIX.length + 32 &&
|
|
26
|
+
spki.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)) {
|
|
27
|
+
return spki.subarray(ED25519_SPKI_PREFIX.length);
|
|
28
|
+
}
|
|
29
|
+
return spki;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function _fingerprintPublicKey(publicKeyPem) {
|
|
33
|
+
return crypto.createHash('sha256').update(_derivePublicKeyRaw(publicKeyPem)).digest('hex');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function _base64UrlEncode(buf) {
|
|
37
|
+
return buf.toString('base64').replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/g, '');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function _loadOrCreateDeviceIdentity(identityPath) {
|
|
41
|
+
try {
|
|
42
|
+
if (fs.existsSync(identityPath)) {
|
|
43
|
+
const parsed = JSON.parse(fs.readFileSync(identityPath, 'utf8'));
|
|
44
|
+
if (parsed?.version === 1 && parsed.deviceId && parsed.publicKeyPem && parsed.privateKeyPem) return parsed;
|
|
45
|
+
}
|
|
46
|
+
} catch { /* regenerate */ }
|
|
47
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
|
|
48
|
+
const publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' }).toString();
|
|
49
|
+
const privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' }).toString();
|
|
50
|
+
const identity = { version: 1, deviceId: _fingerprintPublicKey(publicKeyPem), publicKeyPem, privateKeyPem, createdAtMs: Date.now() };
|
|
51
|
+
fs.mkdirSync(path.dirname(identityPath), { recursive: true });
|
|
52
|
+
fs.writeFileSync(identityPath, JSON.stringify(identity, null, 2) + '\n', { mode: 0o600 });
|
|
53
|
+
return identity;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function _buildDeviceAuth(identity, { clientId, clientMode, role, scopes, token, nonce }) {
|
|
57
|
+
const signedAt = Date.now();
|
|
58
|
+
const payload = ['v2', identity.deviceId, clientId, clientMode, role, scopes.join(','), String(signedAt), token || '', nonce].join('|');
|
|
59
|
+
const privateKey = crypto.createPrivateKey(identity.privateKeyPem);
|
|
60
|
+
const signature = _base64UrlEncode(crypto.sign(null, Buffer.from(payload, 'utf8'), privateKey));
|
|
61
|
+
const publicKeyB64Url = _base64UrlEncode(_derivePublicKeyRaw(identity.publicKeyPem));
|
|
62
|
+
return { id: identity.deviceId, publicKey: publicKeyB64Url, signature, signedAt, nonce };
|
|
63
|
+
}
|
|
64
|
+
|
|
19
65
|
// ─── Configuration ──────────────────────────────────────────────────────────
|
|
20
66
|
|
|
21
|
-
const PORT = parseInt(process.env.SHELLCHAT_PORT || '3001', 10);
|
|
67
|
+
const PORT = parseInt(process.env.CLAWCHATS_PORT || process.env.SHELLCHAT_PORT || '3001', 10);
|
|
22
68
|
const DATA_DIR = path.join(__dirname, 'data');
|
|
23
69
|
const UPLOADS_DIR = path.join(__dirname, 'uploads');
|
|
24
70
|
const WORKSPACES_FILE = path.join(DATA_DIR, 'workspaces.json');
|
|
@@ -128,9 +174,9 @@ function parseConfigField(field) {
|
|
|
128
174
|
}
|
|
129
175
|
|
|
130
176
|
// Load auth token from config.js or env var
|
|
131
|
-
let AUTH_TOKEN = process.env.SHELLCHAT_AUTH_TOKEN || parseConfigField('authToken') || '';
|
|
177
|
+
let AUTH_TOKEN = process.env.CLAWCHATS_AUTH_TOKEN || process.env.SHELLCHAT_AUTH_TOKEN || parseConfigField('authToken') || '';
|
|
132
178
|
if (!AUTH_TOKEN) {
|
|
133
|
-
console.error('WARNING: No auth token configured. Set
|
|
179
|
+
console.error('WARNING: No auth token configured. Set CLAWCHATS_AUTH_TOKEN or create config.js');
|
|
134
180
|
}
|
|
135
181
|
|
|
136
182
|
// Load gateway WebSocket URL
|
|
@@ -837,7 +883,7 @@ async function handleMarkMessagesRead(req, res, params) {
|
|
|
837
883
|
// Broadcast unread-update to ALL browser clients (so other tabs/devices sync)
|
|
838
884
|
const workspace = getWorkspaces().active;
|
|
839
885
|
gatewayClient.broadcastToBrowsers(JSON.stringify({
|
|
840
|
-
type: '
|
|
886
|
+
type: 'clawchats',
|
|
841
887
|
event: 'unread-update',
|
|
842
888
|
workspace,
|
|
843
889
|
threadId,
|
|
@@ -2233,6 +2279,13 @@ class GatewayClient {
|
|
|
2233
2279
|
// Handle connect.challenge (handshake)
|
|
2234
2280
|
if (msg.type === 'event' && msg.event === 'connect.challenge') {
|
|
2235
2281
|
console.log('Received connect.challenge, sending auth...');
|
|
2282
|
+
const nonce = msg.payload?.nonce || '';
|
|
2283
|
+
const identityPath = path.join(DATA_DIR, 'device-identity.json');
|
|
2284
|
+
const identity = _loadOrCreateDeviceIdentity(identityPath);
|
|
2285
|
+
const device = _buildDeviceAuth(identity, {
|
|
2286
|
+
clientId: 'gateway-client', clientMode: 'backend', role: 'operator',
|
|
2287
|
+
scopes: ['operator.read', 'operator.write', 'operator.admin'], token: AUTH_TOKEN, nonce
|
|
2288
|
+
});
|
|
2236
2289
|
this.ws.send(JSON.stringify({
|
|
2237
2290
|
type: 'req',
|
|
2238
2291
|
id: 'gw-connect-1',
|
|
@@ -2243,6 +2296,7 @@ class GatewayClient {
|
|
|
2243
2296
|
client: { id: 'gateway-client', version: '0.1.0', platform: 'node', mode: 'backend' },
|
|
2244
2297
|
role: 'operator',
|
|
2245
2298
|
scopes: ['operator.read', 'operator.write', 'operator.admin'],
|
|
2299
|
+
device,
|
|
2246
2300
|
auth: { token: AUTH_TOKEN },
|
|
2247
2301
|
caps: ['tool-events']
|
|
2248
2302
|
}
|
|
@@ -2315,7 +2369,7 @@ class GatewayClient {
|
|
|
2315
2369
|
|
|
2316
2370
|
saveAssistantMessage(sessionKey, message, seq) {
|
|
2317
2371
|
const parsed = parseSessionKey(sessionKey);
|
|
2318
|
-
if (!parsed) return; // Non-
|
|
2372
|
+
if (!parsed) return; // Non-ClawChats session key, silently ignore
|
|
2319
2373
|
|
|
2320
2374
|
// Guard: verify workspace still exists
|
|
2321
2375
|
const ws = getWorkspaces();
|
|
@@ -2370,7 +2424,7 @@ class GatewayClient {
|
|
|
2370
2424
|
|
|
2371
2425
|
// Broadcast message-saved for active thread reload
|
|
2372
2426
|
this.broadcastToBrowsers(JSON.stringify({
|
|
2373
|
-
type: '
|
|
2427
|
+
type: 'clawchats',
|
|
2374
2428
|
event: 'message-saved',
|
|
2375
2429
|
threadId: parsed.threadId,
|
|
2376
2430
|
workspace: parsed.workspace,
|
|
@@ -2384,7 +2438,7 @@ class GatewayClient {
|
|
|
2384
2438
|
// Always broadcast unread-update — browser sends read receipts to clear
|
|
2385
2439
|
const workspaceUnreadTotal = db.prepare('SELECT COALESCE(SUM(unread_count), 0) as total FROM threads').get().total;
|
|
2386
2440
|
this.broadcastToBrowsers(JSON.stringify({
|
|
2387
|
-
type: '
|
|
2441
|
+
type: 'clawchats',
|
|
2388
2442
|
event: 'unread-update',
|
|
2389
2443
|
workspace: parsed.workspace,
|
|
2390
2444
|
threadId: parsed.threadId,
|
|
@@ -2562,7 +2616,7 @@ class GatewayClient {
|
|
|
2562
2616
|
if (!parsed) return;
|
|
2563
2617
|
|
|
2564
2618
|
this.broadcastToBrowsers(JSON.stringify({
|
|
2565
|
-
type: '
|
|
2619
|
+
type: 'clawchats',
|
|
2566
2620
|
event: 'agent-activity',
|
|
2567
2621
|
workspace: parsed.workspace,
|
|
2568
2622
|
threadId: parsed.threadId,
|
|
@@ -2707,7 +2761,7 @@ class GatewayClient {
|
|
|
2707
2761
|
|
|
2708
2762
|
// Notify browsers to re-render this message with activity data
|
|
2709
2763
|
this.broadcastToBrowsers(JSON.stringify({
|
|
2710
|
-
type: '
|
|
2764
|
+
type: 'clawchats',
|
|
2711
2765
|
event: 'activity-saved',
|
|
2712
2766
|
workspace: parsed.workspace,
|
|
2713
2767
|
threadId: parsed.threadId,
|
|
@@ -2790,7 +2844,7 @@ class GatewayClient {
|
|
|
2790
2844
|
|
|
2791
2845
|
broadcastGatewayStatus(connected) {
|
|
2792
2846
|
const msg = JSON.stringify({
|
|
2793
|
-
type: '
|
|
2847
|
+
type: 'clawchats',
|
|
2794
2848
|
event: 'gateway-status',
|
|
2795
2849
|
connected
|
|
2796
2850
|
});
|
|
@@ -2819,7 +2873,7 @@ class GatewayClient {
|
|
|
2819
2873
|
// Send current gateway status
|
|
2820
2874
|
if (ws.readyState === WS.OPEN) {
|
|
2821
2875
|
ws.send(JSON.stringify({
|
|
2822
|
-
type: '
|
|
2876
|
+
type: 'clawchats',
|
|
2823
2877
|
event: 'gateway-status',
|
|
2824
2878
|
connected: this.connected
|
|
2825
2879
|
}));
|
|
@@ -2833,7 +2887,7 @@ class GatewayClient {
|
|
|
2833
2887
|
}
|
|
2834
2888
|
if (streams.length > 0) {
|
|
2835
2889
|
ws.send(JSON.stringify({
|
|
2836
|
-
type: '
|
|
2890
|
+
type: 'clawchats',
|
|
2837
2891
|
event: 'stream-sync',
|
|
2838
2892
|
streams
|
|
2839
2893
|
}));
|
|
@@ -2870,7 +2924,7 @@ class GatewayClient {
|
|
|
2870
2924
|
// Broadcast unread-update clear to ALL browser clients
|
|
2871
2925
|
const workspaceUnreadTotal = db.prepare('SELECT COALESCE(SUM(unread_count), 0) as total FROM threads').get().total;
|
|
2872
2926
|
this.broadcastToBrowsers(JSON.stringify({
|
|
2873
|
-
type: '
|
|
2927
|
+
type: 'clawchats',
|
|
2874
2928
|
event: 'unread-update',
|
|
2875
2929
|
workspace,
|
|
2876
2930
|
threadId,
|
|
@@ -2898,7 +2952,7 @@ function syncThreadUnreadCount(db, threadId) {
|
|
|
2898
2952
|
function parseSessionKey(sessionKey) {
|
|
2899
2953
|
if (!sessionKey) return null;
|
|
2900
2954
|
const match = sessionKey.match(/^agent:main:([^:]+):chat:([^:]+)$/);
|
|
2901
|
-
if (!match) return null; // Non-
|
|
2955
|
+
if (!match) return null; // Non-ClawChats keys — silently ignore
|
|
2902
2956
|
return { workspace: match[1], threadId: match[2] };
|
|
2903
2957
|
}
|
|
2904
2958
|
|
|
@@ -2919,7 +2973,7 @@ const gatewayClient = new GatewayClient();
|
|
|
2919
2973
|
|
|
2920
2974
|
// ─── createApp Factory ───────────────────────────────────────────────────────
|
|
2921
2975
|
// Returns an isolated instance of the app state + handlers.
|
|
2922
|
-
// Used by the plugin (signaling/index.js) to embed
|
|
2976
|
+
// Used by the plugin (signaling/index.js) to embed ClawChats logic without
|
|
2923
2977
|
// spinning up a standalone HTTP server.
|
|
2924
2978
|
|
|
2925
2979
|
export function createApp(config = {}) {
|
|
@@ -2932,7 +2986,7 @@ export function createApp(config = {}) {
|
|
|
2932
2986
|
|
|
2933
2987
|
let _AUTH_TOKEN = config.authToken !== undefined
|
|
2934
2988
|
? config.authToken
|
|
2935
|
-
: (process.env.SHELLCHAT_AUTH_TOKEN || parseConfigField('authToken') || '');
|
|
2989
|
+
: (process.env.CLAWCHATS_AUTH_TOKEN || process.env.SHELLCHAT_AUTH_TOKEN || parseConfigField('authToken') || '');
|
|
2936
2990
|
|
|
2937
2991
|
// Separate token for gateway WS auth (falls back to _AUTH_TOKEN for direct mode)
|
|
2938
2992
|
const _GATEWAY_TOKEN = config.gatewayToken !== undefined
|
|
@@ -3178,7 +3232,7 @@ export function createApp(config = {}) {
|
|
|
3178
3232
|
const remaining = syncThreadUnreadCount(db, threadId);
|
|
3179
3233
|
const workspace = _getWorkspaces().active;
|
|
3180
3234
|
_gatewayClient.broadcastToBrowsers(JSON.stringify({
|
|
3181
|
-
type: '
|
|
3235
|
+
type: 'clawchats', event: 'unread-update', workspace, threadId,
|
|
3182
3236
|
action: 'read', messageIds, unreadCount: remaining, timestamp: Date.now()
|
|
3183
3237
|
}));
|
|
3184
3238
|
send(res, 200, { unread_count: remaining });
|
|
@@ -3567,7 +3621,14 @@ export function createApp(config = {}) {
|
|
|
3567
3621
|
try { msg = JSON.parse(data); } catch { console.error('Invalid JSON from gateway:', data); return; }
|
|
3568
3622
|
if (msg.type === 'event' && msg.event === 'connect.challenge') {
|
|
3569
3623
|
console.log('Received connect.challenge, sending auth...');
|
|
3570
|
-
|
|
3624
|
+
const _nonce = msg.payload?.nonce || '';
|
|
3625
|
+
const _identityPath = path.join(_DATA_DIR, 'device-identity.json');
|
|
3626
|
+
const _identity = _loadOrCreateDeviceIdentity(_identityPath);
|
|
3627
|
+
const _device = _buildDeviceAuth(_identity, {
|
|
3628
|
+
clientId: 'gateway-client', clientMode: 'backend', role: 'operator',
|
|
3629
|
+
scopes: ['operator.read', 'operator.write', 'operator.admin'], token: _GATEWAY_TOKEN, nonce: _nonce
|
|
3630
|
+
});
|
|
3631
|
+
this.ws.send(JSON.stringify({ type: 'req', id: 'gw-connect-1', method: 'connect', params: { minProtocol: 3, maxProtocol: 3, client: { id: 'gateway-client', version: '0.1.0', platform: 'node', mode: 'backend' }, role: 'operator', scopes: ['operator.read', 'operator.write', 'operator.admin'], device: _device, auth: { token: _GATEWAY_TOKEN }, caps: ['tool-events'] } }));
|
|
3571
3632
|
return;
|
|
3572
3633
|
}
|
|
3573
3634
|
if (msg.type === 'res' && msg.payload?.type === 'hello-ok') { console.log('Gateway handshake complete'); this.connected = true; this.broadcastGatewayStatus(true); }
|
|
@@ -3618,9 +3679,9 @@ export function createApp(config = {}) {
|
|
|
3618
3679
|
const threadInfo = db.prepare('SELECT title FROM threads WHERE id = ?').get(parsed.threadId);
|
|
3619
3680
|
const unreadCount = db.prepare('SELECT COUNT(*) as c FROM unread_messages WHERE thread_id = ?').get(parsed.threadId).c;
|
|
3620
3681
|
const preview = content.length > 120 ? content.substring(0, 120) + '...' : content;
|
|
3621
|
-
this.broadcastToBrowsers(JSON.stringify({ type: '
|
|
3682
|
+
this.broadcastToBrowsers(JSON.stringify({ type: 'clawchats', event: 'message-saved', threadId: parsed.threadId, workspace: parsed.workspace, messageId, timestamp: now, title: threadInfo?.title || 'Chat', preview, unreadCount }));
|
|
3622
3683
|
const workspaceUnreadTotal = db.prepare('SELECT COALESCE(SUM(unread_count), 0) as total FROM threads').get().total;
|
|
3623
|
-
this.broadcastToBrowsers(JSON.stringify({ type: '
|
|
3684
|
+
this.broadcastToBrowsers(JSON.stringify({ type: 'clawchats', event: 'unread-update', workspace: parsed.workspace, threadId: parsed.threadId, messageId, action: 'new', unreadCount, workspaceUnreadTotal, title: threadInfo?.title || 'Chat', preview, timestamp: now }));
|
|
3624
3685
|
console.log(`Saved assistant message to ${parsed.workspace}/${parsed.threadId} (seq: ${seq})`);
|
|
3625
3686
|
} catch (e) { console.error(`Failed to save assistant message:`, e.message); }
|
|
3626
3687
|
}
|
|
@@ -3698,7 +3759,7 @@ export function createApp(config = {}) {
|
|
|
3698
3759
|
broadcastActivityUpdate(runId, log) {
|
|
3699
3760
|
const parsed = log.sessionKey ? parseSessionKey(log.sessionKey) : null;
|
|
3700
3761
|
if (!parsed) return;
|
|
3701
|
-
this.broadcastToBrowsers(JSON.stringify({ type: '
|
|
3762
|
+
this.broadcastToBrowsers(JSON.stringify({ type: 'clawchats', event: 'agent-activity', workspace: parsed.workspace, threadId: parsed.threadId, runId, steps: log.steps, summary: this.generateActivitySummary(log.steps) }));
|
|
3702
3763
|
}
|
|
3703
3764
|
|
|
3704
3765
|
finalizeActivityLog(runId, log) {
|
|
@@ -3763,7 +3824,7 @@ export function createApp(config = {}) {
|
|
|
3763
3824
|
metadata.activitySummary = this.generateActivitySummary(log.steps);
|
|
3764
3825
|
db.prepare('UPDATE messages SET metadata = ? WHERE id = ?').run(JSON.stringify(metadata), msg.id);
|
|
3765
3826
|
console.log(`[ActivityLog] Saved ${toolSteps.length} tool steps for message ${msg.id}`);
|
|
3766
|
-
this.broadcastToBrowsers(JSON.stringify({ type: '
|
|
3827
|
+
this.broadcastToBrowsers(JSON.stringify({ type: 'clawchats', event: 'activity-saved', workspace: parsed.workspace, threadId: parsed.threadId, messageId: msg.id }));
|
|
3767
3828
|
}
|
|
3768
3829
|
}
|
|
3769
3830
|
} catch (e) { console.error('Failed to save activity log:', e.message); }
|
|
@@ -3803,7 +3864,7 @@ export function createApp(config = {}) {
|
|
|
3803
3864
|
}
|
|
3804
3865
|
|
|
3805
3866
|
broadcastGatewayStatus(connected) {
|
|
3806
|
-
this.broadcastToBrowsers(JSON.stringify({ type: '
|
|
3867
|
+
this.broadcastToBrowsers(JSON.stringify({ type: 'clawchats', event: 'gateway-status', connected }));
|
|
3807
3868
|
}
|
|
3808
3869
|
|
|
3809
3870
|
sendToGateway(data) {
|
|
@@ -3822,12 +3883,12 @@ export function createApp(config = {}) {
|
|
|
3822
3883
|
addBrowserClient(ws) {
|
|
3823
3884
|
this.browserClients.set(ws, { activeWorkspace: null, activeThreadId: null });
|
|
3824
3885
|
if (ws.readyState === WS.OPEN) {
|
|
3825
|
-
ws.send(JSON.stringify({ type: '
|
|
3886
|
+
ws.send(JSON.stringify({ type: 'clawchats', event: 'gateway-status', connected: this.connected }));
|
|
3826
3887
|
const streams = [];
|
|
3827
3888
|
for (const [sessionKey, state] of this.streamState.entries()) {
|
|
3828
3889
|
if (state.state === 'streaming') streams.push({ sessionKey, threadId: state.threadId, buffer: state.buffer });
|
|
3829
3890
|
}
|
|
3830
|
-
if (streams.length > 0) ws.send(JSON.stringify({ type: '
|
|
3891
|
+
if (streams.length > 0) ws.send(JSON.stringify({ type: 'clawchats', event: 'stream-sync', streams }));
|
|
3831
3892
|
}
|
|
3832
3893
|
}
|
|
3833
3894
|
|
|
@@ -3847,7 +3908,7 @@ export function createApp(config = {}) {
|
|
|
3847
3908
|
if (deleted.changes > 0) {
|
|
3848
3909
|
syncThreadUnreadCount(db, threadId);
|
|
3849
3910
|
const workspaceUnreadTotal = db.prepare('SELECT COALESCE(SUM(unread_count), 0) as total FROM threads').get().total;
|
|
3850
|
-
this.broadcastToBrowsers(JSON.stringify({ type: '
|
|
3911
|
+
this.broadcastToBrowsers(JSON.stringify({ type: 'clawchats', event: 'unread-update', workspace, threadId, action: 'clear', unreadCount: 0, workspaceUnreadTotal, timestamp: Date.now() }));
|
|
3851
3912
|
}
|
|
3852
3913
|
} catch (e) { console.error('Failed to auto-clear unreads on active-thread:', e.message); }
|
|
3853
3914
|
}
|
|
@@ -3963,7 +4024,7 @@ export function createApp(config = {}) {
|
|
|
3963
4024
|
const token = msg.params?.auth?.token;
|
|
3964
4025
|
if (token === _AUTH_TOKEN || !_AUTH_TOKEN) {
|
|
3965
4026
|
console.log('Browser client authenticated');
|
|
3966
|
-
ws.send(JSON.stringify({ type: 'res', id: msg.id, ok: true, payload: { type: 'hello-ok', protocol: 3, server: { version: '0.1.0', host: '
|
|
4027
|
+
ws.send(JSON.stringify({ type: 'res', id: msg.id, ok: true, payload: { type: 'hello-ok', protocol: 3, server: { version: '0.1.0', host: 'clawchats-backend' } } }));
|
|
3967
4028
|
} else {
|
|
3968
4029
|
console.log('Browser client auth failed');
|
|
3969
4030
|
ws.send(JSON.stringify({ type: 'res', id: msg.id, ok: false, error: { code: 'AUTH_FAILED', message: 'Invalid auth token' } }));
|
|
@@ -3971,12 +4032,12 @@ export function createApp(config = {}) {
|
|
|
3971
4032
|
}
|
|
3972
4033
|
return;
|
|
3973
4034
|
}
|
|
3974
|
-
if (msg.type === 'shellchat') {
|
|
4035
|
+
if (msg.type === 'clawchats' || msg.type === 'shellchat') { // backward compat: accept legacy 'shellchat' type
|
|
3975
4036
|
if (msg.action === 'active-thread') { _gatewayClient.setActiveThread(ws, msg.workspace, msg.threadId); console.log(`Browser client set active thread: ${msg.workspace}/${msg.threadId}`); return; }
|
|
3976
|
-
if (msg.action === 'debug-start') { const result = _debugLogger.start(msg.ts, ws); if (result.error === 'already-active') ws.send(JSON.stringify({ type: '
|
|
3977
|
-
if (msg.action === 'debug-dump') { const { sessionId, files } = _debugLogger.saveDump(msg); ws.send(JSON.stringify({ type: '
|
|
4037
|
+
if (msg.action === 'debug-start') { const result = _debugLogger.start(msg.ts, ws); if (result.error === 'already-active') ws.send(JSON.stringify({ type: 'clawchats', event: 'debug-error', error: 'Recording already active in another tab', sessionId: result.sessionId })); else ws.send(JSON.stringify({ type: 'clawchats', event: 'debug-started', sessionId: result.sessionId })); return; }
|
|
4038
|
+
if (msg.action === 'debug-dump') { const { sessionId, files } = _debugLogger.saveDump(msg); ws.send(JSON.stringify({ type: 'clawchats', event: 'debug-saved', sessionId, files })); return; }
|
|
3978
4039
|
}
|
|
3979
|
-
} catch { /* Not JSON or not a
|
|
4040
|
+
} catch { /* Not JSON or not a ClawChats message, forward to gateway */ }
|
|
3980
4041
|
_gatewayClient.sendToGateway(msgStr);
|
|
3981
4042
|
});
|
|
3982
4043
|
|
|
@@ -4025,7 +4086,7 @@ if (isDirectRun) {
|
|
|
4025
4086
|
});
|
|
4026
4087
|
|
|
4027
4088
|
server.listen(PORT, () => {
|
|
4028
|
-
console.log(`
|
|
4089
|
+
console.log(`ClawChats backend listening on port ${PORT}`);
|
|
4029
4090
|
console.log(`Active workspace: ${app.getWorkspaces().active}`);
|
|
4030
4091
|
console.log(`Data dir: ${app.dataDir}`);
|
|
4031
4092
|
|