ff1-cli 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.
package/dist/index.js ADDED
@@ -0,0 +1,627 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ var __importDefault = (this && this.__importDefault) || function (mod) {
37
+ return (mod && mod.__esModule) ? mod : { "default": mod };
38
+ };
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ // Suppress punycode deprecation warning from jsdom dependency
41
+ process.removeAllListeners('warning');
42
+ process.on('warning', (warning) => {
43
+ if (warning.name === 'DeprecationWarning' && warning.message.includes('punycode')) {
44
+ return; // Ignore punycode deprecation warnings from dependencies
45
+ }
46
+ console.warn(warning.name + ': ' + warning.message);
47
+ });
48
+ require("dotenv/config");
49
+ const commander_1 = require("commander");
50
+ const chalk_1 = __importDefault(require("chalk"));
51
+ const fs_1 = require("fs");
52
+ const readline = __importStar(require("readline"));
53
+ const config_1 = require("./src/config");
54
+ const main_1 = require("./src/main");
55
+ const program = new commander_1.Command();
56
+ /**
57
+ * Display playlist creation summary with next steps.
58
+ *
59
+ * @param {Playlist} playlist - The created playlist object
60
+ * @param {string} outputPath - Path where the playlist was saved
61
+ */
62
+ function displayPlaylistSummary(playlist, outputPath) {
63
+ console.log(chalk_1.default.green('\n✅ Playlist created!'));
64
+ console.log();
65
+ console.log(chalk_1.default.bold('Next steps:'));
66
+ console.log(chalk_1.default.gray(` • View it locally: open ./${outputPath}`));
67
+ console.log(chalk_1.default.gray(` • Send it to your FF1: send last`));
68
+ console.log(chalk_1.default.gray(` • Publish to feed: publish playlist`));
69
+ console.log();
70
+ }
71
+ program
72
+ .name('ff1')
73
+ .description('CLI to fetch NFT information and build DP1 playlists using AI (Grok, ChatGPT, Gemini)')
74
+ .version('1.0.0');
75
+ program
76
+ .command('chat')
77
+ .description('Start an interactive chat to build playlists using natural language')
78
+ .argument('[content]', 'Optional: Direct chat content (non-interactive mode)')
79
+ .option('-o, --output <filename>', 'Output filename for the playlist', 'playlist.json')
80
+ .option('-m, --model <name>', 'AI model to use (grok, chatgpt, gemini) - defaults to config setting')
81
+ .option('-v, --verbose', 'Show detailed technical output of function calls', false)
82
+ .action(async (content, options) => {
83
+ try {
84
+ // Load and validate configuration
85
+ const config = (0, config_1.getConfig)();
86
+ const availableModels = (0, config_1.listAvailableModels)();
87
+ // Validate model selection
88
+ if (options.model && !availableModels.includes(options.model)) {
89
+ console.error(chalk_1.default.red(`❌ Invalid model: "${options.model}"`));
90
+ console.log(chalk_1.default.yellow(`\nAvailable models: ${availableModels.join(', ')}`));
91
+ process.exit(1);
92
+ }
93
+ const modelName = options.model || config.defaultModel;
94
+ const validation = (0, config_1.validateConfig)(modelName);
95
+ if (!validation.valid) {
96
+ console.error(chalk_1.default.red('❌ Configuration Error:'));
97
+ validation.errors.forEach((error) => {
98
+ console.error(chalk_1.default.red(` • ${error}`));
99
+ });
100
+ console.log(chalk_1.default.yellow('\nRun: node index.js config init\n'));
101
+ process.exit(1);
102
+ }
103
+ // NON-INTERACTIVE MODE: If content is provided as argument
104
+ if (content) {
105
+ console.log(chalk_1.default.blue('\n💬 FF1 Playlist Chat (Non-Interactive Mode)\n'));
106
+ console.log(chalk_1.default.gray(`🤖 Using AI model: ${modelName}\n`));
107
+ console.log(chalk_1.default.yellow('Request:'), content);
108
+ console.log(); // Blank line
109
+ try {
110
+ const result = await (0, main_1.buildPlaylist)(content, {
111
+ verbose: options.verbose,
112
+ outputPath: options.output,
113
+ modelName: modelName,
114
+ interactive: false, // Non-interactive mode
115
+ });
116
+ // Print final summary
117
+ if (result && result.playlist) {
118
+ console.log(chalk_1.default.green('\n✅ Playlist Created Successfully!'));
119
+ console.log(chalk_1.default.gray(` Title: ${result.playlist.title}`));
120
+ console.log(chalk_1.default.gray(` Items: ${result.playlist.items?.length || 0}`));
121
+ console.log(chalk_1.default.gray(` Output: ${options.output}\n`));
122
+ }
123
+ process.exit(0);
124
+ }
125
+ catch (error) {
126
+ console.error(chalk_1.default.red('\n❌ Error:'), error.message);
127
+ if (options.verbose) {
128
+ console.error(chalk_1.default.gray(error.stack));
129
+ }
130
+ process.exit(1);
131
+ }
132
+ }
133
+ // INTERACTIVE MODE: Start conversation loop
134
+ console.log(chalk_1.default.blue('\n💬 Welcome to FF1 Playlist Chat!\n'));
135
+ console.log(chalk_1.default.gray('Tell me what playlist you want to create.'));
136
+ console.log(chalk_1.default.gray('Press Ctrl+C to exit.\n'));
137
+ console.log(chalk_1.default.gray(`🤖 Using AI model: ${modelName}\n`));
138
+ console.log(chalk_1.default.gray('Examples:'));
139
+ console.log(chalk_1.default.gray(' - "Get tokens 1,2,3 from Ethereum contract 0xabc"'));
140
+ console.log(chalk_1.default.gray(' - "Get token 42 from Tezos contract KT1abc"'));
141
+ console.log(chalk_1.default.gray(' - "Get 3 from Social Codes and 2 from 0xdef"'));
142
+ console.log(chalk_1.default.gray(' - "Build a playlist of my Tezos works from address tz1... plus 3 from Social Codes"'));
143
+ console.log(chalk_1.default.gray(' (Tip) Add -v to see tool calls'));
144
+ console.log();
145
+ const rl = readline.createInterface({
146
+ input: process.stdin,
147
+ output: process.stdout,
148
+ historySize: 100,
149
+ });
150
+ let closed = false;
151
+ rl.on('close', () => {
152
+ closed = true;
153
+ });
154
+ rl.on('SIGINT', () => {
155
+ rl.close();
156
+ });
157
+ const ask = async () => new Promise((resolve) => {
158
+ if (closed) {
159
+ resolve('');
160
+ return;
161
+ }
162
+ rl.question(chalk_1.default.yellow('You: '), (answer) => {
163
+ resolve(answer.trim());
164
+ });
165
+ });
166
+ // Continuous conversation loop
167
+ while (!closed) {
168
+ const userInput = await ask();
169
+ if (closed) {
170
+ break;
171
+ }
172
+ if (!userInput) {
173
+ continue; // Skip empty input
174
+ }
175
+ console.log(); // Blank line before AI response
176
+ try {
177
+ const result = await (0, main_1.buildPlaylist)(userInput, {
178
+ verbose: options.verbose,
179
+ outputPath: options.output,
180
+ modelName: modelName,
181
+ });
182
+ // Print summary after each response
183
+ // Only show playlist summary for build operations, not send operations
184
+ // Skip summary if playlist was already sent to device
185
+ if (options.verbose) {
186
+ console.log(chalk_1.default.gray(`\n[DEBUG] result.sentToDevice: ${result?.sentToDevice}`));
187
+ console.log(chalk_1.default.gray(`[DEBUG] result.action: ${result?.action}`));
188
+ }
189
+ if (result &&
190
+ result.playlist &&
191
+ result.action !== 'send_playlist' &&
192
+ !result.sentToDevice) {
193
+ displayPlaylistSummary(result.playlist, options.output);
194
+ }
195
+ }
196
+ catch (error) {
197
+ console.error(chalk_1.default.red('❌ Error:'), error.message);
198
+ if (options.verbose) {
199
+ console.error(chalk_1.default.gray(error.stack));
200
+ }
201
+ console.log(); // Blank line after error
202
+ }
203
+ }
204
+ if (closed) {
205
+ throw new Error('readline was closed');
206
+ }
207
+ }
208
+ catch (error) {
209
+ if (error.message !== 'readline was closed') {
210
+ console.error(chalk_1.default.red('\n❌ Error:'), error.message);
211
+ if (process.env.DEBUG) {
212
+ console.error(chalk_1.default.gray(error.stack));
213
+ }
214
+ }
215
+ console.log(chalk_1.default.blue('\n👋 Goodbye!\n'));
216
+ process.exit(0);
217
+ }
218
+ });
219
+ program
220
+ .command('verify')
221
+ .description('Verify a DP1 playlist file against DP-1 specification')
222
+ .argument('<file>', 'Path to the playlist file')
223
+ .action(async (file) => {
224
+ try {
225
+ console.log(chalk_1.default.blue('\n🔍 Verifying playlist...\n'));
226
+ // Import the verification utility
227
+ const verifier = await Promise.resolve().then(() => __importStar(require('./src/utilities/playlist-verifier')));
228
+ const { verifyPlaylistFile, printVerificationResult } = verifier;
229
+ // Verify the playlist
230
+ const result = await verifyPlaylistFile(file);
231
+ // Print results
232
+ printVerificationResult(result, file);
233
+ if (!result.valid) {
234
+ process.exit(1);
235
+ }
236
+ }
237
+ catch (error) {
238
+ console.error(chalk_1.default.red('\n❌ Error:'), error.message);
239
+ process.exit(1);
240
+ }
241
+ });
242
+ program
243
+ .command('validate')
244
+ .description('Validate a DP1 playlist file (alias for verify)')
245
+ .argument('<file>', 'Path to the playlist file')
246
+ .action(async (file) => {
247
+ try {
248
+ console.log(chalk_1.default.blue('\n🔍 Verifying playlist...\n'));
249
+ // Import the verification utility
250
+ const verifier = await Promise.resolve().then(() => __importStar(require('./src/utilities/playlist-verifier')));
251
+ const { verifyPlaylistFile, printVerificationResult } = verifier;
252
+ // Verify the playlist
253
+ const result = await verifyPlaylistFile(file);
254
+ // Print results
255
+ printVerificationResult(result, file);
256
+ if (!result.valid) {
257
+ process.exit(1);
258
+ }
259
+ }
260
+ catch (error) {
261
+ console.error(chalk_1.default.red('\n❌ Error:'), error.message);
262
+ process.exit(1);
263
+ }
264
+ });
265
+ program
266
+ .command('sign')
267
+ .description('Sign a DP1 playlist file with Ed25519 signature')
268
+ .argument('<file>', 'Path to the playlist file to sign')
269
+ .option('-k, --key <privateKey>', 'Ed25519 private key in base64 format (overrides config)')
270
+ .option('-o, --output <file>', 'Output file path (defaults to overwriting input file)')
271
+ .action(async (file, options) => {
272
+ try {
273
+ console.log(chalk_1.default.blue('\n🔏 Signing playlist...\n'));
274
+ // Import the signing utility
275
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
276
+ const { signPlaylistFile } = require('./src/utilities/playlist-signer');
277
+ // Sign the playlist
278
+ const result = await signPlaylistFile(file, options.key, options.output);
279
+ if (result.success) {
280
+ console.log(chalk_1.default.green('\n✅ Playlist signed successfully!'));
281
+ if (result.playlist?.signature) {
282
+ console.log(chalk_1.default.gray(` Signature: ${result.playlist.signature.substring(0, 30)}...`));
283
+ }
284
+ console.log();
285
+ }
286
+ else {
287
+ console.error(chalk_1.default.red('\n❌ Failed to sign playlist:'), result.error);
288
+ process.exit(1);
289
+ }
290
+ }
291
+ catch (error) {
292
+ console.error(chalk_1.default.red('\n❌ Error:'), error.message);
293
+ process.exit(1);
294
+ }
295
+ });
296
+ program
297
+ .command('play')
298
+ .description('Play a media URL on an FF1 device')
299
+ .argument('<url>', 'Media URL to play')
300
+ .option('-d, --device <name>', 'Device name (uses first device if not specified)')
301
+ .option('--skip-verify', 'Skip playlist verification before sending')
302
+ .action(async (url, options) => {
303
+ try {
304
+ console.log(chalk_1.default.blue('\n▶️ Playing URL on FF1 device...\n'));
305
+ try {
306
+ new URL(url);
307
+ }
308
+ catch (error) {
309
+ console.error(chalk_1.default.red('\n❌ Invalid URL:'), error.message);
310
+ process.exit(1);
311
+ }
312
+ const config = (0, config_1.getConfig)();
313
+ const duration = config.defaultDuration || 10;
314
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
315
+ const { buildUrlItem, buildDP1Playlist } = require('./src/utilities/playlist-builder');
316
+ const item = buildUrlItem(url, duration);
317
+ const playlist = await buildDP1Playlist({ items: [item], title: item.title });
318
+ if (!options.skipVerify) {
319
+ const verifier = await Promise.resolve().then(() => __importStar(require('./src/utilities/playlist-verifier')));
320
+ const { verifyPlaylist } = verifier;
321
+ const verifyResult = verifyPlaylist(playlist);
322
+ if (!verifyResult.valid) {
323
+ console.error(chalk_1.default.red('\n❌ Playlist verification failed:'), verifyResult.error);
324
+ if (verifyResult.details && verifyResult.details.length > 0) {
325
+ console.log(chalk_1.default.yellow('\n Validation errors:'));
326
+ verifyResult.details.forEach((detail) => {
327
+ console.log(chalk_1.default.yellow(` • ${detail.path}: ${detail.message}`));
328
+ });
329
+ }
330
+ console.log(chalk_1.default.yellow('\n Use --skip-verify to send anyway (not recommended)\n'));
331
+ process.exit(1);
332
+ }
333
+ }
334
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
335
+ const { sendPlaylistToDevice } = require('./src/utilities/ff1-device');
336
+ const result = await sendPlaylistToDevice({
337
+ playlist,
338
+ deviceName: options.device,
339
+ });
340
+ if (result.success) {
341
+ console.log(chalk_1.default.green('✅ URL sent successfully!'));
342
+ if (result.deviceName) {
343
+ console.log(chalk_1.default.gray(` Device: ${result.deviceName}`));
344
+ }
345
+ if (result.device) {
346
+ console.log(chalk_1.default.gray(` Host: ${result.device}`));
347
+ }
348
+ console.log();
349
+ }
350
+ else {
351
+ console.error(chalk_1.default.red('\n❌ Failed to send URL:'), result.error);
352
+ if (result.details) {
353
+ console.error(chalk_1.default.gray(` Details: ${result.details}`));
354
+ }
355
+ process.exit(1);
356
+ }
357
+ }
358
+ catch (error) {
359
+ console.error(chalk_1.default.red('\n❌ Error:'), error.message);
360
+ process.exit(1);
361
+ }
362
+ });
363
+ program
364
+ .command('send')
365
+ .description('Send a playlist file to an FF1 device')
366
+ .argument('<file>', 'Path to the playlist file')
367
+ .option('-d, --device <name>', 'Device name (uses first device if not specified)')
368
+ .option('--skip-verify', 'Skip playlist verification before sending')
369
+ .action(async (file, options) => {
370
+ try {
371
+ console.log(chalk_1.default.blue('\n📤 Sending playlist to FF1 device...\n'));
372
+ // Read the playlist file
373
+ const content = await fs_1.promises.readFile(file, 'utf-8');
374
+ const playlist = JSON.parse(content);
375
+ // Verify playlist before sending (unless skipped)
376
+ if (!options.skipVerify) {
377
+ console.log(chalk_1.default.cyan('🔍 Verifying playlist...'));
378
+ const verifier = await Promise.resolve().then(() => __importStar(require('./src/utilities/playlist-verifier')));
379
+ const { verifyPlaylist } = verifier;
380
+ const verifyResult = verifyPlaylist(playlist);
381
+ if (!verifyResult.valid) {
382
+ console.error(chalk_1.default.red('\n❌ Playlist verification failed:'), verifyResult.error);
383
+ if (verifyResult.details && verifyResult.details.length > 0) {
384
+ console.log(chalk_1.default.yellow('\n Validation errors:'));
385
+ verifyResult.details.forEach((detail) => {
386
+ console.log(chalk_1.default.yellow(` • ${detail.path}: ${detail.message}`));
387
+ });
388
+ }
389
+ console.log(chalk_1.default.yellow('\n Use --skip-verify to send anyway (not recommended)\n'));
390
+ process.exit(1);
391
+ }
392
+ console.log(chalk_1.default.green('✓ Playlist verified successfully\n'));
393
+ }
394
+ // Import the sending utility
395
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
396
+ const { sendPlaylistToDevice } = require('./src/utilities/ff1-device');
397
+ // Send the playlist
398
+ const result = await sendPlaylistToDevice({
399
+ playlist,
400
+ deviceName: options.device,
401
+ });
402
+ if (result.success) {
403
+ console.log(chalk_1.default.green('✅ Playlist sent successfully!'));
404
+ if (result.deviceName) {
405
+ console.log(chalk_1.default.gray(` Device: ${result.deviceName}`));
406
+ }
407
+ if (result.device) {
408
+ console.log(chalk_1.default.gray(` Host: ${result.device}`));
409
+ }
410
+ console.log();
411
+ }
412
+ else {
413
+ console.error(chalk_1.default.red('\n❌ Failed to send playlist:'), result.error);
414
+ if (result.details) {
415
+ console.error(chalk_1.default.gray(` Details: ${result.details}`));
416
+ }
417
+ process.exit(1);
418
+ }
419
+ }
420
+ catch (error) {
421
+ console.error(chalk_1.default.red('\n❌ Error:'), error.message);
422
+ process.exit(1);
423
+ }
424
+ });
425
+ program
426
+ .command('publish')
427
+ .description('Publish a playlist to a feed server')
428
+ .argument('<file>', 'Path to the playlist file')
429
+ .option('-s, --server <index>', 'Feed server index (use this if multiple servers configured)')
430
+ .action(async (file, options) => {
431
+ try {
432
+ console.log(chalk_1.default.blue('\n📡 Publishing playlist to feed server...\n'));
433
+ const { getFeedConfig } = await Promise.resolve().then(() => __importStar(require('./src/config')));
434
+ const { publishPlaylist } = await Promise.resolve().then(() => __importStar(require('./src/utilities/playlist-publisher')));
435
+ const feedConfig = getFeedConfig();
436
+ if (!feedConfig.baseURLs || feedConfig.baseURLs.length === 0) {
437
+ console.error(chalk_1.default.red('\n❌ No feed servers configured'));
438
+ console.log(chalk_1.default.yellow(' Add feed server URLs to config.json: feed.baseURLs\n'));
439
+ process.exit(1);
440
+ }
441
+ // If multiple servers and no index specified, show options
442
+ let serverUrl = feedConfig.baseURLs[0];
443
+ let serverApiKey = feedConfig.apiKey; // Default to legacy apiKey
444
+ if (feedConfig.baseURLs.length > 1) {
445
+ if (!options.server) {
446
+ console.log(chalk_1.default.yellow('Multiple feed servers found. Select one:'));
447
+ console.log();
448
+ feedConfig.baseURLs.forEach((url, index) => {
449
+ console.log(chalk_1.default.cyan(` ${index}: ${url}`));
450
+ });
451
+ console.log();
452
+ const rl = readline.createInterface({
453
+ input: process.stdin,
454
+ output: process.stdout,
455
+ });
456
+ const selection = await new Promise((resolve) => {
457
+ rl.question(chalk_1.default.yellow('Select server (0-based index): '), (answer) => {
458
+ rl.close();
459
+ resolve(answer.trim());
460
+ });
461
+ });
462
+ console.log();
463
+ options.server = selection;
464
+ }
465
+ const serverIndex = parseInt(options.server || '0', 10);
466
+ if (isNaN(serverIndex) || serverIndex < 0 || serverIndex >= feedConfig.baseURLs.length) {
467
+ console.error(chalk_1.default.red('\n❌ Invalid server index'));
468
+ process.exit(1);
469
+ }
470
+ serverUrl = feedConfig.baseURLs[serverIndex];
471
+ // Use individual server API key if available (new feedServers format)
472
+ if (feedConfig.servers && feedConfig.servers[serverIndex]) {
473
+ serverApiKey = feedConfig.servers[serverIndex].apiKey;
474
+ }
475
+ }
476
+ else if (feedConfig.servers && feedConfig.servers[0]) {
477
+ // Single server with new feedServers format
478
+ serverApiKey = feedConfig.servers[0].apiKey;
479
+ }
480
+ const result = await publishPlaylist(file, serverUrl, serverApiKey);
481
+ if (result.success) {
482
+ console.log(chalk_1.default.green('✅ Playlist published successfully!'));
483
+ if (result.playlistId) {
484
+ console.log(chalk_1.default.gray(` Playlist ID: ${result.playlistId}`));
485
+ }
486
+ console.log(chalk_1.default.gray(` Server: ${result.feedServer}`));
487
+ if (result.message) {
488
+ console.log(chalk_1.default.gray(` Status: ${result.message}`));
489
+ }
490
+ console.log();
491
+ }
492
+ else {
493
+ console.error(chalk_1.default.red('\n❌ Failed to publish playlist'));
494
+ if (result.error) {
495
+ console.error(chalk_1.default.red(` ${result.error}`));
496
+ }
497
+ if (result.message) {
498
+ console.log(chalk_1.default.yellow(`\n${result.message}`));
499
+ }
500
+ console.log();
501
+ process.exit(1);
502
+ }
503
+ }
504
+ catch (error) {
505
+ console.error(chalk_1.default.red('\n❌ Error:'), error.message);
506
+ process.exit(1);
507
+ }
508
+ });
509
+ program
510
+ .command('build')
511
+ .description('Build playlist from structured parameters (JSON file or stdin)')
512
+ .argument('[params-file]', 'Path to JSON parameters file (or use stdin)')
513
+ .option('-o, --output <filename>', 'Output filename for the playlist', 'playlist.json')
514
+ .option('-v, --verbose', 'Show detailed output', false)
515
+ .action(async (paramsFile, options) => {
516
+ try {
517
+ let params;
518
+ if (paramsFile) {
519
+ // Read from file
520
+ const content = await fs_1.promises.readFile(paramsFile, 'utf-8');
521
+ params = JSON.parse(content);
522
+ }
523
+ else {
524
+ // Read from stdin
525
+ const stdin = await new Promise((resolve, reject) => {
526
+ let data = '';
527
+ process.stdin.setEncoding('utf8');
528
+ process.stdin.on('data', (chunk) => {
529
+ data += chunk;
530
+ });
531
+ process.stdin.on('end', () => {
532
+ resolve(data);
533
+ });
534
+ process.stdin.on('error', reject);
535
+ });
536
+ if (!stdin.trim()) {
537
+ console.error(chalk_1.default.red('❌ No parameters provided'));
538
+ console.log(chalk_1.default.yellow('\nUsage:'));
539
+ console.log(' node index.js build params.json');
540
+ console.log(' cat params.json | node index.js build');
541
+ console.log(' echo \'{"requirements":[...]}\' | node index.js build');
542
+ process.exit(1);
543
+ }
544
+ params = JSON.parse(stdin);
545
+ }
546
+ if (options.verbose) {
547
+ console.log(chalk_1.default.blue('\n📋 Parameters:'));
548
+ console.log(chalk_1.default.gray(JSON.stringify(params, null, 2)));
549
+ console.log();
550
+ }
551
+ console.log(chalk_1.default.blue('\n🚀 Building playlist from parameters...\n'));
552
+ const result = await (0, main_1.buildPlaylistDirect)(params, {
553
+ verbose: options.verbose,
554
+ outputPath: options.output,
555
+ });
556
+ if (result && result.playlist) {
557
+ console.log(chalk_1.default.green('\n✅ Playlist Created Successfully!'));
558
+ console.log(chalk_1.default.gray(` Title: ${result.playlist.title}`));
559
+ console.log(chalk_1.default.gray(` Items: ${result.playlist.items?.length || 0}`));
560
+ console.log(chalk_1.default.gray(` Output: ${options.output}\n`));
561
+ }
562
+ }
563
+ catch (error) {
564
+ console.error(chalk_1.default.red('\n❌ Error:'), error.message);
565
+ if (options.verbose) {
566
+ console.error(chalk_1.default.gray(error.stack));
567
+ }
568
+ process.exit(1);
569
+ }
570
+ });
571
+ program
572
+ .command('config')
573
+ .description('Manage configuration')
574
+ .argument('<action>', 'Action: init, show, or validate')
575
+ .action(async (action) => {
576
+ try {
577
+ if (action === 'init') {
578
+ console.log(chalk_1.default.blue('\n🔧 Creating config.json...\n'));
579
+ const configPath = await (0, config_1.createSampleConfig)();
580
+ console.log(chalk_1.default.green(`✓ Created ${configPath}`));
581
+ console.log(chalk_1.default.yellow('\nPlease edit config.json and add your API key.\n'));
582
+ }
583
+ else if (action === 'show') {
584
+ const config = (0, config_1.getConfig)();
585
+ console.log(chalk_1.default.blue('\n⚙️ Current Configuration:\n'));
586
+ console.log(chalk_1.default.bold('Default Model:'), chalk_1.default.white(config.defaultModel));
587
+ console.log(chalk_1.default.bold('Default Duration:'), chalk_1.default.white(config.defaultDuration + 's'));
588
+ console.log(chalk_1.default.bold('\nAvailable Models:\n'));
589
+ const models = (0, config_1.listAvailableModels)();
590
+ models.forEach((modelName) => {
591
+ const modelConfig = config.models[modelName];
592
+ const isCurrent = modelName === config.defaultModel;
593
+ console.log(` ${isCurrent ? chalk_1.default.green('→') : ' '} ${chalk_1.default.bold(modelName)}`);
594
+ console.log(` API Key: ${modelConfig.apiKey && modelConfig.apiKey !== 'your_api_key_here' ? chalk_1.default.green('✓ Set') : chalk_1.default.red('✗ Not set')}`);
595
+ console.log(` Base URL: ${chalk_1.default.gray(modelConfig.baseURL)}`);
596
+ console.log(` Model: ${chalk_1.default.gray(modelConfig.model)}`);
597
+ console.log(` Function Calling: ${modelConfig.supportsFunctionCalling ? chalk_1.default.green('✓ Supported') : chalk_1.default.red('✗ Not supported')}`);
598
+ console.log();
599
+ });
600
+ }
601
+ else if (action === 'validate') {
602
+ const validation = (0, config_1.validateConfig)();
603
+ console.log(chalk_1.default.blue('\n🔍 Validating configuration...\n'));
604
+ if (validation.valid) {
605
+ console.log(chalk_1.default.green('✓ Configuration is valid!\n'));
606
+ }
607
+ else {
608
+ console.log(chalk_1.default.red('✗ Configuration has errors:\n'));
609
+ validation.errors.forEach((error) => {
610
+ console.log(chalk_1.default.red(` • ${error}`));
611
+ });
612
+ console.log();
613
+ process.exit(1);
614
+ }
615
+ }
616
+ else {
617
+ console.error(chalk_1.default.red(`\n❌ Unknown action: ${action}`));
618
+ console.log(chalk_1.default.yellow('Available actions: init, show, validate\n'));
619
+ process.exit(1);
620
+ }
621
+ }
622
+ catch (error) {
623
+ console.error(chalk_1.default.red('\n❌ Error:'), error.message);
624
+ process.exit(1);
625
+ }
626
+ });
627
+ program.parse();