hedgequantx 2.7.18 → 2.7.19
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 +2 -3
- package/src/pages/ai-agents.js +407 -0
package/package.json
CHANGED
package/src/app.js
CHANGED
|
@@ -17,6 +17,7 @@ const log = logger.scope('App');
|
|
|
17
17
|
const { showStats } = require('./pages/stats');
|
|
18
18
|
const { showAccounts } = require('./pages/accounts');
|
|
19
19
|
const { algoTradingMenu } = require('./pages/algo');
|
|
20
|
+
const { aiAgentsMenu, getActiveAgentCount } = require('./pages/ai-agents');
|
|
20
21
|
|
|
21
22
|
// Menus
|
|
22
23
|
const { rithmicMenu, dashboardMenu, handleUpdate } = require('./menus');
|
|
@@ -296,9 +297,7 @@ const run = async () => {
|
|
|
296
297
|
break;
|
|
297
298
|
|
|
298
299
|
case 'aiagents':
|
|
299
|
-
|
|
300
|
-
console.log(chalk.gray(' Configure AI trading agents for automated strategies.'));
|
|
301
|
-
await prompts.waitForEnter();
|
|
300
|
+
await aiAgentsMenu();
|
|
302
301
|
break;
|
|
303
302
|
|
|
304
303
|
case 'update':
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Agents Configuration Page
|
|
3
|
+
*
|
|
4
|
+
* Allows users to configure AI providers for trading strategies.
|
|
5
|
+
* Supports both CLIProxy (paid plans) and direct API keys.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const chalk = require('chalk');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
|
|
13
|
+
const { getLogoWidth, centerText, visibleLength } = require('../ui');
|
|
14
|
+
const { prompts } = require('../utils');
|
|
15
|
+
|
|
16
|
+
// Config file path
|
|
17
|
+
const CONFIG_DIR = path.join(os.homedir(), '.hqx');
|
|
18
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'ai-config.json');
|
|
19
|
+
|
|
20
|
+
// AI Providers list
|
|
21
|
+
const AI_PROVIDERS = [
|
|
22
|
+
{ id: 'anthropic', name: 'Anthropic (Claude)', color: 'magenta' },
|
|
23
|
+
{ id: 'openai', name: 'OpenAI (GPT)', color: 'green' },
|
|
24
|
+
{ id: 'google', name: 'Google (Gemini)', color: 'blue' },
|
|
25
|
+
{ id: 'mistral', name: 'Mistral AI', color: 'yellow' },
|
|
26
|
+
{ id: 'groq', name: 'Groq', color: 'cyan' },
|
|
27
|
+
{ id: 'xai', name: 'xAI (Grok)', color: 'white' },
|
|
28
|
+
{ id: 'perplexity', name: 'Perplexity', color: 'blue' },
|
|
29
|
+
{ id: 'openrouter', name: 'OpenRouter', color: 'gray' },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Load AI config from file
|
|
34
|
+
* @returns {Object} Config object with provider settings
|
|
35
|
+
*/
|
|
36
|
+
const loadConfig = () => {
|
|
37
|
+
try {
|
|
38
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
39
|
+
const data = fs.readFileSync(CONFIG_FILE, 'utf8');
|
|
40
|
+
return JSON.parse(data);
|
|
41
|
+
}
|
|
42
|
+
} catch (error) {
|
|
43
|
+
// Config file doesn't exist or is invalid
|
|
44
|
+
}
|
|
45
|
+
return { providers: {} };
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Save AI config to file
|
|
50
|
+
* @param {Object} config - Config object to save
|
|
51
|
+
* @returns {boolean} Success status
|
|
52
|
+
*/
|
|
53
|
+
const saveConfig = (config) => {
|
|
54
|
+
try {
|
|
55
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
56
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
59
|
+
return true;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Mask API key for display (show first 8 and last 4 chars)
|
|
67
|
+
* @param {string} key - API key
|
|
68
|
+
* @returns {string} Masked key
|
|
69
|
+
*/
|
|
70
|
+
const maskKey = (key) => {
|
|
71
|
+
if (!key || key.length < 16) return '****';
|
|
72
|
+
return key.substring(0, 8) + '...' + key.substring(key.length - 4);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Draw the main providers selection table (2 columns)
|
|
77
|
+
* @param {Object} config - Current config
|
|
78
|
+
* @param {number} boxWidth - Box width
|
|
79
|
+
*/
|
|
80
|
+
const drawProvidersTable = (config, boxWidth) => {
|
|
81
|
+
const W = boxWidth - 2;
|
|
82
|
+
const col1Width = Math.floor(W / 2);
|
|
83
|
+
const col2Width = W - col1Width;
|
|
84
|
+
|
|
85
|
+
// Header
|
|
86
|
+
console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
|
|
87
|
+
console.log(chalk.cyan('║') + chalk.yellow.bold(centerText('AI AGENTS CONFIGURATION', W)) + chalk.cyan('║'));
|
|
88
|
+
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
89
|
+
|
|
90
|
+
// Calculate max name length for alignment
|
|
91
|
+
const maxNameLen = Math.max(...AI_PROVIDERS.map(p => p.name.length));
|
|
92
|
+
|
|
93
|
+
// Provider rows (2 columns)
|
|
94
|
+
const rows = Math.ceil(AI_PROVIDERS.length / 2);
|
|
95
|
+
for (let row = 0; row < rows; row++) {
|
|
96
|
+
const leftIdx = row;
|
|
97
|
+
const rightIdx = row + rows;
|
|
98
|
+
|
|
99
|
+
const leftProvider = AI_PROVIDERS[leftIdx];
|
|
100
|
+
const rightProvider = AI_PROVIDERS[rightIdx];
|
|
101
|
+
|
|
102
|
+
// Left column
|
|
103
|
+
const leftNum = `[${leftIdx + 1}]`;
|
|
104
|
+
const leftName = leftProvider.name;
|
|
105
|
+
const leftConfig = config.providers[leftProvider.id] || {};
|
|
106
|
+
const leftStatus = leftConfig.active ? chalk.green('●') : '';
|
|
107
|
+
const leftText = chalk.cyan(leftNum) + ' ' + chalk[leftProvider.color](leftName) + ' ' + leftStatus;
|
|
108
|
+
const leftLen = visibleLength(leftText);
|
|
109
|
+
const leftPadTotal = col1Width - leftLen;
|
|
110
|
+
const leftPadL = Math.floor(leftPadTotal / 2);
|
|
111
|
+
const leftPadR = leftPadTotal - leftPadL;
|
|
112
|
+
|
|
113
|
+
// Right column
|
|
114
|
+
let rightText = '';
|
|
115
|
+
let rightPadL = 0;
|
|
116
|
+
let rightPadR = col2Width;
|
|
117
|
+
if (rightProvider) {
|
|
118
|
+
const rightNum = `[${rightIdx + 1}]`;
|
|
119
|
+
const rightName = rightProvider.name;
|
|
120
|
+
const rightConfig = config.providers[rightProvider.id] || {};
|
|
121
|
+
const rightStatus = rightConfig.active ? chalk.green('●') : '';
|
|
122
|
+
rightText = chalk.cyan(rightNum) + ' ' + chalk[rightProvider.color](rightName) + ' ' + rightStatus;
|
|
123
|
+
const rightLen = visibleLength(rightText);
|
|
124
|
+
const rightPadTotal = col2Width - rightLen;
|
|
125
|
+
rightPadL = Math.floor(rightPadTotal / 2);
|
|
126
|
+
rightPadR = rightPadTotal - rightPadL;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log(
|
|
130
|
+
chalk.cyan('║') +
|
|
131
|
+
' '.repeat(leftPadL) + leftText + ' '.repeat(leftPadR) +
|
|
132
|
+
' '.repeat(rightPadL) + rightText + ' '.repeat(rightPadR) +
|
|
133
|
+
chalk.cyan('║')
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Footer
|
|
138
|
+
console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
|
|
139
|
+
console.log(chalk.cyan('║') + chalk.red(centerText('[B] Back to Menu', W)) + chalk.cyan('║'));
|
|
140
|
+
console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Draw provider configuration window
|
|
145
|
+
* @param {Object} provider - Provider object
|
|
146
|
+
* @param {Object} config - Current config
|
|
147
|
+
* @param {number} boxWidth - Box width
|
|
148
|
+
*/
|
|
149
|
+
const drawProviderWindow = (provider, config, boxWidth) => {
|
|
150
|
+
const W = boxWidth - 2;
|
|
151
|
+
const col1Width = Math.floor(W / 2);
|
|
152
|
+
const col2Width = W - col1Width;
|
|
153
|
+
const providerConfig = config.providers[provider.id] || {};
|
|
154
|
+
|
|
155
|
+
// Header
|
|
156
|
+
console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
|
|
157
|
+
console.log(chalk.cyan('║') + chalk[provider.color].bold(centerText(provider.name.toUpperCase(), W)) + chalk.cyan('║'));
|
|
158
|
+
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
159
|
+
|
|
160
|
+
// Empty line
|
|
161
|
+
console.log(chalk.cyan('║') + ' '.repeat(W) + chalk.cyan('║'));
|
|
162
|
+
|
|
163
|
+
// Options in 2 columns
|
|
164
|
+
const opt1Title = '[1] Connect via Paid Plan';
|
|
165
|
+
const opt1Desc = 'Uses CLIProxy - No API key needed';
|
|
166
|
+
const opt2Title = '[2] Connect via API Key';
|
|
167
|
+
const opt2Desc = 'Enter your own API key';
|
|
168
|
+
|
|
169
|
+
// Row 1: Titles
|
|
170
|
+
const left1 = chalk.green(opt1Title);
|
|
171
|
+
const right1 = chalk.yellow(opt2Title);
|
|
172
|
+
const left1Len = visibleLength(left1);
|
|
173
|
+
const right1Len = visibleLength(right1);
|
|
174
|
+
const left1PadTotal = col1Width - left1Len;
|
|
175
|
+
const left1PadL = Math.floor(left1PadTotal / 2);
|
|
176
|
+
const left1PadR = left1PadTotal - left1PadL;
|
|
177
|
+
const right1PadTotal = col2Width - right1Len;
|
|
178
|
+
const right1PadL = Math.floor(right1PadTotal / 2);
|
|
179
|
+
const right1PadR = right1PadTotal - right1PadL;
|
|
180
|
+
|
|
181
|
+
console.log(
|
|
182
|
+
chalk.cyan('║') +
|
|
183
|
+
' '.repeat(left1PadL) + left1 + ' '.repeat(left1PadR) +
|
|
184
|
+
' '.repeat(right1PadL) + right1 + ' '.repeat(right1PadR) +
|
|
185
|
+
chalk.cyan('║')
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// Row 2: Descriptions
|
|
189
|
+
const left2 = chalk.gray(opt1Desc);
|
|
190
|
+
const right2 = chalk.gray(opt2Desc);
|
|
191
|
+
const left2Len = visibleLength(left2);
|
|
192
|
+
const right2Len = visibleLength(right2);
|
|
193
|
+
const left2PadTotal = col1Width - left2Len;
|
|
194
|
+
const left2PadL = Math.floor(left2PadTotal / 2);
|
|
195
|
+
const left2PadR = left2PadTotal - left2PadL;
|
|
196
|
+
const right2PadTotal = col2Width - right2Len;
|
|
197
|
+
const right2PadL = Math.floor(right2PadTotal / 2);
|
|
198
|
+
const right2PadR = right2PadTotal - right2PadL;
|
|
199
|
+
|
|
200
|
+
console.log(
|
|
201
|
+
chalk.cyan('║') +
|
|
202
|
+
' '.repeat(left2PadL) + left2 + ' '.repeat(left2PadR) +
|
|
203
|
+
' '.repeat(right2PadL) + right2 + ' '.repeat(right2PadR) +
|
|
204
|
+
chalk.cyan('║')
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// Empty line
|
|
208
|
+
console.log(chalk.cyan('║') + ' '.repeat(W) + chalk.cyan('║'));
|
|
209
|
+
|
|
210
|
+
// Status bar
|
|
211
|
+
console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
|
|
212
|
+
|
|
213
|
+
let statusText = '';
|
|
214
|
+
if (providerConfig.active) {
|
|
215
|
+
const connType = providerConfig.connectionType === 'cliproxy' ? 'CLIProxy' : 'API Key';
|
|
216
|
+
const keyDisplay = providerConfig.apiKey ? maskKey(providerConfig.apiKey) : 'N/A';
|
|
217
|
+
statusText = chalk.green('● ACTIVE') + chalk.gray(' via ') + chalk.cyan(connType);
|
|
218
|
+
if (providerConfig.connectionType === 'apikey' && providerConfig.apiKey) {
|
|
219
|
+
statusText += chalk.gray(' Key: ') + chalk.cyan(keyDisplay);
|
|
220
|
+
}
|
|
221
|
+
} else if (providerConfig.apiKey || providerConfig.connectionType) {
|
|
222
|
+
statusText = chalk.yellow('● CONFIGURED') + chalk.gray(' (not active)');
|
|
223
|
+
} else {
|
|
224
|
+
statusText = chalk.gray('○ NOT CONFIGURED');
|
|
225
|
+
}
|
|
226
|
+
console.log(chalk.cyan('║') + centerText(statusText, W) + chalk.cyan('║'));
|
|
227
|
+
|
|
228
|
+
// Disconnect option if active
|
|
229
|
+
if (providerConfig.active) {
|
|
230
|
+
console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
|
|
231
|
+
console.log(chalk.cyan('║') + chalk.red(centerText('[D] Disconnect', W)) + chalk.cyan('║'));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Back
|
|
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
|
+
* Handle provider configuration
|
|
242
|
+
* @param {Object} provider - Provider to configure
|
|
243
|
+
* @param {Object} config - Current config
|
|
244
|
+
* @returns {Object} Updated config
|
|
245
|
+
*/
|
|
246
|
+
const handleProviderConfig = async (provider, config) => {
|
|
247
|
+
const boxWidth = getLogoWidth();
|
|
248
|
+
|
|
249
|
+
while (true) {
|
|
250
|
+
console.clear();
|
|
251
|
+
drawProviderWindow(provider, config, boxWidth);
|
|
252
|
+
|
|
253
|
+
const input = await prompts.textInput(chalk.cyan('Select option: '));
|
|
254
|
+
const choice = (input || '').toLowerCase().trim();
|
|
255
|
+
|
|
256
|
+
if (choice === 'b' || choice === '') {
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (choice === 'd') {
|
|
261
|
+
// Disconnect
|
|
262
|
+
if (config.providers[provider.id]) {
|
|
263
|
+
config.providers[provider.id].active = false;
|
|
264
|
+
saveConfig(config);
|
|
265
|
+
console.log(chalk.yellow(`\n ${provider.name} disconnected.`));
|
|
266
|
+
await prompts.waitForEnter();
|
|
267
|
+
}
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (choice === '1') {
|
|
272
|
+
// CLIProxy connection
|
|
273
|
+
console.log();
|
|
274
|
+
console.log(chalk.cyan(' Connecting via CLIProxy...'));
|
|
275
|
+
console.log(chalk.gray(' This uses your paid plan (Claude Pro, ChatGPT Plus, etc.)'));
|
|
276
|
+
console.log();
|
|
277
|
+
|
|
278
|
+
// Deactivate all other providers
|
|
279
|
+
Object.keys(config.providers).forEach(id => {
|
|
280
|
+
if (config.providers[id]) config.providers[id].active = false;
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
if (!config.providers[provider.id]) config.providers[provider.id] = {};
|
|
284
|
+
config.providers[provider.id].connectionType = 'cliproxy';
|
|
285
|
+
config.providers[provider.id].active = true;
|
|
286
|
+
config.providers[provider.id].configuredAt = new Date().toISOString();
|
|
287
|
+
|
|
288
|
+
if (saveConfig(config)) {
|
|
289
|
+
console.log(chalk.green(` ✓ ${provider.name} connected via CLIProxy.`));
|
|
290
|
+
} else {
|
|
291
|
+
console.log(chalk.red(' Failed to save config.'));
|
|
292
|
+
}
|
|
293
|
+
await prompts.waitForEnter();
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (choice === '2') {
|
|
298
|
+
// API Key connection
|
|
299
|
+
console.log();
|
|
300
|
+
console.log(chalk.yellow(` Enter your ${provider.name} API key:`));
|
|
301
|
+
console.log(chalk.gray(' (Press Enter to cancel)'));
|
|
302
|
+
console.log();
|
|
303
|
+
|
|
304
|
+
const apiKey = await prompts.textInput(chalk.cyan(' API Key: '), true);
|
|
305
|
+
|
|
306
|
+
if (!apiKey || apiKey.trim() === '') {
|
|
307
|
+
console.log(chalk.gray(' Cancelled.'));
|
|
308
|
+
await prompts.waitForEnter();
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (apiKey.length < 20) {
|
|
313
|
+
console.log(chalk.red(' Invalid API key format (too short).'));
|
|
314
|
+
await prompts.waitForEnter();
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Deactivate all other providers
|
|
319
|
+
Object.keys(config.providers).forEach(id => {
|
|
320
|
+
if (config.providers[id]) config.providers[id].active = false;
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
if (!config.providers[provider.id]) config.providers[provider.id] = {};
|
|
324
|
+
config.providers[provider.id].connectionType = 'apikey';
|
|
325
|
+
config.providers[provider.id].apiKey = apiKey.trim();
|
|
326
|
+
config.providers[provider.id].active = true;
|
|
327
|
+
config.providers[provider.id].configuredAt = new Date().toISOString();
|
|
328
|
+
|
|
329
|
+
if (saveConfig(config)) {
|
|
330
|
+
console.log(chalk.green(` ✓ ${provider.name} connected via API Key.`));
|
|
331
|
+
} else {
|
|
332
|
+
console.log(chalk.red(' Failed to save config.'));
|
|
333
|
+
}
|
|
334
|
+
await prompts.waitForEnter();
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return config;
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Get active AI provider config
|
|
344
|
+
* @returns {Object|null} Active provider config or null
|
|
345
|
+
*/
|
|
346
|
+
const getActiveProvider = () => {
|
|
347
|
+
const config = loadConfig();
|
|
348
|
+
for (const provider of AI_PROVIDERS) {
|
|
349
|
+
const providerConfig = config.providers[provider.id];
|
|
350
|
+
if (providerConfig && providerConfig.active) {
|
|
351
|
+
return {
|
|
352
|
+
id: provider.id,
|
|
353
|
+
name: provider.name,
|
|
354
|
+
connectionType: providerConfig.connectionType,
|
|
355
|
+
apiKey: providerConfig.apiKey || null
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return null;
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Count active AI agents
|
|
364
|
+
* @returns {number} Number of active agents (0 or 1)
|
|
365
|
+
*/
|
|
366
|
+
const getActiveAgentCount = () => {
|
|
367
|
+
const active = getActiveProvider();
|
|
368
|
+
return active ? 1 : 0;
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Main AI Agents menu
|
|
373
|
+
*/
|
|
374
|
+
const aiAgentsMenu = async () => {
|
|
375
|
+
let config = loadConfig();
|
|
376
|
+
const boxWidth = getLogoWidth();
|
|
377
|
+
|
|
378
|
+
while (true) {
|
|
379
|
+
console.clear();
|
|
380
|
+
drawProvidersTable(config, boxWidth);
|
|
381
|
+
|
|
382
|
+
const input = await prompts.textInput(chalk.cyan('Select provider: '));
|
|
383
|
+
const choice = (input || '').toLowerCase().trim();
|
|
384
|
+
|
|
385
|
+
if (choice === 'b' || choice === '') {
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const num = parseInt(choice);
|
|
390
|
+
if (!isNaN(num) && num >= 1 && num <= AI_PROVIDERS.length) {
|
|
391
|
+
config = await handleProviderConfig(AI_PROVIDERS[num - 1], config);
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
console.log(chalk.red(' Invalid option.'));
|
|
396
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
module.exports = {
|
|
401
|
+
aiAgentsMenu,
|
|
402
|
+
getActiveProvider,
|
|
403
|
+
getActiveAgentCount,
|
|
404
|
+
loadConfig,
|
|
405
|
+
saveConfig,
|
|
406
|
+
AI_PROVIDERS
|
|
407
|
+
};
|