ff1-cli 1.0.0 → 1.0.1

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