langtrain 0.1.25 → 0.2.0

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,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
+ }
package/src/cli/index.ts CHANGED
@@ -1,9 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
- import path from 'path';
4
- import { select, isCancel, outro, intro, colors } from './ui'; // Ensure clear is exported if added, otherwise use console.clear()
5
- import { showBanner } from './ui';
6
- import { ensureAuth, handleLogin, getSubscription } from './auth';
3
+ import { select, isCancel, outro, intro, colors, showBanner } from './ui';
4
+ import { ensureAuth, handleLogin, handleLogout, getSubscription, isAuthenticated } from './auth';
7
5
  import { getMenu, MenuState } from './menu';
8
6
  import { getConfig } from './config';
9
7
 
@@ -18,24 +16,66 @@ import { handleDataUpload, handleDataRefine } from './handlers/data';
18
16
  import { handleDeploy } from './handlers/deploy';
19
17
  import { handleDev } from './handlers/dev';
20
18
  import { handleGuardrailList, handleGuardrailCreate } from './handlers/guardrails';
21
-
22
19
  import { handleEnvMenu } from './handlers/env';
23
20
  import { handleLogs } from './handlers/logs';
21
+ import { handleTokens, handleTelemetry } from './handlers/telemetry';
24
22
 
25
23
  // Clients
26
24
  import { SubscriptionInfo, Langvision, Langtune, AgentClient, ModelClient, FileClient, TrainingClient, SecretClient } from '../index';
27
25
  import packageJson from '../../package.json';
28
26
 
27
+ function showStatusBar(plan: SubscriptionInfo | null) {
28
+ const { dim, green, yellow, cyan, bold, gray } = colors;
29
+
30
+ const planLabel = plan?.plan === 'pro'
31
+ ? bold(green('PRO'))
32
+ : plan?.plan === 'enterprise'
33
+ ? bold(green('ENTERPRISE'))
34
+ : dim('FREE');
35
+
36
+ const tokensUsed = plan?.usage?.tokensUsedThisMonth || 0;
37
+ const tokenLimit = plan?.usage?.tokenLimit || 10000;
38
+ const pct = Math.round((tokensUsed / tokenLimit) * 100);
39
+ const tokenBar = pct > 80 ? yellow(`${pct}%`) : green(`${pct}%`);
40
+
41
+ console.log(dim(' ─────────────────────────────────────────────'));
42
+ console.log(` ${dim('Plan:')} ${planLabel} ${dim('│')} ${dim('Tokens:')} ${tokensUsed.toLocaleString()}/${tokenLimit.toLocaleString()} ${tokenBar}`);
43
+ console.log(dim(' ─────────────────────────────────────────────\n'));
44
+ }
45
+
46
+ function buildClients(apiKey: string, baseUrl?: string) {
47
+ return {
48
+ vision: new Langvision({ apiKey }),
49
+ tune: new Langtune({ apiKey }),
50
+ agent: new AgentClient({ apiKey, baseUrl }),
51
+ model: new ModelClient({ apiKey, baseUrl }),
52
+ train: new TrainingClient({ apiKey, baseUrl }),
53
+ secret: new SecretClient({ apiKey, baseUrl }),
54
+ };
55
+ }
56
+
57
+ function getMessageForState(state: MenuState): string {
58
+ switch (state) {
59
+ case 'main': return 'What would you like to do?';
60
+ case 'agents': return 'Agents:';
61
+ case 'text': return 'Langtune (Text):';
62
+ case 'vision': return 'Langvision (Vision):';
63
+ case 'guard': return 'Guardrails:';
64
+ case 'settings': return 'Settings:';
65
+ default: return 'Select an option:';
66
+ }
67
+ }
68
+
29
69
  export async function main() {
30
70
  const program = new Command();
31
71
  const version = packageJson.version;
32
72
 
33
73
  program
34
74
  .name('langtrain')
35
- .description(packageJson.description || 'Langtrain CLI for AI Model Fine-tuning and Generation')
75
+ .description('Langtrain CLI Fine-tuning, Agents, and AI Ops')
36
76
  .version(version);
37
77
 
38
- // Register standalone commands
78
+ // ── Standalone commands (work without interactive mode) ─────────────
39
79
  program.command('init')
40
80
  .description('Initialize a new Langtrain project')
41
81
  .action(handleInit);
@@ -50,7 +90,7 @@ export async function main() {
50
90
  });
