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/LICENSE +21 -0
- package/README.md +65 -0
- package/config.json.example +78 -0
- package/dist/index.js +627 -0
- package/dist/src/ai-orchestrator/index.js +870 -0
- package/dist/src/ai-orchestrator/registry.js +96 -0
- package/dist/src/config.js +352 -0
- package/dist/src/intent-parser/index.js +1342 -0
- package/dist/src/intent-parser/utils.js +108 -0
- package/dist/src/logger.js +72 -0
- package/dist/src/main.js +393 -0
- package/dist/src/types.js +5 -0
- package/dist/src/utilities/address-validator.js +242 -0
- package/dist/src/utilities/domain-resolver.js +291 -0
- package/dist/src/utilities/feed-fetcher.js +387 -0
- package/dist/src/utilities/ff1-device.js +176 -0
- package/dist/src/utilities/functions.js +325 -0
- package/dist/src/utilities/index.js +372 -0
- package/dist/src/utilities/nft-indexer.js +1013 -0
- package/dist/src/utilities/playlist-builder.js +522 -0
- package/dist/src/utilities/playlist-publisher.js +131 -0
- package/dist/src/utilities/playlist-send.js +241 -0
- package/dist/src/utilities/playlist-signer.js +171 -0
- package/dist/src/utilities/playlist-verifier.js +156 -0
- package/dist/src/utils.js +48 -0
- package/docs/CONFIGURATION.md +178 -0
- package/docs/EXAMPLES.md +331 -0
- package/docs/FUNCTION_CALLING.md +92 -0
- package/docs/README.md +267 -0
- package/docs/RELEASING.md +22 -0
- package/package.json +75 -0
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();
|