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.
@@ -0,0 +1,96 @@
1
+ /**
2
+ * In-Memory Registry for Playlists and Items
3
+ * Reduces AI context usage by storing full objects and passing only IDs
4
+ */
5
+ // Internal storage
6
+ const itemRegistry = new Map();
7
+ const playlistRegistry = new Map();
8
+ /**
9
+ * Store a playlist item in the registry
10
+ *
11
+ * @param {string} id - Item ID
12
+ * @param {Object} item - Full DP1 item object
13
+ */
14
+ function storeItem(id, item) {
15
+ if (!id) {
16
+ throw new Error('Item ID is required');
17
+ }
18
+ itemRegistry.set(id, item);
19
+ }
20
+ /**
21
+ * Retrieve a playlist item from the registry
22
+ *
23
+ * @param {string} id - Item ID
24
+ * @returns {Object|undefined} DP1 item object or undefined
25
+ */
26
+ function getItem(id) {
27
+ return itemRegistry.get(id);
28
+ }
29
+ /**
30
+ * Check if an item exists in the registry
31
+ *
32
+ * @param {string} id - Item ID
33
+ * @returns {boolean} True if item exists
34
+ */
35
+ function hasItem(id) {
36
+ return itemRegistry.has(id);
37
+ }
38
+ /**
39
+ * Store a playlist in the registry
40
+ *
41
+ * @param {string} id - Playlist ID
42
+ * @param {Object} playlist - Full DP1 playlist object
43
+ */
44
+ function storePlaylist(id, playlist) {
45
+ if (!id) {
46
+ throw new Error('Playlist ID is required');
47
+ }
48
+ playlistRegistry.set(id, playlist);
49
+ }
50
+ /**
51
+ * Retrieve a playlist from the registry
52
+ *
53
+ * @param {string} id - Playlist ID
54
+ * @returns {Object|undefined} DP1 playlist object or undefined
55
+ */
56
+ function getPlaylist(id) {
57
+ return playlistRegistry.get(id);
58
+ }
59
+ /**
60
+ * Check if a playlist exists in the registry
61
+ *
62
+ * @param {string} id - Playlist ID
63
+ * @returns {boolean} True if playlist exists
64
+ */
65
+ function hasPlaylist(id) {
66
+ return playlistRegistry.has(id);
67
+ }
68
+ /**
69
+ * Clear all registries
70
+ * Should be called after successful playlist build or on error
71
+ */
72
+ function clearRegistries() {
73
+ itemRegistry.clear();
74
+ playlistRegistry.clear();
75
+ }
76
+ /**
77
+ * Get registry statistics (for debugging)
78
+ *
79
+ * @returns {Object} Registry stats
80
+ */
81
+ function getStats() {
82
+ return {
83
+ itemCount: itemRegistry.size,
84
+ playlistCount: playlistRegistry.size,
85
+ };
86
+ }
87
+ module.exports = {
88
+ storeItem,
89
+ getItem,
90
+ hasItem,
91
+ storePlaylist,
92
+ getPlaylist,
93
+ hasPlaylist,
94
+ clearRegistries,
95
+ getStats,
96
+ };
@@ -0,0 +1,352 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getConfig = getConfig;
7
+ exports.sanitizationLevelToNumber = sanitizationLevelToNumber;
8
+ exports.getBrowserConfig = getBrowserConfig;
9
+ exports.getPlaylistConfig = getPlaylistConfig;
10
+ exports.getFeedConfig = getFeedConfig;
11
+ exports.getFF1DeviceConfig = getFF1DeviceConfig;
12
+ exports.getModelConfig = getModelConfig;
13
+ exports.validateConfig = validateConfig;
14
+ exports.createSampleConfig = createSampleConfig;
15
+ exports.listAvailableModels = listAvailableModels;
16
+ const fs_1 = __importDefault(require("fs"));
17
+ const path_1 = __importDefault(require("path"));
18
+ /**
19
+ * Load configuration from config.json or environment variables
20
+ * Priority: config.json > .env > defaults
21
+ *
22
+ * @returns {Object} Configuration object with model settings
23
+ * @returns {string} returns.defaultModel - Name of the default model to use
24
+ * @returns {Object} returns.models - Available models configuration
25
+ * @returns {number} returns.defaultDuration - Default duration per item in seconds
26
+ */
27
+ function loadConfig() {
28
+ const configPath = path_1.default.join(process.cwd(), 'config.json');
29
+ // Default configuration supporting Grok as default
30
+ const defaultConfig = {
31
+ defaultModel: process.env.DEFAULT_MODEL || 'grok',
32
+ models: {
33
+ grok: {
34
+ apiKey: process.env.GROK_API_KEY || '',
35
+ baseURL: process.env.GROK_API_BASE_URL || 'https://api.x.ai/v1',
36
+ model: process.env.GROK_MODEL || 'grok-beta',
37
+ availableModels: ['grok-beta', 'grok-2-1212', 'grok-2-vision-1212'],
38
+ timeout: parseInt(process.env.TIMEOUT || '30000', 10),
39
+ maxRetries: parseInt(process.env.MAX_RETRIES || '3', 10),
40
+ temperature: parseFloat(process.env.TEMPERATURE || '0.3'),
41
+ maxTokens: parseInt(process.env.MAX_TOKENS || '4000', 10),
42
+ supportsFunctionCalling: true,
43
+ },
44
+ chatgpt: {
45
+ apiKey: process.env.OPENAI_API_KEY || '',
46
+ baseURL: 'https://api.openai.com/v1',
47
+ model: 'gpt-4o',
48
+ availableModels: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo'],
49
+ timeout: 30000,
50
+ maxRetries: 3,
51
+ temperature: 0.3,
52
+ maxTokens: 4000,
53
+ supportsFunctionCalling: true,
54
+ },
55
+ gemini: {
56
+ apiKey: process.env.GEMINI_API_KEY || '',
57
+ baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai/',
58
+ model: 'gemini-2.5-flash',
59
+ availableModels: ['gemini-2.5-flash-lite', 'gemini-2.5-flash', 'gemini-flash-lite-latest'],
60
+ timeout: 30000,
61
+ maxRetries: 3,
62
+ temperature: 0.3,
63
+ maxTokens: 4000,
64
+ supportsFunctionCalling: true,
65
+ },
66
+ },
67
+ defaultDuration: parseInt(process.env.DEFAULT_DURATION || '10', 10),
68
+ browser: {
69
+ timeout: parseInt(process.env.BROWSER_TIMEOUT || '90000', 10),
70
+ sanitizationLevel: process.env.SANITIZATION_LEVEL || 'medium',
71
+ },
72
+ feed: {
73
+ baseURLs: process.env.FEED_BASE_URLS
74
+ ? process.env.FEED_BASE_URLS.split(',')
75
+ : ['https://feed.feralfile.com/api/v1'],
76
+ },
77
+ };
78
+ // Try to load config.json if it exists
79
+ if (fs_1.default.existsSync(configPath)) {
80
+ try {
81
+ const fileConfig = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
82
+ // Deep merge models configuration
83
+ const mergedModels = { ...defaultConfig.models };
84
+ if (fileConfig.models) {
85
+ Object.keys(fileConfig.models).forEach((modelName) => {
86
+ mergedModels[modelName] = {
87
+ ...(defaultConfig.models[modelName] || {}),
88
+ ...fileConfig.models[modelName],
89
+ };
90
+ });
91
+ }
92
+ // Merge with defaults, file config takes precedence
93
+ return {
94
+ ...defaultConfig,
95
+ ...fileConfig,
96
+ models: mergedModels,
97
+ };
98
+ }
99
+ catch (_error) {
100
+ console.warn('Warning: Failed to parse config.json, using defaults');
101
+ return defaultConfig;
102
+ }
103
+ }
104
+ // Return default config if no file exists
105
+ return defaultConfig;
106
+ }
107
+ /**
108
+ * Get current configuration
109
+ *
110
+ * @returns {Object} Current configuration
111
+ */
112
+ function getConfig() {
113
+ return loadConfig();
114
+ }
115
+ /**
116
+ * Convert sanitization level string to numeric value
117
+ *
118
+ * @param {string|number} level - Sanitization level ('none', 'low', 'medium', 'high') or number (0-3)
119
+ * @returns {number} Numeric level (0 = none, 1 = low, 2 = medium, 3 = high)
120
+ */
121
+ function sanitizationLevelToNumber(level) {
122
+ if (typeof level === 'number') {
123
+ return level;
124
+ }
125
+ const levelMap = {
126
+ none: 0,
127
+ low: 1,
128
+ medium: 2,
129
+ high: 3,
130
+ };
131
+ return levelMap[level] !== undefined ? levelMap[level] : 2; // Default to medium (2)
132
+ }
133
+ /**
134
+ * Get browser configuration
135
+ *
136
+ * @returns {Object} Browser configuration
137
+ * @returns {number} returns.timeout - Browser timeout in milliseconds
138
+ * @returns {number} returns.sanitizationLevel - Numeric sanitization level (0-3)
139
+ */
140
+ function getBrowserConfig() {
141
+ const config = getConfig();
142
+ const browserConfig = config.browser || {
143
+ timeout: 90000,
144
+ sanitizationLevel: 'medium',
145
+ };
146
+ return {
147
+ timeout: browserConfig.timeout,
148
+ sanitizationLevel: sanitizationLevelToNumber(browserConfig.sanitizationLevel),
149
+ };
150
+ }
151
+ /**
152
+ * Get playlist configuration including private key for signing
153
+ *
154
+ * @returns {Object} Playlist configuration
155
+ * @returns {string|null} returns.privateKey - Ed25519 private key in base64 format (null if not configured)
156
+ */
157
+ function getPlaylistConfig() {
158
+ const config = getConfig();
159
+ const playlistConfig = config.playlist || {};
160
+ return {
161
+ privateKey: playlistConfig.privateKey || process.env.PLAYLIST_PRIVATE_KEY || null,
162
+ };
163
+ }
164
+ /**
165
+ * Get feed configuration for DP1 feed API
166
+ *
167
+ * Supports both legacy (feed.baseURLs/apiKey) and new (feedServers array) formats.
168
+ *
169
+ * @returns {Object} Feed configuration
170
+ * @returns {string[]} returns.baseURLs - Array of base URLs for feed APIs
171
+ * @returns {string} [returns.apiKey] - Optional API key for authentication (legacy)
172
+ * @returns {Array<Object>} [returns.servers] - Array of feed servers with individual API keys (new)
173
+ */
174
+ function getFeedConfig() {
175
+ const config = getConfig();
176
+ // Check for new feedServers format first
177
+ if (config.feedServers && Array.isArray(config.feedServers) && config.feedServers.length > 0) {
178
+ const baseURLs = config.feedServers.map((server) => server.baseUrl);
179
+ return {
180
+ baseURLs,
181
+ servers: config.feedServers,
182
+ };
183
+ }
184
+ // Fall back to legacy feed format
185
+ const feedConfig = config.feed || {};
186
+ // Support both legacy baseURL and new baseURLs
187
+ let urls = [];
188
+ if (feedConfig.baseURLs && Array.isArray(feedConfig.baseURLs)) {
189
+ urls = feedConfig.baseURLs;
190
+ }
191
+ else if (feedConfig.baseURL) {
192
+ urls = [feedConfig.baseURL];
193
+ }
194
+ else {
195
+ // Default feed URL
196
+ urls = ['https://feed.feralfile.com/api/v1'];
197
+ }
198
+ return {
199
+ baseURLs: urls,
200
+ apiKey: feedConfig.apiKey,
201
+ };
202
+ }
203
+ /**
204
+ * Get FF1 device configuration for casting playlists
205
+ *
206
+ * @returns {Object} FF1 device configuration
207
+ * @returns {Array<Object>} returns.devices - Array of configured FF1 devices
208
+ * @returns {string} returns.devices[].host - Device host URL
209
+ * @returns {string} [returns.devices[].apiKey] - Optional device API key
210
+ * @returns {string} [returns.devices[].topicID] - Optional device topic ID
211
+ * @returns {string} [returns.devices[].name] - Optional device name
212
+ */
213
+ function getFF1DeviceConfig() {
214
+ const config = getConfig();
215
+ const ff1Devices = config.ff1Devices || { devices: [] };
216
+ return {
217
+ devices: ff1Devices.devices || [],
218
+ };
219
+ }
220
+ /**
221
+ * Get configuration for a specific model
222
+ *
223
+ * @param {string} [modelName] - Name of the model (defaults to defaultModel from config)
224
+ * @returns {Object} Model configuration
225
+ * @returns {string} returns.apiKey - API key for the model
226
+ * @returns {string} returns.baseURL - Base URL for the API
227
+ * @returns {string} returns.model - Model name/identifier
228
+ * @returns {number} returns.timeout - Request timeout in milliseconds
229
+ * @returns {number} returns.maxRetries - Maximum number of retries
230
+ * @returns {number} returns.temperature - Temperature for generation
231
+ * @returns {number} returns.maxTokens - Maximum tokens for generation
232
+ * @returns {boolean} returns.supportsFunctionCalling - Whether model supports function calling
233
+ * @throws {Error} If model is not configured or doesn't support function calling
234
+ */
235
+ function getModelConfig(modelName) {
236
+ const config = getConfig();
237
+ const selectedModel = modelName || config.defaultModel;
238
+ if (!config.models[selectedModel]) {
239
+ throw new Error(`Model "${selectedModel}" is not configured. Available models: ${Object.keys(config.models).join(', ')}`);
240
+ }
241
+ const modelConfig = config.models[selectedModel];
242
+ if (!modelConfig.supportsFunctionCalling) {
243
+ throw new Error(`Model "${selectedModel}" does not support function calling`);
244
+ }
245
+ const normalizedBaseURL = modelConfig.baseURL?.replace(/\/+$/, '');
246
+ return {
247
+ ...modelConfig,
248
+ baseURL: normalizedBaseURL,
249
+ defaultDuration: config.defaultDuration,
250
+ };
251
+ }
252
+ /**
253
+ * Validate configuration for a specific model
254
+ *
255
+ * @param {string} [modelName] - Name of the model to validate
256
+ * @returns {Object} Validation result
257
+ * @returns {boolean} returns.valid - Whether the configuration is valid
258
+ * @returns {Array<string>} returns.errors - List of validation errors
259
+ */
260
+ function validateConfig(modelName) {
261
+ const errors = [];
262
+ try {
263
+ const config = getConfig();
264
+ const selectedModel = modelName || config.defaultModel;
265
+ if (!config.models[selectedModel]) {
266
+ errors.push(`Model "${selectedModel}" is not configured. Available: ${Object.keys(config.models).join(', ')}`);
267
+ return { valid: false, errors };
268
+ }
269
+ const modelConfig = config.models[selectedModel];
270
+ if (!modelConfig.apiKey || modelConfig.apiKey === 'your_api_key_here') {
271
+ errors.push(`API key for "${selectedModel}" is missing or not configured`);
272
+ }
273
+ if (!modelConfig.baseURL) {
274
+ errors.push(`Base URL for "${selectedModel}" is missing`);
275
+ }
276
+ if (!modelConfig.model) {
277
+ errors.push(`Model identifier for "${selectedModel}" is not set`);
278
+ }
279
+ if (!modelConfig.supportsFunctionCalling) {
280
+ errors.push(`Model "${selectedModel}" does not support function calling (required)`);
281
+ }
282
+ // Validate browser configuration
283
+ if (config.browser) {
284
+ if (config.browser.timeout && typeof config.browser.timeout !== 'number') {
285
+ errors.push('Browser timeout must be a number');
286
+ }
287
+ const validLevels = ['none', 'low', 'medium', 'high'];
288
+ if (config.browser.sanitizationLevel &&
289
+ !validLevels.includes(config.browser.sanitizationLevel) &&
290
+ typeof config.browser.sanitizationLevel !== 'number') {
291
+ errors.push(`Invalid browser.sanitizationLevel: "${config.browser.sanitizationLevel}". Must be one of: ${validLevels.join(', ')} or 0-3`);
292
+ }
293
+ }
294
+ // Validate playlist configuration (optional, but warn if configured incorrectly)
295
+ if (config.playlist && config.playlist.privateKey) {
296
+ const key = config.playlist.privateKey;
297
+ if (key !== 'your_ed25519_private_key_base64_here' &&
298
+ typeof key === 'string' &&
299
+ key.length > 0) {
300
+ // Check if it looks like valid base64
301
+ const base64Regex = /^[A-Za-z0-9+/]+=*$/;
302
+ if (!base64Regex.test(key)) {
303
+ errors.push('playlist.privateKey must be a valid base64-encoded ed25519 private key');
304
+ }
305
+ }
306
+ }
307
+ return {
308
+ valid: errors.length === 0,
309
+ errors,
310
+ };
311
+ }
312
+ catch (error) {
313
+ errors.push(error.message);
314
+ return { valid: false, errors };
315
+ }
316
+ }
317
+ /**
318
+ * Create a sample config.json file from config.json.example
319
+ *
320
+ * Loads the bundled config.json.example template from the package directory
321
+ * and writes it to the user's current working directory.
322
+ *
323
+ * @returns {Promise<string>} Path to the created config file
324
+ * @throws {Error} If config.json already exists or example file is missing
325
+ */
326
+ async function createSampleConfig() {
327
+ const configPath = path_1.default.join(process.cwd(), 'config.json');
328
+ // Check if config.json already exists in user's directory
329
+ if (fs_1.default.existsSync(configPath)) {
330
+ throw new Error('config.json already exists');
331
+ }
332
+ // Look for config.json.example in the package directory
333
+ // When compiled, this file is in dist/src/config.js
334
+ // The template is at the package root: ../../config.json.example
335
+ const packageRoot = path_1.default.join(__dirname, '../..');
336
+ const examplePath = path_1.default.join(packageRoot, 'config.json.example');
337
+ if (!fs_1.default.existsSync(examplePath)) {
338
+ throw new Error(`config.json.example not found at ${examplePath}. This is likely a package installation issue.`);
339
+ }
340
+ const exampleConfig = fs_1.default.readFileSync(examplePath, 'utf-8');
341
+ fs_1.default.writeFileSync(configPath, exampleConfig, 'utf-8');
342
+ return configPath;
343
+ }
344
+ /**
345
+ * List all available models
346
+ *
347
+ * @returns {Array<string>} List of available model names
348
+ */
349
+ function listAvailableModels() {
350
+ const config = getConfig();
351
+ return Object.keys(config.models);
352
+ }