ff1-cli 1.0.0 → 1.0.2

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 CHANGED
@@ -37,7 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  return (mod && mod.__esModule) ? mod : { "default": mod };
38
38
  };
39
39
  Object.defineProperty(exports, "__esModule", { value: true });
40
- // Suppress punycode deprecation warning from jsdom dependency
40
+ // Suppress punycode deprecation warnings from dependencies
41
41
  process.removeAllListeners('warning');
42
42
  process.on('warning', (warning) => {
43
43
  if (warning.name === 'DeprecationWarning' && warning.message.includes('punycode')) {
@@ -49,10 +49,26 @@ require("dotenv/config");
49
49
  const commander_1 = require("commander");
50
50
  const chalk_1 = __importDefault(require("chalk"));
51
51
  const fs_1 = require("fs");
52
+ const crypto_1 = __importDefault(require("crypto"));
52
53
  const readline = __importStar(require("readline"));
54
+ const fs_2 = require("fs");
55
+ const path_1 = require("path");
53
56
  const config_1 = require("./src/config");
54
57
  const main_1 = require("./src/main");
58
+ // Load version from package.json
59
+ // Try built location first (dist/index.js -> ../package.json)
60
+ // Fall back to dev location (index.ts -> ./package.json)
61
+ let packageJsonPath = (0, path_1.resolve)((0, path_1.dirname)(__filename), '..', 'package.json');
62
+ try {
63
+ (0, fs_2.readFileSync)(packageJsonPath, 'utf8');
64
+ }
65
+ catch {
66
+ // Dev mode: tsx runs from project root
67
+ packageJsonPath = (0, path_1.resolve)((0, path_1.dirname)(__filename), 'package.json');
68
+ }
69
+ const { version: packageVersion } = JSON.parse((0, fs_2.readFileSync)(packageJsonPath, 'utf8'));
55
70
  const program = new commander_1.Command();
71
+ const placeholderPattern = /YOUR_|your_/;
56
72
  /**
57
73
  * Display playlist creation summary with next steps.
58
74
  *
@@ -60,24 +76,327 @@ const program = new commander_1.Command();
60
76
  * @param {string} outputPath - Path where the playlist was saved
61
77
  */
62
78
  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`));
79
+ console.log(chalk_1.default.green('\nPlaylist created'));
80
+ console.log(chalk_1.default.dim(` Output: ./${outputPath}`));
81
+ console.log(chalk_1.default.dim(' Next: send last | publish playlist'));
69
82
  console.log();
70
83
  }
84
+ function isMissingConfigValue(value) {
85
+ if (!value) {
86
+ return true;
87
+ }
88
+ return placeholderPattern.test(value);
89
+ }
90
+ async function readConfigFile(configPath) {
91
+ const file = await fs_1.promises.readFile(configPath, 'utf-8');
92
+ return JSON.parse(file);
93
+ }
94
+ async function resolveExistingConfigPath() {
95
+ const { localPath, userPath } = (0, config_1.getConfigPaths)();
96
+ try {
97
+ await fs_1.promises.access(localPath);
98
+ return localPath;
99
+ }
100
+ catch (_error) {
101
+ try {
102
+ await fs_1.promises.access(userPath);
103
+ return userPath;
104
+ }
105
+ catch (_innerError) {
106
+ return null;
107
+ }
108
+ }
109
+ }
110
+ async function ensureConfigFile() {
111
+ const { userPath } = (0, config_1.getConfigPaths)();
112
+ const existingPath = await resolveExistingConfigPath();
113
+ if (existingPath) {
114
+ return { path: existingPath, created: false };
115
+ }
116
+ const createdPath = await (0, config_1.createSampleConfig)(userPath);
117
+ return { path: createdPath, created: true };
118
+ }
119
+ function normalizeDeviceHost(host) {
120
+ let normalized = host.trim();
121
+ if (!normalized) {
122
+ return normalized;
123
+ }
124
+ if (!normalized.startsWith('http://') && !normalized.startsWith('https://')) {
125
+ normalized = `http://${normalized}`;
126
+ }
127
+ try {
128
+ const url = new URL(normalized);
129
+ const port = url.port || '1111';
130
+ return `${url.protocol}//${url.hostname}:${port}`;
131
+ }
132
+ catch (_error) {
133
+ return normalized;
134
+ }
135
+ }
136
+ async function promptYesNo(ask, question, defaultYes = true) {
137
+ const suffix = defaultYes ? 'Y/n' : 'y/N';
138
+ const answer = (await ask(`${question} [${suffix}] `)).trim().toLowerCase();
139
+ if (!answer) {
140
+ return defaultYes;
141
+ }
142
+ return answer === 'y' || answer === 'yes';
143
+ }
71
144
  program
72
145
  .name('ff1')
73
146
  .description('CLI to fetch NFT information and build DP1 playlists using AI (Grok, ChatGPT, Gemini)')
74
- .version('1.0.0');
147
+ .version(packageVersion)
148
+ .addHelpText('after', `\nQuick start:\n 1) ff1 setup\n 2) ff1 chat\n\nDocs: https://github.com/feralfile/ff1-cli\n`);
149
+ program
150
+ .command('setup')
151
+ .description('Guided setup for config, signing key, and device')
152
+ .action(async () => {
153
+ let rl = null;
154
+ try {
155
+ const { path: configPath, created } = await ensureConfigFile();
156
+ if (created) {
157
+ console.log(chalk_1.default.green(`Created ${configPath}`));
158
+ }
159
+ const config = await readConfigFile(configPath);
160
+ const modelNames = Object.keys(config.models || {});
161
+ if (modelNames.length === 0) {
162
+ console.error(chalk_1.default.red('No models found in config.json'));
163
+ process.exit(1);
164
+ }
165
+ console.log(chalk_1.default.blue('\nFF1 Setup\n'));
166
+ rl = readline.createInterface({
167
+ input: process.stdin,
168
+ output: process.stdout,
169
+ });
170
+ const ask = async (question) => new Promise((resolve) => {
171
+ rl.question(chalk_1.default.yellow(question), (answer) => {
172
+ resolve(answer.trim());
173
+ });
174
+ });
175
+ const currentModel = config.defaultModel && modelNames.includes(config.defaultModel)
176
+ ? config.defaultModel
177
+ : modelNames[0];
178
+ let selectedModel = currentModel;
179
+ while (true) {
180
+ const modelAnswer = await ask(`Default model (${modelNames.join(', ')}) [${currentModel}]: `);
181
+ if (!modelAnswer) {
182
+ selectedModel = currentModel;
183
+ break;
184
+ }
185
+ if (modelNames.includes(modelAnswer)) {
186
+ selectedModel = modelAnswer;
187
+ break;
188
+ }
189
+ console.log(chalk_1.default.red(`Unknown model: ${modelAnswer}`));
190
+ }
191
+ config.defaultModel = selectedModel;
192
+ const selectedModelConfig = config.models[selectedModel] || {
193
+ apiKey: '',
194
+ baseURL: '',
195
+ model: '',
196
+ timeout: 0,
197
+ maxRetries: 0,
198
+ temperature: 0,
199
+ maxTokens: 0,
200
+ supportsFunctionCalling: true,
201
+ };
202
+ const hasApiKeyForModel = !isMissingConfigValue(selectedModelConfig.apiKey);
203
+ const keyHelpUrls = {
204
+ grok: 'https://console.x.ai/',
205
+ gpt: 'https://platform.openai.com/api-keys',
206
+ chatgpt: 'https://platform.openai.com/api-keys',
207
+ gemini: 'https://aistudio.google.com/app/apikey',
208
+ };
209
+ if (!hasApiKeyForModel) {
210
+ const helpUrl = keyHelpUrls[selectedModel];
211
+ if (helpUrl) {
212
+ console.log(chalk_1.default.dim(helpUrl));
213
+ }
214
+ }
215
+ const apiKeyPrompt = hasApiKeyForModel
216
+ ? `API key for ${selectedModel} (leave blank to keep current): `
217
+ : `API key for ${selectedModel}: `;
218
+ const apiKeyAnswer = await ask(apiKeyPrompt);
219
+ if (apiKeyAnswer) {
220
+ selectedModelConfig.apiKey = apiKeyAnswer;
221
+ }
222
+ config.models[selectedModel] = selectedModelConfig;
223
+ const currentKey = config.playlist?.privateKey || '';
224
+ let signingKey = currentKey;
225
+ if (isMissingConfigValue(currentKey)) {
226
+ const keyPair = crypto_1.default.generateKeyPairSync('ed25519');
227
+ signingKey = keyPair.privateKey.export({ format: 'der', type: 'pkcs8' }).toString('base64');
228
+ }
229
+ else {
230
+ const keepKey = await promptYesNo(ask, 'Keep existing signing key?', true);
231
+ if (!keepKey) {
232
+ const keyAnswer = await ask('Paste signing key (base64 or hex), or leave blank to regenerate: ');
233
+ if (keyAnswer) {
234
+ signingKey = keyAnswer;
235
+ }
236
+ else {
237
+ const keyPair = crypto_1.default.generateKeyPairSync('ed25519');
238
+ signingKey = keyPair.privateKey
239
+ .export({ format: 'der', type: 'pkcs8' })
240
+ .toString('base64');
241
+ }
242
+ }
243
+ }
244
+ if (signingKey) {
245
+ config.playlist = {
246
+ ...(config.playlist || {}),
247
+ privateKey: signingKey,
248
+ };
249
+ }
250
+ const existingDevice = config.ff1Devices?.devices?.[0];
251
+ {
252
+ const existingHost = existingDevice?.host || '';
253
+ let rawDefaultDeviceId = '';
254
+ if (existingHost) {
255
+ // If host is a .local device, extract just the device ID segment.
256
+ // Otherwise keep the full host (IP address or multi-label domain).
257
+ const hostWithoutScheme = existingHost.replace(/^https?:\/\//, '');
258
+ if (hostWithoutScheme.includes('.local')) {
259
+ rawDefaultDeviceId = hostWithoutScheme.split('.')[0] || '';
260
+ }
261
+ else {
262
+ rawDefaultDeviceId = hostWithoutScheme;
263
+ }
264
+ }
265
+ const defaultDeviceId = isMissingConfigValue(rawDefaultDeviceId) ? '' : rawDefaultDeviceId;
266
+ const idPrompt = defaultDeviceId
267
+ ? `Device ID (e.g. ff1-ABCD1234) [${defaultDeviceId}]: `
268
+ : 'Device ID (e.g. ff1-ABCD1234): ';
269
+ const idAnswer = await ask(idPrompt);
270
+ const rawDeviceId = idAnswer || defaultDeviceId;
271
+ let hostValue = '';
272
+ if (rawDeviceId) {
273
+ const looksLikeHost = rawDeviceId.includes('.') ||
274
+ rawDeviceId.includes(':') ||
275
+ rawDeviceId.startsWith('http');
276
+ if (looksLikeHost) {
277
+ hostValue = normalizeDeviceHost(rawDeviceId);
278
+ }
279
+ else {
280
+ const deviceId = rawDeviceId.startsWith('ff1-') ? rawDeviceId : `ff1-${rawDeviceId}`;
281
+ hostValue = normalizeDeviceHost(`${deviceId}.local`);
282
+ }
283
+ }
284
+ const rawName = existingDevice?.name || 'ff1';
285
+ const defaultName = isMissingConfigValue(rawName) ? '' : rawName;
286
+ const namePrompt = defaultName
287
+ ? `Device name (kitchen, office, etc.) [${defaultName}]: `
288
+ : 'Device name (kitchen, office, etc.): ';
289
+ const nameAnswer = await ask(namePrompt);
290
+ const deviceName = nameAnswer || defaultName || 'ff1';
291
+ if (hostValue) {
292
+ config.ff1Devices = {
293
+ devices: [
294
+ {
295
+ ...existingDevice,
296
+ name: deviceName,
297
+ host: hostValue,
298
+ apiKey: existingDevice?.apiKey || '',
299
+ topicID: existingDevice?.topicID || '',
300
+ },
301
+ ],
302
+ };
303
+ }
304
+ }
305
+ await fs_1.promises.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
306
+ rl.close();
307
+ rl = null;
308
+ console.log(chalk_1.default.green('\nSetup complete'));
309
+ console.log(chalk_1.default.dim(` Config: ${configPath}`));
310
+ const hasApiKey = !isMissingConfigValue(config.models[selectedModel]?.apiKey);
311
+ const hasSigningKey = !isMissingConfigValue(config.playlist?.privateKey || '');
312
+ const hasDevice = Boolean(config.ff1Devices?.devices?.[0]?.host);
313
+ if (!hasApiKey || !hasSigningKey || !hasDevice) {
314
+ console.log(chalk_1.default.yellow('\nNext steps:'));
315
+ if (!hasApiKey) {
316
+ console.log(chalk_1.default.yellow(` • Add API key for ${selectedModel}`));
317
+ }
318
+ if (!hasSigningKey) {
319
+ console.log(chalk_1.default.yellow(' • Add a playlist signing key'));
320
+ }
321
+ if (!hasDevice) {
322
+ console.log(chalk_1.default.yellow(' • Add an FF1 device host'));
323
+ }
324
+ }
325
+ console.log(chalk_1.default.dim('\nRun: ff1 chat'));
326
+ }
327
+ catch (error) {
328
+ console.error(chalk_1.default.red('\nSetup failed:'), error.message);
329
+ process.exit(1);
330
+ }
331
+ finally {
332
+ if (rl) {
333
+ rl.close();
334
+ }
335
+ }
336
+ });
337
+ program
338
+ .command('status')
339
+ .description('Show configuration status')
340
+ .action(async () => {
341
+ try {
342
+ const configPath = await resolveExistingConfigPath();
343
+ if (!configPath) {
344
+ console.log(chalk_1.default.red('config.json not found'));
345
+ console.log(chalk_1.default.dim('Run: ff1 setup'));
346
+ process.exit(1);
347
+ }
348
+ const config = await readConfigFile(configPath);
349
+ const modelNames = Object.keys(config.models || {});
350
+ const defaultModel = config.defaultModel && modelNames.includes(config.defaultModel)
351
+ ? config.defaultModel
352
+ : modelNames[0];
353
+ const defaultModelLabel = defaultModel || 'unknown';
354
+ const defaultModelConfig = defaultModel ? config.models?.[defaultModel] : undefined;
355
+ const statuses = [
356
+ {
357
+ label: 'Config file',
358
+ ok: true,
359
+ detail: configPath,
360
+ },
361
+ {
362
+ label: `Default model (${defaultModelLabel}) API key`,
363
+ ok: defaultModel ? !isMissingConfigValue(defaultModelConfig?.apiKey) : false,
364
+ },
365
+ {
366
+ label: 'Playlist signing key',
367
+ ok: !isMissingConfigValue(config.playlist?.privateKey || ''),
368
+ },
369
+ {
370
+ label: 'FF1 device host',
371
+ ok: !isMissingConfigValue(config.ff1Devices?.devices?.[0]?.host),
372
+ detail: isMissingConfigValue(config.ff1Devices?.devices?.[0]?.host)
373
+ ? undefined
374
+ : config.ff1Devices?.devices?.[0]?.host,
375
+ },
376
+ ];
377
+ console.log(chalk_1.default.blue('\n🔎 FF1 Status\n'));
378
+ statuses.forEach((status) => {
379
+ const label = status.ok ? chalk_1.default.green('OK') : chalk_1.default.red('Missing');
380
+ const detail = status.detail ? chalk_1.default.dim(` (${status.detail})`) : '';
381
+ console.log(`${label} ${status.label}${detail}`);
382
+ });
383
+ const hasMissing = statuses.some((status) => !status.ok);
384
+ if (hasMissing) {
385
+ console.log(chalk_1.default.dim('\nRun: ff1 setup'));
386
+ process.exit(1);
387
+ }
388
+ }
389
+ catch (error) {
390
+ console.error(chalk_1.default.red('\nStatus check failed:'), error.message);
391
+ process.exit(1);
392
+ }
393
+ });
75
394
  program
76
395
  .command('chat')
77
396
  .description('Start an interactive chat to build playlists using natural language')
78
397
  .argument('[content]', 'Optional: Direct chat content (non-interactive mode)')
79
398
  .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')
399
+ .option('-m, --model <name>', 'AI model to use (grok, gpt, gemini) - defaults to config setting')
81
400
  .option('-v, --verbose', 'Show detailed technical output of function calls', false)
82
401
  .action(async (content, options) => {
83
402
  try {
@@ -86,24 +405,24 @@ program
86
405
  const availableModels = (0, config_1.listAvailableModels)();
87
406
  // Validate model selection
88
407
  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(', ')}`));
408
+ console.error(chalk_1.default.red(`Invalid model: "${options.model}"`));
409
+ console.log(chalk_1.default.yellow(`Available models: ${availableModels.join(', ')}`));
91
410
  process.exit(1);
92
411
  }
93
412
  const modelName = options.model || config.defaultModel;
94
413
  const validation = (0, config_1.validateConfig)(modelName);
95
414
  if (!validation.valid) {
96
- console.error(chalk_1.default.red('Configuration Error:'));
415
+ console.error(chalk_1.default.red('Configuration error:'));
97
416
  validation.errors.forEach((error) => {
98
417
  console.error(chalk_1.default.red(` • ${error}`));
99
418
  });
100
- console.log(chalk_1.default.yellow('\nRun: node index.js config init\n'));
419
+ console.log(chalk_1.default.yellow('\nRun: ff1 setup\n'));
101
420
  process.exit(1);
102
421
  }
103
422
  // NON-INTERACTIVE MODE: If content is provided as argument
104
423
  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`));
424
+ console.log(chalk_1.default.blue('\nFF1 Chat (non-interactive)\n'));
425
+ console.log(chalk_1.default.dim(`Model: ${modelName}\n`));
107
426
  console.log(chalk_1.default.yellow('Request:'), content);
108
427
  console.log(); // Blank line
109
428
  try {
@@ -115,32 +434,31 @@ program
115
434
  });
116
435
  // Print final summary
117
436
  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`));
437
+ console.log(chalk_1.default.green('\nPlaylist created'));
438
+ console.log(chalk_1.default.dim(` Title: ${result.playlist.title}`));
439
+ console.log(chalk_1.default.dim(` Items: ${result.playlist.items?.length || 0}`));
440
+ console.log(chalk_1.default.dim(` Output: ${options.output}\n`));
122
441
  }
123
442
  process.exit(0);
124
443
  }
125
444
  catch (error) {
126
- console.error(chalk_1.default.red('\n❌ Error:'), error.message);
445
+ console.error(chalk_1.default.red('\nError:'), error.message);
127
446
  if (options.verbose) {
128
- console.error(chalk_1.default.gray(error.stack));
447
+ console.error(chalk_1.default.dim(error.stack));
129
448
  }
130
449
  process.exit(1);
131
450
  }
132
451
  }
133
452
  // 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'));
453
+ console.log(chalk_1.default.blue('\nFF1 Chat\n'));
454
+ console.log(chalk_1.default.dim('Describe the playlist you want. Ctrl+C to exit.'));
455
+ console.log(chalk_1.default.dim(`Model: ${modelName}\n`));
456
+ console.log(chalk_1.default.dim('Examples:'));
457
+ console.log(chalk_1.default.dim(' • Get tokens 1,2,3 from Ethereum contract 0xabc'));
458
+ console.log(chalk_1.default.dim(' Get token 42 from Tezos contract KT1abc'));
459
+ console.log(chalk_1.default.dim(' Get 3 from Social Codes and 2 from 0xdef'));
460
+ console.log(chalk_1.default.dim(' Build a playlist of my Tezos works from address tz1... plus 3 from Social Codes'));
461
+ console.log(chalk_1.default.dim(' Tip: add -v to see tool calls'));
144
462
  console.log();
145
463
  const rl = readline.createInterface({
146
464
  input: process.stdin,
@@ -183,8 +501,8 @@ program
183
501
  // Only show playlist summary for build operations, not send operations
184
502
  // Skip summary if playlist was already sent to device
185
503
  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}`));
504
+ console.log(chalk_1.default.dim(`\n[DEBUG] result.sentToDevice: ${result?.sentToDevice}`));
505
+ console.log(chalk_1.default.dim(`[DEBUG] result.action: ${result?.action}`));
188
506
  }
189
507
  if (result &&
190
508
  result.playlist &&
@@ -194,9 +512,9 @@ program
194
512
  }
195
513
  }
196
514
  catch (error) {
197
- console.error(chalk_1.default.red('Error:'), error.message);
515
+ console.error(chalk_1.default.red('Error:'), error.message);
198
516
  if (options.verbose) {
199
- console.error(chalk_1.default.gray(error.stack));
517
+ console.error(chalk_1.default.dim(error.stack));
200
518
  }
201
519
  console.log(); // Blank line after error
202
520
  }
@@ -207,12 +525,12 @@ program
207
525
  }
208
526
  catch (error) {
209
527
  if (error.message !== 'readline was closed') {
210
- console.error(chalk_1.default.red('\n❌ Error:'), error.message);
528
+ console.error(chalk_1.default.red('\nError:'), error.message);
211
529
  if (process.env.DEBUG) {
212
- console.error(chalk_1.default.gray(error.stack));
530
+ console.error(chalk_1.default.dim(error.stack));
213
531
  }
214
532
  }
215
- console.log(chalk_1.default.blue('\n👋 Goodbye!\n'));
533
+ console.log(chalk_1.default.blue('\nGoodbye\n'));
216
534
  process.exit(0);
217
535
  }
