hedgequantx 2.9.20 → 2.9.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/package.json +1 -1
  2. package/src/app.js +64 -42
  3. package/src/menus/connect.js +17 -14
  4. package/src/menus/dashboard.js +76 -58
  5. package/src/pages/accounts.js +49 -38
  6. package/src/pages/ai-agents-ui.js +388 -0
  7. package/src/pages/ai-agents.js +494 -0
  8. package/src/pages/ai-models.js +389 -0
  9. package/src/pages/algo/algo-executor.js +307 -0
  10. package/src/pages/algo/copy-executor.js +331 -0
  11. package/src/pages/algo/copy-trading.js +178 -546
  12. package/src/pages/algo/custom-strategy.js +313 -0
  13. package/src/pages/algo/index.js +75 -18
  14. package/src/pages/algo/one-account.js +57 -322
  15. package/src/pages/algo/ui.js +15 -15
  16. package/src/pages/orders.js +22 -19
  17. package/src/pages/positions.js +22 -19
  18. package/src/pages/stats/index.js +16 -15
  19. package/src/pages/user.js +11 -7
  20. package/src/services/ai-supervision/consensus.js +284 -0
  21. package/src/services/ai-supervision/context.js +275 -0
  22. package/src/services/ai-supervision/directive.js +167 -0
  23. package/src/services/ai-supervision/health.js +47 -35
  24. package/src/services/ai-supervision/index.js +359 -0
  25. package/src/services/ai-supervision/parser.js +278 -0
  26. package/src/services/ai-supervision/symbols.js +259 -0
  27. package/src/services/cliproxy/index.js +256 -0
  28. package/src/services/cliproxy/installer.js +111 -0
  29. package/src/services/cliproxy/manager.js +387 -0
  30. package/src/services/index.js +9 -1
  31. package/src/services/llmproxy/index.js +166 -0
  32. package/src/services/llmproxy/manager.js +411 -0
  33. package/src/services/rithmic/accounts.js +6 -8
  34. package/src/ui/box.js +5 -9
  35. package/src/ui/index.js +18 -5
  36. package/src/ui/menu.js +4 -4
@@ -7,13 +7,17 @@ const ora = require('ora');
7
7
 
8
8
  const { connections } = require('../services');
9
9
  const { ACCOUNT_STATUS, ACCOUNT_TYPE } = require('../config');
10
- const { getLogoWidth, getColWidths, drawBoxHeader, drawBoxFooter, draw2ColHeader, visibleLength, displayBanner } = require('../ui');
10
+ const { getLogoWidth, getColWidths, drawBoxHeader, drawBoxFooter, draw2ColHeader, visibleLength, displayBanner, clearScreen } = require('../ui');
11
11
  const { prompts } = require('../utils');
12
12
 
13
13
  /**
14
14
  * Show all accounts
15
15
  */
