@feralfile/cli 1.1.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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +96 -0
  3. package/config.json.example +96 -0
  4. package/dist/index.js +54 -0
  5. package/dist/src/ai-orchestrator/index.js +1019 -0
  6. package/dist/src/ai-orchestrator/registry.js +96 -0
  7. package/dist/src/commands/build.js +69 -0
  8. package/dist/src/commands/chat.js +189 -0
  9. package/dist/src/commands/config.js +68 -0
  10. package/dist/src/commands/device.js +278 -0
  11. package/dist/src/commands/helpers/config-files.js +62 -0
  12. package/dist/src/commands/helpers/device-discovery.js +111 -0
  13. package/dist/src/commands/helpers/playlist-display.js +161 -0
  14. package/dist/src/commands/helpers/prompt.js +65 -0
  15. package/dist/src/commands/helpers/ssh-helpers.js +44 -0
  16. package/dist/src/commands/play.js +110 -0
  17. package/dist/src/commands/publish.js +115 -0
  18. package/dist/src/commands/setup.js +225 -0
  19. package/dist/src/commands/sign.js +41 -0
  20. package/dist/src/commands/ssh.js +108 -0
  21. package/dist/src/commands/status.js +126 -0
  22. package/dist/src/commands/validate.js +18 -0
  23. package/dist/src/config.js +441 -0
  24. package/dist/src/intent-parser/index.js +1382 -0
  25. package/dist/src/intent-parser/utils.js +108 -0
  26. package/dist/src/logger.js +82 -0
  27. package/dist/src/main.js +459 -0
  28. package/dist/src/types.js +5 -0
  29. package/dist/src/utilities/address-validator.js +242 -0
  30. package/dist/src/utilities/device-default.js +36 -0
  31. package/dist/src/utilities/device-lookup.js +107 -0
  32. package/dist/src/utilities/device-normalize.js +62 -0
  33. package/dist/src/utilities/device-upsert.js +91 -0
  34. package/dist/src/utilities/domain-resolver.js +291 -0
  35. package/dist/src/utilities/ed25519-key-derive.js +155 -0
  36. package/dist/src/utilities/feed-fetcher.js +471 -0
  37. package/dist/src/utilities/ff1-compatibility.js +269 -0
  38. package/dist/src/utilities/ff1-device.js +250 -0
  39. package/dist/src/utilities/ff1-discovery.js +330 -0
  40. package/dist/src/utilities/functions.js +308 -0
  41. package/dist/src/utilities/index.js +469 -0
  42. package/dist/src/utilities/nft-indexer.js +1024 -0
  43. package/dist/src/utilities/playlist-builder.js +523 -0
  44. package/dist/src/utilities/playlist-publisher.js +131 -0
  45. package/dist/src/utilities/playlist-send.js +260 -0
  46. package/dist/src/utilities/playlist-signer.js +204 -0
  47. package/dist/src/utilities/playlist-signing-role.js +41 -0
  48. package/dist/src/utilities/playlist-source.js +128 -0
  49. package/dist/src/utilities/playlist-verifier.js +274 -0
  50. package/dist/src/utilities/ssh-access.js +145 -0
  51. package/dist/src/utils.js +48 -0
  52. package/docs/CONFIGURATION.md +206 -0
  53. package/docs/EXAMPLES.md +390 -0
  54. package/docs/FUNCTION_CALLING.md +96 -0
  55. package/docs/PROJECT_SPEC.md +228 -0
  56. package/docs/README.md +348 -0
  57. package/docs/RELEASING.md +73 -0
  58. package/package.json +76 -0
