langtrain 0.1.24 → 0.1.26
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/dist/chunk-7D4BGTDT.js +3 -0
- package/dist/chunk-7D4BGTDT.js.map +1 -0
- package/dist/chunk-O3AFVWU4.mjs +3 -0
- package/dist/chunk-O3AFVWU4.mjs.map +1 -0
- package/dist/cli.js +17 -9
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +17 -9
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +40 -1
- package/dist/index.d.ts +40 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/src/cli/auth.ts +63 -15
- package/src/cli/handlers/telemetry.ts +127 -0
- package/src/cli/index.ts +171 -144
- package/src/cli/menu.ts +36 -34
- package/src/index.ts +1 -0
- package/src/lib/subscription.ts +5 -0
- package/src/lib/usage.ts +55 -0
- package/dist/chunk-Q5EF25B2.js +0 -3
- package/dist/chunk-Q5EF25B2.js.map +0 -1
- package/dist/chunk-XRT7LLNF.mjs +0 -3
- package/dist/chunk-XRT7LLNF.mjs.map +0 -1
package/dist/index.d.mts
CHANGED
|
@@ -136,6 +136,11 @@ interface SubscriptionInfo {
|
|
|
136
136
|
expires_at?: string;
|
|
137
137
|
features: string[];
|
|
138
138
|
limits: any;
|
|
139
|
+
usage?: {
|
|
140
|
+
tokensUsedThisMonth?: number;
|
|
141
|
+
tokenLimit?: number;
|
|
142
|
+
apiCalls?: number;
|
|
143
|
+
};
|
|
139
144
|
}
|
|
140
145
|
interface FeatureCheck {
|
|
141
146
|
feature: string;
|
|
@@ -249,4 +254,38 @@ declare class GuardrailClient {
|
|
|
249
254
|
apply(datasetId: string, guardrailId: string): Promise<any>;
|
|
250
255
|
}
|
|
251
256
|
|
|
252
|
-
|
|
257
|
+
interface UsageSummary {
|
|
258
|
+
workspace_id: string;
|
|
259
|
+
plan: string;
|
|
260
|
+
quotas: Record<string, any>;
|
|
261
|
+
billing?: {
|
|
262
|
+
plan_id: string;
|
|
263
|
+
tokens_used: number;
|
|
264
|
+
tokens_limit: number;
|
|
265
|
+
period_end: string;
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
interface UsageHistoryPoint {
|
|
269
|
+
date: string;
|
|
270
|
+
tokens: number;
|
|
271
|
+
agent_runs: number;
|
|
272
|
+
cost: number;
|
|
273
|
+
}
|
|
274
|
+
declare class UsageClient {
|
|
275
|
+
private config;
|
|
276
|
+
private client;
|
|
277
|
+
constructor(config: {
|
|
278
|
+
apiKey: string;
|
|
279
|
+
baseUrl?: string;
|
|
280
|
+
});
|
|
281
|
+
/**
|
|
282
|
+
* Get current usage summary for a workspace.
|
|
283
|
+
*/
|
|
284
|
+
getSummary(workspaceId: string): Promise<UsageSummary>;
|
|
285
|
+
/**
|
|
286
|
+
* Get historical usage data for charts.
|
|
287
|
+
*/
|
|
288
|
+
getHistory(workspaceId: string, days?: number): Promise<UsageHistoryPoint[]>;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export { type Agent, AgentClient, type AgentCreate, type AgentRun, agent as AgentTypes, type FeatureCheck, FileClient, type FileResponse, type FineTuneJobCreate, type FineTuneJobResponse, type Guardrail, GuardrailClient, type GuardrailConfig, type GuardrailCreate, type Model, ModelClient, models as ModelTypes, type Secret, SecretClient, secrets as SecretTypes, SubscriptionClient, type SubscriptionInfo, TrainingClient, UsageClient, type UsageHistoryPoint, type UsageSummary };
|
package/dist/index.d.ts
CHANGED
|
@@ -136,6 +136,11 @@ interface SubscriptionInfo {
|
|
|
136
136
|
expires_at?: string;
|
|
137
137
|
features: string[];
|
|
138
138
|
limits: any;
|
|
139
|
+
usage?: {
|
|
140
|
+
tokensUsedThisMonth?: number;
|
|
141
|
+
tokenLimit?: number;
|
|
142
|
+
apiCalls?: number;
|
|
143
|
+
};
|
|
139
144
|
}
|
|
140
145
|
interface FeatureCheck {
|
|
141
146
|
feature: string;
|
|
@@ -249,4 +254,38 @@ declare class GuardrailClient {
|
|
|
249
254
|
apply(datasetId: string, guardrailId: string): Promise<any>;
|
|
250
255
|
}
|
|
251
256
|
|
|
252
|
-
|
|
257
|
+
interface UsageSummary {
|
|
258
|
+
workspace_id: string;
|
|
259
|
+
plan: string;
|
|
260
|
+
quotas: Record<string, any>;
|
|
261
|
+
billing?: {
|
|
262
|
+
plan_id: string;
|
|
263
|
+
tokens_used: number;
|
|
264
|
+
tokens_limit: number;
|
|
265
|
+
period_end: string;
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
interface UsageHistoryPoint {
|
|
269
|
+
date: string;
|
|
270
|
+
tokens: number;
|
|
271
|
+
agent_runs: number;
|
|
272
|
+
cost: number;
|
|
273
|
+
}
|
|
274
|
+
declare class UsageClient {
|
|
275
|
+
private config;
|
|
276
|
+
private client;
|
|
277
|
+
constructor(config: {
|
|
278
|
+
apiKey: string;
|
|
279
|
+
baseUrl?: string;
|
|
280
|
+
});
|
|
281
|
+
/**
|
|
282
|
+
* Get current usage summary for a workspace.
|
|
283
|
+
*/
|
|
284
|
+
getSummary(workspaceId: string): Promise<UsageSummary>;
|
|
285
|
+
/**
|
|
286
|
+
* Get historical usage data for charts.
|
|
287
|
+
*/
|
|
288
|
+
getHistory(workspaceId: string, days?: number): Promise<UsageHistoryPoint[]>;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export { type Agent, AgentClient, type AgentCreate, type AgentRun, agent as AgentTypes, type FeatureCheck, FileClient, type FileResponse, type FineTuneJobCreate, type FineTuneJobResponse, type Guardrail, GuardrailClient, type GuardrailConfig, type GuardrailCreate, type Model, ModelClient, models as ModelTypes, type Secret, SecretClient, secrets as SecretTypes, SubscriptionClient, type SubscriptionInfo, TrainingClient, UsageClient, type UsageHistoryPoint, type UsageSummary };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
'use strict';var
|
|
1
|
+
'use strict';var chunk7D4BGTDT_js=require('./chunk-7D4BGTDT.js');Object.defineProperty(exports,"AgentClient",{enumerable:true,get:function(){return chunk7D4BGTDT_js.b}});Object.defineProperty(exports,"AgentTypes",{enumerable:true,get:function(){return chunk7D4BGTDT_js.c}});Object.defineProperty(exports,"FileClient",{enumerable:true,get:function(){return chunk7D4BGTDT_js.d}});Object.defineProperty(exports,"GuardrailClient",{enumerable:true,get:function(){return chunk7D4BGTDT_js.k}});Object.defineProperty(exports,"Langtune",{enumerable:true,get:function(){return chunk7D4BGTDT_js.p}});Object.defineProperty(exports,"Langvision",{enumerable:true,get:function(){return chunk7D4BGTDT_js.o}});Object.defineProperty(exports,"ModelClient",{enumerable:true,get:function(){return chunk7D4BGTDT_js.g}});Object.defineProperty(exports,"ModelTypes",{enumerable:true,get:function(){return chunk7D4BGTDT_js.h}});Object.defineProperty(exports,"SecretClient",{enumerable:true,get:function(){return chunk7D4BGTDT_js.i}});Object.defineProperty(exports,"SecretTypes",{enumerable:true,get:function(){return chunk7D4BGTDT_js.j}});Object.defineProperty(exports,"SubscriptionClient",{enumerable:true,get:function(){return chunk7D4BGTDT_js.f}});Object.defineProperty(exports,"Text",{enumerable:true,get:function(){return chunk7D4BGTDT_js.n}});Object.defineProperty(exports,"TrainingClient",{enumerable:true,get:function(){return chunk7D4BGTDT_js.e}});Object.defineProperty(exports,"UsageClient",{enumerable:true,get:function(){return chunk7D4BGTDT_js.l}});Object.defineProperty(exports,"Vision",{enumerable:true,get:function(){return chunk7D4BGTDT_js.m}});//# sourceMappingURL=index.js.map
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
export{b as AgentClient,c as AgentTypes,d as FileClient,k as GuardrailClient,p as Langtune,o as Langvision,g as ModelClient,h as ModelTypes,i as SecretClient,j as SecretTypes,f as SubscriptionClient,n as Text,e as TrainingClient,l as UsageClient,m as Vision}from'./chunk-O3AFVWU4.mjs';//# sourceMappingURL=index.mjs.map
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
package/package.json
CHANGED
package/src/cli/auth.ts
CHANGED
|
@@ -1,12 +1,25 @@
|
|
|
1
|
-
import { password, isCancel, cancel, intro, green, yellow, red, bgMagenta, black, spinner, gray } from './ui';
|
|
1
|
+
import { password, isCancel, cancel, intro, green, yellow, red, bgMagenta, black, spinner, gray, cyan, dim, bold } from './ui';
|
|
2
2
|
import { getConfig, saveConfig } from './config';
|
|
3
3
|
import { SubscriptionClient, SubscriptionInfo } from '../index';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Quick check if API key is stored (no network call).
|
|
7
|
+
*/
|
|
8
|
+
export function isAuthenticated(): boolean {
|
|
9
|
+
const config = getConfig();
|
|
10
|
+
return !!config.apiKey;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Ensure auth — if not logged in, forces login flow.
|
|
15
|
+
*/
|
|
5
16
|
export async function ensureAuth(): Promise<string> {
|
|
6
17
|
let config = getConfig();
|
|
7
18
|
|
|
8
19
|
if (!config.apiKey) {
|
|
9
|
-
|
|
20
|
+
console.log('');
|
|
21
|
+
console.log(yellow(' Authentication required.'));
|
|
22
|
+
console.log(gray(' Login to access all features.\n'));
|
|
10
23
|
await handleLogin();
|
|
11
24
|
config = getConfig();
|
|
12
25
|
}
|
|
@@ -14,13 +27,20 @@ export async function ensureAuth(): Promise<string> {
|
|
|
14
27
|
return config.apiKey as string;
|
|
15
28
|
}
|
|
16
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Interactive login — Claude-style API key entry with immediate verification.
|
|
32
|
+
*/
|
|
17
33
|
export async function handleLogin() {
|
|
18
34
|
while (true) {
|
|
19
|
-
console.log(
|
|
35
|
+
console.log(dim(' ─────────────────────────────────────'));
|
|
36
|
+
console.log(gray(' Get your API Key at: ') + cyan('https://app.langtrain.xyz/home/api'));
|
|
37
|
+
console.log(dim(' ─────────────────────────────────────\n'));
|
|
38
|
+
|
|
20
39
|
const apiKey = await password({
|
|
21
40
|
message: 'Enter your Langtrain API Key:',
|
|
22
41
|
validate(value) {
|
|
23
42
|
if (!value || value.length === 0) return 'API Key is required';
|
|
43
|
+
if (value.length < 10) return 'Invalid key format';
|
|
24
44
|
},
|
|
25
45
|
});
|
|
26
46
|
|
|
@@ -32,45 +52,73 @@ export async function handleLogin() {
|
|
|
32
52
|
const s = spinner();
|
|
33
53
|
s.start('Verifying API Key...');
|
|
34
54
|
|
|
35
|
-
// Verify key immediately
|
|
36
55
|
try {
|
|
37
56
|
const client = new SubscriptionClient({ apiKey: apiKey as string });
|
|
38
57
|
const info = await client.getStatus();
|
|
39
58
|
|
|
40
|
-
|
|
59
|
+
const planBadge = info.plan === 'pro'
|
|
60
|
+
? bgMagenta(black(' PRO '))
|
|
61
|
+
: info.plan === 'enterprise'
|
|
62
|
+
? bgMagenta(black(' ENTERPRISE '))
|
|
63
|
+
: ' FREE ';
|
|
64
|
+
|
|
65
|
+
s.stop(green(`Authenticated ${planBadge}`));
|
|
66
|
+
|
|
67
|
+
// Show initial token info if available
|
|
68
|
+
if (info.usage) {
|
|
69
|
+
const used = info.usage.tokensUsedThisMonth || 0;
|
|
70
|
+
const limit = info.usage.tokenLimit || 10000;
|
|
71
|
+
const pct = Math.round((used / limit) * 100);
|
|
72
|
+
console.log(dim(` Tokens: ${used.toLocaleString()} / ${limit.toLocaleString()} (${pct}% used)`));
|
|
73
|
+
}
|
|
41
74
|
|
|
42
75
|
const config = getConfig();
|
|
43
76
|
saveConfig({ ...config, apiKey: apiKey as string });
|
|
44
|
-
|
|
45
|
-
return;
|
|
77
|
+
console.log(green(' ✔ Credentials saved to ~/.langtrain/config.json\n'));
|
|
78
|
+
return;
|
|
46
79
|
} catch (e: any) {
|
|
47
80
|
s.stop(red('Invalid API Key. Please try again.'));
|
|
48
|
-
// Loop continues
|
|
49
81
|
}
|
|
50
82
|
}
|
|
51
83
|
}
|
|
52
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Logout — clear stored credentials.
|
|
87
|
+
*/
|
|
88
|
+
export async function handleLogout() {
|
|
89
|
+
const config = getConfig();
|
|
90
|
+
delete config.apiKey;
|
|
91
|
+
saveConfig(config);
|
|
92
|
+
console.log(green('\n ✔ Logged out. Credentials cleared.\n'));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Fetch subscription info for status bar display.
|
|
97
|
+
*/
|
|
53
98
|
export async function getSubscription(apiKey: string): Promise<SubscriptionInfo | null> {
|
|
54
99
|
const client = new SubscriptionClient({ apiKey });
|
|
55
100
|
const s = spinner();
|
|
56
|
-
s.start('
|
|
101
|
+
s.start('Checking subscription...');
|
|
57
102
|
try {
|
|
58
103
|
const info = await client.getStatus();
|
|
59
104
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
105
|
+
const planBadge = info.plan === 'pro'
|
|
106
|
+
? bgMagenta(black(' PRO '))
|
|
107
|
+
: info.plan === 'enterprise'
|
|
108
|
+
? bgMagenta(black(' ENTERPRISE '))
|
|
109
|
+
: bold(' FREE ');
|
|
110
|
+
|
|
111
|
+
s.stop(green(`Plan: ${planBadge}`));
|
|
63
112
|
|
|
64
113
|
if (info.is_active === false) {
|
|
65
|
-
console.log(yellow('
|
|
114
|
+
console.log(yellow(' ⚠ Subscription inactive. Some features may be limited.\n'));
|
|
66
115
|
}
|
|
67
116
|
|
|
68
117
|
return info;
|
|
69
118
|
} catch (e: any) {
|
|
70
119
|
s.stop(red('Failed to verify subscription.'));
|
|
71
120
|
if (e.response && e.response.status === 401) {
|
|
72
|
-
console.log(red('
|
|
73
|
-
// Optionally clear key?
|
|
121
|
+
console.log(red(' API Key expired. Please login again.'));
|
|
74
122
|
}
|
|
75
123
|
return null;
|
|
76
124
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { green, dim, cyan, bold, yellow, gray, spinner } from '../ui';
|
|
2
|
+
import { getConfig } from '../config';
|
|
3
|
+
|
|
4
|
+
// Session-level telemetry tracker
|
|
5
|
+
let sessionStart = Date.now();
|
|
6
|
+
let apiCallCount = 0;
|
|
7
|
+
let totalLatencyMs = 0;
|
|
8
|
+
let errorCount = 0;
|
|
9
|
+
|
|
10
|
+
export function trackApiCall(latencyMs: number, isError: boolean = false) {
|
|
11
|
+
apiCallCount++;
|
|
12
|
+
totalLatencyMs += latencyMs;
|
|
13
|
+
if (isError) errorCount++;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function handleTokens() {
|
|
17
|
+
const config = getConfig();
|
|
18
|
+
const apiKey = config.apiKey;
|
|
19
|
+
|
|
20
|
+
console.log('');
|
|
21
|
+
console.log(bold(' ╔══════════════════════════════════════╗'));
|
|
22
|
+
console.log(bold(' ║ TOKEN USAGE ║'));
|
|
23
|
+
console.log(bold(' ╚══════════════════════════════════════╝'));
|
|
24
|
+
console.log('');
|
|
25
|
+
|
|
26
|
+
if (!apiKey) {
|
|
27
|
+
console.log(yellow(' Login required to view token usage.\n'));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const s = spinner();
|
|
32
|
+
s.start('Fetching token usage...');
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const axios = require('axios');
|
|
36
|
+
const baseUrl = config.baseUrl || 'https://api.langtrain.xyz';
|
|
37
|
+
const res = await axios.get(`${baseUrl}/v1/usage/tokens`, {
|
|
38
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const usage = res.data;
|
|
42
|
+
s.stop(green('Token usage retrieved'));
|
|
43
|
+
console.log('');
|
|
44
|
+
|
|
45
|
+
const used = usage.tokens_used || 0;
|
|
46
|
+
const limit = usage.token_limit || 10000;
|
|
47
|
+
const pct = Math.round((used / limit) * 100);
|
|
48
|
+
const remaining = Math.max(0, limit - used);
|
|
49
|
+
|
|
50
|
+
// Visual bar
|
|
51
|
+
const barWidth = 30;
|
|
52
|
+
const filled = Math.round((pct / 100) * barWidth);
|
|
53
|
+
const bar = '█'.repeat(filled) + '░'.repeat(barWidth - filled);
|
|
54
|
+
const barColor = pct > 90 ? '\x1b[31m' : pct > 70 ? '\x1b[33m' : '\x1b[32m';
|
|
55
|
+
|
|
56
|
+
console.log(` ${dim('Period:')} ${usage.period || 'Current Month'}`);
|
|
57
|
+
console.log(` ${dim('Used:')} ${used.toLocaleString()} tokens`);
|
|
58
|
+
console.log(` ${dim('Limit:')} ${limit.toLocaleString()} tokens`);
|
|
59
|
+
console.log(` ${dim('Remaining:')} ${remaining.toLocaleString()} tokens`);
|
|
60
|
+
console.log(` ${dim('Usage:')} ${barColor}${bar}\x1b[0m ${pct}%`);
|
|
61
|
+
console.log('');
|
|
62
|
+
|
|
63
|
+
if (usage.breakdown) {
|
|
64
|
+
console.log(dim(' ── Breakdown ────────────────────────'));
|
|
65
|
+
console.log(` ${dim('Training:')} ${(usage.breakdown.training || 0).toLocaleString()}`);
|
|
66
|
+
console.log(` ${dim('Inference:')} ${(usage.breakdown.inference || 0).toLocaleString()}`);
|
|
67
|
+
console.log(` ${dim('Agents:')} ${(usage.breakdown.agents || 0).toLocaleString()}`);
|
|
68
|
+
console.log('');
|
|
69
|
+
}
|
|
70
|
+
} catch (e: any) {
|
|
71
|
+
s.stop('');
|
|
72
|
+
// Show mock data if API not available
|
|
73
|
+
console.log(dim(' Token data not available from server.'));
|
|
74
|
+
console.log(dim(' Showing session estimates:\n'));
|
|
75
|
+
|
|
76
|
+
console.log(` ${dim('Session calls:')} ${apiCallCount}`);
|
|
77
|
+
console.log(` ${dim('Est. tokens:')} ~${apiCallCount * 150}`);
|
|
78
|
+
console.log('');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function handleTelemetry() {
|
|
83
|
+
const uptimeMs = Date.now() - sessionStart;
|
|
84
|
+
const uptimeSec = Math.round(uptimeMs / 1000);
|
|
85
|
+
const uptimeMin = Math.floor(uptimeSec / 60);
|
|
86
|
+
const uptimeStr = uptimeMin > 0 ? `${uptimeMin}m ${uptimeSec % 60}s` : `${uptimeSec}s`;
|
|
87
|
+
|
|
88
|
+
const avgLatency = apiCallCount > 0 ? Math.round(totalLatencyMs / apiCallCount) : 0;
|
|
89
|
+
const errorRate = apiCallCount > 0 ? Math.round((errorCount / apiCallCount) * 100) : 0;
|
|
90
|
+
|
|
91
|
+
console.log('');
|
|
92
|
+
console.log(bold(' ╔══════════════════════════════════════╗'));
|
|
93
|
+
console.log(bold(' ║ SESSION TELEMETRY ║'));
|
|
94
|
+
console.log(bold(' ╚══════════════════════════════════════╝'));
|
|
95
|
+
console.log('');
|
|
96
|
+
|
|
97
|
+
console.log(` ${dim('Session:')} ${uptimeStr}`);
|
|
98
|
+
console.log(` ${dim('API calls:')} ${apiCallCount}`);
|
|
99
|
+
console.log(` ${dim('Avg latency:')} ${avgLatency}ms`);
|
|
100
|
+
console.log(` ${dim('Errors:')} ${errorCount} (${errorRate}%)`);
|
|
101
|
+
console.log('');
|
|
102
|
+
|
|
103
|
+
console.log(dim(' ── Environment ──────────────────────'));
|
|
104
|
+
console.log(` ${dim('Node:')} ${process.version}`);
|
|
105
|
+
console.log(` ${dim('Platform:')} ${process.platform} ${process.arch}`);
|
|
106
|
+
console.log(` ${dim('Memory:')} ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)}MB heap`);
|
|
107
|
+
console.log(` ${dim('Config:')} ~/.langtrain/config.json`);
|
|
108
|
+
console.log('');
|
|
109
|
+
|
|
110
|
+
// API health check
|
|
111
|
+
const config = getConfig();
|
|
112
|
+
if (config.apiKey) {
|
|
113
|
+
const s = spinner();
|
|
114
|
+
s.start('Pinging API...');
|
|
115
|
+
try {
|
|
116
|
+
const axios = require('axios');
|
|
117
|
+
const baseUrl = config.baseUrl || 'https://api.langtrain.xyz';
|
|
118
|
+
const start = Date.now();
|
|
119
|
+
await axios.get(`${baseUrl}/health`, { timeout: 5000 });
|
|
120
|
+
const latency = Date.now() - start;
|
|
121
|
+
s.stop(green(`API healthy (${latency}ms)`));
|
|
122
|
+
} catch {
|
|
123
|
+
s.stop(yellow('API unreachable'));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
console.log('');
|
|
127
|
+
}
|