higherup 1.0.1 → 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/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.1';
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,15 +899,51 @@ 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);
483
906
  }
484
907
  catch (error) {
485
908
  console.log(colors.error(`\n[ERROR] ${error.message}\n`));
909
+ // Self-Healing Logic
910
+ if (this.autonomousMode) {
911
+ const healed = await this.attemptSelfHeal(cmd, command, error.message);
912
+ if (healed)
913
+ return;
914
+ }
486
915
  await this.reportResult(cmd.id, error.message, -1, Date.now() - startTime);
487
916
  }
488
917
  }
918
+ async attemptSelfHeal(cmd, originalCommand, error) {
919
+ console.log(colors.info(`\n[SELF-HEALING] Analysing error...`));
920
+ // Heuristic 1: Permission Denied
921
+ if (error.includes('Permission denied') || error.includes('EACCES')) {
922
+ if (!originalCommand.startsWith('sudo ')) {
923
+ console.log(colors.warning(` ➜ Detected permission issue. Retrying with sudo...`));
924
+ await this.executeShellCommand(cmd, `sudo ${originalCommand}`);
925
+ return true;
926
+ }
927
+ }
928
+ // Heuristic 2: Missing Dependencies (npm)
929
+ if (originalCommand.includes('npm') && (error.includes('sh: npm: command not found') || error.includes('ENOENT'))) {
930
+ // Cannot fix missing npm easily without OS detection, but we can log hint
931
+ console.log(colors.warning(` ➜ Missing npm. Attempting to locate...`));
932
+ }
933
+ // Heuristic 3: GIT Lock
934
+ if (originalCommand.startsWith('git') && error.includes('index.lock')) {
935
+ console.log(colors.warning(` ➜ Git lock detected. Removing index.lock...`));
936
+ try {
937
+ await fs.unlink(path.join(this.workspacePath, '.git', 'index.lock'));
938
+ await this.executeShellCommand(cmd, originalCommand);
939
+ return true;
940
+ }
941
+ catch {
942
+ // failed to clean lock
943
+ }
944
+ }
945
+ return false; // Could not heal
946
+ }
489
947
  async reportResult(commandId, output, exitCode, duration) {
490
948
  await this.apiCall('result', {
491
949
  command_id: commandId,
@@ -500,21 +958,24 @@ class HigherupAgent {
500
958
  if (this.pollTimer) {
501
959
  clearInterval(this.pollTimer);
502
960
  }
961
+ if (this.heartbeatTimer) {
962
+ clearInterval(this.heartbeatTimer);
963
+ }
503
964
  try {
504
965
  await this.apiCall('disconnect', {
505
966
  session_id: this.sessionId,
506
967
  workspace_id: this.workspaceId,
507
968
  });
508
969
  }
509
- catch (error) {
970
+ catch {
510
971
  // Ignore disconnect errors
511
972
  }
512
973
  console.log(colors.success('Disconnected. Goodbye!\n'));
513
- console.log(colors.dim(`Session stats: ${this.commandCount} commands executed\n`));
974
+ this.printStats();
514
975
  process.exit(0);
515
976
  }
516
977
  }
517
- // CLI Setup
978
+ // Helper functions
518
979
  async function loadConfig() {
519
980
  try {
520
981
  await fs.mkdir(CONFIG_DIR, { recursive: true });
@@ -529,73 +990,237 @@ async function saveConfig(config) {
529
990
  await fs.mkdir(CONFIG_DIR, { recursive: true });
530
991
  await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2));
531
992
  }
993
+ async function fetchWorkspaces(apiToken) {
994
+ try {
995
+ const response = await fetch(`${API_BASE_URL}?action=status`, {
996
+ method: 'POST',
997
+ headers: {
998
+ 'Content-Type': 'application/json',
999
+ 'x-api-token': apiToken,
1000
+ },
1001
+ body: JSON.stringify({}),
1002
+ });
1003
+ if (!response.ok) {
1004
+ throw new Error('Failed to fetch workspaces');
1005
+ }
1006
+ const data = await response.json();
1007
+ return data.workspaces || [];
1008
+ }
1009
+ catch {
1010
+ return [];
1011
+ }
1012
+ }
1013
+ // CLI Setup
532
1014
  const program = new Command();
533
1015
  program
534
1016
  .name('higherup')
535
- .description('Higherup Local Agent - Give AI agents full access to your development environment')
536
- .version('1.0.0');
1017
+ .description('Higherup Local Agent v' + VERSION + ' - Give AI agents full access to your development environment')
1018
+ .version(VERSION);
1019
+ // Main connect command
537
1020
  program
538
1021
  .command('connect')
539
1022
  .description('Connect to Higherup service')
540
- .requiredOption('-w, --workspace <id>', 'Workspace ID to connect')
541
- .requiredOption('-p, --path <path>', 'Local workspace path', '.')
1023
+ .option('-w, --workspace <id>', 'Workspace ID to connect')
1024
+ .option('-p, --path <path>', 'Local workspace path')
542
1025
  .option('-t, --token <token>', 'API token (or use HIGHERUP_API_TOKEN env var)')
543
1026
  .option('-n, --name <name>', 'Agent name for identification')
544
1027
  .option('--autonomous', 'Enable autonomous mode (unrestricted access)')
1028
+ .option('--auto-reconnect', 'Automatically reconnect on connection loss')
545
1029
  .action(async (options) => {
546
1030
  const config = await loadConfig();
547
- const apiToken = options.token || process.env.HIGHERUP_API_TOKEN || config.apiToken;
1031
+ let apiToken = options.token || process.env.HIGHERUP_API_TOKEN || config.apiToken;
1032
+ let workspaceId = options.workspace || config.defaultWorkspace;
1033
+ let workspacePath = options.path || config.defaultPath || '.';
1034
+ // Interactive setup if missing required options
548
1035
  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);
1036
+ console.log(colors.bold('\n ═══ Higherup Setup ═══\n'));
1037
+ console.log(colors.dim(' Get your API token from: https://higherup.ai/dashboard/api\n'));
1038
+ apiToken = await prompt(' Enter your API token');
1039
+ if (!apiToken) {
1040
+ console.error(colors.error('\n Error: API token is required'));
1041
+ process.exit(1);
1042
+ }
1043
+ if (await confirm(' Save token for future use?')) {
1044
+ config.apiToken = apiToken;
1045
+ await saveConfig(config);
1046
+ console.log(colors.success(' Token saved to ~/.higherup/config.json'));
1047
+ }
1048
+ }
1049
+ // Fetch and select workspace if not provided
1050
+ if (!workspaceId) {
1051
+ const spinner = ora(' Fetching your workspaces...').start();
1052
+ const workspaces = await fetchWorkspaces(apiToken);
1053
+ spinner.stop();
1054
+ if (workspaces.length > 0) {
1055
+ const selected = await selectFromList(workspaces, 'Select a workspace:');
1056
+ if (selected) {
1057
+ workspaceId = selected.id;
1058
+ }
1059
+ }
1060
+ if (!workspaceId) {
1061
+ console.log(colors.dim('\n No workspaces found. Create one at https://higherup.ai/dashboard/workspaces'));
1062
+ workspaceId = await prompt(' Or enter workspace ID manually');
1063
+ }
1064
+ if (!workspaceId) {
1065
+ console.error(colors.error('\n Error: Workspace ID is required'));
1066
+ process.exit(1);
1067
+ }
1068
+ }
1069
+ // Get workspace path
1070
+ if (workspacePath === '.') {
1071
+ const suggested = process.cwd();
1072
+ console.log('');
1073
+ workspacePath = await prompt(` Workspace path`, suggested);
555
1074
  }
556
1075
  const agent = new HigherupAgent();
557
1076
  try {
558
- await agent.connect(options.path, options.workspace, apiToken, options.name, options.autonomous);
1077
+ await agent.connect(workspacePath, workspaceId, apiToken, options.name || config.agentName, options.autonomous, options.autoReconnect);
559
1078
  }
560
1079
  catch (error) {
561
1080
  console.error(colors.error(`\nFailed to connect: ${error.message}`));
562
1081
  process.exit(1);
563
1082
  }
564
1083
  });
1084
+ // Quick connect with just token
1085
+ program
1086
+ .command('quick')
1087
+ .description('Quick connect with interactive workspace selection')
1088
+ .action(async () => {
1089
+ const config = await loadConfig();
1090
+ const apiToken = process.env.HIGHERUP_API_TOKEN || config.apiToken;
1091
+ if (!apiToken) {
1092
+ console.error(colors.error('Error: No API token configured'));
1093
+ console.log(colors.info('Run: higherup config --token <your-token>'));
1094
+ process.exit(1);
1095
+ }
1096
+ const spinner = ora('Fetching workspaces...').start();
1097
+ const workspaces = await fetchWorkspaces(apiToken);
1098
+ spinner.stop();
1099
+ if (workspaces.length === 0) {
1100
+ console.log(colors.warning('No workspaces found.'));
1101
+ console.log(colors.info('Create one at https://higherup.ai/dashboard/workspaces'));
1102
+ process.exit(1);
1103
+ }
1104
+ const selected = await selectFromList(workspaces, 'Select a workspace:');
1105
+ if (!selected) {
1106
+ console.log(colors.warning('No workspace selected.'));
1107
+ process.exit(1);
1108
+ }
1109
+ const agent = new HigherupAgent();
1110
+ await agent.connect(process.cwd(), selected.id, apiToken);
1111
+ });
1112
+ // Init command for first-time setup
1113
+ program
1114
+ .command('init')
1115
+ .description('Initialize Higherup in current directory')
1116
+ .action(async () => {
1117
+ console.log(colors.bold('\n ═══ Higherup Initialization ═══\n'));
1118
+ const config = await loadConfig();
1119
+ // Step 1: API Token
1120
+ if (!config.apiToken) {
1121
+ console.log(colors.info(' Step 1: Configure API Token'));
1122
+ console.log(colors.dim(' Get your token from: https://higherup.ai/dashboard/api\n'));
1123
+ const token = await prompt(' API Token');
1124
+ if (token) {
1125
+ config.apiToken = token;
1126
+ console.log(colors.success(' ✓ Token saved\n'));
1127
+ }
1128
+ }
1129
+ else {
1130
+ console.log(colors.success(' ✓ API Token already configured\n'));
1131
+ }
1132
+ // Step 2: Default workspace
1133
+ console.log(colors.info(' Step 2: Set Default Workspace'));
1134
+ if (config.apiToken) {
1135
+ const spinner = ora(' Fetching workspaces...').start();
1136
+ const workspaces = await fetchWorkspaces(config.apiToken);
1137
+ spinner.stop();
1138
+ if (workspaces.length > 0) {
1139
+ const selected = await selectFromList(workspaces, ' Select default workspace:');
1140
+ if (selected) {
1141
+ config.defaultWorkspace = selected.id;
1142
+ console.log(colors.success(`\n ✓ Default workspace: ${selected.name}`));
1143
+ }
1144
+ }
1145
+ else {
1146
+ const wsId = await prompt(' Workspace ID (or create one at dashboard)');
1147
+ if (wsId)
1148
+ config.defaultWorkspace = wsId;
1149
+ }
1150
+ }
1151
+ // Step 3: Default path
1152
+ console.log(colors.info('\n Step 3: Set Default Path'));
1153
+ const defaultPath = await prompt(' Default workspace path', process.cwd());
1154
+ config.defaultPath = defaultPath;
1155
+ // Step 4: Agent name
1156
+ console.log(colors.info('\n Step 4: Set Agent Name'));
1157
+ const agentName = await prompt(' Agent name', `${os.hostname()}-${os.platform()}`);
1158
+ config.agentName = agentName;
1159
+ await saveConfig(config);
1160
+ console.log(colors.success('\n ═══ Setup Complete! ═══\n'));
1161
+ console.log(' Run ' + colors.bold('higherup connect') + ' to start the agent\n');
1162
+ });
1163
+ // Config command
565
1164
  program
566
1165
  .command('config')
567
1166
  .description('Configure default settings')
568
1167
  .option('-t, --token <token>', 'Set default API token')
569
1168
  .option('-w, --workspace <id>', 'Set default workspace')
1169
+ .option('-p, --path <path>', 'Set default workspace path')
1170
+ .option('-n, --name <name>', 'Set default agent name')
570
1171
  .option('--show', 'Show current configuration')
1172
+ .option('--reset', 'Reset all configuration')
571
1173
  .action(async (options) => {
572
1174
  const config = await loadConfig();
1175
+ if (options.reset) {
1176
+ await saveConfig({});
1177
+ console.log(colors.success('✓ Configuration reset'));
1178
+ return;
1179
+ }
573
1180
  if (options.show) {
574
1181
  console.log(colors.bold('\nHigherup Configuration:'));
575
1182
  console.log(colors.dim(`Config file: ${CONFIG_FILE}\n`));
576
- console.log(` API Token: ${config.apiToken ? colors.success('Set') : colors.warning('Not set')}`);
1183
+ console.log(` API Token: ${config.apiToken ? colors.success('Set (' + config.apiToken.slice(0, 8) + '...)') : colors.warning('Not set')}`);
577
1184
  console.log(` Default Workspace: ${config.defaultWorkspace || colors.dim('Not set')}`);
1185
+ console.log(` Default Path: ${config.defaultPath || colors.dim('Not set')}`);
1186
+ console.log(` Agent Name: ${config.agentName || colors.dim('Not set')}`);
1187
+ console.log(` Log Level: ${config.logLevel || colors.dim('info')}`);
1188
+ console.log(` Auto Reconnect: ${config.autoReconnect ? colors.success('Enabled') : colors.dim('Disabled')}`);
578
1189
  console.log('');
579
1190
  return;
580
1191
  }
1192
+ let updated = false;
581
1193
  if (options.token) {
582
1194
  config.apiToken = options.token;
583
1195
  console.log(colors.success('✓ API token saved'));
1196
+ updated = true;
584
1197
  }
585
1198
  if (options.workspace) {
586
1199
  config.defaultWorkspace = options.workspace;
587
1200
  console.log(colors.success('✓ Default workspace saved'));
1201
+ updated = true;
1202
+ }
1203
+ if (options.path) {
1204
+ config.defaultPath = options.path;
1205
+ console.log(colors.success('✓ Default path saved'));
1206
+ updated = true;
588
1207
  }
589
- if (options.token || options.workspace) {
1208
+ if (options.name) {
1209
+ config.agentName = options.name;
1210
+ console.log(colors.success('✓ Agent name saved'));
1211
+ updated = true;
1212
+ }
1213
+ if (updated) {
590
1214
  await saveConfig(config);
591
1215
  }
592
1216
  else {
593
1217
  console.log('Use --help to see available options');
594
1218
  }
595
1219
  });
1220
+ // Status command
596
1221
  program
597
1222
  .command('status')
598
- .description('Check connection status')
1223
+ .description('Check connection status and active sessions')
599
1224
  .option('-t, --token <token>', 'API token')
600
1225
  .action(async (options) => {
601
1226
  const config = await loadConfig();
@@ -617,17 +1242,20 @@ program
617
1242
  const data = await response.json();
618
1243
  spinner.stop();
619
1244
  if (data.active_sessions && data.active_sessions.length > 0) {
620
- console.log(colors.success('\n✓ Connected sessions:\n'));
1245
+ console.log(colors.success('\n✓ Active Sessions:\n'));
621
1246
  for (const session of data.active_sessions) {
622
1247
  console.log(` ${colors.bold(session.agent_name || 'Agent')}`);
623
1248
  console.log(colors.dim(` Workspace: ${session.workspaces?.name || session.workspace_id}`));
624
1249
  console.log(colors.dim(` Connected: ${new Date(session.connected_at).toLocaleString()}`));
1250
+ if (session.last_activity) {
1251
+ console.log(colors.dim(` Last Activity: ${new Date(session.last_activity).toLocaleString()}`));
1252
+ }
625
1253
  console.log('');
626
1254
  }
627
1255
  }
628
1256
  else {
629
1257
  console.log(colors.warning('\n⚠ No active sessions'));
630
- console.log(colors.dim('Run: higherup connect -w <workspace-id> -p <path>\n'));
1258
+ console.log(colors.dim('Run: higherup connect -w <workspace-id>\n'));
631
1259
  }
632
1260
  }
633
1261
  catch (error) {
@@ -635,5 +1263,109 @@ program
635
1263
  process.exit(1);
636
1264
  }
637
1265
  });
1266
+ // Logs command
1267
+ program
1268
+ .command('logs')
1269
+ .description('View agent logs')
1270
+ .option('-n, --lines <number>', 'Number of lines to show', '50')
1271
+ .option('-f, --follow', 'Follow log output')
1272
+ .action(async (options) => {
1273
+ try {
1274
+ const content = await fs.readFile(LOG_FILE, 'utf8');
1275
+ const lines = content.split('\n');
1276
+ const numLines = parseInt(options.lines);
1277
+ console.log(colors.bold(`\nLast ${numLines} log entries:\n`));
1278
+ console.log(lines.slice(-numLines).join('\n'));
1279
+ }
1280
+ catch {
1281
+ console.log(colors.warning('No logs found.'));
1282
+ }
1283
+ });
1284
+ // Doctor command to diagnose issues
1285
+ program
1286
+ .command('doctor')
1287
+ .description('Diagnose common issues')
1288
+ .action(async () => {
1289
+ console.log(colors.bold('\n Higherup Doctor - Diagnosing issues...\n'));
1290
+ let issues = 0;
1291
+ // Check Node version
1292
+ const nodeVersion = process.version;
1293
+ const majorVersion = parseInt(nodeVersion.slice(1).split('.')[0]);
1294
+ if (majorVersion >= 18) {
1295
+ console.log(colors.success(` ✓ Node.js ${nodeVersion}`));
1296
+ }
1297
+ else {
1298
+ console.log(colors.error(` ✗ Node.js ${nodeVersion} (requires v18+)`));
1299
+ issues++;
1300
+ }
1301
+ // Check config
1302
+ const config = await loadConfig();
1303
+ if (config.apiToken) {
1304
+ console.log(colors.success(' ✓ API token configured'));
1305
+ }
1306
+ else {
1307
+ console.log(colors.warning(' ⚠ API token not configured'));
1308
+ issues++;
1309
+ }
1310
+ // Check network connectivity
1311
+ try {
1312
+ const response = await fetch('https://pltlcpqtivuvyeuywvql.supabase.co/functions/v1/agent-relay?action=ping', {
1313
+ method: 'POST',
1314
+ headers: { 'Content-Type': 'application/json' },
1315
+ body: '{}',
1316
+ });
1317
+ if (response.ok || response.status === 401) {
1318
+ console.log(colors.success(' ✓ Network connectivity'));
1319
+ }
1320
+ else {
1321
+ console.log(colors.error(' ✗ Cannot reach Higherup service'));
1322
+ issues++;
1323
+ }
1324
+ }
1325
+ catch {
1326
+ console.log(colors.error(' ✗ Network error'));
1327
+ issues++;
1328
+ }
1329
+ // Check screen capture tools
1330
+ if (os.platform() === 'linux') {
1331
+ try {
1332
+ await execAsync('which scrot');
1333
+ console.log(colors.success(' ✓ Screen capture tool (scrot)'));
1334
+ }
1335
+ catch {
1336
+ try {
1337
+ await execAsync('which import');
1338
+ console.log(colors.success(' ✓ Screen capture tool (imagemagick)'));
1339
+ }
1340
+ catch {
1341
+ console.log(colors.warning(' ⚠ No screen capture tool found (install scrot)'));
1342
+ }
1343
+ }
1344
+ }
1345
+ else {
1346
+ console.log(colors.success(' ✓ Screen capture available'));
1347
+ }
1348
+ // Summary
1349
+ console.log('');
1350
+ if (issues === 0) {
1351
+ console.log(colors.success(' All checks passed!\n'));
1352
+ }
1353
+ else {
1354
+ console.log(colors.warning(` ${issues} issue(s) found. See above for details.\n`));
1355
+ }
1356
+ });
1357
+ // Version with more info
1358
+ program
1359
+ .command('info')
1360
+ .description('Show version and system information')
1361
+ .action(() => {
1362
+ console.log(colors.bold(`\n Higherup Agent v${VERSION}\n`));
1363
+ console.log(` Node.js: ${process.version}`);
1364
+ console.log(` Platform: ${os.platform()} ${os.arch()}`);
1365
+ console.log(` Hostname: ${os.hostname()}`);
1366
+ console.log(` Config: ${CONFIG_FILE}`);
1367
+ console.log(` API URL: ${API_BASE_URL}`);
1368
+ console.log('');
1369
+ });
638
1370
  program.parse();
639
1371
  //# sourceMappingURL=agent.js.map