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.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { AxiosInstance } from 'axios';
1
2
  import * as langvision from 'langvision';
2
3
  export { langvision as Vision };
3
4
  export { Langvision } from 'langvision';
@@ -5,17 +6,87 @@ import * as langtune from 'langtune';
5
6
  export { langtune as Text };
6
7
  export { Langtune } from 'langtune';
7
8
 
9
+ /** Configuration for all Langtrain SDK clients. */
10
+ interface ClientConfig {
11
+ /** Your Langtrain API key. */
12
+ apiKey: string;
13
+ /** Override the default API base URL. */
14
+ baseUrl?: string;
15
+ /** Request timeout in milliseconds (default: 30000). */
16
+ timeout?: number;
17
+ /** Maximum number of retries on transient errors (default: 2). */
18
+ maxRetries?: number;
19
+ }
20
+ declare class LangtrainError extends Error {
21
+ /** HTTP status code, if available. */
22
+ readonly status?: number;
23
+ /** Raw error code from the API. */
24
+ readonly code?: string;
25
+ /** The original error, if any. */
26
+ readonly cause?: Error;
27
+ constructor(message: string, options?: {
28
+ status?: number;
29
+ code?: string;
30
+ cause?: Error;
31
+ });
32
+ /** True if the error was a network/timeout issue (retryable). */
33
+ get isTransient(): boolean;
34
+ /** True if the API key was invalid or expired. */
35
+ get isAuthError(): boolean;
36
+ /** True if a resource was not found. */
37
+ get isNotFound(): boolean;
38
+ /** True if rate-limited. */
39
+ get isRateLimited(): boolean;
40
+ }
41
+ /**
42
+ * BaseClient — abstract foundation for all Langtrain SDK clients.
43
+ *
44
+ * Provides:
45
+ * - Shared axios instance with API key auth
46
+ * - Configurable timeouts
47
+ * - Automatic retry with exponential backoff on transient errors
48
+ * - Structured error wrapping (LangtrainError)
49
+ */
50
+ declare abstract class BaseClient {
51
+ protected readonly http: AxiosInstance;
52
+ protected readonly maxRetries: number;
53
+ constructor(config: ClientConfig);
54
+ /**
55
+ * Execute a request with automatic retry and error wrapping.
56
+ */
57
+ protected request<T>(fn: () => Promise<T>): Promise<T>;
58
+ /**
59
+ * Wrap any thrown error into a structured LangtrainError.
60
+ */
61
+ private wrapError;
62
+ private sleep;
63
+ }
64
+
8
65
  interface Agent {
9
66
  id: string;
10
67
  workspace_id: string;
11
68
  name: string;
12
69
  description?: string;
13
70
  model_id?: string;
14
- config: Record<string, unknown>;
71
+ config: AgentConfig;
15
72
  is_active: boolean;
16
73
  created_at: string;
17
74
  updated_at: string;
18
75
  }