@@ -0,0 +1,471 @@
1
+ /**
2
+ * DP1 Feed Fetcher
3
+ * Utilities to fetch playlists from DP1 Feed API
4
+ * API: https://github.com/display-protocol/dp1-feed
5
+ */
6
+ const chalk = require('chalk');
7
+ const fuzzysort = require('fuzzysort');
8
+ const { getFeedConfig } = require('../config');
9
+ /**
10
+ * Get feed API base URLs from configuration
11
+ *
12
+ * @returns {string[]} Array of feed API base URLs
13
+ */
14
+ function getFeedApiUrls() {
15
+ const feedConfig = getFeedConfig();
16
+ return feedConfig.baseURLs;
17
+ }
18
+ /**
19
+ * Build a concise feed reachability error message.
20
+ *
21
+ * @param {Array<string>} failedFeeds - Feed URLs that could not be reached
22
+ * @returns {string} Human-readable error message
23
+ */
24
+ function buildFeedUnreachableError(failedFeeds = []) {
25
+ const failedCount = Array.isArray(failedFeeds) ? failedFeeds.length : 0;
26
+ if (failedCount === 0) {
27
+ return 'No reachable feed servers available';
28
+ }
29
+ if (failedCount === 1) {
30
+ return `Configured feed server is unreachable: ${failedFeeds[0]}`;
31
+ }
32
+ return `All configured feed servers are unreachable (${failedCount}): ${failedFeeds.join(', ')}`;
33
+ }
34
+ /**
35
+ * Fetch playlists from a single feed URL with pagination
36
+ *
37
+ * @param {string} feedUrl - Feed API base URL
38
+ * @param {number} limit - Items per page (default: 50, max: 100)
39
+ * @returns {Promise<Object>} Result with playlists and reachability
40
+ */
41
+ async function fetchPlaylistsFromFeed(feedUrl, limit = 100) {
42
+ try {
43
+ // API has a maximum limit of 100
44
+ const validLimit = Math.min(limit, 100);
45
+ const response = await fetch(`${feedUrl}/playlists?limit=${validLimit}&sort=-created`);
46
+ if (!response.ok) {
47
+ console.log(chalk.yellow(` Feed ${feedUrl} returned ${response.status}`));
48
+ return {
49
+ playlists: [],
50
+ reachable: false,
51
+ feedUrl,
52
+ error: `HTTP ${response.status}`,
53
+ };
54
+ }
55
+ const data = await response.json();
56
+ const playlists = data.items || [];
57
+ // Add feedUrl to each playlist for tracking
58
+ return {
59
+ playlists: playlists.map((p) => ({
60
+ ...p,
61
+ feedUrl,
62
+ })),
63
+ reachable: true,
64
+ feedUrl,
65
+ error: null,
66
+ };
67
+ }
68
+ catch (error) {
69
+ console.log(chalk.yellow(` Failed to fetch from ${feedUrl}: ${error.message}`));
70
+ return {
71
+ playlists: [],
72
+ reachable: false,
73
+ feedUrl,
74
+ error: error.message,
75
+ };
76
+ }
77
+ }
78
+ /**
79
+ * Fetch playlists with pagination and fuzzy filtering to save memory
80
+ *
81
+ * @param {string} feedUrl - Feed API base URL
82
+ * @param {string} searchTerm - Search term for fuzzy filtering
83
+ * @param {number} pageSize - Items per page (default: 50, max: 100)
84
+ * @param {number} topN - Keep top N matches per page (default: 10)
85
+ * @param {number} maxItems - Maximum total items to fetch (default: 500)
86
+ * @returns {Promise<Object>} Result with matches and reachability
87
+ */
88
+ async function fetchPlaylistsWithPagination(feedUrl, searchTerm, pageSize = 50, topN = 10, maxItems = 500) {
89
+ const allMatches = [];
90
+ let reachable = false;
91
+ let offset = 0;
92
+ let hasMore = true;
93
+ let totalFetched = 0;
94
+ while (hasMore && totalFetched < maxItems) {
95
+ // Calculate limit for this page (might be less than pageSize on last page)
96
+ // API has a maximum limit of 100
97
+ const remainingItems = maxItems - totalFetched;
98
+ const currentLimit = Math.min(pageSize, remainingItems, 100);
99
+ try {
100
+ const response = await fetch(`${feedUrl}/playlists?limit=${currentLimit}&offset=${offset}&sort=-created`);
101
+ reachable = true;
102
+ if (!response.ok) {
103
+ if (response.status === 404 || response.status === 400) {
104
+ // No more pages or offset not supported
105
+ hasMore = false;
106
+ break;
107
+ }
108
+ break;
109
+ }
110
+ const data = await response.json();
111
+ const playlists = data.items || [];
112
+ if (playlists.length === 0) {
113
+ hasMore = false;
114
+ break;
115
+ }
116
+ totalFetched += playlists.length;
117
+ // Extract titles for fuzzy matching
118
+ const titles = playlists.map((p) => p.title);
119
+ // Fuzzy match on this page
120
+ const results = fuzzysort.go(searchTerm, titles, {
121
+ threshold: -5000, // More lenient threshold
122
+ limit: topN, // Keep only top N per page
123
+ });
124
+ // Map results back to playlist objects
125
+ results.forEach((result) => {
126
+ const playlist = playlists.find((p) => p.title === result.target);
127
+ if (playlist) {
128
+ allMatches.push({
129
+ title: playlist.title,
130
+ id: playlist.id,
131
+ feedUrl,
132
+ score: result.score,
133
+ });
134
+ }
135
+ });
136
+ // Check if we've reached the end
137
+ if (playlists.length < currentLimit) {
138
+ hasMore = false;
139
+ }
140
+ else {
141
+ offset += playlists.length;
142
+ }
143
+ }
144
+ catch (_error) {
145
+ hasMore = false;
146
+ }
147
+ }
148
+ return {
149
+ matches: allMatches,
150
+ reachable,
151
+ feedUrl,
152
+ };
153
+ }
154
+ /**
155
+ * Fetch all playlists from all configured feeds
156
+ *
157
+ * @returns {Promise<Object>} Playlists with feed reachability metadata
158
+ */
159
+ async function fetchAllPlaylists() {
160
+ const feedUrls = getFeedApiUrls();
161
+ // Fetch playlists from all feeds in parallel
162
+ const feedResults = await Promise.all(feedUrls.map((url) => fetchPlaylistsFromFeed(url)));
163
+ const playlists = feedResults.flatMap((result) => result.playlists || []);
164
+ const reachableFeeds = feedResults
165
+ .filter((result) => result.reachable)
166
+ .map((result) => result.feedUrl);
167
+ const unreachableFeeds = feedResults
168
+ .filter((result) => !result.reachable)
169
+ .map((result) => result.feedUrl);
170
+ return {
171
+ playlists,
172
+ reachableFeeds,
173
+ unreachableFeeds,
174
+ };
175
+ }
176
+ /**
177
+ * Search for exact playlist match by name across multiple feeds
178
+ *
179
+ * @param {string} playlistName - Exact playlist name to search for
180
+ * @returns {Promise<Object>} Search result with playlist or error
181
+ */
182
+ async function searchExactPlaylist(playlistName) {
183
+ try {
184
+ const { playlists, reachableFeeds, unreachableFeeds } = await fetchAllPlaylists();
185
+ if (reachableFeeds.length === 0) {
186
+ return {
187
+ success: false,
188
+ errorType: 'feed_unreachable',
189
+ error: buildFeedUnreachableError(unreachableFeeds),
190
+ };
191
+ }
192
+ if (playlists.length === 0) {
193
+ return {
194
+ success: false,
195
+ errorType: 'playlist_not_found',
196
+ error: `No playlists available in reachable feed server(s) for "${playlistName}"`,
197
+ };
198
+ }
199
+ // Find exact match (case-insensitive)
200
+ const normalizedSearchName = playlistName.toLowerCase().trim();
201
+ const exactMatch = playlists.find((p) => p.title.toLowerCase().trim() === normalizedSearchName);
202
+ if (exactMatch) {
203
+ // Fetch full playlist details
204
+ const playlist = await getPlaylistById(exactMatch.id, exactMatch.feedUrl);
205
+ return {
206
+ success: true,
207
+ playlist,
208
+ };
209
+ }
210
+ else {
211
+ return {
212
+ success: false,
213
+ errorType: 'playlist_not_found',
214
+ error: `No exact match found for playlist "${playlistName}"`,
215
+ };
216
+ }
217
+ }
218
+ catch (error) {
219
+ return {
220
+ success: false,
221
+ errorType: 'feed_unreachable',
222
+ error: error.message,
223
+ };
224
+ }
225
+ }
226
+ /**
227
+ * Find the best matching playlist using fuzzy string matching with pagination
228
+ *
229
+ * @param {string} searchTerm - Search term to match against
230
+ * @returns {Promise<Object>} Result with best matching playlist name and map
231
+ */
232
+ async function findBestMatchingPlaylist(searchTerm) {
233
+ try {
234
+ const feedUrls = getFeedApiUrls();
235
+ // Fetch from all feeds in parallel with pagination and filtering
236
+ const allMatchesResults = await Promise.all(feedUrls.map((url) => fetchPlaylistsWithPagination(url, searchTerm, 50, 10)));
237
+ const reachableFeeds = allMatchesResults
238
+ .filter((result) => result.reachable)
239
+ .map((result) => result.feedUrl);
240
+ const unreachableFeeds = allMatchesResults
241
+ .filter((result) => !result.reachable)
242
+ .map((result) => result.feedUrl);
243
+ if (reachableFeeds.length === 0) {
244
+ return {
245
+ success: false,
246
+ errorType: 'feed_unreachable',
247
+ error: buildFeedUnreachableError(unreachableFeeds),
248
+ };
249
+ }
250
+ // Flatten and combine results from all feeds
251
+ const allMatches = allMatchesResults.flatMap((result) => result.matches || []);
252
+ if (allMatches.length === 0) {
253
+ return {
254
+ success: false,
255
+ errorType: 'playlist_not_found',
256
+ error: `No matching playlists found for "${searchTerm}"`,
257
+ };
258
+ }
259
+ // Sort by score (highest first) and get best match
260
+ allMatches.sort((a, b) => b.score - a.score);
261
+ // Build playlistMap for ID lookup
262
+ const playlistMap = {};
263
+ allMatches.forEach((match) => {
264
+ playlistMap[match.title] = {
265
+ id: match.id,
266
+ feedUrl: match.feedUrl,
267
+ };
268
+ });
269
+ const bestMatch = allMatches[0].title;
270
+ // Simplified output: only show the best match, not all alternatives
271
+ console.log(chalk.green(`✓ Found: "${bestMatch}"`));
272
+ return {
273
+ success: true,
274
+ bestMatch,
275
+ playlistMap,
276
+ allMatches: allMatches.map((m) => m.title),
277
+ };
278
+ }
279
+ catch (error) {
280
+ return {
281
+ success: false,
282
+ errorType: 'feed_unreachable',
283
+ error: error.message,
284
+ };
285
+ }
286
+ }
287
+ /**
288
+ * Get playlist by ID or slug from a specific feed or try all feeds
289
+ *
290
+ * @param {string} idOrSlug - Playlist ID (UUID) or slug
291
+ * @param {string} [feedUrl] - Optional specific feed URL to use
292
+ * @returns {Promise<Object>} Playlist object
293
+ */
294
+ async function getPlaylistById(idOrSlug, feedUrl = null) {
295
+ try {
296
+ const feedUrls = feedUrl ? [feedUrl] : getFeedApiUrls();
297
+ // Try each feed URL until we find the playlist
298
+ for (const url of feedUrls) {
299
+ try {
300
+ const response = await fetch(`${url}/playlists/${idOrSlug}`);
301
+ if (response.ok) {
302
+ const playlist = await response.json();
303
+ return playlist;
304
+ }
305
+ }
306
+ catch (_error) {
307
+ // Continue to next feed
308
+ continue;
309
+ }
310
+ }
311
+ throw new Error(`Playlist "${idOrSlug}" not found in any feed`);
312
+ }
313
+ catch (error) {
314
+ throw new Error(`Failed to fetch playlist: ${error.message}`);
315
+ }
316
+ }
317
+ /**
318
+ * Shuffle array using Fisher-Yates algorithm
319
+ *
320
+ * @param {Array} array - Array to shuffle
321
+ * @returns {Array} Shuffled array
322
+ */
323
+ function shuffleArray(array) {
324
+ const shuffled = [...array];
325
+ for (let i = shuffled.length - 1; i > 0; i--) {
326
+ const j = Math.floor(Math.random() * (i + 1));
327
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
328
+ }
329
+ return shuffled;
330
+ }
331
+ /**
332
+ * Get playlist items from a playlist
333
+ *
334
+ * @param {Object} playlist - DP1 playlist object
335
+ * @param {number} quantity - Number of items to extract
336
+ * @param {number} duration - Duration per item in seconds
337
+ * @param {boolean} shuffle - Whether to shuffle and randomly select items
338
+ * @returns {Array<Object>} Array of DP1 playlist items
339
+ */
340
+ function extractPlaylistItems(playlist, quantity, duration, shuffle = true) {
341
+ if (!playlist.items || playlist.items.length === 0) {
342
+ return [];
343
+ }
344
+ let items = playlist.items;
345
+ // Shuffle and randomly select if requested
346
+ if (shuffle && items.length > quantity) {
347
+ items = shuffleArray(items);
348
+ }
349
+ // Take requested quantity
350
+ items = items.slice(0, quantity);
351
+ // Override duration and ensure created field exists
352
+ items = items.map((item) => ({
353
+ ...item,
354
+ duration: duration || item.duration,
355
+ created: item.created || new Date().toISOString(), // Ensure created field exists
356
+ }));
357
+ return items;
358
+ }
359
+ /**
360
+ * Fetch feed playlist (deterministic - exact match only)
361
+ *
362
+ * @param {string} playlistName - Exact playlist name
363
+ * @param {number} quantity - Number of items to fetch
364
+ * @param {number} duration - Duration per item
365
+ * @returns {Promise<Object>} Result with items
366
+ */
367
+ async function fetchFeedPlaylistDirect(playlistName, quantity = 5, duration = 10) {
368
+ const feedUrls = getFeedApiUrls();
369
+ console.log(chalk.cyan(`Searching for playlist "${playlistName}" in ${feedUrls.length} source(s)...`));
370
+ const result = await searchExactPlaylist(playlistName);
371
+ if (!result.success) {
372
+ if (result.errorType === 'feed_unreachable') {
373
+ console.log(chalk.yellow(` Feed server unavailable: ${result.error}`));
374
+ }
375
+ else {
376
+ console.log(chalk.yellow(` Playlist not found: ${result.error}`));
377
+ }
378
+ return {
379
+ success: false,
380
+ error: result.error,
381
+ errorType: result.errorType,
382
+ items: [],
383
+ };
384
+ }
385
+ const items = extractPlaylistItems(result.playlist, quantity, duration);
386
+ console.log(chalk.green(`✓ Got ${items.length} item(s)\n`));
387
+ return {
388
+ success: true,
389
+ playlist: result.playlist,
390
+ items,
391
+ };
392
+ }
393
+ /**
394
+ * Search for playlists using fuzzy matching
395
+ *
396
+ * @param {string} playlistName - Playlist name (can be fuzzy)
397
+ * @param {number} quantity - Number of items to fetch
398
+ * @param {number} duration - Duration per item
399
+ * @returns {Promise<Object>} Result with best match and map for lookup
400
+ */
401
+ async function searchFeedPlaylists(playlistName, quantity = 5, duration = 10) {
402
+ const feedUrls = getFeedApiUrls();
403
+ console.log(chalk.cyan(`Searching for playlist "${playlistName}" in ${feedUrls.length} source(s)...`));
404
+ const result = await findBestMatchingPlaylist(playlistName);
405
+ if (!result.success) {
406
+ if (result.errorType === 'feed_unreachable') {
407
+ console.log(chalk.yellow(` Feed server unavailable: ${result.error}\n`));
408
+ }
409
+ else {
410
+ console.log(chalk.yellow(` Playlist not found: ${result.error}\n`));
411
+ }
412
+ return {
413
+ success: false,
414
+ error: result.error,
415
+ errorType: result.errorType,
416
+ };
417
+ }
418
+ return {
419
+ success: true,
420
+ bestMatch: result.bestMatch,
421
+ playlistMap: result.playlistMap,
422
+ searchTerm: playlistName,
423
+ quantity,
424
+ duration,
425
+ };
426
+ }
427
+ /**
428
+ * Fetch specific playlist by ID or name and extract items
429
+ *
430
+ * @param {string} playlistIdOrName - Playlist ID, slug, or exact name
431
+ * @param {number} quantity - Number of items to fetch
432
+ * @param {number} duration - Duration per item
433
+ * @param {Object} playlistMap - Optional map of names to IDs for lookup
434
+ * @param {boolean} shuffle - Whether to shuffle and randomly select items
435
+ * @returns {Promise<Object>} Result with items
436
+ */
437
+ async function fetchPlaylistItems(playlistIdOrName, quantity = 5, duration = 10, playlistMap = null, shuffle = true) {
438
+ try {
439
+ let playlistId = playlistIdOrName;
440
+ let feedUrl = null;
441
+ // If playlistMap provided, look up the ID from the name
442
+ if (playlistMap && playlistMap[playlistIdOrName]) {
443
+ playlistId = playlistMap[playlistIdOrName].id;
444
+ feedUrl = playlistMap[playlistIdOrName].feedUrl;
445
+ }
446
+ const playlist = await getPlaylistById(playlistId, feedUrl);
447
+ const items = extractPlaylistItems(playlist, quantity, duration, shuffle);
448
+ console.log(chalk.green(`✓ Got ${items.length} item(s) from "${playlist.title || playlistId}"\n`));
449
+ return {
450
+ success: true,
451
+ playlist,
452
+ items,
453
+ };
454
+ }
455
+ catch (error) {
456
+ return {
457
+ success: false,
458
+ error: error.message,
459
+ items: [],
460
+ };
461
+ }
462
+ }
463
+ module.exports = {
464
+ searchExactPlaylist,
465
+ findBestMatchingPlaylist,
466
+ getPlaylistById,
467
+ extractPlaylistItems,
468
+ fetchFeedPlaylistDirect,
469
+ searchFeedPlaylists,
470
+ fetchPlaylistItems,
471
+ };