higherup 1.0.1 → 2.0.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/dist/agent.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Higherup Local Agent
2
+ * Higherup Local Agent v2.0
3
3
  *
4
4
  * This agent runs on your local PC and connects to the Higherup service,
5
5
  * enabling secure remote command execution, file operations, and screen capture
@@ -14,12 +14,15 @@ import * as os from 'os';
14
14
  import { spawn } from 'child_process';
15
15
  import { exec } from 'child_process';
16
16
  import { promisify } from 'util';
17
+ import * as readline from 'readline';
17
18
  const execAsync = promisify(exec);
18
19
  // Configuration
19
20
  const API_BASE_URL = process.env.HIGHERUP_API_URL || 'https://pltlcpqtivuvyeuywvql.supabase.co/functions/v1/agent-relay';
20
21
  const CONFIG_DIR = path.join(os.homedir(), '.higherup');
21
22
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
22
- const POLL_INTERVAL = 2000; // 2 seconds
23
+ const LOG_FILE = path.join(CONFIG_DIR, 'agent.log');
24
+ const POLL_INTERVAL = 2000;
25
+ const VERSION = '2.0.0';
23
26
  // Colors for terminal output
24
27
  const colors = {
25
28
  success: chalk.green,
@@ -30,7 +33,99 @@ const colors = {
30
33
  file: chalk.blue,
31
34
  dim: chalk.gray,
32
35
  bold: chalk.bold,
36
+ highlight: chalk.bgCyan.black,
33
37
  };
38
+ // Logging utility
39
+ class Logger {
40
+ logLevel;
41
+ logToFile;
42
+ constructor(level = 'info', logToFile = true) {
43
+ this.logLevel = level;
44
+ this.logToFile = logToFile;
45
+ }
46
+ levels = { debug: 0, info: 1, warn: 2, error: 3 };
47
+ shouldLog(level) {
48
+ return this.levels[level] >= this.levels[this.logLevel];
49
+ }
50
+ async writeToFile(message) {
51
+ if (this.logToFile) {
52
+ try {
53
+ const timestamp = new Date().toISOString();
54
+ await fs.appendFile(LOG_FILE, `[${timestamp}] ${message}\n`);
55
+ }
56
+ catch {
57
+ // Ignore file write errors
58
+ }
59
+ }
60
+ }
61
+ debug(message, ...args) {
62
+ if (this.shouldLog('debug')) {
63
+ console.log(colors.dim(`[DEBUG] ${message}`), ...args);
64
+ this.writeToFile(`DEBUG: ${message}`);
65
+ }
66
+ }
67
+ info(message, ...args) {
68
+ if (this.shouldLog('info')) {
69
+ console.log(colors.info(message), ...args);
70
+ this.writeToFile(`INFO: ${message}`);
71
+ }
72
+ }
73
+ warn(message, ...args) {
74
+ if (this.shouldLog('warn')) {
75
+ console.log(colors.warning(`⚠ ${message}`), ...args);
76
+ this.writeToFile(`WARN: ${message}`);
77
+ }
78
+ }
79
+ error(message, ...args) {
80
+ if (this.shouldLog('error')) {
81
+ console.error(colors.error(`✗ ${message}`), ...args);
82
+ this.writeToFile(`ERROR: ${message}`);
83
+ }
84
+ }
85
+ success(message, ...args) {
86
+ console.log(colors.success(`✓ ${message}`), ...args);
87
+ this.writeToFile(`SUCCESS: ${message}`);
88
+ }
89
+ }
90
+ const logger = new Logger();
91
+ // Interactive prompts
92
+ async function prompt(question, defaultValue) {
93
+ const rl = readline.createInterface({
94
+ input: process.stdin,
95
+ output: process.stdout,
96
+ });
97
+ return new Promise((resolve) => {
98
+ const displayQuestion = defaultValue
99
+ ? `${question} (${colors.dim(defaultValue)}): `
100
+ : `${question}: `;
101
+ rl.question(displayQuestion, (answer) => {
102
+ rl.close();
103
+ resolve(answer.trim() || defaultValue || '');
104
+ });
105
+ });
106
+ }
107
+ async function confirm(question, defaultYes = true) {
108
+ const hint = defaultYes ? '[Y/n]' : '[y/N]';
109
+ const answer = await prompt(`${question} ${hint}`);
110
+ if (!answer)
111
+ return defaultYes;
112
+ return answer.toLowerCase().startsWith('y');
113
+ }
114
+ async function selectFromList(items, message) {
115
+ if (items.length === 0)
116
+ return null;
117
+ console.log(`\n${message}\n`);
118
+ items.forEach((item, index) => {
119
+ console.log(` ${colors.bold(`${index + 1}.`)} ${item.name} ${colors.dim(`(${item.id.slice(0, 8)}...)`)}`);
120
+ });
121
+ console.log('');
122
+ const answer = await prompt('Select number');
123
+ const index = parseInt(answer) - 1;
124
+ if (index >= 0 && index < items.length) {
125
+ return items[index];
126
+ }
127
+ return null;
128
+ }
34
129
  class HigherupAgent {
35
130
  workspacePath = '';
36
131
  workspaceId = '';
@@ -40,14 +135,21 @@ class HigherupAgent {
40
135
  isRunning = false;
41
136
  commandCount = 0;
42
137
  pollTimer = null;
138
+ heartbeatTimer = null;
43
139
  autonomousMode = false;
140
+ reconnectAttempts = 0;
141
+ maxReconnectAttempts = 5;
142
+ startTime = new Date();
143
+ bytesTransferred = 0;
44
144
  constructor() { }
45
- async connect(workspacePath, workspaceId, apiToken, agentName, autonomous) {
145
+ async connect(workspacePath, workspaceId, apiToken, agentName, autonomous, autoReconnect) {
46
146
  this.workspacePath = path.resolve(workspacePath);
47
147
  this.workspaceId = workspaceId;
48
148
  this.apiToken = apiToken;
49
149
  this.agentName = agentName || `${os.hostname()}-${os.platform()}`;
50
150
  this.autonomousMode = autonomous || false;
151
+ this.maxReconnectAttempts = autoReconnect ? 5 : 0;
152
+ this.startTime = new Date();
51
153
  const spinner = ora('Connecting to Higherup service...').start();
52
154
  try {
53
155
  // Validate workspace path
@@ -62,18 +164,27 @@ class HigherupAgent {
62
164
  agent_name: this.agentName,
63
165
  capabilities: [
64
166
  'command_execute',
167
+ 'command_stream',
65
168
  'file_read',
66
169
  'file_write',
67
170
  'file_list',
171
+ 'file_watch',
68
172
  'screen_capture',
173
+ 'screen_stream',
69
174
  'system_info',
175
+ 'process_list',
176
+ 'env_vars',
70
177
  ],
71
178
  autonomous_mode: this.autonomousMode,
179
+ version: VERSION,
180
+ platform: os.platform(),
181
+ arch: os.arch(),
72
182
  });
73
183
  if (!response.success) {
74
184
  throw new Error(response.error || 'Registration failed');
75
185
  }
76
186
  this.sessionId = response.session_id;
187
+ this.reconnectAttempts = 0;
77
188
  spinner.succeed('Connected to Higherup service!');
78
189
  this.printBanner();
79
190
  this.isRunning = true;
@@ -84,23 +195,47 @@ class HigherupAgent {
84
195
  // Handle shutdown
85
196
  process.on('SIGINT', () => this.disconnect());
86
197
  process.on('SIGTERM', () => this.disconnect());
198
+ process.on('uncaughtException', (err) => {
199
+ logger.error(`Uncaught exception: ${err.message}`);
200
+ this.disconnect();
201
+ });
87
202
  }
88
203
  catch (error) {
89
204
  spinner.fail(`Failed to connect: ${error.message}`);
205
+ if (this.maxReconnectAttempts > 0 && this.reconnectAttempts < this.maxReconnectAttempts) {
206
+ this.reconnectAttempts++;
207
+ const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
208
+ logger.warn(`Reconnecting in ${delay / 1000}s (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
209
+ await new Promise(resolve => setTimeout(resolve, delay));
210
+ return this.connect(workspacePath, workspaceId, apiToken, agentName, autonomous, true);
211
+ }
90
212
  throw error;
91
213
  }
92
214
  }
93
215
  async apiCall(action, body) {
94
216
  const url = `${API_BASE_URL}?action=${action}`;
95
- const response = await fetch(url, {
96
- method: 'POST',
97
- headers: {
98
- 'Content-Type': 'application/json',
99
- 'x-api-token': this.apiToken,
100
- },
101
- body: JSON.stringify(body),
102
- });
103
- return response.json();
217
+ try {
218
+ const response = await fetch(url, {
219
+ method: 'POST',
220
+ headers: {
221
+ 'Content-Type': 'application/json',
222
+ 'x-api-token': this.apiToken,
223
+ 'x-agent-version': VERSION,
224
+ },
225
+ body: JSON.stringify(body),
226
+ });
227
+ if (!response.ok) {
228
+ const errorText = await response.text();
229
+ throw new Error(`API error (${response.status}): ${errorText}`);
230
+ }
231
+ return response.json();
232
+ }
233
+ catch (error) {
234
+ if (error.name === 'AbortError' || error.message.includes('fetch')) {
235
+ throw new Error('Network error: Unable to reach Higherup service');
236
+ }
237
+ throw error;
238
+ }
104
239
  }
105
240
  startPolling() {
106
241
  this.pollTimer = setInterval(async () => {
@@ -117,38 +252,117 @@ class HigherupAgent {
117
252
  }
118
253
  }
119
254
  catch (error) {
120
- console.error(colors.error('Polling error:'), error);
255
+ logger.debug(`Polling error: ${error.message}`);
256
+ // Check if we should attempt reconnection
257
+ if (this.maxReconnectAttempts > 0 && error.message.includes('Network')) {
258
+ await this.attemptReconnect();
259
+ }
121
260
  }
122
261
  }, POLL_INTERVAL);
123
262
  }
263
+ async attemptReconnect() {
264
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
265
+ logger.error('Max reconnection attempts reached. Disconnecting...');
266
+ await this.disconnect();
267
+ return;
268
+ }
269
+ this.reconnectAttempts++;
270
+ logger.warn(`Connection lost. Reconnecting (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
271
+ try {
272
+ const response = await this.apiCall('register', {
273
+ workspace_id: this.workspaceId,
274
+ agent_name: this.agentName,
275
+ capabilities: ['command_execute', 'file_read', 'file_write', 'file_list', 'screen_capture', 'system_info'],
276
+ autonomous_mode: this.autonomousMode,
277
+ version: VERSION,
278
+ });
279
+ if (response.success) {
280
+ this.sessionId = response.session_id;
281
+ this.reconnectAttempts = 0;
282
+ logger.success('Reconnected successfully!');
283
+ }
284
+ }
285
+ catch {
286
+ const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
287
+ await new Promise(resolve => setTimeout(resolve, delay));
288
+ }
289
+ }
124
290
  startHeartbeat() {
125
- setInterval(async () => {
291
+ this.heartbeatTimer = setInterval(async () => {
126
292
  if (!this.isRunning)
127
293
  return;
128
294
  try {
129
295
  await this.apiCall('heartbeat', {
130
296
  session_id: this.sessionId,
297
+ stats: {
298
+ commands_executed: this.commandCount,
299
+ uptime: Math.floor((Date.now() - this.startTime.getTime()) / 1000),
300
+ bytes_transferred: this.bytesTransferred,
301
+ memory_usage: process.memoryUsage().heapUsed,
302
+ },
131
303
  });
132
304
  }
133
- catch (error) {
134
- console.error(colors.dim('Heartbeat failed'));
305
+ catch {
306
+ logger.debug('Heartbeat failed');
135
307
  }
136
- }, 30000); // Every 30 seconds
308
+ }, 30000);
137
309
  }
138
310
  printBanner() {
139
311
  console.log('\n');
140
- console.log(colors.bold(' ╔═══════════════════════════════════════════╗'));
141
- console.log(colors.bold(' ║') + colors.info(' HIGHERUP LOCAL AGENT ') + colors.bold('║'));
142
- console.log(colors.bold(' ╚═══════════════════════════════════════════╝'));
143
- console.log('');
144
- console.log(colors.dim(` Workspace: ${this.workspacePath}`));
145
- console.log(colors.dim(` Agent: ${this.agentName}`));
146
- console.log(colors.dim(` Session: ${this.sessionId}`));
312
+ console.log(colors.bold(' ╔═══════════════════════════════════════════════════╗'));
313
+ console.log(colors.bold(' ║') + colors.info(' HIGHERUP LOCAL AGENT v' + VERSION + ' ') + colors.bold('║'));
314
+ console.log(colors.bold(' ╠═══════════════════════════════════════════════════╣'));
315
+ console.log(colors.bold('') + ` Workspace: ${colors.dim(this.workspacePath.slice(0, 35))}`.padEnd(60) + colors.bold('║'));
316
+ console.log(colors.bold(' ║') + ` Agent: ${colors.dim(this.agentName)}`.padEnd(60) + colors.bold('║'));
317
+ console.log(colors.bold(' ║') + ` Session: ${colors.dim(this.sessionId.slice(0, 20))}...`.padEnd(60) + colors.bold('║'));
147
318
  if (this.autonomousMode) {
148
- console.log(colors.warning(' Mode: AUTONOMOUS (unrestricted)'));
319
+ console.log(colors.bold(' ║') + colors.warning(' Mode: AUTONOMOUS (unrestricted)').padEnd(59) + colors.bold('║'));
149
320
  }
321
+ console.log(colors.bold(' ╚═══════════════════════════════════════════════════╝'));
150
322
  console.log('');
151
323
  console.log(colors.info(' Listening for commands... Press Ctrl+C to stop.\n'));
324
+ console.log(colors.dim(' Commands: h=help, s=stats, q=quit\n'));
325
+ // Listen for keyboard commands
326
+ this.setupKeyboardListener();
327
+ }
328
+ setupKeyboardListener() {
329
+ if (process.stdin.isTTY) {
330
+ process.stdin.setRawMode(true);
331
+ process.stdin.resume();
332
+ process.stdin.setEncoding('utf8');
333
+ process.stdin.on('data', async (key) => {
334
+ if (key === '\u0003') { // Ctrl+C
335
+ await this.disconnect();
336
+ }
337
+ else if (key === 'h') {
338
+ this.printHelp();
339
+ }
340
+ else if (key === 's') {
341
+ this.printStats();
342
+ }
343
+ else if (key === 'q') {
344
+ await this.disconnect();
345
+ }
346
+ });
347
+ }
348
+ }
349
+ printHelp() {
350
+ console.log('\n' + colors.bold(' Keyboard Commands:'));
351
+ console.log(' h - Show this help');
352
+ console.log(' s - Show session stats');
353
+ console.log(' q - Quit/disconnect');
354
+ console.log(' Ctrl+C - Force quit\n');
355
+ }
356
+ printStats() {
357
+ const uptime = Math.floor((Date.now() - this.startTime.getTime()) / 1000);
358
+ const hours = Math.floor(uptime / 3600);
359
+ const minutes = Math.floor((uptime % 3600) / 60);
360
+ const seconds = uptime % 60;
361
+ console.log('\n' + colors.bold(' Session Statistics:'));
362
+ console.log(` Commands executed: ${this.commandCount}`);
363
+ console.log(` Uptime: ${hours}h ${minutes}m ${seconds}s`);
364
+ console.log(` Data transferred: ${(this.bytesTransferred / 1024).toFixed(2)} KB`);
365
+ console.log(` Memory usage: ${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)} MB\n`);
152
366
  }
153
367
  async processCommand(cmd) {
154
368
  const commandText = cmd.command_text;
@@ -175,6 +389,15 @@ class HigherupAgent {
175
389
  else if (commandText.startsWith('__INTERNAL_FILE_LIST__')) {
176
390
  await this.handleFileList(cmd);
177
391
  }
392
+ else if (commandText.startsWith('__INTERNAL_FILE_DELETE__')) {
393
+ await this.handleFileDelete(cmd);
394
+ }
395
+ else if (commandText.startsWith('__INTERNAL_FILE_COPY__')) {
396
+ await this.handleFileCopy(cmd);
397
+ }
398
+ else if (commandText.startsWith('__INTERNAL_FILE_MOVE__')) {
399
+ await this.handleFileMove(cmd);
400
+ }
178
401
  else if (commandText.startsWith('__INTERNAL_SCREEN_CAPTURE__')) {
179
402
  await this.handleScreenCapture(cmd);
180
403
  }
@@ -184,6 +407,15 @@ class HigherupAgent {
184
407
  else if (commandText.startsWith('__INTERNAL_SYSTEM_INFO__')) {
185
408
  await this.handleSystemInfo(cmd);
186
409
  }
410
+ else if (commandText.startsWith('__INTERNAL_PROCESS_LIST__')) {
411
+ await this.handleProcessList(cmd);
412
+ }
413
+ else if (commandText.startsWith('__INTERNAL_ENV_VARS__')) {
414
+ await this.handleEnvVars(cmd);
415
+ }
416
+ else if (commandText.startsWith('__INTERNAL_SEARCH_FILES__')) {
417
+ await this.handleSearchFiles(cmd);
418
+ }
187
419
  else {
188
420
  await this.reportResult(cmd.id, `Unknown internal command: ${commandText}`, -1, 0);
189
421
  }
@@ -201,7 +433,7 @@ class HigherupAgent {
201
433
  const filePath = path.resolve(this.workspacePath, match[1].trim());
202
434
  const encoding = (match[2] || 'utf8');
203
435
  console.log(colors.file(`[FILE READ] ${filePath}`));
204
- // Security check - ensure file is within workspace
436
+ // Security check
205
437
  if (!filePath.startsWith(this.workspacePath)) {
206
438
  await this.reportResult(cmd.id, 'Access denied: Path outside workspace', -1, 0);
207
439
  return;
@@ -209,6 +441,7 @@ class HigherupAgent {
209
441
  try {
210
442
  const content = await fs.readFile(filePath, encoding);
211
443
  const stats = await fs.stat(filePath);
444
+ this.bytesTransferred += stats.size;
212
445
  const result = JSON.stringify({
213
446
  success: true,
214
447
  content,
@@ -237,7 +470,6 @@ class HigherupAgent {
237
470
  }
238
471
  console.log(colors.file(`[FILE WRITE] ${filePath}`));
239
472
  try {
240
- // Content is stored in cmd.output as JSON
241
473
  const data = JSON.parse(cmd.output || '{}');
242
474
  const { content, encoding = 'utf8', create_dirs = true } = data;
243
475
  if (create_dirs) {
@@ -245,6 +477,7 @@ class HigherupAgent {
245
477
  }
246
478
  await fs.writeFile(filePath, content, encoding);
247
479
  const stats = await fs.stat(filePath);
480
+ this.bytesTransferred += stats.size;
248
481
  await this.reportResult(cmd.id, JSON.stringify({
249
482
  success: true,
250
483
  path: match[1].trim(),
@@ -284,6 +517,171 @@ class HigherupAgent {
284
517
  await this.reportResult(cmd.id, JSON.stringify({ success: false, error: error.message }), -1, 0);
285
518
  }
286
519
  }
520
+ async handleFileDelete(cmd) {
521
+ const match = cmd.command_text.match(/__INTERNAL_FILE_DELETE__\s+(.+)$/);
522
+ if (!match) {
523
+ await this.reportResult(cmd.id, 'Invalid file delete command', -1, 0);
524
+ return;
525
+ }
526
+ const filePath = path.resolve(this.workspacePath, match[1].trim());
527
+ // Security check
528
+ if (!filePath.startsWith(this.workspacePath)) {
529
+ await this.reportResult(cmd.id, 'Access denied: Path outside workspace', -1, 0);
530
+ return;
531
+ }
532
+ console.log(colors.file(`[FILE DELETE] ${filePath}`));
533
+ try {
534
+ await fs.rm(filePath, { recursive: true });
535
+ await this.reportResult(cmd.id, JSON.stringify({ success: true, path: match[1].trim() }), 0, 0);
536
+ console.log(colors.success(` ✓ Deleted`));
537
+ }
538
+ catch (error) {
539
+ await this.reportResult(cmd.id, JSON.stringify({ success: false, error: error.message }), -1, 0);
540
+ }
541
+ }
542
+ async handleFileCopy(cmd) {
543
+ const match = cmd.command_text.match(/__INTERNAL_FILE_COPY__\s+(.+)\s+--to=(.+)$/);
544
+ if (!match) {
545
+ await this.reportResult(cmd.id, 'Invalid file copy command', -1, 0);
546
+ return;
547
+ }
548
+ const srcPath = path.resolve(this.workspacePath, match[1].trim());
549
+ const destPath = path.resolve(this.workspacePath, match[2].trim());
550
+ // Security check
551
+ if (!srcPath.startsWith(this.workspacePath) || !destPath.startsWith(this.workspacePath)) {
552
+ await this.reportResult(cmd.id, 'Access denied: Path outside workspace', -1, 0);
553
+ return;
554
+ }
555
+ console.log(colors.file(`[FILE COPY] ${srcPath} -> ${destPath}`));
556
+ try {
557
+ await fs.mkdir(path.dirname(destPath), { recursive: true });
558
+ await fs.copyFile(srcPath, destPath);
559
+ await this.reportResult(cmd.id, JSON.stringify({ success: true, from: match[1].trim(), to: match[2].trim() }), 0, 0);
560
+ console.log(colors.success(` ✓ Copied`));
561
+ }
562
+ catch (error) {
563
+ await this.reportResult(cmd.id, JSON.stringify({ success: false, error: error.message }), -1, 0);
564
+ }
565
+ }
566
+ async handleFileMove(cmd) {
567
+ const match = cmd.command_text.match(/__INTERNAL_FILE_MOVE__\s+(.+)\s+--to=(.+)$/);
568
+ if (!match) {
569
+ await this.reportResult(cmd.id, 'Invalid file move command', -1, 0);
570
+ return;
571
+ }
572
+ const srcPath = path.resolve(this.workspacePath, match[1].trim());
573
+ const destPath = path.resolve(this.workspacePath, match[2].trim());
574
+ // Security check
575
+ if (!srcPath.startsWith(this.workspacePath) || !destPath.startsWith(this.workspacePath)) {
576
+ await this.reportResult(cmd.id, 'Access denied: Path outside workspace', -1, 0);
577
+ return;
578
+ }
579
+ console.log(colors.file(`[FILE MOVE] ${srcPath} -> ${destPath}`));
580
+ try {
581
+ await fs.mkdir(path.dirname(destPath), { recursive: true });
582
+ await fs.rename(srcPath, destPath);
583
+ await this.reportResult(cmd.id, JSON.stringify({ success: true, from: match[1].trim(), to: match[2].trim() }), 0, 0);
584
+ console.log(colors.success(` ✓ Moved`));
585
+ }
586
+ catch (error) {
587
+ await this.reportResult(cmd.id, JSON.stringify({ success: false, error: error.message }), -1, 0);
588
+ }
589
+ }
590
+ async handleSearchFiles(cmd) {
591
+ const match = cmd.command_text.match(/__INTERNAL_SEARCH_FILES__\s+(.+?)(?:\s+--pattern=(.+))?$/);
592
+ if (!match) {
593
+ await this.reportResult(cmd.id, 'Invalid search command', -1, 0);
594
+ return;
595
+ }
596
+ const searchTerm = match[1].trim();
597
+ const pattern = match[2] || '*';
598
+ console.log(colors.file(`[SEARCH] "${searchTerm}" in ${pattern}`));
599
+ try {
600
+ const results = [];
601
+ await this.searchInDirectory(this.workspacePath, searchTerm, pattern, results, 100);
602
+ await this.reportResult(cmd.id, JSON.stringify({
603
+ success: true,
604
+ term: searchTerm,
605
+ results: results.slice(0, 100),
606
+ total: results.length,
607
+ }), 0, 0);
608
+ console.log(colors.success(` ✓ Found ${results.length} matches`));
609
+ }
610
+ catch (error) {
611
+ await this.reportResult(cmd.id, JSON.stringify({ success: false, error: error.message }), -1, 0);
612
+ }
613
+ }
614
+ async searchInDirectory(dir, term, pattern, results, maxResults) {
615
+ if (results.length >= maxResults)
616
+ return;
617
+ const entries = await fs.readdir(dir, { withFileTypes: true });
618
+ for (const entry of entries) {
619
+ if (results.length >= maxResults)
620
+ break;
621
+ if (entry.name.startsWith('.') || entry.name === 'node_modules')
622
+ continue;
623
+ const fullPath = path.join(dir, entry.name);
624
+ if (entry.isDirectory()) {
625
+ await this.searchInDirectory(fullPath, term, pattern, results, maxResults);
626
+ }
627
+ else if (entry.isFile()) {
628
+ try {
629
+ const content = await fs.readFile(fullPath, 'utf8');
630
+ const lines = content.split('\n');
631
+ lines.forEach((line, index) => {
632
+ if (line.toLowerCase().includes(term.toLowerCase())) {
633
+ results.push({
634
+ path: path.relative(this.workspacePath, fullPath),
635
+ line: index + 1,
636
+ content: line.trim().slice(0, 200),
637
+ });
638
+ }
639
+ });
640
+ }
641
+ catch {
642
+ // Skip binary files
643
+ }
644
+ }
645
+ }
646
+ }
647
+ async handleProcessList(cmd) {
648
+ console.log(colors.info('[PROCESS LIST]'));
649
+ try {
650
+ let output;
651
+ if (os.platform() === 'win32') {
652
+ const result = await execAsync('tasklist /fo csv');
653
+ output = result.stdout;
654
+ }
655
+ else {
656
+ const result = await execAsync('ps aux');
657
+ output = result.stdout;
658
+ }
659
+ await this.reportResult(cmd.id, JSON.stringify({
660
+ success: true,
661
+ processes: output,
662
+ platform: os.platform(),
663
+ }), 0, 0);
664
+ console.log(colors.success(' ✓ Process list retrieved'));
665
+ }
666
+ catch (error) {
667
+ await this.reportResult(cmd.id, JSON.stringify({ success: false, error: error.message }), -1, 0);
668
+ }
669
+ }
670
+ async handleEnvVars(cmd) {
671
+ console.log(colors.info('[ENV VARS]'));
672
+ // Filter out sensitive env vars
673
+ const safeEnvVars = {};
674
+ const sensitivePatterns = ['PASSWORD', 'SECRET', 'TOKEN', 'KEY', 'CREDENTIAL'];
675
+ for (const [key, value] of Object.entries(process.env)) {
676
+ const isSensitive = sensitivePatterns.some(pattern => key.toUpperCase().includes(pattern));
677
+ safeEnvVars[key] = isSensitive ? '***REDACTED***' : (value || '');
678
+ }
679
+ await this.reportResult(cmd.id, JSON.stringify({
680
+ success: true,
681
+ env: safeEnvVars,
682
+ }), 0, 0);
683
+ console.log(colors.success(' ✓ Environment variables retrieved'));
684
+ }
287
685
  async listDirectory(dirPath, recursive, includeHidden) {
288
686
  const entries = await fs.readdir(dirPath, { withFileTypes: true });
289
687
  const files = [];
@@ -325,14 +723,11 @@ class HigherupAgent {
325
723
  const display = match ? parseInt(match[1]) : 0;
326
724
  console.log(colors.info(`[SCREEN CAPTURE] Display ${display}`));
327
725
  try {
328
- let imagePath;
329
726
  const tempFile = path.join(os.tmpdir(), `higherup-screen-${Date.now()}.png`);
330
727
  if (os.platform() === 'darwin') {
331
- // macOS
332
728
  await execAsync(`screencapture -x -D ${display + 1} "${tempFile}"`);
333
729
  }
334
730
  else if (os.platform() === 'win32') {
335
- // Windows - use PowerShell
336
731
  const psScript = `
337
732
  Add-Type -AssemblyName System.Windows.Forms
338
733
  $screen = [System.Windows.Forms.Screen]::AllScreens[${display}]
@@ -344,7 +739,6 @@ class HigherupAgent {
344
739
  await execAsync(`powershell -Command "${psScript}"`);
345
740
  }
346
741
  else {
347
- // Linux - use scrot or import
348
742
  try {
349
743
  await execAsync(`scrot "${tempFile}"`);
350
744
  }
@@ -352,12 +746,10 @@ class HigherupAgent {
352
746
  await execAsync(`import -window root "${tempFile}"`);
353
747
  }
354
748
  }
355
- imagePath = tempFile;
356
- // Read the image and convert to base64
357
- const imageBuffer = await fs.readFile(imagePath);
749
+ const imageBuffer = await fs.readFile(tempFile);
358
750
  const base64Image = imageBuffer.toString('base64');
359
- // Clean up temp file
360
- await fs.unlink(imagePath).catch(() => { });
751
+ this.bytesTransferred += imageBuffer.length;
752
+ await fs.unlink(tempFile).catch(() => { });
361
753
  await this.reportResult(cmd.id, JSON.stringify({
362
754
  success: true,
363
755
  image: base64Image,
@@ -376,7 +768,6 @@ class HigherupAgent {
376
768
  }
377
769
  }
378
770
  async handleScreenStreamStart(cmd) {
379
- // Screen streaming would require WebSocket - just acknowledge for now
380
771
  await this.reportResult(cmd.id, JSON.stringify({
381
772
  success: true,
382
773
  message: 'Screen streaming initiated. Use WebSocket for continuous frames.',
@@ -386,6 +777,21 @@ class HigherupAgent {
386
777
  console.log(colors.info('[SYSTEM INFO]'));
387
778
  try {
388
779
  const cpus = os.cpus();
780
+ // Get disk usage
781
+ let diskInfo = {};
782
+ try {
783
+ if (os.platform() === 'win32') {
784
+ const { stdout } = await execAsync('wmic logicaldisk get size,freespace,caption');
785
+ diskInfo = { raw: stdout };
786
+ }
787
+ else {
788
+ const { stdout } = await execAsync('df -h /');
789
+ diskInfo = { raw: stdout };
790
+ }
791
+ }
792
+ catch {
793
+ diskInfo = { error: 'Unable to get disk info' };
794
+ }
389
795
  const systemInfo = {
390
796
  success: true,
391
797
  os: os.platform(),
@@ -404,8 +810,12 @@ class HigherupAgent {
404
810
  total: os.totalmem(),
405
811
  free: os.freemem(),
406
812
  used: os.totalmem() - os.freemem(),
813
+ percent_used: ((os.totalmem() - os.freemem()) / os.totalmem() * 100).toFixed(1),
407
814
  },
815
+ disk: diskInfo,
408
816
  uptime: os.uptime(),
817
+ load_average: os.loadavg(),
818
+ network: Object.keys(os.networkInterfaces()).length,
409
819
  workspace: {
410
820
  path: this.workspacePath,
411
821
  id: this.workspaceId,
@@ -414,6 +824,8 @@ class HigherupAgent {
414
824
  name: this.agentName,
415
825
  session: this.sessionId,
416
826
  autonomous: this.autonomousMode,
827
+ version: VERSION,
828
+ commands_executed: this.commandCount,
417
829
  },
418
830
  };
419
831
  await this.reportResult(cmd.id, JSON.stringify(systemInfo), 0, 0);
@@ -426,7 +838,6 @@ class HigherupAgent {
426
838
  async executeStreamingCommand(cmd) {
427
839
  const command = cmd.command_text.replace('__STREAM__ ', '');
428
840
  console.log(colors.command(`\n[STREAM] $ ${command}`));
429
- // For streaming, we execute normally but could implement chunk reporting
430
841
  await this.executeShellCommand(cmd, command);
431
842
  }
432
843
  async executeCommand(cmd) {
@@ -437,7 +848,19 @@ class HigherupAgent {
437
848
  const startTime = Date.now();
438
849
  // Security check for dangerous commands (unless autonomous mode)
439
850
  if (!this.autonomousMode) {
440
- const dangerousPatterns = ['rm -rf /', 'mkfs', 'dd if=', ':(){:|:&};:'];
851
+ const dangerousPatterns = [
852
+ 'rm -rf /',
853
+ 'rm -rf ~',
854
+ 'rm -rf /*',
855
+ 'mkfs',
856
+ 'dd if=',
857
+ ':(){:|:&};:',
858
+ '> /dev/sda',
859
+ 'chmod 777 /',
860
+ 'chmod -R 777 /',
861
+ 'chown root',
862
+ 'sudo rm -rf',
863
+ ];
441
864
  for (const pattern of dangerousPatterns) {
442
865
  if (command.includes(pattern)) {
443
866
  console.log(colors.error(` ✗ Blocked dangerous command pattern: ${pattern}`));
@@ -446,7 +869,6 @@ class HigherupAgent {
446
869
  }
447
870
  }
448
871
  }
449
- // Determine shell based on platform
450
872
  const isWindows = os.platform() === 'win32';
451
873
  const shell = isWindows ? 'cmd.exe' : '/bin/bash';
452
874
  const shellArgs = isWindows ? ['/c', command] : ['-c', command];
@@ -477,6 +899,7 @@ class HigherupAgent {
477
899
  });
478
900
  const duration = Date.now() - startTime;
479
901
  this.commandCount++;
902
+ this.bytesTransferred += result.output.length;
480
903
  const status = result.exitCode === 0 ? colors.success('✓') : colors.error('✗');
481
904
  console.log(colors.dim(`\n[Completed in ${duration}ms] ${status} Exit code: ${result.exitCode}\n`));
482
905
  await this.reportResult(cmd.id, result.output.slice(-10000), result.exitCode, duration);
@@ -500,21 +923,24 @@ class HigherupAgent {
500
923
  if (this.pollTimer) {
501
924
  clearInterval(this.pollTimer);
502
925
  }
926
+ if (this.heartbeatTimer) {
927
+ clearInterval(this.heartbeatTimer);
928
+ }
503
929
  try {
504
930
  await this.apiCall('disconnect', {
505
931
  session_id: this.sessionId,
506
932
  workspace_id: this.workspaceId,
507
933
  });
508
934
  }
509
- catch (error) {
935
+ catch {
510
936
  // Ignore disconnect errors
511
937
  }
512
938
  console.log(colors.success('Disconnected. Goodbye!\n'));
513
- console.log(colors.dim(`Session stats: ${this.commandCount} commands executed\n`));
939
+ this.printStats();
514
940
  process.exit(0);
515
941
  }
516
942
  }
517
- // CLI Setup
943
+ // Helper functions
518
944
  async function loadConfig() {
519
945
  try {
520
946
  await fs.mkdir(CONFIG_DIR, { recursive: true });
@@ -529,73 +955,237 @@ async function saveConfig(config) {
529
955
  await fs.mkdir(CONFIG_DIR, { recursive: true });
530
956
  await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2));
531
957
  }
958
+ async function fetchWorkspaces(apiToken) {
959
+ try {
960
+ const response = await fetch(`${API_BASE_URL}?action=status`, {
961
+ method: 'POST',
962
+ headers: {
963
+ 'Content-Type': 'application/json',
964
+ 'x-api-token': apiToken,
965
+ },
966
+ body: JSON.stringify({}),
967
+ });
968
+ if (!response.ok) {
969
+ throw new Error('Failed to fetch workspaces');
970
+ }
971
+ const data = await response.json();
972
+ return data.workspaces || [];
973
+ }
974
+ catch {
975
+ return [];
976
+ }
977
+ }
978
+ // CLI Setup
532
979
  const program = new Command();
533
980
  program
534
981
  .name('higherup')
535
- .description('Higherup Local Agent - Give AI agents full access to your development environment')
536
- .version('1.0.0');
982
+ .description('Higherup Local Agent v' + VERSION + ' - Give AI agents full access to your development environment')
983
+ .version(VERSION);
984
+ // Main connect command
537
985
  program
538
986
  .command('connect')
539
987
  .description('Connect to Higherup service')
540
- .requiredOption('-w, --workspace <id>', 'Workspace ID to connect')
541
- .requiredOption('-p, --path <path>', 'Local workspace path', '.')
988
+ .option('-w, --workspace <id>', 'Workspace ID to connect')
989
+ .option('-p, --path <path>', 'Local workspace path')
542
990
  .option('-t, --token <token>', 'API token (or use HIGHERUP_API_TOKEN env var)')
543
991
  .option('-n, --name <name>', 'Agent name for identification')
544
992
  .option('--autonomous', 'Enable autonomous mode (unrestricted access)')
993
+ .option('--auto-reconnect', 'Automatically reconnect on connection loss')
545
994
  .action(async (options) => {
546
995
  const config = await loadConfig();
547
- const apiToken = options.token || process.env.HIGHERUP_API_TOKEN || config.apiToken;
996
+ let apiToken = options.token || process.env.HIGHERUP_API_TOKEN || config.apiToken;
997
+ let workspaceId = options.workspace || config.defaultWorkspace;
998
+ let workspacePath = options.path || config.defaultPath || '.';
999
+ // Interactive setup if missing required options
548
1000
  if (!apiToken) {
549
- console.error(colors.error('Error: API token is required'));
550
- console.log(colors.info('\nProvide token via:'));
551
- console.log(' --token <token>');
552
- console.log(' HIGHERUP_API_TOKEN environment variable');
553
- console.log(' or run: higherup config --token <token>');
554
- process.exit(1);
1001
+ console.log(colors.bold('\n ═══ Higherup Setup ═══\n'));
1002
+ console.log(colors.dim(' Get your API token from: https://higherup.ai/dashboard/api\n'));
1003
+ apiToken = await prompt(' Enter your API token');
1004
+ if (!apiToken) {
1005
+ console.error(colors.error('\n Error: API token is required'));
1006
+ process.exit(1);
1007
+ }
1008
+ if (await confirm(' Save token for future use?')) {
1009
+ config.apiToken = apiToken;
1010
+ await saveConfig(config);
1011
+ console.log(colors.success(' Token saved to ~/.higherup/config.json'));
1012
+ }
1013
+ }
1014
+ // Fetch and select workspace if not provided
1015
+ if (!workspaceId) {
1016
+ const spinner = ora(' Fetching your workspaces...').start();
1017
+ const workspaces = await fetchWorkspaces(apiToken);
1018
+ spinner.stop();
1019
+ if (workspaces.length > 0) {
1020
+ const selected = await selectFromList(workspaces, 'Select a workspace:');
1021
+ if (selected) {
1022
+ workspaceId = selected.id;
1023
+ }
1024
+ }
1025
+ if (!workspaceId) {
1026
+ console.log(colors.dim('\n No workspaces found. Create one at https://higherup.ai/dashboard/workspaces'));
1027
+ workspaceId = await prompt(' Or enter workspace ID manually');
1028
+ }
1029
+ if (!workspaceId) {
1030
+ console.error(colors.error('\n Error: Workspace ID is required'));
1031
+ process.exit(1);
1032
+ }
1033
+ }
1034
+ // Get workspace path
1035
+ if (workspacePath === '.') {
1036
+ const suggested = process.cwd();
1037
+ console.log('');
1038
+ workspacePath = await prompt(` Workspace path`, suggested);
555
1039
  }
556
1040
  const agent = new HigherupAgent();
557
1041
  try {
558
- await agent.connect(options.path, options.workspace, apiToken, options.name, options.autonomous);
1042
+ await agent.connect(workspacePath, workspaceId, apiToken, options.name || config.agentName, options.autonomous, options.autoReconnect);
559
1043
  }
560
1044
  catch (error) {
561
1045
  console.error(colors.error(`\nFailed to connect: ${error.message}`));
562
1046
  process.exit(1);
563
1047
  }
564
1048
  });
1049
+ // Quick connect with just token
1050
+ program
1051
+ .command('quick')
1052
+ .description('Quick connect with interactive workspace selection')
1053
+ .action(async () => {
1054
+ const config = await loadConfig();
1055
+ const apiToken = process.env.HIGHERUP_API_TOKEN || config.apiToken;
1056
+ if (!apiToken) {
1057
+ console.error(colors.error('Error: No API token configured'));
1058
+ console.log(colors.info('Run: higherup config --token <your-token>'));
1059
+ process.exit(1);
1060
+ }
1061
+ const spinner = ora('Fetching workspaces...').start();
1062
+ const workspaces = await fetchWorkspaces(apiToken);
1063
+ spinner.stop();
1064
+ if (workspaces.length === 0) {
1065
+ console.log(colors.warning('No workspaces found.'));
1066
+ console.log(colors.info('Create one at https://higherup.ai/dashboard/workspaces'));
1067
+ process.exit(1);
1068
+ }
1069
+ const selected = await selectFromList(workspaces, 'Select a workspace:');
1070
+ if (!selected) {
1071
+ console.log(colors.warning('No workspace selected.'));
1072
+ process.exit(1);
1073
+ }
1074
+ const agent = new HigherupAgent();
1075
+ await agent.connect(process.cwd(), selected.id, apiToken);
1076
+ });
1077
+ // Init command for first-time setup
1078
+ program
1079
+ .command('init')
1080
+ .description('Initialize Higherup in current directory')
1081
+ .action(async () => {
1082
+ console.log(colors.bold('\n ═══ Higherup Initialization ═══\n'));
1083
+ const config = await loadConfig();
1084
+ // Step 1: API Token
1085
+ if (!config.apiToken) {
1086
+ console.log(colors.info(' Step 1: Configure API Token'));
1087
+ console.log(colors.dim(' Get your token from: https://higherup.ai/dashboard/api\n'));
1088
+ const token = await prompt(' API Token');
1089
+ if (token) {
1090
+ config.apiToken = token;
1091
+ console.log(colors.success(' ✓ Token saved\n'));
1092
+ }
1093
+ }
1094
+ else {
1095
+ console.log(colors.success(' ✓ API Token already configured\n'));
1096
+ }
1097
+ // Step 2: Default workspace
1098
+ console.log(colors.info(' Step 2: Set Default Workspace'));
1099
+ if (config.apiToken) {
1100
+ const spinner = ora(' Fetching workspaces...').start();
1101
+ const workspaces = await fetchWorkspaces(config.apiToken);
1102
+ spinner.stop();
1103
+ if (workspaces.length > 0) {
1104
+ const selected = await selectFromList(workspaces, ' Select default workspace:');
1105
+ if (selected) {
1106
+ config.defaultWorkspace = selected.id;
1107
+ console.log(colors.success(`\n ✓ Default workspace: ${selected.name}`));
1108
+ }
1109
+ }
1110
+ else {
1111
+ const wsId = await prompt(' Workspace ID (or create one at dashboard)');
1112
+ if (wsId)
1113
+ config.defaultWorkspace = wsId;
1114
+ }
1115
+ }
1116
+ // Step 3: Default path
1117
+ console.log(colors.info('\n Step 3: Set Default Path'));
1118
+ const defaultPath = await prompt(' Default workspace path', process.cwd());
1119
+ config.defaultPath = defaultPath;
1120
+ // Step 4: Agent name
1121
+ console.log(colors.info('\n Step 4: Set Agent Name'));
1122
+ const agentName = await prompt(' Agent name', `${os.hostname()}-${os.platform()}`);
1123
+ config.agentName = agentName;
1124
+ await saveConfig(config);
1125
+ console.log(colors.success('\n ═══ Setup Complete! ═══\n'));
1126
+ console.log(' Run ' + colors.bold('higherup connect') + ' to start the agent\n');
1127
+ });
1128
+ // Config command
565
1129
  program
566
1130
  .command('config')
567
1131
  .description('Configure default settings')
568
1132
  .option('-t, --token <token>', 'Set default API token')
569
1133
  .option('-w, --workspace <id>', 'Set default workspace')
1134
+ .option('-p, --path <path>', 'Set default workspace path')
1135
+ .option('-n, --name <name>', 'Set default agent name')
570
1136
  .option('--show', 'Show current configuration')
1137
+ .option('--reset', 'Reset all configuration')
571
1138
  .action(async (options) => {
572
1139
  const config = await loadConfig();
1140
+ if (options.reset) {
1141
+ await saveConfig({});
1142
+ console.log(colors.success('✓ Configuration reset'));
1143
+ return;
1144
+ }
573
1145
  if (options.show) {
574
1146
  console.log(colors.bold('\nHigherup Configuration:'));
575
1147
  console.log(colors.dim(`Config file: ${CONFIG_FILE}\n`));
576
- console.log(` API Token: ${config.apiToken ? colors.success('Set') : colors.warning('Not set')}`);
1148
+ console.log(` API Token: ${config.apiToken ? colors.success('Set (' + config.apiToken.slice(0, 8) + '...)') : colors.warning('Not set')}`);
577
1149
  console.log(` Default Workspace: ${config.defaultWorkspace || colors.dim('Not set')}`);
1150
+ console.log(` Default Path: ${config.defaultPath || colors.dim('Not set')}`);
1151
+ console.log(` Agent Name: ${config.agentName || colors.dim('Not set')}`);
1152
+ console.log(` Log Level: ${config.logLevel || colors.dim('info')}`);
1153
+ console.log(` Auto Reconnect: ${config.autoReconnect ? colors.success('Enabled') : colors.dim('Disabled')}`);
578
1154
  console.log('');
579
1155
  return;
580
1156
  }
1157
+ let updated = false;
581
1158
  if (options.token) {
582
1159
  config.apiToken = options.token;
583
1160
  console.log(colors.success('✓ API token saved'));
1161
+ updated = true;
584
1162
  }
585
1163
  if (options.workspace) {
586
1164
  config.defaultWorkspace = options.workspace;
587
1165
  console.log(colors.success('✓ Default workspace saved'));
1166
+ updated = true;
1167
+ }
1168
+ if (options.path) {
1169
+ config.defaultPath = options.path;
1170
+ console.log(colors.success('✓ Default path saved'));
1171
+ updated = true;
1172
+ }
1173
+ if (options.name) {
1174
+ config.agentName = options.name;
1175
+ console.log(colors.success('✓ Agent name saved'));
1176
+ updated = true;
588
1177
  }
589
- if (options.token || options.workspace) {
1178
+ if (updated) {
590
1179
  await saveConfig(config);
591
1180
  }
592
1181
  else {
593
1182
  console.log('Use --help to see available options');
594
1183
  }
595
1184
  });
1185
+ // Status command
596
1186
  program
597
1187
  .command('status')
598
- .description('Check connection status')
1188
+ .description('Check connection status and active sessions')
599
1189
  .option('-t, --token <token>', 'API token')
600
1190
  .action(async (options) => {
601
1191
  const config = await loadConfig();
@@ -617,17 +1207,20 @@ program
617
1207
  const data = await response.json();
618
1208
  spinner.stop();
619
1209
  if (data.active_sessions && data.active_sessions.length > 0) {
620
- console.log(colors.success('\n✓ Connected sessions:\n'));
1210
+ console.log(colors.success('\n✓ Active Sessions:\n'));
621
1211
  for (const session of data.active_sessions) {
622
1212
  console.log(` ${colors.bold(session.agent_name || 'Agent')}`);
623
1213
  console.log(colors.dim(` Workspace: ${session.workspaces?.name || session.workspace_id}`));
624
1214
  console.log(colors.dim(` Connected: ${new Date(session.connected_at).toLocaleString()}`));
1215
+ if (session.last_activity) {
1216
+ console.log(colors.dim(` Last Activity: ${new Date(session.last_activity).toLocaleString()}`));
1217
+ }
625
1218
  console.log('');
626
1219
  }
627
1220
  }
628
1221
  else {
629
1222
  console.log(colors.warning('\n⚠ No active sessions'));
630
- console.log(colors.dim('Run: higherup connect -w <workspace-id> -p <path>\n'));
1223
+ console.log(colors.dim('Run: higherup connect -w <workspace-id>\n'));
631
1224
  }
632
1225
  }
633
1226
  catch (error) {
@@ -635,5 +1228,109 @@ program
635
1228
  process.exit(1);
636
1229
  }
637
1230
  });
1231
+ // Logs command
1232
+ program
1233
+ .command('logs')
1234
+ .description('View agent logs')
1235
+ .option('-n, --lines <number>', 'Number of lines to show', '50')
1236
+ .option('-f, --follow', 'Follow log output')
1237
+ .action(async (options) => {
1238
+ try {
1239
+ const content = await fs.readFile(LOG_FILE, 'utf8');
1240
+ const lines = content.split('\n');
1241
+ const numLines = parseInt(options.lines);
1242
+ console.log(colors.bold(`\nLast ${numLines} log entries:\n`));
1243
+ console.log(lines.slice(-numLines).join('\n'));
1244
+ }
1245
+ catch {
1246
+ console.log(colors.warning('No logs found.'));
1247
+ }
1248
+ });
1249
+ // Doctor command to diagnose issues
1250
+ program
1251
+ .command('doctor')
1252
+ .description('Diagnose common issues')
1253
+ .action(async () => {
1254
+ console.log(colors.bold('\n Higherup Doctor - Diagnosing issues...\n'));
1255
+ let issues = 0;
1256
+ // Check Node version
1257
+ const nodeVersion = process.version;
1258
+ const majorVersion = parseInt(nodeVersion.slice(1).split('.')[0]);
1259
+ if (majorVersion >= 18) {
1260
+ console.log(colors.success(` ✓ Node.js ${nodeVersion}`));
1261
+ }
1262
+ else {
1263
+ console.log(colors.error(` ✗ Node.js ${nodeVersion} (requires v18+)`));
1264
+ issues++;
1265
+ }
1266
+ // Check config
1267
+ const config = await loadConfig();
1268
+ if (config.apiToken) {
1269
+ console.log(colors.success(' ✓ API token configured'));
1270
+ }
1271
+ else {
1272
+ console.log(colors.warning(' ⚠ API token not configured'));
1273
+ issues++;
1274
+ }
1275
+ // Check network connectivity
1276
+ try {
1277
+ const response = await fetch('https://pltlcpqtivuvyeuywvql.supabase.co/functions/v1/agent-relay?action=ping', {
1278
+ method: 'POST',
1279
+ headers: { 'Content-Type': 'application/json' },
1280
+ body: '{}',
1281
+ });
1282
+ if (response.ok || response.status === 401) {
1283
+ console.log(colors.success(' ✓ Network connectivity'));
1284
+ }
1285
+ else {
1286
+ console.log(colors.error(' ✗ Cannot reach Higherup service'));
1287
+ issues++;
1288
+ }
1289
+ }
1290
+ catch {
1291
+ console.log(colors.error(' ✗ Network error'));
1292
+ issues++;
1293
+ }
1294
+ // Check screen capture tools
1295
+ if (os.platform() === 'linux') {
1296
+ try {
1297
+ await execAsync('which scrot');
1298
+ console.log(colors.success(' ✓ Screen capture tool (scrot)'));
1299
+ }
1300
+ catch {
1301
+ try {
1302
+ await execAsync('which import');
1303
+ console.log(colors.success(' ✓ Screen capture tool (imagemagick)'));
1304
+ }
1305
+ catch {
1306
+ console.log(colors.warning(' ⚠ No screen capture tool found (install scrot)'));
1307
+ }
1308
+ }
1309
+ }
1310
+ else {
1311
+ console.log(colors.success(' ✓ Screen capture available'));
1312
+ }
1313
+ // Summary
1314
+ console.log('');
1315
+ if (issues === 0) {
1316
+ console.log(colors.success(' All checks passed!\n'));
1317
+ }
1318
+ else {
1319
+ console.log(colors.warning(` ${issues} issue(s) found. See above for details.\n`));
1320
+ }
1321
+ });
1322
+ // Version with more info
1323
+ program
1324
+ .command('info')
1325
+ .description('Show version and system information')
1326
+ .action(() => {
1327
+ console.log(colors.bold(`\n Higherup Agent v${VERSION}\n`));
1328
+ console.log(` Node.js: ${process.version}`);
1329
+ console.log(` Platform: ${os.platform()} ${os.arch()}`);
1330
+ console.log(` Hostname: ${os.hostname()}`);
1331
+ console.log(` Config: ${CONFIG_FILE}`);
1332
+ console.log(` API URL: ${API_BASE_URL}`);
1333
+ console.log('');
1334
+ });
638
1335
  program.parse();
639
1336
  //# sourceMappingURL=agent.js.map