@myvillage/cli 1.5.0 → 1.5.1
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/package.json +8 -7
- package/src/agent-runtime/loop.js +9 -9
- package/src/agent-runtime/mcp-client.js +31 -338
- package/src/commands/agent-local.js +24 -7
- package/src/commands/login.js +159 -72
- package/src/commands/logout.js +43 -6
- package/src/commands/soulprint.js +1379 -0
- package/src/index.js +113 -0
- package/src/utils/agent-scaffolder.js +6 -6
- package/src/utils/api.js +1 -1
- package/src/utils/config.js +1 -0
- package/src/utils/soulprint-api.js +136 -0
- package/src/utils/soulprint-workspace.js +158 -0
package/src/index.js
CHANGED
|
@@ -53,6 +53,18 @@ import {
|
|
|
53
53
|
bizreqsStatusCommand,
|
|
54
54
|
bizreqsImportCommand,
|
|
55
55
|
} from './commands/bizreqs.js';
|
|
56
|
+
import {
|
|
57
|
+
soulprintInitCommand,
|
|
58
|
+
soulprintIngestCommand,
|
|
59
|
+
soulprintDatasetListCommand,
|
|
60
|
+
soulprintDatasetPullCommand,
|
|
61
|
+
soulprintTrainCommand,
|
|
62
|
+
soulprintJobsCommand,
|
|
63
|
+
soulprintJobDetailCommand,
|
|
64
|
+
soulprintPushCommand,
|
|
65
|
+
soulprintPublishCommand,
|
|
66
|
+
soulprintModelsCommand,
|
|
67
|
+
} from './commands/soulprint.js';
|
|
56
68
|
|
|
57
69
|
const require = createRequire(import.meta.url);
|
|
58
70
|
const pkg = require('../package.json');
|
|
@@ -73,6 +85,7 @@ export function run() {
|
|
|
73
85
|
program
|
|
74
86
|
.command('login')
|
|
75
87
|
.description('Authenticate with MyVillageOS')
|
|
88
|
+
.option('--no-browser', 'Manual login for headless environments (servers, SSH, etc.)')
|
|
76
89
|
.action(loginCommand);
|
|
77
90
|
|
|
78
91
|
program
|
|
@@ -357,5 +370,105 @@ export function run() {
|
|
|
357
370
|
.option('--contact <name>', 'Contact name')
|
|
358
371
|
.action(bizreqsImportCommand);
|
|
359
372
|
|
|
373
|
+
// ── SoulPrint Studio: Model Training Pipeline ───────────
|
|
374
|
+
|
|
375
|
+
const soulprintCmd = program
|
|
376
|
+
.command('soulprint')
|
|
377
|
+
.description('SoulPrint Studio \u2014 datasets, training, and model publishing');
|
|
378
|
+
|
|
379
|
+
soulprintCmd
|
|
380
|
+
.command('init')
|
|
381
|
+
.description('Initialize local training workspace')
|
|
382
|
+
.option('--skip-python', 'Skip Python venv setup')
|
|
383
|
+
.option('--skip-scripts', 'Skip training script download')
|
|
384
|
+
.action(soulprintInitCommand);
|
|
385
|
+
|
|
386
|
+
soulprintCmd
|
|
387
|
+
.command('ingest <path>')
|
|
388
|
+
.description('Ingest training data into a dataset')
|
|
389
|
+
.requiredOption('--dataset <slug>', 'Target dataset slug')
|
|
390
|
+
.requiredOption('--type <type>', 'Data type: text, image, audio, structured, multimodal')
|
|
391
|
+
.option('--source <source>', 'Ingestion source label', 'CLI')
|
|
392
|
+
.option('--captions <path>', 'Path to captions file (image/multimodal)')
|
|
393
|
+
.option('--transcriptions <path>', 'Path to transcriptions file (audio)')
|
|
394
|
+
.option('--schema <path>', 'Path to schema JSON file (structured)')
|
|
395
|
+
.option('--recursive', 'Recurse into subdirectories')
|
|
396
|
+
.option('--glob <pattern>', 'File glob pattern to filter')
|
|
397
|
+
.option('--split <split>', 'Assign all items to a split: train, validation, test')
|
|
398
|
+
.option('--dry-run', 'Show what would be ingested without uploading')
|
|
399
|
+
.option('--concurrency <n>', 'Parallel upload limit', '5')
|
|
400
|
+
.action(soulprintIngestCommand);
|
|
401
|
+
|
|
402
|
+
const soulprintDatasetCmd = soulprintCmd
|
|
403
|
+
.command('datasets')
|
|
404
|
+
.description('Manage training datasets');
|
|
405
|
+
|
|
406
|
+
soulprintDatasetCmd
|
|
407
|
+
.command('list')
|
|
408
|
+
.description('List available datasets on SoulPrint Studio')
|
|
409
|
+
.option('--type <type>', 'Filter by type: text, image, audio, structured, multimodal')
|
|
410
|
+
.option('--status <status>', 'Filter by status: collecting, ready, training, archived')
|
|
411
|
+
.option('--json', 'Output raw JSON')
|
|
412
|
+
.action(soulprintDatasetListCommand);
|
|
413
|
+
|
|
414
|
+
soulprintDatasetCmd
|
|
415
|
+
.command('pull <slug>')
|
|
416
|
+
.description('Download a dataset to your local machine')
|
|
417
|
+
.option('--version <number>', 'Specific version (default: latest frozen)')
|
|
418
|
+
.option('--split <split>', 'Download only a specific split: train, validation, test')
|
|
419
|
+
.option('--force', 'Re-download even if already present locally')
|
|
420
|
+
.action(soulprintDatasetPullCommand);
|
|
421
|
+
|
|
422
|
+
soulprintCmd
|
|
423
|
+
.command('train')
|
|
424
|
+
.description('Run model training locally')
|
|
425
|
+
.requiredOption('--type <type>', 'Training type: text, image, audio, structured, multimodal')
|
|
426
|
+
.requiredOption('--dataset <slug>', 'Dataset slug to train on')
|
|
427
|
+
.option('--version <number>', 'Dataset version (default: latest)')
|
|
428
|
+
.option('--base <model>', 'Base model identifier')
|
|
429
|
+
.option('--method <method>', 'Training method')
|
|
430
|
+
.option('--config <path>', 'Path to training config YAML file')
|
|
431
|
+
.option('--name <name>', 'Name for the resulting model')
|
|
432
|
+
.option('--dry-run', 'Validate config and dataset without starting training')
|
|
433
|
+
.action(soulprintTrainCommand);
|
|
434
|
+
|
|
435
|
+
soulprintCmd
|
|
436
|
+
.command('jobs [jobId]')
|
|
437
|
+
.description('View training job status')
|
|
438
|
+
.option('--status <status>', 'Filter by status')
|
|
439
|
+
.option('--json', 'Output raw JSON')
|
|
440
|
+
.action((jobId, options) => {
|
|
441
|
+
if (jobId) return soulprintJobDetailCommand(jobId, options);
|
|
442
|
+
return soulprintJobsCommand(options);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
soulprintCmd
|
|
446
|
+
.command('models')
|
|
447
|
+
.description('List models in the registry')
|
|
448
|
+
.option('--status <status>', 'Filter: draft, validated, published, rejected')
|
|
449
|
+
.option('--type <type>', 'Filter: text, image, audio, structured, multimodal')
|
|
450
|
+
.option('--json', 'Output raw JSON')
|
|
451
|
+
.action(soulprintModelsCommand);
|
|
452
|
+
|
|
453
|
+
soulprintCmd
|
|
454
|
+
.command('push <path>')
|
|
455
|
+
.description('Upload locally trained model artifacts to SoulPrint Studio')
|
|
456
|
+
.requiredOption('--name <name>', 'Model name')
|
|
457
|
+
.requiredOption('--type <type>', 'Model type: text, image, audio, structured, multimodal')
|
|
458
|
+
.option('--base <model>', 'Base model it was trained from')
|
|
459
|
+
.option('--method <method>', 'Training method used')
|
|
460
|
+
.option('--job <jobId>', 'Link to an existing training job')
|
|
461
|
+
.option('--description <text>', 'Model description')
|
|
462
|
+
.action(soulprintPushCommand);
|
|
463
|
+
|
|
464
|
+
soulprintCmd
|
|
465
|
+
.command('publish <modelSlug>')
|
|
466
|
+
.description('Publish a validated model to the MyVillage platform')
|
|
467
|
+
.option('--villager <id>', 'Target villager ID to own the model')
|
|
468
|
+
.option('--villages <ids>', 'Comma-separated village IDs to associate')
|
|
469
|
+
.option('--tier <tier>', 'Model tier: FREE, BASIC, PRO, ENTERPRISE', 'FREE')
|
|
470
|
+
.option('--public', 'Make the model publicly available')
|
|
471
|
+
.action(soulprintPublishCommand);
|
|
472
|
+
|
|
360
473
|
program.parse();
|
|
361
474
|
}
|
|
@@ -5,9 +5,9 @@ import { stringify as stringifyYaml } from 'yaml';
|
|
|
5
5
|
// ── MCP Tool Catalog ────────────────────────────────────
|
|
6
6
|
|
|
7
7
|
const TOOL_CATALOG = {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
description: '
|
|
8
|
+
myvillage: {
|
|
9
|
+
url: 'https://mcp.myvillageproject.ai',
|
|
10
|
+
description: 'MyVillageOS platform access (feed, posts, communities, wallet, knowledge)',
|
|
11
11
|
always_enabled: true,
|
|
12
12
|
},
|
|
13
13
|
filesystem: {
|
|
@@ -151,11 +151,11 @@ ${description}
|
|
|
151
151
|
function generateToolsYaml(selectedTools) {
|
|
152
152
|
const servers = {};
|
|
153
153
|
|
|
154
|
-
//
|
|
155
|
-
servers['
|
|
154
|
+
// myvillage is always included
|
|
155
|
+
servers['myvillage'] = TOOL_CATALOG['myvillage'];
|
|
156
156
|
|
|
157
157
|
for (const toolId of selectedTools) {
|
|
158
|
-
if (toolId === '
|
|
158
|
+
if (toolId === 'myvillage') continue;
|
|
159
159
|
if (TOOL_CATALOG[toolId]) {
|
|
160
160
|
servers[toolId] = { ...TOOL_CATALOG[toolId] };
|
|
161
161
|
}
|
package/src/utils/api.js
CHANGED
|
@@ -7,7 +7,7 @@ const require = createRequire(import.meta.url);
|
|
|
7
7
|
const { version } = require('../../package.json');
|
|
8
8
|
const USER_AGENT = `MyVillageOS-CLI/${version}`;
|
|
9
9
|
|
|
10
|
-
function createClient(baseURL) {
|
|
10
|
+
export function createClient(baseURL) {
|
|
11
11
|
const client = axios.create({
|
|
12
12
|
baseURL,
|
|
13
13
|
headers: {
|
package/src/utils/config.js
CHANGED
|
@@ -10,6 +10,7 @@ const DEFAULT_CONFIG = {
|
|
|
10
10
|
networkBaseUrl: 'https://portal.myvillageproject.ai/api/network',
|
|
11
11
|
bizreqsBaseUrl: 'https://portal.myvillageproject.ai/api/bizreqs',
|
|
12
12
|
oauthBaseUrl: 'https://portal.myvillageproject.ai/api/oauth',
|
|
13
|
+
soulprintBaseUrl: 'https://soulprint-studio.myvillageproject.ai/api',
|
|
13
14
|
clientId: 'mvos_aG_c729fuQxvvqYHOnkgTQ',
|
|
14
15
|
callbackPort: 3737,
|
|
15
16
|
anthropicApiKey: null,
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { createClient } from './api.js';
|
|
2
|
+
import { getConfig } from './config.js';
|
|
3
|
+
|
|
4
|
+
// ── Client Factory ─────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
export function getSoulprintClient() {
|
|
7
|
+
const config = getConfig();
|
|
8
|
+
return createClient(config.soulprintBaseUrl);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// ── Datasets ───────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
export async function listDatasets(params = {}) {
|
|
14
|
+
const client = getSoulprintClient();
|
|
15
|
+
const response = await client.get('/datasets', { params });
|
|
16
|
+
return response.data;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function getDataset(slug) {
|
|
20
|
+
const client = getSoulprintClient();
|
|
21
|
+
const response = await client.get(`/datasets/${slug}`);
|
|
22
|
+
return response.data;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function getDatasetDownload(slug, version) {
|
|
26
|
+
const client = getSoulprintClient();
|
|
27
|
+
const params = version ? { version } : {};
|
|
28
|
+
const response = await client.get(`/datasets/${slug}/download`, { params });
|
|
29
|
+
return response.data;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── Training Jobs ──────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
export async function createJob(data) {
|
|
35
|
+
const client = getSoulprintClient();
|
|
36
|
+
const response = await client.post('/jobs', data);
|
|
37
|
+
return response.data;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function listJobs(params = {}) {
|
|
41
|
+
const client = getSoulprintClient();
|
|
42
|
+
const response = await client.get('/jobs', { params });
|
|
43
|
+
return response.data;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function getJob(id) {
|
|
47
|
+
const client = getSoulprintClient();
|
|
48
|
+
const response = await client.get(`/jobs/${id}`);
|
|
49
|
+
return response.data;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function updateJobStatus(id, data) {
|
|
53
|
+
const client = getSoulprintClient();
|
|
54
|
+
const response = await client.patch(`/jobs/${id}/status`, data);
|
|
55
|
+
return response.data;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function completeJob(id, data) {
|
|
59
|
+
const client = getSoulprintClient();
|
|
60
|
+
const response = await client.post(`/jobs/${id}/complete`, data);
|
|
61
|
+
return response.data;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function failJob(id, data) {
|
|
65
|
+
const client = getSoulprintClient();
|
|
66
|
+
const response = await client.post(`/jobs/${id}/fail`, data);
|
|
67
|
+
return response.data;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Models ─────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
export async function listModels(params = {}) {
|
|
73
|
+
const client = getSoulprintClient();
|
|
74
|
+
const response = await client.get('/models', { params });
|
|
75
|
+
return response.data;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function getModel(slug) {
|
|
79
|
+
const client = getSoulprintClient();
|
|
80
|
+
const response = await client.get(`/models/${slug}`);
|
|
81
|
+
return response.data;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function publishModel(slug, data) {
|
|
85
|
+
const client = getSoulprintClient();
|
|
86
|
+
const response = await client.post(`/models/${slug}/publish`, data);
|
|
87
|
+
return response.data;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Ingestion ──────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
export async function ingestText(data) {
|
|
93
|
+
const client = getSoulprintClient();
|
|
94
|
+
const response = await client.post('/ingest/text', data);
|
|
95
|
+
return response.data;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function ingestStructured(data) {
|
|
99
|
+
const client = getSoulprintClient();
|
|
100
|
+
const response = await client.post('/ingest/structured', data);
|
|
101
|
+
return response.data;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function prepareIngestion(data) {
|
|
105
|
+
const client = getSoulprintClient();
|
|
106
|
+
const response = await client.post('/ingest/prepare', data);
|
|
107
|
+
return response.data;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function completeIngestion(ingestionId, data) {
|
|
111
|
+
const client = getSoulprintClient();
|
|
112
|
+
const response = await client.post(`/ingest/${ingestionId}/complete`, data);
|
|
113
|
+
return response.data;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function abortIngestion(ingestionId) {
|
|
117
|
+
const client = getSoulprintClient();
|
|
118
|
+
const response = await client.post(`/ingest/${ingestionId}/abort`);
|
|
119
|
+
return response.data;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── Upload ─────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
export async function getUploadUrls(data) {
|
|
125
|
+
const client = getSoulprintClient();
|
|
126
|
+
const response = await client.post('/upload/presign', data);
|
|
127
|
+
return response.data;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Scripts ────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
export async function getScriptsManifest() {
|
|
133
|
+
const client = getSoulprintClient();
|
|
134
|
+
const response = await client.get('/scripts/manifest');
|
|
135
|
+
return response.data;
|
|
136
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { getConfigDir } from './config.js';
|
|
5
|
+
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
6
|
+
|
|
7
|
+
const SOULPRINT_DIR_NAME = 'soulprint';
|
|
8
|
+
|
|
9
|
+
// ── Directory Helpers ──────────────────────────────────
|
|
10
|
+
|
|
11
|
+
export function getSoulprintDir() {
|
|
12
|
+
return join(getConfigDir(), SOULPRINT_DIR_NAME);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getDatasetsDir() {
|
|
16
|
+
return join(getSoulprintDir(), 'datasets');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getModelsDir() {
|
|
20
|
+
return join(getSoulprintDir(), 'models');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getConfigsDir() {
|
|
24
|
+
return join(getSoulprintDir(), 'configs');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getLogsDir() {
|
|
28
|
+
return join(getSoulprintDir(), 'logs');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getVenvDir() {
|
|
32
|
+
return join(getSoulprintDir(), 'venv');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getScriptsDir() {
|
|
36
|
+
return join(getSoulprintDir(), 'scripts');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Workspace State ────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
export function isWorkspaceInitialized() {
|
|
42
|
+
const dir = getSoulprintDir();
|
|
43
|
+
return existsSync(dir) && existsSync(join(dir, 'workspace.yaml'));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function ensureWorkspace() {
|
|
47
|
+
return isWorkspaceInitialized();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function readWorkspaceConfig() {
|
|
51
|
+
const configPath = join(getSoulprintDir(), 'workspace.yaml');
|
|
52
|
+
if (!existsSync(configPath)) return null;
|
|
53
|
+
try {
|
|
54
|
+
return parseYaml(readFileSync(configPath, 'utf-8'));
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function writeWorkspaceConfig(config) {
|
|
61
|
+
const configPath = join(getSoulprintDir(), 'workspace.yaml');
|
|
62
|
+
writeFileSync(configPath, stringifyYaml(config, { lineWidth: 0 }));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Dataset Helpers ────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
export function getLocalDatasetDir(slug, version) {
|
|
68
|
+
return join(getDatasetsDir(), slug, `v${version}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function isDatasetDownloaded(slug, version) {
|
|
72
|
+
const dir = getLocalDatasetDir(slug, version);
|
|
73
|
+
return existsSync(dir) && existsSync(join(dir, 'manifest.json'));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function listLocalDatasets() {
|
|
77
|
+
const dir = getDatasetsDir();
|
|
78
|
+
if (!existsSync(dir)) return [];
|
|
79
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
80
|
+
.filter(e => e.isDirectory())
|
|
81
|
+
.map(e => e.name);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Python Environment ─────────────────────────────────
|
|
85
|
+
|
|
86
|
+
export function getPythonPath() {
|
|
87
|
+
const venvPython = join(getVenvDir(), 'bin', 'python');
|
|
88
|
+
if (existsSync(venvPython)) return venvPython;
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function isPythonAvailable() {
|
|
93
|
+
try {
|
|
94
|
+
execSync('python3 --version', { stdio: 'pipe' });
|
|
95
|
+
return true;
|
|
96
|
+
} catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function getPythonVersion() {
|
|
102
|
+
try {
|
|
103
|
+
return execSync('python3 --version', { stdio: 'pipe' }).toString().trim();
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function detectGPU() {
|
|
110
|
+
try {
|
|
111
|
+
const nvidiaSmi = execSync('nvidia-smi --query-gpu=name,memory.total --format=csv,noheader', { stdio: 'pipe' }).toString().trim();
|
|
112
|
+
if (nvidiaSmi) {
|
|
113
|
+
const [name, vram] = nvidiaSmi.split(', ');
|
|
114
|
+
return { type: 'cuda', name, vram };
|
|
115
|
+
}
|
|
116
|
+
} catch { /* no CUDA */ }
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const sysctl = execSync('sysctl -n machdep.cpu.brand_string', { stdio: 'pipe' }).toString().trim();
|
|
120
|
+
if (sysctl.includes('Apple')) {
|
|
121
|
+
const mem = execSync('sysctl -n hw.memsize', { stdio: 'pipe' }).toString().trim();
|
|
122
|
+
const memGB = Math.round(parseInt(mem) / 1073741824);
|
|
123
|
+
return { type: 'mps', name: sysctl, vram: `${memGB}GB unified` };
|
|
124
|
+
}
|
|
125
|
+
} catch { /* not Apple Silicon */ }
|
|
126
|
+
|
|
127
|
+
return { type: 'cpu', name: 'CPU only', vram: null };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function getMachineInfo() {
|
|
131
|
+
const gpu = detectGPU();
|
|
132
|
+
return {
|
|
133
|
+
gpu: gpu.name,
|
|
134
|
+
gpuType: gpu.type,
|
|
135
|
+
vram: gpu.vram,
|
|
136
|
+
os: `${process.platform} ${process.arch}`,
|
|
137
|
+
python: getPythonVersion(),
|
|
138
|
+
nodeVersion: process.version,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── Training Job Local State ───────────────────────────
|
|
143
|
+
|
|
144
|
+
export function getJobOutputDir(jobId) {
|
|
145
|
+
return join(getModelsDir(), jobId);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function getJobLogFile(jobId) {
|
|
149
|
+
const dir = getLogsDir();
|
|
150
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
151
|
+
return join(dir, `${jobId}.jsonl`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function appendJobLog(jobId, entry) {
|
|
155
|
+
const logFile = getJobLogFile(jobId);
|
|
156
|
+
const line = JSON.stringify({ ...entry, ts: new Date().toISOString() }) + '\n';
|
|
157
|
+
writeFileSync(logFile, line, { flag: 'a' });
|
|
158
|
+
}
|