indieclaw-agent 1.3.3 → 2.1.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 +416 -8
- package/install.sh +63 -22
- 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,190 @@ 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
|
+
const OPENCLAW_PORTS = [18789, 8080, 11434, 1234, 8000];
|
|
147
|
+
|
|
148
|
+
function tryOpenClawPort(port) {
|
|
149
|
+
return new Promise((resolve) => {
|
|
150
|
+
const req = http.request(
|
|
151
|
+
{
|
|
152
|
+
hostname: '127.0.0.1',
|
|
153
|
+
port,
|
|
154
|
+
path: '/v1/models',
|
|
155
|
+
method: 'GET',
|
|
156
|
+
timeout: 2000,
|
|
157
|
+
},
|
|
158
|
+
(res) => {
|
|
159
|
+
let body = '';
|
|
160
|
+
res.on('data', (chunk) => (body += chunk));
|
|
161
|
+
res.on('end', () => {
|
|
162
|
+
try {
|
|
163
|
+
const json = JSON.parse(body);
|
|
164
|
+
const models = (json.data || []).map((m) => m.id);
|
|
165
|
+
if (models.length > 0) {
|
|
166
|
+
resolve({ available: true, models, port });
|
|
167
|
+
} else {
|
|
168
|
+
resolve({ available: false, models: [], port });
|
|
169
|
+
}
|
|
170
|
+
} catch {
|
|
171
|
+
resolve({ available: false, models: [], port });
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
);
|
|
176
|
+
req.on('timeout', () => {
|
|
177
|
+
req.destroy();
|
|
178
|
+
resolve({ available: false, models: [], port });
|
|
179
|
+
});
|
|
180
|
+
req.on('error', () => {
|
|
181
|
+
resolve({ available: false, models: [], port });
|
|
182
|
+
});
|
|
183
|
+
req.end();
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function detectOpenClaw() {
|
|
188
|
+
// Try all common ports in parallel
|
|
189
|
+
const results = await Promise.all(OPENCLAW_PORTS.map(tryOpenClawPort));
|
|
190
|
+
const found = results.find((r) => r.available);
|
|
191
|
+
if (found) {
|
|
192
|
+
return { available: true, models: found.models, port: found.port };
|
|
193
|
+
}
|
|
194
|
+
return { available: false, models: [], port: null };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Run detection on startup
|
|
198
|
+
detectOpenClaw().then((oc) => {
|
|
199
|
+
if (oc.available) {
|
|
200
|
+
console.log(` [OpenClaw] Detected on port ${oc.port}! Models: ${oc.models.join(', ')}`);
|
|
201
|
+
} else {
|
|
202
|
+
console.log(` [OpenClaw] Not detected on ports ${OPENCLAW_PORTS.join(', ')}`);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// --- Push Notification Helpers ---
|
|
207
|
+
function loadPushTokens() {
|
|
208
|
+
try {
|
|
209
|
+
return JSON.parse(fs.readFileSync(PUSH_TOKENS_FILE, 'utf-8'));
|
|
210
|
+
} catch {
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function savePushTokens(tokens) {
|
|
216
|
+
if (!fs.existsSync(INDIECLAW_DIR)) {
|
|
217
|
+
fs.mkdirSync(INDIECLAW_DIR, { recursive: true, mode: 0o700 });
|
|
218
|
+
}
|
|
219
|
+
fs.writeFileSync(PUSH_TOKENS_FILE, JSON.stringify(tokens), 'utf-8');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function sendPushNotification(title, body, data = {}) {
|
|
223
|
+
const tokens = loadPushTokens();
|
|
224
|
+
if (!tokens.length) return;
|
|
225
|
+
const payload = JSON.stringify(tokens.map(to => ({ to, title, body, data, sound: 'default' })));
|
|
226
|
+
const req = https.request(EXPO_PUSH_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' } });
|
|
227
|
+
req.on('error', () => {});
|
|
228
|
+
req.write(payload);
|
|
229
|
+
req.end();
|
|
230
|
+
}
|
|
51
231
|
|
|
52
232
|
wss.on('connection', (ws) => {
|
|
53
233
|
let authenticated = false;
|
|
54
234
|
|
|
55
|
-
ws.on('message', (raw) => {
|
|
235
|
+
ws.on('message', async (raw) => {
|
|
56
236
|
let msg;
|
|
57
237
|
try {
|
|
58
238
|
msg = JSON.parse(raw.toString());
|
|
@@ -64,7 +244,8 @@ wss.on('connection', (ws) => {
|
|
|
64
244
|
if (!authenticated) {
|
|
65
245
|
if (msg.type === 'auth' && msg.token === AUTH_TOKEN) {
|
|
66
246
|
authenticated = true;
|
|
67
|
-
|
|
247
|
+
const openclaw = await detectOpenClaw();
|
|
248
|
+
return ws.send(JSON.stringify({ type: 'auth', success: true, openclaw }));
|
|
68
249
|
}
|
|
69
250
|
if (msg.type === 'ping') {
|
|
70
251
|
return ws.send(JSON.stringify({ type: 'pong' }));
|
|
@@ -162,6 +343,20 @@ async function handleMessage(ws, msg) {
|
|
|
162
343
|
return handleChatSend(ws, msg);
|
|
163
344
|
case 'chat.stop':
|
|
164
345
|
return handleChatStop(ws, msg);
|
|
346
|
+
case 'push.register':
|
|
347
|
+
return handlePushRegister(ws, msg);
|
|
348
|
+
case 'push.unregister':
|
|
349
|
+
return handlePushUnregister(ws, msg);
|
|
350
|
+
case 'logs.system':
|
|
351
|
+
return handleLogsSystem(ws, msg);
|
|
352
|
+
case 'logs.search':
|
|
353
|
+
return handleLogsSearch(ws, msg);
|
|
354
|
+
case 'cron.history':
|
|
355
|
+
return handleCronHistory(ws, msg);
|
|
356
|
+
case 'agent.list':
|
|
357
|
+
return handleAgentList(ws, msg);
|
|
358
|
+
case 'agent.logs':
|
|
359
|
+
return handleAgentLogs(ws, msg);
|
|
165
360
|
default:
|
|
166
361
|
return replyError(ws, id, `Unknown message type: ${type}`);
|
|
167
362
|
}
|
|
@@ -541,9 +736,19 @@ echo "[$(date)] Done (exit code: $?)"`;
|
|
|
541
736
|
const scriptPath = path.join(os.tmpdir(), `indieclaw-update${ext}`);
|
|
542
737
|
fs.writeFileSync(scriptPath, script, { mode: 0o755 });
|
|
543
738
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
739
|
+
let child;
|
|
740
|
+
if (platform === 'win32') {
|
|
741
|
+
child = spawn('cmd', ['/c', scriptPath], { detached: true, stdio: 'ignore', windowsHide: true });
|
|
742
|
+
} else if (platform === 'linux') {
|
|
743
|
+
// systemd-run launches the script in a separate cgroup scope,
|
|
744
|
+
// so it won't be killed when systemd restarts the agent service
|
|
745
|
+
child = spawn('systemd-run', ['--scope', '--unit=indieclaw-update', 'bash', scriptPath], {
|
|
746
|
+
detached: true,
|
|
747
|
+
stdio: 'ignore'
|
|
748
|
+
});
|
|
749
|
+
} else {
|
|
750
|
+
child = spawn('bash', [scriptPath], { detached: true, stdio: 'ignore' });
|
|
751
|
+
}
|
|
547
752
|
child.unref();
|
|
548
753
|
|
|
549
754
|
reply(ws, id, { updating: true });
|
|
@@ -851,12 +1056,214 @@ function handleChatStop(ws, { id, chatId }) {
|
|
|
851
1056
|
reply(ws, id, { stopped: true });
|
|
852
1057
|
}
|
|
853
1058
|
|
|
1059
|
+
// --- Push Registration ---
|
|
1060
|
+
function handlePushRegister(ws, { id, pushToken }) {
|
|
1061
|
+
const tokens = loadPushTokens();
|
|
1062
|
+
if (!tokens.includes(pushToken)) {
|
|
1063
|
+
tokens.push(pushToken);
|
|
1064
|
+
savePushTokens(tokens);
|
|
1065
|
+
}
|
|
1066
|
+
reply(ws, id, { registered: true });
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
function handlePushUnregister(ws, { id, pushToken }) {
|
|
1070
|
+
let tokens = loadPushTokens();
|
|
1071
|
+
tokens = tokens.filter((t) => t !== pushToken);
|
|
1072
|
+
savePushTokens(tokens);
|
|
1073
|
+
reply(ws, id, { unregistered: true });
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// --- System Logs ---
|
|
1077
|
+
function handleLogsSystem(ws, { id, lines = 200 }) {
|
|
1078
|
+
const platform = os.platform();
|
|
1079
|
+
|
|
1080
|
+
if (platform === 'linux') {
|
|
1081
|
+
exec(`journalctl -n ${lines} --no-pager -o json`, { timeout: 10000, maxBuffer: 1024 * 1024 * 5 }, (err, stdout) => {
|
|
1082
|
+
if (err) return replyError(ws, id, err.message);
|
|
1083
|
+
try {
|
|
1084
|
+
const entries = stdout.trim().split('\n').filter(Boolean).map((line) => {
|
|
1085
|
+
const j = JSON.parse(line);
|
|
1086
|
+
return {
|
|
1087
|
+
timestamp: j.__REALTIME_TIMESTAMP ? new Date(parseInt(j.__REALTIME_TIMESTAMP, 10) / 1000).toISOString() : null,
|
|
1088
|
+
level: j.PRIORITY || 'info',
|
|
1089
|
+
message: j.MESSAGE || '',
|
|
1090
|
+
source: j.SYSLOG_IDENTIFIER || j._COMM || '',
|
|
1091
|
+
};
|
|
1092
|
+
});
|
|
1093
|
+
reply(ws, id, entries);
|
|
1094
|
+
} catch (e) {
|
|
1095
|
+
replyError(ws, id, 'Failed to parse journal entries: ' + e.message);
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
1098
|
+
} else {
|
|
1099
|
+
// macOS
|
|
1100
|
+
exec(`log show --last 1h --style json | head -${lines}`, { timeout: 15000, maxBuffer: 1024 * 1024 * 10 }, (err, stdout) => {
|
|
1101
|
+
if (err) {
|
|
1102
|
+
// Fallback: try system.log
|
|
1103
|
+
return exec(`tail -${lines} /var/log/system.log`, { timeout: 5000 }, (err2, stdout2) => {
|
|
1104
|
+
if (err2) return replyError(ws, id, err2.message);
|
|
1105
|
+
const entries = stdout2.trim().split('\n').filter(Boolean).map((line) => ({
|
|
1106
|
+
timestamp: null,
|
|
1107
|
+
level: 'info',
|
|
1108
|
+
message: line,
|
|
1109
|
+
source: '',
|
|
1110
|
+
}));
|
|
1111
|
+
reply(ws, id, entries);
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
try {
|
|
1115
|
+
const entries = stdout.trim().split('\n').filter(Boolean).map((line) => {
|
|
1116
|
+
try {
|
|
1117
|
+
const j = JSON.parse(line);
|
|
1118
|
+
return {
|
|
1119
|
+
timestamp: j.timestamp || null,
|
|
1120
|
+
level: j.messageType || 'info',
|
|
1121
|
+
message: j.eventMessage || '',
|
|
1122
|
+
source: j.senderImagePath || j.processImagePath || '',
|
|
1123
|
+
};
|
|
1124
|
+
} catch {
|
|
1125
|
+
return { timestamp: null, level: 'info', message: line, source: '' };
|
|
1126
|
+
}
|
|
1127
|
+
});
|
|
1128
|
+
reply(ws, id, entries);
|
|
1129
|
+
} catch (e) {
|
|
1130
|
+
replyError(ws, id, 'Failed to parse log entries: ' + e.message);
|
|
1131
|
+
}
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// --- Log Search ---
|
|
1137
|
+
function handleLogsSearch(ws, { id, query, lines = 100 }) {
|
|
1138
|
+
const platform = os.platform();
|
|
1139
|
+
// Sanitize query to prevent command injection
|
|
1140
|
+
const safeQuery = query.replace(/["`$\\]/g, '');
|
|
1141
|
+
|
|
1142
|
+
if (platform === 'linux') {
|
|
1143
|
+
exec(`journalctl -n ${lines} --no-pager -g "${safeQuery}"`, { timeout: 10000, maxBuffer: 1024 * 1024 * 5 }, (err, stdout) => {
|
|
1144
|
+
if (err) return replyError(ws, id, err.message);
|
|
1145
|
+
const entries = stdout.trim().split('\n').filter(Boolean).map((line) => ({
|
|
1146
|
+
timestamp: null,
|
|
1147
|
+
level: 'info',
|
|
1148
|
+
message: line,
|
|
1149
|
+
source: '',
|
|
1150
|
+
}));
|
|
1151
|
+
reply(ws, id, entries);
|
|
1152
|
+
});
|
|
1153
|
+
} else {
|
|
1154
|
+
// macOS
|
|
1155
|
+
exec(`grep -i "${safeQuery}" /var/log/system.log | tail -${lines}`, { timeout: 10000 }, (err, stdout) => {
|
|
1156
|
+
if (err) return replyError(ws, id, err.message || 'No matches found');
|
|
1157
|
+
const entries = stdout.trim().split('\n').filter(Boolean).map((line) => ({
|
|
1158
|
+
timestamp: null,
|
|
1159
|
+
level: 'info',
|
|
1160
|
+
message: line,
|
|
1161
|
+
source: '',
|
|
1162
|
+
}));
|
|
1163
|
+
reply(ws, id, entries);
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// --- Cron History ---
|
|
1169
|
+
function handleCronHistory(ws, { id }) {
|
|
1170
|
+
const platform = os.platform();
|
|
1171
|
+
|
|
1172
|
+
if (platform === 'linux') {
|
|
1173
|
+
exec('journalctl -u cron -n 50 --no-pager -o json', { timeout: 10000, maxBuffer: 1024 * 1024 * 5 }, (err, stdout) => {
|
|
1174
|
+
if (err) return replyError(ws, id, err.message);
|
|
1175
|
+
try {
|
|
1176
|
+
const entries = stdout.trim().split('\n').filter(Boolean).map((line) => {
|
|
1177
|
+
const j = JSON.parse(line);
|
|
1178
|
+
return {
|
|
1179
|
+
timestamp: j.__REALTIME_TIMESTAMP ? new Date(parseInt(j.__REALTIME_TIMESTAMP, 10) / 1000).toISOString() : null,
|
|
1180
|
+
command: j.MESSAGE || '',
|
|
1181
|
+
exitCode: null,
|
|
1182
|
+
output: j.MESSAGE || '',
|
|
1183
|
+
};
|
|
1184
|
+
});
|
|
1185
|
+
reply(ws, id, entries);
|
|
1186
|
+
} catch (e) {
|
|
1187
|
+
replyError(ws, id, 'Failed to parse cron history: ' + e.message);
|
|
1188
|
+
}
|
|
1189
|
+
});
|
|
1190
|
+
} else {
|
|
1191
|
+
// macOS
|
|
1192
|
+
exec('grep CRON /var/log/system.log | tail -50', { timeout: 5000 }, (err, stdout) => {
|
|
1193
|
+
if (err) {
|
|
1194
|
+
// No cron entries or file not accessible
|
|
1195
|
+
return reply(ws, id, []);
|
|
1196
|
+
}
|
|
1197
|
+
const entries = stdout.trim().split('\n').filter(Boolean).map((line) => ({
|
|
1198
|
+
timestamp: null,
|
|
1199
|
+
command: line,
|
|
1200
|
+
exitCode: null,
|
|
1201
|
+
output: line,
|
|
1202
|
+
}));
|
|
1203
|
+
reply(ws, id, entries);
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// --- Agent List (OpenClaw Models) ---
|
|
1209
|
+
async function handleAgentList(ws, { id }) {
|
|
1210
|
+
const oc = await detectOpenClaw();
|
|
1211
|
+
if (oc.available) {
|
|
1212
|
+
const models = oc.models.map((modelId) => ({
|
|
1213
|
+
id: modelId,
|
|
1214
|
+
name: modelId,
|
|
1215
|
+
status: 'running',
|
|
1216
|
+
port: oc.port,
|
|
1217
|
+
}));
|
|
1218
|
+
return reply(ws, id, models);
|
|
1219
|
+
}
|
|
1220
|
+
reply(ws, id, []);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// --- Agent Logs ---
|
|
1224
|
+
function handleAgentLogs(ws, { id, lines = 200 }) {
|
|
1225
|
+
const platform = os.platform();
|
|
1226
|
+
|
|
1227
|
+
if (platform === 'linux') {
|
|
1228
|
+
exec(`journalctl -u openclaw -n ${lines} --no-pager`, { timeout: 10000, maxBuffer: 1024 * 1024 * 5 }, (err, stdout) => {
|
|
1229
|
+
if (err) return replyError(ws, id, err.message);
|
|
1230
|
+
reply(ws, id, { logs: stdout || '' });
|
|
1231
|
+
});
|
|
1232
|
+
} else {
|
|
1233
|
+
// macOS / fallback: try common log locations
|
|
1234
|
+
const logLocations = [
|
|
1235
|
+
path.join(os.homedir(), '.openclaw', 'logs', 'openclaw.log'),
|
|
1236
|
+
path.join(os.homedir(), '.openclaw', 'openclaw.log'),
|
|
1237
|
+
'/var/log/openclaw.log',
|
|
1238
|
+
];
|
|
1239
|
+
|
|
1240
|
+
let found = false;
|
|
1241
|
+
for (const logPath of logLocations) {
|
|
1242
|
+
try {
|
|
1243
|
+
if (fs.existsSync(logPath)) {
|
|
1244
|
+
exec(`tail -${lines} "${logPath}"`, { timeout: 5000 }, (err, stdout) => {
|
|
1245
|
+
if (err) return reply(ws, id, { logs: '' });
|
|
1246
|
+
reply(ws, id, { logs: stdout || '' });
|
|
1247
|
+
});
|
|
1248
|
+
found = true;
|
|
1249
|
+
break;
|
|
1250
|
+
}
|
|
1251
|
+
} catch {}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
if (!found) {
|
|
1255
|
+
reply(ws, id, { logs: '' });
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
854
1260
|
// --- Graceful Shutdown ---
|
|
855
1261
|
process.on('SIGINT', () => {
|
|
856
1262
|
console.log('\n[agent] Shutting down...');
|
|
857
1263
|
for (const [, term] of terminals) term.kill();
|
|
858
1264
|
for (const [, req] of activeChats) req.destroy();
|
|
859
1265
|
wss.close();
|
|
1266
|
+
if (server) server.close();
|
|
860
1267
|
process.exit(0);
|
|
861
1268
|
});
|
|
862
1269
|
|
|
@@ -864,5 +1271,6 @@ process.on('SIGTERM', () => {
|
|
|
864
1271
|
for (const [, term] of terminals) term.kill();
|
|
865
1272
|
for (const [, req] of activeChats) req.destroy();
|
|
866
1273
|
wss.close();
|
|
1274
|
+
if (server) server.close();
|
|
867
1275
|
process.exit(0);
|
|
868
1276
|
});
|
package/install.sh
CHANGED
|
@@ -23,7 +23,7 @@ else
|
|
|
23
23
|
fi
|
|
24
24
|
|
|
25
25
|
# Step 1: Check/Install Node.js
|
|
26
|
-
echo -e "${YELLOW}[1/
|
|
26
|
+
echo -e "${YELLOW}[1/5]${NC} Checking Node.js..."
|
|
27
27
|
if command -v node &> /dev/null; then
|
|
28
28
|
NODE_VERSION=$(node -v)
|
|
29
29
|
echo -e " ${GREEN}✓${NC} Node.js ${NODE_VERSION} found"
|
|
@@ -46,12 +46,13 @@ else
|
|
|
46
46
|
fi
|
|
47
47
|
|
|
48
48
|
# Step 2: Install indieclaw-agent
|
|
49
|
-
echo -e "${YELLOW}[2/
|
|
50
|
-
$SUDO npm install -g indieclaw-agent 2>/dev/null || npm install -g indieclaw-agent
|
|
51
|
-
|
|
49
|
+
echo -e "${YELLOW}[2/5]${NC} Installing indieclaw-agent..."
|
|
50
|
+
$SUDO npm install -g indieclaw-agent@latest 2>/dev/null || npm install -g indieclaw-agent@latest
|
|
51
|
+
AGENT_VERSION=$(node -e "try{console.log(require(require('child_process').execSync('which indieclaw-agent',{encoding:'utf8'}).trim().replace(/indieclaw-agent$/,'')+'../lib/node_modules/indieclaw-agent/package.json').version)}catch{console.log('2.0.0')}" 2>/dev/null || echo "2.0.0")
|
|
52
|
+
echo -e " ${GREEN}✓${NC} indieclaw-agent v${AGENT_VERSION} installed"
|
|
52
53
|
|
|
53
54
|
# Step 3: Create systemd service
|
|
54
|
-
echo -e "${YELLOW}[3/
|
|
55
|
+
echo -e "${YELLOW}[3/5]${NC} Setting up background service..."
|
|
55
56
|
AGENT_PATH=$(which indieclaw-agent)
|
|
56
57
|
CURRENT_USER=$(whoami)
|
|
57
58
|
|
|
@@ -74,32 +75,72 @@ EOF
|
|
|
74
75
|
|
|
75
76
|
$SUDO systemctl daemon-reload
|
|
76
77
|
$SUDO systemctl enable indieclaw-agent
|
|
77
|
-
$SUDO systemctl
|
|
78
|
+
$SUDO systemctl restart indieclaw-agent
|
|
78
79
|
echo -e " ${GREEN}✓${NC} Service created and started"
|
|
79
80
|
|
|
80
|
-
# Step 4: Wait for token
|
|
81
|
+
# Step 4: Wait for token and detect IP
|
|
82
|
+
echo -e "${YELLOW}[4/5]${NC} Detecting configuration..."
|
|
81
83
|
sleep 2
|
|
82
84
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
echo -e "${CYAN}═══════════════════════════════════════${NC}"
|
|
86
|
-
echo ""
|
|
85
|
+
PORT=3100
|
|
86
|
+
HOSTNAME=$(hostname)
|
|
87
87
|
|
|
88
|
+
# Get token
|
|
88
89
|
if [ -f "$HOME/.indieclaw-token" ]; then
|
|
89
90
|
TOKEN=$(cat "$HOME/.indieclaw-token")
|
|
90
|
-
echo -e " ${GREEN}${BOLD}Setup complete!${NC}"
|
|
91
|
-
echo ""
|
|
92
|
-
echo -e " Your auth token:"
|
|
93
|
-
echo ""
|
|
94
|
-
echo -e " ${BOLD}${CYAN}${TOKEN}${NC}"
|
|
95
|
-
echo ""
|
|
96
|
-
echo -e " Copy this token into the IndieClaw app."
|
|
97
|
-
echo -e " Port: ${BOLD}3100${NC}"
|
|
98
91
|
else
|
|
99
|
-
echo -e " ${
|
|
92
|
+
echo -e " ${YELLOW}⚠${NC} Token file not found yet. Check: cat ~/.indieclaw-token"
|
|
93
|
+
TOKEN=""
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# Detect IP (same logic as agent: try tailscale first, fallback to network)
|
|
97
|
+
MACHINE_IP=""
|
|
98
|
+
if command -v tailscale &> /dev/null; then
|
|
99
|
+
MACHINE_IP=$(tailscale ip -4 2>/dev/null || true)
|
|
100
|
+
fi
|
|
101
|
+
if [ -z "$MACHINE_IP" ]; then
|
|
102
|
+
MACHINE_IP=$(hostname -I 2>/dev/null | awk '{print $1}' || ip route get 1 2>/dev/null | awk '{print $7; exit}' || echo "")
|
|
103
|
+
fi
|
|
104
|
+
if [ -z "$MACHINE_IP" ]; then
|
|
105
|
+
MACHINE_IP="YOUR_SERVER_IP"
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
echo -e " ${GREEN}✓${NC} IP: ${MACHINE_IP}"
|
|
109
|
+
|
|
110
|
+
# Step 5: Check OpenClaw
|
|
111
|
+
echo -e "${YELLOW}[5/5]${NC} Checking OpenClaw..."
|
|
112
|
+
OPENCLAW_STATUS="not detected"
|
|
113
|
+
if curl -s --max-time 2 http://127.0.0.1:18789/v1/models > /dev/null 2>&1; then
|
|
114
|
+
OPENCLAW_STATUS="detected on port 18789"
|
|
115
|
+
echo -e " ${GREEN}✓${NC} OpenClaw detected on port 18789"
|
|
116
|
+
else
|
|
117
|
+
echo -e " ${YELLOW}—${NC} OpenClaw not detected on port 18789 (optional, for AI features)"
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# Build deep link
|
|
121
|
+
DEEP_LINK="indieclaw://connect?host=${MACHINE_IP}&port=${PORT}&token=${TOKEN}&name=${HOSTNAME}&tls=0"
|
|
122
|
+
|
|
123
|
+
# Show results
|
|
124
|
+
echo ""
|
|
125
|
+
echo -e "${CYAN}╔═══════════════════════════════════════╗${NC}"
|
|
126
|
+
echo -e "${CYAN}║ ${BOLD}IndieClaw Agent v${AGENT_VERSION}${NC}${CYAN} ║${NC}"
|
|
127
|
+
echo -e "${CYAN}╠═══════════════════════════════════════╣${NC}"
|
|
128
|
+
echo -e "${CYAN}║${NC} Port: ${BOLD}${PORT}${NC}"
|
|
129
|
+
echo -e "${CYAN}║${NC} IP: ${BOLD}${MACHINE_IP}${NC}"
|
|
130
|
+
echo -e "${CYAN}║${NC} OpenClaw: ${BOLD}${OPENCLAW_STATUS}${NC}"
|
|
131
|
+
echo -e "${CYAN}╚═══════════════════════════════════════╝${NC}"
|
|
132
|
+
echo ""
|
|
133
|
+
|
|
134
|
+
if [ -n "$TOKEN" ]; then
|
|
135
|
+
echo -e " ${BOLD}Auth Token:${NC}"
|
|
136
|
+
echo -e " ${CYAN}${TOKEN}${NC}"
|
|
137
|
+
echo ""
|
|
138
|
+
echo -e " ${BOLD}Deep Link (paste in phone browser):${NC}"
|
|
139
|
+
echo -e " ${CYAN}${DEEP_LINK}${NC}"
|
|
100
140
|
echo ""
|
|
101
|
-
echo -e "
|
|
102
|
-
echo -e " ${BOLD}
|
|
141
|
+
echo -e " ${BOLD}Or scan QR code:${NC}"
|
|
142
|
+
echo -e " Run: ${BOLD}indieclaw-agent${NC} interactively to see QR code"
|
|
143
|
+
echo -e " Or: ${BOLD}sudo journalctl -u indieclaw-agent --no-pager | head -50${NC}"
|
|
103
144
|
fi
|
|
104
145
|
|
|
105
146
|
echo ""
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "indieclaw-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "2.1.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": {
|