indieclaw-agent 2.0.0 → 2.2.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 +85 -68
  2. package/install.sh +69 -22
  3. package/package.json +1 -1
package/index.js CHANGED
@@ -90,6 +90,7 @@ if (TLS_ENABLED) {
90
90
 
91
91
  const terminals = new Map(); // id -> pty process
92
92
  const activeChats = new Map(); // id -> http.ClientRequest
93
+ const activeSearches = new Map(); // ws -> child_process (one search per connection)
93
94
 
94
95
  // --- Deep Link & QR Code ---
95
96
  function getMachineIP() {
@@ -143,12 +144,14 @@ try {
143
144
  }
144
145
 
145
146
  // --- OpenClaw Detection ---
146
- function detectOpenClaw() {
147
+ const OPENCLAW_PORTS = [18789, 8080, 11434, 1234, 8000];
148
+
149
+ function tryOpenClawPort(port) {
147
150
  return new Promise((resolve) => {
148
151
  const req = http.request(
149
152
  {
150
153
  hostname: '127.0.0.1',
151
- port: 18789,
154
+ port,
152
155
  path: '/v1/models',
153
156
  method: 'GET',
154
157
  timeout: 2000,
@@ -160,30 +163,44 @@ function detectOpenClaw() {
160
163
  try {
161
164
  const json = JSON.parse(body);
162
165
  const models = (json.data || []).map((m) => m.id);
163
- resolve({ available: true, models });
166
+ if (models.length > 0) {
167
+ resolve({ available: true, models, port });
168
+ } else {
169
+ resolve({ available: false, models: [], port });
170
+ }
164
171
  } catch {
165
- resolve({ available: false, models: [] });
172
+ resolve({ available: false, models: [], port });
166
173
  }
167
174
  });
168
175
  }
169
176
  );
170
177
  req.on('timeout', () => {
171
178
  req.destroy();
172
- resolve({ available: false, models: [] });
179
+ resolve({ available: false, models: [], port });
173
180
  });
174
181
  req.on('error', () => {
175
- resolve({ available: false, models: [] });
182
+ resolve({ available: false, models: [], port });
176
183
  });
177
184
  req.end();
178
185
  });
179
186
  }
180
187
 
188
+ async function detectOpenClaw() {
189
+ // Try all common ports in parallel
190
+ const results = await Promise.all(OPENCLAW_PORTS.map(tryOpenClawPort));
191
+ const found = results.find((r) => r.available);
192
+ if (found) {
193
+ return { available: true, models: found.models, port: found.port };
194
+ }
195
+ return { available: false, models: [], port: null };
196
+ }
197
+
181
198
  // Run detection on startup
182
199
  detectOpenClaw().then((oc) => {
183
200
  if (oc.available) {
184
- console.log(` [OpenClaw] Detected! Models: ${oc.models.join(', ')}`);
201
+ console.log(` [OpenClaw] Detected on port ${oc.port}! Models: ${oc.models.join(', ')}`);
185
202
  } else {
186
- console.log(' [OpenClaw] Not detected on port 18789');
203
+ console.log(` [OpenClaw] Not detected on ports ${OPENCLAW_PORTS.join(', ')}`);
187
204
  }
188
205
  });
189
206
 
@@ -243,7 +260,9 @@ wss.on('connection', (ws) => {
243
260
  }
244
261
 
245
262
  // Route message
246
- handleMessage(ws, msg);
263
+ handleMessage(ws, msg).catch((err) => {
264
+ console.error('[handleMessage] Unhandled error:', err.message || err);
265
+ });
247
266
  });
248
267
 
249
268
  ws.on('close', () => {
@@ -261,6 +280,12 @@ wss.on('connection', (ws) => {
261
280
  activeChats.delete(id);
262
281
  }
263
282
  }
283
+ // Kill any active search process for this connection
284
+ const search = activeSearches.get(ws);
285
+ if (search) {
286
+ search.kill();
287
+ activeSearches.delete(ws);
288
+ }
264
289
  });
265
290
  });
266
291
 
@@ -338,9 +363,9 @@ async function handleMessage(ws, msg) {
338
363
  case 'cron.history':
339
364
  return handleCronHistory(ws, msg);
340
365
  case 'agent.list':
341
- return handleAgentList(ws, msg);
366
+ return await handleAgentList(ws, msg);
342
367
  case 'agent.logs':
343
- return handleAgentLogs(ws, msg);
368
+ return await handleAgentLogs(ws, msg);
344
369
  default:
345
370
  return replyError(ws, id, `Unknown message type: ${type}`);
346
371
  }
