meetscribe 1.0.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,182 @@
1
+ /**
2
+ * Protocol command - get meeting protocol
3
+ */
4
+
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import chalk from 'chalk';
8
+ import ora from 'ora';
9
+
10
+ import { getApiKey, getApiUrl } from '../utils/config.js';
11
+ import { MeetScribeClient } from '../../sdk/client.js';
12
+ import { success, error, json } from '../utils/output.js';
13
+
14
+ /**
15
+ * Format protocol to markdown
16
+ */
17
+ function formatMarkdown(protocol) {
18
+ const lines = [];
19
+
20
+ lines.push(`# Meeting Protocol`);
21
+ lines.push('');
22
+
23
+ if (protocol.summary) {
24
+ lines.push('## Summary');
25
+ lines.push('');
26
+ lines.push(protocol.summary);
27
+ lines.push('');
28
+ }
29
+
30
+ if (protocol.participants_list && protocol.participants_list.length > 0) {
31
+ lines.push('## Participants');
32
+ lines.push('');
33
+ protocol.participants_list.forEach(p => {
34
+ const role = p.role ? ` (${p.role})` : '';
35
+ lines.push(`- ${p.name}${role}`);
36
+ });
37
+ lines.push('');
38
+ }
39
+
40
+ if (protocol.key_points && protocol.key_points.length > 0) {
41
+ lines.push('## Key Points');
42
+ lines.push('');
43
+ protocol.key_points.forEach(point => {
44
+ lines.push(`- ${point}`);
45
+ });
46
+ lines.push('');
47
+ }
48
+
49
+ if (protocol.decisions && protocol.decisions.length > 0) {
50
+ lines.push('## Decisions');
51
+ lines.push('');
52
+ protocol.decisions.forEach(decision => {
53
+ lines.push(`- ${decision}`);
54
+ });
55
+ lines.push('');
56
+ }
57
+
58
+ if (protocol.action_items && protocol.action_items.length > 0) {
59
+ lines.push('## Action Items');
60
+ lines.push('');
61
+ protocol.action_items.forEach(task => {
62
+ const assignee = task.assignee ? ` — ${task.assignee}` : '';
63
+ const deadline = task.deadline ? `, ${task.deadline}` : '';
64
+ const status = task.status === 'done' ? '[x]' : '[ ]';
65
+ lines.push(`- ${status} ${task.text}${assignee}${deadline}`);
66
+ });
67
+ lines.push('');
68
+ }
69
+
70
+ if (protocol.open_questions && protocol.open_questions.length > 0) {
71
+ lines.push('## Open Questions');
72
+ lines.push('');
73
+ protocol.open_questions.forEach(q => {
74
+ lines.push(`- ${q}`);
75
+ });
76
+ lines.push('');
77
+ }
78
+
79
+ if (protocol.next_meeting) {
80
+ lines.push('## Next Meeting');
81
+ lines.push('');
82
+ lines.push(protocol.next_meeting);
83
+ lines.push('');
84
+ }
85
+
86
+ return lines.join('\n');
87
+ }
88
+
89
+ /**
90
+ * Format protocol to plain text
91
+ */
92
+ function formatText(protocol) {
93
+ const lines = [];
94
+
95
+ lines.push('MEETING PROTOCOL');
96
+ lines.push('================');
97
+ lines.push('');
98
+
99
+ if (protocol.summary) {
100
+ lines.push('SUMMARY:');
101
+ lines.push(protocol.summary);
102
+ lines.push('');
103
+ }
104
+
105
+ if (protocol.action_items && protocol.action_items.length > 0) {
106
+ lines.push('ACTION ITEMS:');
107
+ protocol.action_items.forEach((task, i) => {
108
+ const assignee = task.assignee ? ` [${task.assignee}]` : '';
109
+ const deadline = task.deadline ? ` (${task.deadline})` : '';
110
+ lines.push(`${i + 1}. ${task.text}${assignee}${deadline}`);
111
+ });
112
+ lines.push('');
113
+ }
114
+
115
+ if (protocol.key_points && protocol.key_points.length > 0) {
116
+ lines.push('KEY POINTS:');
117
+ protocol.key_points.forEach((point, i) => {
118
+ lines.push(`${i + 1}. ${point}`);
119
+ });
120
+ lines.push('');
121
+ }
122
+
123
+ return lines.join('\n');
124
+ }
125
+
126
+ /**
127
+ * Format output based on format option
128
+ */
129
+ function formatOutput(protocol, format) {
130
+ switch (format) {
131
+ case 'markdown':
132
+ case 'md':
133
+ return formatMarkdown(protocol);
134
+
135
+ case 'txt':
136
+ case 'text':
137
+ return formatText(protocol);
138
+
139
+ case 'json':
140
+ default:
141
+ return JSON.stringify(protocol, null, 2);
142
+ }
143
+ }
144
+
145
+ export async function protocolCommand(materialId, options, command) {
146
+ // Check auth
147
+ const apiKey = command?.parent?.opts()?.apiKey || getApiKey();
148
+ if (!apiKey) {
149
+ error('Not authenticated. Run `meetscribe login` first.');
150
+ process.exit(1);
151
+ }
152
+
153
+ const client = new MeetScribeClient({
154
+ apiKey,
155
+ baseUrl: command?.parent?.opts()?.apiUrl || getApiUrl()
156
+ });
157
+
158
+ const spinner = ora('Fetching protocol...').start();
159
+
160
+ try {
161
+ const result = await client.getProtocol(materialId);
162
+ spinner.stop();
163
+
164
+ const protocol = result.data || result;
165
+ const output = formatOutput(protocol, options.format);
166
+
167
+ if (options.output) {
168
+ const outputPath = path.resolve(options.output);
169
+ fs.writeFileSync(outputPath, output);
170
+ console.log();
171
+ success(`Protocol saved to: ${outputPath}`);
172
+ } else {
173
+ console.log();
174
+ console.log(output);
175
+ }
176
+
177
+ } catch (err) {
178
+ spinner.fail('Failed to fetch protocol');
179
+ error(err.message);
180
+ process.exit(1);
181
+ }
182
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Status command - check job status
3
+ */
4
+
5
+ import chalk from 'chalk';
6
+ import ora from 'ora';
7
+
8
+ import { getApiKey, getApiUrl } from '../utils/config.js';
9
+ import { MeetScribeClient } from '../../sdk/client.js';
10
+ import { error, json, statusBadge, formatDuration } from '../utils/output.js';
11
+
12
+ export async function statusCommand(jobId, options, command) {
13
+ // Check auth
14
+ const apiKey = command?.parent?.opts()?.apiKey || getApiKey();
15
+ if (!apiKey) {
16
+ error('Not authenticated. Run `meetscribe login` first.');
17
+ process.exit(1);
18
+ }
19
+
20
+ const client = new MeetScribeClient({
21
+ apiKey,
22
+ baseUrl: command?.parent?.opts()?.apiUrl || getApiUrl()
23
+ });
24
+
25
+ const spinner = ora('Fetching status...').start();
26
+
27
+ try {
28
+ const result = await client.getJobStatus(jobId);
29
+ spinner.stop();
30
+
31
+ const data = result.data || result;
32
+
33
+ console.log();
34
+ console.log(chalk.bold('Job Status'));
35
+ console.log();
36
+ console.log(` ID: ${chalk.cyan(jobId)}`);
37
+ console.log(` Status: ${statusBadge(data.status)}`);
38
+
39
+ if (data.progress !== undefined) {
40
+ console.log(` Progress: ${data.progress}%`);
41
+ }
42
+
43
+ if (data.status_details) {
44
+ console.log(` Details: ${data.status_details}`);
45
+ }
46
+
47
+ if (data.duration) {
48
+ console.log(` Duration: ${formatDuration(data.duration)}`);
49
+ }
50
+
51
+ if (data.error) {
52
+ console.log(` Error: ${chalk.red(data.error)}`);
53
+ }
54
+
55
+ console.log();
56
+
57
+ } catch (err) {
58
+ spinner.fail('Failed to fetch status');
59
+ error(err.message);
60
+ process.exit(1);
61
+ }
62
+ }
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Transcribe command - upload and transcribe audio/video files
3
+ */
4
+
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import chalk from 'chalk';
8
+ import ora from 'ora';
9
+
10
+ import { getApiKey, getApiUrl } from '../utils/config.js';
11
+ import { MeetScribeClient } from '../../sdk/client.js';
12
+ import { success, error, info, json, formatDuration } from '../utils/output.js';
13
+
14
+ /**
15
+ * Format transcript to different output formats
16
+ */
17
+ function formatOutput(result, format) {
18
+ const data = result.data || result;
19
+
20
+ switch (format) {
21
+ case 'json':
22
+ return JSON.stringify(data, null, 2);
23
+
24
+ case 'txt':
25
+ return data.transcript || data.transcription || data.text || '';
26
+
27
+ case 'srt':
28
+ return formatSrt(data.segments || []);
29
+
30
+ case 'vtt':
31
+ return formatVtt(data.segments || []);
32
+
33
+ default:
34
+ return JSON.stringify(data, null, 2);
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Format to SRT subtitle format
40
+ */
41
+ function formatSrt(segments) {
42
+ if (!segments || segments.length === 0) {
43
+ return '1\n00:00:00,000 --> 00:00:01,000\n(No segments available)\n';
44
+ }
45
+
46
+ return segments.map((seg, i) => {
47
+ const start = formatTimeSrt(seg.start || seg.start_time || 0);
48
+ const end = formatTimeSrt(seg.end || seg.end_time || 0);
49
+ const text = seg.text || seg.content || '';
50
+
51
+ return `${i + 1}\n${start} --> ${end}\n${text}\n`;
52
+ }).join('\n');
53
+ }
54
+
55
+ /**
56
+ * Format to VTT subtitle format
57
+ */
58
+ function formatVtt(segments) {
59
+ if (!segments || segments.length === 0) {
60
+ return 'WEBVTT\n\n00:00:00.000 --> 00:00:01.000\n(No segments available)\n';
61
+ }
62
+
63
+ const lines = ['WEBVTT', ''];
64
+
65
+ segments.forEach((seg) => {
66
+ const start = formatTimeVtt(seg.start || seg.start_time || 0);
67
+ const end = formatTimeVtt(seg.end || seg.end_time || 0);
68
+ const text = seg.text || seg.content || '';
69
+
70
+ lines.push(`${start} --> ${end}`);
71
+ lines.push(text);
72
+ lines.push('');
73
+ });
74
+
75
+ return lines.join('\n');
76
+ }
77
+
78
+ /**
79
+ * Format seconds to SRT time format (HH:MM:SS,mmm)
80
+ */
81
+ function formatTimeSrt(seconds) {
82
+ const h = Math.floor(seconds / 3600);
83
+ const m = Math.floor((seconds % 3600) / 60);
84
+ const s = Math.floor(seconds % 60);
85
+ const ms = Math.floor((seconds % 1) * 1000);
86
+
87
+ return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')},${ms.toString().padStart(3, '0')}`;
88
+ }
89
+
90
+ /**
91
+ * Format seconds to VTT time format (HH:MM:SS.mmm)
92
+ */
93
+ function formatTimeVtt(seconds) {
94
+ const h = Math.floor(seconds / 3600);
95
+ const m = Math.floor((seconds % 3600) / 60);
96
+ const s = Math.floor(seconds % 60);
97
+ const ms = Math.floor((seconds % 1) * 1000);
98
+
99
+ return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}.${ms.toString().padStart(3, '0')}`;
100
+ }
101
+
102
+ /**
103
+ * Main transcribe command handler
104
+ */
105
+ export async function transcribeCommand(file, options, command) {
106
+ // Check auth
107
+ const apiKey = command?.parent?.opts()?.apiKey || getApiKey();
108
+ if (!apiKey) {
109
+ error('Not authenticated. Run `meetscribe login` first.');
110
+ process.exit(1);
111
+ }
112
+
113
+ // Check file exists
114
+ const filePath = path.resolve(file);
115
+ if (!fs.existsSync(filePath)) {
116
+ error(`File not found: ${filePath}`);
117
+ process.exit(1);
118
+ }
119
+
120
+ const stat = fs.statSync(filePath);
121
+ const fileSizeMB = (stat.size / (1024 * 1024)).toFixed(2);
122
+ const fileName = path.basename(filePath);
123
+
124
+ console.log();
125
+ info(`Transcribing: ${fileName} (${fileSizeMB} MB)`);
126
+ console.log();
127
+
128
+ const client = new MeetScribeClient({
129
+ apiKey,
130
+ baseUrl: command?.parent?.opts()?.apiUrl || getApiUrl()
131
+ });
132
+
133
+ // Upload file
134
+ const uploadSpinner = ora('Uploading file...').start();
135
+
136
+ let uploadProgress = 0;
137
+ const onProgress = (progressEvent) => {
138
+ if (progressEvent.total) {
139
+ const percent = Math.round((progressEvent.loaded / progressEvent.total) * 100);
140
+ if (percent > uploadProgress) {
141
+ uploadProgress = percent;
142
+ uploadSpinner.text = `Uploading file... ${percent}%`;
143
+ }
144
+ }
145
+ };
146
+
147
+ try {
148
+ const response = await client.transcribe(filePath, {
149
+ language: options.language,
150
+ speakers: options.speakers ? parseInt(options.speakers) : undefined,
151
+ onProgress
152
+ });
153
+
154
+ uploadSpinner.succeed('File uploaded');
155
+
156
+ const jobId = response.data?.job_id || response.job_id;
157
+
158
+ if (!jobId) {
159
+ error('Server did not return job ID');
160
+ console.log(chalk.dim(JSON.stringify(response, null, 2)));
161
+ process.exit(1);
162
+ }
163
+
164
+ // If --no-wait, just return job ID
165
+ if (options.wait === false) {
166
+ console.log();
167
+ success(`Job created: ${jobId}`);
168
+ console.log();
169
+ console.log(chalk.dim('Check status with:'));
170
+ console.log(chalk.cyan(` meetscribe status ${jobId}`));
171
+ console.log();
172
+ return;
173
+ }
174
+
175
+ // Wait for completion
176
+ const waitSpinner = ora('Processing transcription...').start();
177
+ let lastStatus = '';
178
+
179
+ try {
180
+ const result = await client.waitForJob(jobId, {
181
+ pollInterval: 3000,
182
+ onProgress: (status) => {
183
+ const statusText = status.data?.status_details || status.data?.status || 'processing';
184
+ if (statusText !== lastStatus) {
185
+ lastStatus = statusText;
186
+ waitSpinner.text = `Processing: ${statusText}`;
187
+ }
188
+ }
189
+ });
190
+
191
+ waitSpinner.succeed('Transcription complete');
192
+
193
+ // Format output
194
+ const output = formatOutput(result, options.format);
195
+
196
+ // Save to file or print
197
+ if (options.output) {
198
+ const outputPath = path.resolve(options.output);
199
+ fs.writeFileSync(outputPath, output);
200
+ console.log();
201
+ success(`Saved to: ${outputPath}`);
202
+ } else {
203
+ console.log();
204
+ console.log(output);
205
+ }
206
+
207
+ // Show summary
208
+ const data = result.data || result;
209
+ if (data.duration) {
210
+ console.log();
211
+ console.log(chalk.dim(`Duration: ${formatDuration(data.duration)}`));
212
+ }
213
+
214
+ } catch (err) {
215
+ waitSpinner.fail('Transcription failed');
216
+ error(err.message);
217
+ process.exit(1);
218
+ }
219
+
220
+ } catch (err) {
221
+ uploadSpinner.fail('Upload failed');
222
+ error(err.message);
223
+
224
+ if (err.code === 'ECONNREFUSED') {
225
+ console.log();
226
+ info('Cannot connect to API server. Check your internet connection or API URL.');
227
+ }
228
+
229
+ process.exit(1);
230
+ }
231
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Whoami command - show current user info
3
+ */
4
+
5
+ import chalk from 'chalk';
6
+ import ora from 'ora';
7
+
8
+ import { getApiKey, getApiUrl, isAuthenticated } from '../utils/config.js';
9
+ import { MeetScribeClient } from '../../sdk/client.js';
10
+ import { error, info } from '../utils/output.js';
11
+
12
+ export async function whoamiCommand(options, command) {
13
+ if (!isAuthenticated()) {
14
+ console.log();
15
+ info('Not logged in');
16
+ console.log();
17
+ console.log(chalk.dim('Run `meetscribe login` to authenticate'));
18
+ console.log();
19
+ return;
20
+ }
21
+
22
+ const apiKey = command?.parent?.opts()?.apiKey || getApiKey();
23
+
24
+ const client = new MeetScribeClient({
25
+ apiKey,
26
+ baseUrl: command?.parent?.opts()?.apiUrl || getApiUrl()
27
+ });
28
+
29
+ const spinner = ora('Fetching account info...').start();
30
+
31
+ try {
32
+ const result = await client.getAccount();
33
+ spinner.stop();
34
+
35
+ const account = result.data || result;
36
+
37
+ console.log();
38
+ console.log(chalk.bold('Account Info'));
39
+ console.log();
40
+ console.log(` Username: ${chalk.cyan(account.username || account.name || '-')}`);
41
+
42
+ if (account.email) {
43
+ console.log(` Email: ${account.email}`);
44
+ }
45
+
46
+ if (account.telegram_id) {
47
+ console.log(` Telegram: ${account.telegram_id}`);
48
+ }
49
+
50
+ if (account.plan) {
51
+ console.log(` Plan: ${account.plan}`);
52
+ }
53
+
54
+ // Show API key info
55
+ const maskedKey = apiKey.slice(0, 8) + '...' + apiKey.slice(-4);
56
+ console.log(` API Key: ${chalk.dim(maskedKey)}`);
57
+
58
+ console.log();
59
+
60
+ // Show usage if available
61
+ if (account.usage) {
62
+ console.log(chalk.bold('Usage'));
63
+ console.log();
64
+ console.log(` Today: ${account.usage.today || 0} requests`);
65
+ console.log(` Total: ${account.usage.total || 0} requests`);
66
+ console.log();
67
+ }
68
+
69
+ } catch (err) {
70
+ spinner.fail('Failed to fetch account info');
71
+ error(err.message);
72
+
73
+ // Show basic info anyway
74
+ console.log();
75
+ const maskedKey = apiKey.slice(0, 8) + '...' + apiKey.slice(-4);
76
+ console.log(` API Key: ${chalk.dim(maskedKey)}`);
77
+ console.log();
78
+ }
79
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * MeetScribe CLI Entry Point
3
+ */
4
+
5
+ import { Command } from 'commander';
6
+ import chalk from 'chalk';
7
+
8
+ import { loginCommand } from './commands/login.js';
9
+ import { configCommand } from './commands/config.js';
10
+ import { transcribeCommand } from './commands/transcribe.js';
11
+ import { statusCommand } from './commands/status.js';
12
+ import { listCommand } from './commands/list.js';
13
+ import { protocolCommand } from './commands/protocol.js';
14
+ import { whoamiCommand } from './commands/whoami.js';
15
+
16
+ const VERSION = '1.0.0';
17
+
18
+ export function cli() {
19
+ const program = new Command();
20
+
21
+ program
22
+ .name('meetscribe')
23
+ .description('MeetScribe CLI - Meeting transcription and protocol generation')
24
+ .version(VERSION)
25
+ .option('--api-key <key>', 'API key (overrides config)')
26
+ .option('--api-url <url>', 'API URL (overrides config)');
27
+
28
+ // Auth commands
29
+ program
30
+ .command('login')
31
+ .description('Authenticate with MeetScribe')
32
+ .option('--device', 'Use device flow (for servers without browser)')
33
+ .option('--key', 'Enter API key manually')
34
+ .action(loginCommand);
35
+
36
+ program
37
+ .command('logout')
38
+ .description('Remove saved API key')
39
+ .action(async () => {
40
+ const { clearConfig } = await import('./utils/config.js');
41
+ clearConfig();
42
+ console.log(chalk.green('✓'), 'Logged out successfully');
43
+ });
44
+
45
+ program
46
+ .command('whoami')
47
+ .description('Show current user info')
48
+ .action(whoamiCommand);
49
+
50
+ // Config commands
51
+ const config = program
52
+ .command('config')
53
+ .description('Manage configuration');
54
+
55
+ config
56
+ .command('show')
57
+ .description('Show current configuration')
58
+ .action(() => configCommand('show'));
59
+
60
+ config
61
+ .command('set-key <key>')
62
+ .description('Set API key')
63
+ .action((key) => configCommand('set-key', key));
64
+
65
+ config
66
+ .command('set-url <url>')
67
+ .description('Set API URL')
68
+ .action((url) => configCommand('set-url', url));
69
+
70
+ config
71
+ .command('set-language <lang>')
72
+ .description('Set default language (ru, en, auto)')
73
+ .action((lang) => configCommand('set-language', lang));
74
+
75
+ config
76
+ .command('reset')
77
+ .description('Reset configuration to defaults')
78
+ .action(() => configCommand('reset'));
79
+
80
+ // Main commands
81
+ program
82
+ .command('transcribe <file>')
83
+ .description('Transcribe audio/video file')
84
+ .option('-l, --language <lang>', 'Language (ru, en, auto)', 'auto')
85
+ .option('-s, --speakers <num>', 'Number of speakers for diarization')
86
+ .option('-o, --output <file>', 'Output file path')
87
+ .option('-f, --format <format>', 'Output format (json, txt, srt, vtt)', 'json')
88
+ .option('--no-wait', 'Return job ID immediately without waiting')
89
+ .action(transcribeCommand);
90
+
91
+ program
92
+ .command('status <jobId>')
93
+ .description('Check job status')
94
+ .action(statusCommand);
95
+
96
+ program
97
+ .command('list')
98
+ .alias('ls')
99
+ .description('List transcribed materials')
100
+ .option('-n, --limit <num>', 'Number of items', '20')
101
+ .option('--status <status>', 'Filter by status (pending, processing, completed, error)')
102
+ .option('-f, --format <format>', 'Output format (table, json)', 'table')
103
+ .action(listCommand);
104
+
105
+ program
106
+ .command('protocol <materialId>')
107
+ .description('Get meeting protocol')
108
+ .option('-f, --format <format>', 'Output format (json, markdown, txt)', 'json')
109
+ .option('-o, --output <file>', 'Output file path')
110
+ .action(protocolCommand);
111
+
112
+ // Parse and run
113
+ program.parse();
114
+ }