51
91
 
52
92
  program.command('dev')
53
- .description('Start local development server (Watch Mode)')
93
+ .description('Start local development server')
54
94
  .action(async () => {
55
95
  const config = getConfig();
56
96
  const apiKey = config.apiKey || '';
@@ -76,57 +116,91 @@ export async function main() {
76
116
  await handleLogs(client, agent);
77
117
  });
78
118
 
119
+ program.command('login')
120
+ .description('Authenticate with your API key')
121
+ .action(async () => {
122
+ await handleLogin();
123
+ });
124
+
125
+ program.command('logout')
126
+ .description('Clear stored credentials')
127
+ .action(async () => {
128
+ await handleLogout();
129
+ });
130
+
131
+ program.command('tokens')
132
+ .description('View token usage for current period')
133
+ .action(handleTokens);
134
+
135
+ // ── Data commands ──────────────────────────────────────────────────
136
+ const dataCommand = program.command('data').description('Manage datasets');
137
+
138
+ dataCommand.command('upload [file]')
139
+ .description('Upload a dataset')
140
+ .action(async () => {
141
+ const config = getConfig();
142
+ const client = new FileClient({ apiKey: config.apiKey || '', baseUrl: config.baseUrl });
143
+ await handleDataUpload(client);
144
+ });
145
+
146
+ dataCommand.command('refine [fileId]')
147
+ .description('Refine a dataset using guardrails')
148
+ .action(async (fileId) => {
149
+ const config = getConfig();
150
+ const client = new FileClient({ apiKey: config.apiKey || '', baseUrl: config.baseUrl });
151
+ await handleDataRefine(client, fileId);
152
+ });
153
+
154
+ // ── Guardrail commands ─────────────────────────────────────────────
155
+ const guardCommand = program.command('guardrails').description('Manage data guardrails');
156
+
157
+ guardCommand.command('list')
158
+ .description('List available guardrails')
159
+ .action(async () => await handleGuardrailList(null));
160
+
161
+ guardCommand.command('create')
162
+ .description('Create a new guardrail')
163
+ .action(async () => await handleGuardrailCreate(null));
164
+
165
+ // ── Interactive mode (default action) ──────────────────────────────
79
166
  program.action(async () => {
80
167
  showBanner(version);
81
168
 
82
- // 1. Auth & Plan Check Force
83
- // 1. Auth & Plan Check (Lazy)
84
- // 0. First Run Check
169
+ // First-run check
85
170
  const isFirstRun = process.argv.includes('--first-run');
86
171
  if (isFirstRun) {
87
- // Check if interactive
88
172
  if (process.stdin.isTTY) {
89
173
  intro('Welcome to Langtrain! Let\'s get you set up.');
90
174
  await handleLogin();
91
- // Reload config after login
92
175
  } else {
93
176
  console.log('Langtrain installed! Run "npx langtrain login" to authenticate.');
94
177
  process.exit(0);
95
178
  }
96
179
  }
97
180
 
98
- // 1. Auth & Plan Check (Lazy)
181
+ // ── Auth-gated flow ───────────────────────────────────────────
99
182
  let config = getConfig();
100
183
  let apiKey = config.apiKey || '';
184
+ let authed = isAuthenticated();
101
185
  let plan: SubscriptionInfo | null = null;
102
186
 
103
- // Try to fetch plan if key exists?
104
- if (apiKey) {
187
+ // If authenticated, show status bar
188
+ if (authed && apiKey) {
105
189
  try { plan = await getSubscription(apiKey); } catch { }
190
+ showStatusBar(plan);
191
+ } else {
192
+ console.log(colors.dim(' Not logged in. Only basic options available.\n'));
106
193
  }
107
194
 
108
- // 2. Global Client Init
109
- let clients = {
110
- vision: new Langvision({ apiKey }),
111
- tune: new Langtune({ apiKey }),
112
- agent: new AgentClient({ apiKey, baseUrl: config.baseUrl }),
113
- model: new ModelClient({ apiKey, baseUrl: config.baseUrl }),
114
- train: new TrainingClient({ apiKey, baseUrl: config.baseUrl }),
115
- secret: new SecretClient({ apiKey, baseUrl: config.baseUrl })
116
- };
117
-
118
- // 3. Navigation Loop
195
+ let clients = authed ? buildClients(apiKey, config.baseUrl) : null;
196
+
197
+ // ── Navigation loop ──────────────────────────────────────────
119
198
  let currentState: MenuState = 'main';
120
199
 
121
200
  while (true) {
122
- // Clear screen for clean sub-menu navigation?
123
- // Maybe not full clear to keep banner, but at least separate visual blocks.
124
- // showBanner(version); // Re-showing banner might be too much flickering.
125
- // console.log(''); // simple spacer
126
-
127
201
  const operation = await select({
128
202
  message: getMessageForState(currentState),
129
- options: getMenu(currentState, plan, !!apiKey)
203
+ options: getMenu(currentState, plan, authed)
130
204
  });
131
205
 
132
206
  if (isCancel(operation)) {
@@ -141,135 +215,88 @@ export async function main() {
141
215
 
142
216
  const op = operation as string;
143
217
 
144
- // Navigation Logic
145
- if (op === 'exit') {
146
- outro('Goodbye!');
147
- process.exit(0);
148
- }
149
- if (op === 'back') {
150
- currentState = 'main';
151
- continue;
152
- }
153
- if (op.startsWith('nav-')) {
154
- currentState = op.replace('nav-', '') as MenuState;
155
- continue;
156
- }
218
+ // Navigation
219
+ if (op === 'exit') { outro('Goodbye!'); process.exit(0); }
220
+ if (op === 'back') { currentState = 'main'; continue; }
221
+ if (op.startsWith('nav-')) { currentState = op.replace('nav-', '') as MenuState; continue; }
157
222
 
158
- // Action Logic
223
+ // Actions
159
224
  try {
160
225
  switch (op) {
226
+ // Auth
161
227
  case 'login':
162
228
  await handleLogin();
163
229
  config = getConfig();
164
230
  apiKey = config.apiKey || '';
165
- clients = {
166
- vision: new Langvision({ apiKey }),
167
- tune: new Langtune({ apiKey }),
168
- agent: new AgentClient({ apiKey, baseUrl: config.baseUrl }),
169
- model: new ModelClient({ apiKey, baseUrl: config.baseUrl }),
170
- train: new TrainingClient({ apiKey, baseUrl: config.baseUrl }),
171
- secret: new SecretClient({ apiKey, baseUrl: config.baseUrl })
172
- };
173
- try { plan = await getSubscription(apiKey); } catch { }
231
+ authed = isAuthenticated();
232
+ if (authed) {
233
+ try { plan = await getSubscription(apiKey); } catch { }
234
+ clients = buildClients(apiKey, config.baseUrl);
235
+ console.clear();
236
+ showBanner(version);
237
+ showStatusBar(plan);
238
+ }
239
+ break;
240
+
241
+ case 'logout':
242
+ await handleLogout();
243
+ apiKey = '';
244
+ authed = false;
245
+ plan = null;
246
+ clients = null;
247
+ console.clear();
248
+ showBanner(version);
249
+ console.log(colors.dim(' Logged out. Only basic options available.\n'));
174
250
  break;
251
+
252
+ case 'docs':
253
+ console.log(colors.cyan('\n 📖 https://docs.langtrain.ai\n'));
254
+ break;
255
+
256
+ // Status & info
175
257
  case 'status': await handleSubscriptionStatus(); break;
176
- case 'init': await handleInit(); break;
177
- case 'deploy': await handleDeploy(clients.agent); break;
178
- case 'dev': await handleDev(clients.agent); break;
179
- case 'env': await handleEnvMenu(clients.secret); break;
180
- case 'logs': await handleLogs(clients.agent); break;
258
+ case 'tokens': await handleTokens(); break;
259
+ case 'telemetry': await handleTelemetry(); break;
181
260
  case 'doctor': await handleDoctor(); break;
182
- case 'tune-finetune': await handleTuneFinetune(clients.tune, clients.model); break;
183
- case 'tune-list': await handleTuneList(clients.train); break;
184
- case 'tune-generate': await handleTuneGenerate(clients.tune); break;
185
- case 'vision-finetune': await handleVisionFinetune(clients.vision, clients.model); break;
186
- case 'vision-generate': await handleVisionGenerate(clients.vision); break;
187
- case 'agent-list': await handleAgentList(clients.agent); break;
188
- case 'agent-create': await handleAgentCreate(clients.agent, clients.model); break;
189
- case 'agent-delete': await handleAgentDelete(clients.agent); break;
190
- case 'data-upload': await handleDataUpload(new FileClient({ apiKey })); break;
261
+
262
+ // Project
263
+ case 'init': await handleInit(); break;
264
+ case 'deploy': if (clients) await handleDeploy(clients.agent); break;
265
+ case 'dev': if (clients) await handleDev(clients.agent); break;
266
+ case 'env': if (clients) await handleEnvMenu(clients.secret); break;
267
+ case 'logs': if (clients) await handleLogs(clients.agent); break;
268
+
269
+ // Tune
270
+ case 'tune-finetune': if (clients) await handleTuneFinetune(clients.tune, clients.model); break;
271
+ case 'tune-list': if (clients) await handleTuneList(clients.train); break;
272
+ case 'tune-generate': if (clients) await handleTuneGenerate(clients.tune); break;
273
+
274
+ // Vision
275
+ case 'vision-finetune': if (clients) await handleVisionFinetune(clients.vision, clients.model); break;
276
+ case 'vision-generate': if (clients) await handleVisionGenerate(clients.vision); break;
277
+
278
+ // Agents
279
+ case 'agent-list': if (clients) await handleAgentList(clients.agent); break;
280
+ case 'agent-create': if (clients) await handleAgentCreate(clients.agent, clients.model); break;
281
+ case 'agent-delete': if (clients) await handleAgentDelete(clients.agent); break;
282
+
283
+ // Data
284
+ case 'data-upload':
285
+ if (apiKey) await handleDataUpload(new FileClient({ apiKey }));
286
+ break;
287
+ case 'data-refine':
288
+ if (apiKey) await handleDataRefine(new FileClient({ apiKey }));
289
+ break;
290
+
291
+ // Guardrails
191
292
  case 'guard-list': await handleGuardrailList(null); break;
192
293
  case 'guard-create': await handleGuardrailCreate(null); break;
193
- case 'data-refine': await handleDataRefine(new FileClient({ apiKey })); break;
194
294
  }
195
-
196
- // After action, where do we go?
197
- // Stay in current state (sub-menu) is usually preferred.
198
-
199
295
  } catch (error: any) {
200
296
  outro(colors.red(`Error: ${error.message}`));
201
297
  }
202
298
  }
203
299
  });
204
300
 
205
- const dataCommand = program.command('data')
206
- .description('Manage datasets');
207
-
208
- dataCommand.command('upload [file]')
209
- .description('Upload a dataset')
210
- .action(async (file) => {
211
- const config = getConfig();
212
- const apiKey = config.apiKey || '';
213
- const client = new FileClient({ apiKey, baseUrl: config.baseUrl });
214
- // handleDataUpload only takes client, file is prompted inside or we need to update handleDataUpload signature
215
- await handleDataUpload(client);
216
- });
217
-
218
- dataCommand.command('analyze')
219
- .description('Analyze a dataset with AI')
220
- .action(async () => {
221
- const config = getConfig();
222
- const apiKey = config.apiKey || '';
223
- const client = new FileClient({ apiKey, baseUrl: config.baseUrl });
224
- // handleDataAnalyze needs to be exported/imported
225
- // Assuming I named it handleDataAnalyze in previous step (I did edit existing function, likely need to rename or export new one)
226
- // Wait, I updated handleDataList in previous step to be the analyze function?
227
- // No, I added code TO handleDataList or replaced it?
228
- // Let me check previous tool call.
229
- // I replaced the end of handleDataList (the mocked download part) with analyze logic?
230
- // I should verify data.ts structure.
231
- // Let's assume I need to properly export handleDataAnalyze.
232
- // For now, I'll register it assuming export.
233
- // For now, I'll register it assuming export.
234
- const { handleDataList } = require('./handlers/data');
235
- await handleDataList(client);
236
- });
237
-
238
- dataCommand.command('refine [fileId]')
239
- .description('Refine a dataset using guardrails')
240
- .action(async (fileId) => {
241
- const config = getConfig();
242
- const apiKey = config.apiKey || '';
243
- const client = new FileClient({ apiKey, baseUrl: config.baseUrl });
244
- await handleDataRefine(client, fileId);
245
- });
246
-
247
- const guardCommand = program.command('guardrails')
248
- .description('Manage data guardrails');
249
-
250
- guardCommand.command('list')
251
- .description('List available guardrails')
252
- .action(async () => {
253
- await handleGuardrailList(null);
254
- });
255
-
256
- guardCommand.command('create')
257
- .description('Create a new guardrail')
258
- .action(async () => {
259
- await handleGuardrailCreate(null);
260
- });
261
-
262
-
263
301
  main().catch(console.error);
264
-
265
- function getMessageForState(state: MenuState): string {
266
- switch (state) {
267
- case 'main': return 'Main Menu:';
268
- case 'agents': return 'Agents & Tools:';
269
- case 'text': return 'Langtune (Text Operations):';
270
- case 'vision': return 'Langvision (Vision Operations):';
271
- case 'settings': return 'Settings:';
272
- default: return 'Select an option:';
273
- }
274
- }
275
302
  }
package/src/cli/menu.ts CHANGED
@@ -9,45 +9,44 @@ export interface MenuOption {
9
9
  export type MenuState = 'main' | 'agents' | 'text' | 'vision' | 'guard' | 'settings';
10
10
 
11
11
  export function getMenu(state: MenuState, plan: SubscriptionInfo | null, isAuthenticated: boolean): MenuOption[] {
12
- const isPro = plan?.plan === 'pro' || plan?.plan === 'enterprise';
13
12
 
14
- // If not authenticated, force login or limited menu?
15
- // User requested "lazy auth", so we should show menu but maybe highlight login or allow navigation and prompt later.
16
- // Let's add a visual cue.
13
+ // ── Unauthenticated: Claude-style minimal menu ─────────────────────────
14
+ if (!isAuthenticated) {
15
+ return [
16
+ { value: 'login', label: '→ Login', hint: 'Authenticate with your API key' },
17
+ { value: 'docs', label: ' Documentation', hint: 'https://docs.langtrain.ai' },
18
+ { value: 'exit', label: ' Exit' }
19
+ ];
20
+ }
21
+
22
+ // ── Authenticated menus ────────────────────────────────────────────────
23
+ const planLabel = plan?.plan === 'pro' ? 'PRO' : plan?.plan === 'enterprise' ? 'ENTERPRISE' : 'FREE';
17
24
 
18
25
  switch (state) {
19
26
  case 'main':
20
- const menu: MenuOption[] = [
21
- { value: 'nav-agents', label: 'Agents', hint: 'Manage & Chat with AI Agents' },
22
- { value: 'nav-text', label: 'Langtune (Text)', hint: 'Fine-tuning & Generation' },
23
- { value: 'nav-vision', label: 'Langvision (Vision)', hint: 'Vision Analysis & Tuning' },
24
- { value: 'nav-guard', label: 'Data Guardrails', hint: 'Quality & Safety Rules' },
25
- { value: 'init', label: 'Initialize Project', hint: 'Scaffold new Langtrain app' },
26
- { value: 'deploy', label: 'Deploy', hint: 'Push config to Cloud' },
27
- { value: 'dev', label: 'Start Dev Server', hint: 'Watch mode' },
28
- { value: 'env', label: 'Secrets (Env)', hint: 'Manage API Keys' },
29
- { value: 'logs', label: 'Logs', hint: 'View Agent Logs' },
30
- { value: 'doctor', label: 'Doctor', hint: 'Check environment health' },
31
- { value: 'nav-settings', label: 'Settings', hint: 'Subscription & Auth' }
27
+ return [
28
+ { value: 'nav-agents', label: ' Agents', hint: 'Manage & deploy AI agents' },
29
+ { value: 'nav-text', label: ' Langtune', hint: 'Text fine-tuning & generation' },
30
+ { value: 'nav-vision', label: ' Langvision', hint: 'Vision fine-tuning & analysis' },
31
+ { value: 'nav-guard', label: ' Guardrails', hint: 'Data quality & safety rules' },
32
+ { value: 'init', label: ' Init Project', hint: 'Scaffold new Langtrain app' },
33
+ { value: 'deploy', label: ' Deploy', hint: 'Push to Langtrain Cloud' },
34
+ { value: 'dev', label: ' Dev Server', hint: 'Local watch mode' },
35
+ { value: 'env', label: ' Secrets', hint: 'Manage environment variables' },
36
+ { value: 'logs', label: ' Logs', hint: 'Stream agent logs' },
37
+ { value: 'tokens', label: ' Token Usage', hint: 'View consumption this period' },
38
+ { value: 'telemetry', label: ' Telemetry', hint: 'Session stats & API health' },
39
+ { value: 'doctor', label: ' Doctor', hint: 'Check environment health' },
40
+ { value: 'nav-settings', label: ' Settings', hint: `Plan: ${planLabel}` },
41
+ { value: 'exit', label: ' Exit' }
32
42
  ];
33
43
 
34
- if (!isAuthenticated) {
35
- // menu.unshift({ value: 'login', label: 'Login to Langtrain', hint: 'Required for most features' });
36
- // Actually, let's make Login the first option if not authenticated
37
- // But keep the others so user can see what's available (and get prompted)
38
- menu.unshift({ value: 'login', label: 'Login to Langtrain', hint: 'Required for most features' });
39
- }
40
-
41
- // Always add Exit
42
- menu.push({ value: 'exit', label: 'Exit' });
43
- return menu;
44
-
45
44
  case 'agents':
46
45
  return [
47
46
  { value: 'agent-list', label: 'List & Run Agents', hint: 'Chat with active agents' },
48
47
  { value: 'agent-create', label: 'Create New Agent', hint: 'Deploy a new agent' },
49
48
  { value: 'agent-delete', label: 'Delete Agent', hint: 'Remove an agent' },
50
- { value: 'back', label: '← Back to Main Menu' }
49
+ { value: 'back', label: '← Back' }
51
50
  ];
52
51
 
53
52
  case 'text':
@@ -56,7 +55,7 @@ export function getMenu(state: MenuState, plan: SubscriptionInfo | null, isAuthe
56
55
  { value: 'tune-list', label: 'List Jobs', hint: 'Check training status' },
57
56
  { value: 'tune-generate', label: 'Generate Text', hint: 'Test your models' },
58
57
  { value: 'data-upload', label: 'Upload Dataset', hint: 'Upload JSONL for training' },
59
- { value: 'back', label: '← Back to Main Menu' }
58
+ { value: 'back', label: '← Back' }
60
59
  ];
61
60
 
62
61
  case 'guard':
@@ -64,21 +63,24 @@ export function getMenu(state: MenuState, plan: SubscriptionInfo | null, isAuthe
64
63
  { value: 'guard-list', label: 'List Guardrails', hint: 'View active rules' },
65
64
  { value: 'guard-create', label: 'Create Guardrail', hint: 'Define new rules' },
66
65
  { value: 'data-refine', label: 'Refine Dataset', hint: 'Apply guardrail to data' },
67
- { value: 'back', label: '← Back to Main Menu' }
66
+ { value: 'back', label: '← Back' }
68
67
  ];
69
68
 
70
69
  case 'vision':
71
70
  return [
72
71
  { value: 'vision-finetune', label: 'Fine-tune Vision Model', hint: 'Create custom VLM' },
73
72
  { value: 'vision-generate', label: 'Generate Vision Response', hint: 'Test vision models' },
74
- { value: 'back', label: '← Back to Main Menu' }
73
+ { value: 'back', label: '← Back' }
75
74
  ];
76
75
 
77
76
  case 'settings':
78
77
  return [
79
- { value: 'status', label: isAuthenticated ? `Subscription Status (${plan?.plan || 'Free'})` : 'Check Status (Login required)' },
80
- { value: 'login', label: isAuthenticated ? 'Update API Key' : 'Login' },
81
- { value: 'back', label: ' Back to Main Menu' }
78
+ { value: 'status', label: `Subscription (${planLabel})`, hint: 'View plan details' },
79
+ { value: 'tokens', label: 'Token Usage', hint: 'View consumption' },
80
+ { value: 'telemetry', label: 'Telemetry', hint: 'Session & API health' },
81
+ { value: 'login', label: 'Update API Key', hint: 'Change credentials' },
82
+ { value: 'logout', label: 'Logout', hint: 'Clear stored credentials' },
83
+ { value: 'back', label: '← Back' }
82
84
  ];
83
85
 
84
86
  default: