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/README.md +380 -99
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +792 -60
- package/dist/agent.js.map +1 -1
- package/package.json +2 -3
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
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
|
134
|
-
|
|
305
|
+
catch {
|
|
306
|
+
logger.debug('Heartbeat failed');
|
|
135
307
|
}
|
|
136
|
-
}, 30000);
|
|
308
|
+
}, 30000);
|
|
137
309
|
}
|
|
138
310
|
printBanner() {
|
|
139
311
|
console.log('\n');
|
|
140
|
-
console.log(colors.bold('
|
|
141
|
-
console.log(colors.bold(' ║') + colors.info('
|
|
142
|
-
console.log(colors.bold('
|
|
143
|
-
console.log('');
|
|
144
|
-
console.log(colors.
|
|
145
|
-
console.log(colors.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
360
|
-
await fs.unlink(
|
|
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 = [
|
|
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
|
|
970
|
+
catch {
|
|
510
971
|
// Ignore disconnect errors
|
|
511
972
|
}
|
|
512
973
|
console.log(colors.success('Disconnected. Goodbye!\n'));
|
|
513
|
-
|
|
974
|
+
this.printStats();
|
|
514
975
|
process.exit(0);
|
|
515
976
|
}
|
|
516
977
|
}
|
|
517
|
-
//
|
|
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(
|
|
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
|
-
.
|
|
541
|
-
.
|
|
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
|
-
|
|
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.
|
|
550
|
-
console.log(colors.
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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(
|
|
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:
|
|
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.
|
|
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✓
|
|
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
|
|
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
|