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