indieclaw-agent 1.3.3 → 2.0.0
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 +424 -8
- package/package.json +2 -1
package/index.js
CHANGED
|
@@ -7,11 +7,28 @@ const path = require('path');
|
|
|
7
7
|
const os = require('os');
|
|
8
8
|
const crypto = require('crypto');
|
|
9
9
|
const http = require('http');
|
|
10
|
+
const https = require('https');
|
|
11
|
+
|
|
12
|
+
// --- Version from package.json ---
|
|
13
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'));
|
|
14
|
+
const VERSION = pkg.version;
|
|
10
15
|
|
|
11
16
|
// --- Configuration ---
|
|
12
17
|
const PORT = parseInt(process.env.INDIECLAW_PORT || '3100', 10);
|
|
13
18
|
const TOKEN_FILE = path.join(os.homedir(), '.indieclaw-token');
|
|
14
19
|
|
|
20
|
+
// --- TLS Configuration ---
|
|
21
|
+
const TLS_ENABLED = process.env.INDIECLAW_TLS === '1';
|
|
22
|
+
const INDIECLAW_DIR = path.join(os.homedir(), '.indieclaw');
|
|
23
|
+
const DEFAULT_TLS_CERT = path.join(INDIECLAW_DIR, 'cert.pem');
|
|
24
|
+
const DEFAULT_TLS_KEY = path.join(INDIECLAW_DIR, 'key.pem');
|
|
25
|
+
const TLS_CERT_PATH = process.env.INDIECLAW_TLS_CERT || DEFAULT_TLS_CERT;
|
|
26
|
+
const TLS_KEY_PATH = process.env.INDIECLAW_TLS_KEY || DEFAULT_TLS_KEY;
|
|
27
|
+
|
|
28
|
+
// --- Push Notification Config ---
|
|
29
|
+
const EXPO_PUSH_URL = 'https://exp.host/--/api/v2/push/send';
|
|
30
|
+
const PUSH_TOKENS_FILE = path.join(INDIECLAW_DIR, 'push-tokens.json');
|
|
31
|
+
|
|
15
32
|
function getOrCreateToken() {
|
|
16
33
|
try {
|
|
17
34
|
return fs.readFileSync(TOKEN_FILE, 'utf-8').trim();
|
|
@@ -32,27 +49,174 @@ try {
|
|
|
32
49
|
console.log('[agent] node-pty not available — terminal feature disabled');
|
|
33
50
|
}
|
|
34
51
|
|
|
52
|
+
// --- TLS Setup ---
|
|
53
|
+
function ensureTlsCerts() {
|
|
54
|
+
if (!fs.existsSync(INDIECLAW_DIR)) {
|
|
55
|
+
fs.mkdirSync(INDIECLAW_DIR, { recursive: true, mode: 0o700 });
|
|
56
|
+
}
|
|
57
|
+
if (!fs.existsSync(TLS_CERT_PATH) || !fs.existsSync(TLS_KEY_PATH)) {
|
|
58
|
+
console.log('[agent] Generating self-signed TLS certificate...');
|
|
59
|
+
try {
|
|
60
|
+
execSync(
|
|
61
|
+
`openssl req -x509 -newkey rsa:2048 -keyout "${TLS_KEY_PATH}" -out "${TLS_CERT_PATH}" -days 365 -nodes -subj "/CN=indieclaw-agent"`,
|
|
62
|
+
{ timeout: 15000, stdio: 'pipe' }
|
|
63
|
+
);
|
|
64
|
+
fs.chmodSync(TLS_KEY_PATH, 0o600);
|
|
65
|
+
fs.chmodSync(TLS_CERT_PATH, 0o644);
|
|
66
|
+
console.log('[agent] Self-signed certificate generated.');
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.error('[agent] Failed to generate TLS certificate:', err.message);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
35
74
|
// --- WebSocket Server ---
|
|
36
|
-
|
|
75
|
+
let wss;
|
|
76
|
+
let server;
|
|
77
|
+
|
|
78
|
+
if (TLS_ENABLED) {
|
|
79
|
+
ensureTlsCerts();
|
|
80
|
+
const tlsOptions = {
|
|
81
|
+
cert: fs.readFileSync(TLS_CERT_PATH),
|
|
82
|
+
key: fs.readFileSync(TLS_KEY_PATH),
|
|
83
|
+
};
|
|
84
|
+
server = https.createServer(tlsOptions);
|
|
85
|
+
wss = new WebSocketServer({ server });
|
|
86
|
+
server.listen(PORT);
|
|
87
|
+
} else {
|
|
88
|
+
wss = new WebSocketServer({ port: PORT });
|
|
89
|
+
}
|
|
90
|
+
|
|
37
91
|
const terminals = new Map(); // id -> pty process
|
|
38
92
|
const activeChats = new Map(); // id -> http.ClientRequest
|
|
39
93
|
|
|
94
|
+
// --- Deep Link & QR Code ---
|
|
95
|
+
function getMachineIP() {
|
|
96
|
+
// Try Tailscale first
|
|
97
|
+
try {
|
|
98
|
+
const tsIP = execSync('tailscale ip -4', { timeout: 3000, stdio: 'pipe' }).toString().trim();
|
|
99
|
+
if (tsIP) return tsIP;
|
|
100
|
+
} catch {}
|
|
101
|
+
|
|
102
|
+
// Fallback to network interfaces
|
|
103
|
+
const ifaces = os.networkInterfaces();
|
|
104
|
+
for (const name of Object.keys(ifaces)) {
|
|
105
|
+
for (const iface of ifaces[name]) {
|
|
106
|
+
if (iface.family === 'IPv4' && !iface.internal) {
|
|
107
|
+
return iface.address;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return '127.0.0.1';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const machineIP = getMachineIP();
|
|
115
|
+
const deepLink = `indieclaw://connect?host=${machineIP}&port=${PORT}&token=${AUTH_TOKEN}&name=${os.hostname()}&tls=${TLS_ENABLED ? '1' : '0'}`;
|
|
116
|
+
|
|
40
117
|
console.log('');
|
|
41
118
|
console.log(' ╔═══════════════════════════════════════╗');
|
|
42
|
-
console.log(
|
|
119
|
+
console.log(` ║ IndieClaw Agent v${VERSION.padEnd(16)}║`);
|
|
43
120
|
console.log(' ╠═══════════════════════════════════════╣');
|
|
44
|
-
console.log(` ║ Port: ${PORT}
|
|
121
|
+
console.log(` ║ Port: ${String(PORT).padEnd(30)}║`);
|
|
45
122
|
console.log(` ║ Token: ${AUTH_TOKEN.substring(0, 12)}... ║`);
|
|
123
|
+
console.log(` ║ TLS: ${TLS_ENABLED ? 'Enabled ' : 'Disabled'} ║`);
|
|
46
124
|
console.log(' ╚═══════════════════════════════════════╝');
|
|
47
125
|
console.log('');
|
|
48
126
|
console.log(` Full token: ${AUTH_TOKEN}`);
|
|
49
127
|
console.log(' Enter this token in the IndieClaw mobile app to connect.');
|
|
50
128
|
console.log('');
|
|
129
|
+
console.log(` Deep link: ${deepLink}`);
|
|
130
|
+
console.log('');
|
|
131
|
+
|
|
132
|
+
// Try to show QR code (optional dependency)
|
|
133
|
+
try {
|
|
134
|
+
const qrcode = require('qrcode-terminal');
|
|
135
|
+
qrcode.generate(deepLink, { small: true }, (qr) => {
|
|
136
|
+
console.log(' Scan this QR code with your phone:');
|
|
137
|
+
console.log('');
|
|
138
|
+
qr.split('\n').forEach((line) => console.log(' ' + line));
|
|
139
|
+
console.log('');
|
|
140
|
+
});
|
|
141
|
+
} catch {
|
|
142
|
+
// qrcode-terminal not installed, skip QR display
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// --- OpenClaw Detection ---
|
|
146
|
+
function detectOpenClaw() {
|
|
147
|
+
return new Promise((resolve) => {
|
|
148
|
+
const req = http.request(
|
|
149
|
+
{
|
|
150
|
+
hostname: '127.0.0.1',
|
|
151
|
+
port: 18789,
|
|
152
|
+
path: '/v1/models',
|
|
153
|
+
method: 'GET',
|
|
154
|
+
timeout: 2000,
|
|
155
|
+
},
|
|
156
|
+
(res) => {
|
|
157
|
+
let body = '';
|
|
158
|
+
res.on('data', (chunk) => (body += chunk));
|
|
159
|
+
res.on('end', () => {
|
|
160
|
+
try {
|
|
161
|
+
const json = JSON.parse(body);
|
|
162
|
+
const models = (json.data || []).map((m) => m.id);
|
|
163
|
+
resolve({ available: true, models });
|
|
164
|
+
} catch {
|
|
165
|
+
resolve({ available: false, models: [] });
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
);
|
|
170
|
+
req.on('timeout', () => {
|
|
171
|
+
req.destroy();
|
|
172
|
+
resolve({ available: false, models: [] });
|
|
173
|
+
});
|
|
174
|
+
req.on('error', () => {
|
|
175
|
+
resolve({ available: false, models: [] });
|
|
176
|
+
});
|
|
177
|
+
req.end();
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Run detection on startup
|
|
182
|
+
detectOpenClaw().then((oc) => {
|
|
183
|
+
if (oc.available) {
|
|
184
|
+
console.log(` [OpenClaw] Detected! Models: ${oc.models.join(', ')}`);
|
|
185
|
+
} else {
|
|
186
|
+
console.log(' [OpenClaw] Not detected on port 18789');
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// --- Push Notification Helpers ---
|
|
191
|
+
function loadPushTokens() {
|
|
192
|
+
try {
|
|
193
|
+
return JSON.parse(fs.readFileSync(PUSH_TOKENS_FILE, 'utf-8'));
|
|
194
|
+
} catch {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function savePushTokens(tokens) {
|
|
200
|
+
if (!fs.existsSync(INDIECLAW_DIR)) {
|
|
201
|
+
fs.mkdirSync(INDIECLAW_DIR, { recursive: true, mode: 0o700 });
|
|
202
|
+
}
|
|
203
|
+
fs.writeFileSync(PUSH_TOKENS_FILE, JSON.stringify(tokens), 'utf-8');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function sendPushNotification(title, body, data = {}) {
|
|
207
|
+
const tokens = loadPushTokens();
|
|
208
|
+
if (!tokens.length) return;
|
|
209
|
+
const payload = JSON.stringify(tokens.map(to => ({ to, title, body, data, sound: 'default' })));
|
|
210
|
+
const req = https.request(EXPO_PUSH_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' } });
|
|
211
|
+
req.on('error', () => {});
|
|
212
|
+
req.write(payload);
|
|
213
|
+
req.end();
|
|
214
|
+
}
|
|
51
215
|
|
|
52
216
|
wss.on('connection', (ws) => {
|
|
53
217
|
let authenticated = false;
|
|
54
218
|
|
|
55
|
-
ws.on('message', (raw) => {
|
|
219
|
+
ws.on('message', async (raw) => {
|
|
56
220
|
let msg;
|
|
57
221
|
try {
|
|
58
222
|
msg = JSON.parse(raw.toString());
|
|
@@ -64,7 +228,8 @@ wss.on('connection', (ws) => {
|
|
|
64
228
|
if (!authenticated) {
|
|
65
229
|
if (msg.type === 'auth' && msg.token === AUTH_TOKEN) {
|
|
66
230
|
authenticated = true;
|
|
67
|
-
|
|
231
|
+
const openclaw = await detectOpenClaw();
|
|
232
|
+
return ws.send(JSON.stringify({ type: 'auth', success: true, openclaw }));
|
|
68
233
|
}
|
|
69
234
|
if (msg.type === 'ping') {
|
|
70
235
|
return ws.send(JSON.stringify({ type: 'pong' }));
|
|
@@ -162,6 +327,20 @@ async function handleMessage(ws, msg) {
|
|
|
162
327
|
return handleChatSend(ws, msg);
|
|
163
328
|
case 'chat.stop':
|
|
164
329
|
return handleChatStop(ws, msg);
|
|
330
|
+
case 'push.register':
|
|
331
|
+
return handlePushRegister(ws, msg);
|
|
332
|
+
case 'push.unregister':
|
|
333
|
+
return handlePushUnregister(ws, msg);
|
|
334
|
+
case 'logs.system':
|
|
335
|
+
return handleLogsSystem(ws, msg);
|
|
336
|
+
case 'logs.search':
|
|
337
|
+
return handleLogsSearch(ws, msg);
|
|
338
|
+
case 'cron.history':
|
|
339
|
+
return handleCronHistory(ws, msg);
|
|
340
|
+
case 'agent.list':
|
|
341
|
+
return handleAgentList(ws, msg);
|
|
342
|
+
case 'agent.logs':
|
|
343
|
+
return handleAgentLogs(ws, msg);
|
|
165
344
|
default:
|
|
166
345
|
return replyError(ws, id, `Unknown message type: ${type}`);
|
|
167
346
|
}
|
|
@@ -541,9 +720,19 @@ echo "[$(date)] Done (exit code: $?)"`;
|
|
|
541
720
|
const scriptPath = path.join(os.tmpdir(), `indieclaw-update${ext}`);
|
|
542
721
|
fs.writeFileSync(scriptPath, script, { mode: 0o755 });
|
|
543
722
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
723
|
+
let child;
|
|
724
|
+
if (platform === 'win32') {
|
|
725
|
+
child = spawn('cmd', ['/c', scriptPath], { detached: true, stdio: 'ignore', windowsHide: true });
|
|
726
|
+
} else if (platform === 'linux') {
|
|
727
|
+
// systemd-run launches the script in a separate cgroup scope,
|
|
728
|
+
// so it won't be killed when systemd restarts the agent service
|
|
729
|
+
child = spawn('systemd-run', ['--scope', '--unit=indieclaw-update', 'bash', scriptPath], {
|
|
730
|
+
detached: true,
|
|
731
|
+
stdio: 'ignore'
|
|
732
|
+
});
|
|
733
|
+
} else {
|
|
734
|
+
child = spawn('bash', [scriptPath], { detached: true, stdio: 'ignore' });
|
|
735
|
+
}
|
|
547
736
|
child.unref();
|
|
548
737
|
|
|
549
738
|
reply(ws, id, { updating: true });
|
|
@@ -851,12 +1040,238 @@ function handleChatStop(ws, { id, chatId }) {
|
|
|
851
1040
|
reply(ws, id, { stopped: true });
|
|
852
1041
|
}
|
|
853
1042
|
|
|
1043
|
+
// --- Push Registration ---
|
|
1044
|
+
function handlePushRegister(ws, { id, pushToken }) {
|
|
1045
|
+
const tokens = loadPushTokens();
|
|
1046
|
+
if (!tokens.includes(pushToken)) {
|
|
1047
|
+
tokens.push(pushToken);
|
|
1048
|
+
savePushTokens(tokens);
|
|
1049
|
+
}
|
|
1050
|
+
reply(ws, id, { registered: true });
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function handlePushUnregister(ws, { id, pushToken }) {
|
|
1054
|
+
let tokens = loadPushTokens();
|
|
1055
|
+
tokens = tokens.filter((t) => t !== pushToken);
|
|
1056
|
+
savePushTokens(tokens);
|
|
1057
|
+
reply(ws, id, { unregistered: true });
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// --- System Logs ---
|
|
1061
|
+
function handleLogsSystem(ws, { id, lines = 200 }) {
|
|
1062
|
+
const platform = os.platform();
|
|
1063
|
+
|
|
1064
|
+
if (platform === 'linux') {
|
|
1065
|
+
exec(`journalctl -n ${lines} --no-pager -o json`, { timeout: 10000, maxBuffer: 1024 * 1024 * 5 }, (err, stdout) => {
|
|
1066
|
+
if (err) return replyError(ws, id, err.message);
|
|
1067
|
+
try {
|
|
1068
|
+
const entries = stdout.trim().split('\n').filter(Boolean).map((line) => {
|
|
1069
|
+
const j = JSON.parse(line);
|
|
1070
|
+
return {
|
|
1071
|
+
timestamp: j.__REALTIME_TIMESTAMP ? new Date(parseInt(j.__REALTIME_TIMESTAMP, 10) / 1000).toISOString() : null,
|
|
1072
|
+
level: j.PRIORITY || 'info',
|
|
1073
|
+
message: j.MESSAGE || '',
|
|
1074
|
+
source: j.SYSLOG_IDENTIFIER || j._COMM || '',
|
|
1075
|
+
};
|
|
1076
|
+
});
|
|
1077
|
+
reply(ws, id, entries);
|
|
1078
|
+
} catch (e) {
|
|
1079
|
+
replyError(ws, id, 'Failed to parse journal entries: ' + e.message);
|
|
1080
|
+
}
|
|
1081
|
+
});
|
|
1082
|
+
} else {
|
|
1083
|
+
// macOS
|
|
1084
|
+
exec(`log show --last 1h --style json | head -${lines}`, { timeout: 15000, maxBuffer: 1024 * 1024 * 10 }, (err, stdout) => {
|
|
1085
|
+
if (err) {
|
|
1086
|
+
// Fallback: try system.log
|
|
1087
|
+
return exec(`tail -${lines} /var/log/system.log`, { timeout: 5000 }, (err2, stdout2) => {
|
|
1088
|
+
if (err2) return replyError(ws, id, err2.message);
|
|
1089
|
+
const entries = stdout2.trim().split('\n').filter(Boolean).map((line) => ({
|
|
1090
|
+
timestamp: null,
|
|
1091
|
+
level: 'info',
|
|
1092
|
+
message: line,
|
|
1093
|
+
source: '',
|
|
1094
|
+
}));
|
|
1095
|
+
reply(ws, id, entries);
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
try {
|
|
1099
|
+
const entries = stdout.trim().split('\n').filter(Boolean).map((line) => {
|
|
1100
|
+
try {
|
|
1101
|
+
const j = JSON.parse(line);
|
|
1102
|
+
return {
|
|
1103
|
+
timestamp: j.timestamp || null,
|
|
1104
|
+
level: j.messageType || 'info',
|
|
1105
|
+
message: j.eventMessage || '',
|
|
1106
|
+
source: j.senderImagePath || j.processImagePath || '',
|
|
1107
|
+
};
|
|
1108
|
+
} catch {
|
|
1109
|
+
return { timestamp: null, level: 'info', message: line, source: '' };
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1112
|
+
reply(ws, id, entries);
|
|
1113
|
+
} catch (e) {
|
|
1114
|
+
replyError(ws, id, 'Failed to parse log entries: ' + e.message);
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// --- Log Search ---
|
|
1121
|
+
function handleLogsSearch(ws, { id, query, lines = 100 }) {
|
|
1122
|
+
const platform = os.platform();
|
|
1123
|
+
// Sanitize query to prevent command injection
|
|
1124
|
+
const safeQuery = query.replace(/["`$\\]/g, '');
|
|
1125
|
+
|
|
1126
|
+
if (platform === 'linux') {
|
|
1127
|
+
exec(`journalctl -n ${lines} --no-pager -g "${safeQuery}"`, { timeout: 10000, maxBuffer: 1024 * 1024 * 5 }, (err, stdout) => {
|
|
1128
|
+
if (err) return replyError(ws, id, err.message);
|
|
1129
|
+
const entries = stdout.trim().split('\n').filter(Boolean).map((line) => ({
|
|
1130
|
+
timestamp: null,
|
|
1131
|
+
level: 'info',
|
|
1132
|
+
message: line,
|
|
1133
|
+
source: '',
|
|
1134
|
+
}));
|
|
1135
|
+
reply(ws, id, entries);
|
|
1136
|
+
});
|
|
1137
|
+
} else {
|
|
1138
|
+
// macOS
|
|
1139
|
+
exec(`grep -i "${safeQuery}" /var/log/system.log | tail -${lines}`, { timeout: 10000 }, (err, stdout) => {
|
|
1140
|
+
if (err) return replyError(ws, id, err.message || 'No matches found');
|
|
1141
|
+
const entries = stdout.trim().split('\n').filter(Boolean).map((line) => ({
|
|
1142
|
+
timestamp: null,
|
|
1143
|
+
level: 'info',
|
|
1144
|
+
message: line,
|
|
1145
|
+
source: '',
|
|
1146
|
+
}));
|
|
1147
|
+
reply(ws, id, entries);
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// --- Cron History ---
|
|
1153
|
+
function handleCronHistory(ws, { id }) {
|
|
1154
|
+
const platform = os.platform();
|
|
1155
|
+
|
|
1156
|
+
if (platform === 'linux') {
|
|
1157
|
+
exec('journalctl -u cron -n 50 --no-pager -o json', { timeout: 10000, maxBuffer: 1024 * 1024 * 5 }, (err, stdout) => {
|
|
1158
|
+
if (err) return replyError(ws, id, err.message);
|
|
1159
|
+
try {
|
|
1160
|
+
const entries = stdout.trim().split('\n').filter(Boolean).map((line) => {
|
|
1161
|
+
const j = JSON.parse(line);
|
|
1162
|
+
return {
|
|
1163
|
+
timestamp: j.__REALTIME_TIMESTAMP ? new Date(parseInt(j.__REALTIME_TIMESTAMP, 10) / 1000).toISOString() : null,
|
|
1164
|
+
command: j.MESSAGE || '',
|
|
1165
|
+
exitCode: null,
|
|
1166
|
+
output: j.MESSAGE || '',
|
|
1167
|
+
};
|
|
1168
|
+
});
|
|
1169
|
+
reply(ws, id, entries);
|
|
1170
|
+
} catch (e) {
|
|
1171
|
+
replyError(ws, id, 'Failed to parse cron history: ' + e.message);
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1174
|
+
} else {
|
|
1175
|
+
// macOS
|
|
1176
|
+
exec('grep CRON /var/log/system.log | tail -50', { timeout: 5000 }, (err, stdout) => {
|
|
1177
|
+
if (err) {
|
|
1178
|
+
// No cron entries or file not accessible
|
|
1179
|
+
return reply(ws, id, []);
|
|
1180
|
+
}
|
|
1181
|
+
const entries = stdout.trim().split('\n').filter(Boolean).map((line) => ({
|
|
1182
|
+
timestamp: null,
|
|
1183
|
+
command: line,
|
|
1184
|
+
exitCode: null,
|
|
1185
|
+
output: line,
|
|
1186
|
+
}));
|
|
1187
|
+
reply(ws, id, entries);
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// --- Agent List (OpenClaw Models) ---
|
|
1193
|
+
function handleAgentList(ws, { id }) {
|
|
1194
|
+
const req = http.request(
|
|
1195
|
+
{
|
|
1196
|
+
hostname: '127.0.0.1',
|
|
1197
|
+
port: 18789,
|
|
1198
|
+
path: '/v1/models',
|
|
1199
|
+
method: 'GET',
|
|
1200
|
+
timeout: 2000,
|
|
1201
|
+
},
|
|
1202
|
+
(res) => {
|
|
1203
|
+
let body = '';
|
|
1204
|
+
res.on('data', (chunk) => (body += chunk));
|
|
1205
|
+
res.on('end', () => {
|
|
1206
|
+
try {
|
|
1207
|
+
const json = JSON.parse(body);
|
|
1208
|
+
const models = (json.data || []).map((m) => ({
|
|
1209
|
+
id: m.id,
|
|
1210
|
+
name: m.id,
|
|
1211
|
+
status: 'running',
|
|
1212
|
+
port: 18789,
|
|
1213
|
+
}));
|
|
1214
|
+
reply(ws, id, models);
|
|
1215
|
+
} catch {
|
|
1216
|
+
reply(ws, id, []);
|
|
1217
|
+
}
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
);
|
|
1221
|
+
req.on('timeout', () => {
|
|
1222
|
+
req.destroy();
|
|
1223
|
+
reply(ws, id, []);
|
|
1224
|
+
});
|
|
1225
|
+
req.on('error', () => {
|
|
1226
|
+
reply(ws, id, []);
|
|
1227
|
+
});
|
|
1228
|
+
req.end();
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// --- Agent Logs ---
|
|
1232
|
+
function handleAgentLogs(ws, { id, lines = 200 }) {
|
|
1233
|
+
const platform = os.platform();
|
|
1234
|
+
|
|
1235
|
+
if (platform === 'linux') {
|
|
1236
|
+
exec(`journalctl -u openclaw -n ${lines} --no-pager`, { timeout: 10000, maxBuffer: 1024 * 1024 * 5 }, (err, stdout) => {
|
|
1237
|
+
if (err) return replyError(ws, id, err.message);
|
|
1238
|
+
reply(ws, id, { logs: stdout || '' });
|
|
1239
|
+
});
|
|
1240
|
+
} else {
|
|
1241
|
+
// macOS / fallback: try common log locations
|
|
1242
|
+
const logLocations = [
|
|
1243
|
+
path.join(os.homedir(), '.openclaw', 'logs', 'openclaw.log'),
|
|
1244
|
+
path.join(os.homedir(), '.openclaw', 'openclaw.log'),
|
|
1245
|
+
'/var/log/openclaw.log',
|
|
1246
|
+
];
|
|
1247
|
+
|
|
1248
|
+
let found = false;
|
|
1249
|
+
for (const logPath of logLocations) {
|
|
1250
|
+
try {
|
|
1251
|
+
if (fs.existsSync(logPath)) {
|
|
1252
|
+
exec(`tail -${lines} "${logPath}"`, { timeout: 5000 }, (err, stdout) => {
|
|
1253
|
+
if (err) return reply(ws, id, { logs: '' });
|
|
1254
|
+
reply(ws, id, { logs: stdout || '' });
|
|
1255
|
+
});
|
|
1256
|
+
found = true;
|
|
1257
|
+
break;
|
|
1258
|
+
}
|
|
1259
|
+
} catch {}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
if (!found) {
|
|
1263
|
+
reply(ws, id, { logs: '' });
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
854
1268
|
// --- Graceful Shutdown ---
|
|
855
1269
|
process.on('SIGINT', () => {
|
|
856
1270
|
console.log('\n[agent] Shutting down...');
|
|
857
1271
|
for (const [, term] of terminals) term.kill();
|
|
858
1272
|
for (const [, req] of activeChats) req.destroy();
|
|
859
1273
|
wss.close();
|
|
1274
|
+
if (server) server.close();
|
|
860
1275
|
process.exit(0);
|
|
861
1276
|
});
|
|
862
1277
|
|
|
@@ -864,5 +1279,6 @@ process.on('SIGTERM', () => {
|
|
|
864
1279
|
for (const [, term] of terminals) term.kill();
|
|
865
1280
|
for (const [, req] of activeChats) req.destroy();
|
|
866
1281
|
wss.close();
|
|
1282
|
+
if (server) server.close();
|
|
867
1283
|
process.exit(0);
|
|
868
1284
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "indieclaw-agent",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Manage your server from your phone. Agent for the IndieClaw mobile app.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"node": ">=18.0.0"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
+
"qrcode-terminal": "^0.12.0",
|
|
37
38
|
"ws": "^8.0.0"
|
|
38
39
|
},
|
|
39
40
|
"optionalDependencies": {
|