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.
- package/package.json +1 -1
- package/src/app.js +64 -42
- package/src/menus/connect.js +17 -14
- package/src/menus/dashboard.js +76 -58
- package/src/pages/accounts.js +49 -38
- package/src/pages/ai-agents-ui.js +388 -0
- package/src/pages/ai-agents.js +494 -0
- package/src/pages/ai-models.js +389 -0
- package/src/pages/algo/algo-executor.js +307 -0
- package/src/pages/algo/copy-executor.js +331 -0
- package/src/pages/algo/copy-trading.js +178 -546
- package/src/pages/algo/custom-strategy.js +313 -0
- package/src/pages/algo/index.js +75 -18
- package/src/pages/algo/one-account.js +57 -322
- package/src/pages/algo/ui.js +15 -15
- package/src/pages/orders.js +22 -19
- package/src/pages/positions.js +22 -19
- package/src/pages/stats/index.js +16 -15
- package/src/pages/user.js +11 -7
- package/src/services/ai-supervision/consensus.js +284 -0
- package/src/services/ai-supervision/context.js +275 -0
- package/src/services/ai-supervision/directive.js +167 -0
- package/src/services/ai-supervision/health.js +47 -35
- package/src/services/ai-supervision/index.js +359 -0
- package/src/services/ai-supervision/parser.js +278 -0
- package/src/services/ai-supervision/symbols.js +259 -0
- package/src/services/cliproxy/index.js +256 -0
- package/src/services/cliproxy/installer.js +111 -0
- package/src/services/cliproxy/manager.js +387 -0
- package/src/services/index.js +9 -1
- package/src/services/llmproxy/index.js +166 -0
- package/src/services/llmproxy/manager.js +411 -0
- package/src/services/rithmic/accounts.js +6 -8
- package/src/ui/box.js +5 -9
- package/src/ui/index.js +18 -5
- package/src/ui/menu.js +4 -4
package/src/pages/accounts.js
CHANGED
|
@@ -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
|
-
|
|
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('
|
|
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('
|
|
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.
|
|
82
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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 -
|
|
120
|
-
const getStatusDisplay = (
|
|
121
|
-
|
|
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
|
|
144
|
+
return { text: String(status), color: 'yellow' };
|
|
131
145
|
};
|
|
132
|
-
const status1 = getStatusDisplay(acc1
|
|
133
|
-
const status2 = acc2 ? getStatusDisplay(acc2
|
|
134
|
-
console.log(chalk.cyan('║') + fmtRow('Status:', chalk[status1.color](status1.text), col1) + chalk.cyan(
|
|
135
|
-
|
|
136
|
-
//
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (
|
|
145
|
-
|
|
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
|
|
161
|
+
return { text: String(algo), color: 'cyan' };
|
|
151
162
|
};
|
|
152
|
-
const
|
|
153
|
-
const
|
|
154
|
-
console.log(chalk.cyan('║') + fmtRow('
|
|
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('
|
|
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
|
+
};
|