@myvillage/cli 1.5.0 → 1.6.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,68 @@
1
+ import chalk from 'chalk';
2
+ import inquirer from 'inquirer';
3
+ import { isAuthenticated } from '../utils/auth.js';
4
+ import { brand } from '../utils/brand.js';
5
+ import { createPost } from '../utils/api.js';
6
+ import { resolveVillageContext } from '../utils/village-resolver.js';
7
+
8
+ export async function storyCommand(options) {
9
+ if (!isAuthenticated()) {
10
+ console.log(chalk.red(" \u2717 Authentication required. Run 'myvillage login' first."));
11
+ return;
12
+ }
13
+
14
+ const ctx = await resolveVillageContext();
15
+ if (!ctx) return;
16
+
17
+ // Guided prompts
18
+ const answers = await inquirer.prompt([
19
+ {
20
+ type: 'editor',
21
+ name: 'story',
22
+ message: 'What story, memory, or piece of wisdom would you like to share?',
23
+ validate: (v) => v.trim().length > 0 || 'Please share something.',
24
+ },
25
+ {
26
+ type: 'input',
27
+ name: 'source',
28
+ message: 'Who is this from? (you, a family elder, a community member)',
29
+ default: 'me',
30
+ },
31
+ {
32
+ type: 'input',
33
+ name: 'connection',
34
+ message: 'Is this connected to something the village is building? (optional)',
35
+ },
36
+ ]);
37
+
38
+ // Build tags
39
+ const tags = ['knowledge-contribution'];
40
+ const sourceName = answers.source.trim();
41
+ if (sourceName && sourceName.toLowerCase() !== 'me') {
42
+ tags.push(`source:${sourceName}`);
43
+ }
44
+ if (answers.connection?.trim()) {
45
+ tags.push(`theme:${answers.connection.trim()}`);
46
+ }
47
+
48
+ // Format body with attribution
49
+ let body = answers.story.trim();
50
+ if (sourceName && sourceName.toLowerCase() !== 'me') {
51
+ body = `"${body}"\n\n\u2014 ${sourceName}`;
52
+ }
53
+
54
+ try {
55
+ await createPost({
56
+ communitySlug: ctx.community.slug,
57
+ postType: 'DISCUSSION',
58
+ title: 'Knowledge Contribution',
59
+ body,
60
+ tags,
61
+ });
62
+
63
+ console.log(brand.green(` \u2713 Story shared with ${ctx.village.name}`));
64
+ } catch (err) {
65
+ const msg = err.response?.data?.error || err.message;
66
+ console.log(chalk.red(` \u2717 Failed to share story: ${msg}`));
67
+ }
68
+ }
package/src/index.js CHANGED
@@ -53,6 +53,22 @@ import {
53
53
  bizreqsStatusCommand,
54
54
  bizreqsImportCommand,
55
55
  } from './commands/bizreqs.js';
56
+ import { checkinCommand } from './commands/checkin.js';
57
+ import { discoverCommand } from './commands/discover.js';
58
+ import { logCommand } from './commands/log.js';
59
+ import { storyCommand } from './commands/story.js';
60
+ import {
61
+ soulprintInitCommand,
62
+ soulprintIngestCommand,
63
+ soulprintDatasetListCommand,
64
+ soulprintDatasetPullCommand,
65
+ soulprintTrainCommand,
66
+ soulprintJobsCommand,
67
+ soulprintJobDetailCommand,
68
+ soulprintPushCommand,
69
+ soulprintPublishCommand,
70
+ soulprintModelsCommand,
71
+ } from './commands/soulprint.js';
56
72
 
57
73
  const require = createRequire(import.meta.url);
58
74
  const pkg = require('../package.json');