16
16
  const showAccounts = async (service) => {
17
+ // Clear screen and show banner
18
+ clearScreen();
19
+ displayBanner();
20
+
17
21
  const boxWidth = getLogoWidth();
18
22
  const { col1, col2 } = getColWidths(boxWidth);
19
23
 
@@ -28,13 +32,12 @@ const showAccounts = async (service) => {
28
32
  let spinner;
29
33
 
30
34
  try {
31
- // Single spinner for loading (appears below the dashboard header)
32
- spinner = ora({ text: 'Loading accounts...', color: 'yellow' }).start();
35
+ spinner = ora({ text: 'LOADING ACCOUNTS...', color: 'yellow' }).start();
33
36
 
34
37
  const allConns = connections.count() > 0 ? connections.getAll() : (service ? [{ service, propfirm: service.propfirm?.name || 'Unknown', type: 'single' }] : []);
35
38
 
36
39
  if (allConns.length === 0) {
37
- spinner.fail('No connections found');
40
+ spinner.fail('NO CONNECTIONS FOUND');
38
41
  await prompts.waitForEnter();
39
42
  return;
40
43
  }
@@ -60,7 +63,7 @@ const showAccounts = async (service) => {
60
63
  }
61
64
 
62
65
  if (allAccounts.length === 0) {
63
- spinner.fail('No accounts found');
66
+ spinner.fail('NO ACCOUNTS FOUND');
64
67
  await prompts.waitForEnter();
65
68
  return;
66
69
  }
@@ -78,8 +81,11 @@ const showAccounts = async (service) => {
78
81
  } catch (e) {}
79
82
  }
80
83
 
81
- spinner.succeed('Accounts loaded');
82
- console.log();
84
+ spinner.stop();
85
+
86
+ // Clear and show banner again before displaying accounts
87
+ clearScreen();
88
+ displayBanner();
83
89
 
84
90
  // Display accounts
85
91
  drawBoxHeader('TRADING ACCOUNTS', boxWidth);
@@ -91,12 +97,20 @@ const showAccounts = async (service) => {
91
97
  const name1 = String(acc1.accountName || acc1.rithmicAccountId || acc1.accountId || `Account #${i + 1}`);
92
98
  const name2 = acc2 ? String(acc2.accountName || acc2.rithmicAccountId || acc2.accountId || `Account #${i + 2}`) : '';
93
99
 
94
- draw2ColHeader(name1.substring(0, col1 - 4), name2 ? name2.substring(0, col2 - 4) : '', boxWidth);
100
+ // For single account, use full width; for pairs, use 2-column layout
101
+ const sep = acc2 ? '│' : '║';
102
+ const rightCol = acc2 ? col2 : col2;
103
+
104
+ // Header row with account name(s)
105
+ const h1 = centerText(name1.substring(0, col1 - 4), col1);
106
+ const h2 = acc2 ? centerText(name2.substring(0, col2 - 4), col2) : ' '.repeat(col2);
107
+ console.log(chalk.cyan('║') + chalk.cyan.bold(h1) + chalk.cyan(sep) + chalk.cyan.bold(h2) + chalk.cyan('║'));
108
+ console.log(chalk.cyan('╠') + chalk.cyan('─'.repeat(col1)) + chalk.cyan(acc2 ? '┼' : '┼') + chalk.cyan('─'.repeat(col2)) + chalk.cyan('╣'));
95
109
 
96
110
  // PropFirm
97
111
  const pf1 = chalk.magenta(acc1.propfirm || 'Unknown');
98
112
  const pf2 = acc2 ? chalk.magenta(acc2.propfirm || 'Unknown') : '';
99
- console.log(chalk.cyan('║') + fmtRow('PropFirm:', pf1, col1) + chalk.cyan('│') + (acc2 ? fmtRow('PropFirm:', pf2, col2) : ' '.repeat(col2)) + chalk.cyan('║'));
113
+ console.log(chalk.cyan('║') + fmtRow('PropFirm:', pf1, col1) + chalk.cyan(sep) + (acc2 ? fmtRow('PropFirm:', pf2, col2) : ' '.repeat(col2)) + chalk.cyan('║'));
100
114
 
101
115
  // Balance
102
116
  const bal1 = acc1.balance;
@@ -105,7 +119,7 @@ const showAccounts = async (service) => {
105
119
  const balStr2 = bal2 !== null && bal2 !== undefined ? '$' + Number(bal2).toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2}) : '--';
106
120
  const balColor1 = bal1 === null || bal1 === undefined ? chalk.gray : (bal1 >= 0 ? chalk.green : chalk.red);
107
121
  const balColor2 = bal2 === null || bal2 === undefined ? chalk.gray : (bal2 >= 0 ? chalk.green : chalk.red);
108
- console.log(chalk.cyan('║') + fmtRow('Balance:', balColor1(balStr1), col1) + chalk.cyan('│') + (acc2 ? fmtRow('Balance:', balColor2(balStr2), col2) : ' '.repeat(col2)) + chalk.cyan('║'));
122
+ console.log(chalk.cyan('║') + fmtRow('Balance:', balColor1(balStr1), col1) + chalk.cyan(sep) + (acc2 ? fmtRow('Balance:', balColor2(balStr2), col2) : ' '.repeat(col2)) + chalk.cyan('║'));
109
123
 
110
124
  // P&L
111
125
  const pnl1 = acc1.profitAndLoss;
@@ -114,44 +128,41 @@ const showAccounts = async (service) => {
114
128
  const pnlStr2 = pnl2 !== null && pnl2 !== undefined ? (pnl2 >= 0 ? '+' : '') + '$' + Number(pnl2).toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2}) : '--';
115
129
  const pnlColor1 = pnl1 === null || pnl1 === undefined ? chalk.gray : (pnl1 >= 0 ? chalk.green : chalk.red);
116
130
  const pnlColor2 = pnl2 === null || pnl2 === undefined ? chalk.gray : (pnl2 >= 0 ? chalk.green : chalk.red);
117
- console.log(chalk.cyan('║') + fmtRow('P&L:', pnlColor1(pnlStr1), col1) + chalk.cyan('│') + (acc2 ? fmtRow('P&L:', pnlColor2(pnlStr2), col2) : ' '.repeat(col2)) + chalk.cyan('║'));
131
+ console.log(chalk.cyan('║') + fmtRow('P&L:', pnlColor1(pnlStr1), col1) + chalk.cyan(sep) + (acc2 ? fmtRow('P&L:', pnlColor2(pnlStr2), col2) : ' '.repeat(col2)) + chalk.cyan('║'));
118
132
 
119
- // Status - handle both string from API and numeric lookup
120
- const getStatusDisplay = (status) => {
121
- if (!status && status !== 0) return { text: '--', color: 'gray' };
133
+ // Status - from Rithmic RMS API (field 154003), N/A if not available
134
+ const getStatusDisplay = (acc) => {
135
+ const status = acc.status;
136
+ if (status === null || status === undefined) return { text: 'N/A', color: 'gray' };
122
137
  if (typeof status === 'string') {
123
- // Direct string from Rithmic API (e.g., "Active", "Disabled")
124
138
  const lowerStatus = status.toLowerCase();
125
139
  if (lowerStatus.includes('active') || lowerStatus.includes('open')) return { text: status, color: 'green' };
126
140
  if (lowerStatus.includes('disabled') || lowerStatus.includes('closed')) return { text: status, color: 'red' };
127
141
  if (lowerStatus.includes('halt')) return { text: status, color: 'red' };
128
142
  return { text: status, color: 'yellow' };
129
143
  }
130
- return ACCOUNT_STATUS[status] || { text: 'Unknown', color: 'gray' };
144
+ return { text: String(status), color: 'yellow' };
131
145
  };
132
- const status1 = getStatusDisplay(acc1.status);
133
- const status2 = acc2 ? getStatusDisplay(acc2.status) : null;
134
- console.log(chalk.cyan('║') + fmtRow('Status:', chalk[status1.color](status1.text), col1) + chalk.cyan('│') + (acc2 ? fmtRow('Status:', chalk[status2.color](status2.text), col2) : ' '.repeat(col2)) + chalk.cyan('║'));
135
-
136
- // Type/Algorithm - handle both string from API and numeric lookup
137
- const getTypeDisplay = (type, algorithm) => {
138
- // Prefer algorithm from RMS info if available
139
- const value = algorithm || type;
140
- if (!value && value !== 0) return { text: '--', color: 'gray' };
141
- if (typeof value === 'string') {
142
- // Direct string from Rithmic API
143
- const lowerValue = value.toLowerCase();
144
- if (lowerValue.includes('eval')) return { text: value, color: 'yellow' };
145
- if (lowerValue.includes('live') || lowerValue.includes('funded')) return { text: value, color: 'green' };
146
- if (lowerValue.includes('sim') || lowerValue.includes('demo')) return { text: value, color: 'gray' };
147
- if (lowerValue.includes('express')) return { text: value, color: 'magenta' };
148
- return { text: value, color: 'cyan' };
146
+ const status1 = getStatusDisplay(acc1);
147
+ const status2 = acc2 ? getStatusDisplay(acc2) : null;
148
+ console.log(chalk.cyan('║') + fmtRow('Status:', chalk[status1.color](status1.text), col1) + chalk.cyan(sep) + (acc2 ? fmtRow('Status:', chalk[status2.color](status2.text), col2) : ' '.repeat(col2)) + chalk.cyan('║'));
149
+
150
+ // Algorithm - from Rithmic RMS API (field 150142), N/A if not available
151
+ const getAlgorithmDisplay = (acc) => {
152
+ const algo = acc.algorithm;
153
+ if (algo === null || algo === undefined) return { text: 'N/A', color: 'gray' };
154
+ if (typeof algo === 'string') {
155
+ const lowerAlgo = algo.toLowerCase();
156
+ if (lowerAlgo.includes('eval')) return { text: algo, color: 'yellow' };
157
+ if (lowerAlgo.includes('live') || lowerAlgo.includes('funded')) return { text: algo, color: 'green' };
158
+ if (lowerAlgo.includes('sim') || lowerAlgo.includes('demo')) return { text: algo, color: 'gray' };
159
+ return { text: algo, color: 'cyan' };
149
160
  }
150
- return ACCOUNT_TYPE[value] || { text: 'Unknown', color: 'white' };
161
+ return { text: String(algo), color: 'cyan' };
151
162
  };
152
- const type1 = getTypeDisplay(acc1.type, acc1.algorithm);
153
- const type2 = acc2 ? getTypeDisplay(acc2.type, acc2.algorithm) : null;
154
- console.log(chalk.cyan('║') + fmtRow('Type:', chalk[type1.color](type1.text), col1) + chalk.cyan('│') + (acc2 ? fmtRow('Type:', chalk[type2.color](type2.text), col2) : ' '.repeat(col2)) + chalk.cyan('║'));
163
+ const algo1 = getAlgorithmDisplay(acc1);
164
+ const algo2 = acc2 ? getAlgorithmDisplay(acc2) : null;
165
+ console.log(chalk.cyan('║') + fmtRow('Algorithm:', chalk[algo1.color](algo1.text), col1) + chalk.cyan(sep) + (acc2 ? fmtRow('Algorithm:', chalk[algo2.color](algo2.text), col2) : ' '.repeat(col2)) + chalk.cyan('║'));
155
166
 
156
167
  if (i + 2 < allAccounts.length) {
157
168
  console.log(chalk.cyan('╠') + chalk.cyan('═'.repeat(col1)) + chalk.cyan('╪') + chalk.cyan('═'.repeat(col2)) + chalk.cyan('╣'));
@@ -162,7 +173,7 @@ const showAccounts = async (service) => {
162
173
  console.log();
163
174
 
164
175
  } catch (error) {
165
- if (spinner) spinner.fail('Error loading accounts: ' + error.message);
176
+ if (spinner) spinner.fail('ERROR LOADING ACCOUNTS: ' + error.message.toUpperCase());
166
177
  }
167
178
 
168
179
  await prompts.waitForEnter();
@@ -0,0 +1,388 @@
1
+ /**
2
+ * AI Agents UI Components
3
+ *
4
+ * UI drawing functions for the AI Agents configuration page.
5
+ */
6
+
7
+ const chalk = require('chalk');
8
+ const ora = require('ora');
9
+ const { centerText, visibleLength } = require('../ui');
10
+ const cliproxy = require('../services/cliproxy');
11
+ const { runPreflightCheck, formatPreflightResults, getPreflightSummary } = require('../services/ai-supervision');
12
+
13
+ /**
14
+ * Draw a 2-column row with perfect alignment
15
+ * @param {string} leftText - Left column text
16
+ * @param {string} rightText - Right column text
17
+ * @param {number} W - Inner width
18
+ * @param {number} padding - Left padding for each column (default 3)
19
+ */
20
+ const draw2ColRow = (leftText, rightText, W, padding = 3) => {
21
+ const colWidth = Math.floor(W / 2);
22
+ const leftLen = visibleLength(leftText);
23
+ const rightLen = visibleLength(rightText || '');
24
+
25
+ // Left column: padding + text + fill to colWidth
26
+ const leftFill = colWidth - padding - leftLen;
27
+ const leftCol = ' '.repeat(padding) + leftText + ' '.repeat(Math.max(0, leftFill));
28
+
29
+ // Right column: padding + text + fill to remaining width
30
+ const rightColWidth = W - colWidth;
31
+ const rightFill = rightColWidth - padding - rightLen;
32
+ const rightCol = ' '.repeat(padding) + (rightText || '') + ' '.repeat(Math.max(0, rightFill));
33
+
34
+ console.log(chalk.cyan('║') + leftCol + rightCol + chalk.cyan('║'));
35
+ };
36
+
37
+ /**
38
+ * Draw 2-column table with title and back option
39
+ * @param {string} title - Table title
40
+ * @param {Function} titleColor - Chalk color function
41
+ * @param {Array} items - Items to display
42
+ * @param {string} backText - Back button text
43
+ * @param {number} W - Inner width
44
+ */
45
+ const draw2ColTable = (title, titleColor, items, backText, W) => {
46
+ // New rectangle (banner is always closed)
47
+ console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
48
+ console.log(chalk.cyan('║') + titleColor(centerText(title, W)) + chalk.cyan('║'));
49
+ console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
50
+
51
+ const rows = Math.ceil(items.length / 2);
52
+ for (let row = 0; row < rows; row++) {
53
+ const left = items[row];
54
+ const right = items[row + rows];
55
+ draw2ColRow(left || '', right || '', W);
56
+ }
57
+
58
+ console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
59
+ console.log(chalk.cyan('║') + chalk.red(centerText(backText, W)) + chalk.cyan('║'));
60
+ console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
61
+ };
62
+
63
+ /**
64
+ * Draw centered 2-column row
65
+ * @param {string} leftText - Left column text
66
+ * @param {string} rightText - Right column text
67
+ * @param {number} W - Inner width
68
+ */
69
+ const draw2ColRowCentered = (leftText, rightText, W) => {
70
+ const colWidth = Math.floor(W / 2);
71
+ const leftLen = visibleLength(leftText);
72
+ const rightLen = visibleLength(rightText || '');
73
+
74
+ // Center left text in left column
75
+ const leftPadTotal = colWidth - leftLen;
76
+ const leftPadL = Math.floor(leftPadTotal / 2);
77
+ const leftPadR = leftPadTotal - leftPadL;
78
+ const leftCol = ' '.repeat(Math.max(0, leftPadL)) + leftText + ' '.repeat(Math.max(0, leftPadR));
79
+
80
+ // Center right text in right column
81
+ const rightColWidth = W - colWidth;
82
+ const rightPadTotal = rightColWidth - rightLen;
83
+ const rightPadL = Math.floor(rightPadTotal / 2);
84
+ const rightPadR = rightPadTotal - rightPadL;
85
+ const rightCol = ' '.repeat(Math.max(0, rightPadL)) + (rightText || '') + ' '.repeat(Math.max(0, rightPadR));
86
+
87
+ console.log(chalk.cyan('║') + leftCol + rightCol + chalk.cyan('║'));
88
+ };
89
+
90
+ /**
91
+ * Draw providers table with vertically aligned columns
92
+ * @param {Array} providers - List of AI providers
93
+ * @param {Object} config - Current config
94
+ * @param {number} boxWidth - Box width
95
+ * @param {boolean} showTest - Show [T] TEST option
96
+ */
97
+ const drawProvidersTable = (providers, config, boxWidth, showTest = false) => {
98
+ const W = boxWidth - 2;
99
+ const colWidth = Math.floor(W / 2);
100
+
101
+ // Get connected providers (have auth files)
102
+ const connected = cliproxy.getConnectedProviders();
103
+
104
+ // New rectangle (banner is always closed)
105
+ console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
106
+ console.log(chalk.cyan('║') + chalk.cyan.bold(centerText('AI AGENTS CONFIGURATION', W)) + chalk.cyan('║'));
107
+ console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
108
+
109
+ const rows = Math.ceil(providers.length / 2);
110
+
111
+ // Find max name length across ALL providers for consistent alignment
112
+ const maxNameLen = Math.max(...providers.map(p => p.name.length));
113
+
114
+ // Fixed format: "● [XX] NAME" where XX is 2-digit padded number
115
+ // Total content width = 2 (● ) + 4 ([XX]) + 1 (space) + maxNameLen
116
+ const contentWidth = 2 + 4 + 1 + maxNameLen;
117
+ const leftPad = Math.floor((colWidth - contentWidth) / 2);
118
+ const rightPad = Math.floor(((W - colWidth) - contentWidth) / 2);
119
+
120
+ for (let row = 0; row < rows; row++) {
121
+ const leftP = providers[row];
122
+ const rightP = providers[row + rows];
123
+
124
+ // Left column
125
+ let leftCol = '';
126
+ if (leftP) {
127
+ const num = String(row + 1).padStart(2);
128
+ // Show cyan dot if provider has auth file (connected via OAuth)
129
+ const isConnected = connected[leftP.id] || config.providers[leftP.id]?.active;
130
+ const status = isConnected ? chalk.cyan('● ') : ' ';
131
+ const name = leftP.provider ? leftP.provider.name : leftP.name;
132
+ const namePadded = name.toUpperCase().padEnd(maxNameLen);
133
+ const content = status + chalk.yellow(`[${num}]`) + ' ' + chalk.cyan(namePadded);
134
+ const contentLen = 2 + 4 + 1 + maxNameLen;
135
+ const padR = colWidth - leftPad - contentLen;
136
+ leftCol = ' '.repeat(leftPad) + content + ' '.repeat(Math.max(0, padR));
137
+ } else {
138
+ leftCol = ' '.repeat(colWidth);
139
+ }
140
+
141
+ // Right column
142
+ let rightCol = '';
143
+ const rightColWidth = W - colWidth;
144
+ if (rightP) {
145
+ const num = String(row + rows + 1).padStart(2);
146
+ // Show cyan dot if provider has auth file (connected via OAuth)
147
+ const isConnected = connected[rightP.id] || config.providers[rightP.id]?.active;
148
+ const status = isConnected ? chalk.cyan('● ') : ' ';
149
+ const name = rightP.provider ? rightP.provider.name : rightP.name;
150
+ const namePadded = name.toUpperCase().padEnd(maxNameLen);
151
+ const content = status + chalk.yellow(`[${num}]`) + ' ' + chalk.cyan(namePadded);
152
+ const contentLen = 2 + 4 + 1 + maxNameLen;
153
+ const padR2 = rightColWidth - rightPad - contentLen;
154
+ rightCol = ' '.repeat(rightPad) + content + ' '.repeat(Math.max(0, padR2));
155
+ } else {
156
+ rightCol = ' '.repeat(rightColWidth);
157
+ }
158
+
159
+ console.log(chalk.cyan('║') + leftCol + rightCol + chalk.cyan('║'));
160
+ }
161
+
162
+ // Show [T] TEST option if agents are configured
163
+ if (showTest) {
164
+ console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
165
+ console.log(chalk.cyan('║') + chalk.green(centerText('[T] TEST ALL CONNECTIONS', W)) + chalk.cyan('║'));
166
+ }
167
+
168
+ console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
169
+ console.log(chalk.cyan('║') + chalk.red(centerText('[B] BACK TO MENU', W)) + chalk.cyan('║'));
170
+ console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
171
+ };
172
+
173
+ /**
174
+ * Draw models table with 2-column layout
175
+ * @param {Object} provider - Provider object
176
+ * @param {Array} models - List of models
177
+ * @param {number} boxWidth - Box width
178
+ */
179
+ const drawModelsTable = (provider, models, boxWidth) => {
180
+ const W = boxWidth - 2;
181
+ const colWidth = Math.floor(W / 2);
182
+
183
+ // New rectangle
184
+ console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
185
+ console.log(chalk.cyan('║') + chalk[provider.color].bold(centerText(`${provider.name.toUpperCase()} - SELECT MODEL`, W)) + chalk.cyan('║'));
186
+ console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
187
+
188
+ // Calculate rows (2 columns)
189
+ const rows = Math.ceil(models.length / 2);
190
+
191
+ // Find max model name length for alignment
192
+ const maxNameLen = Math.min(
193
+ Math.max(...models.map(m => m.name.length)),
194
+ colWidth - 8 // [XX] + space + padding
195
+ );
196
+
197
+ for (let row = 0; row < rows; row++) {
198
+ const leftIdx = row;
199
+ const rightIdx = row + rows;
200
+ const leftModel = models[leftIdx];
201
+ const rightModel = models[rightIdx];
202
+
203
+ // Left column
204
+ let leftCol = '';
205
+ if (leftModel) {
206
+ const num = String(leftIdx + 1).padStart(2);
207
+ const name = leftModel.name.length > maxNameLen
208
+ ? leftModel.name.substring(0, maxNameLen - 2) + '..'
209
+ : leftModel.name.padEnd(maxNameLen);
210
+ leftCol = ` ${chalk.cyan(`[${num}]`)} ${chalk.white(name)}`;
211
+ const leftLen = 2 + 4 + 1 + maxNameLen; // padding + [XX] + space + name
212
+ leftCol += ' '.repeat(Math.max(0, colWidth - leftLen));
213
+ } else {
214
+ leftCol = ' '.repeat(colWidth);
215
+ }
216
+
217
+ // Right column
218
+ let rightCol = '';
219
+ const rightColWidth = W - colWidth;
220
+ if (rightModel) {
221
+ const num = String(rightIdx + 1).padStart(2);
222
+ const name = rightModel.name.length > maxNameLen
223
+ ? rightModel.name.substring(0, maxNameLen - 2) + '..'
224
+ : rightModel.name.padEnd(maxNameLen);
225
+ rightCol = ` ${chalk.cyan(`[${num}]`)} ${chalk.white(name)}`;
226
+ const rightLen = 2 + 4 + 1 + maxNameLen;
227
+ rightCol += ' '.repeat(Math.max(0, rightColWidth - rightLen));
228
+ } else {
229
+ rightCol = ' '.repeat(rightColWidth);
230
+ }
231
+
232
+ console.log(chalk.cyan('║') + leftCol + rightCol + chalk.cyan('║'));
233
+ }
234
+
235
+ console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
236
+ console.log(chalk.cyan('║') + chalk.red(centerText('[B] BACK', W)) + chalk.cyan('║'));
237
+ console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
238
+ };
239
+
240
+ /**
241
+ * Draw provider configuration window
242
+ * Shows connection options based on provider capabilities (OAuth and/or API Key)
243
+ * @param {Object} provider - Provider object with supportsOAuth and supportsApiKey flags
244
+ * @param {Object} config - Current config
245
+ * @param {number} boxWidth - Box width
246
+ */
247
+ const drawProviderWindow = (provider, config, boxWidth) => {
248
+ const W = boxWidth - 2;
249
+ const col1Width = Math.floor(W / 2);
250
+ const col2Width = W - col1Width;
251
+ const providerConfig = config.providers[provider.id] || {};
252
+
253
+ // Check provider capabilities (default to both if not specified)
254
+ const supportsOAuth = provider.supportsOAuth !== false;
255
+ const supportsApiKey = provider.supportsApiKey !== false;
256
+
257
+ // New rectangle (banner is always closed)
258
+ console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
259
+ console.log(chalk.cyan('║') + chalk[provider.color].bold(centerText(provider.name.toUpperCase(), W)) + chalk.cyan('║'));
260
+ console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
261
+
262
+ // Display connection options based on provider capabilities
263
+ if (supportsOAuth && supportsApiKey) {
264
+ // Both options: 2 columns
265
+ const opt1 = '[1] CONNECT VIA PAID PLAN';
266
+ const opt2 = '[2] CONNECT VIA API KEY';
267
+
268
+ const left1 = chalk.green(opt1);
269
+ const right1 = chalk.yellow(opt2);
270
+ const left1Len = visibleLength(left1);
271
+ const right1Len = visibleLength(right1);
272
+ const left1PadTotal = col1Width - left1Len;
273
+ const left1PadL = Math.floor(left1PadTotal / 2);
274
+ const left1PadR = left1PadTotal - left1PadL;
275
+ const right1PadTotal = col2Width - right1Len;
276
+ const right1PadL = Math.floor(right1PadTotal / 2);
277
+ const right1PadR = right1PadTotal - right1PadL;
278
+
279
+ console.log(
280
+ chalk.cyan('║') +
281
+ ' '.repeat(left1PadL) + left1 + ' '.repeat(left1PadR) +
282
+ ' '.repeat(right1PadL) + right1 + ' '.repeat(right1PadR) +
283
+ chalk.cyan('║')
284
+ );
285
+ } else if (supportsApiKey) {
286
+ // API Key only: centered single option
287
+ const opt = '[1] CONNECT VIA API KEY';
288
+ console.log(chalk.cyan('║') + chalk.yellow(centerText(opt, W)) + chalk.cyan('║'));
289
+ } else if (supportsOAuth) {
290
+ // OAuth only: centered single option
291
+ const opt = '[1] CONNECT VIA PAID PLAN';
292
+ console.log(chalk.cyan('║') + chalk.green(centerText(opt, W)) + chalk.cyan('║'));
293
+ }
294
+
295
+ // Status bar
296
+ console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
297
+
298
+ let statusText = '';
299
+ if (providerConfig.active) {
300
+ const connType = providerConfig.connectionType === 'cliproxy' ? 'CLIPROXY' : 'API KEY';
301
+ const modelName = (providerConfig.modelName || 'N/A').toUpperCase();
302
+ statusText = chalk.green('● ACTIVE') + chalk.gray(' MODEL: ') + chalk.yellow(modelName) + chalk.gray(' VIA ') + chalk.cyan(connType);
303
+ } else if (providerConfig.apiKey || providerConfig.connectionType) {
304
+ statusText = chalk.yellow('● CONFIGURED') + chalk.gray(' (NOT ACTIVE)');
305
+ } else {
306
+ statusText = chalk.gray('○ NOT CONFIGURED');
307
+ }
308
+ console.log(chalk.cyan('║') + centerText(statusText, W) + chalk.cyan('║'));
309
+
310
+ // Disconnect option if active
311
+ if (providerConfig.active) {
312
+ console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
313
+ console.log(chalk.cyan('║') + chalk.red(centerText('[D] DISCONNECT', W)) + chalk.cyan('║'));
314
+ }
315
+
316
+ // Back
317
+ console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
318
+ console.log(chalk.cyan('║') + chalk.red(centerText('[B] BACK', W)) + chalk.cyan('║'));
319
+ console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
320
+ };
321
+
322
+ /**
323
+ * Draw and run connection test for all agents
324
+ * @param {Array} agents - Array of agent configs
325
+ * @param {number} boxWidth - Box width
326
+ * @param {Function} clearWithBanner - Function to clear and show banner
327
+ * @returns {Promise<Object>} Test results
328
+ */
329
+ const drawConnectionTest = async (agents, boxWidth, clearWithBanner) => {
330
+ if (agents.length === 0) {
331
+ console.log(chalk.yellow('\n No agents configured. Connect an agent first.'));
332
+ return { success: false, error: 'No agents' };
333
+ }
334
+
335
+ const W = boxWidth - 2;
336
+
337
+ // Show loading state with spinner
338
+ clearWithBanner();
339
+ console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
340
+ console.log(chalk.cyan('║') + chalk.yellow.bold(centerText('AI AGENTS CONNECTION TEST', W)) + chalk.cyan('║'));
341
+ console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
342
+ console.log(chalk.cyan('║') + ' '.repeat(W) + chalk.cyan('║'));
343
+
344
+ // Start spinner on the loading line
345
+ const spinnerText = 'Testing connections... Please wait';
346
+ const spinner = ora({
347
+ text: spinnerText,
348
+ spinner: 'dots',
349
+ color: 'yellow'
350
+ }).start();
351
+
352
+ // Run pre-flight check
353
+ const results = await runPreflightCheck(agents);
354
+
355
+ // Stop spinner
356
+ spinner.stop();
357
+
358
+ // Clear and redraw with results
359
+ clearWithBanner();
360
+ console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
361
+ console.log(chalk.cyan('║') + chalk.yellow.bold(centerText('AI AGENTS CONNECTION TEST', W)) + chalk.cyan('║'));
362
+ console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
363
+
364
+ // Display results
365
+ const lines = formatPreflightResults(results, boxWidth);
366
+ for (const line of lines) {
367
+ const lineLen = visibleLength(line);
368
+ const padding = Math.max(0, W - lineLen);
369
+ console.log(chalk.cyan('║') + line + ' '.repeat(padding) + chalk.cyan('║'));
370
+ }
371
+
372
+ // Summary
373
+ console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
374
+ const summary = getPreflightSummary(results);
375
+ console.log(chalk.cyan('║') + centerText(summary.text, W) + chalk.cyan('║'));
376
+ console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
377
+
378
+ return results;
379
+ };
380
+
381
+ module.exports = {
382
+ draw2ColRow,
383
+ draw2ColTable,
384
+ drawProvidersTable,
385
+ drawModelsTable,
386
+ drawProviderWindow,
387
+ drawConnectionTest
388
+ };