218
536
  });
@@ -222,7 +540,7 @@ program
222
540
  .argument('<file>', 'Path to the playlist file')
223
541
  .action(async (file) => {
224
542
  try {
225
- console.log(chalk_1.default.blue('\n🔍 Verifying playlist...\n'));
543
+ console.log(chalk_1.default.blue('\nVerify playlist\n'));
226
544
  // Import the verification utility
227
545
  const verifier = await Promise.resolve().then(() => __importStar(require('./src/utilities/playlist-verifier')));
228
546
  const { verifyPlaylistFile, printVerificationResult } = verifier;
@@ -235,7 +553,7 @@ program
235
553
  }
236
554
  }
237
555
  catch (error) {
238
- console.error(chalk_1.default.red('\n❌ Error:'), error.message);
556
+ console.error(chalk_1.default.red('\nError:'), error.message);
239
557
  process.exit(1);
240
558
  }
241
559
  });
@@ -245,7 +563,7 @@ program
245
563
  .argument('<file>', 'Path to the playlist file')
246
564
  .action(async (file) => {
247
565
  try {
248
- console.log(chalk_1.default.blue('\n🔍 Verifying playlist...\n'));
566
+ console.log(chalk_1.default.blue('\nVerify playlist\n'));
249
567
  // Import the verification utility
250
568
  const verifier = await Promise.resolve().then(() => __importStar(require('./src/utilities/playlist-verifier')));
251
569
  const { verifyPlaylistFile, printVerificationResult } = verifier;
@@ -258,7 +576,7 @@ program
258
576
  }
259
577
  }
260
578
  catch (error) {
261
- console.error(chalk_1.default.red('\n❌ Error:'), error.message);
579
+ console.error(chalk_1.default.red('\nError:'), error.message);
262
580
  process.exit(1);
263
581
  }
264
582
  });
@@ -270,26 +588,26 @@ program
270
588
  .option('-o, --output <file>', 'Output file path (defaults to overwriting input file)')
271
589
  .action(async (file, options) => {
272
590
  try {
273
- console.log(chalk_1.default.blue('\n🔏 Signing playlist...\n'));
591
+ console.log(chalk_1.default.blue('\nSign playlist\n'));
274
592
  // Import the signing utility
275
593
  // eslint-disable-next-line @typescript-eslint/no-require-imports
276
594
  const { signPlaylistFile } = require('./src/utilities/playlist-signer');
277
595
  // Sign the playlist
278
596
  const result = await signPlaylistFile(file, options.key, options.output);
279
597
  if (result.success) {
280
- console.log(chalk_1.default.green('\n✅ Playlist signed successfully!'));
598
+ console.log(chalk_1.default.green('\nPlaylist signed'));
281
599
  if (result.playlist?.signature) {
282
- console.log(chalk_1.default.gray(` Signature: ${result.playlist.signature.substring(0, 30)}...`));
600
+ console.log(chalk_1.default.dim(` Signature: ${result.playlist.signature.substring(0, 30)}...`));
283
601
  }
284
602
  console.log();
285
603
  }
286
604
  else {
287
- console.error(chalk_1.default.red('\n❌ Failed to sign playlist:'), result.error);
605
+ console.error(chalk_1.default.red('\nSign failed:'), result.error);
288
606
  process.exit(1);
289
607
  }
290
608
  }
291
609
  catch (error) {
292
- console.error(chalk_1.default.red('\n❌ Error:'), error.message);
610
+ console.error(chalk_1.default.red('\nError:'), error.message);
293
611
  process.exit(1);
294
612
  }
295
613
  });