@@ -73,6 +89,7 @@ export function run() {
73
89
  program
74
90
  .command('login')
75
91
  .description('Authenticate with MyVillageOS')
92
+ .option('--no-browser', 'Manual login for headless environments (servers, SSH, etc.)')
76
93
  .action(loginCommand);
77
94
 
78
95
  program
@@ -308,6 +325,30 @@ export function run() {
308
325
  .description('Remove an MCP server tool from a local agent')
309
326
  .action(agentRemoveToolCommand);
310
327
 
328
+ // ── Village Content Pipeline ───────────────────────────
329
+
330
+ program
331
+ .command('checkin')
332
+ .description('Check in with your village and share updates')
333
+ .option('--skip-discover', 'Skip the auto-discover step')
334
+ .action(checkinCommand);
335
+
336
+ program
337
+ .command('discover')
338
+ .description('Find connections between villages')
339
+ .action(discoverCommand);
340
+
341
+ program
342
+ .command('log [text]')
343
+ .description('Quick build log entry')
344
+ .option('-t, --track <track>', 'Build track (game, portal, agent, data, robot, general)')
345
+ .action(logCommand);
346
+
347
+ program
348
+ .command('story')
349
+ .description('Share a story or wisdom with the network')
350
+ .action(storyCommand);
351
+
311
352
  // ── BizReqs: Business Requirements Pipeline ───────────
312
353
 
313
354
  const bizreqsCmd = program
@@ -357,5 +398,105 @@ export function run() {
357
398
  .option('--contact <name>', 'Contact name')
358
399
  .action(bizreqsImportCommand);
359
400
 
401
+ // ── SoulPrint Studio: Model Training Pipeline ───────────
402
+
403
+ const soulprintCmd = program
404
+ .command('soulprint')
405
+ .description('SoulPrint Studio \u2014 datasets, training, and model publishing');
406
+
407
+ soulprintCmd
408
+ .command('init')
409
+ .description('Initialize local training workspace')
410
+ .option('--skip-python', 'Skip Python venv setup')
411
+ .option('--skip-scripts', 'Skip training script download')
412
+ .action(soulprintInitCommand);
413
+
414
+ soulprintCmd
415
+ .command('ingest <path>')
416
+ .description('Ingest training data into a dataset')
417
+ .requiredOption('--dataset <slug>', 'Target dataset slug')
418
+ .requiredOption('--type <type>', 'Data type: text, image, audio, structured, multimodal')
419
+ .option('--source <source>', 'Ingestion source label', 'CLI')
420
+ .option('--captions <path>', 'Path to captions file (image/multimodal)')
421
+ .option('--transcriptions <path>', 'Path to transcriptions file (audio)')
422
+ .option('--schema <path>', 'Path to schema JSON file (structured)')
423
+ .option('--recursive', 'Recurse into subdirectories')
424
+ .option('--glob <pattern>', 'File glob pattern to filter')
425
+ .option('--split <split>', 'Assign all items to a split: train, validation, test')
426
+ .option('--dry-run', 'Show what would be ingested without uploading')
427
+ .option('--concurrency <n>', 'Parallel upload limit', '5')
428
+ .action(soulprintIngestCommand);
429
+
430
+ const soulprintDatasetCmd = soulprintCmd
431
+ .command('datasets')
432
+ .description('Manage training datasets');
433
+
434
+ soulprintDatasetCmd
435
+ .command('list')
436
+ .description('List available datasets on SoulPrint Studio')
437
+ .option('--type <type>', 'Filter by type: text, image, audio, structured, multimodal')
438
+ .option('--status <status>', 'Filter by status: collecting, ready, training, archived')
439
+ .option('--json', 'Output raw JSON')
440
+ .action(soulprintDatasetListCommand);
441
+
442
+ soulprintDatasetCmd
443
+ .command('pull <slug>')
444
+ .description('Download a dataset to your local machine')
445
+ .option('--version <number>', 'Specific version (default: latest frozen)')
446
+ .option('--split <split>', 'Download only a specific split: train, validation, test')
447
+ .option('--force', 'Re-download even if already present locally')
448
+ .action(soulprintDatasetPullCommand);
449
+
450
+ soulprintCmd
451
+ .command('train')
452
+ .description('Run model training locally')
453
+ .requiredOption('--type <type>', 'Training type: text, image, audio, structured, multimodal')
454
+ .requiredOption('--dataset <slug>', 'Dataset slug to train on')
455
+ .option('--version <number>', 'Dataset version (default: latest)')
456
+ .option('--base <model>', 'Base model identifier')
457
+ .option('--method <method>', 'Training method')
458
+ .option('--config <path>', 'Path to training config YAML file')
459
+ .option('--name <name>', 'Name for the resulting model')
460
+ .option('--dry-run', 'Validate config and dataset without starting training')
461
+ .action(soulprintTrainCommand);
462
+
463
+ soulprintCmd
464
+ .command('jobs [jobId]')
465
+ .description('View training job status')
466
+ .option('--status <status>', 'Filter by status')
467
+ .option('--json', 'Output raw JSON')
468
+ .action((jobId, options) => {
469
+ if (jobId) return soulprintJobDetailCommand(jobId, options);
470
+ return soulprintJobsCommand(options);
471
+ });
472
+
473
+ soulprintCmd
474
+ .command('models')
475
+ .description('List models in the registry')
476
+ .option('--status <status>', 'Filter: draft, validated, published, rejected')
477
+ .option('--type <type>', 'Filter: text, image, audio, structured, multimodal')
478
+ .option('--json', 'Output raw JSON')
479
+ .action(soulprintModelsCommand);
480
+
481
+ soulprintCmd
482
+ .command('push <path>')
483
+ .description('Upload locally trained model artifacts to SoulPrint Studio')
484
+ .requiredOption('--name <name>', 'Model name')
485
+ .requiredOption('--type <type>', 'Model type: text, image, audio, structured, multimodal')
486
+ .option('--base <model>', 'Base model it was trained from')
487
+ .option('--method <method>', 'Training method used')
488
+ .option('--job <jobId>', 'Link to an existing training job')
489
+ .option('--description <text>', 'Model description')
490
+ .action(soulprintPushCommand);
491
+
492
+ soulprintCmd
493
+ .command('publish <modelSlug>')
494
+ .description('Publish a validated model to the MyVillage platform')
495
+ .option('--villager <id>', 'Target villager ID to own the model')
496
+ .option('--villages <ids>', 'Comma-separated village IDs to associate')
497
+ .option('--tier <tier>', 'Model tier: FREE, BASIC, PRO, ENTERPRISE', 'FREE')
498
+ .option('--public', 'Make the model publicly available')
499
+ .action(soulprintPublishCommand);
500
+
360
501
  program.parse();
361
502
  }
@@ -5,9 +5,9 @@ import { stringify as stringifyYaml } from 'yaml';
5
5
  // ── MCP Tool Catalog ────────────────────────────────────
6
6
 
7
7
  const TOOL_CATALOG = {
8
- 'man-feed': {
9
- command: 'internal',
10
- description: 'MAN feed access (post, read, react)',
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
- // man-feed is always included
155
- servers['man-feed'] = TOOL_CATALOG['man-feed'];
154
+ // myvillage is always included
155
+ servers['myvillage'] = TOOL_CATALOG['myvillage'];
156
156
 
157
157
  for (const toolId of selectedTools) {
158
- if (toolId === 'man-feed') continue;
158
+ if (toolId === 'myvillage') continue;
159
159
  if (TOOL_CATALOG[toolId]) {
160
160
  servers[toolId] = { ...TOOL_CATALOG[toolId] };
161
161
  }
@@ -0,0 +1,30 @@
1
+ import chalk from 'chalk';
2
+ import inquirer from 'inquirer';
3
+ import { getConfig } from './config.js';
4
+ import { brand } from './brand.js';
5
+
6
+ export async function getAnthropicProvider() {
7
+ const { createAnthropic } = await import('@ai-sdk/anthropic');
8
+ const config = getConfig();
9
+ let apiKey = config.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
10
+
11
+ if (!apiKey) {
12
+ console.log(chalk.yellow('\n Anthropic API key required for AI features.'));
13
+ console.log(brand.teal(' Get yours at: https://console.anthropic.com\n'));
14
+
15
+ const { key } = await inquirer.prompt([{
16
+ type: 'password',
17
+ name: 'key',
18
+ message: 'Anthropic API key:',
19
+ mask: '*',
20
+ validate: (input) => input.trim().length > 0 || 'API key is required',
21
+ }]);
22
+
23
+ apiKey = key.trim();
24
+ const { setConfig } = await import('./config.js');
25
+ setConfig({ anthropicApiKey: apiKey });
26
+ console.log(brand.green(' \u2713 API key saved to ~/.myvillage/config.json\n'));
27
+ }
28
+
29
+ return createAnthropic({ apiKey });
30
+ }
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: {
@@ -379,6 +379,37 @@ export async function registerOAuthClient(name, appType, redirectUris) {
379
379
  return response.data;
380
380
  }
381
381
 
382
+ // ── Platform API Client (/api) ──────────────────────────
383
+
384
+ export function getPlatformClient() {
385
+ const config = getConfig();
386
+ return createClient(config.platformBaseUrl);
387
+ }
388
+
389
+ export async function getVillagerVillages(villagerUuid) {
390
+ const client = getPlatformClient();
391
+ const response = await client.get(`/villagers/${villagerUuid}/villages`);
392
+ return response.data;
393
+ }
394
+
395
+ export async function getVillagerByVillagerId(villagerId) {
396
+ const client = getPlatformClient();
397
+ const response = await client.get(`/villagers/by-villager-id/${villagerId}`);
398
+ return response.data;
399
+ }
400
+
401
+ export async function getVillageCommunities(villageUuid) {
402
+ const client = getNetworkClient();
403
+ const response = await client.get('/communities', { params: { villageId: villageUuid } });
404
+ return response.data;
405
+ }
406
+
407
+ export async function listPostsByFilters(params = {}) {
408
+ const client = getNetworkClient();
409
+ const response = await client.get('/posts', { params });
410
+ return response.data;
411
+ }
412
+
382
413
  // ── BizReqs API Client (/api/bizreqs) ───────────────────
383
414
 
384
415
  export function getBizReqsClient() {
@@ -10,6 +10,8 @@ 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',
14
+ platformBaseUrl: 'https://portal.myvillageproject.ai/api',
13
15
  clientId: 'mvos_aG_c729fuQxvvqYHOnkgTQ',
14
16
  callbackPort: 3737,
15
17
  anthropicApiKey: null,
@@ -743,3 +743,88 @@ export function formatPagination(meta) {
743
743
  console.log(chalk.dim(` ── More results available. Run with --cursor=${meta.nextCursor} to see next page\n`));
744
744
  }
745
745
  }
746
+
747
+ // ── MAN Content Pipeline ────────────────────────────────
748
+
749
+ export function formatCheckinSummary(post, meta) {
750
+ const b = (s) => chalk.hex('#B07C00')(s);
751
+ const width = 56;
752
+ const bdr = '\u2500'.repeat(width);
753
+
754
+ const lines = [''];
755
+ lines.push(` ${b('\u250C' + bdr + '\u2510')}`);
756
+ lines.push(` ${b('\u2502')} ${brand.gold('\u2743 Village Check-in')}${' '.repeat(width - 19)}${b('\u2502')}`);
757
+ lines.push(` ${b('\u2502' + '\u2500'.repeat(width) + '\u2502')}`);
758
+
759
+ if (meta.villageName) {
760
+ const vl = ` Village: ${meta.villageName}`;
761
+ lines.push(` ${b('\u2502')}${brand.cream(vl).padEnd(width + 12)}${b('\u2502')}`);
762
+ }
763
+ if (meta.buildTrack) {
764
+ const tl = ` Track: ${meta.buildTrack}`;
765
+ lines.push(` ${b('\u2502')}${brand.teal(tl).padEnd(width + 12)}${b('\u2502')}`);
766
+ }
767
+ if (meta.themes?.length) {
768
+ const th = ` Themes: ${meta.themes.join(', ')}`;
769
+ lines.push(` ${b('\u2502')}${chalk.white(th).padEnd(width + 12)}${b('\u2502')}`);
770
+ }
771
+ if (meta.needs?.length) {
772
+ const nd = ` Needs: ${meta.needs.join(', ')}`;
773
+ lines.push(` ${b('\u2502')}${chalk.yellow(nd).padEnd(width + 12)}${b('\u2502')}`);
774
+ }
775
+ if (meta.offers?.length) {
776
+ const of_ = ` Offers: ${meta.offers.join(', ')}`;
777
+ lines.push(` ${b('\u2502')}${brand.green(of_).padEnd(width + 12)}${b('\u2502')}`);
778
+ }
779
+ if (post?.data?.id) {
780
+ const id = ` Post ID: ${post.data.id.slice(0, 8)}...`;
781
+ lines.push(` ${b('\u2502')}${chalk.dim(id).padEnd(width + 12)}${b('\u2502')}`);
782
+ }
783
+
784
+ lines.push(` ${b('\u2514' + bdr + '\u2518')}`);
785
+ lines.push('');
786
+
787
+ console.log(lines.join('\n'));
788
+ }
789
+
790
+ export function formatConnection(connection) {
791
+ const b = (s) => chalk.hex('#B07C00')(s);
792
+ const width = 56;
793
+ const bdr = '\u2500'.repeat(width);
794
+
795
+ const lines = [''];
796
+ lines.push(` ${b('\u250C' + bdr + '\u2510')}`);
797
+ lines.push(` ${b('\u2502')} ${brand.gold('\u21C4 Connection Found')}${' '.repeat(width - 20)}${b('\u2502')}`);
798
+ lines.push(` ${b('\u2502' + '\u2500'.repeat(width) + '\u2502')}`);
799
+
800
+ if (connection.villageName) {
801
+ const vl = ` Village: ${connection.villageName}`;
802
+ lines.push(` ${b('\u2502')}${brand.cream(vl).padEnd(width + 12)}${b('\u2502')}`);
803
+ }
804
+ if (connection.topic) {
805
+ const tp = ` Topic: ${connection.topic}`;
806
+ lines.push(` ${b('\u2502')}${chalk.white(tp).padEnd(width + 12)}${b('\u2502')}`);
807
+ }
808
+ if (connection.matchType) {
809
+ const mt = ` Match: ${connection.matchType}`;
810
+ lines.push(` ${b('\u2502')}${brand.teal(mt).padEnd(width + 12)}${b('\u2502')}`);
811
+ }
812
+ if (connection.score !== undefined) {
813
+ const sc = ` Relevance: ${'\u2605'.repeat(Math.min(connection.score, 5))}${'\u2606'.repeat(Math.max(0, 5 - connection.score))}`;
814
+ lines.push(` ${b('\u2502')}${brand.gold(sc).padEnd(width + 12)}${b('\u2502')}`);
815
+ }
816
+
817
+ lines.push(` ${b('\u2514' + bdr + '\u2518')}`);
818
+ lines.push('');
819
+
820
+ console.log(lines.join('\n'));
821
+ }
822
+
823
+ export function formatBuildLog(post) {
824
+ const tags = post.tags || [];
825
+ const track = tags.find((t) => t.startsWith('track:'))?.replace('track:', '') || 'general';
826
+ const body = post.body?.length > 60 ? post.body.slice(0, 57) + '...' : post.body || '';
827
+ const time = relativeTime(post.createdAt);
828
+
829
+ console.log(` ${brand.teal(`[${track}]`)} ${chalk.white(body)} ${chalk.dim(time)}`);
830
+ }
@@ -0,0 +1,16 @@
1
+ import chalk from 'chalk';
2
+
3
+ /**
4
+ * Trigger a MAN post from an automated event (deploy, publish, etc.).
5
+ * Stub implementation — future commands will call this to auto-generate MAN posts.
6
+ *
7
+ * @param {object} params
8
+ * @param {string} params.trigger - What triggered this (e.g., 'deploy', 'publish')
9
+ * @param {string} params.artifactId - ID of the artifact (game ID, model ID, etc.)
10
+ * @param {string} params.summary - Human-readable summary of what happened
11
+ * @param {string} params.villageId - Village UUID
12
+ * @param {string} params.communitySlug - Target community slug
13
+ */
14
+ export async function triggerMANPost({ trigger, artifactId, summary, villageId, communitySlug }) {
15
+ console.log(chalk.dim(` [MAN hook] ${trigger}: ${summary} (not yet wired — future feature)`));
16
+ }
@@ -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
+ }