@hasna/connectors 1.3.19 → 1.3.21

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,78 @@
1
+ import type { MinimaxClient } from './client';
2
+ import type {
3
+ VideoModel,
4
+ VideoGenerateRequest,
5
+ VideoGenerateResponse,
6
+ VideoStatusResponse,
7
+ VideoFileResponse,
8
+ } from '../types';
9
+
10
+ export interface VideoOptions {
11
+ model?: VideoModel;
12
+ firstFrameImage?: string;
13
+ subjectReference?: string[];
14
+ promptOptimizer?: boolean;
15
+ }
16
+
17
+ export class VideoApi {
18
+ constructor(private readonly client: MinimaxClient) {}
19
+
20
+ async generate(prompt: string, options: VideoOptions = {}): Promise<VideoGenerateResponse> {
21
+ const request: VideoGenerateRequest = {
22
+ model: options.model || 'T2V-01',
23
+ prompt,
24
+ prompt_optimizer: options.promptOptimizer ?? true,
25
+ };
26
+
27
+ if (options.firstFrameImage) {
28
+ request.first_frame_image = options.firstFrameImage;
29
+ request.model = options.model || 'I2V-01';
30
+ }
31
+
32
+ if (options.subjectReference) {
33
+ request.subject_reference = options.subjectReference;
34
+ }
35
+
36
+ return this.client.post<VideoGenerateResponse>('/video_generation', request);
37
+ }
38
+
39
+ async getStatus(taskId: string): Promise<VideoStatusResponse> {
40
+ return this.client.get<VideoStatusResponse>('/query/video_generation', { task_id: taskId });
41
+ }
42
+
43
+ async getFileUrl(fileId: string): Promise<string> {
44
+ const response = await this.client.get<VideoFileResponse>('/files/retrieve', { file_id: fileId });
45
+ return response.file.download_url;
46
+ }
47
+
48
+ async download(fileId: string): Promise<Buffer> {
49
+ const url = await this.getFileUrl(fileId);
50
+ return this.client.downloadFile(url);
51
+ }
52
+
53
+ async generateAndWait(
54
+ prompt: string,
55
+ options: VideoOptions = {},
56
+ pollIntervalMs = 10000,
57
+ maxAttempts = 120
58
+ ): Promise<{ fileId: string; downloadUrl: string }> {
59
+ const job = await this.generate(prompt, options);
60
+ const taskId = job.task_id;
61
+
62
+ for (let i = 0; i < maxAttempts; i++) {
63
+ await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
64
+ const status = await this.getStatus(taskId);
65
+
66
+ if (status.status === 'Success' && status.file_id) {
67
+ const url = await this.getFileUrl(status.file_id);
68
+ return { fileId: status.file_id, downloadUrl: url };
69
+ }
70
+
71
+ if (status.status === 'Fail') {
72
+ throw new Error(`Video generation failed: ${status.base_resp?.status_msg || 'Unknown error'}`);
73
+ }
74
+ }
75
+
76
+ throw new Error('Video generation timed out');
77
+ }
78
+ }
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env bun
2
+ import { Command } from 'commander';
3
+ import { writeFile } from 'fs/promises';
4
+ import { resolve } from 'path';
5
+ import { Minimax } from '../api';
6
+ import {
7
+ getApiKey,
8
+ setApiKey,
9
+ getGroupId,
10
+ setGroupId,
11
+ clearConfig,
12
+ getConfigDir,
13
+ setProfileOverride,
14
+ getCurrentProfile,
15
+ setCurrentProfile,
16
+ listProfiles,
17
+ createProfile,
18
+ deleteProfile,
19
+ profileExists,
20
+ } from '../utils/config';
21
+ import { success, error, info, print } from '../utils/output';
22
+ import type { OutputFormat } from '../utils/output';
23
+
24
+ const CONNECTOR_NAME = 'connect-minimax';
25
+ const VERSION = '0.1.0';
26
+
27
+ const program = new Command();
28
+
29
+ program
30
+ .name(CONNECTOR_NAME)
31
+ .description('Minimax API connector - Video, music, image, TTS, and sound effects')
32
+ .version(VERSION)
33
+ .option('-k, --api-key <key>', 'API key (overrides config)')
34
+ .option('-f, --format <format>', 'Output format (json, pretty)', 'pretty')
35
+ .option('-p, --profile <profile>', 'Use a specific profile')
36
+ .hook('preAction', (thisCommand) => {
37
+ const opts = thisCommand.opts();
38
+ if (opts.profile) {
39
+ if (!profileExists(opts.profile)) {
40
+ error(`Profile "${opts.profile}" does not exist.`);
41
+ process.exit(1);
42
+ }
43
+ setProfileOverride(opts.profile);
44
+ }
45
+ if (opts.apiKey) process.env.MINIMAX_API_KEY = opts.apiKey;
46
+ });
47
+
48
+ function getFormat(cmd: Command): OutputFormat {
49
+ return (cmd.parent?.opts().format || 'pretty') as OutputFormat;
50
+ }
51
+
52
+ function getClient(): Minimax {
53
+ const apiKey = getApiKey();
54
+ if (!apiKey) {
55
+ error(`No API key configured. Run "${CONNECTOR_NAME} config set-key <key>" or set MINIMAX_API_KEY.`);
56
+ process.exit(1);
57
+ }
58
+ const groupId = getGroupId();
59
+ return new Minimax({ apiKey, groupId });
60
+ }
61
+
62
+ // Config
63
+ const configCmd = program.command('config').description('Manage configuration');
64
+
65
+ configCmd.command('set-key <key>').description('Set API key').action((key) => {
66
+ setApiKey(key);
67
+ success('API key saved');
68
+ });
69
+
70
+ configCmd.command('set-group <id>').description('Set group ID').action((id) => {
71
+ setGroupId(id);
72
+ success('Group ID saved');
73
+ });
74
+
75
+ configCmd.command('show').description('Show current config').action(() => {
76
+ const key = getApiKey();
77
+ const group = getGroupId();
78
+ info(`Profile: ${getCurrentProfile()}`);
79
+ info(`API Key: ${key ? key.substring(0, 6) + '...' : 'not set'}`);
80
+ info(`Group ID: ${group || 'not set'}`);
81
+ info(`Config dir: ${getConfigDir()}`);
82
+ });
83
+
84
+ configCmd.command('clear').description('Clear config').action(() => {
85
+ clearConfig();
86
+ success('Config cleared');
87
+ });
88
+
89
+ // Profile
90
+ const profileCmd = program.command('profile').description('Manage profiles');
91
+ profileCmd.command('list').description('List profiles').action(() => {
92
+ const profiles = listProfiles();
93
+ const current = getCurrentProfile();
94
+ if (profiles.length === 0) { info('No profiles'); return; }
95
+ profiles.forEach(p => console.log(p === current ? `* ${p}` : ` ${p}`));
96
+ });
97
+ profileCmd.command('create <name>').description('Create profile').action((name) => {
98
+ createProfile(name) ? success(`Profile "${name}" created`) : error(`Profile "${name}" already exists`);
99
+ });
100
+ profileCmd.command('use <name>').description('Switch profile').action((name) => {
101
+ setCurrentProfile(name);
102
+ success(`Switched to profile "${name}"`);
103
+ });
104
+ profileCmd.command('delete <name>').description('Delete profile').action((name) => {
105
+ deleteProfile(name) ? success(`Profile "${name}" deleted`) : error(`Cannot delete "${name}"`);
106
+ });
107
+
108
+ // Video
109
+ const videoCmd = program.command('video').description('Video generation');
110
+ videoCmd
111
+ .command('generate <prompt>')
112
+ .description('Generate a video from a text prompt')
113
+ .option('-m, --model <model>', 'Model (T2V-01, I2V-01)', 'T2V-01')
114
+ .option('-o, --output <path>', 'Save video to file')
115
+ .option('--image <url>', 'First frame image (switches to I2V)')
116
+ .option('--no-optimize', 'Disable prompt optimizer')
117
+ .action(async (prompt, opts, cmd) => {
118
+ const client = getClient();
119
+ info('Starting video generation...');
120
+ try {
121
+ const result = await client.video.generateAndWait(prompt, {
122
+ model: opts.model,
123
+ firstFrameImage: opts.image,
124
+ promptOptimizer: opts.optimize !== false,
125
+ });
126
+ if (opts.output) {
127
+ const buffer = await client.video.download(result.fileId);
128
+ await writeFile(resolve(opts.output), buffer);
129
+ success(`Video saved to: ${opts.output}`);
130
+ } else {
131
+ print(result, getFormat(cmd));
132
+ }
133
+ } catch (e: any) { error(e.message); process.exit(1); }
134
+ });
135
+
136
+ // Music
137
+ const musicCmd = program.command('music').description('Music generation');
138
+ musicCmd
139
+ .command('generate <prompt>')
140
+ .description('Generate music from a prompt')
141
+ .option('-o, --output <path>', 'Save audio to file')
142
+ .option('--lyrics <text>', 'Lyrics for the song')
143
+ .option('--genre <genre>', 'Music genre')
144
+ .option('--mood <mood>', 'Desired mood')
145
+ .option('--tempo <bpm>', 'Tempo in BPM', parseInt)
146
+ .option('--duration <seconds>', 'Duration in seconds', parseInt)
147
+ .action(async (prompt, opts, cmd) => {
148
+ const client = getClient();
149
+ info('Starting music generation...');
150
+ try {
151
+ const result = await client.music.generateAndWait(prompt, {
152
+ lyrics: opts.lyrics,
153
+ genre: opts.genre,
154
+ mood: opts.mood,
155
+ tempo: opts.tempo,
156
+ duration: opts.duration,
157
+ });
158
+ if (opts.output) {
159
+ const buffer = await client.music.download(result.audioUrl);
160
+ await writeFile(resolve(opts.output), buffer);
161
+ success(`Music saved to: ${opts.output}`);
162
+ } else {
163
+ print(result, getFormat(cmd));
164
+ }
165
+ } catch (e: any) { error(e.message); process.exit(1); }
166
+ });
167
+
168
+ // TTS
169
+ const ttsCmd = program.command('tts').description('Text-to-speech');
170
+ ttsCmd
171
+ .command('generate <text>')
172
+ .description('Generate speech from text')
173
+ .option('-o, --output <path>', 'Save audio to file (required)')
174
+ .option('-m, --model <model>', 'Model', 'speech-02-hd')
175
+ .option('--voice <id>', 'Voice ID')
176
+ .option('--speed <n>', 'Speed (0.5-2.0)', parseFloat)
177
+ .option('--format <fmt>', 'Audio format (mp3, wav, flac)', 'mp3')
178
+ .option('--language <code>', 'Language boost code')
179
+ .action(async (text, opts, cmd) => {
180
+ const client = getClient();
181
+ if (!opts.output) { error('--output is required'); process.exit(1); }
182
+ info('Generating speech...');
183
+ try {
184
+ const buffer = await client.tts.generateToBuffer(text, {
185
+ model: opts.model,
186
+ voiceId: opts.voice,
187
+ speed: opts.speed,
188
+ format: opts.format,
189
+ languageBoost: opts.language,
190
+ });
191
+ await writeFile(resolve(opts.output), buffer);
192
+ success(`Audio saved to: ${opts.output}`);
193
+ } catch (e: any) { error(e.message); process.exit(1); }
194
+ });
195
+
196
+ // Image
197
+ const imageCmd = program.command('image').description('Image generation');
198
+ imageCmd
199
+ .command('generate <prompt>')
200
+ .description('Generate an image from a prompt')
201
+ .option('-o, --output <path>', 'Save image to file')
202
+ .option('--aspect <ratio>', 'Aspect ratio (1:1, 16:9, 9:16, 4:3, 3:4)', '1:1')
203
+ .option('-n, --count <n>', 'Number of images', parseInt, 1)
204
+ .action(async (prompt, opts, cmd) => {
205
+ const client = getClient();
206
+ info('Starting image generation...');
207
+ try {
208
+ const result = await client.image.generateAndWait(prompt, {
209
+ aspectRatio: opts.aspect,
210
+ n: opts.count,
211
+ });
212
+ if (opts.output) {
213
+ const buffer = await client.image.download(result.fileId);
214
+ await writeFile(resolve(opts.output), buffer);
215
+ success(`Image saved to: ${opts.output}`);
216
+ } else {
217
+ print(result, getFormat(cmd));
218
+ }
219
+ } catch (e: any) { error(e.message); process.exit(1); }
220
+ });
221
+
222
+ // Sound Effects
223
+ const sfxCmd = program.command('sfx').description('Sound effects generation');
224
+ sfxCmd
225
+ .command('generate <prompt>')
226
+ .description('Generate a sound effect from a prompt')
227
+ .option('-o, --output <path>', 'Save audio to file')
228
+ .option('--duration <seconds>', 'Duration in seconds', parseInt)
229
+ .action(async (prompt, opts, cmd) => {
230
+ const client = getClient();
231
+ info('Generating sound effect...');
232
+ try {
233
+ const result = await client.soundEffects.generateAndWait(prompt, {
234
+ duration: opts.duration,
235
+ });
236
+ if (opts.output) {
237
+ const buffer = await client.soundEffects.download(result.audioUrl);
238
+ await writeFile(resolve(opts.output), buffer);
239
+ success(`Sound effect saved to: ${opts.output}`);
240
+ } else {
241
+ print(result, getFormat(cmd));
242
+ }
243
+ } catch (e: any) { error(e.message); process.exit(1); }
244
+ });
245
+
246
+ program.parse();
@@ -0,0 +1,19 @@
1
+ export { Minimax, Connector } from './api';
2
+ export * from './types';
3
+
4
+ export { MinimaxClient, VideoApi, MusicApi, TTSApi, ImageApi, SoundEffectsApi } from './api';
5
+
6
+ export {
7
+ getApiKey,
8
+ setApiKey,
9
+ getGroupId,
10
+ setGroupId,
11
+ getCurrentProfile,
12
+ setCurrentProfile,
13
+ listProfiles,
14
+ createProfile,
15
+ deleteProfile,
16
+ loadProfile,
17
+ saveProfile,
18
+ clearConfig,
19
+ } from './utils/config';
@@ -0,0 +1,182 @@
1
+ export interface MinimaxConfig {
2
+ apiKey: string;
3
+ groupId?: string;
4
+ baseUrl?: string;
5
+ }
6
+
7
+ // Models
8
+ export type VideoModel = 'T2V-01' | 'T2V-01-Director' | 'I2V-01' | 'I2V-01-Director' | 'S2V-01';
9
+ export type MusicModel = 'music-01';
10
+ export type TTSModel = 'speech-02' | 'speech-02-hd' | 'speech-02-turbo';
11
+ export type ImageModel = 'image-01';
12
+
13
+ // Video Generation
14
+ export interface VideoGenerateRequest {
15
+ model: VideoModel;
16
+ prompt?: string;
17
+ first_frame_image?: string;
18
+ subject_reference?: string[];
19
+ prompt_optimizer?: boolean;
20
+ }
21
+
22
+ export interface VideoGenerateResponse {
23
+ task_id: string;
24
+ base_resp?: { status_code: number; status_msg: string };
25
+ }
26
+
27
+ export interface VideoStatusResponse {
28
+ task_id: string;
29
+ status: 'Queueing' | 'Processing' | 'Success' | 'Fail';
30
+ file_id?: string;
31
+ base_resp?: { status_code: number; status_msg: string };
32
+ }
33
+
34
+ export interface VideoFileResponse {
35
+ file: {
36
+ file_id: string;
37
+ bytes: number;
38
+ created_at: number;
39
+ filename: string;
40
+ purpose: string;
41
+ download_url: string;
42
+ };
43
+ base_resp?: { status_code: number; status_msg: string };
44
+ }
45
+
46
+ // Music Generation
47
+ export interface MusicGenerateRequest {
48
+ model: MusicModel;
49
+ lyrics?: string;
50
+ refer_voice?: string;
51
+ refer_instrumental?: string;
52
+ prompt?: string;
53
+ genre?: string;
54
+ mood?: string;
55
+ tempo?: number;
56
+ duration?: number;
57
+ }
58
+
59
+ export interface MusicGenerateResponse {
60
+ task_id: string;
61
+ base_resp?: { status_code: number; status_msg: string };
62
+ }
63
+
64
+ export interface MusicStatusResponse {
65
+ task_id: string;
66
+ status: 'Queueing' | 'Processing' | 'Success' | 'Fail';
67
+ audio_file?: string;
68
+ extra_info?: {
69
+ audio_url?: string;
70
+ lyrics?: string;
71
+ instrumental_url?: string;
72
+ };
73
+ base_resp?: { status_code: number; status_msg: string };
74
+ }
75
+
76
+ // TTS (Text to Audio)
77
+ export interface TTSRequest {
78
+ model: TTSModel;
79
+ text: string;
80
+ voice_setting?: {
81
+ voice_id?: string;
82
+ speed?: number;
83
+ vol?: number;
84
+ pitch?: number;
85
+ emotion?: string;
86
+ };
87
+ audio_setting?: {
88
+ sample_rate?: number;
89
+ bitrate?: number;
90
+ format?: 'mp3' | 'wav' | 'pcm' | 'flac';
91
+ channel?: number;
92
+ };
93
+ language_boost?: string;
94
+ }
95
+
96
+ export interface TTSResponse {
97
+ data?: {
98
+ audio?: string;
99
+ };
100
+ extra_info?: {
101
+ audio_length?: number;
102
+ audio_sample_rate?: number;
103
+ audio_size?: number;
104
+ bitrate?: number;
105
+ word_count?: number;
106
+ invisible_character_ratio?: number;
107
+ };
108
+ base_resp?: { status_code: number; status_msg: string };
109
+ }
110
+
111
+ // Image Generation
112
+ export interface ImageGenerateRequest {
113
+ model: ImageModel;
114
+ prompt: string;
115
+ aspect_ratio?: '1:1' | '16:9' | '9:16' | '4:3' | '3:4';
116
+ n?: number;
117
+ prompt_optimizer?: boolean;
118
+ }
119
+
120
+ export interface ImageGenerateResponse {
121
+ task_id: string;
122
+ base_resp?: { status_code: number; status_msg: string };
123
+ }
124
+
125
+ export interface ImageStatusResponse {
126
+ task_id: string;
127
+ status: 'Queueing' | 'Processing' | 'Success' | 'Fail';
128
+ file_id?: string;
129
+ base_resp?: { status_code: number; status_msg: string };
130
+ }
131
+
132
+ // Sound Effects
133
+ export interface SoundEffectRequest {
134
+ model: string;
135
+ prompt: string;
136
+ duration?: number;
137
+ }
138
+
139
+ export interface SoundEffectResponse {
140
+ task_id: string;
141
+ base_resp?: { status_code: number; status_msg: string };
142
+ }
143
+
144
+ export interface SoundEffectStatusResponse {
145
+ task_id: string;
146
+ status: 'Queueing' | 'Processing' | 'Success' | 'Fail';
147
+ audio_file?: string;
148
+ extra_info?: {
149
+ audio_url?: string;
150
+ };
151
+ base_resp?: { status_code: number; status_msg: string };
152
+ }
153
+
154
+ // Voice Clone
155
+ export interface VoiceCloneRequest {
156
+ file: Buffer;
157
+ voice_id?: string;
158
+ }
159
+
160
+ export interface VoiceListResponse {
161
+ voices: Array<{
162
+ voice_id: string;
163
+ name: string;
164
+ language?: string;
165
+ description?: string;
166
+ }>;
167
+ }
168
+
169
+ // Shared
170
+ export type OutputFormat = 'json' | 'pretty';
171
+
172
+ export class MinimaxApiError extends Error {
173
+ public readonly statusCode: number;
174
+ public readonly error?: { status_code: number; status_msg: string };
175
+
176
+ constructor(message: string, statusCode: number, error?: { status_code: number; status_msg: string }) {
177
+ super(message);
178
+ this.name = 'MinimaxApiError';
179
+ this.statusCode = statusCode;
180
+ this.error = error;
181
+ }
182
+ }
@@ -0,0 +1,125 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, rmSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import { join } from 'path';
4
+
5
+ const CONNECTOR_NAME = 'connect-minimax';
6
+ const DEFAULT_PROFILE = 'default';
7
+
8
+ export interface ProfileConfig {
9
+ apiKey?: string;
10
+ groupId?: string;
11
+ }
12
+
13
+ let profileOverride: string | undefined;
14
+
15
+ const CONFIG_DIR = join(homedir(), '.hasna', 'connectors', CONNECTOR_NAME);
16
+ const PROFILES_DIR = join(CONFIG_DIR, 'profiles');
17
+ const CURRENT_PROFILE_FILE = join(CONFIG_DIR, 'current_profile');
18
+
19
+ export function setProfileOverride(profile: string | undefined): void {
20
+ profileOverride = profile;
21
+ }
22
+
23
+ export function ensureConfigDir(): void {
24
+ if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
25
+ if (!existsSync(PROFILES_DIR)) mkdirSync(PROFILES_DIR, { recursive: true });
26
+ }
27
+
28
+ function getProfilePath(profile: string): string {
29
+ return join(PROFILES_DIR, `${profile}.json`);
30
+ }
31
+
32
+ export function getCurrentProfile(): string {
33
+ if (profileOverride) return profileOverride;
34
+ ensureConfigDir();
35
+ if (existsSync(CURRENT_PROFILE_FILE)) {
36
+ try {
37
+ const profile = readFileSync(CURRENT_PROFILE_FILE, 'utf-8').trim();
38
+ if (profile && profileExists(profile)) return profile;
39
+ } catch {}
40
+ }
41
+ return DEFAULT_PROFILE;
42
+ }
43
+
44
+ export function setCurrentProfile(profile: string): void {
45
+ ensureConfigDir();
46
+ if (!profileExists(profile) && profile !== DEFAULT_PROFILE) {
47
+ throw new Error(`Profile "${profile}" does not exist`);
48
+ }
49
+ writeFileSync(CURRENT_PROFILE_FILE, profile);
50
+ }
51
+
52
+ export function profileExists(profile: string): boolean {
53
+ return existsSync(getProfilePath(profile));
54
+ }
55
+
56
+ export function listProfiles(): string[] {
57
+ ensureConfigDir();
58
+ if (!existsSync(PROFILES_DIR)) return [];
59
+ return readdirSync(PROFILES_DIR)
60
+ .filter(f => f.endsWith('.json'))
61
+ .map(f => f.replace('.json', ''))
62
+ .sort();
63
+ }
64
+
65
+ export function createProfile(profile: string, config: ProfileConfig = {}): boolean {
66
+ ensureConfigDir();
67
+ if (profileExists(profile)) return false;
68
+ if (!/^[a-zA-Z0-9_-]+$/.test(profile)) {
69
+ throw new Error('Profile name can only contain letters, numbers, hyphens, and underscores');
70
+ }
71
+ writeFileSync(getProfilePath(profile), JSON.stringify(config, null, 2));
72
+ return true;
73
+ }
74
+
75
+ export function deleteProfile(profile: string): boolean {
76
+ if (profile === DEFAULT_PROFILE) return false;
77
+ if (!profileExists(profile)) return false;
78
+ if (getCurrentProfile() === profile) setCurrentProfile(DEFAULT_PROFILE);
79
+ rmSync(getProfilePath(profile));
80
+ return true;
81
+ }
82
+
83
+ export function loadProfile(profile?: string): ProfileConfig {
84
+ ensureConfigDir();
85
+ const profilePath = getProfilePath(profile || getCurrentProfile());
86
+ if (!existsSync(profilePath)) return {};
87
+ try {
88
+ return JSON.parse(readFileSync(profilePath, 'utf-8'));
89
+ } catch {
90
+ return {};
91
+ }
92
+ }
93
+
94
+ export function saveProfile(config: ProfileConfig, profile?: string): void {
95
+ ensureConfigDir();
96
+ writeFileSync(getProfilePath(profile || getCurrentProfile()), JSON.stringify(config, null, 2));
97
+ }
98
+
99
+ export function getApiKey(): string | undefined {
100
+ return process.env.MINIMAX_API_KEY || loadProfile().apiKey;
101
+ }
102
+
103
+ export function setApiKey(apiKey: string): void {
104
+ const config = loadProfile();
105
+ config.apiKey = apiKey;
106
+ saveProfile(config);
107
+ }
108
+
109
+ export function getGroupId(): string | undefined {
110
+ return process.env.MINIMAX_GROUP_ID || loadProfile().groupId;
111
+ }
112
+
113
+ export function setGroupId(groupId: string): void {
114
+ const config = loadProfile();
115
+ config.groupId = groupId;
116
+ saveProfile(config);
117
+ }
118
+
119
+ export function clearConfig(): void {
120
+ saveProfile({});
121
+ }
122
+
123
+ export function getConfigDir(): string {
124
+ return CONFIG_DIR;
125
+ }
@@ -0,0 +1,40 @@
1
+ import chalk from 'chalk';
2
+
3
+ export type OutputFormat = 'json' | 'pretty';
4
+
5
+ export function formatOutput(data: unknown, format: OutputFormat = 'pretty'): string {
6
+ if (format === 'json') return JSON.stringify(data, null, 2);
7
+ return formatPretty(data);
8
+ }
9
+
10
+ function formatPretty(data: unknown, indent = 0): string {
11
+ if (data === null || data === undefined) return chalk.gray('null');
12
+ if (typeof data !== 'object') return String(data);
13
+
14
+ const spaces = ' '.repeat(indent);
15
+ const entries = Object.entries(data as Record<string, unknown>);
16
+
17
+ return entries
18
+ .map(([key, value]) => {
19
+ if (Array.isArray(value)) {
20
+ if (value.length === 0) return `${spaces}${chalk.blue(key)}: ${chalk.gray('[]')}`;
21
+ if (typeof value[0] === 'object') {
22
+ return `${spaces}${chalk.blue(key)}:\n${value.map(v => formatPretty(v, indent + 1)).join('\n')}`;
23
+ }
24
+ return `${spaces}${chalk.blue(key)}: ${value.join(', ')}`;
25
+ }
26
+ if (typeof value === 'object' && value !== null) {
27
+ return `${spaces}${chalk.blue(key)}:\n${formatPretty(value, indent + 1)}`;
28
+ }
29
+ return `${spaces}${chalk.blue(key)}: ${chalk.white(String(value))}`;
30
+ })
31
+ .join('\n');
32
+ }
33
+
34
+ export function success(message: string): void { console.log(chalk.green('✓'), message); }
35
+ export function error(message: string): void { console.error(chalk.red('✗'), message); }
36
+ export function warn(message: string): void { console.warn(chalk.yellow('⚠'), message); }
37
+ export function info(message: string): void { console.log(chalk.blue('ℹ'), message); }
38
+ export function print(data: unknown, format: OutputFormat = 'pretty'): void {
39
+ console.log(formatOutput(data, format));
40
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src"
6
+ },
7
+ "include": ["src/**/*.ts"]
8
+ }