hedgequantx 2.7.22 → 2.7.23
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/package.json +1 -1
- package/src/pages/ai-agents-ui.js +28 -2
- package/src/pages/ai-agents.js +123 -12
- package/src/services/cliproxy.js +71 -15
package/package.json
CHANGED
|
@@ -60,14 +60,40 @@ const draw2ColTable = (title, titleColor, items, backText, W) => {
|
|
|
60
60
|
* @param {Array} providers - List of AI providers
|
|
61
61
|
* @param {Object} config - Current config
|
|
62
62
|
* @param {number} boxWidth - Box width
|
|
63
|
+
* @param {string} cliproxyUrl - Current CLIProxy URL (optional)
|
|
63
64
|
*/
|
|
64
|
-
const drawProvidersTable = (providers, config, boxWidth) => {
|
|
65
|
+
const drawProvidersTable = (providers, config, boxWidth, cliproxyUrl = null) => {
|
|
65
66
|
const W = boxWidth - 2;
|
|
67
|
+
|
|
68
|
+
console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
|
|
69
|
+
console.log(chalk.cyan('║') + chalk.yellow.bold(centerText('AI AGENTS CONFIGURATION', W)) + chalk.cyan('║'));
|
|
70
|
+
|
|
71
|
+
// Show CLIProxy URL if provided
|
|
72
|
+
if (cliproxyUrl) {
|
|
73
|
+
console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
|
|
74
|
+
const proxyText = chalk.gray('CLIProxy: ') + chalk.cyan(cliproxyUrl);
|
|
75
|
+
console.log(chalk.cyan('║') + centerText(proxyText, W) + chalk.cyan('║'));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
79
|
+
|
|
66
80
|
const items = providers.map((p, i) => {
|
|
67
81
|
const status = config.providers[p.id]?.active ? chalk.green(' ●') : '';
|
|
68
82
|
return chalk.cyan(`[${i + 1}]`) + ' ' + chalk[p.color](p.name) + status;
|
|
69
83
|
});
|
|
70
|
-
|
|
84
|
+
|
|
85
|
+
const rows = Math.ceil(items.length / 2);
|
|
86
|
+
for (let row = 0; row < rows; row++) {
|
|
87
|
+
const left = items[row];
|
|
88
|
+
const right = items[row + rows];
|
|
89
|
+
draw2ColRow(left || '', right || '', W);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
|
|
93
|
+
console.log(chalk.cyan('║') + chalk.gray(centerText('[C] Configure CLIProxy URL', W)) + chalk.cyan('║'));
|
|
94
|
+
console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
|
|
95
|
+
console.log(chalk.cyan('║') + chalk.red(centerText('[B] Back to Menu', W)) + chalk.cyan('║'));
|
|
96
|
+
console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
|
|
71
97
|
};
|
|
72
98
|
|
|
73
99
|
/**
|
package/src/pages/ai-agents.js
CHANGED
|
@@ -15,7 +15,7 @@ const { getLogoWidth } = require('../ui');
|
|
|
15
15
|
const { prompts } = require('../utils');
|
|
16
16
|
const { fetchModelsFromApi } = require('./ai-models');
|
|
17
17
|
const { drawProvidersTable, drawModelsTable, drawProviderWindow } = require('./ai-agents-ui');
|
|
18
|
-
const { isCliProxyRunning, fetchModelsFromCliProxy, getOAuthUrl, checkOAuthStatus } = require('../services/cliproxy');
|
|
18
|
+
const { isCliProxyRunning, fetchModelsFromCliProxy, getOAuthUrl, checkOAuthStatus, getCliProxyUrl, setCliProxyUrl, DEFAULT_CLIPROXY_URL } = require('../services/cliproxy');
|
|
19
19
|
|
|
20
20
|
// Config file path
|
|
21
21
|
const CONFIG_DIR = path.join(os.homedir(), '.hqx');
|
|
@@ -101,18 +101,63 @@ const activateProvider = (config, providerId, data) => {
|
|
|
101
101
|
/** Handle CLIProxy connection */
|
|
102
102
|
const handleCliProxyConnection = async (provider, config, boxWidth) => {
|
|
103
103
|
console.log();
|
|
104
|
-
const
|
|
105
|
-
const
|
|
104
|
+
const currentUrl = getCliProxyUrl();
|
|
105
|
+
const spinner = ora({ text: `Checking CLIProxy at ${currentUrl}...`, color: 'yellow' }).start();
|
|
106
|
+
let proxyStatus = await isCliProxyRunning();
|
|
106
107
|
|
|
107
108
|
if (!proxyStatus.running) {
|
|
108
|
-
spinner.fail(
|
|
109
|
-
console.log(
|
|
110
|
-
console.log(chalk.
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
spinner.fail(`CLIProxy not reachable at ${currentUrl}`);
|
|
110
|
+
console.log();
|
|
111
|
+
console.log(chalk.yellow(' CLIProxy Options:'));
|
|
112
|
+
console.log(chalk.gray(' [1] Local - localhost:8317 (default)'));
|
|
113
|
+
console.log(chalk.gray(' [2] Remote - Enter custom URL (e.g., http://your-pc-ip:8317)'));
|
|
114
|
+
console.log(chalk.gray(' [B] Back'));
|
|
115
|
+
console.log();
|
|
116
|
+
|
|
117
|
+
const urlChoice = await prompts.textInput(chalk.cyan(' Select option: '));
|
|
118
|
+
|
|
119
|
+
if (!urlChoice || urlChoice.toLowerCase() === 'b') {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let newUrl = null;
|
|
124
|
+
if (urlChoice === '1') {
|
|
125
|
+
newUrl = DEFAULT_CLIPROXY_URL;
|
|
126
|
+
} else if (urlChoice === '2') {
|
|
127
|
+
console.log(chalk.gray('\n Enter CLIProxy URL (e.g., http://192.168.1.100:8317):'));
|
|
128
|
+
const customUrl = await prompts.textInput(chalk.cyan(' URL: '));
|
|
129
|
+
if (!customUrl || customUrl.trim() === '') {
|
|
130
|
+
console.log(chalk.gray(' Cancelled.'));
|
|
131
|
+
await prompts.waitForEnter();
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
newUrl = customUrl.trim();
|
|
135
|
+
// Add http:// if missing
|
|
136
|
+
if (!newUrl.startsWith('http://') && !newUrl.startsWith('https://')) {
|
|
137
|
+
newUrl = 'http://' + newUrl;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (newUrl) {
|
|
142
|
+
const testSpinner = ora({ text: `Testing connection to ${newUrl}...`, color: 'yellow' }).start();
|
|
143
|
+
proxyStatus = await isCliProxyRunning(newUrl);
|
|
144
|
+
|
|
145
|
+
if (!proxyStatus.running) {
|
|
146
|
+
testSpinner.fail(`Cannot connect to ${newUrl}`);
|
|
147
|
+
console.log(chalk.gray(` Error: ${proxyStatus.error || 'Connection failed'}`));
|
|
148
|
+
console.log(chalk.yellow('\n Make sure CLIProxy is running and accessible.'));
|
|
149
|
+
await prompts.waitForEnter();
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
testSpinner.succeed(`Connected to CLIProxy at ${newUrl}`);
|
|
154
|
+
setCliProxyUrl(newUrl);
|
|
155
|
+
} else {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
spinner.succeed(`CLIProxy connected at ${currentUrl}`);
|
|
113
160
|
}
|
|
114
|
-
|
|
115
|
-
spinner.succeed('CLIProxy is running');
|
|
116
161
|
const oauthResult = await getOAuthUrl(provider.id);
|
|
117
162
|
|
|
118
163
|
if (!oauthResult.success) {
|
|
@@ -303,6 +348,66 @@ const getActiveProvider = () => {
|
|
|
303
348
|
/** Count active AI agents */
|
|
304
349
|
const getActiveAgentCount = () => getActiveProvider() ? 1 : 0;
|
|
305
350
|
|
|
351
|
+
/** Configure CLIProxy URL */
|
|
352
|
+
const configureCliProxyUrl = async () => {
|
|
353
|
+
const currentUrl = getCliProxyUrl();
|
|
354
|
+
console.clear();
|
|
355
|
+
console.log(chalk.yellow('\n Configure CLIProxy URL\n'));
|
|
356
|
+
console.log(chalk.gray(` Current: ${currentUrl}`));
|
|
357
|
+
console.log();
|
|
358
|
+
console.log(chalk.white(' [1] Local - localhost:8317 (default)'));
|
|
359
|
+
console.log(chalk.white(' [2] Remote - Enter custom URL'));
|
|
360
|
+
console.log(chalk.white(' [B] Back'));
|
|
361
|
+
console.log();
|
|
362
|
+
|
|
363
|
+
const choice = await prompts.textInput(chalk.cyan(' Select option: '));
|
|
364
|
+
|
|
365
|
+
if (!choice || choice.toLowerCase() === 'b') return;
|
|
366
|
+
|
|
367
|
+
if (choice === '1') {
|
|
368
|
+
setCliProxyUrl(DEFAULT_CLIPROXY_URL);
|
|
369
|
+
console.log(chalk.green(`\n ✓ CLIProxy URL set to ${DEFAULT_CLIPROXY_URL}`));
|
|
370
|
+
await prompts.waitForEnter();
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (choice === '2') {
|
|
375
|
+
console.log(chalk.gray('\n Enter CLIProxy URL (e.g., http://192.168.1.100:8317):'));
|
|
376
|
+
const customUrl = await prompts.textInput(chalk.cyan(' URL: '));
|
|
377
|
+
|
|
378
|
+
if (!customUrl || customUrl.trim() === '') {
|
|
379
|
+
console.log(chalk.gray(' Cancelled.'));
|
|
380
|
+
await prompts.waitForEnter();
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
let newUrl = customUrl.trim();
|
|
385
|
+
if (!newUrl.startsWith('http://') && !newUrl.startsWith('https://')) {
|
|
386
|
+
newUrl = 'http://' + newUrl;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Test connection
|
|
390
|
+
const spinner = ora({ text: `Testing connection to ${newUrl}...`, color: 'yellow' }).start();
|
|
391
|
+
const status = await isCliProxyRunning(newUrl);
|
|
392
|
+
|
|
393
|
+
if (status.running) {
|
|
394
|
+
spinner.succeed(`Connected to ${newUrl}`);
|
|
395
|
+
setCliProxyUrl(newUrl);
|
|
396
|
+
console.log(chalk.green(`\n ✓ CLIProxy URL saved.`));
|
|
397
|
+
} else {
|
|
398
|
+
spinner.warn(`Cannot connect to ${newUrl}`);
|
|
399
|
+
console.log(chalk.gray(` Error: ${status.error || 'Connection failed'}`));
|
|
400
|
+
console.log(chalk.yellow('\n Save anyway? (URL will be used when CLIProxy is available)'));
|
|
401
|
+
const save = await prompts.textInput(chalk.cyan(' Save? (y/N): '));
|
|
402
|
+
if (save && save.toLowerCase() === 'y') {
|
|
403
|
+
setCliProxyUrl(newUrl);
|
|
404
|
+
console.log(chalk.green(' ✓ URL saved.'));
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
await prompts.waitForEnter();
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
|
|
306
411
|
/** Main AI Agents menu */
|
|
307
412
|
const aiAgentsMenu = async () => {
|
|
308
413
|
let config = loadConfig();
|
|
@@ -310,13 +415,19 @@ const aiAgentsMenu = async () => {
|
|
|
310
415
|
|
|
311
416
|
while (true) {
|
|
312
417
|
console.clear();
|
|
313
|
-
|
|
418
|
+
const cliproxyUrl = getCliProxyUrl();
|
|
419
|
+
drawProvidersTable(AI_PROVIDERS, config, boxWidth, cliproxyUrl);
|
|
314
420
|
|
|
315
|
-
const input = await prompts.textInput(chalk.cyan('Select
|
|
421
|
+
const input = await prompts.textInput(chalk.cyan('Select (1-8/C/B): '));
|
|
316
422
|
const choice = (input || '').toLowerCase().trim();
|
|
317
423
|
|
|
318
424
|
if (choice === 'b' || choice === '') break;
|
|
319
425
|
|
|
426
|
+
if (choice === 'c') {
|
|
427
|
+
await configureCliProxyUrl();
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
|
|
320
431
|
const num = parseInt(choice);
|
|
321
432
|
if (!isNaN(num) && num >= 1 && num <= AI_PROVIDERS.length) {
|
|
322
433
|
config = await handleProviderConfig(AI_PROVIDERS[num - 1], config);
|
package/src/services/cliproxy.js
CHANGED
|
@@ -1,16 +1,61 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CLIProxy Service
|
|
3
3
|
*
|
|
4
|
-
* Connects to CLIProxyAPI
|
|
4
|
+
* Connects to CLIProxyAPI for AI provider access
|
|
5
5
|
* via paid plans (Claude Pro, ChatGPT Plus, etc.)
|
|
6
6
|
*
|
|
7
|
+
* Supports both local (localhost:8317) and remote connections.
|
|
7
8
|
* Docs: https://help.router-for.me
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
const http = require('http');
|
|
12
|
+
const https = require('https');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const fs = require('fs');
|
|
11
16
|
|
|
12
|
-
//
|
|
13
|
-
const
|
|
17
|
+
// Config file path (same as ai-agents)
|
|
18
|
+
const CONFIG_DIR = path.join(os.homedir(), '.hqx');
|
|
19
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'ai-config.json');
|
|
20
|
+
|
|
21
|
+
// Default CLIProxy endpoint
|
|
22
|
+
const DEFAULT_CLIPROXY_URL = 'http://localhost:8317';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get CLIProxy URL from config or default
|
|
26
|
+
* @returns {string} CLIProxy base URL
|
|
27
|
+
*/
|
|
28
|
+
const getCliProxyUrl = () => {
|
|
29
|
+
try {
|
|
30
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
31
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
32
|
+
if (config.cliproxyUrl && config.cliproxyUrl.trim()) {
|
|
33
|
+
return config.cliproxyUrl.trim();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} catch (error) { /* ignore */ }
|
|
37
|
+
return DEFAULT_CLIPROXY_URL;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Set CLIProxy URL in config
|
|
42
|
+
* @param {string} url - CLIProxy URL
|
|
43
|
+
* @returns {boolean} Success status
|
|
44
|
+
*/
|
|
45
|
+
const setCliProxyUrl = (url) => {
|
|
46
|
+
try {
|
|
47
|
+
let config = { providers: {} };
|
|
48
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
49
|
+
config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
50
|
+
}
|
|
51
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
52
|
+
config.cliproxyUrl = url;
|
|
53
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
54
|
+
return true;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
14
59
|
|
|
15
60
|
/**
|
|
16
61
|
* Make HTTP request to CLIProxy
|
|
@@ -18,24 +63,30 @@ const CLIPROXY_BASE = 'http://localhost:8317';
|
|
|
18
63
|
* @param {string} method - HTTP method
|
|
19
64
|
* @param {Object} headers - Request headers
|
|
20
65
|
* @param {number} timeout - Timeout in ms (default 60000 per RULES.md #15)
|
|
66
|
+
* @param {string} baseUrl - Optional base URL override
|
|
21
67
|
* @returns {Promise<Object>} { success, data, error }
|
|
22
68
|
*/
|
|
23
|
-
const fetchCliProxy = (path, method = 'GET', headers = {}, timeout = 60000) => {
|
|
69
|
+
const fetchCliProxy = (path, method = 'GET', headers = {}, timeout = 60000, baseUrl = null) => {
|
|
24
70
|
return new Promise((resolve) => {
|
|
25
|
-
const
|
|
71
|
+
const base = baseUrl || getCliProxyUrl();
|
|
72
|
+
const url = new URL(path, base);
|
|
73
|
+
const isHttps = url.protocol === 'https:';
|
|
74
|
+
const httpModule = isHttps ? https : http;
|
|
75
|
+
|
|
26
76
|
const options = {
|
|
27
77
|
hostname: url.hostname,
|
|
28
|
-
port: url.port || 8317,
|
|
78
|
+
port: url.port || (isHttps ? 443 : 8317),
|
|
29
79
|
path: url.pathname + url.search,
|
|
30
80
|
method,
|
|
31
81
|
headers: {
|
|
32
82
|
'Content-Type': 'application/json',
|
|
33
83
|
...headers
|
|
34
84
|
},
|
|
35
|
-
timeout
|
|
85
|
+
timeout,
|
|
86
|
+
rejectUnauthorized: false // Allow self-signed certs for remote
|
|
36
87
|
};
|
|
37
88
|
|
|
38
|
-
const req =
|
|
89
|
+
const req = httpModule.request(options, (res) => {
|
|
39
90
|
let data = '';
|
|
40
91
|
res.on('data', chunk => data += chunk);
|
|
41
92
|
res.on('end', () => {
|
|
@@ -53,7 +104,7 @@ const fetchCliProxy = (path, method = 'GET', headers = {}, timeout = 60000) => {
|
|
|
53
104
|
|
|
54
105
|
req.on('error', (error) => {
|
|
55
106
|
if (error.code === 'ECONNREFUSED') {
|
|
56
|
-
resolve({ success: false, error: 'CLIProxy not
|
|
107
|
+
resolve({ success: false, error: 'CLIProxy not reachable', data: null });
|
|
57
108
|
} else {
|
|
58
109
|
resolve({ success: false, error: error.message, data: null });
|
|
59
110
|
}
|
|
@@ -69,14 +120,17 @@ const fetchCliProxy = (path, method = 'GET', headers = {}, timeout = 60000) => {
|
|
|
69
120
|
};
|
|
70
121
|
|
|
71
122
|
/**
|
|
72
|
-
* Check if CLIProxy is running
|
|
73
|
-
* @
|
|
123
|
+
* Check if CLIProxy is running/reachable
|
|
124
|
+
* @param {string} url - Optional URL to test (uses config if not provided)
|
|
125
|
+
* @returns {Promise<Object>} { running, error, url }
|
|
74
126
|
*/
|
|
75
|
-
const isCliProxyRunning = async () => {
|
|
76
|
-
const
|
|
127
|
+
const isCliProxyRunning = async (url = null) => {
|
|
128
|
+
const testUrl = url || getCliProxyUrl();
|
|
129
|
+
const result = await fetchCliProxy('/v1/models', 'GET', {}, 5000, testUrl);
|
|
77
130
|
return {
|
|
78
131
|
running: result.success,
|
|
79
|
-
error: result.success ? null : result.error
|
|
132
|
+
error: result.success ? null : result.error,
|
|
133
|
+
url: testUrl
|
|
80
134
|
};
|
|
81
135
|
};
|
|
82
136
|
|
|
@@ -189,7 +243,9 @@ const getAuthFiles = async () => {
|
|
|
189
243
|
};
|
|
190
244
|
|
|
191
245
|
module.exports = {
|
|
192
|
-
|
|
246
|
+
DEFAULT_CLIPROXY_URL,
|
|
247
|
+
getCliProxyUrl,
|
|
248
|
+
setCliProxyUrl,
|
|
193
249
|
isCliProxyRunning,
|
|
194
250
|
fetchModelsFromCliProxy,
|
|
195
251
|
getOAuthUrl,
|