@@ -301,12 +619,12 @@ program
301
619
  .option('--skip-verify', 'Skip playlist verification before sending')
302
620
  .action(async (url, options) => {
303
621
  try {
304
- console.log(chalk_1.default.blue('\n▶️ Playing URL on FF1 device...\n'));
622
+ console.log(chalk_1.default.blue('\nPlay on FF1\n'));
305
623
  try {
306
624
  new URL(url);
307
625
  }
308
626
  catch (error) {
309
- console.error(chalk_1.default.red('\n❌ Invalid URL:'), error.message);
627
+ console.error(chalk_1.default.red('\nInvalid URL:'), error.message);
310
628
  process.exit(1);
311
629
  }
312
630
  const config = (0, config_1.getConfig)();
@@ -320,7 +638,7 @@ program
320
638
  const { verifyPlaylist } = verifier;
321
639
  const verifyResult = verifyPlaylist(playlist);
322
640
  if (!verifyResult.valid) {
323
- console.error(chalk_1.default.red('\n❌ Playlist verification failed:'), verifyResult.error);
641
+ console.error(chalk_1.default.red('\nPlaylist verification failed:'), verifyResult.error);
324
642
  if (verifyResult.details && verifyResult.details.length > 0) {
325
643
  console.log(chalk_1.default.yellow('\n Validation errors:'));
326
644
  verifyResult.details.forEach((detail) => {
@@ -338,25 +656,25 @@ program
338
656
  deviceName: options.device,
339
657
  });
340
658
  if (result.success) {
341
- console.log(chalk_1.default.green(' URL sent successfully!'));
659
+ console.log(chalk_1.default.green(' Sent'));
342
660
  if (result.deviceName) {
343
- console.log(chalk_1.default.gray(` Device: ${result.deviceName}`));
661
+ console.log(chalk_1.default.dim(` Device: ${result.deviceName}`));
344
662
  }
345
663
  if (result.device) {
346
- console.log(chalk_1.default.gray(` Host: ${result.device}`));
664
+ console.log(chalk_1.default.dim(` Host: ${result.device}`));
347
665
  }
348
666
  console.log();
349
667
  }
350
668
  else {
351
- console.error(chalk_1.default.red('\n❌ Failed to send URL:'), result.error);
669
+ console.error(chalk_1.default.red('\nSend failed:'), result.error);
352
670
  if (result.details) {
353
- console.error(chalk_1.default.gray(` Details: ${result.details}`));
671
+ console.error(chalk_1.default.dim(` Details: ${result.details}`));
354
672
  }
355
673
  process.exit(1);
356
674
  }
357
675
  }
358
676
  catch (error) {
359
- console.error(chalk_1.default.red('\n❌ Error:'), error.message);
677
+ console.error(chalk_1.default.red('\nError:'), error.message);
360
678
  process.exit(1);
361
679
  }
362
680
  });
@@ -368,18 +686,18 @@ program
368
686
  .option('--skip-verify', 'Skip playlist verification before sending')
369
687
  .action(async (file, options) => {
370
688
  try {
371
- console.log(chalk_1.default.blue('\n📤 Sending playlist to FF1 device...\n'));
689
+ console.log(chalk_1.default.blue('\nSend playlist to FF1\n'));
372
690
  // Read the playlist file
373
691
  const content = await fs_1.promises.readFile(file, 'utf-8');
374
692
  const playlist = JSON.parse(content);
375
693
  // Verify playlist before sending (unless skipped)
376
694
  if (!options.skipVerify) {
377
- console.log(chalk_1.default.cyan('🔍 Verifying playlist...'));
695
+ console.log(chalk_1.default.cyan('Verify playlist'));
378
696
  const verifier = await Promise.resolve().then(() => __importStar(require('./src/utilities/playlist-verifier')));
379
697
  const { verifyPlaylist } = verifier;
380
698
  const verifyResult = verifyPlaylist(playlist);
381
699
  if (!verifyResult.valid) {
382
- console.error(chalk_1.default.red('\n❌ Playlist verification failed:'), verifyResult.error);
700
+ console.error(chalk_1.default.red('\nPlaylist verification failed:'), verifyResult.error);
383
701
  if (verifyResult.details && verifyResult.details.length > 0) {
384
702
  console.log(chalk_1.default.yellow('\n Validation errors:'));
385
703
  verifyResult.details.forEach((detail) => {
@@ -389,7 +707,7 @@ program
389
707
  console.log(chalk_1.default.yellow('\n Use --skip-verify to send anyway (not recommended)\n'));
390
708
  process.exit(1);
391
709
  }
392
- console.log(chalk_1.default.green('✓ Playlist verified successfully\n'));
710
+ console.log(chalk_1.default.green('✓ Verified\n'));
393
711
  }
394
712
  // Import the sending utility
395
713
  // eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -400,25 +718,25 @@ program
400
718
  deviceName: options.device,
401
719
  });
402
720
  if (result.success) {
403
- console.log(chalk_1.default.green(' Playlist sent successfully!'));
721
+ console.log(chalk_1.default.green(' Sent'));
404
722
  if (result.deviceName) {
405
- console.log(chalk_1.default.gray(` Device: ${result.deviceName}`));
723
+ console.log(chalk_1.default.dim(` Device: ${result.deviceName}`));
406
724
  }
407
725
  if (result.device) {
408
- console.log(chalk_1.default.gray(` Host: ${result.device}`));
726
+ console.log(chalk_1.default.dim(` Host: ${result.device}`));
409
727
  }
410
728
  console.log();
411
729
  }
412
730
  else {
413
- console.error(chalk_1.default.red('\n❌ Failed to send playlist:'), result.error);
731
+ console.error(chalk_1.default.red('\nSend failed:'), result.error);
414
732
  if (result.details) {
415
- console.error(chalk_1.default.gray(` Details: ${result.details}`));
733
+ console.error(chalk_1.default.dim(` Details: ${result.details}`));
416
734
  }
417
735
  process.exit(1);
418
736
  }
419
737
  }
420
738
  catch (error) {
421
- console.error(chalk_1.default.red('\n❌ Error:'), error.message);
739
+ console.error(chalk_1.default.red('\nError:'), error.message);
422
740
  process.exit(1);
423
741
  }
424
742
  });
@@ -429,13 +747,13 @@ program
429
747
  .option('-s, --server <index>', 'Feed server index (use this if multiple servers configured)')
430
748
  .action(async (file, options) => {
431
749
  try {
432
- console.log(chalk_1.default.blue('\n📡 Publishing playlist to feed server...\n'));
750
+ console.log(chalk_1.default.blue('\nPublish playlist\n'));
433
751
  const { getFeedConfig } = await Promise.resolve().then(() => __importStar(require('./src/config')));
434
752
  const { publishPlaylist } = await Promise.resolve().then(() => __importStar(require('./src/utilities/playlist-publisher')));
435
753
  const feedConfig = getFeedConfig();
436
754
  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'));
755
+ console.error(chalk_1.default.red('\nNo feed servers configured'));
756
+ console.log(chalk_1.default.yellow(' Add feed server URLs to config.json: feed.baseURLs\n'));
439
757
  process.exit(1);
440
758
  }
441
759
  // If multiple servers and no index specified, show options
@@ -464,7 +782,7 @@ program
464
782
  }
465
783
  const serverIndex = parseInt(options.server || '0', 10);
466
784
  if (isNaN(serverIndex) || serverIndex < 0 || serverIndex >= feedConfig.baseURLs.length) {
467
- console.error(chalk_1.default.red('\n❌ Invalid server index'));
785
+ console.error(chalk_1.default.red('\nInvalid server index'));
468
786
  process.exit(1);
469
787
  }
470
788
  serverUrl = feedConfig.baseURLs[serverIndex];
@@ -479,20 +797,20 @@ program
479
797
  }
480
798
  const result = await publishPlaylist(file, serverUrl, serverApiKey);
481
799
  if (result.success) {
482
- console.log(chalk_1.default.green('✅ Playlist published successfully!'));
800
+ console.log(chalk_1.default.green('Published'));
483
801
  if (result.playlistId) {
484
- console.log(chalk_1.default.gray(` Playlist ID: ${result.playlistId}`));
802
+ console.log(chalk_1.default.dim(` Playlist ID: ${result.playlistId}`));
485
803
  }
486
- console.log(chalk_1.default.gray(` Server: ${result.feedServer}`));
804
+ console.log(chalk_1.default.dim(` Server: ${result.feedServer}`));
487
805
  if (result.message) {
488
- console.log(chalk_1.default.gray(` Status: ${result.message}`));
806
+ console.log(chalk_1.default.dim(` Status: ${result.message}`));
489
807
  }
490
808
  console.log();
491
809
  }
492
810
  else {
493
- console.error(chalk_1.default.red('\n❌ Failed to publish playlist'));
811
+ console.error(chalk_1.default.red('\nPublish failed'));
494
812
  if (result.error) {
495
- console.error(chalk_1.default.red(` ${result.error}`));
813
+ console.error(chalk_1.default.red(` ${result.error}`));
496
814
  }
497
815
  if (result.message) {
498
816
  console.log(chalk_1.default.yellow(`\n${result.message}`));
@@ -502,7 +820,7 @@ program
502
820
  }
503
821
  }
504
822
  catch (error) {
505
- console.error(chalk_1.default.red('\n❌ Error:'), error.message);
823
+ console.error(chalk_1.default.red('\nError:'), error.message);
506
824
  process.exit(1);
507
825
  }
508
826
  });
@@ -534,36 +852,36 @@ program
534
852
  process.stdin.on('error', reject);
535
853
  });
536
854
  if (!stdin.trim()) {
537
- console.error(chalk_1.default.red('No parameters provided'));
855
+ console.error(chalk_1.default.red('No parameters provided'));
538
856
  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');
857
+ console.log(' ff1 build params.json');
858
+ console.log(' cat params.json | ff1 build');
859
+ console.log(' echo \'{"requirements":[...]}\' | ff1 build');
542
860
  process.exit(1);
543
861
  }
544
862
  params = JSON.parse(stdin);
545
863
  }
546
864
  if (options.verbose) {
547
- console.log(chalk_1.default.blue('\n📋 Parameters:'));
548
- console.log(chalk_1.default.gray(JSON.stringify(params, null, 2)));
865
+ console.log(chalk_1.default.blue('\nParameters:'));
866
+ console.log(chalk_1.default.dim(JSON.stringify(params, null, 2)));
549
867
  console.log();
550
868
  }
551
- console.log(chalk_1.default.blue('\n🚀 Building playlist from parameters...\n'));
869
+ console.log(chalk_1.default.blue('\nBuild playlist from parameters\n'));
552
870
  const result = await (0, main_1.buildPlaylistDirect)(params, {
553
871
  verbose: options.verbose,
554
872
  outputPath: options.output,
555
873
  });
556
874
  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`));
875
+ console.log(chalk_1.default.green('\nPlaylist created'));
876
+ console.log(chalk_1.default.dim(` Title: ${result.playlist.title}`));
877
+ console.log(chalk_1.default.dim(` Items: ${result.playlist.items?.length || 0}`));
878
+ console.log(chalk_1.default.dim(` Output: ${options.output}\n`));
561
879
  }
562
880
  }
563
881
  catch (error) {
564
- console.error(chalk_1.default.red('\n❌ Error:'), error.message);
882
+ console.error(chalk_1.default.red('\nError:'), error.message);
565
883
  if (options.verbose) {
566
- console.error(chalk_1.default.gray(error.stack));
884
+ console.error(chalk_1.default.dim(error.stack));
567
885
  }
568
886
  process.exit(1);
569
887
  }
@@ -575,37 +893,38 @@ program
575
893
  .action(async (action) => {
576
894
  try {
577
895
  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'));
896
+ console.log(chalk_1.default.blue('\nCreate config.json\n'));
897
+ const { userPath } = (0, config_1.getConfigPaths)();
898
+ const configPath = await (0, config_1.createSampleConfig)(userPath);
899
+ console.log(chalk_1.default.green(`Created ${configPath}`));
900
+ console.log(chalk_1.default.yellow('\nNext: ff1 setup\n'));
582
901
  }
583
902
  else if (action === 'show') {
584
903
  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'));
904
+ console.log(chalk_1.default.blue('\nCurrent configuration\n'));
905
+ console.log(chalk_1.default.bold('Default model:'), chalk_1.default.white(config.defaultModel));
906
+ console.log(chalk_1.default.bold('Default duration:'), chalk_1.default.white(config.defaultDuration + 's'));
907
+ console.log(chalk_1.default.bold('\nAvailable models:\n'));
589
908
  const models = (0, config_1.listAvailableModels)();
590
909
  models.forEach((modelName) => {
591
910
  const modelConfig = config.models[modelName];
592
911
  const isCurrent = modelName === config.defaultModel;
593
912
  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')}`);
913
+ console.log(` API key: ${modelConfig.apiKey && modelConfig.apiKey !== 'your_api_key_here' ? chalk_1.default.green('Set') : chalk_1.default.red('Missing')}`);
914
+ console.log(` Base URL: ${chalk_1.default.dim(modelConfig.baseURL)}`);
915
+ console.log(` Model: ${chalk_1.default.dim(modelConfig.model)}`);
916
+ console.log(` Function calling: ${modelConfig.supportsFunctionCalling ? chalk_1.default.green('Supported') : chalk_1.default.red('Not supported')}`);
598
917
  console.log();
599
918
  });
600
919
  }
601
920
  else if (action === 'validate') {
602
921
  const validation = (0, config_1.validateConfig)();
603
- console.log(chalk_1.default.blue('\n🔍 Validating configuration...\n'));
922
+ console.log(chalk_1.default.blue('\nValidate configuration\n'));
604
923
  if (validation.valid) {
605
- console.log(chalk_1.default.green('Configuration is valid!\n'));
924
+ console.log(chalk_1.default.green('Configuration is valid\n'));
606
925
  }
607
926
  else {
608
- console.log(chalk_1.default.red('Configuration has errors:\n'));
927
+ console.log(chalk_1.default.red('Configuration has errors:\n'));
609
928
  validation.errors.forEach((error) => {
610
929
  console.log(chalk_1.default.red(` • ${error}`));
611
930
  });
@@ -614,13 +933,13 @@ program
614
933
  }
615
934
  }
616
935
  else {
617
- console.error(chalk_1.default.red(`\n❌ Unknown action: ${action}`));
936
+ console.error(chalk_1.default.red(`\nUnknown action: ${action}`));
618
937
  console.log(chalk_1.default.yellow('Available actions: init, show, validate\n'));
619
938
  process.exit(1);
620
939
  }
621
940
  }
622
941
  catch (error) {
623
- console.error(chalk_1.default.red('\n❌ Error:'), error.message);
942
+ console.error(chalk_1.default.red('\nError:'), error.message);
624
943
  process.exit(1);
625
944
  }
626
945
  });