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,108 @@
1
+ "use strict";
2
+ /**
3
+ * Intent Parser Utilities
4
+ * Validation and constraint enforcement for parsed requirements
5
+ */
6
+ var __importDefault = (this && this.__importDefault) || function (mod) {
7
+ return (mod && mod.__esModule) ? mod : { "default": mod };
8
+ };
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.applyConstraints = applyConstraints;
11
+ const chalk_1 = __importDefault(require("chalk"));
12
+ /**
13
+ * Apply constraints to parsed requirements
14
+ *
15
+ * @param {Object} params - Parsed parameters
16
+ * @param {Array<Object>} params.requirements - Array of requirements
17
+ * @param {Object} [params.playlistSettings] - Playlist settings
18
+ * @param {Object} config - Application config
19
+ * @returns {Object} Validated parameters
20
+ */
21
+ function applyConstraints(params, config) {
22
+ // Validate requirements array
23
+ if (!params.requirements || !Array.isArray(params.requirements)) {
24
+ throw new Error('Requirements must be an array');
25
+ }
26
+ if (params.requirements.length === 0) {
27
+ throw new Error('At least one requirement is needed');
28
+ }
29
+ // Validate each requirement
30
+ params.requirements = params.requirements.map((req, index) => {
31
+ const r = req;
32
+ if (!r.type) {
33
+ throw new Error(`Requirement ${index + 1}: type is required`);
34
+ }
35
+ // Validate based on type
36
+ if (r.type === 'build_playlist') {
37
+ if (!r.blockchain) {
38
+ throw new Error(`Requirement ${index + 1}: blockchain is required for build_playlist`);
39
+ }
40
+ if (!r.contractAddress) {
41
+ throw new Error(`Requirement ${index + 1}: contractAddress is required for build_playlist`);
42
+ }
43
+ if (!r.tokenIds || r.tokenIds.length === 0) {
44
+ throw new Error(`Requirement ${index + 1}: tokenIds are required for build_playlist`);
45
+ }
46
+ }
47
+ else if (r.type === 'query_address') {
48
+ if (!r.ownerAddress) {
49
+ throw new Error(`Requirement ${index + 1}: ownerAddress is required for query_address`);
50
+ }
51
+ }
52
+ else if (r.type === 'fetch_feed') {
53
+ if (!r.playlistName) {
54
+ throw new Error(`Requirement ${index + 1}: playlistName is required for fetch_feed`);
55
+ }
56
+ }
57
+ else {
58
+ throw new Error(`Requirement ${index + 1}: invalid type "${r.type}"`);
59
+ }
60
+ // Set default quantity if not provided
61
+ // Allow "all" as a string value for query_address type
62
+ let quantity;
63
+ if (r.quantity === 'all' || r.quantity === null || r.quantity === undefined) {
64
+ quantity = r.type === 'query_address' ? 'all' : 5;
65
+ }
66
+ else if (typeof r.quantity === 'string') {
67
+ // Try to parse string numbers
68
+ const parsed = parseInt(r.quantity, 10);
69
+ quantity = isNaN(parsed) ? r.quantity : parsed;
70
+ }
71
+ else {
72
+ quantity = r.quantity;
73
+ }
74
+ return {
75
+ ...r,
76
+ quantity: quantity, // No cap - registry system handles large playlists efficiently
77
+ tokenIds: r.tokenIds || [],
78
+ };
79
+ });
80
+ // Note: No cap needed - registry system handles large playlists efficiently
81
+ // Full items are stored in memory, only IDs are sent to AI model
82
+ const hasAllQuantity = params.requirements.some((r) => r.quantity === 'all');
83
+ const totalRequested = params.requirements.reduce((sum, r) => {
84
+ if (typeof r.quantity === 'number') {
85
+ return sum + r.quantity;
86
+ }
87
+ return sum;
88
+ }, 0);
89
+ if (hasAllQuantity) {
90
+ console.log(chalk_1.default.yellow(`\n⚠️ Requesting all tokens from one or more addresses. This may take a while to fetch and process...\n`));
91
+ }
92
+ else if (totalRequested > 100) {
93
+ console.log(chalk_1.default.yellow(`\n⚠️ Requesting ${totalRequested} items. This may take a while to fetch and process...\n`));
94
+ }
95
+ // Set playlist defaults
96
+ if (!params.playlistSettings) {
97
+ params.playlistSettings = {};
98
+ }
99
+ // Only set durationPerItem if not already specified
100
+ if (params.playlistSettings.durationPerItem === undefined) {
101
+ params.playlistSettings.durationPerItem = config.defaultDuration || 10;
102
+ }
103
+ // Only set preserveOrder if not already specified
104
+ if (params.playlistSettings.preserveOrder === undefined) {
105
+ params.playlistSettings.preserveOrder = true;
106
+ }
107
+ return params;
108
+ }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ /**
3
+ * Simple logging utility that respects verbose mode
4
+ */
5
+ var __importDefault = (this && this.__importDefault) || function (mod) {
6
+ return (mod && mod.__esModule) ? mod : { "default": mod };
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.setVerbose = setVerbose;
10
+ exports.debug = debug;
11
+ exports.info = info;
12
+ exports.warn = warn;
13
+ exports.error = error;
14
+ exports.always = always;
15
+ const chalk_1 = __importDefault(require("chalk"));
16
+ // Global verbose flag
17
+ let isVerbose = false;
18
+ /**
19
+ * Set verbose mode
20
+ * @param {boolean} verbose - Whether to enable verbose logging
21
+ */
22
+ function setVerbose(verbose) {
23
+ isVerbose = verbose;
24
+ }
25
+ /**
26
+ * Log debug message (only in verbose mode)
27
+ * @param {...any} args - Arguments to log
28
+ */
29
+ function debug(...args) {
30
+ if (isVerbose) {
31
+ console.log(chalk_1.default.gray('[DEBUG]'), ...args);
32
+ }
33
+ }
34
+ /**
35
+ * Log info message (only in verbose mode)
36
+ * @param {...any} args - Arguments to log
37
+ */
38
+ function info(...args) {
39
+ if (isVerbose) {
40
+ console.log(chalk_1.default.blue('[INFO]'), ...args);
41
+ }
42
+ }
43
+ /**
44
+ * Log warning message (only in verbose mode)
45
+ * @param {...any} args - Arguments to log
46
+ */
47
+ function warn(...args) {
48
+ if (isVerbose) {
49
+ console.warn(chalk_1.default.yellow('[WARN]'), ...args);
50
+ }
51
+ }
52
+ /**
53
+ * Log error message (always shown, but with more details in verbose mode)
54
+ * @param {...any} args - Arguments to log
55
+ */
56
+ function error(...args) {
57
+ if (isVerbose) {
58
+ console.error(chalk_1.default.red('[ERROR]'), ...args);
59
+ }
60
+ else {
61
+ // In non-verbose mode, errors are still shown but handled by the caller
62
+ // This allows the orchestrator to show clean error messages
63
+ console.error(chalk_1.default.red('[ERROR]'), ...args);
64
+ }
65
+ }
66
+ /**
67
+ * Log message that always shows (bypass verbose mode)
68
+ * @param {...any} args - Arguments to log
69
+ */
70
+ function always(...args) {
71
+ console.log(...args);
72
+ }
@@ -0,0 +1,393 @@
1
+ "use strict";
2
+ /**
3
+ * Main Flow Controller
4
+ * Handles both deterministic and AI-driven playlist creation
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
39
+ var __importDefault = (this && this.__importDefault) || function (mod) {
40
+ return (mod && mod.__esModule) ? mod : { "default": mod };
41
+ };
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.validateRequirements = validateRequirements;
44
+ exports.applyPlaylistDefaults = applyPlaylistDefaults;
45
+ exports.buildPlaylistDirect = buildPlaylistDirect;
46
+ exports.buildPlaylist = buildPlaylist;
47
+ // Suppress Ed25519 experimental warning immediately
48
+ const originalEmitWarning = process.emitWarning;
49
+ process.emitWarning = function (warning, type, ctor) {
50
+ if (((typeof type === 'string' && type === 'ExperimentalWarning') ||
51
+ (typeof type === 'object' && type?.name === 'ExperimentalWarning')) &&
52
+ typeof warning === 'string' &&
53
+ warning.includes('Ed25519')) {
54
+ return; // Suppress this warning
55
+ }
56
+ return originalEmitWarning.apply(this, [warning, type, ctor]);
57
+ };
58
+ const chalk_1 = __importDefault(require("chalk"));
59
+ const readline = __importStar(require("readline"));
60
+ const config_1 = require("./config");
61
+ const logger = __importStar(require("./logger"));
62
+ // Lazy load utilities and orchestrator to avoid circular dependencies
63
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
64
+ const getUtilities = () => require('./utilities');
65
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
66
+ const getIntentParser = () => require('./intent-parser');
67
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
68
+ const getAIOrchestrator = () => require('./ai-orchestrator');
69
+ /**
70
+ * Validate and apply constraints to requirements
71
+ *
72
+ * @param {Array<Object>} requirements - Array of requirements
73
+ * @returns {Array<Object>} Validated requirements
74
+ */
75
+ function validateRequirements(requirements) {
76
+ if (!requirements || !Array.isArray(requirements) || requirements.length === 0) {
77
+ throw new Error('At least one requirement is needed');
78
+ }
79
+ return requirements.map((req, index) => {
80
+ // Validate based on requirement type
81
+ if (req.type === 'fetch_feed') {
82
+ // Feed playlist requirement - only needs playlistName and quantity
83
+ if (!req.playlistName) {
84
+ throw new Error(`Requirement ${index + 1}: playlistName is required for fetch_feed`);
85
+ }
86
+ const quantity = typeof req.quantity === 'number' ? Math.min(req.quantity, 20) : 5;
87
+ return {
88
+ ...req,
89
+ quantity,
90
+ };
91
+ }
92
+ // Query address requirement
93
+ if (req.type === 'query_address') {
94
+ // Query all NFTs from an owner address
95
+ if (!req.ownerAddress) {
96
+ throw new Error(`Requirement ${index + 1}: ownerAddress is required for query_address`);
97
+ }
98
+ // Allow "all" as a string, or cap numeric values
99
+ let quantity;
100
+ if (req.quantity === 'all') {
101
+ quantity = 'all';
102
+ }
103
+ else if (typeof req.quantity === 'number') {
104
+ quantity = Math.min(req.quantity, 100);
105
+ }
106
+ else {
107
+ quantity = undefined;
108
+ }
109
+ return {
110
+ ...req,
111
+ quantity,
112
+ };
113
+ }
114
+ // Build playlist requirement
115
+ if (req.type === 'build_playlist') {
116
+ if (!req.blockchain) {
117
+ throw new Error(`Requirement ${index + 1}: blockchain is required for build_playlist`);
118
+ }
119
+ if (!req.tokenIds || req.tokenIds.length === 0) {
120
+ throw new Error(`Requirement ${index + 1}: at least one token ID is required`);
121
+ }
122
+ const quantity = typeof req.quantity === 'number'
123
+ ? Math.min(req.quantity, 20)
124
+ : Math.min(req.tokenIds.length, 20);
125
+ return {
126
+ ...req,
127
+ quantity,
128
+ tokenIds: req.tokenIds || [],
129
+ };
130
+ }
131
+ throw new Error(`Requirement ${index + 1}: invalid type "${req.type}"`);
132
+ });
133
+ }
134
+ /**
135
+ * Apply playlist settings defaults
136
+ *
137
+ * @param {Object} settings - Playlist settings
138
+ * @returns {Object} Settings with defaults
139
+ */
140
+ function applyPlaylistDefaults(settings = {}) {
141
+ const config = (0, config_1.getConfig)();
142
+ return {
143
+ title: settings.title || null,
144
+ slug: settings.slug || null,
145
+ durationPerItem: settings.durationPerItem || config.defaultDuration || 10,
146
+ preserveOrder: settings.preserveOrder !== false,
147
+ deviceName: settings.deviceName,
148
+ };
149
+ }
150
+ /**
151
+ * Build playlist deterministically from structured parameters
152
+ * Path 1: Direct execution (no AI)
153
+ *
154
+ * @param {Object} params - Playlist parameters
155
+ * @param {Array<Object>} params.requirements - Array of requirements
156
+ * @param {Object} [params.playlistSettings] - Playlist settings
157
+ * @param {Object} options - Options
158
+ * @param {boolean} [options.verbose=false] - Verbose output
159
+ * @param {string} [options.outputPath='playlist.json'] - Output path
160
+ * @returns {Promise<Object>} Result with playlist
161
+ */
162
+ async function buildPlaylistDirect(params, options = {}) {
163
+ const requirements = validateRequirements(params.requirements);
164
+ const playlistSettings = applyPlaylistDefaults(params.playlistSettings);
165
+ const utilities = getUtilities();
166
+ const config = (0, config_1.getConfig)();
167
+ // Initialize utilities with config (indexer endpoint, API key, etc.)
168
+ utilities.initializeUtilities(config);
169
+ return await utilities.buildPlaylistDirect({ requirements, playlistSettings }, options);
170
+ }
171
+ /**
172
+ * Build playlist from natural language request
173
+ * Path 2: AI-driven execution (intent parser → AI orchestrator → utilities)
174
+ *
175
+ * @param {string} userRequest - Natural language request
176
+ * @param {Object} options - Options
177
+ * @param {boolean} [options.verbose=false] - Verbose output
178
+ * @param {string} [options.outputPath='playlist.json'] - Output path
179
+ * @param {string} [options.modelName] - AI model to use
180
+ * @param {boolean} [options.interactive=true] - Interactive mode (allow clarification prompts)
181
+ * @returns {Promise<Object>} Result with playlist
182
+ */
183
+ async function buildPlaylist(userRequest, options = {}) {
184
+ const { verbose = false, outputPath = 'playlist.json', modelName, interactive = true } = options;
185
+ // Enable verbose logging if requested
186
+ if (verbose) {
187
+ logger.setVerbose(true);
188
+ }
189
+ // Initialize utilities with config (indexer endpoint, API key, etc.)
190
+ const utilities = getUtilities();
191
+ const config = (0, config_1.getConfig)();
192
+ utilities.initializeUtilities(config);
193
+ try {
194
+ const trimmedRequest = userRequest.trim();
195
+ const sendMatch = trimmedRequest.match(/^send(?:\s+(?:last|playlist|the playlist))?(?:\s+to\s+(.+))?$/i);
196
+ if (sendMatch) {
197
+ const deviceName = sendMatch[1]?.trim();
198
+ const { confirmPlaylistForSending } = await Promise.resolve().then(() => __importStar(require('./utilities/playlist-send')));
199
+ const confirmation = await confirmPlaylistForSending(outputPath, deviceName);
200
+ if (!confirmation.success) {
201
+ if (confirmation.message) {
202
+ console.log(chalk_1.default.red(`\n${confirmation.message}`));
203
+ }
204
+ return {
205
+ success: false,
206
+ error: confirmation.error || 'Failed to prepare playlist for sending',
207
+ action: 'send_playlist',
208
+ playlist: null,
209
+ };
210
+ }
211
+ const sendResult = await utilities.sendToDevice(confirmation.playlist, confirmation.deviceName);
212
+ if (sendResult.success) {
213
+ console.log(chalk_1.default.green('\n✅ Playlist sent successfully!'));
214
+ if (sendResult.deviceName) {
215
+ console.log(chalk_1.default.gray(` Device: ${sendResult.deviceName}`));
216
+ }
217
+ console.log();
218
+ return {
219
+ success: true,
220
+ playlist: confirmation.playlist,
221
+ action: 'send_playlist',
222
+ };
223
+ }
224
+ console.log();
225
+ console.error(chalk_1.default.red('❌ Failed to send playlist'));
226
+ if (sendResult.error) {
227
+ console.error(chalk_1.default.red(` ${sendResult.error}`));
228
+ }
229
+ return {
230
+ success: false,
231
+ error: sendResult.error || 'Failed to send playlist',
232
+ playlist: null,
233
+ action: 'send_playlist',
234
+ };
235
+ }
236
+ // STEP 1: INTENT PARSER
237
+ // Parse user intent into structured requirements
238
+ const { processIntentParserRequest } = getIntentParser();
239
+ let intentParserResult = await processIntentParserRequest(userRequest, {
240
+ modelName,
241
+ });
242
+ // Handle interactive clarification loop
243
+ while (intentParserResult.needsMoreInfo) {
244
+ if (!interactive) {
245
+ // Non-interactive mode: cannot ask for clarification
246
+ console.error(chalk_1.default.red('\n❌ More information needed but running in non-interactive mode. Please provide a complete request.'));
247
+ if (intentParserResult.question) {
248
+ console.error(chalk_1.default.yellow('\nAI asked: ') + intentParserResult.question);
249
+ }
250
+ process.exit(1);
251
+ }
252
+ // Ask user for clarification
253
+ const rl = readline.createInterface({
254
+ input: process.stdin,
255
+ output: process.stdout,
256
+ });
257
+ // Display the AI's question before asking for input
258
+ if (intentParserResult.question) {
259
+ console.log(chalk_1.default.cyan('\n🤖 ') + intentParserResult.question);
260
+ }
261
+ const userResponse = await new Promise((resolve) => {
262
+ rl.question(chalk_1.default.yellow('Your response: '), (answer) => {
263
+ rl.close();
264
+ resolve(answer.trim());
265
+ });
266
+ });
267
+ if (!userResponse) {
268
+ console.error(chalk_1.default.red('\n❌ No response provided. Exiting.'));
269
+ process.exit(1);
270
+ }
271
+ console.log();
272
+ // Continue intent parser conversation
273
+ intentParserResult = await processIntentParserRequest(userResponse, {
274
+ modelName,
275
+ conversationContext: {
276
+ messages: intentParserResult.messages,
277
+ },
278
+ });
279
+ }
280
+ if (!intentParserResult.approved) {
281
+ console.error(chalk_1.default.red('\n❌ Request not approved by intent parser'));
282
+ return null;
283
+ }
284
+ const params = intentParserResult.params;
285
+ // Check if this is a send_playlist action
286
+ if (params && params.action === 'send_playlist') {
287
+ // Handle playlist sending directly
288
+ const sendParams = params;
289
+ const utilities = getUtilities();
290
+ console.log();
291
+ console.log(chalk_1.default.cyan('Sending to device...'));
292
+ const sendResult = await utilities.sendToDevice(sendParams.playlist, sendParams.deviceName);
293
+ if (sendResult.success) {
294
+ console.log(chalk_1.default.green('\n✅ Playlist sent successfully!'));
295
+ if (sendResult.deviceName) {
296
+ console.log(chalk_1.default.gray(` Device: ${sendResult.deviceName}`));
297
+ }
298
+ console.log();
299
+ return {
300
+ success: true,
301
+ playlist: sendParams.playlist,
302
+ action: 'send_playlist',
303
+ };
304
+ }
305
+ else {
306
+ // Send failed - return error without showing the playlist summary
307
+ console.log();
308
+ console.error(chalk_1.default.red('❌ Failed to send playlist'));
309
+ if (sendResult.error) {
310
+ console.error(chalk_1.default.red(` ${sendResult.error}`));
311
+ }
312
+ return {
313
+ success: false,
314
+ error: sendResult.error || 'Failed to send playlist',
315
+ playlist: null,
316
+ action: 'send_playlist',
317
+ };
318
+ }
319
+ }
320
+ // Check if this is a publish_playlist action
321
+ if (params && params.action === 'publish_playlist') {
322
+ // Publishing was already handled by intent parser, just return the result
323
+ const publishParams = params;
324
+ if (publishParams.success) {
325
+ return {
326
+ success: true,
327
+ action: 'publish_playlist',
328
+ playlistId: publishParams.playlistId,
329
+ feedServer: publishParams.feedServer,
330
+ };
331
+ }
332
+ else {
333
+ return {
334
+ success: false,
335
+ error: publishParams.error,
336
+ action: 'publish_playlist',
337
+ };
338
+ }
339
+ }
340
+ // STEP 2: AI ORCHESTRATOR (Function Calling)
341
+ // AI orchestrates function calls to build playlist
342
+ const { buildPlaylistWithAI } = getAIOrchestrator();
343
+ let result = await buildPlaylistWithAI(params, {
344
+ modelName,
345
+ verbose,
346
+ outputPath,
347
+ interactive,
348
+ });
349
+ // Handle confirmation loop in interactive mode
350
+ while (result.needsConfirmation && interactive) {
351
+ console.log(chalk_1.default.yellow('\n' + result.question));
352
+ console.log();
353
+ const rl = readline.createInterface({
354
+ input: process.stdin,
355
+ output: process.stdout,
356
+ });
357
+ const userResponse = await new Promise((resolve) => {
358
+ rl.question(chalk_1.default.cyan('Your response: '), (answer) => {
359
+ rl.close();
360
+ resolve(answer.trim());
361
+ });
362
+ });
363
+ if (!userResponse) {
364
+ console.error(chalk_1.default.red('\n❌ No response provided. Canceling.'));
365
+ return null;
366
+ }
367
+ console.log();
368
+ // Continue orchestrator with user's response
369
+ result = await buildPlaylistWithAI(result.params, {
370
+ modelName,
371
+ verbose,
372
+ outputPath,
373
+ interactive,
374
+ conversationContext: {
375
+ messages: result.messages,
376
+ userResponse,
377
+ },
378
+ });
379
+ }
380
+ // If no playlist was built, display the AI's message
381
+ if (!result.playlist && result.message) {
382
+ console.log(chalk_1.default.yellow('\n⚠️ ' + result.message));
383
+ }
384
+ return result;
385
+ }
386
+ catch (error) {
387
+ console.error(chalk_1.default.red('\n❌ Error:'), error.message);
388
+ if (verbose) {
389
+ console.error(chalk_1.default.gray(error.stack));
390
+ }
391
+ throw error;
392
+ }
393
+ }
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ /**
3
+ * Type definitions for FF1-CLI
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });