hedgequantx 2.6.160 → 2.6.162

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.
Files changed (45) hide show
  1. package/package.json +1 -1
  2. package/src/menus/ai-agent-connect.js +181 -0
  3. package/src/menus/ai-agent-models.js +219 -0
  4. package/src/menus/ai-agent-oauth.js +292 -0
  5. package/src/menus/ai-agent-ui.js +141 -0
  6. package/src/menus/ai-agent.js +88 -1489
  7. package/src/pages/algo/copy-engine.js +449 -0
  8. package/src/pages/algo/copy-trading.js +11 -543
  9. package/src/pages/algo/smart-logs-data.js +218 -0
  10. package/src/pages/algo/smart-logs.js +9 -214
  11. package/src/pages/algo/ui-constants.js +144 -0
  12. package/src/pages/algo/ui-summary.js +184 -0
  13. package/src/pages/algo/ui.js +42 -526
  14. package/src/pages/stats-calculations.js +191 -0
  15. package/src/pages/stats-ui.js +381 -0
  16. package/src/pages/stats.js +14 -507
  17. package/src/services/ai/client-analysis.js +194 -0
  18. package/src/services/ai/client-models.js +333 -0
  19. package/src/services/ai/client.js +6 -489
  20. package/src/services/ai/index.js +2 -257
  21. package/src/services/ai/proxy-install.js +249 -0
  22. package/src/services/ai/proxy-manager.js +29 -411
  23. package/src/services/ai/proxy-remote.js +161 -0
  24. package/src/services/ai/strategy-supervisor.js +10 -765
  25. package/src/services/ai/supervisor-data.js +195 -0
  26. package/src/services/ai/supervisor-optimize.js +215 -0
  27. package/src/services/ai/supervisor-sync.js +178 -0
  28. package/src/services/ai/supervisor-utils.js +158 -0
  29. package/src/services/ai/supervisor.js +50 -515
  30. package/src/services/ai/validation.js +250 -0
  31. package/src/services/hqx-server-events.js +110 -0
  32. package/src/services/hqx-server-handlers.js +217 -0
  33. package/src/services/hqx-server-latency.js +136 -0
  34. package/src/services/hqx-server.js +51 -403
  35. package/src/services/position-constants.js +28 -0
  36. package/src/services/position-manager.js +105 -554
  37. package/src/services/position-momentum.js +206 -0
  38. package/src/services/projectx/accounts.js +142 -0
  39. package/src/services/projectx/index.js +40 -289
  40. package/src/services/projectx/trading.js +180 -0
  41. package/src/services/rithmic/handlers.js +2 -208
  42. package/src/services/rithmic/index.js +32 -542
  43. package/src/services/rithmic/latency-tracker.js +182 -0
  44. package/src/services/rithmic/specs.js +146 -0
  45. package/src/services/rithmic/trade-history.js +254 -0
@@ -1,38 +1,17 @@
1
- /**
2
- * AI Agent Menu
3
- * Configure multiple AI provider connections
4
- */
5
-
1
+ /** AI Agent Menu - Configure multiple AI provider connections */
6
2
  const chalk = require('chalk');
7
- const ora = require('ora');
8
-
9
- const { getLogoWidth, drawBoxHeader, drawBoxHeaderContinue, drawBoxFooter, displayBanner } = require('../ui');
3
+ const { getLogoWidth, drawBoxHeaderContinue, drawBoxFooter, displayBanner } = require('../ui');
10
4
  const { prompts } = require('../utils');
11
5
  const aiService = require('../services/ai');
12
6
  const { getCategories, getProvidersByCategory } = require('../services/ai/providers');
13
- const oauthAnthropic = require('../services/ai/oauth-anthropic');
14
- const oauthOpenai = require('../services/ai/oauth-openai');
15
- const oauthGemini = require('../services/ai/oauth-gemini');
16
- const oauthQwen = require('../services/ai/oauth-qwen');
17
- const oauthIflow = require('../services/ai/oauth-iflow');
18
- const proxyManager = require('../services/ai/proxy-manager');
7
+ const { makeLine, make2ColRow, menuRow2, menuItem, getProviderColor, getBoxDimensions } = require('./ai-agent-ui');
8
+ const { setupOAuthConnection, getOAuthConfig } = require('./ai-agent-oauth');
9
+ const { selectModelFromList, selectModel } = require('./ai-agent-models');
10
+ const { collectCredentials, validateAndFetchModels, addConnectedAgent } = require('./ai-agent-connect');
19
11
 
