hedgequantx 2.7.21 → 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/app.js +12 -2
- package/src/menus/dashboard.js +2 -2
- package/src/pages/ai-agents-ui.js +211 -0
- package/src/pages/ai-agents.js +293 -297
- package/src/services/cliproxy.js +255 -0
package/package.json
CHANGED
package/src/app.js
CHANGED
|
@@ -126,7 +126,16 @@ const banner = async () => {
|
|
|
126
126
|
|
|
127
127
|
const tagline = isMobile ? `HQX v${version}` : `Prop Futures Algo Trading v${version}`;
|
|
128
128
|
console.log(chalk.cyan('║') + chalk.white(centerText(tagline, innerWidth)) + chalk.cyan('║'));
|
|
129
|
-
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Display banner with closed bottom (standalone)
|
|
133
|
+
*/
|
|
134
|
+
const bannerClosed = async () => {
|
|
135
|
+
await banner();
|
|
136
|
+
const termWidth = process.stdout.columns || 100;
|
|
137
|
+
const boxWidth = termWidth < 60 ? Math.max(termWidth - 2, 40) : Math.max(getLogoWidth(), 98);
|
|
138
|
+
console.log(chalk.cyan('╚' + '═'.repeat(boxWidth - 2) + '╝'));
|
|
130
139
|
};
|
|
131
140
|
|
|
132
141
|
const getFullLogo = () => [
|
|
@@ -187,7 +196,8 @@ const run = async () => {
|
|
|
187
196
|
const totalContentWidth = numCols * colWidth;
|
|
188
197
|
const leftMargin = Math.max(2, Math.floor((innerWidth - totalContentWidth) / 2));
|
|
189
198
|
|
|
190
|
-
|
|
199
|
+
// Continue from banner (connected rectangle)
|
|
200
|
+
console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
|
|
191
201
|
console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PROPFIRM', innerWidth)) + chalk.cyan('║'));
|
|
192
202
|
console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
|
|
193
203
|
|
package/src/menus/dashboard.js
CHANGED
|
@@ -30,8 +30,8 @@ const dashboardMenu = async (service) => {
|
|
|
30
30
|
return chalk.cyan('║') + content + ' '.repeat(Math.max(0, padding)) + chalk.cyan('║');
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
-
//
|
|
34
|
-
console.log(chalk.cyan('
|
|
33
|
+
// Continue from banner (connected rectangle)
|
|
34
|
+
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
35
35
|
console.log(makeLine(chalk.yellow.bold('Welcome, HQX Trader!'), 'center'));
|
|
36
36
|
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
37
37
|
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Agents UI Components
|
|
3
|
+
*
|
|
4
|
+
* UI drawing functions for the AI Agents configuration page.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
const { centerText, visibleLength } = require('../ui');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Draw a 2-column row
|
|
12
|
+
* @param {string} leftText - Left column text
|
|
13
|
+
* @param {string} rightText - Right column text
|
|
14
|
+
* @param {number} W - Inner width
|
|
15
|
+
*/
|
|
16
|
+
const draw2ColRow = (leftText, rightText, W) => {
|
|
17
|
+
const col1Width = Math.floor(W / 2);
|
|
18
|
+
const col2Width = W - col1Width;
|
|
19
|
+
const leftLen = visibleLength(leftText);
|
|
20
|
+
const leftPad = col1Width - leftLen;
|
|
21
|
+
const leftPadL = Math.floor(leftPad / 2);
|
|
22
|
+
const rightLen = visibleLength(rightText || '');
|
|
23
|
+
const rightPad = col2Width - rightLen;
|
|
24
|
+
const rightPadL = Math.floor(rightPad / 2);
|
|
25
|
+
console.log(
|
|
26
|
+
chalk.cyan('║') +
|
|
27
|
+
' '.repeat(leftPadL) + leftText + ' '.repeat(leftPad - leftPadL) +
|
|
28
|
+
' '.repeat(rightPadL) + (rightText || '') + ' '.repeat(rightPad - rightPadL) +
|
|
29
|
+
chalk.cyan('║')
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Draw 2-column table with title and back option
|
|
35
|
+
* @param {string} title - Table title
|
|
36
|
+
* @param {Function} titleColor - Chalk color function
|
|
37
|
+
* @param {Array} items - Items to display
|
|
38
|
+
* @param {string} backText - Back button text
|
|
39
|
+
* @param {number} W - Inner width
|
|
40
|
+
*/
|
|
41
|
+
const draw2ColTable = (title, titleColor, items, backText, W) => {
|
|
42
|
+
console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
|
|
43
|
+
console.log(chalk.cyan('║') + titleColor(centerText(title, W)) + chalk.cyan('║'));
|
|
44
|
+
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
45
|
+
|
|
46
|
+
const rows = Math.ceil(items.length / 2);
|
|
47
|
+
for (let row = 0; row < rows; row++) {
|
|
48
|
+
const left = items[row];
|
|
49
|
+
const right = items[row + rows];
|
|
50
|
+
draw2ColRow(left || '', right || '', W);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
|
|
54
|
+
console.log(chalk.cyan('║') + chalk.red(centerText(backText, W)) + chalk.cyan('║'));
|
|
55
|
+
console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Draw providers table
|
|
60
|
+
* @param {Array} providers - List of AI providers
|
|
61
|
+
* @param {Object} config - Current config
|
|
62
|
+
* @param {number} boxWidth - Box width
|
|
63
|
+
* @param {string} cliproxyUrl - Current CLIProxy URL (optional)
|
|
64
|
+
*/
|
|
65
|
+
const drawProvidersTable = (providers, config, boxWidth, cliproxyUrl = null) => {
|
|
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
|
+
|
|
80
|
+
const items = providers.map((p, i) => {
|
|
81
|
+
const status = config.providers[p.id]?.active ? chalk.green(' ●') : '';
|
|
82
|
+
return chalk.cyan(`[${i + 1}]`) + ' ' + chalk[p.color](p.name) + status;
|
|
83
|
+
});
|
|
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) + '╝'));
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Draw models table
|
|
101
|
+
* @param {Object} provider - Provider object
|
|
102
|
+
* @param {Array} models - List of models
|
|
103
|
+
* @param {number} boxWidth - Box width
|
|
104
|
+
*/
|
|
105
|
+
const drawModelsTable = (provider, models, boxWidth) => {
|
|
106
|
+
const W = boxWidth - 2;
|
|
107
|
+
const items = models.map((m, i) => chalk.cyan(`[${i + 1}]`) + ' ' + chalk.white(m.name));
|
|
108
|
+
draw2ColTable(`${provider.name.toUpperCase()} - MODELS`, chalk[provider.color].bold, items, '[B] Back', W);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Draw provider configuration window
|
|
113
|
+
* @param {Object} provider - Provider object
|
|
114
|
+
* @param {Object} config - Current config
|
|
115
|
+
* @param {number} boxWidth - Box width
|
|
116
|
+
*/
|
|
117
|
+
const drawProviderWindow = (provider, config, boxWidth) => {
|
|
118
|
+
const W = boxWidth - 2;
|
|
119
|
+
const col1Width = Math.floor(W / 2);
|
|
120
|
+
const col2Width = W - col1Width;
|
|
121
|
+
const providerConfig = config.providers[provider.id] || {};
|
|
122
|
+
|
|
123
|
+
// Header
|
|
124
|
+
console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
|
|
125
|
+
console.log(chalk.cyan('║') + chalk[provider.color].bold(centerText(provider.name.toUpperCase(), W)) + chalk.cyan('║'));
|
|
126
|
+
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
127
|
+
|
|
128
|
+
// Empty line
|
|
129
|
+
console.log(chalk.cyan('║') + ' '.repeat(W) + chalk.cyan('║'));
|
|
130
|
+
|
|
131
|
+
// Options in 2 columns
|
|
132
|
+
const opt1Title = '[1] Connect via Paid Plan';
|
|
133
|
+
const opt1Desc = 'Uses CLIProxy - No API key needed';
|
|
134
|
+
const opt2Title = '[2] Connect via API Key';
|
|
135
|
+
const opt2Desc = 'Enter your own API key';
|
|
136
|
+
|
|
137
|
+
// Row 1: Titles
|
|
138
|
+
const left1 = chalk.green(opt1Title);
|
|
139
|
+
const right1 = chalk.yellow(opt2Title);
|
|
140
|
+
const left1Len = visibleLength(left1);
|
|
141
|
+
const right1Len = visibleLength(right1);
|
|
142
|
+
const left1PadTotal = col1Width - left1Len;
|
|
143
|
+
const left1PadL = Math.floor(left1PadTotal / 2);
|
|
144
|
+
const left1PadR = left1PadTotal - left1PadL;
|
|
145
|
+
const right1PadTotal = col2Width - right1Len;
|
|
146
|
+
const right1PadL = Math.floor(right1PadTotal / 2);
|
|
147
|
+
const right1PadR = right1PadTotal - right1PadL;
|
|
148
|
+
|
|
149
|
+
console.log(
|
|
150
|
+
chalk.cyan('║') +
|
|
151
|
+
' '.repeat(left1PadL) + left1 + ' '.repeat(left1PadR) +
|
|
152
|
+
' '.repeat(right1PadL) + right1 + ' '.repeat(right1PadR) +
|
|
153
|
+
chalk.cyan('║')
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Row 2: Descriptions
|
|
157
|
+
const left2 = chalk.gray(opt1Desc);
|
|
158
|
+
const right2 = chalk.gray(opt2Desc);
|
|
159
|
+
const left2Len = visibleLength(left2);
|
|
160
|
+
const right2Len = visibleLength(right2);
|
|
161
|
+
const left2PadTotal = col1Width - left2Len;
|
|
162
|
+
const left2PadL = Math.floor(left2PadTotal / 2);
|
|
163
|
+
const left2PadR = left2PadTotal - left2PadL;
|
|
164
|
+
const right2PadTotal = col2Width - right2Len;
|
|
165
|
+
const right2PadL = Math.floor(right2PadTotal / 2);
|
|
166
|
+
const right2PadR = right2PadTotal - right2PadL;
|
|
167
|
+
|
|
168
|
+
console.log(
|
|
169
|
+
chalk.cyan('║') +
|
|
170
|
+
' '.repeat(left2PadL) + left2 + ' '.repeat(left2PadR) +
|
|
171
|
+
' '.repeat(right2PadL) + right2 + ' '.repeat(right2PadR) +
|
|
172
|
+
chalk.cyan('║')
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Empty line
|
|
176
|
+
console.log(chalk.cyan('║') + ' '.repeat(W) + chalk.cyan('║'));
|
|
177
|
+
|
|
178
|
+
// Status bar
|
|
179
|
+
console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
|
|
180
|
+
|
|
181
|
+
let statusText = '';
|
|
182
|
+
if (providerConfig.active) {
|
|
183
|
+
const connType = providerConfig.connectionType === 'cliproxy' ? 'CLIProxy' : 'API Key';
|
|
184
|
+
const modelName = providerConfig.modelName || 'N/A';
|
|
185
|
+
statusText = chalk.green('● ACTIVE') + chalk.gray(' Model: ') + chalk.yellow(modelName) + chalk.gray(' via ') + chalk.cyan(connType);
|
|
186
|
+
} else if (providerConfig.apiKey || providerConfig.connectionType) {
|
|
187
|
+
statusText = chalk.yellow('● CONFIGURED') + chalk.gray(' (not active)');
|
|
188
|
+
} else {
|
|
189
|
+
statusText = chalk.gray('○ NOT CONFIGURED');
|
|
190
|
+
}
|
|
191
|
+
console.log(chalk.cyan('║') + centerText(statusText, W) + chalk.cyan('║'));
|
|
192
|
+
|
|
193
|
+
// Disconnect option if active
|
|
194
|
+
if (providerConfig.active) {
|
|
195
|
+
console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
|
|
196
|
+
console.log(chalk.cyan('║') + chalk.red(centerText('[D] Disconnect', W)) + chalk.cyan('║'));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Back
|
|
200
|
+
console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
|
|
201
|
+
console.log(chalk.cyan('║') + chalk.red(centerText('[B] Back', W)) + chalk.cyan('║'));
|
|
202
|
+
console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
module.exports = {
|
|
206
|
+
draw2ColRow,
|
|
207
|
+
draw2ColTable,
|
|
208
|
+
drawProvidersTable,
|
|
209
|
+
drawModelsTable,
|
|
210
|
+
drawProviderWindow
|
|
211
|
+
};
|
package/src/pages/ai-agents.js
CHANGED
|
@@ -9,11 +9,13 @@ const chalk = require('chalk');
|
|
|
9
9
|
const os = require('os');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const fs = require('fs');
|
|
12
|
-
|
|
13
12
|
const ora = require('ora');
|
|
14
|
-
|
|
13
|
+
|
|
14
|
+
const { getLogoWidth } = require('../ui');
|
|
15
15
|
const { prompts } = require('../utils');
|
|
16
16
|
const { fetchModelsFromApi } = require('./ai-models');
|
|
17
|
+
const { drawProvidersTable, drawModelsTable, drawProviderWindow } = require('./ai-agents-ui');
|
|
18
|
+
const { isCliProxyRunning, fetchModelsFromCliProxy, getOAuthUrl, checkOAuthStatus, getCliProxyUrl, setCliProxyUrl, DEFAULT_CLIPROXY_URL } = require('../services/cliproxy');
|
|
17
19
|
|
|
18
20
|
// Config file path
|
|
19
21
|
const CONFIG_DIR = path.join(os.homedir(), '.hqx');
|
|
@@ -31,32 +33,20 @@ const AI_PROVIDERS = [
|
|
|
31
33
|
{ id: 'openrouter', name: 'OpenRouter', color: 'gray' },
|
|
32
34
|
];
|
|
33
35
|
|
|
34
|
-
/**
|
|
35
|
-
* Load AI config from file
|
|
36
|
-
* @returns {Object} Config object with provider settings
|
|
37
|
-
*/
|
|
36
|
+
/** Load AI config from file */
|
|
38
37
|
const loadConfig = () => {
|
|
39
38
|
try {
|
|
40
39
|
if (fs.existsSync(CONFIG_FILE)) {
|
|
41
|
-
|
|
42
|
-
return JSON.parse(data);
|
|
40
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
43
41
|
}
|
|
44
|
-
} catch (error) {
|
|
45
|
-
// Config file doesn't exist or is invalid
|
|
46
|
-
}
|
|
42
|
+
} catch (error) { /* ignore */ }
|
|
47
43
|
return { providers: {} };
|
|
48
44
|
};
|
|
49
45
|
|
|
50
|
-
/**
|
|
51
|
-
* Save AI config to file
|
|
52
|
-
* @param {Object} config - Config object to save
|
|
53
|
-
* @returns {boolean} Success status
|
|
54
|
-
*/
|
|
46
|
+
/** Save AI config to file */
|
|
55
47
|
const saveConfig = (config) => {
|
|
56
48
|
try {
|
|
57
|
-
if (!fs.existsSync(CONFIG_DIR)) {
|
|
58
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
59
|
-
}
|
|
49
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
60
50
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
61
51
|
return true;
|
|
62
52
|
} catch (error) {
|
|
@@ -64,87 +54,28 @@ const saveConfig = (config) => {
|
|
|
64
54
|
}
|
|
65
55
|
};
|
|
66
56
|
|
|
67
|
-
/**
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const leftLen = visibleLength(leftText);
|
|
84
|
-
const leftPad = col1Width - leftLen;
|
|
85
|
-
const leftPadL = Math.floor(leftPad / 2);
|
|
86
|
-
const rightLen = visibleLength(rightText || '');
|
|
87
|
-
const rightPad = col2Width - rightLen;
|
|
88
|
-
const rightPadL = Math.floor(rightPad / 2);
|
|
89
|
-
console.log(
|
|
90
|
-
chalk.cyan('║') +
|
|
91
|
-
' '.repeat(leftPadL) + leftText + ' '.repeat(leftPad - leftPadL) +
|
|
92
|
-
' '.repeat(rightPadL) + (rightText || '') + ' '.repeat(rightPad - rightPadL) +
|
|
93
|
-
chalk.cyan('║')
|
|
94
|
-
);
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Draw 2-column table
|
|
99
|
-
*/
|
|
100
|
-
const draw2ColTable = (title, titleColor, items, backText, W) => {
|
|
101
|
-
console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
|
|
102
|
-
console.log(chalk.cyan('║') + titleColor(centerText(title, W)) + chalk.cyan('║'));
|
|
103
|
-
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
104
|
-
|
|
105
|
-
const rows = Math.ceil(items.length / 2);
|
|
106
|
-
for (let row = 0; row < rows; row++) {
|
|
107
|
-
const left = items[row];
|
|
108
|
-
const right = items[row + rows];
|
|
109
|
-
draw2ColRow(left || '', right || '', W);
|
|
57
|
+
/** Select a model from a pre-fetched list */
|
|
58
|
+
const selectModelFromList = async (provider, models, boxWidth) => {
|
|
59
|
+
while (true) {
|
|
60
|
+
console.clear();
|
|
61
|
+
drawModelsTable(provider, models, boxWidth);
|
|
62
|
+
|
|
63
|
+
const input = await prompts.textInput(chalk.cyan('Select model: '));
|
|
64
|
+
const choice = (input || '').toLowerCase().trim();
|
|
65
|
+
|
|
66
|
+
if (choice === 'b' || choice === '') return null;
|
|
67
|
+
|
|
68
|
+
const num = parseInt(choice);
|
|
69
|
+
if (!isNaN(num) && num >= 1 && num <= models.length) return models[num - 1];
|
|
70
|
+
|
|
71
|
+
console.log(chalk.red(' Invalid option.'));
|
|
72
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
110
73
|
}
|
|
111
|
-
|
|
112
|
-
console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
|
|
113
|
-
console.log(chalk.cyan('║') + chalk.red(centerText(backText, W)) + chalk.cyan('║'));
|
|
114
|
-
console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Draw providers table
|
|
119
|
-
*/
|
|
120
|
-
const drawProvidersTable = (config, boxWidth) => {
|
|
121
|
-
const W = boxWidth - 2;
|
|
122
|
-
const items = AI_PROVIDERS.map((p, i) => {
|
|
123
|
-
const status = config.providers[p.id]?.active ? chalk.green(' ●') : '';
|
|
124
|
-
return chalk.cyan(`[${i + 1}]`) + ' ' + chalk[p.color](p.name) + status;
|
|
125
|
-
});
|
|
126
|
-
draw2ColTable('AI AGENTS CONFIGURATION', chalk.yellow.bold, items, '[B] Back to Menu', W);
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Draw models table
|
|
131
|
-
*/
|
|
132
|
-
const drawModelsTable = (provider, models, boxWidth) => {
|
|
133
|
-
const W = boxWidth - 2;
|
|
134
|
-
const items = models.map((m, i) => chalk.cyan(`[${i + 1}]`) + ' ' + chalk.white(m.name));
|
|
135
|
-
draw2ColTable(`${provider.name.toUpperCase()} - MODELS`, chalk[provider.color].bold, items, '[B] Back', W);
|
|
136
74
|
};
|
|
137
75
|
|
|
138
|
-
/**
|
|
139
|
-
* Select a model for a provider (fetches from API)
|
|
140
|
-
* @param {Object} provider - Provider object
|
|
141
|
-
* @param {string} apiKey - API key for fetching models
|
|
142
|
-
* @returns {Object|null} Selected model or null if cancelled/failed
|
|
143
|
-
*/
|
|
76
|
+
/** Select a model for a provider (fetches from API) */
|
|
144
77
|
const selectModel = async (provider, apiKey) => {
|
|
145
78
|
const boxWidth = getLogoWidth();
|
|
146
|
-
|
|
147
|
-
// Fetch models from API
|
|
148
79
|
const spinner = ora({ text: 'Fetching models from API...', color: 'yellow' }).start();
|
|
149
80
|
const result = await fetchModelsFromApi(provider.id, apiKey);
|
|
150
81
|
|
|
@@ -155,129 +86,212 @@ const selectModel = async (provider, apiKey) => {
|
|
|
155
86
|
}
|
|
156
87
|
|
|
157
88
|
spinner.succeed(`Found ${result.models.length} models`);
|
|
158
|
-
|
|
89
|
+
return selectModelFromList(provider, result.models, boxWidth);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** Deactivate all providers and activate one */
|
|
93
|
+
const activateProvider = (config, providerId, data) => {
|
|
94
|
+
Object.keys(config.providers).forEach(id => {
|
|
95
|
+
if (config.providers[id]) config.providers[id].active = false;
|
|
96
|
+
});
|
|
97
|
+
if (!config.providers[providerId]) config.providers[providerId] = {};
|
|
98
|
+
Object.assign(config.providers[providerId], data, { active: true, configuredAt: new Date().toISOString() });
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/** Handle CLIProxy connection */
|
|
102
|
+
const handleCliProxyConnection = async (provider, config, boxWidth) => {
|
|
103
|
+
console.log();
|
|
104
|
+
const currentUrl = getCliProxyUrl();
|
|
105
|
+
const spinner = ora({ text: `Checking CLIProxy at ${currentUrl}...`, color: 'yellow' }).start();
|
|
106
|
+
let proxyStatus = await isCliProxyRunning();
|
|
159
107
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
108
|
+
if (!proxyStatus.running) {
|
|
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();
|
|
163
116
|
|
|
164
|
-
const
|
|
165
|
-
const choice = (input || '').toLowerCase().trim();
|
|
117
|
+
const urlChoice = await prompts.textInput(chalk.cyan(' Select option: '));
|
|
166
118
|
|
|
167
|
-
if (
|
|
168
|
-
return
|
|
119
|
+
if (!urlChoice || urlChoice.toLowerCase() === 'b') {
|
|
120
|
+
return false;
|
|
169
121
|
}
|
|
170
122
|
|
|
171
|
-
|
|
172
|
-
if (
|
|
173
|
-
|
|
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
|
+
}
|
|
174
139
|
}
|
|
175
140
|
|
|
176
|
-
|
|
177
|
-
|
|
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}`);
|
|
160
|
+
}
|
|
161
|
+
const oauthResult = await getOAuthUrl(provider.id);
|
|
162
|
+
|
|
163
|
+
if (!oauthResult.success) {
|
|
164
|
+
// OAuth not supported - try direct model fetch
|
|
165
|
+
console.log(chalk.gray(` OAuth not available for ${provider.name}, checking models...`));
|
|
166
|
+
const modelsResult = await fetchModelsFromCliProxy();
|
|
167
|
+
|
|
168
|
+
if (!modelsResult.success || modelsResult.models.length === 0) {
|
|
169
|
+
console.log(chalk.red(` No models available via CLIProxy for ${provider.name}`));
|
|
170
|
+
console.log(chalk.gray(` Error: ${modelsResult.error || 'Unknown'}`));
|
|
171
|
+
await prompts.waitForEnter();
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const selectedModel = await selectModelFromList(provider, modelsResult.models, boxWidth);
|
|
176
|
+
if (!selectedModel) return false;
|
|
177
|
+
|
|
178
|
+
activateProvider(config, provider.id, {
|
|
179
|
+
connectionType: 'cliproxy',
|
|
180
|
+
modelId: selectedModel.id,
|
|
181
|
+
modelName: selectedModel.name
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
if (saveConfig(config)) {
|
|
185
|
+
console.log(chalk.green(`\n ✓ ${provider.name} connected via CLIProxy.`));
|
|
186
|
+
console.log(chalk.cyan(` Model: ${selectedModel.name}`));
|
|
187
|
+
}
|
|
188
|
+
await prompts.waitForEnter();
|
|
189
|
+
return true;
|
|
178
190
|
}
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Draw provider configuration window
|
|
183
|
-
* @param {Object} provider - Provider object
|
|
184
|
-
* @param {Object} config - Current config
|
|
185
|
-
* @param {number} boxWidth - Box width
|
|
186
|
-
*/
|
|
187
|
-
const drawProviderWindow = (provider, config, boxWidth) => {
|
|
188
|
-
const W = boxWidth - 2;
|
|
189
|
-
const col1Width = Math.floor(W / 2);
|
|
190
|
-
const col2Width = W - col1Width;
|
|
191
|
-
const providerConfig = config.providers[provider.id] || {};
|
|
192
191
|
|
|
193
|
-
//
|
|
194
|
-
console.log(chalk.cyan('
|
|
195
|
-
console.log(chalk.
|
|
196
|
-
console.log(chalk.
|
|
192
|
+
// OAuth flow
|
|
193
|
+
console.log(chalk.cyan('\n Open this URL in your browser to authenticate:\n'));
|
|
194
|
+
console.log(chalk.yellow(` ${oauthResult.url}\n`));
|
|
195
|
+
console.log(chalk.gray(' Waiting for authentication... (Press Enter to cancel)'));
|
|
197
196
|
|
|
198
|
-
|
|
199
|
-
|
|
197
|
+
let authenticated = false;
|
|
198
|
+
const maxWait = 120000, pollInterval = 3000;
|
|
199
|
+
let waited = 0;
|
|
200
200
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
201
|
+
const pollPromise = (async () => {
|
|
202
|
+
while (waited < maxWait) {
|
|
203
|
+
await new Promise(r => setTimeout(r, pollInterval));
|
|
204
|
+
waited += pollInterval;
|
|
205
|
+
if (oauthResult.state) {
|
|
206
|
+
const statusResult = await checkOAuthStatus(oauthResult.state);
|
|
207
|
+
if (statusResult.success && statusResult.status === 'ok') { authenticated = true; return true; }
|
|
208
|
+
if (statusResult.status === 'error') {
|
|
209
|
+
console.log(chalk.red(`\n Authentication error: ${statusResult.error || 'Unknown'}`));
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return false;
|
|
215
|
+
})();
|
|
206
216
|
|
|
207
|
-
|
|
208
|
-
const left1 = chalk.green(opt1Title);
|
|
209
|
-
const right1 = chalk.yellow(opt2Title);
|
|
210
|
-
const left1Len = visibleLength(left1);
|
|
211
|
-
const right1Len = visibleLength(right1);
|
|
212
|
-
const left1PadTotal = col1Width - left1Len;
|
|
213
|
-
const left1PadL = Math.floor(left1PadTotal / 2);
|
|
214
|
-
const left1PadR = left1PadTotal - left1PadL;
|
|
215
|
-
const right1PadTotal = col2Width - right1Len;
|
|
216
|
-
const right1PadL = Math.floor(right1PadTotal / 2);
|
|
217
|
-
const right1PadR = right1PadTotal - right1PadL;
|
|
217
|
+
await Promise.race([pollPromise, prompts.waitForEnter()]);
|
|
218
218
|
|
|
219
|
-
|
|
220
|
-
chalk.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
);
|
|
219
|
+
if (!authenticated) {
|
|
220
|
+
console.log(chalk.yellow(' Authentication cancelled or timed out.'));
|
|
221
|
+
await prompts.waitForEnter();
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
225
224
|
|
|
226
|
-
|
|
227
|
-
const left2 = chalk.gray(opt1Desc);
|
|
228
|
-
const right2 = chalk.gray(opt2Desc);
|
|
229
|
-
const left2Len = visibleLength(left2);
|
|
230
|
-
const right2Len = visibleLength(right2);
|
|
231
|
-
const left2PadTotal = col1Width - left2Len;
|
|
232
|
-
const left2PadL = Math.floor(left2PadTotal / 2);
|
|
233
|
-
const left2PadR = left2PadTotal - left2PadL;
|
|
234
|
-
const right2PadTotal = col2Width - right2Len;
|
|
235
|
-
const right2PadL = Math.floor(right2PadTotal / 2);
|
|
236
|
-
const right2PadR = right2PadTotal - right2PadL;
|
|
225
|
+
console.log(chalk.green(' ✓ Authentication successful!'));
|
|
237
226
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
227
|
+
const modelsResult = await fetchModelsFromCliProxy();
|
|
228
|
+
if (modelsResult.success && modelsResult.models.length > 0) {
|
|
229
|
+
const selectedModel = await selectModelFromList(provider, modelsResult.models, boxWidth);
|
|
230
|
+
if (selectedModel) {
|
|
231
|
+
activateProvider(config, provider.id, {
|
|
232
|
+
connectionType: 'cliproxy',
|
|
233
|
+
modelId: selectedModel.id,
|
|
234
|
+
modelName: selectedModel.name
|
|
235
|
+
});
|
|
236
|
+
if (saveConfig(config)) {
|
|
237
|
+
console.log(chalk.green(`\n ✓ ${provider.name} connected via CLIProxy.`));
|
|
238
|
+
console.log(chalk.cyan(` Model: ${selectedModel.name}`));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
activateProvider(config, provider.id, {
|
|
243
|
+
connectionType: 'cliproxy',
|
|
244
|
+
modelId: null,
|
|
245
|
+
modelName: 'Default'
|
|
246
|
+
});
|
|
247
|
+
if (saveConfig(config)) console.log(chalk.green(`\n ✓ ${provider.name} connected via CLIProxy.`));
|
|
248
|
+
}
|
|
244
249
|
|
|
245
|
-
|
|
246
|
-
|
|
250
|
+
await prompts.waitForEnter();
|
|
251
|
+
return true;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
/** Handle API Key connection */
|
|
255
|
+
const handleApiKeyConnection = async (provider, config) => {
|
|
256
|
+
console.clear();
|
|
257
|
+
console.log(chalk.yellow(`\n Enter your ${provider.name} API key:`));
|
|
258
|
+
console.log(chalk.gray(' (Press Enter to cancel)\n'));
|
|
247
259
|
|
|
248
|
-
|
|
249
|
-
console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
|
|
260
|
+
const apiKey = await prompts.textInput(chalk.cyan(' API Key: '), true);
|
|
250
261
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
statusText = chalk.green('● ACTIVE') + chalk.gray(' Model: ') + chalk.yellow(modelName) + chalk.gray(' via ') + chalk.cyan(connType);
|
|
256
|
-
} else if (providerConfig.apiKey || providerConfig.connectionType) {
|
|
257
|
-
statusText = chalk.yellow('● CONFIGURED') + chalk.gray(' (not active)');
|
|
258
|
-
} else {
|
|
259
|
-
statusText = chalk.gray('○ NOT CONFIGURED');
|
|
262
|
+
if (!apiKey || apiKey.trim() === '') {
|
|
263
|
+
console.log(chalk.gray(' Cancelled.'));
|
|
264
|
+
await prompts.waitForEnter();
|
|
265
|
+
return false;
|
|
260
266
|
}
|
|
261
|
-
console.log(chalk.cyan('║') + centerText(statusText, W) + chalk.cyan('║'));
|
|
262
267
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
268
|
+
if (apiKey.length < 20) {
|
|
269
|
+
console.log(chalk.red(' Invalid API key format (too short).'));
|
|
270
|
+
await prompts.waitForEnter();
|
|
271
|
+
return false;
|
|
267
272
|
}
|
|
268
273
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
274
|
+
const selectedModel = await selectModel(provider, apiKey.trim());
|
|
275
|
+
if (!selectedModel) return false;
|
|
276
|
+
|
|
277
|
+
activateProvider(config, provider.id, {
|
|
278
|
+
connectionType: 'apikey',
|
|
279
|
+
apiKey: apiKey.trim(),
|
|
280
|
+
modelId: selectedModel.id,
|
|
281
|
+
modelName: selectedModel.name
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
if (saveConfig(config)) {
|
|
285
|
+
console.log(chalk.green(`\n ✓ ${provider.name} connected via API Key.`));
|
|
286
|
+
console.log(chalk.cyan(` Model: ${selectedModel.name}`));
|
|
287
|
+
} else {
|
|
288
|
+
console.log(chalk.red('\n Failed to save config.'));
|
|
289
|
+
}
|
|
290
|
+
await prompts.waitForEnter();
|
|
291
|
+
return true;
|
|
273
292
|
};
|
|
274
293
|
|
|
275
|
-
/**
|
|
276
|
-
* Handle provider configuration
|
|
277
|
-
* @param {Object} provider - Provider to configure
|
|
278
|
-
* @param {Object} config - Current config
|
|
279
|
-
* @returns {Object} Updated config
|
|
280
|
-
*/
|
|
294
|
+
/** Handle provider configuration */
|
|
281
295
|
const handleProviderConfig = async (provider, config) => {
|
|
282
296
|
const boxWidth = getLogoWidth();
|
|
283
297
|
|
|
@@ -288,94 +302,23 @@ const handleProviderConfig = async (provider, config) => {
|
|
|
288
302
|
const input = await prompts.textInput(chalk.cyan('Select option: '));
|
|
289
303
|
const choice = (input || '').toLowerCase().trim();
|
|
290
304
|
|
|
291
|
-
if (choice === 'b' || choice === '')
|
|
292
|
-
break;
|
|
293
|
-
}
|
|
305
|
+
if (choice === 'b' || choice === '') break;
|
|
294
306
|
|
|
295
|
-
if (choice === 'd') {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
console.log(chalk.yellow(`\n ${provider.name} disconnected.`));
|
|
301
|
-
await prompts.waitForEnter();
|
|
302
|
-
}
|
|
307
|
+
if (choice === 'd' && config.providers[provider.id]) {
|
|
308
|
+
config.providers[provider.id].active = false;
|
|
309
|
+
saveConfig(config);
|
|
310
|
+
console.log(chalk.yellow(`\n ${provider.name} disconnected.`));
|
|
311
|
+
await prompts.waitForEnter();
|
|
303
312
|
continue;
|
|
304
313
|
}
|
|
305
314
|
|
|
306
315
|
if (choice === '1') {
|
|
307
|
-
|
|
308
|
-
console.log();
|
|
309
|
-
console.log(chalk.cyan(' CLIProxy uses your paid plan subscription.'));
|
|
310
|
-
console.log(chalk.gray(' Model selection will be available after connecting.'));
|
|
311
|
-
console.log();
|
|
312
|
-
|
|
313
|
-
// Deactivate all other providers
|
|
314
|
-
Object.keys(config.providers).forEach(id => {
|
|
315
|
-
if (config.providers[id]) config.providers[id].active = false;
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
if (!config.providers[provider.id]) config.providers[provider.id] = {};
|
|
319
|
-
config.providers[provider.id].connectionType = 'cliproxy';
|
|
320
|
-
config.providers[provider.id].modelId = null;
|
|
321
|
-
config.providers[provider.id].modelName = 'N/A';
|
|
322
|
-
config.providers[provider.id].active = true;
|
|
323
|
-
config.providers[provider.id].configuredAt = new Date().toISOString();
|
|
324
|
-
|
|
325
|
-
if (saveConfig(config)) {
|
|
326
|
-
console.log(chalk.green(` ✓ ${provider.name} connected via CLIProxy.`));
|
|
327
|
-
} else {
|
|
328
|
-
console.log(chalk.red(' Failed to save config.'));
|
|
329
|
-
}
|
|
330
|
-
await prompts.waitForEnter();
|
|
316
|
+
await handleCliProxyConnection(provider, config, boxWidth);
|
|
331
317
|
continue;
|
|
332
318
|
}
|
|
333
319
|
|
|
334
320
|
if (choice === '2') {
|
|
335
|
-
|
|
336
|
-
console.clear();
|
|
337
|
-
console.log(chalk.yellow(`\n Enter your ${provider.name} API key:`));
|
|
338
|
-
console.log(chalk.gray(' (Press Enter to cancel)'));
|
|
339
|
-
console.log();
|
|
340
|
-
|
|
341
|
-
const apiKey = await prompts.textInput(chalk.cyan(' API Key: '), true);
|
|
342
|
-
|
|
343
|
-
if (!apiKey || apiKey.trim() === '') {
|
|
344
|
-
console.log(chalk.gray(' Cancelled.'));
|
|
345
|
-
await prompts.waitForEnter();
|
|
346
|
-
continue;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (apiKey.length < 20) {
|
|
350
|
-
console.log(chalk.red(' Invalid API key format (too short).'));
|
|
351
|
-
await prompts.waitForEnter();
|
|
352
|
-
continue;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// Fetch models from API with the provided key
|
|
356
|
-
const selectedModel = await selectModel(provider, apiKey.trim());
|
|
357
|
-
if (!selectedModel) continue;
|
|
358
|
-
|
|
359
|
-
// Deactivate all other providers
|
|
360
|
-
Object.keys(config.providers).forEach(id => {
|
|
361
|
-
if (config.providers[id]) config.providers[id].active = false;
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
if (!config.providers[provider.id]) config.providers[provider.id] = {};
|
|
365
|
-
config.providers[provider.id].connectionType = 'apikey';
|
|
366
|
-
config.providers[provider.id].apiKey = apiKey.trim();
|
|
367
|
-
config.providers[provider.id].modelId = selectedModel.id;
|
|
368
|
-
config.providers[provider.id].modelName = selectedModel.name;
|
|
369
|
-
config.providers[provider.id].active = true;
|
|
370
|
-
config.providers[provider.id].configuredAt = new Date().toISOString();
|
|
371
|
-
|
|
372
|
-
if (saveConfig(config)) {
|
|
373
|
-
console.log(chalk.green(`\n ✓ ${provider.name} connected via API Key.`));
|
|
374
|
-
console.log(chalk.cyan(` Model: ${selectedModel.name}`));
|
|
375
|
-
} else {
|
|
376
|
-
console.log(chalk.red('\n Failed to save config.'));
|
|
377
|
-
}
|
|
378
|
-
await prompts.waitForEnter();
|
|
321
|
+
await handleApiKeyConnection(provider, config);
|
|
379
322
|
continue;
|
|
380
323
|
}
|
|
381
324
|
}
|
|
@@ -383,53 +326,106 @@ const handleProviderConfig = async (provider, config) => {
|
|
|
383
326
|
return config;
|
|
384
327
|
};
|
|
385
328
|
|
|
386
|
-
/**
|
|
387
|
-
* Get active AI provider config
|
|
388
|
-
* @returns {Object|null} Active provider config or null
|
|
389
|
-
*/
|
|
329
|
+
/** Get active AI provider config */
|
|
390
330
|
const getActiveProvider = () => {
|
|
391
331
|
const config = loadConfig();
|
|
392
332
|
for (const provider of AI_PROVIDERS) {
|
|
393
|
-
const
|
|
394
|
-
if (
|
|
333
|
+
const pc = config.providers[provider.id];
|
|
334
|
+
if (pc && pc.active) {
|
|
395
335
|
return {
|
|
396
336
|
id: provider.id,
|
|
397
337
|
name: provider.name,
|
|
398
|
-
connectionType:
|
|
399
|
-
apiKey:
|
|
400
|
-
modelId:
|
|
401
|
-
modelName:
|
|
338
|
+
connectionType: pc.connectionType,
|
|
339
|
+
apiKey: pc.apiKey || null,
|
|
340
|
+
modelId: pc.modelId || null,
|
|
341
|
+
modelName: pc.modelName || null
|
|
402
342
|
};
|
|
403
343
|
}
|
|
404
344
|
}
|
|
405
345
|
return null;
|
|
406
346
|
};
|
|
407
347
|
|
|
408
|
-
/**
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
*/
|
|
412
|
-
const
|
|
413
|
-
const
|
|
414
|
-
|
|
348
|
+
/** Count active AI agents */
|
|
349
|
+
const getActiveAgentCount = () => getActiveProvider() ? 1 : 0;
|
|
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
|
+
}
|
|
415
409
|
};
|
|
416
410
|
|
|
417
|
-
/**
|
|
418
|
-
* Main AI Agents menu
|
|
419
|
-
*/
|
|
411
|
+
/** Main AI Agents menu */
|
|
420
412
|
const aiAgentsMenu = async () => {
|
|
421
413
|
let config = loadConfig();
|
|
422
414
|
const boxWidth = getLogoWidth();
|
|
423
415
|
|
|
424
416
|
while (true) {
|
|
425
417
|
console.clear();
|
|
426
|
-
|
|
418
|
+
const cliproxyUrl = getCliProxyUrl();
|
|
419
|
+
drawProvidersTable(AI_PROVIDERS, config, boxWidth, cliproxyUrl);
|
|
427
420
|
|
|
428
|
-
const input = await prompts.textInput(chalk.cyan('Select
|
|
421
|
+
const input = await prompts.textInput(chalk.cyan('Select (1-8/C/B): '));
|
|
429
422
|
const choice = (input || '').toLowerCase().trim();
|
|
430
423
|
|
|
431
|
-
if (choice === 'b' || choice === '')
|
|
432
|
-
|
|
424
|
+
if (choice === 'b' || choice === '') break;
|
|
425
|
+
|
|
426
|
+
if (choice === 'c') {
|
|
427
|
+
await configureCliProxyUrl();
|
|
428
|
+
continue;
|
|
433
429
|
}
|
|
434
430
|
|
|
435
431
|
const num = parseInt(choice);
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLIProxy Service
|
|
3
|
+
*
|
|
4
|
+
* Connects to CLIProxyAPI for AI provider access
|
|
5
|
+
* via paid plans (Claude Pro, ChatGPT Plus, etc.)
|
|
6
|
+
*
|
|
7
|
+
* Supports both local (localhost:8317) and remote connections.
|
|
8
|
+
* Docs: https://help.router-for.me
|
|
9
|
+
*/
|
|
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');
|
|
16
|
+
|
|
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
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Make HTTP request to CLIProxy
|
|
62
|
+
* @param {string} path - API path
|
|
63
|
+
* @param {string} method - HTTP method
|
|
64
|
+
* @param {Object} headers - Request headers
|
|
65
|
+
* @param {number} timeout - Timeout in ms (default 60000 per RULES.md #15)
|
|
66
|
+
* @param {string} baseUrl - Optional base URL override
|
|
67
|
+
* @returns {Promise<Object>} { success, data, error }
|
|
68
|
+
*/
|
|
69
|
+
const fetchCliProxy = (path, method = 'GET', headers = {}, timeout = 60000, baseUrl = null) => {
|
|
70
|
+
return new Promise((resolve) => {
|
|
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
|
+
|
|
76
|
+
const options = {
|
|
77
|
+
hostname: url.hostname,
|
|
78
|
+
port: url.port || (isHttps ? 443 : 8317),
|
|
79
|
+
path: url.pathname + url.search,
|
|
80
|
+
method,
|
|
81
|
+
headers: {
|
|
82
|
+
'Content-Type': 'application/json',
|
|
83
|
+
...headers
|
|
84
|
+
},
|
|
85
|
+
timeout,
|
|
86
|
+
rejectUnauthorized: false // Allow self-signed certs for remote
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const req = httpModule.request(options, (res) => {
|
|
90
|
+
let data = '';
|
|
91
|
+
res.on('data', chunk => data += chunk);
|
|
92
|
+
res.on('end', () => {
|
|
93
|
+
try {
|
|
94
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
95
|
+
resolve({ success: true, data: JSON.parse(data) });
|
|
96
|
+
} else {
|
|
97
|
+
resolve({ success: false, error: `HTTP ${res.statusCode}`, data: null });
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
resolve({ success: false, error: 'Invalid JSON response', data: null });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
req.on('error', (error) => {
|
|
106
|
+
if (error.code === 'ECONNREFUSED') {
|
|
107
|
+
resolve({ success: false, error: 'CLIProxy not reachable', data: null });
|
|
108
|
+
} else {
|
|
109
|
+
resolve({ success: false, error: error.message, data: null });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
req.on('timeout', () => {
|
|
114
|
+
req.destroy();
|
|
115
|
+
resolve({ success: false, error: 'Request timeout', data: null });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
req.end();
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
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 }
|
|
126
|
+
*/
|
|
127
|
+
const isCliProxyRunning = async (url = null) => {
|
|
128
|
+
const testUrl = url || getCliProxyUrl();
|
|
129
|
+
const result = await fetchCliProxy('/v1/models', 'GET', {}, 5000, testUrl);
|
|
130
|
+
return {
|
|
131
|
+
running: result.success,
|
|
132
|
+
error: result.success ? null : result.error,
|
|
133
|
+
url: testUrl
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Fetch available models from CLIProxy
|
|
139
|
+
* @returns {Promise<Object>} { success, models, error }
|
|
140
|
+
*/
|
|
141
|
+
const fetchModelsFromCliProxy = async () => {
|
|
142
|
+
const result = await fetchCliProxy('/v1/models');
|
|
143
|
+
|
|
144
|
+
if (!result.success) {
|
|
145
|
+
return { success: false, models: [], error: result.error };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Parse OpenAI-compatible format: { data: [{ id, ... }] }
|
|
149
|
+
const data = result.data;
|
|
150
|
+
if (!data || !data.data || !Array.isArray(data.data)) {
|
|
151
|
+
return { success: false, models: [], error: 'Invalid response format' };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const models = data.data
|
|
155
|
+
.filter(m => m.id)
|
|
156
|
+
.map(m => ({
|
|
157
|
+
id: m.id,
|
|
158
|
+
name: m.id
|
|
159
|
+
}));
|
|
160
|
+
|
|
161
|
+
if (models.length === 0) {
|
|
162
|
+
return { success: false, models: [], error: 'No models available' };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { success: true, models, error: null };
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get OAuth URL for a provider
|
|
170
|
+
* @param {string} providerId - Provider ID (anthropic, openai, google, etc.)
|
|
171
|
+
* @returns {Promise<Object>} { success, url, state, error }
|
|
172
|
+
*/
|
|
173
|
+
const getOAuthUrl = async (providerId) => {
|
|
174
|
+
// Map HQX provider IDs to CLIProxy endpoints
|
|
175
|
+
const oauthEndpoints = {
|
|
176
|
+
anthropic: '/v0/management/anthropic-auth-url',
|
|
177
|
+
openai: '/v0/management/codex-auth-url',
|
|
178
|
+
google: '/v0/management/gemini-cli-auth-url',
|
|
179
|
+
// Others may not have OAuth support in CLIProxy
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const endpoint = oauthEndpoints[providerId];
|
|
183
|
+
if (!endpoint) {
|
|
184
|
+
return { success: false, url: null, state: null, error: 'OAuth not supported for this provider' };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const result = await fetchCliProxy(endpoint);
|
|
188
|
+
|
|
189
|
+
if (!result.success) {
|
|
190
|
+
return { success: false, url: null, state: null, error: result.error };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const data = result.data;
|
|
194
|
+
if (!data || !data.url) {
|
|
195
|
+
return { success: false, url: null, state: null, error: 'Invalid OAuth response' };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
success: true,
|
|
200
|
+
url: data.url,
|
|
201
|
+
state: data.state || null,
|
|
202
|
+
error: null
|
|
203
|
+
};
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Check OAuth status
|
|
208
|
+
* @param {string} state - OAuth state from getOAuthUrl
|
|
209
|
+
* @returns {Promise<Object>} { success, status, error }
|
|
210
|
+
*/
|
|
211
|
+
const checkOAuthStatus = async (state) => {
|
|
212
|
+
const result = await fetchCliProxy(`/v0/management/get-auth-status?state=${encodeURIComponent(state)}`);
|
|
213
|
+
|
|
214
|
+
if (!result.success) {
|
|
215
|
+
return { success: false, status: null, error: result.error };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const data = result.data;
|
|
219
|
+
// status can be: "wait", "ok", "error"
|
|
220
|
+
return {
|
|
221
|
+
success: true,
|
|
222
|
+
status: data.status || 'unknown',
|
|
223
|
+
error: data.error || null
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get CLIProxy auth files (connected accounts)
|
|
229
|
+
* @returns {Promise<Object>} { success, files, error }
|
|
230
|
+
*/
|
|
231
|
+
const getAuthFiles = async () => {
|
|
232
|
+
const result = await fetchCliProxy('/v0/management/auth-files');
|
|
233
|
+
|
|
234
|
+
if (!result.success) {
|
|
235
|
+
return { success: false, files: [], error: result.error };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
success: true,
|
|
240
|
+
files: result.data?.files || [],
|
|
241
|
+
error: null
|
|
242
|
+
};
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
module.exports = {
|
|
246
|
+
DEFAULT_CLIPROXY_URL,
|
|
247
|
+
getCliProxyUrl,
|
|
248
|
+
setCliProxyUrl,
|
|
249
|
+
isCliProxyRunning,
|
|
250
|
+
fetchModelsFromCliProxy,
|
|
251
|
+
getOAuthUrl,
|
|
252
|
+
checkOAuthStatus,
|
|
253
|
+
getAuthFiles,
|
|
254
|
+
fetchCliProxy
|
|
255
|
+
};
|