maiass 5.7.31

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.
@@ -0,0 +1,476 @@
1
+ // Account info functionality for nodemaiass
2
+ // Port of bashmaiass --account-info command
3
+
4
+ import { log } from './logger.js';
5
+ import { SYMBOLS } from './symbols.js';
6
+ import { loadEnvironmentConfig } from './config.js';
7
+ import { generateMachineFingerprint } from './machine-fingerprint.js';
8
+ import { retrieveSecureVariable, storeSecureVariable, removeSecureVariable } from './secure-storage.js';
9
+ import { getSingleCharInput } from './input-utils.js';
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+
13
+ /**
14
+ * Mask a sensitive token (show start and end only)
15
+ * @param {string} token - Token to mask
16
+ * @returns {string} Masked token
17
+ */
18
+ function maskToken(token) {
19
+ if (!token) return 'none';
20
+ const n = token.length;
21
+ if (n <= 10) {
22
+ return `${token.substring(0, 1)}***${token.substring(n - 1)}`;
23
+ } else {
24
+ return `${token.substring(0, 6)}***${token.substring(n - 4)}`;
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Get client version from package.json
30
+ * @returns {string} Version string
31
+ */
32
+ function getClientVersion() {
33
+ try {
34
+ const packagePath = path.resolve(process.cwd(), 'package.json');
35
+ if (fs.existsSync(packagePath)) {
36
+ const packageData = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
37
+ return packageData.version || '0.0.0';
38
+ }
39
+ } catch (error) {
40
+ // Ignore errors, fallback to default
41
+ }
42
+ return '0.0.0';
43
+ }
44
+
45
+ /**
46
+ * Create anonymous subscription if needed
47
+ * @returns {Promise<string|null>} API token or null
48
+ */
49
+ async function createAnonymousSubscriptionIfNeeded() {
50
+ const debugMode = process.env.MAIASS_DEBUG === 'true';
51
+
52
+ try {
53
+ log.info(SYMBOLS.INFO, 'No AI API key found. Creating anonymous subscription...');
54
+
55
+ const machineFingerprint = generateMachineFingerprint();
56
+ const endpoint = process.env.MAIASS_AI_HOST || 'https://pound.maiass.net';
57
+
58
+ if (debugMode) {
59
+ log.debug(SYMBOLS.INFO, `[MAIASS DEBUG] Creating anonymous subscription at: ${endpoint}/v1/token`);
60
+ }
61
+
62
+ const response = await fetch(`${endpoint}/v1/token`, {
63
+ method: 'POST',
64
+ headers: {
65
+ 'Content-Type': 'application/json',
66
+ 'X-Client-Name': 'nodemaiass',
67
+ 'X-Client-Version': '5.7.5'
68
+ },
69
+ body: JSON.stringify({
70
+ machine_fingerprint: machineFingerprint
71
+ })
72
+ });
73
+
74
+ if (!response.ok) {
75
+ const errorText = await response.text();
76
+ let errorData = {};
77
+ try {
78
+ errorData = JSON.parse(errorText);
79
+ } catch (e) {
80
+ errorData = { error: errorText };
81
+ }
82
+
83
+ log.error(SYMBOLS.CROSS, `Failed to create anonymous subscription: ${errorData.error || response.statusText}`);
84
+ return null;
85
+ }
86
+
87
+ const data = await response.json();
88
+
89
+ // Extract fields - try multiple field names for compatibility
90
+ const apiKey = data.apiKey || data.api_key || data.token;
91
+ const subscriptionId = data.id || data.subscription_id;
92
+ const credits = data.creditsRemaining || data.credits_remaining || data.credits;
93
+ const purchaseUrl = data.purchaseUrl || data.payment_url || data.top_up_url;
94
+
95
+ if (apiKey) {
96
+ // Store the token in secure storage
97
+ const stored = storeSecureVariable('MAIASS_AI_TOKEN', apiKey);
98
+
99
+ if (subscriptionId) {
100
+ storeSecureVariable('MAIASS_SUBSCRIPTION_ID', subscriptionId);
101
+ }
102
+
103
+ if (stored) {
104
+ log.success(SYMBOLS.CHECKMARK, 'Anonymous subscription created and stored securely');
105
+ log.info(SYMBOLS.INFO, ` API Key: ${apiKey.substring(0, 8)}...`);
106
+ log.info(SYMBOLS.INFO, ` Credits: ${credits || 'N/A'}`);
107
+
108
+ if (subscriptionId) {
109
+ log.info(SYMBOLS.INFO, ` Subscription ID: ${subscriptionId.substring(0, 12)}...`);
110
+ }
111
+
112
+ // Warn if zero credits
113
+ if (!credits || credits === 0) {
114
+ log.warning(SYMBOLS.WARNING, '⚠️ Your anonymous API key has zero credits. Please purchase credits to use AI features.');
115
+ if (purchaseUrl) {
116
+ log.info(SYMBOLS.INFO, ` Purchase credits here: ${purchaseUrl}`);
117
+ }
118
+ }
119
+ } else {
120
+ log.warning(SYMBOLS.WARNING, 'Anonymous subscription created but could not store securely');
121
+ }
122
+
123
+ // Set in environment for this session
124
+ process.env.MAIASS_AI_TOKEN = apiKey;
125
+ if (subscriptionId) {
126
+ process.env.MAIASS_SUBSCRIPTION_ID = subscriptionId;
127
+ }
128
+
129
+ return apiKey;
130
+ }
131
+
132
+ return null;
133
+ } catch (error) {
134
+ log.error(SYMBOLS.CROSS, `Error creating anonymous subscription: ${error.message}`);
135
+ return null;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Query account info from the API
141
+ * @param {string} apiKey - API key to use
142
+ * @returns {Promise<Object>} Account info response
143
+ */
144
+ async function queryAccountInfo(apiKey) {
145
+ const clientVersion = getClientVersion();
146
+ const clientName = 'nodemaiass';
147
+ const baseHost = process.env.MAIASS_AI_HOST || 'https://pound.maiass.net';
148
+ const debugMode = process.env.MAIASS_DEBUG === 'true';
149
+
150
+ // Try multiple endpoints with fallbacks
151
+ const endpoints = [
152
+ `${baseHost}/account-info`,
153
+ `${baseHost}/v1/account-info`
154
+ ];
155
+
156
+ for (const endpoint of endpoints) {
157
+ if (debugMode) {
158
+ log.debug(SYMBOLS.INFO, `[MAIASS DEBUG] Querying account info at: ${endpoint}`);
159
+ }
160
+
161
+ try {
162
+ // Try GET first
163
+ let response = await fetch(endpoint, {
164
+ method: 'GET',
165
+ headers: {
166
+ 'Authorization': `Bearer ${apiKey}`,
167
+ 'X-Client-Name': clientName,
168
+ 'X-Client-Version': clientVersion
169
+ }
170
+ });
171
+
172
+ if (debugMode) {
173
+ log.debug(SYMBOLS.INFO, `[MAIASS DEBUG] GET ${endpoint} - Status: ${response.status}`);
174
+ }
175
+
176
+ // If GET fails, try POST
177
+ if (!response.ok && response.status !== 404) {
178
+ if (debugMode) {
179
+ log.debug(SYMBOLS.INFO, `[MAIASS DEBUG] Retrying with POST to ${endpoint}`);
180
+ }
181
+
182
+ response = await fetch(endpoint, {
183
+ method: 'POST',
184
+ headers: {
185
+ 'Content-Type': 'application/json',
186
+ 'X-Client-Name': clientName,
187
+ 'X-Client-Version': clientVersion
188
+ },
189
+ body: JSON.stringify({ api_key: apiKey })
190
+ });
191
+
192
+ if (debugMode) {
193
+ log.debug(SYMBOLS.INFO, `[MAIASS DEBUG] POST ${endpoint} - Status: ${response.status}`);
194
+ }
195
+ }
196
+
197
+ if (response.ok) {
198
+ const data = await response.json();
199
+ return {
200
+ success: true,
201
+ data,
202
+ status: response.status,
203
+ endpoint
204
+ };
205
+ }
206
+
207
+ // If this endpoint fails, collect error info and try the next one
208
+ let errorText = '';
209
+ let errorData = null;
210
+ try {
211
+ errorText = await response.text();
212
+ errorData = JSON.parse(errorText);
213
+ } catch (e) {
214
+ // Not JSON, use raw text
215
+ }
216
+
217
+ if (debugMode) {
218
+ log.debug(SYMBOLS.WARNING, `[MAIASS DEBUG] ${endpoint} failed: ${response.status} ${errorText}`);
219
+ }
220
+
221
+ // If this is a 403 error, return it immediately for handling
222
+ if (response.status === 403) {
223
+ return {
224
+ success: false,
225
+ error: errorData?.error || errorText || 'Invalid API key',
226
+ status: 403,
227
+ endpoint
228
+ };
229
+ }
230
+
231
+ } catch (error) {
232
+ if (debugMode) {
233
+ log.debug(SYMBOLS.WARNING, `[MAIASS DEBUG] ${endpoint} error: ${error.message}`);
234
+ }
235
+ }
236
+ }
237
+
238
+ return {
239
+ success: false,
240
+ error: 'All endpoints failed',
241
+ status: 'unknown'
242
+ };
243
+ }
244
+
245
+ /**
246
+ * Prompt user to update or delete API key
247
+ * @param {string} currentKey - Current API key
248
+ * @returns {Promise<string|null>} New API key or null if deleted
249
+ */
250
+ async function promptApiKeyManagement(currentKey) {
251
+ const maskedKey = maskToken(currentKey);
252
+
253
+ console.log('');
254
+ log.warning(SYMBOLS.WARNING, `Current API key is invalid: ${maskedKey}`);
255
+ console.log('');
256
+ console.log('What would you like to do?');
257
+ console.log(' [u] Update API key');
258
+ console.log(' [d] Delete API key (will create anonymous subscription)');
259
+ console.log(' [q] Quit');
260
+ console.log('');
261
+
262
+ const choice = await getSingleCharInput('Choose an option [u/d/q]: ');
263
+
264
+ switch (choice.toLowerCase()) {
265
+ case 'u':
266
+ // Update API key
267
+ const readline = await import('readline');
268
+ const rl = readline.createInterface({
269
+ input: process.stdin,
270
+ output: process.stdout
271
+ });
272
+
273
+ return new Promise((resolve) => {
274
+ rl.question('Enter new API key: ', (newKey) => {
275
+ rl.close();
276
+ if (newKey.trim()) {
277
+ // Store the new key
278
+ const stored = storeSecureVariable('MAIASS_AI_TOKEN', newKey.trim());
279
+ if (stored) {
280
+ log.success(SYMBOLS.CHECKMARK, 'API key updated and stored securely');
281
+ process.env.MAIASS_AI_TOKEN = newKey.trim();
282
+ resolve(newKey.trim());
283
+ } else {
284
+ log.warning(SYMBOLS.WARNING, 'API key updated but could not store securely');
285
+ process.env.MAIASS_AI_TOKEN = newKey.trim();
286
+ resolve(newKey.trim());
287
+ }
288
+ } else {
289
+ log.warning(SYMBOLS.WARNING, 'No API key entered');
290
+ resolve(null);
291
+ }
292
+ });
293
+ });
294
+
295
+ case 'd':
296
+ // Delete API key
297
+ const removed = removeSecureVariable('MAIASS_AI_TOKEN');
298
+ removeSecureVariable('MAIASS_SUBSCRIPTION_ID'); // Also remove subscription ID
299
+ delete process.env.MAIASS_AI_TOKEN;
300
+ delete process.env.MAIASS_SUBSCRIPTION_ID;
301
+
302
+ if (removed) {
303
+ log.success(SYMBOLS.CHECKMARK, 'API key deleted from secure storage');
304
+ } else {
305
+ log.warning(SYMBOLS.WARNING, 'Could not delete API key from secure storage');
306
+ }
307
+
308
+ log.info(SYMBOLS.INFO, 'Will create new anonymous subscription...');
309
+ return null;
310
+
311
+ case 'q':
312
+ default:
313
+ log.info(SYMBOLS.INFO, 'Exiting...');
314
+ process.exit(0);
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Handle account info command
320
+ * @param {Object} options - Command options
321
+ */
322
+ export async function handleAccountInfoCommand(options = {}) {
323
+ const { json = false } = options;
324
+ const debugMode = process.env.MAIASS_DEBUG === 'true';
325
+
326
+ try {
327
+ // Load configuration to get tokens
328
+ loadEnvironmentConfig();
329
+
330
+ // Get API key from environment or secure storage
331
+ let apiKey = process.env.MAIASS_AI_TOKEN;
332
+
333
+ if (!apiKey) {
334
+ // Try to get from secure storage
335
+ apiKey = retrieveSecureVariable('MAIASS_AI_TOKEN');
336
+ if (apiKey) {
337
+ process.env.MAIASS_AI_TOKEN = apiKey;
338
+ }
339
+ }
340
+
341
+ // If still no API key and AI mode is not off, create anonymous subscription
342
+ if (!apiKey || apiKey === 'DISABLED') {
343
+ const aiMode = process.env.MAIASS_AI_MODE || 'ask';
344
+ if (aiMode !== 'off') {
345
+ if (debugMode) {
346
+ log.debug(SYMBOLS.INFO, '[MAIASS DEBUG] No AI token found, creating anonymous subscription...');
347
+ }
348
+ apiKey = await createAnonymousSubscriptionIfNeeded();
349
+ if (!apiKey) {
350
+ log.error(SYMBOLS.CROSS, 'Failed to create anonymous subscription');
351
+ process.exit(1);
352
+ }
353
+ } else {
354
+ log.warning(SYMBOLS.WARNING, 'AI mode is off. Skipping AI token setup.');
355
+ process.exit(1);
356
+ }
357
+ }
358
+
359
+ // Query account info
360
+ const result = await queryAccountInfo(apiKey);
361
+
362
+ if (!result.success) {
363
+ // Check if it's an invalid API key error (403)
364
+ if (result.status === 403 || (result.error && result.error.includes('Invalid API key'))) {
365
+ // Offer to update or delete the API key
366
+ const newApiKey = await promptApiKeyManagement(apiKey);
367
+
368
+ if (newApiKey) {
369
+ // Retry with new API key
370
+ const retryResult = await queryAccountInfo(newApiKey);
371
+ if (retryResult.success) {
372
+ // Continue with successful result
373
+ result.success = true;
374
+ result.data = retryResult.data;
375
+ result.status = retryResult.status;
376
+ } else {
377
+ log.error(SYMBOLS.CROSS, `New API key also failed: ${retryResult.error}`);
378
+ process.exit(1);
379
+ }
380
+ } else {
381
+ // API key was deleted, create anonymous subscription
382
+ apiKey = await createAnonymousSubscriptionIfNeeded();
383
+ if (!apiKey) {
384
+ log.error(SYMBOLS.CROSS, 'Failed to create anonymous subscription');
385
+ process.exit(1);
386
+ }
387
+
388
+ // Retry with anonymous subscription
389
+ const retryResult = await queryAccountInfo(apiKey);
390
+ if (retryResult.success) {
391
+ result.success = true;
392
+ result.data = retryResult.data;
393
+ result.status = retryResult.status;
394
+ } else {
395
+ log.error(SYMBOLS.CROSS, `Anonymous subscription also failed: ${retryResult.error}`);
396
+ process.exit(1);
397
+ }
398
+ }
399
+ } else {
400
+ log.error(SYMBOLS.CROSS, `Failed to query account info: ${result.error}`);
401
+ process.exit(1);
402
+ }
403
+ }
404
+
405
+ const data = result.data;
406
+
407
+ // If JSON requested, output JSON (without exposing full token)
408
+ if (json) {
409
+ const safeData = {
410
+ tokens_used: data.tokens_used || data.credits_used || '-',
411
+ tokens_remaining: data.tokens_remaining || data.credits_remaining || '-',
412
+ quota: data.quota || '-',
413
+ subscription_type: data.subscription_type || '-',
414
+ customer_email: data.customer_email || '-',
415
+ status: data.status || result.status
416
+ };
417
+ console.log(JSON.stringify(safeData, null, 2));
418
+ return;
419
+ }
420
+
421
+ // Human-readable summary (default)
422
+ const tokensUsed = data.tokens_used || data.credits_used || '-';
423
+ const tokensRemaining = data.tokens_remaining || data.credits_remaining || '-';
424
+ const quota = data.quota || '-';
425
+ const subType = data.subscription_type || '-';
426
+ const custEmail = data.customer_email || '-';
427
+ const statusField = data.status || result.status;
428
+
429
+ const maskedKey = maskToken(apiKey);
430
+ const credit = 'Nugét';
431
+
432
+ console.log('');
433
+ console.log('Account Info');
434
+ console.log('------------');
435
+ console.log(`API Token: ${maskedKey}`);
436
+
437
+ const subscriptionId = process.env.MAIASS_SUBSCRIPTION_ID;
438
+ if (subscriptionId) {
439
+ console.log(`Subscription ID: ${subscriptionId}`);
440
+ }
441
+
442
+ console.log(`Type: ${subType}`);
443
+ console.log(`Email: ${custEmail}`);
444
+ console.log(`${credit}s Used: ${tokensUsed}`);
445
+ console.log(`${credit}s Remaining:${tokensRemaining}`);
446
+ console.log(`Quota: ${quota}`);
447
+
448
+ // Explain status codes clearly
449
+ if (result.status === 403 || statusField === 403) {
450
+ console.log('Status: 403 Forbidden');
451
+ console.log('Explanation: Your token was rejected. Ensure it is correct, not expired, and associated with an active subscription.');
452
+ } else if (result.status === 401 || statusField === 401) {
453
+ console.log('Status: 401 Unauthorized');
454
+ console.log('Explanation: Missing or invalid credentials. Try updating your token with \'nma config --global maiass_token=your_token\'.');
455
+ } else if (result.status >= 200 && result.status < 300) {
456
+ console.log(`Status: ${result.status} OK`);
457
+ } else {
458
+ console.log(`Status: ${result.status || 'unknown'}`);
459
+ }
460
+
461
+ console.log('');
462
+
463
+ } catch (error) {
464
+ if (debugMode) {
465
+ log.debug(SYMBOLS.WARNING, `[MAIASS DEBUG] Account info error: ${error.stack || error.message}`);
466
+ }
467
+ log.error(SYMBOLS.CROSS, `Account info failed: ${error.message}`);
468
+ process.exit(1);
469
+ }
470
+ }
471
+
472
+ export default {
473
+ handleAccountInfoCommand,
474
+ maskToken,
475
+ getClientVersion
476
+ };
package/lib/colors.js ADDED
@@ -0,0 +1,49 @@
1
+ // Color definitions for MAIASS CLI
2
+ import chalk from 'chalk';
3
+
4
+ export default {
5
+
6
+ BCyan: (text) => chalk.bold(chalk.cyan(text)),
7
+ BRed: (text) => chalk.bold(chalk.red(text)),
8
+ BGreen: (text) => chalk.bold(chalk.green(text)),
9
+ BBlue: (text) => chalk.bold(chalk.blue(text)),
10
+ BYellow: (text) => chalk.bold(chalk.yellow(text)),
11
+ BPurple: (text) => chalk.bold(chalk.magenta(text)),
12
+ BWhite: (text) => chalk.bold(chalk.white(text)),
13
+ BMagenta: (text) => chalk.bold(chalk.magenta(text)),
14
+ BAqua: (text) => chalk.bold(chalk.cyanBright(text)),
15
+ BGray: (text) => chalk.bold(chalk.gray(text)),
16
+ BPink: (text) => chalk.bold(chalk.magentaBright(text)),
17
+ // soft pink
18
+ BSoftPink: (text) => chalk.bold(chalk.hex('#FFB6C1')(text)),
19
+
20
+ BLime: (text) => chalk.bold(chalk.greenBright(text)),
21
+ BYellowBright: (text) => chalk.bold(chalk.yellowBright(text)),
22
+ BBlueBright: (text) => chalk.bold(chalk.blueBright(text)),
23
+ BOrange: (text) => chalk.bold(chalk.hex('#FFA500')(text)),
24
+ Orange: (text) => chalk.hex('#FFA500')(text),
25
+ Cyan: (text) => chalk.cyan(text),
26
+ Red: (text) => chalk.red(text),
27
+ Green: (text) => chalk.green(text),
28
+ Blue: (text) => chalk.blue(text),
29
+ Yellow: (text) => chalk.yellow(text),
30
+ Purple: (text) => chalk.magenta(text),
31
+ White: (text) => chalk.white(text),
32
+ Gray: (text) => chalk.gray(text),
33
+ Magenta: (text) => chalk.magenta(text),
34
+ Aqua: (text) => chalk.cyanBright(text),
35
+ Pink: (text) => chalk.magentaBright(text),
36
+ SoftPink: (text) => chalk.hex('#FFB6C1')(text),
37
+ Lime: (text) => chalk.greenBright(text),
38
+ YellowBright: (text) => chalk.yellowBright(text),
39
+ BlueBright: (text) => chalk.blueBright(text),
40
+ SkyBlue: (text) => chalk.bold.cyanBright(text),
41
+ BlueOnWhite: (text) => chalk.blue.bgWhite(text),
42
+ BBlueOnWhite: (text) => chalk.bold.blue.bgWhite(text),
43
+
44
+ // Helper function to format messages with MAIASS branding
45
+ formatMessage: (icon, message) => {
46
+ const prefix = chalk.bold.cyanBright('| ))');
47
+ return `${icon} ${prefix} ${message}`;
48
+ },
49
+ };