20
- /**
21
- * Main AI Agent menu
22
- */
12
+ /** Main AI Agent menu */
23
13
  const aiAgentMenu = async () => {
24
- const boxWidth = getLogoWidth();
25
- const W = boxWidth - 2;
26
-
27
- const makeLine = (content, align = 'left') => {
28
- const plainLen = content.replace(/\x1b\[[0-9;]*m/g, '').length;
29
- const padding = W - plainLen;
30
- if (align === 'center') {
31
- const leftPad = Math.floor(padding / 2);
32
- return chalk.cyan('║') + ' '.repeat(leftPad) + content + ' '.repeat(padding - leftPad) + chalk.cyan('║');
33
- }
34
- return chalk.cyan('║') + ' ' + content + ' '.repeat(Math.max(0, padding - 1)) + chalk.cyan('║');
35
- };
14
+ const { boxWidth, W } = getBoxDimensions();
36
15
 
37
16
  console.clear();
38
17
  displayBanner();
@@ -43,32 +22,27 @@ const aiAgentMenu = async () => {
43
22
  const agentCount = agents.length;
44
23
 
45
24
  if (agentCount === 0) {
46
- console.log(makeLine(chalk.white('STATUS: NO AGENTS CONNECTED'), 'left'));
25
+ console.log(makeLine(W, chalk.white('STATUS: NO AGENTS CONNECTED')));
47
26
  } else {
48
- console.log(makeLine(chalk.green(`STATUS: ${agentCount} AGENT${agentCount > 1 ? 'S' : ''} CONNECTED`), 'left'));
27
+ console.log(makeLine(W, chalk.green(`STATUS: ${agentCount} AGENT${agentCount > 1 ? 'S' : ''} CONNECTED`)));
49
28
  console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
50
29
 
51
30
  // List all agents
52
31
  for (let i = 0; i < agents.length; i++) {
53
32
  const agent = agents[i];
54
- // Show ACTIVE marker (if single agent, it's always active)
55
33
  const isActive = agent.isActive || agents.length === 1;
56
34
  const activeMarker = isActive ? ' [ACTIVE]' : '';
57
- const providerColor = agent.providerId === 'anthropic' ? chalk.magenta :
58
- agent.providerId === 'openai' ? chalk.green :
59
- agent.providerId === 'openrouter' ? chalk.yellow : chalk.cyan;
35
+ const providerColor = getProviderColor(agent.providerId);
60
36
 
61
- // Calculate max lengths to fit in box
62
37
  const prefix = `[${i + 1}] `;
63
38
  const suffix = ` - ${agent.model || 'N/A'}`;
64
39
  const maxNameLen = W - prefix.length - activeMarker.length - suffix.length - 2;
65
40
 
66
- // Truncate agent name if too long
67
41
  const displayName = agent.name.length > maxNameLen
68
42
  ? agent.name.substring(0, maxNameLen - 3) + '...'
69
43
  : agent.name;
70
44
 
71
- console.log(makeLine(
45
+ console.log(makeLine(W,
72
46
  chalk.white(prefix) +
73
47
  providerColor(displayName) +
74
48
  chalk.green(activeMarker) +
@@ -79,44 +53,18 @@ const aiAgentMenu = async () => {
79
53
 
80
54
  console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
81
55
 
82
- // Menu in 2 columns
83
- const colWidth = Math.floor(W / 2);
84
-
85
- const menuRow2 = (col1, col2 = '') => {
86
- const c1Plain = col1.replace(/\x1b\[[0-9;]*m/g, '');
87
- const c2Plain = col2.replace(/\x1b\[[0-9;]*m/g, '');
88
-
89
- const pad1Left = Math.floor((colWidth - c1Plain.length) / 2);
90
- const pad1Right = colWidth - c1Plain.length - pad1Left;
91
-
92
- const col2Width = W - colWidth;
93
- const pad2Left = Math.floor((col2Width - c2Plain.length) / 2);
94
- const pad2Right = col2Width - c2Plain.length - pad2Left;
95
-
96
- const line =
97
- ' '.repeat(pad1Left) + col1 + ' '.repeat(pad1Right) +
98
- ' '.repeat(pad2Left) + col2 + ' '.repeat(pad2Right);
99
-
100
- console.log(chalk.cyan('║') + line + chalk.cyan('║'));
101
- };
102
-
103
- const menuItem = (key, label, color) => {
104
- const text = `[${key}] ${label.padEnd(14)}`;
105
- return color(text);
106
- };
107
-
108
56
  // Menu options in 2 columns
109
57
  if (agentCount > 0) {
110
58
  if (agentCount > 1) {
111
- menuRow2(menuItem('+', 'ADD AGENT', chalk.green), menuItem('S', 'SET ACTIVE', chalk.cyan));
112
- menuRow2(menuItem('M', 'CHANGE MODEL', chalk.yellow), menuItem('R', 'REMOVE AGENT', chalk.red));
113
- menuRow2(menuItem('X', 'REMOVE ALL', chalk.red), menuItem('<', 'BACK', chalk.white));
59
+ console.log(menuRow2(W, menuItem('+', 'ADD AGENT', chalk.green), menuItem('S', 'SET ACTIVE', chalk.cyan)));
60
+ console.log(menuRow2(W, menuItem('M', 'CHANGE MODEL', chalk.yellow), menuItem('R', 'REMOVE AGENT', chalk.red)));
61
+ console.log(menuRow2(W, menuItem('X', 'REMOVE ALL', chalk.red), menuItem('<', 'BACK', chalk.white)));
114
62
  } else {
115
- menuRow2(menuItem('+', 'ADD AGENT', chalk.green), menuItem('M', 'CHANGE MODEL', chalk.yellow));
116
- menuRow2(menuItem('R', 'REMOVE AGENT', chalk.red), menuItem('<', 'BACK', chalk.white));
63
+ console.log(menuRow2(W, menuItem('+', 'ADD AGENT', chalk.green), menuItem('M', 'CHANGE MODEL', chalk.yellow)));
64
+ console.log(menuRow2(W, menuItem('R', 'REMOVE AGENT', chalk.red), menuItem('<', 'BACK', chalk.white)));
117
65
  }
118
66
  } else {
119
- menuRow2(menuItem('+', 'ADD AGENT', chalk.green), menuItem('<', 'BACK', chalk.white));
67
+ console.log(menuRow2(W, menuItem('+', 'ADD AGENT', chalk.green), menuItem('<', 'BACK', chalk.white)));
120
68
  }
121
69
 
122
70
  drawBoxFooter(boxWidth);
@@ -134,19 +82,13 @@ const aiAgentMenu = async () => {
134
82
  case '+':
135
83
  return await selectCategory();
136
84
  case 's':
137
- if (agentCount > 1) {
138
- return await selectActiveAgent();
139
- }
85
+ if (agentCount > 1) return await selectActiveAgent();
140
86
  return await aiAgentMenu();
141
87
  case 'm':
142
- if (agentCount > 0) {
143
- return await selectAgentForModelChange();
144
- }
88
+ if (agentCount > 0) return await selectAgentForModelChange();
145
89
  return await aiAgentMenu();
146
90
  case 'r':
147
- if (agentCount > 0) {
148
- return await selectAgentToRemove();
149
- }
91
+ if (agentCount > 0) return await selectAgentToRemove();
150
92
  return await aiAgentMenu();
151
93
  case 'x':
152
94
  if (agentCount > 1) {
@@ -163,67 +105,34 @@ const aiAgentMenu = async () => {
163
105
  }
164
106
  };
165
107
 
166
- /**
167
- * Show agent details
168
- */
108
+ /** Show agent details */
169
109
  const showAgentDetails = async (agent) => {
170
- const boxWidth = getLogoWidth();
171
- const W = boxWidth - 2;
172
-
173
- const makeLine = (content) => {
174
- const plainLen = content.replace(/\x1b\[[0-9;]*m/g, '').length;
175
- const padding = W - plainLen;
176
- return chalk.cyan('║') + ' ' + content + ' '.repeat(Math.max(0, padding - 1)) + chalk.cyan('║');
177
- };
110
+ const { boxWidth, W } = getBoxDimensions();
178
111
 
179
112
  console.clear();
180
113
  displayBanner();
181
114
  drawBoxHeaderContinue('AGENT DETAILS', boxWidth);
182
115
 
183
- const providerColor = agent.providerId === 'anthropic' ? chalk.magenta :
184
- agent.providerId === 'openai' ? chalk.green :
185
- agent.providerId === 'openrouter' ? chalk.yellow : chalk.cyan;
116
+ const providerColor = getProviderColor(agent.providerId);
186
117
 
187
- console.log(makeLine(chalk.white('NAME: ') + providerColor(agent.name)));
188
- console.log(makeLine(chalk.white('PROVIDER: ') + chalk.white(agent.provider?.name || agent.providerId)));
189
- console.log(makeLine(chalk.white('MODEL: ') + chalk.white(agent.model || 'N/A')));
190
- console.log(makeLine(chalk.white('STATUS: ') + (agent.isActive ? chalk.green('ACTIVE') : chalk.white('STANDBY'))));
118
+ console.log(makeLine(W, chalk.white('NAME: ') + providerColor(agent.name)));
119
+ console.log(makeLine(W, chalk.white('PROVIDER: ') + chalk.white(agent.provider?.name || agent.providerId)));
120
+ console.log(makeLine(W, chalk.white('MODEL: ') + chalk.white(agent.model || 'N/A')));
121
+ console.log(makeLine(W, chalk.white('STATUS: ') + (agent.isActive ? chalk.green('ACTIVE') : chalk.white('STANDBY'))));
191
122
 
192
123
  console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
193
124
 
194
- // Menu in 2 columns
195
- const colWidth = Math.floor(W / 2);
196
-
197
- const menuRow = (col1, col2 = '') => {
198
- const c1Plain = col1.replace(/\x1b\[[0-9;]*m/g, '');
199
- const c2Plain = col2.replace(/\x1b\[[0-9;]*m/g, '');
200
-
201
- const pad1Left = Math.floor((colWidth - c1Plain.length) / 2);
202
- const pad1Right = colWidth - c1Plain.length - pad1Left;
203
-
204
- const col2Width = W - colWidth;
205
- const pad2Left = Math.floor((col2Width - c2Plain.length) / 2);
206
- const pad2Right = col2Width - c2Plain.length - pad2Left;
207
-
208
- const line =
209
- ' '.repeat(pad1Left) + col1 + ' '.repeat(pad1Right) +
210
- ' '.repeat(pad2Left) + col2 + ' '.repeat(pad2Right);
211
-
212
- console.log(chalk.cyan('║') + line + chalk.cyan('║'));
213
- };
214
-
215
125
  if (!agent.isActive) {
216
- menuRow(chalk.cyan('[A] SET AS ACTIVE'), chalk.yellow('[M] CHANGE MODEL'));
217
- menuRow(chalk.red('[R] REMOVE'), chalk.white('[<] BACK'));
126
+ console.log(menuRow2(W, chalk.cyan('[A] SET AS ACTIVE'), chalk.yellow('[M] CHANGE MODEL')));
127
+ console.log(menuRow2(W, chalk.red('[R] REMOVE'), chalk.white('[<] BACK')));
218
128
  } else {
219
- menuRow(chalk.yellow('[M] CHANGE MODEL'), chalk.red('[R] REMOVE'));
220
- menuRow(chalk.white('[<] BACK'), '');
129
+ console.log(menuRow2(W, chalk.yellow('[M] CHANGE MODEL'), chalk.red('[R] REMOVE')));
130
+ console.log(menuRow2(W, chalk.white('[<] BACK'), ''));
221
131
  }
222
132
 
223
133
  drawBoxFooter(boxWidth);
224
134
 
225
135
  const choice = await prompts.textInput(chalk.cyan('SELECT:'));
226
-
227
136
  const agentDisplayName = agent.model ? `${agent.name} (${agent.model})` : agent.name;
228
137
 
229
138
  switch ((choice || '').toLowerCase()) {
@@ -235,7 +144,7 @@ const showAgentDetails = async (agent) => {
235
144
  }
236
145
  return await aiAgentMenu();
237
146
  case 'm':
238
- return await selectModel(agent);
147
+ return await selectModel(agent, aiAgentMenu);
239
148
  case 'r':
240
149
  aiService.removeAgent(agent.id);
241
150
  console.log(chalk.yellow(`\n ${agentDisplayName} REMOVED`));
@@ -249,18 +158,9 @@ const showAgentDetails = async (agent) => {
249
158
  }
250
159
  };
251
160
 
252
- /**
253
- * Select active agent
254
- */
161
+ /** Select active agent */
255
162
  const selectActiveAgent = async () => {
256
- const boxWidth = getLogoWidth();
257
- const W = boxWidth - 2;
258
-
259
- const makeLine = (content) => {
260
- const plainLen = content.replace(/\x1b\[[0-9;]*m/g, '').length;
261
- const padding = W - plainLen;
262
- return chalk.cyan('║') + ' ' + content + ' '.repeat(Math.max(0, padding - 1)) + chalk.cyan('║');
263
- };
163
+ const { boxWidth, W } = getBoxDimensions();
264
164
 
265
165
  console.clear();
266
166
  displayBanner();
@@ -271,23 +171,18 @@ const selectActiveAgent = async () => {
271
171
  for (let i = 0; i < agents.length; i++) {
272
172
  const agent = agents[i];
273
173
  const activeMarker = agent.isActive ? chalk.yellow(' (CURRENT)') : '';
274
- const providerColor = agent.providerId === 'anthropic' ? chalk.magenta :
275
- agent.providerId === 'openai' ? chalk.green : chalk.cyan;
276
-
174
+ const providerColor = getProviderColor(agent.providerId);
277
175
  const modelDisplay = agent.model ? chalk.gray(` (${agent.model})`) : '';
278
- console.log(makeLine(
279
- chalk.white(`[${i + 1}] `) + providerColor(agent.name) + modelDisplay + activeMarker
280
- ));
176
+ console.log(makeLine(W, chalk.white(`[${i + 1}] `) + providerColor(agent.name) + modelDisplay + activeMarker));
281
177
  }
282
178
 
283
- console.log(makeLine(''));
284
- console.log(makeLine(chalk.white('[<] BACK')));
179
+ console.log(makeLine(W, ''));
180
+ console.log(makeLine(W, chalk.white('[<] BACK')));
285
181
 
286
182
  drawBoxFooter(boxWidth);
287
183
 
288
184
  const choice = await prompts.textInput(chalk.cyan('SELECT AGENT:'));
289
185
 
290
- // Empty input or < = go back
291
186
  if (!choice || choice.trim() === '' || choice === '<' || choice?.toLowerCase() === 'b') {
292
187
  return await aiAgentMenu();
293
188
  }
@@ -305,24 +200,15 @@ const selectActiveAgent = async () => {
305
200
  return await aiAgentMenu();
306
201
  };
307
202
 
308
- /**
309
- * Select agent to change model
310
- */
203
+ /** Select agent to change model */
311
204
  const selectAgentForModelChange = async () => {
312
205
  const agents = aiService.getAgents();
313
206
 
314
207
  if (agents.length === 1) {
315
- return await selectModel(agents[0]);
208
+ return await selectModel(agents[0], aiAgentMenu);
316
209
  }
317
210
 
318
- const boxWidth = getLogoWidth();
319
- const W = boxWidth - 2;
320
-
321
- const makeLine = (content) => {
322
- const plainLen = content.replace(/\x1b\[[0-9;]*m/g, '').length;
323
- const padding = W - plainLen;
324
- return chalk.cyan('║') + ' ' + content + ' '.repeat(Math.max(0, padding - 1)) + chalk.cyan('║');
325
- };
211
+ const { boxWidth, W } = getBoxDimensions();
326
212
 
327
213
  console.clear();
328
214
  displayBanner();
@@ -330,19 +216,16 @@ const selectAgentForModelChange = async () => {
330
216
 
331
217
  for (let i = 0; i < agents.length; i++) {
332
218
  const agent = agents[i];
333
- console.log(makeLine(
334
- chalk.white(`[${i + 1}] `) + chalk.cyan(agent.name) + chalk.white(` - ${agent.model}`)
335
- ));
219
+ console.log(makeLine(W, chalk.white(`[${i + 1}] `) + chalk.cyan(agent.name) + chalk.white(` - ${agent.model}`)));
336
220
  }
337
221
 
338
- console.log(makeLine(''));
339
- console.log(makeLine(chalk.white('[<] BACK')));
222
+ console.log(makeLine(W, ''));
223
+ console.log(makeLine(W, chalk.white('[<] BACK')));
340
224
 
341
225
  drawBoxFooter(boxWidth);
342
226
 
343
227
  const choice = await prompts.textInput(chalk.cyan('SELECT AGENT:'));
344
228
 
345
- // Empty input or < = go back
346
229
  if (!choice || choice.trim() === '' || choice === '<' || choice?.toLowerCase() === 'b') {
347
230
  return await aiAgentMenu();
348
231
  }
@@ -352,12 +235,10 @@ const selectAgentForModelChange = async () => {
352
235
  return await aiAgentMenu();
353
236
  }
354
237
 
355
- return await selectModel(agents[index]);
238
+ return await selectModel(agents[index], aiAgentMenu);
356
239
  };
357
240
 
358
- /**
359
- * Select agent to remove
360
- */
241
+ /** Select agent to remove */
361
242
  const selectAgentToRemove = async () => {
362
243
  const agents = aiService.getAgents();
363
244
 
@@ -368,14 +249,7 @@ const selectAgentToRemove = async () => {
368
249
  return await aiAgentMenu();
369
250
  }
370
251
 
371
- const boxWidth = getLogoWidth();
372
- const W = boxWidth - 2;
373
-
374
- const makeLine = (content) => {
375
- const plainLen = content.replace(/\x1b\[[0-9;]*m/g, '').length;
376
- const padding = W - plainLen;
377
- return chalk.cyan('║') + ' ' + content + ' '.repeat(Math.max(0, padding - 1)) + chalk.cyan('║');
378
- };
252
+ const { boxWidth, W } = getBoxDimensions();
379
253
 
380
254
  console.clear();
381
255
  displayBanner();
@@ -384,19 +258,16 @@ const selectAgentToRemove = async () => {
384
258
  for (let i = 0; i < agents.length; i++) {
385
259
  const agent = agents[i];
386
260
  const modelDisplay = agent.model ? chalk.gray(` (${agent.model})`) : '';
387
- console.log(makeLine(
388
- chalk.white(`[${i + 1}] `) + chalk.red(agent.name) + modelDisplay
389
- ));
261
+ console.log(makeLine(W, chalk.white(`[${i + 1}] `) + chalk.red(agent.name) + modelDisplay));
390
262
  }
391
263
 
392
- console.log(makeLine(''));
393
- console.log(makeLine(chalk.white('[<] BACK')));
264
+ console.log(makeLine(W, ''));
265
+ console.log(makeLine(W, chalk.white('[<] BACK')));
394
266
 
395
267
  drawBoxFooter(boxWidth);
396
268
 
397
269
  const choice = await prompts.textInput(chalk.cyan('SELECT AGENT TO REMOVE:'));
398
270
 
399
- // Empty input or < = go back
400
271
  if (!choice || choice.trim() === '' || choice === '<' || choice?.toLowerCase() === 'b') {
401
272
  return await aiAgentMenu();
402
273
  }
@@ -414,27 +285,9 @@ const selectAgentToRemove = async () => {
414
285
  return await aiAgentMenu();
415
286
  };
416
287
 
417
- /**
418
- * Select provider category
419
- */
288
+ /** Select provider category */
420
289
  const selectCategory = async () => {
421
- const boxWidth = getLogoWidth();
422
- const W = boxWidth - 2;
423
- const col1Width = Math.floor(W / 2);
424
-
425
- const makeLine = (content) => {
426
- const plainLen = content.replace(/\x1b\[[0-9;]*m/g, '').length;
427
- const padding = W - plainLen;
428
- return chalk.cyan('║') + ' ' + content + ' '.repeat(Math.max(0, padding - 1)) + chalk.cyan('║');
429
- };
430
-
431
- const make2ColRow = (left, right) => {
432
- const leftPlain = left.replace(/\x1b\[[0-9;]*m/g, '').length;
433
- const rightPlain = right.replace(/\x1b\[[0-9;]*m/g, '').length;
434
- const leftPadded = ' ' + left + ' '.repeat(Math.max(0, col1Width - leftPlain - 1));
435
- const rightPadded = right + ' '.repeat(Math.max(0, W - col1Width - rightPlain));
436
- return chalk.cyan('║') + leftPadded + rightPadded + chalk.cyan('║');
437
- };
290
+ const { boxWidth, W } = getBoxDimensions();
438
291
 
439
292
  console.clear();
440
293
  displayBanner();
@@ -442,30 +295,22 @@ const selectCategory = async () => {
442
295
 
443
296
  const categories = getCategories();
444
297
 
445
- // Display in 2 columns - Numbers in cyan, titles in yellow
446
- console.log(make2ColRow(
298
+ console.log(make2ColRow(W,
447
299
  chalk.cyan('[1]') + chalk.yellow(' UNIFIED (RECOMMENDED)'),
448
300
  chalk.cyan('[2]') + chalk.yellow(' DIRECT PROVIDERS')
449
301
  ));
450
- console.log(make2ColRow(
451
- chalk.white(' 1 API = 100+ models'),
452
- chalk.white(' Connect to each provider')
453
- ));
454
- console.log(makeLine(''));
455
- console.log(make2ColRow(
302
+ console.log(make2ColRow(W, chalk.white(' 1 API = 100+ models'), chalk.white(' Connect to each provider')));
303
+ console.log(makeLine(W, ''));
304
+ console.log(make2ColRow(W,
456
305
  chalk.cyan('[3]') + chalk.yellow(' LOCAL (FREE)'),
457
306
  chalk.cyan('[4]') + chalk.yellow(' CUSTOM')
458
307
  ));
459
- console.log(make2ColRow(
460
- chalk.white(' Run on your machine'),
461
- chalk.white(' Self-hosted solutions')
462
- ));
308
+ console.log(make2ColRow(W, chalk.white(' Run on your machine'), chalk.white(' Self-hosted solutions')));
463
309
 
464
310
  drawBoxFooter(boxWidth);
465
311
 
466
312
  const choice = await prompts.textInput(chalk.cyan('SELECT (1-4):'));
467
313
 
468
- // Empty input = go back
469
314
  if (!choice || choice.trim() === '' || choice === '<' || choice?.toLowerCase() === 'b') {
470
315
  return await aiAgentMenu();
471
316
  }
@@ -475,32 +320,14 @@ const selectCategory = async () => {
475
320
  return await aiAgentMenu();
476
321
  }
477
322
 
478
- const selectedCategory = categories[index];
479
- return await selectProvider(selectedCategory.id);
323
+ return await selectProvider(categories[index].id);
480
324
  };
481
325
 
482
- /**
483
- * Select AI provider from category
484
- */
326
+ /** Select AI provider from category */
485
327
  const selectProvider = async (categoryId) => {
486
- const boxWidth = getLogoWidth();
487
- const W = boxWidth - 2;
328
+ const { boxWidth, W } = getBoxDimensions();
488
329
  const col1Width = Math.floor(W / 2);
489
330
 
490
- const makeLine = (content) => {
491
- const plainLen = content.replace(/\x1b\[[0-9;]*m/g, '').length;
492
- const padding = W - plainLen;
493
- return chalk.cyan('║') + ' ' + content + ' '.repeat(Math.max(0, padding - 1)) + chalk.cyan('║');
494
- };
495
-
496
- const make2ColRow = (left, right) => {
497
- const leftPlain = left.replace(/\x1b\[[0-9;]*m/g, '').length;
498
- const rightPlain = right.replace(/\x1b\[[0-9;]*m/g, '').length;
499
- const leftPadded = ' ' + left + ' '.repeat(Math.max(0, col1Width - leftPlain - 1));
500
- const rightPadded = right + ' '.repeat(Math.max(0, W - col1Width - rightPlain));
501
- return chalk.cyan('║') + leftPadded + rightPadded + chalk.cyan('║');
502
- };
503
-
504
331
  console.clear();
505
332
  displayBanner();
506
333
 
@@ -511,49 +338,42 @@ const selectProvider = async (categoryId) => {
511
338
  const providers = getProvidersByCategory(categoryId);
512
339
 
513
340
  if (providers.length === 0) {
514
- console.log(makeLine(chalk.white('NO PROVIDERS IN THIS CATEGORY')));
341
+ console.log(makeLine(W, chalk.white('NO PROVIDERS IN THIS CATEGORY')));
515
342
  drawBoxFooter(boxWidth);
516
343
  await prompts.waitForEnter();
517
344
  return await selectCategory();
518
345
  }
519
346
 
520
- // Display providers in 2 columns - Numbers in cyan, names in yellow
347
+ // Display providers in 2 columns
521
348
  for (let i = 0; i < providers.length; i += 2) {
522
349
  const left = providers[i];
523
350
  const right = providers[i + 1];
524
351
 
525
- // Provider names - number in cyan, name in yellow
526
352
  const leftNum = `[${i + 1}]`;
527
353
  const rightNum = right ? `[${i + 2}]` : '';
528
354
  const leftName = ` ${left.name}`;
529
355
  const rightName = right ? ` ${right.name}` : '';
530
356
 
531
- const leftFull = leftNum + leftName;
532
- const rightFull = rightNum + rightName;
533
-
534
- console.log(make2ColRow(
357
+ console.log(make2ColRow(W,
535
358
  chalk.cyan(leftNum) + chalk.yellow(leftName.length > col1Width - leftNum.length - 3 ? leftName.substring(0, col1Width - leftNum.length - 6) + '...' : leftName),
536
359
  right ? chalk.cyan(rightNum) + chalk.yellow(rightName.length > col1Width - rightNum.length - 3 ? rightName.substring(0, col1Width - rightNum.length - 6) + '...' : rightName) : ''
537
360
  ));
538
361
 
539
- // Descriptions (truncated)
540
362
  const leftDesc = ' ' + left.description;
541
363
  const rightDesc = right ? ' ' + right.description : '';
542
364
 
543
- console.log(make2ColRow(
365
+ console.log(make2ColRow(W,
544
366
  chalk.white(leftDesc.length > col1Width - 3 ? leftDesc.substring(0, col1Width - 6) + '...' : leftDesc),
545
367
  chalk.white(rightDesc.length > col1Width - 3 ? rightDesc.substring(0, col1Width - 6) + '...' : rightDesc)
546
368
  ));
547
369
 
548
- console.log(makeLine(''));
370
+ console.log(makeLine(W, ''));
549
371
  }
550
372
 
551
373
  drawBoxFooter(boxWidth);
552
374
 
553
- const maxNum = providers.length;
554
- const choice = await prompts.textInput(chalk.cyan(`SELECT (1-${maxNum}):`));
375
+ const choice = await prompts.textInput(chalk.cyan(`SELECT (1-${providers.length}):`));
555
376
 
556
- // Empty input = go back
557
377
  if (!choice || choice.trim() === '' || choice === '<' || choice?.toLowerCase() === 'b') {
558
378
  return await selectCategory();
559
379
  }
@@ -563,32 +383,14 @@ const selectProvider = async (categoryId) => {
563
383
  return await selectCategory();
564
384
  }
565
385
 
566
- const selectedProvider = providers[index];
567
- return await selectProviderOption(selectedProvider);
386
+ return await selectProviderOption(providers[index]);
568
387
  };
569
388
 
570
- /**
571
- * Select connection option for provider
572
- */
389
+ /** Select connection option for provider */
573
390
  const selectProviderOption = async (provider) => {
574
- const boxWidth = getLogoWidth();
575
- const W = boxWidth - 2;
391
+ const { boxWidth, W } = getBoxDimensions();
576
392
  const col1Width = Math.floor(W / 2);
577
393
 
578
- const makeLine = (content) => {
579
- const plainLen = content.replace(/\x1b\[[0-9;]*m/g, '').length;
580
- const padding = W - plainLen;
581
- return chalk.cyan('║') + ' ' + content + ' '.repeat(Math.max(0, padding - 1)) + chalk.cyan('║');
582
- };
583
-
584
- const make2ColRow = (left, right) => {
585
- const leftPlain = left.replace(/\x1b\[[0-9;]*m/g, '').length;
586
- const rightPlain = right.replace(/\x1b\[[0-9;]*m/g, '').length;
587
- const leftPadded = ' ' + left + ' '.repeat(Math.max(0, col1Width - leftPlain - 1));
588
- const rightPadded = right + ' '.repeat(Math.max(0, W - col1Width - rightPlain));
589
- return chalk.cyan('║') + leftPadded + rightPadded + chalk.cyan('║');
590
- };
591
-
592
394
  // If only one option, skip selection
593
395
  if (provider.options.length === 1) {
594
396
  return await setupConnection(provider, provider.options[0]);
@@ -598,46 +400,42 @@ const selectProviderOption = async (provider) => {
598
400
  displayBanner();
599
401
  drawBoxHeaderContinue(provider.name, boxWidth);
600
402
 
601
- console.log(makeLine(chalk.white('SELECT CONNECTION METHOD:')));
602
- console.log(makeLine(''));
403
+ console.log(makeLine(W, chalk.white('SELECT CONNECTION METHOD:')));
404
+ console.log(makeLine(W, ''));
603
405
 
604
- // Display options in 2 columns - Numbers in cyan, labels in yellow
406
+ // Display options in 2 columns
605
407
  for (let i = 0; i < provider.options.length; i += 2) {
606
408
  const left = provider.options[i];
607
409
  const right = provider.options[i + 1];
608
410
 
609
- // Option labels - number in cyan, label in yellow
610
- console.log(make2ColRow(
411
+ console.log(make2ColRow(W,
611
412
  chalk.cyan(`[${i + 1}]`) + chalk.yellow(` ${left.label}`),
612
413
  right ? chalk.cyan(`[${i + 2}]`) + chalk.yellow(` ${right.label}`) : ''
613
414
  ));
614
415
 
615
- // First description line
616
416
  const leftDesc1 = left.description[0] ? ' ' + left.description[0] : '';
617
417
  const rightDesc1 = right?.description[0] ? ' ' + right.description[0] : '';
618
- console.log(make2ColRow(
418
+ console.log(make2ColRow(W,
619
419
  chalk.white(leftDesc1.length > col1Width - 2 ? leftDesc1.substring(0, col1Width - 5) + '...' : leftDesc1),
620
420
  chalk.white(rightDesc1.length > col1Width - 2 ? rightDesc1.substring(0, col1Width - 5) + '...' : rightDesc1)
621
421
  ));
622
422
 
623
- // Second description line if exists
624
423
  const leftDesc2 = left.description[1] ? ' ' + left.description[1] : '';
625
424
  const rightDesc2 = right?.description[1] ? ' ' + right.description[1] : '';
626
425
  if (leftDesc2 || rightDesc2) {
627
- console.log(make2ColRow(
426
+ console.log(make2ColRow(W,
628
427
  chalk.white(leftDesc2.length > col1Width - 2 ? leftDesc2.substring(0, col1Width - 5) + '...' : leftDesc2),
629
428
  chalk.white(rightDesc2.length > col1Width - 2 ? rightDesc2.substring(0, col1Width - 5) + '...' : rightDesc2)
630
429
  ));
631
430
  }
632
431
 
633
- console.log(makeLine(''));
432
+ console.log(makeLine(W, ''));
634
433
  }
635
434
 
636
435
  drawBoxFooter(boxWidth);
637
436
 
638
437
  const choice = await prompts.textInput(chalk.cyan('SELECT:'));
639
438
 
640
- // Empty input = go back
641
439
  if (!choice || choice.trim() === '' || choice === '<' || choice?.toLowerCase() === 'b') {
642
440
  return await selectProvider(provider.category);
643
441
  }
@@ -647,1237 +445,38 @@ const selectProviderOption = async (provider) => {
647
445
  return await selectProvider(provider.category);
648
446
  }
649
447
 
650
- const selectedOption = provider.options[index];
651
- return await setupConnection(provider, selectedOption);
652
- };
653
-
654
- /**
655
- * Open URL in default browser
656
- * @returns {Promise<boolean>} true if browser opened, false if failed
657
- */
658
- const openBrowser = (url) => {
659
- return new Promise((resolve) => {
660
- const { exec } = require('child_process');
661
- const platform = process.platform;
662
-
663
- let cmd;
664
- if (platform === 'darwin') cmd = `open "${url}"`;
665
- else if (platform === 'win32') cmd = `start "" "${url}"`;
666
- else cmd = `xdg-open "${url}"`;
667
-
668
- exec(cmd, (err) => {
669
- resolve(!err);
670
- });
671
- });
672
- };
673
-
674
- /**
675
- * Get instructions for each credential type
676
- */
677
- const getCredentialInstructions = (provider, option, field) => {
678
- const instructions = {
679
- apiKey: {
680
- title: 'API KEY REQUIRED',
681
- steps: [
682
- '1. CLICK THE LINK BELOW TO OPEN IN BROWSER',
683
- '2. SIGN IN OR CREATE AN ACCOUNT',
684
- '3. GENERATE A NEW API KEY',
685
- '4. COPY AND PASTE IT HERE'
686
- ]
687
- },
688
- sessionKey: {
689
- title: 'SESSION KEY REQUIRED (SUBSCRIPTION PLAN)',
690
- steps: [
691
- '1. OPEN THE LINK BELOW IN YOUR BROWSER',
692
- '2. SIGN IN WITH YOUR SUBSCRIPTION ACCOUNT',
693
- '3. OPEN DEVELOPER TOOLS (F12 OR CMD+OPT+I)',
694
- '4. GO TO APPLICATION > COOKIES',
695
- '5. FIND "sessionKey" OR SIMILAR TOKEN',
696
- '6. COPY THE VALUE AND PASTE IT HERE'
697
- ]
698
- },
699
- accessToken: {
700
- title: 'ACCESS TOKEN REQUIRED (SUBSCRIPTION PLAN)',
701
- steps: [
702
- '1. OPEN THE LINK BELOW IN YOUR BROWSER',
703
- '2. SIGN IN WITH YOUR SUBSCRIPTION ACCOUNT',
704
- '3. OPEN DEVELOPER TOOLS (F12 OR CMD+OPT+I)',
705
- '4. GO TO APPLICATION > COOKIES OR LOCAL STORAGE',
706
- '5. FIND "accessToken" OR "token"',
707
- '6. COPY THE VALUE AND PASTE IT HERE'
708
- ]
709
- },
710
- endpoint: {
711
- title: 'ENDPOINT URL',
712
- steps: [
713
- '1. ENTER THE API ENDPOINT URL',
714
- '2. USUALLY http://localhost:PORT FOR LOCAL'
715
- ]
716
- },
717
- model: {
718
- title: 'MODEL NAME',
719
- steps: [
720
- '1. ENTER THE MODEL NAME TO USE',
721
- '2. CHECK PROVIDER DOCS FOR AVAILABLE MODELS'
722
- ]
723
- }
724
- };
725
-
726
- return instructions[field] || { title: field.toUpperCase(), steps: [] };
727
- };
728
-
729
- /**
730
- * Get OAuth config for provider
731
- */
732
- const getOAuthConfig = (providerId) => {
733
- const configs = {
734
- anthropic: {
735
- name: 'CLAUDE PRO/MAX',
736
- accountName: 'Claude',
737
- oauthModule: oauthAnthropic,
738
- authorizeArgs: ['max'],
739
- optionId: 'oauth_max',
740
- agentName: 'Claude Pro/Max',
741
- codeFormat: 'abc123...#xyz789...'
742
- },
743
- openai: {
744
- name: 'CHATGPT PLUS/PRO',
745
- accountName: 'ChatGPT',
746
- oauthModule: oauthOpenai,
747
- authorizeArgs: [],
748
- optionId: 'oauth_plus',
749
- agentName: 'ChatGPT Plus/Pro',
750
- codeFormat: 'authorization_code'
751
- },
752
- gemini: {
753
- name: 'GEMINI ADVANCED',
754
- accountName: 'Google',
755
- oauthModule: oauthGemini,
756
- authorizeArgs: [],
757
- optionId: 'oauth_advanced',
758
- agentName: 'Gemini Advanced',
759
- codeFormat: 'authorization_code'
760
- },
761
- qwen: {
762
- name: 'QWEN CHAT',
763
- accountName: 'Qwen',
764
- oauthModule: oauthQwen,
765
- authorizeArgs: [],
766
- optionId: 'oauth_chat',
767
- agentName: 'Qwen Chat',
768
- isDeviceFlow: true
769
- },
770
- iflow: {
771
- name: 'IFLOW',
772
- accountName: 'iFlow',
773
- oauthModule: oauthIflow,
774
- authorizeArgs: [],
775
- optionId: 'oauth_sub',
776
- agentName: 'iFlow',
777
- codeFormat: 'authorization_code'
778
- }
779
- };
780
- return configs[providerId];
781
- };
782
-
783
- /**
784
- * Setup OAuth connection for any provider with OAuth support
785
- * Uses CLIProxyAPI for proper OAuth handling and API access
786
- */
787
- const setupOAuthConnection = async (provider) => {
788
- const config = getOAuthConfig(provider.id);
789
- if (!config) {
790
- console.log(chalk.red('OAuth not supported for this provider'));
791
- await prompts.waitForEnter();
792
- return await selectProviderOption(provider);
793
- }
794
-
795
- // Use CLIProxyAPI for OAuth flow - it handles token exchange and API calls
796
- return await setupProxyOAuth(provider, config);
448
+ return await setupConnection(provider, provider.options[index]);
797
449
  };
798
450
 
799
- /**
800
- * Setup OAuth via Manual Code Entry (unified flow for local and VPS)
801
- * User copies the authorization code from the URL or page
802
- */
803
- const setupRemoteOAuth = async (provider, config) => {
804
- const boxWidth = getLogoWidth();
805
- const W = boxWidth - 2;
806
-
807
- const makeLine = (content) => {
808
- const plainLen = content.replace(/\x1b\[[0-9;]*m/g, '').length;
809
- const padding = W - plainLen;
810
- return chalk.cyan('║') + ' ' + content + ' '.repeat(Math.max(0, padding - 1)) + chalk.cyan('║');
811
- };
812
-
813
- // Get the right OAuth module for this provider
814
- const oauthModules = {
815
- anthropic: oauthAnthropic,
816
- openai: oauthOpenai,
817
- gemini: oauthGemini,
818
- qwen: oauthQwen,
819
- iflow: oauthIflow
820
- };
821
-
822
- const oauthModule = oauthModules[provider.id];
823
- if (!oauthModule) {
824
- console.log(chalk.red(`OAuth not supported for ${provider.id}`));
825
- await prompts.waitForEnter();
826
- return await selectProviderOption(provider);
827
- }
828
-
829
- // Generate OAuth URL using the provider's oauth module
830
- const authResult = oauthModule.authorize(config.optionId || 'max');
831
- const url = authResult.url;
832
- const verifier = authResult.verifier;
833
-
834
- // Show instructions
835
- console.clear();
836
- displayBanner();
837
- drawBoxHeaderContinue(`CONNECT ${config.name}`, boxWidth);
838
-
839
- console.log(makeLine(chalk.yellow('CONNECT YOUR ACCOUNT')));
840
- console.log(makeLine(''));
841
- console.log(makeLine(chalk.white('1. OPEN THE LINK BELOW IN YOUR BROWSER')));
842
- console.log(makeLine(''));
843
- console.log(makeLine(chalk.white(`2. LOGIN WITH YOUR ${config.accountName.toUpperCase()} ACCOUNT`)));
844
- console.log(makeLine(''));
845
- console.log(makeLine(chalk.white('3. CLICK "AUTHORIZE"')));
846
- console.log(makeLine(''));
847
- console.log(makeLine(chalk.green('4. COPY THE CODE FROM THE URL BAR')));
848
- console.log(makeLine(chalk.white(' Look for: code=XXXXXX in the URL')));
849
- console.log(makeLine(chalk.white(' Copy everything after code= until &')));
850
- console.log(makeLine(''));
851
- console.log(makeLine(chalk.white('5. PASTE THE CODE BELOW')));
852
- console.log(makeLine(''));
853
-
854
- drawBoxFooter(boxWidth);
855
-
856
- // Display URL outside the box for easy copy
857
- console.log();
858
- console.log(chalk.yellow(' OPEN THIS URL IN YOUR BROWSER:'));
859
- console.log();
860
- console.log(chalk.cyan(` ${url}`));
861
- console.log();
862
-
863
- // Get code from user
864
- const code = await prompts.textInput(chalk.cyan('PASTE AUTHORIZATION CODE:'));
865
-
866
- if (!code || code.trim() === '<' || code.trim() === '') {
867
- return await selectProviderOption(provider);
868
- }
869
-
870
- // Exchange code for tokens
871
- const spinner = ora({ text: 'Exchanging code for tokens...', color: 'cyan' }).start();
872
-
873
- const result = await oauthModule.exchange(code.trim(), verifier);
874
-
875
- if (result.type === 'failed') {
876
- spinner.fail(`Authentication failed: ${result.error || 'Invalid code'}`);
877
- await prompts.waitForEnter();
878
- return await selectProviderOption(provider);
879
- }
880
-
881
- spinner.succeed('Authorization successful!');
882
-
883
- // Save credentials
884
- const credentials = {
885
- oauth: {
886
- access: result.access,
887
- refresh: result.refresh,
888
- expires: result.expires,
889
- apiKey: result.apiKey,
890
- email: result.email
891
- }
892
- };
893
-
894
- // Try to fetch models with the new token
895
- spinner.text = 'Fetching available models from API...';
896
- spinner.start();
897
-
898
- let models = [];
899
- let fetchError = null;
900
- try {
901
- const { fetchModelsWithOAuth } = require('../services/ai/client');
902
- models = await fetchModelsWithOAuth(provider.id, result.access);
903
- } catch (e) {
904
- fetchError = e.message;
905
- }
906
-
907
- // RULE: Models MUST come from API - no hardcoded fallback
908
- if (!models || models.length === 0) {
909
- spinner.fail('Could not fetch models from API');
910
- console.log();
911
- console.log(chalk.red(' ERROR: Unable to retrieve models from provider API'));
912
- console.log(chalk.white(' Possible causes:'));
913
- console.log(chalk.gray(' - OAuth token may not have permission to list models'));
914
- console.log(chalk.gray(' - Network issue or API temporarily unavailable'));
915
- console.log(chalk.gray(' - Provider API may have changed'));
916
- if (fetchError) {
917
- console.log(chalk.gray(` - Error: ${fetchError}`));
918
- }
919
- console.log();
920
- console.log(chalk.yellow(' Please try again or use API Key authentication instead.'));
921
- await prompts.waitForEnter();
922
- return await selectProviderOption(provider);
923
- }
924
-
925
- spinner.succeed(`Found ${models.length} models`);
926
-
927
- // Let user select model from list
928
- const selectedModel = await selectModelFromList(models, config.name);
929
- if (!selectedModel) {
930
- return await selectProviderOption(provider);
931
- }
932
-
933
- // Add agent
934
- try {
935
- await aiService.addAgent(provider.id, config.optionId, credentials, selectedModel, config.agentName);
936
-
937
- console.log(chalk.green(`\n CONNECTED TO ${config.name}`));
938
- console.log(chalk.white(` MODEL: ${selectedModel}`));
939
- console.log(chalk.white(' UNLIMITED USAGE WITH YOUR SUBSCRIPTION'));
940
- } catch (error) {
941
- console.log(chalk.red(`\n FAILED TO SAVE: ${error.message}`));
942
- }
943
-
944
- await prompts.waitForEnter();
945
- return await aiAgentMenu();
946
- };
947
-
948
- // NOTE: promptForModelName was removed - models MUST come from API (RULES.md)
949
-
950
- /**
951
- * Setup OAuth using CLIProxyAPI
952
- * CLIProxyAPI handles OAuth flow, token storage, and API calls
953
- * Models are fetched from CLIProxyAPI /v1/models endpoint
954
- */
955
- const setupProxyOAuth = async (provider, config) => {
956
- const boxWidth = getLogoWidth();
957
- const W = boxWidth - 2;
958
-
959
- const makeLine = (content) => {
960
- const plainLen = content.replace(/\x1b\[[0-9;]*m/g, '').length;
961
- const padding = W - plainLen;
962
- return chalk.cyan('║') + ' ' + content + ' '.repeat(Math.max(0, padding - 1)) + chalk.cyan('║');
963
- };
964
-
965
- // Step 1: Ensure CLIProxyAPI is installed and running
966
- const spinner = ora({ text: 'Setting up CLIProxyAPI...', color: 'cyan' }).start();
967
-
968
- try {
969
- await proxyManager.ensureRunning();
970
- spinner.succeed('CLIProxyAPI ready');
971
- } catch (error) {
972
- spinner.fail(`Failed to start CLIProxyAPI: ${error.message}`);
973
- console.log();
974
- console.log(chalk.yellow(' CLIProxyAPI is required for OAuth authentication.'));
975
- console.log(chalk.gray(' It will be downloaded automatically on first use.'));
976
- console.log();
977
- await prompts.waitForEnter();
978
- return await selectProviderOption(provider);
979
- }
980
-
981
- // Step 2: Get OAuth URL from CLIProxyAPI
982
- spinner.text = 'Getting authorization URL...';
983
- spinner.start();
984
-
985
- let authUrl, authState;
986
- try {
987
- const authInfo = await proxyManager.getAuthUrl(provider.id);
988
- authUrl = authInfo.url;
989
- authState = authInfo.state;
990
- spinner.succeed('Authorization URL ready');
991
- } catch (error) {
992
- spinner.fail(`Failed to get auth URL: ${error.message}`);
993
- await prompts.waitForEnter();
994
- return await selectProviderOption(provider);
995
- }
996
-
997
- // Step 3: Show instructions to user
998
- console.clear();
999
- displayBanner();
1000
- drawBoxHeaderContinue(`CONNECT ${config.name}`, boxWidth);
1001
-
1002
- console.log(makeLine(chalk.yellow('CONNECT YOUR ACCOUNT')));
1003
- console.log(makeLine(''));
1004
- console.log(makeLine(chalk.white('1. OPEN THE LINK BELOW IN YOUR BROWSER')));
1005
- console.log(makeLine(''));
1006
- console.log(makeLine(chalk.white(`2. LOGIN WITH YOUR ${config.accountName.toUpperCase()} ACCOUNT`)));
1007
- console.log(makeLine(''));
1008
- console.log(makeLine(chalk.white('3. CLICK "AUTHORIZE"')));
1009
- console.log(makeLine(''));
1010
- console.log(makeLine(chalk.green('4. WAIT FOR CONFIRMATION HERE')));
1011
- console.log(makeLine(chalk.white(' (The page will close automatically)')));
1012
- console.log(makeLine(''));
1013
-
1014
- drawBoxFooter(boxWidth);
1015
-
1016
- // Display URL
1017
- console.log();
1018
- console.log(chalk.yellow(' OPEN THIS URL IN YOUR BROWSER:'));
1019
- console.log();
1020
- console.log(chalk.cyan(` ${authUrl}`));
1021
- console.log();
1022
-
1023
- // Try to open browser automatically
1024
- const browserOpened = await openBrowser(authUrl);
1025
- if (browserOpened) {
1026
- console.log(chalk.gray(' (Browser opened automatically)'));
1027
- } else {
1028
- console.log(chalk.gray(' (Copy and paste the URL in your browser)'));
1029
- }
1030
- console.log();
1031
-
1032
- // Step 4: Wait for OAuth callback
1033
- // Detect if we're on a remote server (SSH) - callback won't work automatically
1034
- const isRemote = process.env.SSH_CONNECTION || process.env.SSH_CLIENT ||
1035
- (process.env.DISPLAY === undefined && process.platform === 'linux');
1036
-
1037
- let authSuccess = false;
1038
-
1039
- if (isRemote) {
1040
- // Remote/VPS: Ask for callback URL directly (no point waiting)
1041
- console.log(chalk.yellow(' After authorizing, paste the callback URL from your browser:'));
1042
- console.log(chalk.gray(' (The page showing http://localhost:54545/callback?code=...)'));
1043
- console.log();
1044
-
1045
- // Use inquirer directly for reliable input on VPS
1046
- const inquirer = require('inquirer');
1047
- let callbackUrl = '';
1048
- try {
1049
- const result = await inquirer.prompt([{
1050
- type: 'input',
1051
- name: 'callbackUrl',
1052
- message: 'Callback URL:',
1053
- prefix: ' '
1054
- }]);
1055
- callbackUrl = result.callbackUrl || '';
1056
- } catch (e) {
1057
- // Inquirer failed, try readline directly
1058
- const readline = require('readline');
1059
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1060
- callbackUrl = await new Promise(resolve => {
1061
- rl.question(' Callback URL: ', answer => {
1062
- rl.close();
1063
- resolve(answer || '');
1064
- });
1065
- });
1066
- }
1067
-
1068
- if (callbackUrl && callbackUrl.trim()) {
1069
- const submitSpinner = ora({ text: 'Submitting callback...', color: 'cyan' }).start();
1070
- try {
1071
- await proxyManager.submitCallback(callbackUrl.trim(), provider.id);
1072
- // Callback succeeded - CLIProxyAPI handles the token exchange
1073
- await new Promise(resolve => setTimeout(resolve, 500));
1074
- authSuccess = true;
1075
- submitSpinner.succeed('Authorization successful!');
1076
- } catch (submitError) {
1077
- submitSpinner.fail(`Failed: ${submitError.message}`);
1078
- await prompts.waitForEnter();
1079
- return await selectProviderOption(provider);
1080
- }
1081
- } else {
1082
- console.log(chalk.gray(' Cancelled.'));
1083
- await prompts.waitForEnter();
1084
- return await selectProviderOption(provider);
1085
- }
1086
- } else {
1087
- // Local: Wait for automatic callback
1088
- const waitSpinner = ora({ text: 'Waiting for authorization...', color: 'cyan' }).start();
1089
-
1090
- try {
1091
- await proxyManager.waitForAuth(authState, 300000, (status) => {
1092
- waitSpinner.text = status;
1093
- });
1094
- authSuccess = true;
1095
- waitSpinner.succeed('Authorization successful!');
1096
- } catch (error) {
1097
- waitSpinner.fail(`Authorization failed: ${error.message}`);
1098
- await prompts.waitForEnter();
1099
- return await selectProviderOption(provider);
1100
- }
1101
- }
1102
-
1103
- if (!authSuccess) {
1104
- return await selectProviderOption(provider);
1105
- }
1106
-
1107
- // Wrap entire post-auth flow in try-catch to catch any uncaught errors
1108
- try {
1109
- // Step 5: Fetch models from CLIProxyAPI (filtered by provider)
1110
- const modelSpinner = ora({ text: 'Fetching available models from API...', color: 'cyan' }).start();
1111
-
1112
- let models = [];
1113
- try {
1114
- // Get models filtered by provider using owned_by field from API
1115
- models = await proxyManager.getModels(provider.id);
1116
- modelSpinner.succeed(`Found ${models.length} models for ${provider.name}`);
1117
- } catch (error) {
1118
- modelSpinner.fail(`Failed to fetch models: ${error.message}`);
1119
- await prompts.waitForEnter();
1120
- return await selectProviderOption(provider);
1121
- }
1122
-
1123
- if (!models || models.length === 0) {
1124
- console.log();
1125
- console.log(chalk.red(' ERROR: No models found for this provider'));
1126
- console.log(chalk.gray(' Make sure your subscription is active and authorization completed.'));
1127
- console.log();
1128
- await prompts.waitForEnter();
1129
- return await selectProviderOption(provider);
1130
- }
1131
-
1132
- // Step 6: Let user select model
1133
- const selectedModel = await selectModelFromList(models, config.name);
1134
- if (!selectedModel) {
1135
- return await selectProviderOption(provider);
1136
- }
1137
-
1138
- // Step 7: Save agent config (CLIProxyAPI stores the actual tokens)
1139
- // We just save a reference to use the proxy
1140
- const credentials = {
1141
- useProxy: true,
1142
- provider: provider.id
1143
- };
1144
-
1145
- try {
1146
- await aiService.addAgent(provider.id, config.optionId, credentials, selectedModel, config.agentName);
1147
-
1148
- console.log(chalk.green(`\n CONNECTED TO ${config.name}`));
1149
- console.log(chalk.white(` MODEL: ${selectedModel}`));
1150
- console.log(chalk.white(' UNLIMITED USAGE WITH YOUR SUBSCRIPTION'));
1151
- } catch (error) {
1152
- console.log(chalk.red(`\n FAILED TO SAVE: ${error.message}`));
1153
- }
1154
-
1155
- await prompts.waitForEnter();
1156
- return await aiAgentMenu();
1157
- } catch (unexpectedError) {
1158
- // Catch any uncaught errors in the post-auth flow
1159
- console.log();
1160
- console.log(chalk.red(` UNEXPECTED ERROR: ${unexpectedError.message}`));
1161
- console.log(chalk.gray(` Stack: ${unexpectedError.stack}`));
1162
- console.log();
1163
- await prompts.waitForEnter();
1164
- return await selectProviderOption(provider);
1165
- }
1166
- };
1167
-
1168
- /**
1169
- * Setup OAuth with browser redirect flow (Claude, OpenAI, Gemini, iFlow)
1170
- * Legacy method - kept for API key users
1171
- */
1172
- const setupBrowserOAuth = async (provider, config) => {
1173
- const boxWidth = getLogoWidth();
1174
- const W = boxWidth - 2;
1175
-
1176
- const makeLine = (content) => {
1177
- const plainLen = content.replace(/\x1b\[[0-9;]*m/g, '').length;
1178
- const padding = W - plainLen;
1179
- return chalk.cyan('║') + ' ' + content + ' '.repeat(Math.max(0, padding - 1)) + chalk.cyan('║');
1180
- };
1181
-
1182
- console.clear();
1183
- displayBanner();
1184
- drawBoxHeaderContinue(`${config.name} LOGIN`, boxWidth);
1185
-
1186
- console.log(makeLine(chalk.yellow('OAUTH AUTHENTICATION')));
1187
- console.log(makeLine(''));
1188
- console.log(makeLine(chalk.white('1. A BROWSER WINDOW WILL OPEN')));
1189
- console.log(makeLine(chalk.white(`2. LOGIN WITH YOUR ${config.accountName.toUpperCase()} ACCOUNT`)));
1190
- console.log(makeLine(chalk.white('3. COPY THE AUTHORIZATION CODE')));
1191
- console.log(makeLine(chalk.white('4. PASTE IT HERE')));
1192
- console.log(makeLine(''));
1193
- console.log(makeLine(chalk.white('OPENING BROWSER IN 3 SECONDS...')));
1194
-
1195
- drawBoxFooter(boxWidth);
1196
-
1197
- // Generate OAuth URL
1198
- const authResult = config.oauthModule.authorize(...config.authorizeArgs);
1199
- const url = authResult.url;
1200
- const verifier = authResult.verifier;
1201
- const state = authResult.state;
1202
- const redirectUri = authResult.redirectUri;
1203
-
1204
- // Wait a moment then open browser
1205
- await new Promise(resolve => setTimeout(resolve, 3000));
1206
- const browserOpened = await openBrowser(url);
1207
-
1208
- // Redraw with code input
1209
- console.clear();
1210
- displayBanner();
1211
- drawBoxHeaderContinue(`${config.name} LOGIN`, boxWidth);
1212
-
1213
- if (browserOpened) {
1214
- console.log(makeLine(chalk.green('BROWSER OPENED')));
1215
- console.log(makeLine(''));
1216
- console.log(makeLine(chalk.white('AFTER LOGGING IN, YOU WILL SEE A CODE')));
1217
- console.log(makeLine(chalk.white('COPY THE ENTIRE CODE AND PASTE IT BELOW')));
1218
- console.log(makeLine(''));
1219
- if (config.codeFormat) {
1220
- console.log(makeLine(chalk.white(`THE CODE LOOKS LIKE: ${config.codeFormat}`)));
1221
- console.log(makeLine(''));
1222
- }
1223
- console.log(makeLine(chalk.white('TYPE < TO CANCEL')));
1224
- drawBoxFooter(boxWidth);
1225
- console.log();
1226
- } else {
1227
- console.log(makeLine(chalk.yellow('COULD NOT OPEN BROWSER (VPS/SSH?)')));
1228
- console.log(makeLine(''));
1229
-
1230
- // Different instructions for different providers
1231
- if (provider.id === 'anthropic') {
1232
- console.log(makeLine(chalk.white('AFTER LOGGING IN, YOU WILL SEE A CODE')));
1233
- console.log(makeLine(chalk.white('COPY THE ENTIRE CODE AND PASTE IT BELOW')));
1234
- console.log(makeLine(''));
1235
- console.log(makeLine(chalk.white('THE CODE LOOKS LIKE: abc123...#xyz789...')));
1236
- } else {
1237
- // Gemini, OpenAI, iFlow redirect to localhost - need full URL
1238
- console.log(makeLine(chalk.white('AFTER LOGGING IN, YOU WILL SEE A BLANK PAGE')));
1239
- console.log(makeLine(chalk.white('THE URL WILL START WITH: localhost:...')));
1240
- console.log(makeLine(''));
1241
- console.log(makeLine(chalk.green('COPY THE ENTIRE URL FROM THE ADDRESS BAR')));
1242
- console.log(makeLine(chalk.white('AND PASTE IT BELOW')));
1243
- }
1244
- console.log(makeLine(''));
1245
- console.log(makeLine(chalk.white('TYPE < TO CANCEL')));
1246
- drawBoxFooter(boxWidth);
1247
- // Display URL outside the box for easy copy-paste (no line breaks)
1248
- console.log();
1249
- console.log(chalk.yellow(' OPEN THIS URL IN YOUR BROWSER:'));
1250
- console.log();
1251
- console.log(chalk.cyan(` ${url}`));
1252
- console.log();
1253
- }
1254
-
1255
- const promptText = provider.id === 'anthropic'
1256
- ? 'PASTE AUTHORIZATION CODE:'
1257
- : 'PASTE FULL CALLBACK URL:';
1258
-
1259
- const input = await prompts.textInput(chalk.cyan(promptText));
1260
-
1261
- if (!input || input === '<') {
1262
- return await selectProviderOption(provider);
1263
- }
1264
-
1265
- // Extract code from URL if user pasted full callback URL
1266
- let code = input.trim();
1267
- if (code.includes('code=')) {
1268
- try {
1269
- const urlObj = new URL(code.replace('localhost', 'http://localhost'));
1270
- code = urlObj.searchParams.get('code') || code;
1271
- } catch (e) {
1272
- // Try regex extraction as fallback
1273
- const match = code.match(/[?&]code=([^&]+)/);
1274
- if (match) code = match[1];
1275
- }
1276
- }
1277
-
1278
- // Exchange code for tokens
1279
- const spinner = ora({ text: 'EXCHANGING CODE FOR TOKENS...', color: 'cyan' }).start();
1280
-
1281
- let result;
1282
- if (provider.id === 'anthropic') {
1283
- result = await config.oauthModule.exchange(code, verifier);
1284
- } else if (provider.id === 'gemini') {
1285
- result = await config.oauthModule.exchange(code);
1286
- } else if (provider.id === 'iflow') {
1287
- result = await config.oauthModule.exchange(code, redirectUri);
1288
- } else {
1289
- result = await config.oauthModule.exchange(code, verifier);
1290
- }
1291
-
1292
- if (result.type === 'failed') {
1293
- spinner.fail(`AUTHENTICATION FAILED: ${result.error || 'Invalid code'}`);
1294
- await prompts.waitForEnter();
1295
- return await selectProviderOption(provider);
1296
- }
1297
-
1298
- spinner.text = 'FETCHING AVAILABLE MODELS...';
1299
-
1300
- // Store OAuth credentials
1301
- const credentials = {
1302
- oauth: {
1303
- access: result.access,
1304
- refresh: result.refresh,
1305
- expires: result.expires,
1306
- apiKey: result.apiKey,
1307
- email: result.email
1308
- }
1309
- };
1310
-
1311
- // For iFlow, the API key is the main credential
1312
- if (provider.id === 'iflow' && result.apiKey) {
1313
- credentials.apiKey = result.apiKey;
1314
- }
1315
-
1316
- // Fetch available models for the provider
1317
- let models = [];
1318
- try {
1319
- const { fetchModelsWithOAuth } = require('../services/ai/client');
1320
- models = await fetchModelsWithOAuth(provider.id, result.access);
1321
- } catch (e) {
1322
- // Fallback to default models if fetch fails
1323
- }
1324
-
1325
- // NO hardcoded fallback - models MUST come from API
1326
- // Rule: ZERO fake/mock data - API only
1327
- if (!models || models.length === 0) {
1328
- spinner.fail('NO MODELS AVAILABLE FROM API');
1329
- console.log(chalk.red('\n Could not fetch models from provider API'));
1330
- console.log(chalk.gray(' Please check your OAuth credentials or try again'));
1331
- await prompts.waitForEnter();
1332
- return await selectProviderOption(provider);
1333
- } else {
1334
- spinner.succeed(`FOUND ${models.length} MODELS`);
1335
- }
1336
-
1337
- // Let user select model
1338
- const selectedModel = await selectModelFromList(models, config.name);
1339
- if (!selectedModel) {
1340
- return await selectProviderOption(provider);
1341
- }
1342
-
1343
- // Add agent with OAuth credentials
1344
- try {
1345
- await aiService.addAgent(provider.id, config.optionId, credentials, selectedModel, config.agentName);
1346
-
1347
- console.log(chalk.green(`\n CONNECTED TO ${config.name}`));
1348
- console.log(chalk.white(` MODEL: ${selectedModel}`));
1349
- console.log(chalk.white(' UNLIMITED USAGE WITH YOUR SUBSCRIPTION'));
1350
- } catch (error) {
1351
- console.log(chalk.red(`\n FAILED TO SAVE: ${error.message}`));
1352
- }
1353
-
1354
- await prompts.waitForEnter();
1355
- return await aiAgentMenu();
1356
- };
1357
-
1358
- /**
1359
- * Setup OAuth with device flow (Qwen)
1360
- */
1361
- const setupDeviceFlowOAuth = async (provider, config) => {
1362
- const boxWidth = getLogoWidth();
1363
- const W = boxWidth - 2;
1364
-
1365
- const makeLine = (content) => {
1366
- const plainLen = content.replace(/\x1b\[[0-9;]*m/g, '').length;
1367
- const padding = W - plainLen;
1368
- return chalk.cyan('║') + ' ' + content + ' '.repeat(Math.max(0, padding - 1)) + chalk.cyan('║');
1369
- };
1370
-
1371
- console.clear();
1372
- displayBanner();
1373
- drawBoxHeaderContinue(`${config.name} LOGIN`, boxWidth);
1374
-
1375
- console.log(makeLine(chalk.yellow('DEVICE FLOW AUTHENTICATION')));
1376
- console.log(makeLine(''));
1377
- console.log(makeLine(chalk.white('INITIATING DEVICE AUTHORIZATION...')));
1378
-
1379
- drawBoxFooter(boxWidth);
1380
-
1381
- // Initiate device flow
1382
- const spinner = ora({ text: 'GETTING DEVICE CODE...', color: 'cyan' }).start();
1383
-
1384
- const deviceResult = await config.oauthModule.initiateDeviceFlow();
1385
-
1386
- if (deviceResult.type === 'failed') {
1387
- spinner.fail(`FAILED: ${deviceResult.error}`);
1388
- await prompts.waitForEnter();
1389
- return await selectProviderOption(provider);
1390
- }
1391
-
1392
- spinner.stop();
1393
-
1394
- // Show user code and verification URL
1395
- console.clear();
1396
- displayBanner();
1397
- drawBoxHeaderContinue(`${config.name} LOGIN`, boxWidth);
1398
-
1399
- console.log(makeLine(chalk.yellow('DEVICE FLOW AUTHENTICATION')));
1400
- console.log(makeLine(''));
1401
- console.log(makeLine(chalk.white('1. OPEN THE URL BELOW IN YOUR BROWSER')));
1402
- console.log(makeLine(''));
1403
- console.log(makeLine(chalk.white('2. ENTER THIS CODE WHEN PROMPTED:')));
1404
- console.log(makeLine(''));
1405
- console.log(makeLine(chalk.green.bold(` ${deviceResult.userCode}`)));
1406
- console.log(makeLine(''));
1407
- console.log(makeLine(chalk.white('3. AUTHORIZE THE APPLICATION')));
1408
- console.log(makeLine(''));
1409
- console.log(makeLine(chalk.white('WAITING FOR AUTHORIZATION...')));
1410
- console.log(makeLine(chalk.white('(THIS WILL AUTO-DETECT WHEN YOU AUTHORIZE)')));
1411
-
1412
- drawBoxFooter(boxWidth);
1413
-
1414
- // Display URL outside the box for easy copy-paste
1415
- const verificationUrl = deviceResult.verificationUriComplete || deviceResult.verificationUri;
1416
- console.log();
1417
- console.log(chalk.yellow(' OPEN THIS URL IN YOUR BROWSER:'));
1418
- console.log();
1419
- console.log(chalk.cyan(` ${verificationUrl}`));
1420
- console.log();
1421
-
1422
- // Poll for token
1423
- const pollSpinner = ora({ text: 'WAITING FOR AUTHORIZATION...', color: 'cyan' }).start();
1424
-
1425
- let pollResult;
1426
- const maxAttempts = 60;
1427
- let interval = deviceResult.interval || 5;
1428
-
1429
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
1430
- await new Promise(resolve => setTimeout(resolve, interval * 1000));
1431
-
1432
- pollResult = await config.oauthModule.pollForToken(deviceResult.deviceCode, deviceResult.verifier);
1433
-
1434
- if (pollResult.type === 'success') {
1435
- break;
1436
- } else if (pollResult.type === 'slow_down') {
1437
- interval = Math.min(interval + 2, 10);
1438
- pollSpinner.text = `WAITING FOR AUTHORIZATION... (slowing down)`;
1439
- } else if (pollResult.type === 'failed') {
1440
- pollSpinner.fail(`AUTHENTICATION FAILED: ${pollResult.error}`);
1441
- await prompts.waitForEnter();
1442
- return await selectProviderOption(provider);
1443
- }
1444
-
1445
- pollSpinner.text = `WAITING FOR AUTHORIZATION... (${attempt + 1}/${maxAttempts})`;
1446
- }
1447
-
1448
- if (!pollResult || pollResult.type !== 'success') {
1449
- pollSpinner.fail('AUTHENTICATION TIMED OUT');
1450
- await prompts.waitForEnter();
1451
- return await selectProviderOption(provider);
1452
- }
1453
-
1454
- pollSpinner.text = 'FETCHING AVAILABLE MODELS FROM API...';
1455
-
1456
- // Store OAuth credentials
1457
- const credentials = {
1458
- oauth: {
1459
- access: pollResult.access,
1460
- refresh: pollResult.refresh,
1461
- expires: pollResult.expires,
1462
- resourceUrl: pollResult.resourceUrl
1463
- }
1464
- };
1465
-
1466
- // Fetch models from API - NO hardcoded fallback (RULES.md)
1467
- let models = [];
1468
- let fetchError = null;
1469
- try {
1470
- const { fetchModelsWithOAuth } = require('../services/ai/client');
1471
- models = await fetchModelsWithOAuth(provider.id, pollResult.access);
1472
- } catch (e) {
1473
- fetchError = e.message;
1474
- }
1475
-
1476
- if (!models || models.length === 0) {
1477
- pollSpinner.fail('Could not fetch models from API');
1478
- console.log();
1479
- console.log(chalk.red(' ERROR: Unable to retrieve models from provider API'));
1480
- console.log(chalk.white(' Possible causes:'));
1481
- console.log(chalk.gray(' - OAuth token may not have permission to list models'));
1482
- console.log(chalk.gray(' - Network issue or API temporarily unavailable'));
1483
- if (fetchError) {
1484
- console.log(chalk.gray(` - Error: ${fetchError}`));
1485
- }
1486
- console.log();
1487
- console.log(chalk.yellow(' Please try again or use API Key authentication instead.'));
1488
- await prompts.waitForEnter();
1489
- return await selectProviderOption(provider);
1490
- }
1491
-
1492
- pollSpinner.succeed(`Found ${models.length} models`);
1493
-
1494
- // Let user select model from API list
1495
- const selectedModel = await selectModelFromList(models, config.name);
1496
- if (!selectedModel) {
1497
- return await selectProviderOption(provider);
1498
- }
1499
-
1500
- // Add agent with OAuth credentials
1501
- try {
1502
- await aiService.addAgent(provider.id, config.optionId, credentials, selectedModel, config.agentName);
1503
-
1504
- console.log(chalk.green(`\n CONNECTED TO ${config.name}`));
1505
- console.log(chalk.white(` MODEL: ${selectedModel}`));
1506
- console.log(chalk.white(' UNLIMITED USAGE WITH YOUR SUBSCRIPTION'));
1507
- } catch (error) {
1508
- console.log(chalk.red(`\n FAILED TO SAVE: ${error.message}`));
1509
- }
1510
-
1511
- await prompts.waitForEnter();
1512
- return await aiAgentMenu();
1513
- };
1514
-
1515
- /**
1516
- * Setup connection with credentials
1517
- */
451
+ /** Setup connection with credentials */
1518
452
  const setupConnection = async (provider, option) => {
1519
453
  // Handle OAuth flow separately
1520
454
  if (option.authType === 'oauth') {
1521
- return await setupOAuthConnection(provider);
1522
- }
1523
-
1524
- const boxWidth = getLogoWidth();
1525
- const W = boxWidth - 2;
1526
-
1527
- const makeLine = (content) => {
1528
- const plainLen = content.replace(/\x1b\[[0-9;]*m/g, '').length;
1529
- const padding = W - plainLen;
1530
- return chalk.cyan('║') + ' ' + content + ' '.repeat(Math.max(0, padding - 1)) + chalk.cyan('║');
1531
- };
1532
-
1533
- // Collect credentials based on fields
1534
- const credentials = {};
1535
-
1536
- for (const field of option.fields) {
1537
- // Show instructions for this field
1538
- console.clear();
1539
- displayBanner();
1540
- drawBoxHeaderContinue(`CONNECT TO ${provider.name}`, boxWidth);
1541
-
1542
- const instructions = getCredentialInstructions(provider, option, field);
1543
-
1544
- console.log(makeLine(chalk.yellow(instructions.title)));
1545
- console.log(makeLine(''));
1546
-
1547
- // Show steps
1548
- for (const step of instructions.steps) {
1549
- console.log(makeLine(chalk.white(step)));
1550
- }
1551
-
1552
- console.log(makeLine(''));
1553
-
1554
- // Show URL for reference (no browser needed - user already has API key)
1555
- if (option.url && (field === 'apiKey' || field === 'sessionKey' || field === 'accessToken')) {
1556
- console.log(makeLine(chalk.cyan('GET KEY: ') + chalk.white(option.url)));
1557
- }
1558
-
1559
- // Show default for endpoint
1560
- if (field === 'endpoint' && option.defaultEndpoint) {
1561
- console.log(makeLine(chalk.white(`DEFAULT: ${option.defaultEndpoint}`)));
1562
- }
1563
-
1564
- console.log(makeLine(''));
1565
- console.log(makeLine(chalk.white('TYPE < TO GO BACK')));
1566
-
1567
- drawBoxFooter(boxWidth);
1568
- console.log();
1569
-
1570
- let value;
1571
-
1572
- switch (field) {
1573
- case 'apiKey':
1574
- value = await prompts.textInput(chalk.cyan('PASTE API KEY:'));
1575
- if (!value || value.trim() === '<' || value.trim() === '') return await selectProviderOption(provider);
1576
- credentials.apiKey = value.trim();
1577
- break;
1578
-
1579
- case 'sessionKey':
1580
- value = await prompts.textInput(chalk.cyan('PASTE SESSION KEY:'));
1581
- if (!value || value.trim() === '<' || value.trim() === '') return await selectProviderOption(provider);
1582
- credentials.sessionKey = value.trim();
1583
- break;
1584
-
1585
- case 'accessToken':
1586
- value = await prompts.textInput(chalk.cyan('PASTE ACCESS TOKEN:'));
1587
- if (!value || value.trim() === '<' || value.trim() === '') return await selectProviderOption(provider);
1588
- credentials.accessToken = value.trim();
1589
- break;
1590
-
1591
- case 'endpoint':
1592
- const defaultEndpoint = option.defaultEndpoint || '';
1593
- value = await prompts.textInput(chalk.cyan(`ENDPOINT [${defaultEndpoint || 'required'}]:`));
1594
- if (!value || value.trim() === '<' || value.trim() === '') {
1595
- if (!defaultEndpoint) return await selectProviderOption(provider);
1596
- }
1597
- credentials.endpoint = (value && value.trim() !== '<' ? value : defaultEndpoint).trim();
1598
- if (!credentials.endpoint) return await selectProviderOption(provider);
1599
- break;
1600
-
1601
- case 'model':
1602
- value = await prompts.textInput(chalk.cyan('MODEL NAME:'));
1603
- if (!value || value.trim() === '<' || value.trim() === '') return await selectProviderOption(provider);
1604
- credentials.model = value.trim();
1605
- break;
1606
- }
455
+ return await setupOAuthConnection(provider, selectProviderOption, selectModelFromList, aiAgentMenu);
1607
456
  }
1608
457
 
1609
- // Validate connection
1610
- console.log();
1611
- const spinner = ora({ text: 'VALIDATING CONNECTION...', color: 'cyan' }).start();
1612
-
1613
- const validation = await aiService.validateConnection(provider.id, option.id, credentials);
1614
-
1615
- if (!validation.valid) {
1616
- spinner.fail(`CONNECTION FAILED: ${validation.error}`);
1617
- await prompts.waitForEnter();
458
+ // Collect credentials
459
+ const credentials = await collectCredentials(provider, option);
460
+ if (!credentials) {
1618
461
  return await selectProviderOption(provider);
1619
462
  }
1620
463
 
1621
- spinner.text = 'FETCHING AVAILABLE MODELS...';
1622
-
1623
- // Fetch models from real API
1624
- const { fetchAnthropicModels, fetchGeminiModels, fetchOpenAIModels } = require('../services/ai/client');
1625
-
1626
- let models = null;
1627
- if (provider.id === 'anthropic') {
1628
- models = await fetchAnthropicModels(credentials.apiKey);
1629
- } else if (provider.id === 'gemini') {
1630
- models = await fetchGeminiModels(credentials.apiKey);
1631
- } else {
1632
- const endpoint = credentials.endpoint || provider.endpoint;
1633
- models = await fetchOpenAIModels(endpoint, credentials.apiKey);
1634
- }
1635
-
1636
- if (!models || models.length === 0) {
1637
- spinner.fail('COULD NOT FETCH MODELS FROM API');
1638
- console.log(chalk.white(' Check your API key or network connection.'));
464
+ // Validate and fetch models
465
+ const result = await validateAndFetchModels(provider, option, credentials);
466
+ if (!result.valid) {
1639
467
  await prompts.waitForEnter();
1640
468
  return await selectProviderOption(provider);
1641
469
  }
1642
470
 
1643
- spinner.succeed(`FOUND ${models.length} MODELS`);
1644
-
1645
471
  // Let user select a model
1646
- const selectedModel = await selectModelFromList(models, provider.name);
472
+ const selectedModel = await selectModelFromList(result.models, provider.name);
1647
473
  if (!selectedModel) {
1648
474
  return await selectProviderOption(provider);
1649
475
  }
1650
476
 
1651
477
  // Add as new agent with selected model
1652
- try {
1653
- await aiService.addAgent(provider.id, option.id, credentials, selectedModel, provider.name);
1654
-
1655
- console.log(chalk.green(`\n AGENT ADDED: ${provider.name}`));
1656
- console.log(chalk.white(` MODEL: ${selectedModel}`));
1657
- } catch (error) {
1658
- console.log(chalk.red(`\n FAILED TO SAVE: ${error.message}`));
1659
- }
1660
-
1661
- await prompts.waitForEnter();
1662
- return await aiAgentMenu();
1663
- };
1664
-
1665
- /**
1666
- * Select model from a list (used when adding new agent)
1667
- * @param {Array} models - Array of model IDs from API
1668
- * @param {string} providerName - Provider name for display
1669
- * @returns {string|null} Selected model ID or null if cancelled
1670
- *
1671
- * Data source: models array comes from provider API (/v1/models)
1672
- */
1673
- const selectModelFromList = async (models, providerName) => {
1674
- const boxWidth = getLogoWidth();
1675
- const W = boxWidth - 2;
1676
-
1677
- const makeLine = (content) => {
1678
- const plainLen = content.replace(/\x1b\[[0-9;]*m/g, '').length;
1679
- const padding = W - plainLen;
1680
- return chalk.cyan('║') + ' ' + content + ' '.repeat(Math.max(0, padding - 1)) + chalk.cyan('║');
1681
- };
1682
-
1683
- console.clear();
1684
- displayBanner();
1685
- drawBoxHeaderContinue(`SELECT MODEL - ${providerName}`, boxWidth);
1686
-
1687
- if (!models || models.length === 0) {
1688
- console.log(makeLine(chalk.red('NO MODELS AVAILABLE')));
1689
- console.log(makeLine(chalk.white('[<] BACK')));
1690
- drawBoxFooter(boxWidth);
1691
- await prompts.waitForEnter();
1692
- return null;
1693
- }
1694
-
1695
- // Sort models (newest first)
1696
- const sortedModels = [...models].sort((a, b) => b.localeCompare(a));
1697
-
1698
- // Display models in 2 columns
1699
- const rows = Math.ceil(sortedModels.length / 2);
1700
- const colWidth = Math.floor((W - 4) / 2);
1701
-
1702
- for (let i = 0; i < rows; i++) {
1703
- const leftIndex = i;
1704
- const rightIndex = i + rows;
1705
-
1706
- // Left column
1707
- const leftModel = sortedModels[leftIndex];
1708
- const leftNum = chalk.cyan(`[${leftIndex + 1}]`);
1709
- const leftName = leftModel.length > colWidth - 6
1710
- ? leftModel.substring(0, colWidth - 9) + '...'
1711
- : leftModel;
1712
- const leftText = `${leftNum} ${chalk.yellow(leftName)}`;
1713
- const leftPlain = `[${leftIndex + 1}] ${leftName}`;
1714
-
1715
- // Right column (if exists)
1716
- let rightText = '';
1717
- let rightPlain = '';
1718
- if (rightIndex < sortedModels.length) {
1719
- const rightModel = sortedModels[rightIndex];
1720
- const rightNum = chalk.cyan(`[${rightIndex + 1}]`);
1721
- const rightName = rightModel.length > colWidth - 6
1722
- ? rightModel.substring(0, colWidth - 9) + '...'
1723
- : rightModel;
1724
- rightText = `${rightNum} ${chalk.yellow(rightName)}`;
1725
- rightPlain = `[${rightIndex + 1}] ${rightName}`;
1726
- }
1727
-
1728
- // Pad left column and combine
1729
- const leftPadding = colWidth - leftPlain.length;
1730
- const line = leftText + ' '.repeat(Math.max(2, leftPadding)) + rightText;
1731
- console.log(makeLine(line));
1732
- }
1733
-
1734
- console.log(makeLine(''));
1735
- console.log(makeLine(chalk.white('[<] BACK')));
1736
-
1737
- drawBoxFooter(boxWidth);
1738
-
1739
- const choice = await prompts.textInput(chalk.cyan('SELECT MODEL:'));
1740
-
1741
- // Empty input or < = go back
1742
- if (!choice || choice.trim() === '' || choice === '<' || choice?.toLowerCase() === 'b') {
1743
- return null;
1744
- }
1745
-
1746
- const index = parseInt(choice) - 1;
1747
- if (isNaN(index) || index < 0 || index >= sortedModels.length) {
1748
- return await selectModelFromList(models, providerName);
1749
- }
1750
-
1751
- return sortedModels[index];
1752
- };
1753
-
1754
- /**
1755
- * Select/change model for an agent
1756
- * Fetches available models from the provider's API
1757
- */
1758
- const selectModel = async (agent) => {
1759
- const boxWidth = getLogoWidth();
1760
- const W = boxWidth - 2;
1761
-
1762
- const makeLine = (content) => {
1763
- const plainLen = content.replace(/\x1b\[[0-9;]*m/g, '').length;
1764
- const padding = W - plainLen;
1765
- return chalk.cyan('║') + ' ' + content + ' '.repeat(Math.max(0, padding - 1)) + chalk.cyan('║');
1766
- };
1767
-
1768
- console.clear();
1769
- displayBanner();
1770
- drawBoxHeaderContinue(`SELECT MODEL - ${agent.name}`, boxWidth);
1771
-
1772
- console.log(makeLine(chalk.white('FETCHING AVAILABLE MODELS FROM API...')));
1773
- drawBoxFooter(boxWidth);
1774
-
1775
- // Fetch models from real API
1776
- const { fetchAnthropicModels, fetchAnthropicModelsOAuth, fetchGeminiModels, fetchOpenAIModels } = require('../services/ai/client');
1777
-
1778
- let models = null;
1779
- const agentCredentials = aiService.getAgentCredentials(agent.id);
1780
-
1781
- if (agent.providerId === 'anthropic') {
1782
- // Check if OAuth credentials or OAuth-like token (sk-ant-oat...)
1783
- const token = agentCredentials?.apiKey || agentCredentials?.accessToken || agentCredentials?.sessionKey;
1784
- const isOAuthToken = agentCredentials?.oauth?.access || (token && token.startsWith('sk-ant-oat'));
1785
-
1786
- if (isOAuthToken) {
1787
- // Use OAuth endpoint with Bearer token
1788
- const accessToken = agentCredentials?.oauth?.access || token;
1789
- models = await fetchAnthropicModelsOAuth(accessToken);
1790
- } else {
1791
- // Standard API key
1792
- models = await fetchAnthropicModels(token);
1793
- }
1794
- } else if (agent.providerId === 'gemini') {
1795
- // Google Gemini API
1796
- models = await fetchGeminiModels(agentCredentials?.apiKey);
1797
- } else {
1798
- // OpenAI-compatible providers
1799
- const endpoint = agentCredentials?.endpoint || agent.provider?.endpoint;
1800
- models = await fetchOpenAIModels(endpoint, agentCredentials?.apiKey);
1801
- }
1802
-
1803
- // Redraw with results
1804
- console.clear();
1805
- displayBanner();
1806
- drawBoxHeaderContinue(`SELECT MODEL - ${agent.name}`, boxWidth);
1807
-
1808
- if (!models || models.length === 0) {
1809
- console.log(makeLine(chalk.red('COULD NOT FETCH MODELS FROM API')));
1810
- console.log(makeLine(chalk.white('Check your API key or network connection.')));
1811
- console.log(makeLine(''));
1812
- console.log(makeLine(chalk.white('[<] BACK')));
1813
- drawBoxFooter(boxWidth);
1814
-
1815
- await prompts.waitForEnter();
1816
- return await aiAgentMenu();
1817
- }
1818
-
1819
- // Sort models (newest first typically)
1820
- models.sort((a, b) => b.localeCompare(a));
1821
-
1822
- // Display models in 2 columns
1823
- const rows = Math.ceil(models.length / 2);
1824
- const colWidth = Math.floor((W - 4) / 2);
1825
-
1826
- for (let i = 0; i < rows; i++) {
1827
- const leftIndex = i;
1828
- const rightIndex = i + rows;
1829
-
1830
- // Left column
1831
- const leftModel = models[leftIndex];
1832
- const leftNum = chalk.cyan(`[${leftIndex + 1}]`);
1833
- const leftCurrent = leftModel === agent.model ? chalk.green(' *') : '';
1834
- const leftName = leftModel.length > colWidth - 8
1835
- ? leftModel.substring(0, colWidth - 11) + '...'
1836
- : leftModel;
1837
- const leftText = `${leftNum} ${chalk.yellow(leftName)}${leftCurrent}`;
1838
- const leftPlain = `[${leftIndex + 1}] ${leftName}${leftModel === agent.model ? ' *' : ''}`;
1839
-
1840
- // Right column (if exists)
1841
- let rightText = '';
1842
- let rightPlain = '';
1843
- if (rightIndex < models.length) {
1844
- const rightModel = models[rightIndex];
1845
- const rightNum = chalk.cyan(`[${rightIndex + 1}]`);
1846
- const rightCurrent = rightModel === agent.model ? chalk.green(' *') : '';
1847
- const rightName = rightModel.length > colWidth - 8
1848
- ? rightModel.substring(0, colWidth - 11) + '...'
1849
- : rightModel;
1850
- rightText = `${rightNum} ${chalk.yellow(rightName)}${rightCurrent}`;
1851
- rightPlain = `[${rightIndex + 1}] ${rightName}${rightModel === agent.model ? ' *' : ''}`;
1852
- }
1853
-
1854
- // Pad left column and combine
1855
- const leftPadding = colWidth - leftPlain.length;
1856
- const line = leftText + ' '.repeat(Math.max(2, leftPadding)) + rightText;
1857
- console.log(makeLine(line));
1858
- }
1859
-
1860
- console.log(makeLine(''));
1861
- console.log(makeLine(chalk.white('[<] BACK') + chalk.white(' * = CURRENT')));
1862
-
1863
- drawBoxFooter(boxWidth);
1864
-
1865
- const choice = await prompts.textInput(chalk.cyan('SELECT MODEL:'));
1866
-
1867
- // Empty input or < = go back
1868
- if (!choice || choice.trim() === '' || choice === '<' || choice?.toLowerCase() === 'b') {
1869
- return await aiAgentMenu();
1870
- }
1871
-
1872
- const index = parseInt(choice) - 1;
1873
- if (isNaN(index) || index < 0 || index >= models.length) {
1874
- return await selectModel(agent);
1875
- }
1876
-
1877
- const selectedModel = models[index];
1878
- aiService.updateAgent(agent.id, { model: selectedModel });
478
+ await addConnectedAgent(provider, option, credentials, selectedModel);
1879
479
 
1880
- console.log(chalk.green(`\n MODEL CHANGED TO: ${selectedModel}`));
1881
480
  await prompts.waitForEnter();
1882
481
  return await aiAgentMenu();
1883
482
  };