76
+ interface AgentConfig {
77
+ system_prompt?: string;
78
+ temperature?: number;
79
+ max_tokens?: number;
80
+ tools?: string[];
81
+ [key: string]: unknown;
82
+ }
83
+ interface AgentCreate {
84
+ workspace_id: string;
85
+ name: string;
86
+ description?: string;
87
+ model_id?: string;
88
+ config?: AgentConfig;
89
+ }
19
90
  interface AgentRun {
20
91
  id: string;
21
92
  conversation_id: string;
@@ -25,35 +96,33 @@ interface AgentRun {
25
96
  latency_ms: number;
26
97
  tokens_used: number;
27
98
  }
28
- declare class AgentClient {
29
- private config;
30
- private client;
31
- constructor(config: {
32
- apiKey: string;
33
- baseUrl?: string;
34
- });
99
+ /**
100
+ * Client for managing AI agents — create, execute, and monitor.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * const agents = new AgentClient({ apiKey: 'lt_...' });
105
+ * const list = await agents.list();
106
+ * const result = await agents.execute(list[0].id, 'Hello!');
107
+ * ```
108
+ */
109
+ declare class AgentClient extends BaseClient {
110
+ constructor(config: ClientConfig);
111
+ /** List all agents, optionally filtered by workspace. */
35
112
  list(workspaceId?: string): Promise<Agent[]>;
113
+ /** Get a single agent by ID. */
36
114
  get(agentId: string): Promise<Agent>;
115
+ /** Create a new agent. */
37
116
  create(agent: AgentCreate): Promise<Agent>;
117
+ /** Delete an agent by ID. */
38
118
  delete(agentId: string): Promise<void>;
39
- execute(agentId: string, input: any, messages?: any[], conversationId?: string): Promise<AgentRun>;
40
- logs(agentId: string, limit?: number): Promise<{
41
- logs: string[];
42
- }>;
43
- }
44
- interface AgentConfig {
45
- system_prompt?: string;
46
- temperature?: number;
47
- max_tokens?: number;
48
- tools?: string[];
49
- [key: string]: any;
50
- }
51
- interface AgentCreate {
52
- workspace_id: string;
53
- name: string;
54
- description?: string;
55
- model_id?: string;
56
- config?: AgentConfig;
119
+ /** Execute an agent with input and optional conversation context. */
120
+ execute(agentId: string, input: string, messages?: Array<{
121
+ role: string;
122
+ content: string;
123
+ }>, conversationId?: string): Promise<AgentRun>;
124
+ /** Fetch recent logs for an agent. */
125
+ logs(agentId: string, limit?: number): Promise<string[]>;
57
126
  }
58
127
 
59
128
  type agent_Agent = Agent;
@@ -66,15 +135,6 @@ declare namespace agent {
66
135
  export { type agent_Agent as Agent, agent_AgentClient as AgentClient, type agent_AgentConfig as AgentConfig, type agent_AgentCreate as AgentCreate, type agent_AgentRun as AgentRun };
67
136
  }
68
137
 
69
- declare class FileClient {
70
- private client;
71
- constructor(config: {
72
- apiKey: string;
73
- baseUrl?: string;
74
- });
75
- upload(file: any, workspaceId?: string, purpose?: string): Promise<FileResponse>;
76
- list(workspaceId: string, purpose?: string): Promise<FileResponse[]>;
77
- }
78
138
  interface FileResponse {
79
139
  id: string;
80
140
  filename: string;
@@ -82,17 +142,33 @@ interface FileResponse {
82
142
  bytes: number;
83
143
  created_at: string;
84
144
  }
145
+ /**
146
+ * Client for managing file uploads (datasets for fine-tuning).
147
+ *
148
+ * @example
149
+ * ```ts
150
+ * const files = new FileClient({ apiKey: 'lt_...' });
151
+ * const uploaded = await files.upload('./data.jsonl');
152
+ * ```
153
+ */
154
+ declare class FileClient extends BaseClient {
155
+ constructor(config: ClientConfig);
156
+ /** Upload a file from a local path. */
157
+ upload(filePath: string, workspaceId?: string, purpose?: string): Promise<FileResponse>;
158
+ /** List files in a workspace, optionally filtered by purpose. */
159
+ list(workspaceId: string, purpose?: string): Promise<FileResponse[]>;
160
+ /** Delete a file by ID. */
161
+ delete(fileId: string): Promise<void>;
162
+ }
85
163
 
86
- declare class TrainingClient {
87
- private client;
88
- constructor(config: {
89
- apiKey: string;
90
- baseUrl?: string;
91
- });
92
- createJob(job: FineTuneJobCreate): Promise<FineTuneJobResponse>;
93
- listJobs(workspaceId: string, limit?: number): Promise<FineTuneJobList>;
94
- getJob(jobId: string): Promise<FineTuneJobResponse>;
95
- cancelJob(jobId: string): Promise<FineTuneJobResponse>;
164
+ interface FineTuneHyperparameters {
165
+ epochs?: number;
166
+ learning_rate?: number;
167
+ batch_size?: number;
168
+ warmup_steps?: number;
169
+ lora_rank?: number;
170
+ lora_alpha?: number;
171
+ weight_decay?: number;
96
172
  }
97
173
  interface FineTuneJobCreate {
98
174
  name?: string;
@@ -102,40 +178,61 @@ interface FineTuneJobCreate {
102
178
  guardrail_id?: string;
103
179
  task?: 'text' | 'vision';
104
180
  training_method?: 'sft' | 'dpo' | 'rlhf' | 'lora' | 'qlora';
105
- hyperparameters?: any;
106
- [key: string]: any;
181
+ hyperparameters?: FineTuneHyperparameters;
107
182
  }
108
183
  interface FineTuneJobResponse {
109
184
  id: string;
110
185
  name: string;
111
- status: string;
186
+ status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
112
187
  progress: number;
113
188
  error_message?: string;
189
+ base_model?: string;
190
+ training_method?: string;
114
191
  created_at: string;
115
- [key: string]: any;
192
+ started_at?: string;
193
+ completed_at?: string;
116
194
  }
117
195
  interface FineTuneJobList {
118
196
  data: FineTuneJobResponse[];
119
197
  has_more: boolean;
120
198
  }
121
-
122
- declare class SubscriptionClient {
123
- private client;
124
- constructor(config: {
125
- apiKey: string;
126
- baseUrl?: string;
127
- });
128
- getStatus(): Promise<SubscriptionInfo>;
129
- checkFeature(feature: string): Promise<FeatureCheck>;
130
- getLimits(): Promise<any>;
199
+ /**
200
+ * Client for managing fine-tuning training jobs.
201
+ *
202
+ * @example
203
+ * ```ts
204
+ * const training = new TrainingClient({ apiKey: 'lt_...' });
205
+ * const job = await training.createJob({
206
+ * base_model: 'meta-llama/Llama-3.1-8B',
207
+ * dataset_id: 'file_abc123',
208
+ * training_method: 'lora',
209
+ * });
210
+ * ```
211
+ */
212
+ declare class TrainingClient extends BaseClient {
213
+ constructor(config: ClientConfig);
214
+ /** Create a new fine-tuning job. */
215
+ createJob(job: FineTuneJobCreate): Promise<FineTuneJobResponse>;
216
+ /** List fine-tuning jobs for a workspace. */
217
+ listJobs(workspaceId: string, limit?: number): Promise<FineTuneJobList>;
218
+ /** Get a specific job by ID. */
219
+ getJob(jobId: string): Promise<FineTuneJobResponse>;
220
+ /** Cancel a running job. */
221
+ cancelJob(jobId: string): Promise<FineTuneJobResponse>;
131
222
  }
223
+
132
224
  interface SubscriptionInfo {
133
225
  is_active: boolean;
134
226
  plan: string;
135
227
  plan_name: string;
136
228
  expires_at?: string;
137
229
  features: string[];
138
- limits: any;
230
+ limits: Record<string, number>;
231
+ usage?: {
232
+ tokensUsedThisMonth?: number;
233
+ tokenLimit?: number;
234
+ apiCalls?: number;
235
+ };
139
236
  }
140
237
  interface FeatureCheck {
141
238
  feature: string;
@@ -143,6 +240,25 @@ interface FeatureCheck {
143
240
  limit?: number;
144
241
  used?: number;
145
242
  }
243
+ /**
244
+ * Client for checking subscription status and feature access.
245
+ *
246
+ * @example
247
+ * ```ts
248
+ * const sub = new SubscriptionClient({ apiKey: 'lt_...' });
249
+ * const status = await sub.getStatus();
250
+ * console.log(status.plan); // 'pro'
251
+ * ```
252
+ */
253
+ declare class SubscriptionClient extends BaseClient {
254
+ constructor(config: ClientConfig);
255
+ /** Get current subscription status. */
256
+ getStatus(): Promise<SubscriptionInfo>;
257
+ /** Check if a specific feature is available on the current plan. */
258
+ checkFeature(feature: string): Promise<FeatureCheck>;
259
+ /** Get usage analytics for the current subscription. */
260
+ getLimits(): Promise<Record<string, unknown>>;
261
+ }
146
262
 
147
263
  interface Permission {
148
264
  id: string;
@@ -155,7 +271,7 @@ interface Permission {
155
271
  allow_view: boolean;
156
272
  allow_fine_tuning: boolean;
157
273
  organization: string;
158
- group: any;
274
+ group: string | null;
159
275
  is_blocking: boolean;
160
276
  }
161
277
  interface Model {
@@ -168,13 +284,21 @@ interface Model {
168
284
  parent: string | null;
169
285
  task?: 'text' | 'vision' | 'agent';
170
286
  }
171
- declare class ModelClient {
172
- private client;
173
- constructor(config: {
174
- apiKey: string;
175
- baseUrl?: string;
176
- });
177
- list(task?: string): Promise<Model[]>;
287
+ /**
288
+ * Client for browsing available base models.
289
+ *
290
+ * @example
291
+ * ```ts
292
+ * const models = new ModelClient({ apiKey: 'lt_...' });
293
+ * const all = await models.list();
294
+ * const textModels = await models.list('text');
295
+ * ```
296
+ */
297
+ declare class ModelClient extends BaseClient {
298
+ constructor(config: ClientConfig);
299
+ /** List available models, optionally filtered by task type. */
300
+ list(task?: 'text' | 'vision' | 'agent'): Promise<Model[]>;
301
+ /** Get a specific model by ID. */
178
302
  get(modelId: string): Promise<Model>;
179
303
  }
180
304
 
@@ -191,15 +315,23 @@ interface Secret {
191
315
  created_at: string;
192
316
  updated_at: string;
193
317
  }
194
- declare class SecretClient {
195
- private config;
196
- private client;
197
- constructor(config: {
198
- apiKey: string;
199
- baseUrl?: string;
200
- });
318
+ /**
319
+ * Client for managing workspace secrets and environment variables.
320
+ *
321
+ * @example
322
+ * ```ts
323
+ * const secrets = new SecretClient({ apiKey: 'lt_...' });
324
+ * await secrets.set('OPENAI_KEY', 'sk-...');
325
+ * const all = await secrets.list();
326
+ * ```
327
+ */
328
+ declare class SecretClient extends BaseClient {
329
+ constructor(config: ClientConfig);
330
+ /** List all secrets in a workspace. Values are redacted. */
201
331
  list(workspaceId?: string): Promise<Secret[]>;
332
+ /** Set (create or update) a secret. */
202
333
  set(key: string, value: string, workspaceId?: string): Promise<Secret>;
334
+ /** Delete a secret by key. */
203
335
  delete(key: string, workspaceId?: string): Promise<void>;
204
336
  }
205
337
 
@@ -235,18 +367,75 @@ interface GuardrailCreate {
235
367
  description?: string;
236
368
  config: GuardrailConfig;
237
369
  }
238
- declare class GuardrailClient {
239
- private config;
240
- private client;
241
- constructor(config: {
242
- apiKey: string;
243
- baseUrl?: string;
244
- });
370
+ interface GuardrailApplyResult {
371
+ total_rows: number;
372
+ passed: number;
373
+ failed: number;
374
+ violations: Array<{
375
+ row: number;
376
+ rule: string;
377
+ message: string;
378
+ }>;
379
+ }
380
+ /**
381
+ * Client for managing data guardrails (PII, profanity, length, regex).
382
+ *
383
+ * @example
384
+ * ```ts
385
+ * const guardrails = new GuardrailClient({ apiKey: 'lt_...' });
386
+ * const rule = await guardrails.create({
387
+ * name: 'PII Filter',
388
+ * config: { pii_enabled: true, profanity_enabled: false },
389
+ * });
390
+ * ```
391
+ */
392
+ declare class GuardrailClient extends BaseClient {
393
+ constructor(config: ClientConfig);
394
+ /** List guardrails, optionally filtered by workspace. */
245
395
  list(workspaceId?: string): Promise<Guardrail[]>;
396
+ /** Get a guardrail by ID. */
246
397
  get(guardrailId: string): Promise<Guardrail>;
398
+ /** Create a new guardrail. */
247
399
  create(data: GuardrailCreate): Promise<Guardrail>;
400
+ /** Delete a guardrail by ID. */
248
401
  delete(guardrailId: string): Promise<void>;
249
- apply(datasetId: string, guardrailId: string): Promise<any>;
402
+ /** Apply a guardrail to a dataset for validation. */
403
+ apply(datasetId: string, guardrailId: string): Promise<GuardrailApplyResult>;
404
+ }
405
+
406
+ interface UsageSummary {
407
+ workspace_id: string;
408
+ plan: string;
409
+ quotas: Record<string, number>;
410
+ billing?: {
411
+ plan_id: string;
412
+ tokens_used: number;
413
+ tokens_limit: number;
414
+ period_end: string;
415
+ };
416
+ }
417
+ interface UsageHistoryPoint {
418
+ date: string;
419
+ tokens: number;
420
+ agent_runs: number;
421
+ cost: number;
422
+ }
423
+ /**
424
+ * Client for querying usage metrics and billing data.
425
+ *
426
+ * @example
427
+ * ```ts
428
+ * const usage = new UsageClient({ apiKey: 'lt_...' });
429
+ * const summary = await usage.getSummary('ws_abc123');
430
+ * console.log(summary.billing?.tokens_used);
431
+ * ```
432
+ */
433
+ declare class UsageClient extends BaseClient {
434
+ constructor(config: ClientConfig);
435
+ /** Get current usage summary for a workspace. */
436
+ getSummary(workspaceId: string): Promise<UsageSummary>;
437
+ /** Get historical usage data for charts. */
438
+ getHistory(workspaceId: string, days?: number): Promise<UsageHistoryPoint[]>;
250
439
  }
251
440
 
252
- 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 };
441
+ export { type Agent, AgentClient, type AgentConfig, type AgentCreate, type AgentRun, agent as AgentTypes, BaseClient, type ClientConfig, type FeatureCheck, FileClient, type FileResponse, type FineTuneHyperparameters, type FineTuneJobCreate, type FineTuneJobList, type FineTuneJobResponse, type Guardrail, type GuardrailApplyResult, GuardrailClient, type GuardrailConfig, type GuardrailCreate, LangtrainError, type Model, ModelClient, models as ModelTypes, type Permission, 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 chunkQ5EF25B2_js=require('./chunk-Q5EF25B2.js');chunkQ5EF25B2_js.r();Object.defineProperty(exports,"AgentClient",{enumerable:true,get:function(){return chunkQ5EF25B2_js.d}});Object.defineProperty(exports,"AgentTypes",{enumerable:true,get:function(){return chunkQ5EF25B2_js.e}});Object.defineProperty(exports,"FileClient",{enumerable:true,get:function(){return chunkQ5EF25B2_js.f}});Object.defineProperty(exports,"GuardrailClient",{enumerable:true,get:function(){return chunkQ5EF25B2_js.m}});Object.defineProperty(exports,"Langtune",{enumerable:true,get:function(){return chunkQ5EF25B2_js.q}});Object.defineProperty(exports,"Langvision",{enumerable:true,get:function(){return chunkQ5EF25B2_js.p}});Object.defineProperty(exports,"ModelClient",{enumerable:true,get:function(){return chunkQ5EF25B2_js.i}});Object.defineProperty(exports,"ModelTypes",{enumerable:true,get:function(){return chunkQ5EF25B2_js.j}});Object.defineProperty(exports,"SecretClient",{enumerable:true,get:function(){return chunkQ5EF25B2_js.k}});Object.defineProperty(exports,"SecretTypes",{enumerable:true,get:function(){return chunkQ5EF25B2_js.l}});Object.defineProperty(exports,"SubscriptionClient",{enumerable:true,get:function(){return chunkQ5EF25B2_js.h}});Object.defineProperty(exports,"Text",{enumerable:true,get:function(){return chunkQ5EF25B2_js.o}});Object.defineProperty(exports,"TrainingClient",{enumerable:true,get:function(){return chunkQ5EF25B2_js.g}});Object.defineProperty(exports,"Vision",{enumerable:true,get:function(){return chunkQ5EF25B2_js.n}});//# sourceMappingURL=index.js.map
1
+ 'use strict';var chunkZN3AO753_js=require('./chunk-ZN3AO753.js');Object.defineProperty(exports,"AgentClient",{enumerable:true,get:function(){return chunkZN3AO753_js.d}});Object.defineProperty(exports,"AgentTypes",{enumerable:true,get:function(){return chunkZN3AO753_js.e}});Object.defineProperty(exports,"BaseClient",{enumerable:true,get:function(){return chunkZN3AO753_js.c}});Object.defineProperty(exports,"FileClient",{enumerable:true,get:function(){return chunkZN3AO753_js.f}});Object.defineProperty(exports,"GuardrailClient",{enumerable:true,get:function(){return chunkZN3AO753_js.m}});Object.defineProperty(exports,"LangtrainError",{enumerable:true,get:function(){return chunkZN3AO753_js.b}});Object.defineProperty(exports,"Langtune",{enumerable:true,get:function(){return chunkZN3AO753_js.r}});Object.defineProperty(exports,"Langvision",{enumerable:true,get:function(){return chunkZN3AO753_js.q}});Object.defineProperty(exports,"ModelClient",{enumerable:true,get:function(){return chunkZN3AO753_js.i}});Object.defineProperty(exports,"ModelTypes",{enumerable:true,get:function(){return chunkZN3AO753_js.j}});Object.defineProperty(exports,"SecretClient",{enumerable:true,get:function(){return chunkZN3AO753_js.k}});Object.defineProperty(exports,"SecretTypes",{enumerable:true,get:function(){return chunkZN3AO753_js.l}});Object.defineProperty(exports,"SubscriptionClient",{enumerable:true,get:function(){return chunkZN3AO753_js.h}});Object.defineProperty(exports,"Text",{enumerable:true,get:function(){return chunkZN3AO753_js.p}});Object.defineProperty(exports,"TrainingClient",{enumerable:true,get:function(){return chunkZN3AO753_js.g}});Object.defineProperty(exports,"UsageClient",{enumerable:true,get:function(){return chunkZN3AO753_js.n}});Object.defineProperty(exports,"Vision",{enumerable:true,get:function(){return chunkZN3AO753_js.o}});//# sourceMappingURL=index.js.map
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import {r}from'./chunk-XRT7LLNF.mjs';export{d as AgentClient,e as AgentTypes,f as FileClient,m as GuardrailClient,q as Langtune,p as Langvision,i as ModelClient,j as ModelTypes,k as SecretClient,l as SecretTypes,h as SubscriptionClient,o as Text,g as TrainingClient,n as Vision}from'./chunk-XRT7LLNF.mjs';r();//# sourceMappingURL=index.mjs.map
1
+ export{d as AgentClient,e as AgentTypes,c as BaseClient,f as FileClient,m as GuardrailClient,b as LangtrainError,r as Langtune,q as Langvision,i as ModelClient,j as ModelTypes,k as SecretClient,l as SecretTypes,h as SubscriptionClient,p as Text,g as TrainingClient,n as UsageClient,o as Vision}from'./chunk-36QS5AXY.mjs';//# sourceMappingURL=index.mjs.map
2
2
  //# sourceMappingURL=index.mjs.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "langtrain",
3
- "version": "0.1.25",
3
+ "version": "0.2.0",
4
4
  "description": "Unified JavaScript SDK for Langtrain Ecosystem",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
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
- intro(yellow('Authentication required to verify plan & features.'));
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(gray('\nGet your API Key at: https://langtrain.xyz/settings/keys\n'));
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
- s.stop(green(`Authenticated as ${info.plan === 'pro' ? 'PRO' : info.plan.toUpperCase()}`));
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
- // intro(green('API Key saved successfully!')); // success message above is enough
45
- return; // Exit loop on success
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('Verifying subscription plan...');
101
+ s.start('Checking subscription...');
57
102
  try {
58
103
  const info = await client.getStatus();
59
104
 
60
- // Enhance: Show plan details immediately on auth check
61
- const planLabel = info.plan === 'pro' ? bgMagenta(' PRO ') : info.plan.toUpperCase();
62
- s.stop(green(`Authenticated as ${planLabel}`));
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('Warning: Your subscription is not active. Some features may be limited.'));
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('Invalid API Key. Please run login again.'));
73
- // Optionally clear key?
121
+ console.log(red(' API Key expired. Please login again.'));
74
122
  }
75
123
  return null;
76
124
  }