indieclaw-agent 2.3.0 → 2.4.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/index.js +231 -137
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const { WebSocketServer } = require('ws');
|
|
4
|
+
const WebSocket = require('ws');
|
|
4
5
|
const { execSync, exec } = require('child_process');
|
|
5
6
|
const fs = require('fs');
|
|
6
7
|
const path = require('path');
|
|
@@ -8,6 +9,7 @@ const os = require('os');
|
|
|
8
9
|
const crypto = require('crypto');
|
|
9
10
|
const http = require('http');
|
|
10
11
|
const https = require('https');
|
|
12
|
+
const net = require('net');
|
|
11
13
|
|
|
12
14
|
// --- Version from package.json ---
|
|
13
15
|
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'));
|
|
@@ -89,7 +91,7 @@ if (TLS_ENABLED) {
|
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
const terminals = new Map(); // id -> pty process
|
|
92
|
-
const activeChats = new Map(); //
|
|
94
|
+
const activeChats = new Map(); // chatId -> { runId, _ws }
|
|
93
95
|
const activeSearches = new Map(); // ws -> child_process (one search per connection)
|
|
94
96
|
|
|
95
97
|
// --- Deep Link & QR Code ---
|
|
@@ -143,51 +145,197 @@ try {
|
|
|
143
145
|
// qrcode-terminal not installed, skip QR display
|
|
144
146
|
}
|
|
145
147
|
|
|
146
|
-
// --- OpenClaw Detection ---
|
|
147
|
-
|
|
148
|
+
// --- OpenClaw Detection, Config & Gateway Client ---
|
|
149
|
+
const OPENCLAW_CONFIG_PATHS = [
|
|
150
|
+
path.join(os.homedir(), '.openclaw', 'openclaw.json'),
|
|
151
|
+
path.join(os.homedir(), '.openclaw', 'openclaw.json5'),
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
function readOpenClawConfig() {
|
|
155
|
+
for (const cfgPath of OPENCLAW_CONFIG_PATHS) {
|
|
156
|
+
try {
|
|
157
|
+
if (!fs.existsSync(cfgPath)) continue;
|
|
158
|
+
let raw = fs.readFileSync(cfgPath, 'utf-8');
|
|
159
|
+
raw = raw.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
160
|
+
raw = raw.replace(/,\s*([\]}])/g, '$1');
|
|
161
|
+
const config = JSON.parse(raw);
|
|
162
|
+
const gw = config.gateway || {};
|
|
163
|
+
return {
|
|
164
|
+
port: gw.port || 18789,
|
|
165
|
+
token: gw.auth?.token || gw.auth?.password || null,
|
|
166
|
+
host: gw.bind || '127.0.0.1',
|
|
167
|
+
};
|
|
168
|
+
} catch {}
|
|
169
|
+
}
|
|
170
|
+
// Fallback: check env var
|
|
171
|
+
return {
|
|
172
|
+
port: parseInt(process.env.OPENCLAW_GATEWAY_PORT || '18789', 10),
|
|
173
|
+
token: process.env.OPENCLAW_GATEWAY_TOKEN || null,
|
|
174
|
+
host: '127.0.0.1',
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let openClawConfig = readOpenClawConfig();
|
|
179
|
+
|
|
180
|
+
// TCP port check — fast and reliable, no auth needed
|
|
181
|
+
function isPortListening(port, host = '127.0.0.1') {
|
|
148
182
|
return new Promise((resolve) => {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
183
|
+
const sock = net.createConnection({ port, host }, () => {
|
|
184
|
+
sock.destroy();
|
|
185
|
+
resolve(true);
|
|
186
|
+
});
|
|
187
|
+
sock.on('error', () => resolve(false));
|
|
188
|
+
sock.setTimeout(2000, () => { sock.destroy(); resolve(false); });
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function detectOpenClaw() {
|
|
193
|
+
openClawConfig = readOpenClawConfig();
|
|
194
|
+
const listening = await isPortListening(openClawConfig.port);
|
|
195
|
+
if (listening) {
|
|
196
|
+
return { available: true, models: ['openclaw'], port: openClawConfig.port };
|
|
197
|
+
}
|
|
198
|
+
// Fallback: check if config file exists (installed but not running)
|
|
199
|
+
const installed = OPENCLAW_CONFIG_PATHS.some((p) => fs.existsSync(p));
|
|
200
|
+
if (installed) {
|
|
201
|
+
return { available: false, models: [], port: openClawConfig.port, installed: true };
|
|
202
|
+
}
|
|
203
|
+
return { available: false, models: [], port: null };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// --- OpenClaw Gateway WebSocket Client ---
|
|
207
|
+
let ocGateway = null;
|
|
208
|
+
let ocReady = false;
|
|
209
|
+
const ocPending = new Map();
|
|
210
|
+
const ocChatCallbacks = new Map(); // runId -> { ws, chatId }
|
|
211
|
+
|
|
212
|
+
function connectOcGateway() {
|
|
213
|
+
return new Promise((resolve, reject) => {
|
|
214
|
+
openClawConfig = readOpenClawConfig();
|
|
215
|
+
const url = `ws://127.0.0.1:${openClawConfig.port}`;
|
|
216
|
+
|
|
217
|
+
if (ocGateway) { try { ocGateway.close(); } catch {} }
|
|
218
|
+
ocGateway = null;
|
|
219
|
+
ocReady = false;
|
|
220
|
+
|
|
221
|
+
const ws = new WebSocket(url);
|
|
222
|
+
let connectReqId = null;
|
|
223
|
+
const timeout = setTimeout(() => { ws.close(); reject(new Error('Gateway timeout')); }, 10000);
|
|
224
|
+
|
|
225
|
+
ws.on('message', (data) => {
|
|
226
|
+
let msg;
|
|
227
|
+
try { msg = JSON.parse(data.toString()); } catch { return; }
|
|
228
|
+
|
|
229
|
+
// Step 1: Gateway sends connect.challenge
|
|
230
|
+
if (msg.type === 'event' && msg.event === 'connect.challenge') {
|
|
231
|
+
connectReqId = crypto.randomUUID();
|
|
232
|
+
const params = {
|
|
233
|
+
minProtocol: 3, maxProtocol: 3,
|
|
234
|
+
client: { id: 'indieclaw-agent', version: VERSION, platform: os.platform(), mode: 'operator' },
|
|
235
|
+
role: 'operator',
|
|
236
|
+
scopes: ['operator.read', 'operator.write'],
|
|
237
|
+
};
|
|
238
|
+
if (openClawConfig.token) params.auth = { token: openClawConfig.token };
|
|
239
|
+
ws.send(JSON.stringify({ type: 'req', id: connectReqId, method: 'connect', params }));
|
|
240
|
+
return;
|
|
158
241
|
}
|
|
159
242
|
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
return;
|
|
243
|
+
// Step 2: Handle hello-ok response
|
|
244
|
+
if (msg.type === 'res' && msg.id === connectReqId) {
|
|
245
|
+
clearTimeout(timeout);
|
|
246
|
+
if (msg.ok) {
|
|
247
|
+
ocGateway = ws;
|
|
248
|
+
ocReady = true;
|
|
249
|
+
resolve(ws);
|
|
250
|
+
} else {
|
|
251
|
+
ws.close();
|
|
252
|
+
reject(new Error(msg.error?.message || 'Gateway auth failed'));
|
|
171
253
|
}
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
172
256
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
}
|
|
257
|
+
// Handle other req/res
|
|
258
|
+
if (msg.type === 'res' && msg.id) {
|
|
259
|
+
const pending = ocPending.get(msg.id);
|
|
260
|
+
if (pending) {
|
|
261
|
+
ocPending.delete(msg.id);
|
|
262
|
+
clearTimeout(pending.timeout);
|
|
263
|
+
if (msg.ok) pending.resolve(msg.payload);
|
|
264
|
+
else pending.reject(new Error(msg.error?.message || 'Request failed'));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Handle chat streaming events
|
|
269
|
+
if (msg.type === 'event' && msg.event === 'chat') {
|
|
270
|
+
const p = msg.payload;
|
|
271
|
+
const cb = ocChatCallbacks.get(p.runId);
|
|
272
|
+
if (!cb) return;
|
|
273
|
+
|
|
274
|
+
if (p.state === 'delta') {
|
|
275
|
+
// Extract text from delta — try common field paths
|
|
276
|
+
const text = typeof p.message === 'string' ? p.message
|
|
277
|
+
: p.message?.content || p.message?.text || '';
|
|
278
|
+
if (text) send(cb.ws, { type: 'chat.stream', id: cb.chatId, content: text });
|
|
279
|
+
} else if (p.state === 'final') {
|
|
280
|
+
const text = typeof p.message === 'string' ? p.message
|
|
281
|
+
: p.message?.content || p.message?.text || '';
|
|
282
|
+
if (text) send(cb.ws, { type: 'chat.stream', id: cb.chatId, content: text });
|
|
283
|
+
send(cb.ws, { type: 'chat.done', id: cb.chatId });
|
|
284
|
+
ocChatCallbacks.delete(p.runId);
|
|
285
|
+
} else if (p.state === 'error') {
|
|
286
|
+
send(cb.ws, { type: 'chat.done', id: cb.chatId, error: p.errorMessage || 'OpenClaw error' });
|
|
287
|
+
ocChatCallbacks.delete(p.runId);
|
|
288
|
+
} else if (p.state === 'aborted') {
|
|
289
|
+
send(cb.ws, { type: 'chat.done', id: cb.chatId });
|
|
290
|
+
ocChatCallbacks.delete(p.runId);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
183
293
|
});
|
|
294
|
+
|
|
295
|
+
ws.on('error', (err) => { clearTimeout(timeout); ocReady = false; reject(err); });
|
|
296
|
+
ws.on('close', () => {
|
|
297
|
+
ocReady = false;
|
|
298
|
+
ocGateway = null;
|
|
299
|
+
// Notify all pending chat streams that gateway disconnected
|
|
300
|
+
for (const [runId, cb] of ocChatCallbacks) {
|
|
301
|
+
send(cb.ws, { type: 'chat.done', id: cb.chatId, error: 'OpenClaw gateway disconnected' });
|
|
302
|
+
ocChatCallbacks.delete(runId);
|
|
303
|
+
}
|
|
304
|
+
// Reject all pending requests
|
|
305
|
+
for (const [id, p] of ocPending) {
|
|
306
|
+
clearTimeout(p.timeout);
|
|
307
|
+
p.reject(new Error('Gateway disconnected'));
|
|
308
|
+
ocPending.delete(id);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function getOcGateway() {
|
|
315
|
+
if (ocGateway && ocGateway.readyState === WebSocket.OPEN && ocReady) return ocGateway;
|
|
316
|
+
return connectOcGateway();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function ocRequest(method, params) {
|
|
320
|
+
return new Promise(async (resolve, reject) => {
|
|
321
|
+
try {
|
|
322
|
+
const ws = await getOcGateway();
|
|
323
|
+
const id = crypto.randomUUID();
|
|
324
|
+
const t = setTimeout(() => { ocPending.delete(id); reject(new Error('Timeout')); }, 30000);
|
|
325
|
+
ocPending.set(id, { resolve, reject, timeout: t });
|
|
326
|
+
ws.send(JSON.stringify({ type: 'req', id, method, params }));
|
|
327
|
+
} catch (err) { reject(err); }
|
|
184
328
|
});
|
|
185
329
|
}
|
|
186
330
|
|
|
187
331
|
// Run detection on startup
|
|
188
332
|
detectOpenClaw().then((oc) => {
|
|
189
333
|
if (oc.available) {
|
|
190
|
-
console.log(` [OpenClaw] Detected (gateway
|
|
334
|
+
console.log(` [OpenClaw] Detected (gateway on port ${oc.port})`);
|
|
335
|
+
// Pre-connect to gateway
|
|
336
|
+
connectOcGateway().catch(() => {});
|
|
337
|
+
} else if (oc.installed) {
|
|
338
|
+
console.log(` [OpenClaw] Installed but gateway not running (port ${oc.port})`);
|
|
191
339
|
} else {
|
|
192
340
|
console.log(' [OpenClaw] Not detected');
|
|
193
341
|
}
|
|
@@ -263,10 +411,13 @@ wss.on('connection', (ws) => {
|
|
|
263
411
|
}
|
|
264
412
|
}
|
|
265
413
|
// Clean up any active chat streams owned by this connection
|
|
266
|
-
for (const [
|
|
267
|
-
if (
|
|
268
|
-
|
|
269
|
-
|
|
414
|
+
for (const [chatId, chat] of activeChats) {
|
|
415
|
+
if (chat._ws === ws) {
|
|
416
|
+
if (chat.runId) {
|
|
417
|
+
ocRequest('chat.abort', { runId: chat.runId }).catch(() => {});
|
|
418
|
+
ocChatCallbacks.delete(chat.runId);
|
|
419
|
+
}
|
|
420
|
+
activeChats.delete(chatId);
|
|
270
421
|
}
|
|
271
422
|
}
|
|
272
423
|
// Kill any active search process for this connection
|
|
@@ -338,7 +489,7 @@ async function handleMessage(ws, msg) {
|
|
|
338
489
|
case 'terminal.stop':
|
|
339
490
|
return handleTerminalStop(ws, msg);
|
|
340
491
|
case 'chat.send':
|
|
341
|
-
return handleChatSend(ws, msg);
|
|
492
|
+
return await handleChatSend(ws, msg);
|
|
342
493
|
case 'chat.stop':
|
|
343
494
|
return handleChatStop(ws, msg);
|
|
344
495
|
case 'push.register':
|
|
@@ -951,104 +1102,45 @@ function handleTerminalStop(ws, { id }) {
|
|
|
951
1102
|
reply(ws, id, { stopped: true });
|
|
952
1103
|
}
|
|
953
1104
|
|
|
954
|
-
// --- Chat (OpenClaw Proxy) ---
|
|
955
|
-
function handleChatSend(ws, { id, messages
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
const body = JSON.stringify({
|
|
960
|
-
model: 'openclaw:main',
|
|
961
|
-
messages,
|
|
962
|
-
stream: true,
|
|
963
|
-
});
|
|
964
|
-
|
|
965
|
-
const req = http.request(
|
|
966
|
-
{
|
|
967
|
-
hostname: host,
|
|
968
|
-
port,
|
|
969
|
-
path: '/v1/chat/completions',
|
|
970
|
-
method: 'POST',
|
|
971
|
-
headers: {
|
|
972
|
-
'Content-Type': 'application/json',
|
|
973
|
-
Authorization: `Bearer ${openclawToken}`,
|
|
974
|
-
'x-openclaw-agent-id': 'main',
|
|
975
|
-
},
|
|
976
|
-
},
|
|
977
|
-
(res) => {
|
|
978
|
-
if (res.statusCode !== 200) {
|
|
979
|
-
let errorBody = '';
|
|
980
|
-
res.on('data', (chunk) => (errorBody += chunk));
|
|
981
|
-
res.on('end', () => {
|
|
982
|
-
send(ws, { type: 'chat.done', id, error: `OpenClaw error ${res.statusCode}: ${errorBody}` });
|
|
983
|
-
activeChats.delete(id);
|
|
984
|
-
});
|
|
985
|
-
return;
|
|
986
|
-
}
|
|
1105
|
+
// --- Chat (OpenClaw Gateway Proxy) ---
|
|
1106
|
+
async function handleChatSend(ws, { id, messages }) {
|
|
1107
|
+
try {
|
|
1108
|
+
// Connect to OpenClaw gateway (auto-reconnects if needed)
|
|
1109
|
+
await getOcGateway();
|
|
987
1110
|
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
const lines = buffer.split('\n');
|
|
994
|
-
buffer = lines.pop() || '';
|
|
995
|
-
|
|
996
|
-
for (const line of lines) {
|
|
997
|
-
const trimmed = line.trim();
|
|
998
|
-
if (!trimmed || trimmed.startsWith(':')) continue;
|
|
999
|
-
if (trimmed === 'data: [DONE]') {
|
|
1000
|
-
if (!sentDone) {
|
|
1001
|
-
sentDone = true;
|
|
1002
|
-
send(ws, { type: 'chat.done', id });
|
|
1003
|
-
activeChats.delete(id);
|
|
1004
|
-
}
|
|
1005
|
-
return;
|
|
1006
|
-
}
|
|
1007
|
-
if (trimmed.startsWith('data: ')) {
|
|
1008
|
-
try {
|
|
1009
|
-
const json = JSON.parse(trimmed.slice(6));
|
|
1010
|
-
const content = json.choices?.[0]?.delta?.content;
|
|
1011
|
-
if (content) {
|
|
1012
|
-
send(ws, { type: 'chat.stream', id, content });
|
|
1013
|
-
}
|
|
1014
|
-
} catch {}
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
});
|
|
1111
|
+
// Extract the last user message from the conversation
|
|
1112
|
+
const lastUserMsg = [...messages].reverse().find(m => m.role === 'user');
|
|
1113
|
+
if (!lastUserMsg) {
|
|
1114
|
+
return send(ws, { type: 'chat.done', id, error: 'No user message found' });
|
|
1115
|
+
}
|
|
1018
1116
|
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
send(ws, { type: 'chat.done', id });
|
|
1023
|
-
activeChats.delete(id);
|
|
1024
|
-
}
|
|
1025
|
-
});
|
|
1117
|
+
// Build a session key — unique per chat conversation
|
|
1118
|
+
const sessionKey = `agent:indieclaw:mobile:${id}`;
|
|
1119
|
+
const idempotencyKey = crypto.randomUUID();
|
|
1026
1120
|
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
});
|
|
1034
|
-
}
|
|
1035
|
-
);
|
|
1121
|
+
// Send chat request to gateway via WebSocket protocol
|
|
1122
|
+
const result = await ocRequest('chat.send', {
|
|
1123
|
+
sessionKey,
|
|
1124
|
+
message: { role: 'user', content: lastUserMsg.content },
|
|
1125
|
+
idempotencyKey,
|
|
1126
|
+
});
|
|
1036
1127
|
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1128
|
+
// Register for streaming events using the runId from the response
|
|
1129
|
+
const runId = result?.runId || result?.id || id;
|
|
1130
|
+
ocChatCallbacks.set(runId, { ws, chatId: id });
|
|
1131
|
+
activeChats.set(id, { runId, _ws: ws });
|
|
1041
1132
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
req.end();
|
|
1133
|
+
} catch (err) {
|
|
1134
|
+
send(ws, { type: 'chat.done', id, error: `OpenClaw: ${err.message}` });
|
|
1135
|
+
}
|
|
1046
1136
|
}
|
|
1047
1137
|
|
|
1048
1138
|
function handleChatStop(ws, { id, chatId }) {
|
|
1049
|
-
const
|
|
1050
|
-
if (
|
|
1051
|
-
|
|
1139
|
+
const chat = activeChats.get(chatId);
|
|
1140
|
+
if (chat && chat.runId) {
|
|
1141
|
+
// Abort the chat via OpenClaw gateway
|
|
1142
|
+
ocRequest('chat.abort', { runId: chat.runId }).catch(() => {});
|
|
1143
|
+
ocChatCallbacks.delete(chat.runId);
|
|
1052
1144
|
activeChats.delete(chatId);
|
|
1053
1145
|
}
|
|
1054
1146
|
reply(ws, id, { stopped: true });
|
|
@@ -1272,19 +1364,21 @@ function handleAgentLogs(ws, { id, lines = 200 }) {
|
|
|
1272
1364
|
}
|
|
1273
1365
|
|
|
1274
1366
|
// --- Graceful Shutdown ---
|
|
1275
|
-
|
|
1276
|
-
console.log('\n[agent] Shutting down...');
|
|
1367
|
+
function gracefulShutdown() {
|
|
1277
1368
|
for (const [, term] of terminals) term.kill();
|
|
1278
|
-
|
|
1369
|
+
activeChats.clear();
|
|
1370
|
+
ocChatCallbacks.clear();
|
|
1371
|
+
if (ocGateway) { try { ocGateway.close(); } catch {} }
|
|
1279
1372
|
wss.close();
|
|
1280
1373
|
if (server) server.close();
|
|
1281
1374
|
process.exit(0);
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
process.on('SIGINT', () => {
|
|
1378
|
+
console.log('\n[agent] Shutting down...');
|
|
1379
|
+
gracefulShutdown();
|
|
1282
1380
|
});
|
|
1283
1381
|
|
|
1284
1382
|
process.on('SIGTERM', () => {
|
|
1285
|
-
|
|
1286
|
-
for (const [, req] of activeChats) req.destroy();
|
|
1287
|
-
wss.close();
|
|
1288
|
-
if (server) server.close();
|
|
1289
|
-
process.exit(0);
|
|
1383
|
+
gracefulShutdown();
|
|
1290
1384
|
});
|