hedgequantx 2.7.22 → 2.7.24
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 +2 -1
- package/src/pages/ai-agents-ui.js +28 -2
- package/src/pages/ai-agents.js +88 -53
- package/src/services/cliproxy/index.js +184 -0
- package/src/services/cliproxy/manager.js +417 -0
- package/src/services/cliproxy.js +0 -199
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hedgequantx",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.24",
|
|
4
4
|
"description": "HedgeQuantX - Prop Futures Trading CLI",
|
|
5
5
|
"main": "src/app.js",
|
|
6
6
|
"bin": {
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"inquirer": "^8.2.6",
|
|
65
65
|
"ora": "^5.4.1",
|
|
66
66
|
"protobufjs": "^8.0.0",
|
|
67
|
+
"tar": "^7.5.2",
|
|
67
68
|
"uuid": "^9.0.1",
|
|
68
69
|
"ws": "^8.18.3"
|
|
69
70
|
},
|
|
@@ -60,14 +60,40 @@ const draw2ColTable = (title, titleColor, items, backText, W) => {
|
|
|
60
60
|
* @param {Array} providers - List of AI providers
|
|
61
61
|
* @param {Object} config - Current config
|
|
62
62
|
* @param {number} boxWidth - Box width
|
|
63
|
+
* @param {string} cliproxyUrl - Current CLIProxy URL (optional)
|
|
63
64
|
*/
|
|
64
|
-
const drawProvidersTable = (providers, config, boxWidth) => {
|
|
65
|
+
const drawProvidersTable = (providers, config, boxWidth, cliproxyUrl = null) => {
|
|
65
66
|
const W = boxWidth - 2;
|
|
67
|
+
|
|
68
|
+
console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
|
|
69
|
+
console.log(chalk.cyan('║') + chalk.yellow.bold(centerText('AI AGENTS CONFIGURATION', W)) + chalk.cyan('║'));
|
|
70
|
+
|
|
71
|
+
// Show CLIProxy URL if provided
|
|
72
|
+
if (cliproxyUrl) {
|
|
73
|
+
console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
|
|
74
|
+
const proxyText = chalk.gray('CLIProxy: ') + chalk.cyan(cliproxyUrl);
|
|
75
|
+
console.log(chalk.cyan('║') + centerText(proxyText, W) + chalk.cyan('║'));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
79
|
+
|
|
66
80
|
const items = providers.map((p, i) => {
|
|
67
81
|
const status = config.providers[p.id]?.active ? chalk.green(' ●') : '';
|
|
68
82
|
return chalk.cyan(`[${i + 1}]`) + ' ' + chalk[p.color](p.name) + status;
|
|
69
83
|
});
|
|
70
|
-
|
|
84
|
+
|
|
85
|
+
const rows = Math.ceil(items.length / 2);
|
|
86
|
+
for (let row = 0; row < rows; row++) {
|
|
87
|
+
const left = items[row];
|
|
88
|
+
const right = items[row + rows];
|
|
89
|
+
draw2ColRow(left || '', right || '', W);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
|
|
93
|
+
console.log(chalk.cyan('║') + chalk.gray(centerText('[S] CLIProxy Status', W)) + chalk.cyan('║'));
|
|
94
|
+
console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
|
|
95
|
+
console.log(chalk.cyan('║') + chalk.red(centerText('[B] Back to Menu', W)) + chalk.cyan('║'));
|
|
96
|
+
console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
|
|
71
97
|
};
|
|
72
98
|
|
|
73
99
|
/**
|
package/src/pages/ai-agents.js
CHANGED
|
@@ -15,7 +15,7 @@ const { getLogoWidth } = require('../ui');
|
|
|
15
15
|
const { prompts } = require('../utils');
|
|
16
16
|
const { fetchModelsFromApi } = require('./ai-models');
|
|
17
17
|
const { drawProvidersTable, drawModelsTable, drawProviderWindow } = require('./ai-agents-ui');
|
|
18
|
-
const
|
|
18
|
+
const cliproxy = require('../services/cliproxy');
|
|
19
19
|
|
|
20
20
|
// Config file path
|
|
21
21
|
const CONFIG_DIR = path.join(os.homedir(), '.hqx');
|
|
@@ -98,31 +98,53 @@ const activateProvider = (config, providerId, data) => {
|
|
|
98
98
|
Object.assign(config.providers[providerId], data, { active: true, configuredAt: new Date().toISOString() });
|
|
99
99
|
};
|
|
100
100
|
|
|
101
|
-
/** Handle CLIProxy connection */
|
|
101
|
+
/** Handle CLIProxy connection (with auto-install) */
|
|
102
102
|
const handleCliProxyConnection = async (provider, config, boxWidth) => {
|
|
103
103
|
console.log();
|
|
104
|
-
const spinner = ora({ text: 'Checking CLIProxy status...', color: 'yellow' }).start();
|
|
105
|
-
const proxyStatus = await isCliProxyRunning();
|
|
106
104
|
|
|
107
|
-
if
|
|
108
|
-
|
|
109
|
-
console.log(chalk.yellow('
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
105
|
+
// Check if CLIProxyAPI is installed
|
|
106
|
+
if (!cliproxy.isInstalled()) {
|
|
107
|
+
console.log(chalk.yellow(' CLIProxyAPI not installed. Installing...'));
|
|
108
|
+
const spinner = ora({ text: 'Downloading CLIProxyAPI...', color: 'yellow' }).start();
|
|
109
|
+
|
|
110
|
+
const installResult = await cliproxy.install((msg, percent) => {
|
|
111
|
+
spinner.text = `${msg} ${percent}%`;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (!installResult.success) {
|
|
115
|
+
spinner.fail(`Installation failed: ${installResult.error}`);
|
|
116
|
+
await prompts.waitForEnter();
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
spinner.succeed('CLIProxyAPI installed');
|
|
113
120
|
}
|
|
114
121
|
|
|
115
|
-
|
|
116
|
-
|
|
122
|
+
// Check if running, start if not
|
|
123
|
+
let status = await cliproxy.isRunning();
|
|
124
|
+
if (!status.running) {
|
|
125
|
+
const spinner = ora({ text: 'Starting CLIProxyAPI...', color: 'yellow' }).start();
|
|
126
|
+
const startResult = await cliproxy.start();
|
|
127
|
+
|
|
128
|
+
if (!startResult.success) {
|
|
129
|
+
spinner.fail(`Failed to start: ${startResult.error}`);
|
|
130
|
+
await prompts.waitForEnter();
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
spinner.succeed('CLIProxyAPI started');
|
|
134
|
+
} else {
|
|
135
|
+
console.log(chalk.green(' ✓ CLIProxyAPI is running'));
|
|
136
|
+
}
|
|
117
137
|
|
|
118
|
-
if
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
138
|
+
// Check if provider supports OAuth
|
|
139
|
+
const oauthProviders = ['anthropic', 'openai', 'google', 'qwen'];
|
|
140
|
+
if (!oauthProviders.includes(provider.id)) {
|
|
141
|
+
// Try to fetch models directly
|
|
142
|
+
console.log(chalk.gray(` Checking available models for ${provider.name}...`));
|
|
143
|
+
const modelsResult = await cliproxy.fetchProviderModels(provider.id);
|
|
122
144
|
|
|
123
145
|
if (!modelsResult.success || modelsResult.models.length === 0) {
|
|
124
|
-
console.log(chalk.red(` No models available
|
|
125
|
-
console.log(chalk.gray(
|
|
146
|
+
console.log(chalk.red(` No models available for ${provider.name}`));
|
|
147
|
+
console.log(chalk.gray(' This provider may require API key connection.'));
|
|
126
148
|
await prompts.waitForEnter();
|
|
127
149
|
return false;
|
|
128
150
|
}
|
|
@@ -144,42 +166,25 @@ const handleCliProxyConnection = async (provider, config, boxWidth) => {
|
|
|
144
166
|
return true;
|
|
145
167
|
}
|
|
146
168
|
|
|
147
|
-
// OAuth flow
|
|
148
|
-
console.log(chalk.cyan(
|
|
149
|
-
|
|
150
|
-
console.log(chalk.gray(' Waiting for authentication... (Press Enter to cancel)'));
|
|
151
|
-
|
|
152
|
-
let authenticated = false;
|
|
153
|
-
const maxWait = 120000, pollInterval = 3000;
|
|
154
|
-
let waited = 0;
|
|
155
|
-
|
|
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
|
-
})();
|
|
169
|
+
// OAuth flow - get login URL
|
|
170
|
+
console.log(chalk.cyan(`\n Starting OAuth login for ${provider.name}...`));
|
|
171
|
+
const loginResult = await cliproxy.getLoginUrl(provider.id);
|
|
171
172
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (!authenticated) {
|
|
175
|
-
console.log(chalk.yellow(' Authentication cancelled or timed out.'));
|
|
173
|
+
if (!loginResult.success) {
|
|
174
|
+
console.log(chalk.red(` OAuth error: ${loginResult.error}`));
|
|
176
175
|
await prompts.waitForEnter();
|
|
177
176
|
return false;
|
|
178
177
|
}
|
|
179
178
|
|
|
180
|
-
console.log(chalk.
|
|
179
|
+
console.log(chalk.cyan('\n Open this URL in your browser to authenticate:\n'));
|
|
180
|
+
console.log(chalk.yellow(` ${loginResult.url}\n`));
|
|
181
|
+
console.log(chalk.gray(' After authenticating, press Enter to continue...'));
|
|
182
|
+
|
|
183
|
+
await prompts.waitForEnter();
|
|
184
|
+
|
|
185
|
+
// Try to fetch models after auth
|
|
186
|
+
const modelsResult = await cliproxy.fetchProviderModels(provider.id);
|
|
181
187
|
|
|
182
|
-
const modelsResult = await fetchModelsFromCliProxy();
|
|
183
188
|
if (modelsResult.success && modelsResult.models.length > 0) {
|
|
184
189
|
const selectedModel = await selectModelFromList(provider, modelsResult.models, boxWidth);
|
|
185
190
|
if (selectedModel) {
|
|
@@ -189,17 +194,20 @@ const handleCliProxyConnection = async (provider, config, boxWidth) => {
|
|
|
189
194
|
modelName: selectedModel.name
|
|
190
195
|
});
|
|
191
196
|
if (saveConfig(config)) {
|
|
192
|
-
console.log(chalk.green(`\n ✓ ${provider.name} connected via
|
|
197
|
+
console.log(chalk.green(`\n ✓ ${provider.name} connected via Paid Plan.`));
|
|
193
198
|
console.log(chalk.cyan(` Model: ${selectedModel.name}`));
|
|
194
199
|
}
|
|
195
200
|
}
|
|
196
201
|
} else {
|
|
202
|
+
// No models but auth might have worked
|
|
197
203
|
activateProvider(config, provider.id, {
|
|
198
204
|
connectionType: 'cliproxy',
|
|
199
205
|
modelId: null,
|
|
200
|
-
modelName: '
|
|
206
|
+
modelName: 'Auto'
|
|
201
207
|
});
|
|
202
|
-
if (saveConfig(config))
|
|
208
|
+
if (saveConfig(config)) {
|
|
209
|
+
console.log(chalk.green(`\n ✓ ${provider.name} connected via Paid Plan.`));
|
|
210
|
+
}
|
|
203
211
|
}
|
|
204
212
|
|
|
205
213
|
await prompts.waitForEnter();
|
|
@@ -303,6 +311,26 @@ const getActiveProvider = () => {
|
|
|
303
311
|
/** Count active AI agents */
|
|
304
312
|
const getActiveAgentCount = () => getActiveProvider() ? 1 : 0;
|
|
305
313
|
|
|
314
|
+
/** Show CLIProxy status */
|
|
315
|
+
const showCliProxyStatus = async () => {
|
|
316
|
+
console.clear();
|
|
317
|
+
console.log(chalk.yellow('\n CLIProxyAPI Status\n'));
|
|
318
|
+
|
|
319
|
+
const installed = cliproxy.isInstalled();
|
|
320
|
+
console.log(chalk.gray(' Installed: ') + (installed ? chalk.green('Yes') : chalk.red('No')));
|
|
321
|
+
|
|
322
|
+
if (installed) {
|
|
323
|
+
const status = await cliproxy.isRunning();
|
|
324
|
+
console.log(chalk.gray(' Running: ') + (status.running ? chalk.green('Yes') : chalk.red('No')));
|
|
325
|
+
console.log(chalk.gray(' Version: ') + chalk.cyan(cliproxy.CLIPROXY_VERSION));
|
|
326
|
+
console.log(chalk.gray(' Port: ') + chalk.cyan(cliproxy.DEFAULT_PORT));
|
|
327
|
+
console.log(chalk.gray(' Install dir: ') + chalk.cyan(cliproxy.INSTALL_DIR));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
console.log();
|
|
331
|
+
await prompts.waitForEnter();
|
|
332
|
+
};
|
|
333
|
+
|
|
306
334
|
/** Main AI Agents menu */
|
|
307
335
|
const aiAgentsMenu = async () => {
|
|
308
336
|
let config = loadConfig();
|
|
@@ -310,13 +338,20 @@ const aiAgentsMenu = async () => {
|
|
|
310
338
|
|
|
311
339
|
while (true) {
|
|
312
340
|
console.clear();
|
|
313
|
-
|
|
341
|
+
const status = await cliproxy.isRunning();
|
|
342
|
+
const statusText = status.running ? `localhost:${cliproxy.DEFAULT_PORT}` : 'Not running';
|
|
343
|
+
drawProvidersTable(AI_PROVIDERS, config, boxWidth, statusText);
|
|
314
344
|
|
|
315
|
-
const input = await prompts.textInput(chalk.cyan('Select
|
|
345
|
+
const input = await prompts.textInput(chalk.cyan('Select (1-8/S/B): '));
|
|
316
346
|
const choice = (input || '').toLowerCase().trim();
|
|
317
347
|
|
|
318
348
|
if (choice === 'b' || choice === '') break;
|
|
319
349
|
|
|
350
|
+
if (choice === 's') {
|
|
351
|
+
await showCliProxyStatus();
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
|
|
320
355
|
const num = parseInt(choice);
|
|
321
356
|
if (!isNaN(num) && num >= 1 && num <= AI_PROVIDERS.length) {
|
|
322
357
|
config = await handleProviderConfig(AI_PROVIDERS[num - 1], config);
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLIProxy Service
|
|
3
|
+
*
|
|
4
|
+
* Provides OAuth connections to paid AI plans (Claude Pro, ChatGPT Plus, etc.)
|
|
5
|
+
* via the embedded CLIProxyAPI binary.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const http = require('http');
|
|
9
|
+
const manager = require('./manager');
|
|
10
|
+
|
|
11
|
+
// Re-export manager functions
|
|
12
|
+
const {
|
|
13
|
+
CLIPROXY_VERSION,
|
|
14
|
+
INSTALL_DIR,
|
|
15
|
+
AUTH_DIR,
|
|
16
|
+
DEFAULT_PORT,
|
|
17
|
+
isInstalled,
|
|
18
|
+
install,
|
|
19
|
+
isRunning,
|
|
20
|
+
start,
|
|
21
|
+
stop,
|
|
22
|
+
ensureRunning,
|
|
23
|
+
getLoginUrl
|
|
24
|
+
} = manager;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Make HTTP request to local CLIProxyAPI
|
|
28
|
+
* @param {string} path - API path
|
|
29
|
+
* @param {string} method - HTTP method
|
|
30
|
+
* @param {Object} body - Request body (optional)
|
|
31
|
+
* @param {number} timeout - Timeout in ms (default 60000 per RULES.md #15)
|
|
32
|
+
* @returns {Promise<Object>} { success, data, error }
|
|
33
|
+
*/
|
|
34
|
+
const fetchLocal = (path, method = 'GET', body = null, timeout = 60000) => {
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
const options = {
|
|
37
|
+
hostname: 'localhost',
|
|
38
|
+
port: DEFAULT_PORT,
|
|
39
|
+
path,
|
|
40
|
+
method,
|
|
41
|
+
headers: { 'Content-Type': 'application/json' },
|
|
42
|
+
timeout
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const req = http.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
|
+
const parsed = data ? JSON.parse(data) : {};
|
|
52
|
+
resolve({ success: true, data: parsed, error: null });
|
|
53
|
+
} else {
|
|
54
|
+
resolve({ success: false, error: `HTTP ${res.statusCode}`, data: null });
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
resolve({ success: false, error: 'Invalid JSON response', data: null });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
req.on('error', (error) => {
|
|
63
|
+
if (error.code === 'ECONNREFUSED') {
|
|
64
|
+
resolve({ success: false, error: 'CLIProxyAPI not running', data: null });
|
|
65
|
+
} else {
|
|
66
|
+
resolve({ success: false, error: error.message, data: null });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
req.on('timeout', () => {
|
|
71
|
+
req.destroy();
|
|
72
|
+
resolve({ success: false, error: 'Request timeout', data: null });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (body) {
|
|
76
|
+
req.write(JSON.stringify(body));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
req.end();
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Fetch available models from CLIProxyAPI
|
|
85
|
+
* @returns {Promise<Object>} { success, models, error }
|
|
86
|
+
*/
|
|
87
|
+
const fetchModels = async () => {
|
|
88
|
+
const result = await fetchLocal('/v1/models');
|
|
89
|
+
|
|
90
|
+
if (!result.success) {
|
|
91
|
+
return { success: false, models: [], error: result.error };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const data = result.data;
|
|
95
|
+
if (!data || !data.data || !Array.isArray(data.data)) {
|
|
96
|
+
return { success: false, models: [], error: 'Invalid response format' };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const models = data.data
|
|
100
|
+
.filter(m => m.id)
|
|
101
|
+
.map(m => ({ id: m.id, name: m.id }));
|
|
102
|
+
|
|
103
|
+
if (models.length === 0) {
|
|
104
|
+
return { success: false, models: [], error: 'No models available' };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { success: true, models, error: null };
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get provider-specific models
|
|
112
|
+
* @param {string} providerId - Provider ID
|
|
113
|
+
* @returns {Promise<Object>} { success, models, error }
|
|
114
|
+
*/
|
|
115
|
+
const fetchProviderModels = async (providerId) => {
|
|
116
|
+
const result = await fetchModels();
|
|
117
|
+
if (!result.success) return result;
|
|
118
|
+
|
|
119
|
+
// Filter by provider prefix
|
|
120
|
+
const prefixMap = {
|
|
121
|
+
anthropic: 'claude',
|
|
122
|
+
openai: 'gpt',
|
|
123
|
+
google: 'gemini',
|
|
124
|
+
qwen: 'qwen'
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const prefix = prefixMap[providerId];
|
|
128
|
+
if (!prefix) return result;
|
|
129
|
+
|
|
130
|
+
const filtered = result.models.filter(m =>
|
|
131
|
+
m.id.toLowerCase().includes(prefix)
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
success: true,
|
|
136
|
+
models: filtered.length > 0 ? filtered : result.models,
|
|
137
|
+
error: null
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Chat completion request
|
|
143
|
+
* @param {string} model - Model ID
|
|
144
|
+
* @param {Array} messages - Chat messages
|
|
145
|
+
* @param {Object} options - Additional options
|
|
146
|
+
* @returns {Promise<Object>} { success, response, error }
|
|
147
|
+
*/
|
|
148
|
+
const chatCompletion = async (model, messages, options = {}) => {
|
|
149
|
+
const body = {
|
|
150
|
+
model,
|
|
151
|
+
messages,
|
|
152
|
+
stream: false,
|
|
153
|
+
...options
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const result = await fetchLocal('/v1/chat/completions', 'POST', body);
|
|
157
|
+
|
|
158
|
+
if (!result.success) {
|
|
159
|
+
return { success: false, response: null, error: result.error };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { success: true, response: result.data, error: null };
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
module.exports = {
|
|
166
|
+
// Manager
|
|
167
|
+
CLIPROXY_VERSION,
|
|
168
|
+
INSTALL_DIR,
|
|
169
|
+
AUTH_DIR,
|
|
170
|
+
DEFAULT_PORT,
|
|
171
|
+
isInstalled,
|
|
172
|
+
install,
|
|
173
|
+
isRunning,
|
|
174
|
+
start,
|
|
175
|
+
stop,
|
|
176
|
+
ensureRunning,
|
|
177
|
+
getLoginUrl,
|
|
178
|
+
|
|
179
|
+
// API
|
|
180
|
+
fetchLocal,
|
|
181
|
+
fetchModels,
|
|
182
|
+
fetchProviderModels,
|
|
183
|
+
chatCompletion
|
|
184
|
+
};
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLIProxyAPI Manager
|
|
3
|
+
*
|
|
4
|
+
* Downloads, installs and manages CLIProxyAPI binary for OAuth connections
|
|
5
|
+
* to paid AI plans (Claude Pro, ChatGPT Plus, Gemini, etc.)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const https = require('https');
|
|
12
|
+
const http = require('http');
|
|
13
|
+
const { spawn } = require('child_process');
|
|
14
|
+
const { createGunzip } = require('zlib');
|
|
15
|
+
const tar = require('tar');
|
|
16
|
+
|
|
17
|
+
// CLIProxyAPI version and download URLs
|
|
18
|
+
const CLIPROXY_VERSION = '6.6.88';
|
|
19
|
+
const GITHUB_RELEASE_BASE = 'https://github.com/router-for-me/CLIProxyAPI/releases/download';
|
|
20
|
+
|
|
21
|
+
// Installation directory
|
|
22
|
+
const INSTALL_DIR = path.join(os.homedir(), '.hqx', 'cliproxy');
|
|
23
|
+
const BINARY_NAME = process.platform === 'win32' ? 'cli-proxy-api.exe' : 'cli-proxy-api';
|
|
24
|
+
const BINARY_PATH = path.join(INSTALL_DIR, BINARY_NAME);
|
|
25
|
+
const PID_FILE = path.join(INSTALL_DIR, 'cliproxy.pid');
|
|
26
|
+
const AUTH_DIR = path.join(INSTALL_DIR, 'auths');
|
|
27
|
+
|
|
28
|
+
// Default port
|
|
29
|
+
const DEFAULT_PORT = 8317;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get download URL for current platform
|
|
33
|
+
* @returns {Object} { url, filename } or null if unsupported
|
|
34
|
+
*/
|
|
35
|
+
const getDownloadUrl = () => {
|
|
36
|
+
const platform = process.platform;
|
|
37
|
+
const arch = process.arch;
|
|
38
|
+
|
|
39
|
+
let osName, archName, ext;
|
|
40
|
+
|
|
41
|
+
if (platform === 'darwin') {
|
|
42
|
+
osName = 'darwin';
|
|
43
|
+
ext = 'tar.gz';
|
|
44
|
+
} else if (platform === 'linux') {
|
|
45
|
+
osName = 'linux';
|
|
46
|
+
ext = 'tar.gz';
|
|
47
|
+
} else if (platform === 'win32') {
|
|
48
|
+
osName = 'windows';
|
|
49
|
+
ext = 'zip';
|
|
50
|
+
} else {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (arch === 'x64' || arch === 'amd64') {
|
|
55
|
+
archName = 'amd64';
|
|
56
|
+
} else if (arch === 'arm64') {
|
|
57
|
+
archName = 'arm64';
|
|
58
|
+
} else {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const filename = `CLIProxyAPI_${CLIPROXY_VERSION}_${osName}_${archName}.${ext}`;
|
|
63
|
+
const url = `${GITHUB_RELEASE_BASE}/v${CLIPROXY_VERSION}/${filename}`;
|
|
64
|
+
|
|
65
|
+
return { url, filename, ext };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if CLIProxyAPI is installed
|
|
70
|
+
* @returns {boolean}
|
|
71
|
+
*/
|
|
72
|
+
const isInstalled = () => {
|
|
73
|
+
return fs.existsSync(BINARY_PATH);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Download file from URL
|
|
78
|
+
* @param {string} url - URL to download
|
|
79
|
+
* @param {string} destPath - Destination path
|
|
80
|
+
* @param {Function} onProgress - Progress callback (percent)
|
|
81
|
+
* @returns {Promise<boolean>}
|
|
82
|
+
*/
|
|
83
|
+
const downloadFile = (url, destPath, onProgress = null) => {
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
const file = fs.createWriteStream(destPath);
|
|
86
|
+
|
|
87
|
+
const request = (url.startsWith('https') ? https : http).get(url, (response) => {
|
|
88
|
+
// Handle redirects
|
|
89
|
+
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
90
|
+
file.close();
|
|
91
|
+
fs.unlinkSync(destPath);
|
|
92
|
+
return downloadFile(response.headers.location, destPath, onProgress)
|
|
93
|
+
.then(resolve)
|
|
94
|
+
.catch(reject);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (response.statusCode !== 200) {
|
|
98
|
+
file.close();
|
|
99
|
+
fs.unlinkSync(destPath);
|
|
100
|
+
return reject(new Error(`HTTP ${response.statusCode}`));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const totalSize = parseInt(response.headers['content-length'], 10);
|
|
104
|
+
let downloadedSize = 0;
|
|
105
|
+
|
|
106
|
+
response.on('data', (chunk) => {
|
|
107
|
+
downloadedSize += chunk.length;
|
|
108
|
+
if (onProgress && totalSize) {
|
|
109
|
+
onProgress(Math.round((downloadedSize / totalSize) * 100));
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
response.pipe(file);
|
|
114
|
+
|
|
115
|
+
file.on('finish', () => {
|
|
116
|
+
file.close();
|
|
117
|
+
resolve(true);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
request.on('error', (err) => {
|
|
122
|
+
file.close();
|
|
123
|
+
if (fs.existsSync(destPath)) fs.unlinkSync(destPath);
|
|
124
|
+
reject(err);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
request.setTimeout(120000, () => {
|
|
128
|
+
request.destroy();
|
|
129
|
+
reject(new Error('Download timeout'));
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Extract tar.gz file
|
|
136
|
+
* @param {string} archivePath - Path to archive
|
|
137
|
+
* @param {string} destDir - Destination directory
|
|
138
|
+
* @returns {Promise<boolean>}
|
|
139
|
+
*/
|
|
140
|
+
const extractTarGz = (archivePath, destDir) => {
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
fs.createReadStream(archivePath)
|
|
143
|
+
.pipe(createGunzip())
|
|
144
|
+
.pipe(tar.extract({ cwd: destDir }))
|
|
145
|
+
.on('finish', () => resolve(true))
|
|
146
|
+
.on('error', reject);
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Extract zip file (Windows)
|
|
152
|
+
* @param {string} archivePath - Path to archive
|
|
153
|
+
* @param {string} destDir - Destination directory
|
|
154
|
+
* @returns {Promise<boolean>}
|
|
155
|
+
*/
|
|
156
|
+
const extractZip = async (archivePath, destDir) => {
|
|
157
|
+
const { execSync } = require('child_process');
|
|
158
|
+
|
|
159
|
+
if (process.platform === 'win32') {
|
|
160
|
+
// Use PowerShell on Windows
|
|
161
|
+
execSync(`powershell -Command "Expand-Archive -Path '${archivePath}' -DestinationPath '${destDir}' -Force"`, {
|
|
162
|
+
stdio: 'ignore'
|
|
163
|
+
});
|
|
164
|
+
} else {
|
|
165
|
+
// Use unzip on Unix
|
|
166
|
+
execSync(`unzip -o "${archivePath}" -d "${destDir}"`, { stdio: 'ignore' });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return true;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Install CLIProxyAPI
|
|
174
|
+
* @param {Function} onProgress - Progress callback (message, percent)
|
|
175
|
+
* @returns {Promise<Object>} { success, error }
|
|
176
|
+
*/
|
|
177
|
+
const install = async (onProgress = null) => {
|
|
178
|
+
try {
|
|
179
|
+
const download = getDownloadUrl();
|
|
180
|
+
if (!download) {
|
|
181
|
+
return { success: false, error: 'Unsupported platform' };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Create install directory
|
|
185
|
+
if (!fs.existsSync(INSTALL_DIR)) {
|
|
186
|
+
fs.mkdirSync(INSTALL_DIR, { recursive: true });
|
|
187
|
+
}
|
|
188
|
+
if (!fs.existsSync(AUTH_DIR)) {
|
|
189
|
+
fs.mkdirSync(AUTH_DIR, { recursive: true });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const archivePath = path.join(INSTALL_DIR, download.filename);
|
|
193
|
+
|
|
194
|
+
// Download
|
|
195
|
+
if (onProgress) onProgress('Downloading CLIProxyAPI...', 0);
|
|
196
|
+
await downloadFile(download.url, archivePath, (percent) => {
|
|
197
|
+
if (onProgress) onProgress('Downloading CLIProxyAPI...', percent);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Extract
|
|
201
|
+
if (onProgress) onProgress('Extracting...', 100);
|
|
202
|
+
if (download.ext === 'tar.gz') {
|
|
203
|
+
await extractTarGz(archivePath, INSTALL_DIR);
|
|
204
|
+
} else {
|
|
205
|
+
await extractZip(archivePath, INSTALL_DIR);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Clean up archive
|
|
209
|
+
if (fs.existsSync(archivePath)) {
|
|
210
|
+
fs.unlinkSync(archivePath);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Make executable on Unix
|
|
214
|
+
if (process.platform !== 'win32' && fs.existsSync(BINARY_PATH)) {
|
|
215
|
+
fs.chmodSync(BINARY_PATH, '755');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!fs.existsSync(BINARY_PATH)) {
|
|
219
|
+
return { success: false, error: 'Binary not found after extraction' };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { success: true, error: null };
|
|
223
|
+
} catch (error) {
|
|
224
|
+
return { success: false, error: error.message };
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Check if CLIProxyAPI is running
|
|
230
|
+
* @returns {Promise<Object>} { running, pid }
|
|
231
|
+
*/
|
|
232
|
+
const isRunning = async () => {
|
|
233
|
+
// Check PID file
|
|
234
|
+
if (fs.existsSync(PID_FILE)) {
|
|
235
|
+
try {
|
|
236
|
+
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim(), 10);
|
|
237
|
+
// Check if process exists
|
|
238
|
+
process.kill(pid, 0);
|
|
239
|
+
return { running: true, pid };
|
|
240
|
+
} catch (e) {
|
|
241
|
+
// Process doesn't exist, clean up PID file
|
|
242
|
+
fs.unlinkSync(PID_FILE);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Also check by trying to connect
|
|
247
|
+
return new Promise((resolve) => {
|
|
248
|
+
const req = http.get(`http://localhost:${DEFAULT_PORT}/v1/models`, (res) => {
|
|
249
|
+
resolve({ running: res.statusCode === 200, pid: null });
|
|
250
|
+
});
|
|
251
|
+
req.on('error', () => resolve({ running: false, pid: null }));
|
|
252
|
+
req.setTimeout(2000, () => {
|
|
253
|
+
req.destroy();
|
|
254
|
+
resolve({ running: false, pid: null });
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Start CLIProxyAPI
|
|
261
|
+
* @returns {Promise<Object>} { success, error, pid }
|
|
262
|
+
*/
|
|
263
|
+
const start = async () => {
|
|
264
|
+
if (!isInstalled()) {
|
|
265
|
+
return { success: false, error: 'CLIProxyAPI not installed', pid: null };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const status = await isRunning();
|
|
269
|
+
if (status.running) {
|
|
270
|
+
return { success: true, error: null, pid: status.pid };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
const args = [
|
|
275
|
+
'--port', String(DEFAULT_PORT),
|
|
276
|
+
'--auth-dir', AUTH_DIR
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
const child = spawn(BINARY_PATH, args, {
|
|
280
|
+
detached: true,
|
|
281
|
+
stdio: 'ignore',
|
|
282
|
+
cwd: INSTALL_DIR
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
child.unref();
|
|
286
|
+
|
|
287
|
+
// Save PID
|
|
288
|
+
fs.writeFileSync(PID_FILE, String(child.pid));
|
|
289
|
+
|
|
290
|
+
// Wait for startup
|
|
291
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
292
|
+
|
|
293
|
+
const runStatus = await isRunning();
|
|
294
|
+
if (runStatus.running) {
|
|
295
|
+
return { success: true, error: null, pid: child.pid };
|
|
296
|
+
} else {
|
|
297
|
+
return { success: false, error: 'Failed to start CLIProxyAPI', pid: null };
|
|
298
|
+
}
|
|
299
|
+
} catch (error) {
|
|
300
|
+
return { success: false, error: error.message, pid: null };
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Stop CLIProxyAPI
|
|
306
|
+
* @returns {Promise<Object>} { success, error }
|
|
307
|
+
*/
|
|
308
|
+
const stop = async () => {
|
|
309
|
+
const status = await isRunning();
|
|
310
|
+
if (!status.running) {
|
|
311
|
+
return { success: true, error: null };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
if (status.pid) {
|
|
316
|
+
process.kill(status.pid, 'SIGTERM');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (fs.existsSync(PID_FILE)) {
|
|
320
|
+
fs.unlinkSync(PID_FILE);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return { success: true, error: null };
|
|
324
|
+
} catch (error) {
|
|
325
|
+
return { success: false, error: error.message };
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Ensure CLIProxyAPI is installed and running
|
|
331
|
+
* @param {Function} onProgress - Progress callback
|
|
332
|
+
* @returns {Promise<Object>} { success, error }
|
|
333
|
+
*/
|
|
334
|
+
const ensureRunning = async (onProgress = null) => {
|
|
335
|
+
// Check if installed
|
|
336
|
+
if (!isInstalled()) {
|
|
337
|
+
if (onProgress) onProgress('Installing CLIProxyAPI...', 0);
|
|
338
|
+
const installResult = await install(onProgress);
|
|
339
|
+
if (!installResult.success) {
|
|
340
|
+
return installResult;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Check if running
|
|
345
|
+
const status = await isRunning();
|
|
346
|
+
if (status.running) {
|
|
347
|
+
return { success: true, error: null };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Start
|
|
351
|
+
if (onProgress) onProgress('Starting CLIProxyAPI...', 100);
|
|
352
|
+
return start();
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Get OAuth login URL for a provider
|
|
357
|
+
* @param {string} provider - Provider ID (anthropic, openai, google, etc.)
|
|
358
|
+
* @returns {Promise<Object>} { success, url, error }
|
|
359
|
+
*/
|
|
360
|
+
const getLoginUrl = async (provider) => {
|
|
361
|
+
const providerFlags = {
|
|
362
|
+
anthropic: '--claude-login',
|
|
363
|
+
openai: '--codex-login',
|
|
364
|
+
google: '--gemini-login',
|
|
365
|
+
qwen: '--qwen-login'
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const flag = providerFlags[provider];
|
|
369
|
+
if (!flag) {
|
|
370
|
+
return { success: false, url: null, error: 'Provider not supported for OAuth' };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// For headless/VPS, use --no-browser flag
|
|
374
|
+
return new Promise((resolve) => {
|
|
375
|
+
const args = [flag, '--no-browser'];
|
|
376
|
+
const child = spawn(BINARY_PATH, args, {
|
|
377
|
+
cwd: INSTALL_DIR,
|
|
378
|
+
env: { ...process.env, AUTH_DIR: AUTH_DIR }
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
let output = '';
|
|
382
|
+
|
|
383
|
+
child.stdout.on('data', (data) => {
|
|
384
|
+
output += data.toString();
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
child.stderr.on('data', (data) => {
|
|
388
|
+
output += data.toString();
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// Look for URL in output
|
|
392
|
+
setTimeout(() => {
|
|
393
|
+
const urlMatch = output.match(/https?:\/\/[^\s]+/);
|
|
394
|
+
if (urlMatch) {
|
|
395
|
+
resolve({ success: true, url: urlMatch[0], error: null });
|
|
396
|
+
} else {
|
|
397
|
+
resolve({ success: false, url: null, error: 'Could not get login URL' });
|
|
398
|
+
}
|
|
399
|
+
}, 3000);
|
|
400
|
+
});
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
module.exports = {
|
|
404
|
+
CLIPROXY_VERSION,
|
|
405
|
+
INSTALL_DIR,
|
|
406
|
+
BINARY_PATH,
|
|
407
|
+
AUTH_DIR,
|
|
408
|
+
DEFAULT_PORT,
|
|
409
|
+
getDownloadUrl,
|
|
410
|
+
isInstalled,
|
|
411
|
+
install,
|
|
412
|
+
isRunning,
|
|
413
|
+
start,
|
|
414
|
+
stop,
|
|
415
|
+
ensureRunning,
|
|
416
|
+
getLoginUrl
|
|
417
|
+
};
|
package/src/services/cliproxy.js
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
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
|
-
};
|