@@ -1062,7 +1087,7 @@ function handleLogsSystem(ws, { id, lines = 200 }) {
1062
1087
  const platform = os.platform();
1063
1088
 
1064
1089
  if (platform === 'linux') {
1065
- exec(`journalctl -n ${lines} --no-pager -o json`, { timeout: 10000, maxBuffer: 1024 * 1024 * 5 }, (err, stdout) => {
1090
+ exec(`journalctl -n ${lines} --no-pager --since "1 hour ago" -o json`, { timeout: 10000, maxBuffer: 1024 * 1024 * 5 }, (err, stdout) => {
1066
1091
  if (err) return replyError(ws, id, err.message);
1067
1092
  try {
1068
1093
  const entries = stdout.trim().split('\n').filter(Boolean).map((line) => {
@@ -1121,32 +1146,44 @@ function handleLogsSystem(ws, { id, lines = 200 }) {
1121
1146
  function handleLogsSearch(ws, { id, query, lines = 100 }) {
1122
1147
  const platform = os.platform();
1123
1148
  // Sanitize query to prevent command injection
1124
- const safeQuery = query.replace(/["`$\\]/g, '');
1149
+ const safeQuery = query.replace(/["`$\\!;']/g, '');
1150
+
1151
+ if (!safeQuery || safeQuery.length < 2) {
1152
+ return reply(ws, id, []);
1153
+ }
1125
1154
 
1155
+ // Kill any previous search for this connection to prevent pile-up
1156
+ const prev = activeSearches.get(ws);
1157
+ if (prev) {
1158
+ prev.kill();
1159
+ activeSearches.delete(ws);
1160
+ }
1161
+
1162
+ let cmd;
1126
1163
  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
- });
1164
+ // --since "24h ago" limits search scope instead of grepping entire journal
1165
+ cmd = `journalctl -n ${lines} --no-pager --since "24 hours ago" -g "${safeQuery}"`;
1137
1166
  } 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
- });
1167
+ cmd = `grep -i "${safeQuery}" /var/log/system.log | tail -${lines}`;
1149
1168
  }
1169
+
1170
+ const child = exec(cmd, { timeout: 10000, maxBuffer: 1024 * 1024 * 2 }, (err, stdout) => {
1171
+ activeSearches.delete(ws);
1172
+ if (err) {
1173
+ // journalctl returns exit code 1 when no matches found — not an error
1174
+ if (err.killed) return; // process was killed by a newer search
1175
+ return reply(ws, id, []);
1176
+ }
1177
+ const entries = stdout.trim().split('\n').filter(Boolean).map((line) => ({
1178
+ timestamp: null,
1179
+ level: 'info',
1180
+ message: line,
1181
+ source: '',
1182
+ }));
1183
+ reply(ws, id, entries);
1184
+ });
1185
+
1186
+ activeSearches.set(ws, child);
1150
1187
  }
1151
1188
 
1152
1189
  // --- Cron History ---
@@ -1190,42 +1227,22 @@ function handleCronHistory(ws, { id }) {
1190
1227
  }
1191
1228
 
1192
1229
  // --- 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
- });
1230
+ async function handleAgentList(ws, { id }) {
1231
+ try {
1232
+ const oc = await detectOpenClaw();
1233
+ if (oc.available) {
1234
+ const models = oc.models.map((modelId) => ({
1235
+ id: modelId,
1236
+ name: modelId,
1237
+ status: 'running',
1238
+ port: oc.port,
1239
+ }));
1240
+ return reply(ws, id, models);
1219
1241
  }
1220
- );
1221
- req.on('timeout', () => {
1222
- req.destroy();
1223
1242
  reply(ws, id, []);
1224
- });
1225
- req.on('error', () => {
1226
- reply(ws, id, []);
1227
- });
1228
- req.end();
1243
+ } catch (err) {
1244
+ replyError(ws, id, 'OpenClaw detection failed: ' + (err.message || String(err)));
1245
+ }
1229
1246
  }
1230
1247
 
1231
1248
  // --- Agent Logs ---
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,78 @@ 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}"
100
137
  echo ""
101
- echo -e " Run this to see your token:"
102
- echo -e " ${BOLD}cat ~/.indieclaw-token${NC}"
138
+ echo -e " ${BOLD}Deep Link (paste in IndieClaw app → Add Machine → Paste Link):${NC}"
139
+ echo -e " ${CYAN}${DEEP_LINK}${NC}"
140
+ echo ""
141
+
142
+ # Generate QR code using the installed qrcode-terminal package
143
+ AGENT_MODULES=$(npm root -g 2>/dev/null)/indieclaw-agent/node_modules
144
+ if [ -d "$AGENT_MODULES/qrcode-terminal" ]; then
145
+ echo -e " ${BOLD}Scan with IndieClaw app:${NC}"
146
+ echo ""
147
+ node -e "require('${AGENT_MODULES}/qrcode-terminal').generate('${DEEP_LINK}', {small: true}, function(qr) { qr.split('\n').forEach(function(l) { console.log(' ' + l) }) })" 2>/dev/null || true
148
+ echo ""
149
+ fi
103
150
  fi
104
151
 
105
152
  echo ""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "indieclaw-agent",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "Manage your server from your phone. Agent for the IndieClaw mobile app.",
5
5
  "main": "index.js",
6
6
  "bin": {