higherup 1.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -11
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +757 -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.0';
|
|
23
26
|
// Colors for terminal output
|
|
24
27
|
const colors = {
|
|
25
28
|
success: chalk.green,
|
|
@@ -30,7 +33,99 @@ const colors = {
|
|
|
30
33
|
file: chalk.blue,
|
|
31
34
|
dim: chalk.gray,
|
|
32
35
|
bold: chalk.bold,
|
|
36
|
+
highlight: chalk.bgCyan.black,
|
|
33
37
|
};
|
|
38
|
+
// Logging utility
|
|
39
|
+
class Logger {
|
|
40
|
+
logLevel;
|
|
41
|
+
logToFile;
|
|
42
|
+
constructor(level = 'info', logToFile = true) {
|
|
43
|
+
this.logLevel = level;
|
|
44
|
+
this.logToFile = logToFile;
|
|
45
|
+
}
|
|
46
|
+
levels = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
47
|
+
shouldLog(level) {
|
|
48
|
+
return this.levels[level] >= this.levels[this.logLevel];
|
|
49
|
+
}
|
|
50
|
+
async writeToFile(message) {
|
|
51
|
+
if (this.logToFile) {
|
|
52
|
+
try {
|
|
53
|
+
const timestamp = new Date().toISOString();
|
|
54
|
+
await fs.appendFile(LOG_FILE, `[${timestamp}] ${message}\n`);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Ignore file write errors
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
debug(message, ...args) {
|
|
62
|
+
if (this.shouldLog('debug')) {
|
|
63
|
+
console.log(colors.dim(`[DEBUG] ${message}`), ...args);
|
|
64
|
+
this.writeToFile(`DEBUG: ${message}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
info(message, ...args) {
|
|
68
|
+
if (this.shouldLog('info')) {
|
|
69
|
+
console.log(colors.info(message), ...args);
|
|
70
|
+
this.writeToFile(`INFO: ${message}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
warn(message, ...args) {
|
|
74
|
+
if (this.shouldLog('warn')) {
|
|
75
|
+
console.log(colors.warning(`⚠ ${message}`), ...args);
|
|
76
|
+
this.writeToFile(`WARN: ${message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
error(message, ...args) {
|
|
80
|
+
if (this.shouldLog('error')) {
|
|
81
|
+
console.error(colors.error(`✗ ${message}`), ...args);
|
|
82
|
+
this.writeToFile(`ERROR: ${message}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
success(message, ...args) {
|
|
86
|
+
console.log(colors.success(`✓ ${message}`), ...args);
|
|
87
|
+
this.writeToFile(`SUCCESS: ${message}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const logger = new Logger();
|
|
91
|
+
// Interactive prompts
|
|
92
|
+
async function prompt(question, defaultValue) {
|
|
93
|
+
const rl = readline.createInterface({
|
|
94
|
+
input: process.stdin,
|
|
95
|
+
output: process.stdout,
|
|
96
|
+
});
|
|
97
|
+
return new Promise((resolve) => {
|
|
98
|
+
const displayQuestion = defaultValue
|
|
99
|
+
? `${question} (${colors.dim(defaultValue)}): `
|
|
100
|
+
: `${question}: `;
|
|
101
|
+
rl.question(displayQuestion, (answer) => {
|
|
102
|
+
rl.close();
|
|
103
|
+
resolve(answer.trim() || defaultValue || '');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
async function confirm(question, defaultYes = true) {
|
|
108
|
+
const hint = defaultYes ? '[Y/n]' : '[y/N]';
|
|
109
|
+
const answer = await prompt(`${question} ${hint}`);
|
|
110
|
+
if (!answer)
|
|
111
|
+
return defaultYes;
|
|
112
|
+
return answer.toLowerCase().startsWith('y');
|
|
113
|
+
}
|
|
114
|
+
async function selectFromList(items, message) {
|
|
115
|
+
if (items.length === 0)
|
|
116
|
+
return null;
|
|
117
|
+
console.log(`\n${message}\n`);
|
|
118
|
+
items.forEach((item, index) => {
|
|
119
|
+
console.log(` ${colors.bold(`${index + 1}.`)} ${item.name} ${colors.dim(`(${item.id.slice(0, 8)}...)`)}`);
|
|
120
|
+
});
|
|
121
|
+
console.log('');
|
|
122
|
+
const answer = await prompt('Select number');
|
|
123
|
+
const index = parseInt(answer) - 1;
|
|
124
|
+
if (index >= 0 && index < items.length) {
|
|
125
|
+
return items[index];
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
34
129
|
class HigherupAgent {
|
|
35
130
|
workspacePath = '';
|
|
36
131
|
workspaceId = '';
|
|
@@ -40,14 +135,21 @@ class HigherupAgent {
|
|
|
40
135
|
isRunning = false;
|
|
41
136
|
commandCount = 0;
|
|
42
137
|
pollTimer = null;
|
|
138
|
+
heartbeatTimer = null;
|
|
43
139
|
autonomousMode = false;
|
|
140
|
+
reconnectAttempts = 0;
|
|
141
|
+
maxReconnectAttempts = 5;
|
|
142
|
+
startTime = new Date();
|
|
143
|
+
bytesTransferred = 0;
|
|
44
144
|
constructor() { }
|
|
45
|
-
async connect(workspacePath, workspaceId, apiToken, agentName, autonomous) {
|
|
145
|
+
async connect(workspacePath, workspaceId, apiToken, agentName, autonomous, autoReconnect) {
|
|
46
146
|
this.workspacePath = path.resolve(workspacePath);
|
|
47
147
|
this.workspaceId = workspaceId;
|
|
48
148
|
this.apiToken = apiToken;
|
|
49
149
|
this.agentName = agentName || `${os.hostname()}-${os.platform()}`;
|
|
50
150
|
this.autonomousMode = autonomous || false;
|
|
151
|
+
this.maxReconnectAttempts = autoReconnect ? 5 : 0;
|
|
152
|
+
this.startTime = new Date();
|
|
51
153
|
const spinner = ora('Connecting to Higherup service...').start();
|
|
52
154
|
try {
|
|
53
155
|
// Validate workspace path
|
|
@@ -62,18 +164,27 @@ class HigherupAgent {
|
|
|
62
164
|
agent_name: this.agentName,
|
|
63
165
|
capabilities: [
|
|
64
166
|
'command_execute',
|
|
167
|
+
'command_stream',
|
|
65
168
|
'file_read',
|
|
66
169
|
'file_write',
|
|
67
170
|
'file_list',
|
|
171
|
+
'file_watch',
|
|
68
172
|
'screen_capture',
|
|
173
|
+
'screen_stream',
|
|
69
174
|
'system_info',
|
|
175
|
+
'process_list',
|
|
176
|
+
'env_vars',
|
|
70
177
|
],
|
|
71
178
|
autonomous_mode: this.autonomousMode,
|
|
179
|
+
version: VERSION,
|
|
180
|
+
platform: os.platform(),
|
|
181
|
+
arch: os.arch(),
|
|
72
182
|
});
|
|
73
183
|
if (!response.success) {
|
|
74
184
|
throw new Error(response.error || 'Registration failed');
|
|
75
185
|
}
|
|
76
186
|
this.sessionId = response.session_id;
|
|
187
|
+
this.reconnectAttempts = 0;
|
|
77
188
|
spinner.succeed('Connected to Higherup service!');
|
|
78
189
|
this.printBanner();
|
|
79
190
|
this.isRunning = true;
|
|
@@ -84,23 +195,47 @@ class HigherupAgent {
|
|
|
84
195
|
// Handle shutdown
|
|
85
196
|
process.on('SIGINT', () => this.disconnect());
|
|
86
197
|
process.on('SIGTERM', () => this.disconnect());
|
|
198
|
+
process.on('uncaughtException', (err) => {
|
|
199
|
+
logger.error(`Uncaught exception: ${err.message}`);
|
|
200
|
+
this.disconnect();
|
|
201
|
+
});
|
|
87
202
|
}
|
|
88
203
|
catch (error) {
|
|
89
204
|
spinner.fail(`Failed to connect: ${error.message}`);
|
|
205
|
+
if (this.maxReconnectAttempts > 0 && this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
206
|
+
this.reconnectAttempts++;
|
|
207
|
+
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
|
|
208
|
+
logger.warn(`Reconnecting in ${delay / 1000}s (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
|
|
209
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
210
|
+
return this.connect(workspacePath, workspaceId, apiToken, agentName, autonomous, true);
|
|
211
|
+
}
|
|
90
212
|
throw error;
|
|
91
213
|
}
|
|
92
214
|
}
|
|
93
215
|
async apiCall(action, body) {
|
|
94
216
|
const url = `${API_BASE_URL}?action=${action}`;
|
|
95
|
-
|
|
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,6 +899,7 @@ class HigherupAgent {
|
|
|
477
899
|
});
|
|
478
900
|
const duration = Date.now() - startTime;
|
|
479
901
|
this.commandCount++;
|
|
902
|
+
this.bytesTransferred += result.output.length;
|
|
480
903
|
const status = result.exitCode === 0 ? colors.success('✓') : colors.error('✗');
|
|
481
904
|
console.log(colors.dim(`\n[Completed in ${duration}ms] ${status} Exit code: ${result.exitCode}\n`));
|
|
482
905
|
await this.reportResult(cmd.id, result.output.slice(-10000), result.exitCode, duration);
|
|
@@ -500,21 +923,24 @@ class HigherupAgent {
|
|
|
500
923
|
if (this.pollTimer) {
|
|
501
924
|
clearInterval(this.pollTimer);
|
|
502
925
|
}
|
|
926
|
+
if (this.heartbeatTimer) {
|
|
927
|
+
clearInterval(this.heartbeatTimer);
|
|
928
|
+
}
|
|
503
929
|
try {
|
|
504
930
|
await this.apiCall('disconnect', {
|
|
505
931
|
session_id: this.sessionId,
|
|
506
932
|
workspace_id: this.workspaceId,
|
|
507
933
|
});
|
|
508
934
|
}
|
|
509
|
-
catch
|
|
935
|
+
catch {
|
|
510
936
|
// Ignore disconnect errors
|
|
511
937
|
}
|
|
512
938
|
console.log(colors.success('Disconnected. Goodbye!\n'));
|
|
513
|
-
|
|
939
|
+
this.printStats();
|
|
514
940
|
process.exit(0);
|
|
515
941
|
}
|
|
516
942
|
}
|
|
517
|
-
//
|
|
943
|
+
// Helper functions
|
|
518
944
|
async function loadConfig() {
|
|
519
945
|
try {
|
|
520
946
|
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
@@ -529,73 +955,237 @@ async function saveConfig(config) {
|
|
|
529
955
|
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
530
956
|
await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
531
957
|
}
|
|
958
|
+
async function fetchWorkspaces(apiToken) {
|
|
959
|
+
try {
|
|
960
|
+
const response = await fetch(`${API_BASE_URL}?action=status`, {
|
|
961
|
+
method: 'POST',
|
|
962
|
+
headers: {
|
|
963
|
+
'Content-Type': 'application/json',
|
|
964
|
+
'x-api-token': apiToken,
|
|
965
|
+
},
|
|
966
|
+
body: JSON.stringify({}),
|
|
967
|
+
});
|
|
968
|
+
if (!response.ok) {
|
|
969
|
+
throw new Error('Failed to fetch workspaces');
|
|
970
|
+
}
|
|
971
|
+
const data = await response.json();
|
|
972
|
+
return data.workspaces || [];
|
|
973
|
+
}
|
|
974
|
+
catch {
|
|
975
|
+
return [];
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
// CLI Setup
|
|
532
979
|
const program = new Command();
|
|
533
980
|
program
|
|
534
981
|
.name('higherup')
|
|
535
|
-
.description('Higherup Local Agent - Give AI agents full access to your development environment')
|
|
536
|
-
.version(
|
|
982
|
+
.description('Higherup Local Agent v' + VERSION + ' - Give AI agents full access to your development environment')
|
|
983
|
+
.version(VERSION);
|
|
984
|
+
// Main connect command
|
|
537
985
|
program
|
|
538
986
|
.command('connect')
|
|
539
987
|
.description('Connect to Higherup service')
|
|
540
|
-
.
|
|
541
|
-
.
|
|
988
|
+
.option('-w, --workspace <id>', 'Workspace ID to connect')
|
|
989
|
+
.option('-p, --path <path>', 'Local workspace path')
|
|
542
990
|
.option('-t, --token <token>', 'API token (or use HIGHERUP_API_TOKEN env var)')
|
|
543
991
|
.option('-n, --name <name>', 'Agent name for identification')
|
|
544
992
|
.option('--autonomous', 'Enable autonomous mode (unrestricted access)')
|
|
993
|
+
.option('--auto-reconnect', 'Automatically reconnect on connection loss')
|
|
545
994
|
.action(async (options) => {
|
|
546
995
|
const config = await loadConfig();
|
|
547
|
-
|
|
996
|
+
let apiToken = options.token || process.env.HIGHERUP_API_TOKEN || config.apiToken;
|
|
997
|
+
let workspaceId = options.workspace || config.defaultWorkspace;
|
|
998
|
+
let workspacePath = options.path || config.defaultPath || '.';
|
|
999
|
+
// Interactive setup if missing required options
|
|
548
1000
|
if (!apiToken) {
|
|
549
|
-
console.
|
|
550
|
-
console.log(colors.
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
1001
|
+
console.log(colors.bold('\n ═══ Higherup Setup ═══\n'));
|
|
1002
|
+
console.log(colors.dim(' Get your API token from: https://higherup.ai/dashboard/api\n'));
|
|
1003
|
+
apiToken = await prompt(' Enter your API token');
|
|
1004
|
+
if (!apiToken) {
|
|
1005
|
+
console.error(colors.error('\n Error: API token is required'));
|
|
1006
|
+
process.exit(1);
|
|
1007
|
+
}
|
|
1008
|
+
if (await confirm(' Save token for future use?')) {
|
|
1009
|
+
config.apiToken = apiToken;
|
|
1010
|
+
await saveConfig(config);
|
|
1011
|
+
console.log(colors.success(' Token saved to ~/.higherup/config.json'));
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
// Fetch and select workspace if not provided
|
|
1015
|
+
if (!workspaceId) {
|
|
1016
|
+
const spinner = ora(' Fetching your workspaces...').start();
|
|
1017
|
+
const workspaces = await fetchWorkspaces(apiToken);
|
|
1018
|
+
spinner.stop();
|
|
1019
|
+
if (workspaces.length > 0) {
|
|
1020
|
+
const selected = await selectFromList(workspaces, 'Select a workspace:');
|
|
1021
|
+
if (selected) {
|
|
1022
|
+
workspaceId = selected.id;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
if (!workspaceId) {
|
|
1026
|
+
console.log(colors.dim('\n No workspaces found. Create one at https://higherup.ai/dashboard/workspaces'));
|
|
1027
|
+
workspaceId = await prompt(' Or enter workspace ID manually');
|
|
1028
|
+
}
|
|
1029
|
+
if (!workspaceId) {
|
|
1030
|
+
console.error(colors.error('\n Error: Workspace ID is required'));
|
|
1031
|
+
process.exit(1);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
// Get workspace path
|
|
1035
|
+
if (workspacePath === '.') {
|
|
1036
|
+
const suggested = process.cwd();
|
|
1037
|
+
console.log('');
|
|
1038
|
+
workspacePath = await prompt(` Workspace path`, suggested);
|
|
555
1039
|
}
|
|
556
1040
|
const agent = new HigherupAgent();
|
|
557
1041
|
try {
|
|
558
|
-
await agent.connect(
|
|
1042
|
+
await agent.connect(workspacePath, workspaceId, apiToken, options.name || config.agentName, options.autonomous, options.autoReconnect);
|
|
559
1043
|
}
|
|
560
1044
|
catch (error) {
|
|
561
1045
|
console.error(colors.error(`\nFailed to connect: ${error.message}`));
|
|
562
1046
|
process.exit(1);
|
|
563
1047
|
}
|
|
564
1048
|
});
|
|
1049
|
+
// Quick connect with just token
|
|
1050
|
+
program
|
|
1051
|
+
.command('quick')
|
|
1052
|
+
.description('Quick connect with interactive workspace selection')
|
|
1053
|
+
.action(async () => {
|
|
1054
|
+
const config = await loadConfig();
|
|
1055
|
+
const apiToken = process.env.HIGHERUP_API_TOKEN || config.apiToken;
|
|
1056
|
+
if (!apiToken) {
|
|
1057
|
+
console.error(colors.error('Error: No API token configured'));
|
|
1058
|
+
console.log(colors.info('Run: higherup config --token <your-token>'));
|
|
1059
|
+
process.exit(1);
|
|
1060
|
+
}
|
|
1061
|
+
const spinner = ora('Fetching workspaces...').start();
|
|
1062
|
+
const workspaces = await fetchWorkspaces(apiToken);
|
|
1063
|
+
spinner.stop();
|
|
1064
|
+
if (workspaces.length === 0) {
|
|
1065
|
+
console.log(colors.warning('No workspaces found.'));
|
|
1066
|
+
console.log(colors.info('Create one at https://higherup.ai/dashboard/workspaces'));
|
|
1067
|
+
process.exit(1);
|
|
1068
|
+
}
|
|
1069
|
+
const selected = await selectFromList(workspaces, 'Select a workspace:');
|
|
1070
|
+
if (!selected) {
|
|
1071
|
+
console.log(colors.warning('No workspace selected.'));
|
|
1072
|
+
process.exit(1);
|
|
1073
|
+
}
|
|
1074
|
+
const agent = new HigherupAgent();
|
|
1075
|
+
await agent.connect(process.cwd(), selected.id, apiToken);
|
|
1076
|
+
});
|
|
1077
|
+
// Init command for first-time setup
|
|
1078
|
+
program
|
|
1079
|
+
.command('init')
|
|
1080
|
+
.description('Initialize Higherup in current directory')
|
|
1081
|
+
.action(async () => {
|
|
1082
|
+
console.log(colors.bold('\n ═══ Higherup Initialization ═══\n'));
|
|
1083
|
+
const config = await loadConfig();
|
|
1084
|
+
// Step 1: API Token
|
|
1085
|
+
if (!config.apiToken) {
|
|
1086
|
+
console.log(colors.info(' Step 1: Configure API Token'));
|
|
1087
|
+
console.log(colors.dim(' Get your token from: https://higherup.ai/dashboard/api\n'));
|
|
1088
|
+
const token = await prompt(' API Token');
|
|
1089
|
+
if (token) {
|
|
1090
|
+
config.apiToken = token;
|
|
1091
|
+
console.log(colors.success(' ✓ Token saved\n'));
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
else {
|
|
1095
|
+
console.log(colors.success(' ✓ API Token already configured\n'));
|
|
1096
|
+
}
|
|
1097
|
+
// Step 2: Default workspace
|
|
1098
|
+
console.log(colors.info(' Step 2: Set Default Workspace'));
|
|
1099
|
+
if (config.apiToken) {
|
|
1100
|
+
const spinner = ora(' Fetching workspaces...').start();
|
|
1101
|
+
const workspaces = await fetchWorkspaces(config.apiToken);
|
|
1102
|
+
spinner.stop();
|
|
1103
|
+
if (workspaces.length > 0) {
|
|
1104
|
+
const selected = await selectFromList(workspaces, ' Select default workspace:');
|
|
1105
|
+
if (selected) {
|
|
1106
|
+
config.defaultWorkspace = selected.id;
|
|
1107
|
+
console.log(colors.success(`\n ✓ Default workspace: ${selected.name}`));
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
else {
|
|
1111
|
+
const wsId = await prompt(' Workspace ID (or create one at dashboard)');
|
|
1112
|
+
if (wsId)
|
|
1113
|
+
config.defaultWorkspace = wsId;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
// Step 3: Default path
|
|
1117
|
+
console.log(colors.info('\n Step 3: Set Default Path'));
|
|
1118
|
+
const defaultPath = await prompt(' Default workspace path', process.cwd());
|
|
1119
|
+
config.defaultPath = defaultPath;
|
|
1120
|
+
// Step 4: Agent name
|
|
1121
|
+
console.log(colors.info('\n Step 4: Set Agent Name'));
|
|
1122
|
+
const agentName = await prompt(' Agent name', `${os.hostname()}-${os.platform()}`);
|
|
1123
|
+
config.agentName = agentName;
|
|
1124
|
+
await saveConfig(config);
|
|
1125
|
+
console.log(colors.success('\n ═══ Setup Complete! ═══\n'));
|
|
1126
|
+
console.log(' Run ' + colors.bold('higherup connect') + ' to start the agent\n');
|
|
1127
|
+
});
|
|
1128
|
+
// Config command
|
|
565
1129
|
program
|
|
566
1130
|
.command('config')
|
|
567
1131
|
.description('Configure default settings')
|
|
568
1132
|
.option('-t, --token <token>', 'Set default API token')
|
|
569
1133
|
.option('-w, --workspace <id>', 'Set default workspace')
|
|
1134
|
+
.option('-p, --path <path>', 'Set default workspace path')
|
|
1135
|
+
.option('-n, --name <name>', 'Set default agent name')
|
|
570
1136
|
.option('--show', 'Show current configuration')
|
|
1137
|
+
.option('--reset', 'Reset all configuration')
|
|
571
1138
|
.action(async (options) => {
|
|
572
1139
|
const config = await loadConfig();
|
|
1140
|
+
if (options.reset) {
|
|
1141
|
+
await saveConfig({});
|
|
1142
|
+
console.log(colors.success('✓ Configuration reset'));
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
573
1145
|
if (options.show) {
|
|
574
1146
|
console.log(colors.bold('\nHigherup Configuration:'));
|
|
575
1147
|
console.log(colors.dim(`Config file: ${CONFIG_FILE}\n`));
|
|
576
|
-
console.log(` API Token:
|
|
1148
|
+
console.log(` API Token: ${config.apiToken ? colors.success('Set (' + config.apiToken.slice(0, 8) + '...)') : colors.warning('Not set')}`);
|
|
577
1149
|
console.log(` Default Workspace: ${config.defaultWorkspace || colors.dim('Not set')}`);
|
|
1150
|
+
console.log(` Default Path: ${config.defaultPath || colors.dim('Not set')}`);
|
|
1151
|
+
console.log(` Agent Name: ${config.agentName || colors.dim('Not set')}`);
|
|
1152
|
+
console.log(` Log Level: ${config.logLevel || colors.dim('info')}`);
|
|
1153
|
+
console.log(` Auto Reconnect: ${config.autoReconnect ? colors.success('Enabled') : colors.dim('Disabled')}`);
|
|
578
1154
|
console.log('');
|
|
579
1155
|
return;
|
|
580
1156
|
}
|
|
1157
|
+
let updated = false;
|
|
581
1158
|
if (options.token) {
|
|
582
1159
|
config.apiToken = options.token;
|
|
583
1160
|
console.log(colors.success('✓ API token saved'));
|
|
1161
|
+
updated = true;
|
|
584
1162
|
}
|
|
585
1163
|
if (options.workspace) {
|
|
586
1164
|
config.defaultWorkspace = options.workspace;
|
|
587
1165
|
console.log(colors.success('✓ Default workspace saved'));
|
|
1166
|
+
updated = true;
|
|
1167
|
+
}
|
|
1168
|
+
if (options.path) {
|
|
1169
|
+
config.defaultPath = options.path;
|
|
1170
|
+
console.log(colors.success('✓ Default path saved'));
|
|
1171
|
+
updated = true;
|
|
1172
|
+
}
|
|
1173
|
+
if (options.name) {
|
|
1174
|
+
config.agentName = options.name;
|
|
1175
|
+
console.log(colors.success('✓ Agent name saved'));
|
|
1176
|
+
updated = true;
|
|
588
1177
|
}
|
|
589
|
-
if (
|
|
1178
|
+
if (updated) {
|
|
590
1179
|
await saveConfig(config);
|
|
591
1180
|
}
|
|
592
1181
|
else {
|
|
593
1182
|
console.log('Use --help to see available options');
|
|
594
1183
|
}
|
|
595
1184
|
});
|
|
1185
|
+
// Status command
|
|
596
1186
|
program
|
|
597
1187
|
.command('status')
|
|
598
|
-
.description('Check connection status')
|
|
1188
|
+
.description('Check connection status and active sessions')
|
|
599
1189
|
.option('-t, --token <token>', 'API token')
|
|
600
1190
|
.action(async (options) => {
|
|
601
1191
|
const config = await loadConfig();
|
|
@@ -617,17 +1207,20 @@ program
|
|
|
617
1207
|
const data = await response.json();
|
|
618
1208
|
spinner.stop();
|
|
619
1209
|
if (data.active_sessions && data.active_sessions.length > 0) {
|
|
620
|
-
console.log(colors.success('\n✓
|
|
1210
|
+
console.log(colors.success('\n✓ Active Sessions:\n'));
|
|
621
1211
|
for (const session of data.active_sessions) {
|
|
622
1212
|
console.log(` ${colors.bold(session.agent_name || 'Agent')}`);
|
|
623
1213
|
console.log(colors.dim(` Workspace: ${session.workspaces?.name || session.workspace_id}`));
|
|
624
1214
|
console.log(colors.dim(` Connected: ${new Date(session.connected_at).toLocaleString()}`));
|
|
1215
|
+
if (session.last_activity) {
|
|
1216
|
+
console.log(colors.dim(` Last Activity: ${new Date(session.last_activity).toLocaleString()}`));
|
|
1217
|
+
}
|
|
625
1218
|
console.log('');
|
|
626
1219
|
}
|
|
627
1220
|
}
|
|
628
1221
|
else {
|
|
629
1222
|
console.log(colors.warning('\n⚠ No active sessions'));
|
|
630
|
-
console.log(colors.dim('Run: higherup connect -w <workspace-id
|
|
1223
|
+
console.log(colors.dim('Run: higherup connect -w <workspace-id>\n'));
|
|
631
1224
|
}
|
|
632
1225
|
}
|
|
633
1226
|
catch (error) {
|
|
@@ -635,5 +1228,109 @@ program
|
|
|
635
1228
|
process.exit(1);
|
|
636
1229
|
}
|
|
637
1230
|
});
|
|
1231
|
+
// Logs command
|
|
1232
|
+
program
|
|
1233
|
+
.command('logs')
|
|
1234
|
+
.description('View agent logs')
|
|
1235
|
+
.option('-n, --lines <number>', 'Number of lines to show', '50')
|
|
1236
|
+
.option('-f, --follow', 'Follow log output')
|
|
1237
|
+
.action(async (options) => {
|
|
1238
|
+
try {
|
|
1239
|
+
const content = await fs.readFile(LOG_FILE, 'utf8');
|
|
1240
|
+
const lines = content.split('\n');
|
|
1241
|
+
const numLines = parseInt(options.lines);
|
|
1242
|
+
console.log(colors.bold(`\nLast ${numLines} log entries:\n`));
|
|
1243
|
+
console.log(lines.slice(-numLines).join('\n'));
|
|
1244
|
+
}
|
|
1245
|
+
catch {
|
|
1246
|
+
console.log(colors.warning('No logs found.'));
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
// Doctor command to diagnose issues
|
|
1250
|
+
program
|
|
1251
|
+
.command('doctor')
|
|
1252
|
+
.description('Diagnose common issues')
|
|
1253
|
+
.action(async () => {
|
|
1254
|
+
console.log(colors.bold('\n Higherup Doctor - Diagnosing issues...\n'));
|
|
1255
|
+
let issues = 0;
|
|
1256
|
+
// Check Node version
|
|
1257
|
+
const nodeVersion = process.version;
|
|
1258
|
+
const majorVersion = parseInt(nodeVersion.slice(1).split('.')[0]);
|
|
1259
|
+
if (majorVersion >= 18) {
|
|
1260
|
+
console.log(colors.success(` ✓ Node.js ${nodeVersion}`));
|
|
1261
|
+
}
|
|
1262
|
+
else {
|
|
1263
|
+
console.log(colors.error(` ✗ Node.js ${nodeVersion} (requires v18+)`));
|
|
1264
|
+
issues++;
|
|
1265
|
+
}
|
|
1266
|
+
// Check config
|
|
1267
|
+
const config = await loadConfig();
|
|
1268
|
+
if (config.apiToken) {
|
|
1269
|
+
console.log(colors.success(' ✓ API token configured'));
|
|
1270
|
+
}
|
|
1271
|
+
else {
|
|
1272
|
+
console.log(colors.warning(' ⚠ API token not configured'));
|
|
1273
|
+
issues++;
|
|
1274
|
+
}
|
|
1275
|
+
// Check network connectivity
|
|
1276
|
+
try {
|
|
1277
|
+
const response = await fetch('https://pltlcpqtivuvyeuywvql.supabase.co/functions/v1/agent-relay?action=ping', {
|
|
1278
|
+
method: 'POST',
|
|
1279
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1280
|
+
body: '{}',
|
|
1281
|
+
});
|
|
1282
|
+
if (response.ok || response.status === 401) {
|
|
1283
|
+
console.log(colors.success(' ✓ Network connectivity'));
|
|
1284
|
+
}
|
|
1285
|
+
else {
|
|
1286
|
+
console.log(colors.error(' ✗ Cannot reach Higherup service'));
|
|
1287
|
+
issues++;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
catch {
|
|
1291
|
+
console.log(colors.error(' ✗ Network error'));
|
|
1292
|
+
issues++;
|
|
1293
|
+
}
|
|
1294
|
+
// Check screen capture tools
|
|
1295
|
+
if (os.platform() === 'linux') {
|
|
1296
|
+
try {
|
|
1297
|
+
await execAsync('which scrot');
|
|
1298
|
+
console.log(colors.success(' ✓ Screen capture tool (scrot)'));
|
|
1299
|
+
}
|
|
1300
|
+
catch {
|
|
1301
|
+
try {
|
|
1302
|
+
await execAsync('which import');
|
|
1303
|
+
console.log(colors.success(' ✓ Screen capture tool (imagemagick)'));
|
|
1304
|
+
}
|
|
1305
|
+
catch {
|
|
1306
|
+
console.log(colors.warning(' ⚠ No screen capture tool found (install scrot)'));
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
else {
|
|
1311
|
+
console.log(colors.success(' ✓ Screen capture available'));
|
|
1312
|
+
}
|
|
1313
|
+
// Summary
|
|
1314
|
+
console.log('');
|
|
1315
|
+
if (issues === 0) {
|
|
1316
|
+
console.log(colors.success(' All checks passed!\n'));
|
|
1317
|
+
}
|
|
1318
|
+
else {
|
|
1319
|
+
console.log(colors.warning(` ${issues} issue(s) found. See above for details.\n`));
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1322
|
+
// Version with more info
|
|
1323
|
+
program
|
|
1324
|
+
.command('info')
|
|
1325
|
+
.description('Show version and system information')
|
|
1326
|
+
.action(() => {
|
|
1327
|
+
console.log(colors.bold(`\n Higherup Agent v${VERSION}\n`));
|
|
1328
|
+
console.log(` Node.js: ${process.version}`);
|
|
1329
|
+
console.log(` Platform: ${os.platform()} ${os.arch()}`);
|
|
1330
|
+
console.log(` Hostname: ${os.hostname()}`);
|
|
1331
|
+
console.log(` Config: ${CONFIG_FILE}`);
|
|
1332
|
+
console.log(` API URL: ${API_BASE_URL}`);
|
|
1333
|
+
console.log('');
|
|
1334
|
+
});
|
|
638
1335
|
program.parse();
|
|
639
1336
|
//# sourceMappingURL=agent.js.map
|