langtrain 0.1.18 → 0.1.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/README.md +4 -4
- package/dist/cli.js +7 -5
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +7 -5
- package/dist/cli.mjs.map +1 -1
- package/package.json +3 -2
- package/src/cli/auth.ts +1 -0
- package/src/cli/handlers/agent.ts +3 -3
- package/src/cli/handlers/data.ts +84 -0
- package/src/cli/handlers/deploy.ts +62 -0
- package/src/cli/handlers/dev.ts +42 -0
- package/src/cli/handlers/doctor.ts +54 -0
- package/src/cli/handlers/init.ts +104 -0
- package/src/cli/handlers/tune.ts +103 -1
- package/src/cli/index.ts +42 -6
- package/src/cli/menu.ts +7 -1
- package/src/cli/ui.ts +24 -7
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { text, select, confirm, isCancel, cancel, spinner, intro, red, green, yellow, gray } from '../ui';
|
|
2
|
+
import { getConfig } from '../config';
|
|
3
|
+
import { FileClient } from '../../index';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
|
|
7
|
+
export async function handleDataUpload(client: FileClient) {
|
|
8
|
+
const config = getConfig();
|
|
9
|
+
let workspaceId = config.workspace_id;
|
|
10
|
+
|
|
11
|
+
if (!workspaceId) {
|
|
12
|
+
// Optional: ask for workspace ID or try to infer?
|
|
13
|
+
// For upload, workspace_id is often optional (inferred from API key's default workspace)
|
|
14
|
+
// But let's ask if user wants to specify.
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const filePath = await text({
|
|
18
|
+
message: 'Path to file:',
|
|
19
|
+
placeholder: './dataset.jsonl',
|
|
20
|
+
validate(value) {
|
|
21
|
+
if (!value) return 'Required';
|
|
22
|
+
if (!fs.existsSync(value)) return 'File not found';
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (isCancel(filePath)) return;
|
|
27
|
+
|
|
28
|
+
const purpose = await select({
|
|
29
|
+
message: 'File Purpose:',
|
|
30
|
+
options: [
|
|
31
|
+
{ value: 'fine-tune', label: 'Fine-tuning (JSONL)' },
|
|
32
|
+
{ value: 'vision-tune', label: 'Vision Tuning (Image/Zip)' },
|
|
33
|
+
{ value: 'agent-knowledge', label: 'Agent Knowledge' }
|
|
34
|
+
]
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (isCancel(purpose)) return;
|
|
38
|
+
|
|
39
|
+
const s = spinner();
|
|
40
|
+
s.start('Uploading file...');
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const result = await client.upload(filePath as string, workspaceId, purpose as string);
|
|
44
|
+
s.stop(green('File uploaded successfully!'));
|
|
45
|
+
console.log(gray(`ID: ${result.id}`));
|
|
46
|
+
console.log(gray(`Name: ${result.filename}`));
|
|
47
|
+
console.log(gray(`Bytes: ${result.bytes}`));
|
|
48
|
+
} catch (e: any) {
|
|
49
|
+
s.stop(red(`Upload failed: ${e.message}`));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function handleDataList(client: FileClient) {
|
|
54
|
+
const config = getConfig();
|
|
55
|
+
let workspaceId = config.workspace_id;
|
|
56
|
+
|
|
57
|
+
if (!workspaceId) {
|
|
58
|
+
// Try without workspace ID (some APIs return user's files)
|
|
59
|
+
// or ask
|
|
60
|
+
workspaceId = await text({ message: 'Enter Workspace ID (optional):', initialValue: '' });
|
|
61
|
+
if (isCancel(workspaceId)) return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const s = spinner();
|
|
65
|
+
s.start('Fetching files...');
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const files = await client.list(workspaceId as string);
|
|
69
|
+
s.stop(`Found ${files.length} files`);
|
|
70
|
+
|
|
71
|
+
if (files.length === 0) {
|
|
72
|
+
console.log(yellow('No files found.'));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Just list them for now, maybe select to delete later?
|
|
77
|
+
files.forEach(f => {
|
|
78
|
+
console.log(`${f.id.padEnd(30)} ${f.filename.padEnd(20)} ${f.purpose} (${f.bytes}b)`);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
} catch (e: any) {
|
|
82
|
+
s.stop(red(`Failed to list files: ${e.message}`));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { intro, outro, spinner, green, red, yellow, showSuccess } from '../ui';
|
|
2
|
+
import { getConfig } from '../config';
|
|
3
|
+
import { AgentClient, AgentCreate } from '../../index';
|
|
4
|
+
|
|
5
|
+
export async function handleDeploy(client: AgentClient) {
|
|
6
|
+
intro('Deploying configuration to Langtrain Cloud...');
|
|
7
|
+
|
|
8
|
+
const config = getConfig();
|
|
9
|
+
const agents = config.agents || [];
|
|
10
|
+
|
|
11
|
+
if (agents.length === 0) {
|
|
12
|
+
intro(yellow('No agents found in langtrain.config.json'));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Iterate and deploy
|
|
17
|
+
for (const agentConfig of agents) {
|
|
18
|
+
const s = spinner();
|
|
19
|
+
s.start(`Deploying agent: ${agentConfig.name}...`);
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
// Check if agent exists (by name? logic needed)
|
|
23
|
+
// Ideally we store ID in config after create, but for now let's just create new or try to find by name.
|
|
24
|
+
// Listing all agents is expensive if many, but safe for now.
|
|
25
|
+
const existingAgents = await client.list();
|
|
26
|
+
const existing = existingAgents.find(a => a.name === agentConfig.name);
|
|
27
|
+
|
|
28
|
+
if (existing) {
|
|
29
|
+
// Update (Note: SDK didn't expose update in my view_file, assuming create or need to add update)
|
|
30
|
+
// If update not available, we skip or warn.
|
|
31
|
+
// Let's assume we can't update yet as per SDK view.
|
|
32
|
+
// So we just skip if exists.
|
|
33
|
+
s.stop(yellow(`Agent ${agentConfig.name} already exists (ID: ${existing.id}). Skipping update (not supported yet).`));
|
|
34
|
+
} else {
|
|
35
|
+
// Create
|
|
36
|
+
const payload: AgentCreate = {
|
|
37
|
+
workspace_id: config.workspace_id || (existingAgents[0]?.workspace_id) || '', // Need a way to get workspace!
|
|
38
|
+
name: agentConfig.name,
|
|
39
|
+
description: agentConfig.description,
|
|
40
|
+
config: agentConfig.config
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Fallback for workspace_id
|
|
44
|
+
if (!payload.workspace_id) {
|
|
45
|
+
// Try to get from first agent or error
|
|
46
|
+
// Realistically, user needs to set workspace_id in config or we infer from API key scope.
|
|
47
|
+
// Let's warn.
|
|
48
|
+
s.stop(red(`Failed: Workspace ID missing in config for ${agentConfig.name}`));
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
await client.create(payload);
|
|
53
|
+
s.stop(green(`Agent ${agentConfig.name} deployed successfully!`));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
} catch (e: any) {
|
|
57
|
+
s.stop(red(`Failed to deploy ${agentConfig.name}: ${e.message}`));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
showSuccess('Deployment complete.');
|
|
62
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { intro, outro, spinner, green, red, yellow, showInfo, gray } from '../ui';
|
|
2
|
+
import { AgentClient } from '../../index';
|
|
3
|
+
import { handleDeploy } from './deploy';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
|
|
7
|
+
export async function handleDev(client: AgentClient) {
|
|
8
|
+
intro('Starting Langtrain Development Server...');
|
|
9
|
+
|
|
10
|
+
const configPath = path.join(process.cwd(), 'langtrain.config.json');
|
|
11
|
+
if (!fs.existsSync(configPath)) {
|
|
12
|
+
intro(red('langtrain.config.json not found. Run "lt init" first.'));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
console.log(gray(`Watching ${configPath} for changes...`));
|
|
17
|
+
|
|
18
|
+
let isDeploying = false;
|
|
19
|
+
|
|
20
|
+
// Initial Deploy
|
|
21
|
+
await handleDeploy(client);
|
|
22
|
+
|
|
23
|
+
fs.watch(configPath, async (eventType) => {
|
|
24
|
+
if (eventType === 'change' && !isDeploying) {
|
|
25
|
+
isDeploying = true;
|
|
26
|
+
console.log(yellow('Configuration changed. Redeploying...'));
|
|
27
|
+
// Wait a bit for file write to complete
|
|
28
|
+
await new Promise(r => setTimeout(r, 500));
|
|
29
|
+
try {
|
|
30
|
+
await handleDeploy(client);
|
|
31
|
+
} catch (e: any) {
|
|
32
|
+
console.error(red(`Deploy failed: ${e.message}`));
|
|
33
|
+
} finally {
|
|
34
|
+
isDeploying = false;
|
|
35
|
+
console.log(gray(`Watching ${configPath}...`));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Keep process alive
|
|
41
|
+
await new Promise(() => { });
|
|
42
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { intro, outro, showSuccess, showError, showWarning, showInfo, spinner, colors } from '../ui';
|
|
2
|
+
import { getConfig } from '../config';
|
|
3
|
+
import { getSubscription } from '../auth';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
|
|
6
|
+
export async function handleDoctor() {
|
|
7
|
+
intro('Running Langtrain Doctor...');
|
|
8
|
+
|
|
9
|
+
const s = spinner();
|
|
10
|
+
let issues = 0;
|
|
11
|
+
|
|
12
|
+
// 1. Check Node Environment
|
|
13
|
+
s.start('Checking Node.js environment...');
|
|
14
|
+
const nodeVersion = process.version;
|
|
15
|
+
const platform = os.platform();
|
|
16
|
+
const arch = os.arch();
|
|
17
|
+
|
|
18
|
+
if (parseInt(nodeVersion.replace('v', '').split('.')[0]) < 18) {
|
|
19
|
+
s.stop(colors.red(`Node.js version ${nodeVersion} is outdated. Please upgrade to v18+.`));
|
|
20
|
+
issues++;
|
|
21
|
+
} else {
|
|
22
|
+
s.stop(`Node.js ${nodeVersion} (${platform} ${arch})`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 2. Check Configuration
|
|
26
|
+
s.start('Checking configuration...');
|
|
27
|
+
const config = getConfig();
|
|
28
|
+
if (!config.apiKey) {
|
|
29
|
+
s.stop(colors.yellow('API Key is missing. Run `langtrain login` or set LANGTRAIN_API_KEY.'));
|
|
30
|
+
issues++;
|
|
31
|
+
} else {
|
|
32
|
+
s.stop('Configuration found.');
|
|
33
|
+
|
|
34
|
+
// 3. Check API Connectivity
|
|
35
|
+
s.start('Checking API connectivity...');
|
|
36
|
+
try {
|
|
37
|
+
const plan = await getSubscription(config.apiKey);
|
|
38
|
+
s.stop(`Connected to Langtrain Cloud (Plan: ${colors.green(plan?.plan || 'unknown')})`);
|
|
39
|
+
} catch (e: any) {
|
|
40
|
+
s.stop(colors.red(`Failed to connect to Langtrain Cloud: ${e.message}`));
|
|
41
|
+
issues++;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.log(''); // Spacer
|
|
46
|
+
|
|
47
|
+
if (issues === 0) {
|
|
48
|
+
showSuccess('Your Langtrain environment is healthy! Ready to build.');
|
|
49
|
+
} else {
|
|
50
|
+
showWarning(`Found ${issues} issue(s). Please resolve them for the best experience.`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
outro('Doctor check complete.');
|
|
54
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { text, confirm, select, isCancel, cancel, intro, outro, showSuccess, showInfo, spinner, colors } from '../ui';
|
|
4
|
+
import { getConfig } from '../config';
|
|
5
|
+
import { handleLogin } from '../auth';
|
|
6
|
+
|
|
7
|
+
export async function handleInit() {
|
|
8
|
+
intro('Initializing new Langtrain project...');
|
|
9
|
+
|
|
10
|
+
const cwd = process.cwd();
|
|
11
|
+
|
|
12
|
+
// 1. Check if already initialized
|
|
13
|
+
if (fs.existsSync(path.join(cwd, 'langtrain.config.json'))) {
|
|
14
|
+
showInfo('langtrain.config.json already exists in this directory.');
|
|
15
|
+
const overwrite = await confirm({
|
|
16
|
+
message: 'Do you want to re-initialize and overwrite the config?',
|
|
17
|
+
initialValue: false
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (isCancel(overwrite) || !overwrite) {
|
|
21
|
+
outro('Initialization cancelled.');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 2. Ask for Project Details
|
|
27
|
+
const projectName = await text({
|
|
28
|
+
message: 'What is the name of your project?',
|
|
29
|
+
placeholder: 'my-ai-app',
|
|
30
|
+
initialValue: path.basename(cwd),
|
|
31
|
+
validate(value) {
|
|
32
|
+
if (!value || value.length === 0) return 'Project name is required!';
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (isCancel(projectName)) {
|
|
37
|
+
cancel('Operation cancelled.');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let config = getConfig();
|
|
42
|
+
let apiKey = config.apiKey;
|
|
43
|
+
|
|
44
|
+
if (apiKey) {
|
|
45
|
+
showSuccess('Found existing Langtrain credentials.');
|
|
46
|
+
} else {
|
|
47
|
+
const shouldLogin = await confirm({
|
|
48
|
+
message: 'You are not logged in. Do you want to log in now?',
|
|
49
|
+
initialValue: true
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (isCancel(shouldLogin)) {
|
|
53
|
+
cancel('Operation cancelled.');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (shouldLogin) {
|
|
58
|
+
await handleLogin();
|
|
59
|
+
config = getConfig(); // Reload config
|
|
60
|
+
apiKey = config.apiKey;
|
|
61
|
+
} else {
|
|
62
|
+
apiKey = await text({
|
|
63
|
+
message: 'Enter your Langtrain API Key (optional for local dev):',
|
|
64
|
+
placeholder: 'lt_sk_...',
|
|
65
|
+
initialValue: ''
|
|
66
|
+
}) as string;
|
|
67
|
+
|
|
68
|
+
if (isCancel(apiKey)) {
|
|
69
|
+
cancel('Operation cancelled.');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 3. Create Config File
|
|
76
|
+
const s = spinner();
|
|
77
|
+
s.start('Creating configuration...');
|
|
78
|
+
|
|
79
|
+
const configContent = {
|
|
80
|
+
name: projectName,
|
|
81
|
+
apiKey: apiKey || undefined,
|
|
82
|
+
environment: 'development',
|
|
83
|
+
agents: [
|
|
84
|
+
{
|
|
85
|
+
name: 'support-bot',
|
|
86
|
+
description: 'A helpful customer support assistant',
|
|
87
|
+
config: {
|
|
88
|
+
model: 'llama-3-8b',
|
|
89
|
+
system_prompt: 'You are a helpful customer support assistant.',
|
|
90
|
+
temperature: 0.7
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
fs.writeFileSync(path.join(cwd, 'langtrain.config.json'), JSON.stringify(configContent, null, 2));
|
|
97
|
+
|
|
98
|
+
showSuccess('Project initialized successfully!');
|
|
99
|
+
console.log(colors.dim('\nNext steps:'));
|
|
100
|
+
console.log(` 1. Run ${colors.cyan('lt deploy')} to push your agent to the cloud.`);
|
|
101
|
+
console.log(` 2. Run ${colors.cyan('lt dev')} to start the local development loop.`);
|
|
102
|
+
|
|
103
|
+
outro('Happy coding!');
|
|
104
|
+
}
|
package/src/cli/handlers/tune.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { text, select, confirm, isCancel, cancel, spinner, intro, red, green, yellow, bgCyan, black, gradient } from '../ui';
|
|
1
|
+
import { text, select, confirm, isCancel, cancel, spinner, intro, red, green, yellow, bgCyan, black, gradient, gray } from '../ui';
|
|
2
2
|
import { getConfig } from '../config';
|
|
3
3
|
import { Langtune, ModelClient, SubscriptionClient, FileClient, TrainingClient } from '../../index';
|
|
4
4
|
|
|
@@ -153,3 +153,105 @@ export async function handleTuneGenerate(tune: Langtune) {
|
|
|
153
153
|
throw e;
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
|
+
|
|
157
|
+
export async function handleTuneList(trainingClient: TrainingClient) {
|
|
158
|
+
const s = spinner();
|
|
159
|
+
s.start('Fetching fine-tuning jobs...');
|
|
160
|
+
|
|
161
|
+
// We need workspace ID, usually from config or first agent?
|
|
162
|
+
// For now, let's just ask or list from all available if API supports it (it requires workspace_id)
|
|
163
|
+
// Let's assume user knows it or we can find it.
|
|
164
|
+
// Simplified: Just ask for Workspace ID if not in config (we don't save it yet)
|
|
165
|
+
// BETTER: Get it from an existing agent or config.
|
|
166
|
+
const config = getConfig();
|
|
167
|
+
let workspaceId = config.workspace_id;
|
|
168
|
+
|
|
169
|
+
if (!workspaceId) {
|
|
170
|
+
s.stop(yellow('Workspace ID required to list jobs.'));
|
|
171
|
+
workspaceId = await text({ message: 'Enter Workspace ID:' });
|
|
172
|
+
if (isCancel(workspaceId)) return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const jobs = await trainingClient.listJobs(workspaceId as string);
|
|
177
|
+
s.stop(`Found ${jobs.data.length} jobs`);
|
|
178
|
+
|
|
179
|
+
if (jobs.data.length === 0) {
|
|
180
|
+
console.log(yellow('No jobs found.'));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const selectedJob = await select({
|
|
185
|
+
message: 'Select a job to view details:',
|
|
186
|
+
options: jobs.data.map(j => ({
|
|
187
|
+
value: j.id,
|
|
188
|
+
label: `${j.name || j.id} (${j.status})`,
|
|
189
|
+
hint: `Created: ${new Date(j.created_at).toLocaleDateString()}`
|
|
190
|
+
}))
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (isCancel(selectedJob)) return;
|
|
194
|
+
|
|
195
|
+
await handleTuneStatus(trainingClient, selectedJob as string);
|
|
196
|
+
|
|
197
|
+
} catch (e: any) {
|
|
198
|
+
s.stop(red(`Failed to list jobs: ${e.message}`));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function handleTuneStatus(trainingClient: TrainingClient, jobId?: string) {
|
|
203
|
+
let id = jobId;
|
|
204
|
+
if (!id) {
|
|
205
|
+
id = await text({ message: 'Enter Job ID:' }) as string;
|
|
206
|
+
if (isCancel(id)) return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const s = spinner();
|
|
210
|
+
s.start(`Fetching status for ${id}...`);
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const job = await trainingClient.getJob(id);
|
|
214
|
+
s.stop(`Job Status: ${job.status.toUpperCase()}`);
|
|
215
|
+
|
|
216
|
+
console.log(gray('------------------------------------------------'));
|
|
217
|
+
console.log(`${bgCyan(black(' Job Details '))}`);
|
|
218
|
+
console.log(`ID: ${job.id}`);
|
|
219
|
+
console.log(`Name: ${job.name}`);
|
|
220
|
+
console.log(`Status: ${job.status === 'succeeded' ? green(job.status) : job.status}`);
|
|
221
|
+
console.log(`Model: ${job.base_model}`);
|
|
222
|
+
console.log(`Progress: ${job.progress || 0}%`);
|
|
223
|
+
if (job.error_message) console.log(red(`Error: ${job.error_message}`));
|
|
224
|
+
console.log(gray('------------------------------------------------'));
|
|
225
|
+
|
|
226
|
+
if (job.status === 'running' || job.status === 'queued') {
|
|
227
|
+
const action = await select({
|
|
228
|
+
message: 'Action:',
|
|
229
|
+
options: [
|
|
230
|
+
{ value: 'refresh', label: 'Refresh Status' },
|
|
231
|
+
{ value: 'cancel', label: 'Cancel Job' },
|
|
232
|
+
{ value: 'back', label: 'Back' }
|
|
233
|
+
]
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
if (action === 'refresh') await handleTuneStatus(trainingClient, id);
|
|
237
|
+
if (action === 'cancel') await handleTuneCancel(trainingClient, id);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
} catch (e: any) {
|
|
241
|
+
s.stop(red(`Failed to get job status: ${e.message}`));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export async function handleTuneCancel(trainingClient: TrainingClient, jobId: string) {
|
|
246
|
+
const confirmCancel = await confirm({ message: 'Are you sure you want to cancel this job?' });
|
|
247
|
+
if (!confirmCancel || isCancel(confirmCancel)) return;
|
|
248
|
+
|
|
249
|
+
const s = spinner();
|
|
250
|
+
s.start('Canceling job...');
|
|
251
|
+
try {
|
|
252
|
+
await trainingClient.cancelJob(jobId);
|
|
253
|
+
s.stop(green('Job canceled successfully.'));
|
|
254
|
+
} catch (e: any) {
|
|
255
|
+
s.stop(red(`Failed to cancel job: ${e.message}`));
|
|
256
|
+
}
|
|
257
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import {
|
|
4
|
+
import { select, isCancel, outro, intro, colors } from './ui'; // Ensure clear is exported if added, otherwise use console.clear()
|
|
5
5
|
import { showBanner } from './ui';
|
|
6
6
|
import { ensureAuth, handleLogin, getSubscription } from './auth';
|
|
7
7
|
import { getMenu, MenuState } from './menu';
|
|
@@ -9,12 +9,17 @@ import { getConfig } from './config';
|
|
|
9
9
|
|
|
10
10
|
// Handlers
|
|
11
11
|
import { handleSubscriptionStatus } from './handlers/subscription';
|
|
12
|
-
import { handleTuneFinetune, handleTuneGenerate } from './handlers/tune';
|
|
12
|
+
import { handleTuneFinetune, handleTuneGenerate, handleTuneList } from './handlers/tune';
|
|
13
13
|
import { handleVisionFinetune, handleVisionGenerate } from './handlers/vision';
|
|
14
14
|
import { handleAgentCreate, handleAgentDelete, handleAgentList } from './handlers/agent';
|
|
15
|
+
import { handleInit } from './handlers/init';
|
|
16
|
+
import { handleDoctor } from './handlers/doctor';
|
|
17
|
+
import { handleDataUpload } from './handlers/data';
|
|
18
|
+
import { handleDeploy } from './handlers/deploy';
|
|
19
|
+
import { handleDev } from './handlers/dev';
|
|
15
20
|
|
|
16
21
|
// Clients
|
|
17
|
-
import { SubscriptionInfo, Langvision, Langtune, AgentClient, ModelClient } from '../index';
|
|
22
|
+
import { SubscriptionInfo, Langvision, Langtune, AgentClient, ModelClient, FileClient, TrainingClient } from '../index';
|
|
18
23
|
import packageJson from '../../package.json';
|
|
19
24
|
|
|
20
25
|
export async function main() {
|
|
@@ -26,6 +31,29 @@ export async function main() {
|
|
|
26
31
|
.description(packageJson.description || 'Langtrain CLI for AI Model Fine-tuning and Generation')
|
|
27
32
|
.version(version);
|
|
28
33
|
|
|
34
|
+
// Register standalone commands
|
|
35
|
+
program.command('init')
|
|
36
|
+
.description('Initialize a new Langtrain project')
|
|
37
|
+
.action(handleInit);
|
|
38
|
+
|
|
39
|
+
program.command('deploy')
|
|
40
|
+
.description('Deploy configuration to Langtrain Cloud')
|
|
41
|
+
.action(async () => {
|
|
42
|
+
const config = getConfig();
|
|
43
|
+
const apiKey = config.apiKey || '';
|
|
44
|
+
const client = new AgentClient({ apiKey, baseUrl: config.baseUrl });
|
|
45
|
+
await handleDeploy(client);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
program.command('dev')
|
|
49
|
+
.description('Start local development server (Watch Mode)')
|
|
50
|
+
.action(async () => {
|
|
51
|
+
const config = getConfig();
|
|
52
|
+
const apiKey = config.apiKey || '';
|
|
53
|
+
const client = new AgentClient({ apiKey, baseUrl: config.baseUrl });
|
|
54
|
+
await handleDev(client);
|
|
55
|
+
});
|
|
56
|
+
|
|
29
57
|
program.action(async () => {
|
|
30
58
|
showBanner(version);
|
|
31
59
|
|
|
@@ -60,7 +88,8 @@ export async function main() {
|
|
|
60
88
|
vision: new Langvision({ apiKey }),
|
|
61
89
|
tune: new Langtune({ apiKey }),
|
|
62
90
|
agent: new AgentClient({ apiKey, baseUrl: config.baseUrl }),
|
|
63
|
-
model: new ModelClient({ apiKey, baseUrl: config.baseUrl })
|
|
91
|
+
model: new ModelClient({ apiKey, baseUrl: config.baseUrl }),
|
|
92
|
+
train: new TrainingClient({ apiKey, baseUrl: config.baseUrl })
|
|
64
93
|
};
|
|
65
94
|
|
|
66
95
|
// 3. Navigation Loop
|
|
@@ -114,25 +143,32 @@ export async function main() {
|
|
|
114
143
|
vision: new Langvision({ apiKey }),
|
|
115
144
|
tune: new Langtune({ apiKey }),
|
|
116
145
|
agent: new AgentClient({ apiKey, baseUrl: config.baseUrl }),
|
|
117
|
-
model: new ModelClient({ apiKey, baseUrl: config.baseUrl })
|
|
146
|
+
model: new ModelClient({ apiKey, baseUrl: config.baseUrl }),
|
|
147
|
+
train: new TrainingClient({ apiKey, baseUrl: config.baseUrl })
|
|
118
148
|
};
|
|
119
149
|
try { plan = await getSubscription(apiKey); } catch { }
|
|
120
150
|
break;
|
|
121
151
|
case 'status': await handleSubscriptionStatus(); break;
|
|
152
|
+
case 'init': await handleInit(); break;
|
|
153
|
+
case 'deploy': await handleDeploy(clients.agent); break;
|
|
154
|
+
case 'dev': await handleDev(clients.agent); break;
|
|
155
|
+
case 'doctor': await handleDoctor(); break;
|
|
122
156
|
case 'tune-finetune': await handleTuneFinetune(clients.tune, clients.model); break;
|
|
157
|
+
case 'tune-list': await handleTuneList(clients.train); break;
|
|
123
158
|
case 'tune-generate': await handleTuneGenerate(clients.tune); break;
|
|
124
159
|
case 'vision-finetune': await handleVisionFinetune(clients.vision, clients.model); break;
|
|
125
160
|
case 'vision-generate': await handleVisionGenerate(clients.vision); break;
|
|
126
161
|
case 'agent-list': await handleAgentList(clients.agent); break;
|
|
127
162
|
case 'agent-create': await handleAgentCreate(clients.agent, clients.model); break;
|
|
128
163
|
case 'agent-delete': await handleAgentDelete(clients.agent); break;
|
|
164
|
+
case 'data-upload': await handleDataUpload(new FileClient({ apiKey })); break;
|
|
129
165
|
}
|
|
130
166
|
|
|
131
167
|
// After action, where do we go?
|
|
132
168
|
// Stay in current state (sub-menu) is usually preferred.
|
|
133
169
|
|
|
134
170
|
} catch (error: any) {
|
|
135
|
-
outro(red(`Error: ${error.message}`));
|
|
171
|
+
outro(colors.red(`Error: ${error.message}`));
|
|
136
172
|
}
|
|
137
173
|
}
|
|
138
174
|
});
|
package/src/cli/menu.ts
CHANGED
|
@@ -21,6 +21,10 @@ export function getMenu(state: MenuState, plan: SubscriptionInfo | null, isAuthe
|
|
|
21
21
|
{ value: 'nav-agents', label: 'Agents', hint: 'Manage & Chat with AI Agents' },
|
|
22
22
|
{ value: 'nav-text', label: 'Langtune (Text)', hint: 'Fine-tuning & Generation' },
|
|
23
23
|
{ value: 'nav-vision', label: 'Langvision (Vision)', hint: 'Vision Analysis & Tuning' },
|
|
24
|
+
{ value: 'init', label: 'Initialize Project', hint: 'Scaffold new Langtrain app' },
|
|
25
|
+
{ value: 'deploy', label: 'Deploy', hint: 'Push config to Cloud' },
|
|
26
|
+
{ value: 'dev', label: 'Start Dev Server', hint: 'Watch mode' },
|
|
27
|
+
{ value: 'doctor', label: 'Doctor', hint: 'Check environment health' },
|
|
24
28
|
{ value: 'nav-settings', label: 'Settings', hint: 'Subscription & Auth' }
|
|
25
29
|
];
|
|
26
30
|
|
|
@@ -37,7 +41,7 @@ export function getMenu(state: MenuState, plan: SubscriptionInfo | null, isAuthe
|
|
|
37
41
|
|
|
38
42
|
case 'agents':
|
|
39
43
|
return [
|
|
40
|
-
{ value: 'agent-list', label: 'List & Run Agents', hint: '
|
|
44
|
+
{ value: 'agent-list', label: 'List & Run Agents', hint: 'Chat with active agents' },
|
|
41
45
|
{ value: 'agent-create', label: 'Create New Agent', hint: 'Deploy a new agent' },
|
|
42
46
|
{ value: 'agent-delete', label: 'Delete Agent', hint: 'Remove an agent' },
|
|
43
47
|
{ value: 'back', label: '← Back to Main Menu' }
|
|
@@ -46,7 +50,9 @@ export function getMenu(state: MenuState, plan: SubscriptionInfo | null, isAuthe
|
|
|
46
50
|
case 'text':
|
|
47
51
|
return [
|
|
48
52
|
{ value: 'tune-finetune', label: 'Fine-tune Text Model', hint: 'Create custom LLM' },
|
|
53
|
+
{ value: 'tune-list', label: 'List Jobs', hint: 'Check training status' },
|
|
49
54
|
{ value: 'tune-generate', label: 'Generate Text', hint: 'Test your models' },
|
|
55
|
+
{ value: 'data-upload', label: 'Upload Dataset', hint: 'Upload JSONL for training' },
|
|
50
56
|
{ value: 'back', label: '← Back to Main Menu' }
|
|
51
57
|
];
|
|
52
58
|
|
package/src/cli/ui.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { text, select, confirm, password, isCancel, cancel } from '@clack/prompts';
|
|
2
|
-
import { bgCyan, black, red, green, yellow, gray, cyan, bold } from 'kleur/colors';
|
|
3
|
-
|
|
4
|
-
// Gradient removed for cleaner look, or keep if user likes it but wants no emojis?
|
|
5
|
-
// User said "remove emojis", didn't explicitly say "remove colors/gradients", but "clean UI" usually implies less noise.
|
|
6
|
-
// I will keep the banner gradient as it is a brand element, but remove emojis from intro/outro.
|
|
1
|
+
import { text, select, confirm, password, isCancel, cancel, note } from '@clack/prompts';
|
|
2
|
+
import { bgCyan, black, red, green, yellow, gray, cyan, bold, dim, blue } from 'kleur/colors';
|
|
7
3
|
import gradient from 'gradient-string';
|
|
8
4
|
|
|
5
|
+
// Re-export specific prompts to keep imports clean in other files
|
|
6
|
+
export { text, select, confirm, password, isCancel, cancel, note };
|
|
7
|
+
|
|
9
8
|
export function showBanner(version: string) {
|
|
10
9
|
console.clear();
|
|
11
10
|
const banner = `
|
|
@@ -47,4 +46,22 @@ export function showSuccess(message: string) {
|
|
|
47
46
|
console.log(green(`✔ ${message}`));
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
export
|
|
49
|
+
export function showWarning(message: string) {
|
|
50
|
+
console.log(yellow(`⚠ Warning: ${message}`));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function showInfo(message: string) {
|
|
54
|
+
console.log(blue(`ℹ ${message}`));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function showDim(message: string) {
|
|
58
|
+
console.log(dim(message));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Re-export for backward compatibility
|
|
62
|
+
export { bgCyan, black, red, green, yellow, gray, cyan, bold, dim, blue, gradient };
|
|
63
|
+
|
|
64
|
+
export const colors = {
|
|
65
|
+
bgCyan, black, red, green, yellow, gray, cyan, bold, dim, blue
|
|
66
|
+
};
|
|
67
|
+
|