Initial commit

This commit is contained in:
Johan Jongsma 2026-02-01 08:03:40 +00:00
commit 60a6d973f5
5 changed files with 395 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules/
.env
*.log
.DS_Store

37
package-lock.json generated Normal file
View File

@ -0,0 +1,37 @@
{
"name": "clawdnode-gateway",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "clawdnode-gateway",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"ws": "^8.19.0"
}
},
"node_modules/ws": {
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}

16
package.json Normal file
View File

@ -0,0 +1,16 @@
{
"name": "clawdnode-gateway",
"version": "1.0.0",
"description": "",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"ws": "^8.19.0"
}
}

327
server.js Normal file
View File

@ -0,0 +1,327 @@
#!/usr/bin/env node
/**
* ClawdNode Gateway - Custom protocol for direct phone control
*
* Protocol:
* - Server sends: {"type":"welcome","protocol":"clawdnode/1.0","serverTime":...}
* - Client sends: {"type":"hello","client":"clawdnode-android","version":"..."}
* - Server sends: {"type":"ready","message":"Connected"}
* - Then bidirectional events/commands
*
* If a client connects speaking Clawdbot protocol, we send a clear error.
*/
const http = require('http');
const { WebSocketServer, WebSocket } = require('ws');
const fs = require('fs');
const HTTP_PORT = 9877;
const WS_PORT = 9878;
const LOG_FILE = '/tmp/clawdnode-gateway.log';
const PROTOCOL_VERSION = 'clawdnode/1.0';
// Connected clients
const clients = new Map();
let commandQueue = [];
// Logging
const logStream = fs.createWriteStream(LOG_FILE, { flags: 'a' });
function log(msg) {
const ts = new Date().toISOString();
const line = `[${ts}] ${msg}`;
console.log(line);
logStream.write(line + '\n');
}
// Callback for notifications/events (James can hook into this)
let onNotification = null;
let onCall = null;
// ============================================
// WebSocket Server (for Android app)
// ============================================
const wss = new WebSocketServer({ port: WS_PORT });
wss.on('connection', (ws, req) => {
const clientId = `client_${Date.now()}`;
const ip = req.socket.remoteAddress;
log(`📱 [CONNECT] ${clientId} from ${ip}`);
clients.set(clientId, { ws, ip, connectedAt: Date.now(), ready: false });
// Send our protocol welcome - this identifies us as ClawdNode gateway
ws.send(JSON.stringify({
type: 'welcome',
protocol: PROTOCOL_VERSION,
serverTime: Date.now(),
message: 'ClawdNode Gateway - Send {"type":"hello",...} to connect'
}));
ws.on('message', (data) => {
try {
const msg = JSON.parse(data.toString());
handleMessage(clientId, msg, ws);
} catch (e) {
log(`❌ [ERROR] Parse error from ${clientId}: ${e.message}`);
ws.send(JSON.stringify({
type: 'error',
code: 'PARSE_ERROR',
message: 'Invalid JSON'
}));
}
});
ws.on('close', (code, reason) => {
log(`📱 [DISCONNECT] ${clientId} code=${code} reason=${reason || 'n/a'}`);
clients.delete(clientId);
});
ws.on('error', (err) => {
log(`❌ [WS_ERROR] ${clientId}: ${err.message}`);
});
});
function handleMessage(clientId, msg, ws) {
const type = msg.type || msg.event || 'unknown';
const client = clients.get(clientId);
// Detect Clawdbot protocol attempts
if (msg.method === 'connect' || msg.event === 'connect.handshake' || msg.type === 'req') {
log(`⚠️ [PROTOCOL_ERROR] ${clientId} is using Clawdbot protocol`);
ws.send(JSON.stringify({
type: 'error',
code: 'WRONG_PROTOCOL',
message: 'This is ClawdNode Gateway (port 9878), not Clawdbot Gateway (port 18789). ' +
'Update your app to use ws://100.123.216.65:9878 with ClawdNode protocol.',
expected: PROTOCOL_VERSION,
received: msg.method || msg.type
}));
ws.close(4000, 'Wrong protocol - use ClawdNode protocol');
return;
}
switch (type) {
case 'hello':
// Client hello - mark as ready
log(`👋 [HELLO] ${clientId}: ${msg.client || 'unknown'} v${msg.version || '?'}`);
if (client) {
client.ready = true;
client.clientInfo = { client: msg.client, version: msg.version };
}
ws.send(JSON.stringify({
type: 'ready',
message: 'Connected to ClawdNode Gateway',
protocol: PROTOCOL_VERSION
}));
break;
case 'notification':
log(`🔔 [NOTIFICATION] ${msg.app || msg.packageName}:`);
log(` Title: ${msg.title}`);
log(` Text: ${msg.text}`);
log(` Actions: ${JSON.stringify(msg.actions || [])}`);
log(` ID: ${msg.id}`);
if (onNotification) onNotification(msg);
break;
case 'call':
log(`📞 [CALL] ${msg.state}:`);
log(` Number: ${msg.number}`);
log(` Contact: ${msg.contact || 'Unknown'}`);
log(` CallID: ${msg.callId}`);
if (onCall) onCall(msg);
// Alert on incoming calls
if (msg.state === 'incoming' || msg.state === 'ringing') {
log(`🚨 INCOMING CALL from ${msg.contact || msg.number}`);
}
break;
case 'call.incoming':
log(`📞 [INCOMING CALL]:`);
log(` Number: ${msg.number}`);
log(` Contact: ${msg.contact || 'Unknown'}`);
if (onCall) onCall({ ...msg, state: 'incoming' });
break;
case 'response':
case 'result':
log(`✅ [RESPONSE] Command ${msg.commandId}: ${msg.success ? 'OK' : 'FAILED'}`);
if (msg.error) log(` Error: ${msg.error}`);
break;
case 'log':
log(`📋 [LOG] ${msg.message}`);
break;
case 'lifecycle':
log(`🔄 [LIFECYCLE:${msg.event}] ${msg.message || ''}`);
break;
default:
log(`⚡ [EVENT:${type}] ${JSON.stringify(msg)}`);
}
}
function broadcast(msg) {
const data = JSON.stringify(msg);
let sent = 0;
clients.forEach((client, id) => {
if (client.ws.readyState === WebSocket.OPEN && client.ready) {
client.ws.send(data);
sent++;
}
});
return sent;
}
function sendCommand(command, params = {}) {
const msg = {
type: 'command',
command,
params,
commandId: `cmd_${Date.now()}`,
timestamp: Date.now()
};
const sent = broadcast(msg);
if (sent === 0) {
commandQueue.push(msg);
log(`📤 [QUEUE] No ready clients, queued: ${command}`);
} else {
log(`📤 [COMMAND] Sent to ${sent} client(s): ${command}`);
}
return msg.commandId;
}
// ============================================
// HTTP API (for James to send commands)
// ============================================
const httpServer = http.createServer(async (req, res) => {
res.setHeader('Content-Type', 'application/json');
res.setHeader('Access-Control-Allow-Origin', '*');
if (req.method === 'OPTIONS') {
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
res.end();
return;
}
const body = await parseBody(req);
const url = req.url.split('?')[0];
switch (url) {
case '/health':
const readyClients = Array.from(clients.values()).filter(c => c.ready).length;
res.end(JSON.stringify({
status: 'ok',
protocol: PROTOCOL_VERSION,
clients: clients.size,
readyClients,
queuedCommands: commandQueue.length,
time: new Date().toISOString()
}));
break;
case '/clients':
const clientList = [];
clients.forEach((c, id) => clientList.push({
id,
ip: c.ip,
ready: c.ready,
clientInfo: c.clientInfo,
connectedAt: c.connectedAt,
uptime: Date.now() - c.connectedAt
}));
res.end(JSON.stringify({ clients: clientList }));
break;
case '/command':
if (req.method !== 'POST') {
res.statusCode = 405;
res.end(JSON.stringify({ error: 'POST required' }));
return;
}
const cmdId = sendCommand(body.command, body.params || {});
res.end(JSON.stringify({ status: 'sent', commandId: cmdId, clients: clients.size }));
break;
case '/notification/action':
if (req.method !== 'POST') {
res.statusCode = 405;
res.end(JSON.stringify({ error: 'POST required' }));
return;
}
const actionCmdId = sendCommand('notification.action', {
notificationId: body.notificationId || body.id,
action: body.action,
replyText: body.replyText || body.text
});
log(`🔔 [ACTION] Triggering "${body.action}" on ${body.notificationId || body.id}`);
res.end(JSON.stringify({ status: 'sent', commandId: actionCmdId }));
break;
case '/call/answer':
const answerCmdId = sendCommand('call.answer', { callId: body.callId });
log(`📞 [ANSWER] Answering call ${body.callId}`);
res.end(JSON.stringify({ status: 'sent', commandId: answerCmdId }));
break;
case '/call/reject':
const rejectCmdId = sendCommand('call.reject', { callId: body.callId });
log(`📞 [REJECT] Rejecting call ${body.callId}`);
res.end(JSON.stringify({ status: 'sent', commandId: rejectCmdId }));
break;
case '/call/hangup':
const hangupCmdId = sendCommand('call.hangup', { callId: body.callId });
log(`📞 [HANGUP] Hanging up call ${body.callId}`);
res.end(JSON.stringify({ status: 'sent', commandId: hangupCmdId }));
break;
default:
res.end(JSON.stringify({
name: 'ClawdNode Gateway',
protocol: PROTOCOL_VERSION,
endpoints: [
'GET /health - Server status',
'GET /clients - Connected clients',
'POST /command - Send raw command',
'POST /notification/action - Trigger notification action',
'POST /call/answer - Answer incoming call',
'POST /call/reject - Reject incoming call',
'POST /call/hangup - Hang up active call'
],
note: 'This is ClawdNode Gateway (custom protocol), NOT Clawdbot Gateway.'
}));
}
});
function parseBody(req) {
return new Promise((resolve) => {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
try { resolve(JSON.parse(body || '{}')); }
catch { resolve({}); }
});
});
}
httpServer.listen(HTTP_PORT, '0.0.0.0', () => {
log('================================================');
log('ClawdNode Gateway - Custom Protocol');
log(`Protocol: ${PROTOCOL_VERSION}`);
log('================================================');
log(`HTTP API: http://100.123.216.65:${HTTP_PORT}`);
log(`WebSocket: ws://100.123.216.65:${WS_PORT}`);
log(`Log file: ${LOG_FILE}`);
log('');
log('NOTE: This is NOT Clawdbot Gateway (port 18789).');
log(' Configure ClawdNode app to use port 9878.');
log('================================================');
});
// Export for potential module use
module.exports = { sendCommand, broadcast, onNotification, onCall };

10
watch-calls.sh Executable file
View File

@ -0,0 +1,10 @@
#!/bin/bash
LAST_CALL=""
while true; do
CALL=$(grep -o "INCOMING.*CallID.*" /tmp/clawdnode-gateway.log 2>/dev/null | tail -1)
if [ -n "$CALL" ] && [ "$CALL" != "$LAST_CALL" ]; then
echo "🚨 $CALL"
LAST_CALL="$CALL"
fi
sleep 1
done