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.
Files changed (3) hide show
  1. package/index.js +416 -8
  2. package/install.sh +63 -22
  3. 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
- const wss = new WebSocketServer({ port: PORT });
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(' ║ IndieClaw Agent v1.0.0 ║');
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
- return ws.send(JSON.stringify({ type: 'auth', success: true }));
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
- const child = platform === 'win32'
545
- ? spawn('cmd', ['/c', scriptPath], { detached: true, stdio: 'ignore', windowsHide: true })
546
- : spawn('bash', [scriptPath], { detached: true, stdio: 'ignore' });
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/4]${NC} Checking Node.js..."
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/4]${NC} Installing indieclaw-agent..."
50
- $SUDO npm install -g indieclaw-agent 2>/dev/null || npm install -g indieclaw-agent
51
- echo -e " ${GREEN}✓${NC} indieclaw-agent installed"
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/4]${NC} Setting up background service..."
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 start indieclaw-agent
78
+ $SUDO systemctl restart indieclaw-agent
78
79
  echo -e " ${GREEN}✓${NC} Service created and started"
79
80
 
80
- # Step 4: Wait for token to be generated
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
- # Show token
84
- echo ""
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 " ${GREEN}${BOLD}Setup complete!${NC}"
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 " Run this to see your token:"
102
- echo -e " ${BOLD}cat ~/.indieclaw-token${NC}"
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.3",
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": {