@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,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
+ }
44
+ else if (r.type === 'query_address') {
45
+ if (!r.ownerAddress) {
46
+ throw new Error(`Requirement ${index + 1}: ownerAddress is required for query_address`);
47
+ }
48
+ }
49
+ else if (r.type === 'fetch_feed') {
50
+ if (!r.playlistName) {
51
+ throw new Error(`Requirement ${index + 1}: playlistName is required for fetch_feed`);
52
+ }
53
+ }
54
+ else {
55
+ throw new Error(`Requirement ${index + 1}: invalid type "${r.type}"`);
56
+ }
57
+ // Set default quantity if not provided
58
+ // Allow "all" as a string value for query_address type
59
+ let quantity;
60
+ if (r.quantity === 'all') {
61
+ quantity = 'all';
62
+ }
63
+ else if (r.quantity === null || r.quantity === undefined) {
64
+ quantity = 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(`\nRequesting 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(`\nRequesting ${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,82 @@
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.verbose = verbose;
14
+ exports.error = error;
15
+ exports.always = always;
16
+ const chalk_1 = __importDefault(require("chalk"));
17
+ // Global verbose flag
18
+ let isVerbose = false;
19
+ /**
20
+ * Set verbose mode
21
+ * @param {boolean} verbose - Whether to enable verbose logging
22
+ */
23
+ function setVerbose(verbose) {
24
+ isVerbose = verbose;
25
+ }
26
+ /**
27
+ * Log debug message (only in verbose mode)
28
+ * @param {...any} args - Arguments to log
29
+ */
30
+ function debug(...args) {
31
+ if (isVerbose) {
32
+ console.log(chalk_1.default.dim('[DEBUG]'), ...args);
33
+ }
34
+ }
35
+ /**
36
+ * Log info message (only in verbose mode)
37
+ * @param {...any} args - Arguments to log
38
+ */
39
+ function info(...args) {
40
+ if (isVerbose) {
41
+ console.log(chalk_1.default.blue('[INFO]'), ...args);
42
+ }
43
+ }
44
+ /**
45
+ * Log warning message (only in verbose mode)
46
+ * @param {...any} args - Arguments to log
47
+ */
48
+ function warn(...args) {
49
+ if (isVerbose) {
50
+ console.warn(chalk_1.default.yellow('[WARN]'), ...args);
51
+ }
52
+ }
53
+ /**
54
+ * Log message only in verbose mode (no prefix)
55
+ * @param {...any} args - Arguments to log
56
+ */
57
+ function verbose(...args) {
58
+ if (isVerbose) {
59
+ console.log(...args);
60
+ }
61
+ }
62
+ /**
63
+ * Log error message (always shown, but with more details in verbose mode)
64
+ * @param {...any} args - Arguments to log
65
+ */
66
+ function error(...args) {
67
+ if (isVerbose) {
68
+ console.error(chalk_1.default.red('[ERROR]'), ...args);
69
+ }
70
+ else {
71
+ // In non-verbose mode, errors are still shown but handled by the caller
72
+ // This allows the orchestrator to show clean error messages
73
+ console.error(chalk_1.default.red('[ERROR]'), ...args);
74
+ }
75
+ }
76
+ /**
77
+ * Log message that always shows (bypass verbose mode)
78
+ * @param {...any} args - Arguments to log
79
+ */
80
+ function always(...args) {
81
+ console.log(...args);
82
+ }
@@ -0,0 +1,459 @@
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.SEND_SHORTCUT_PATTERN = void 0;
44
+ exports.resolveSendShortcutDevice = resolveSendShortcutDevice;
45
+ exports.resolveEffectiveDeviceName = resolveEffectiveDeviceName;
46
+ exports.resolveSendPlaylistDeviceName = resolveSendPlaylistDeviceName;
47
+ exports.validateRequirements = validateRequirements;
48
+ exports.applyPlaylistDefaults = applyPlaylistDefaults;
49
+ exports.buildPlaylistDirect = buildPlaylistDirect;
50
+ exports.buildPlaylist = buildPlaylist;
51
+ // Suppress Ed25519 experimental warning immediately
52
+ const originalEmitWarning = process.emitWarning;
53
+ process.emitWarning = function (warning, type, ctor) {
54
+ if (((typeof type === 'string' && type === 'ExperimentalWarning') ||
55
+ (typeof type === 'object' && type?.name === 'ExperimentalWarning')) &&
56
+ typeof warning === 'string' &&
57
+ warning.includes('Ed25519')) {
58
+ return; // Suppress this warning
59
+ }
60
+ return originalEmitWarning.apply(this, [warning, type, ctor]);
61
+ };
62
+ const chalk_1 = __importDefault(require("chalk"));
63
+ const readline = __importStar(require("readline"));
64
+ const config_1 = require("./config");
65
+ const logger = __importStar(require("./logger"));
66
+ // Lazy load utilities and orchestrator to avoid circular dependencies
67
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
68
+ const getUtilities = () => require('./utilities');
69
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
70
+ const getIntentParser = () => require('./intent-parser');
71
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
72
+ const getAIOrchestrator = () => require('./ai-orchestrator');
73
+ /**
74
+ * Regex that matches the inline "send last / send playlist / send to <device>"
75
+ * shortcut inside the chat flow. Exported for testing.
76
+ */
77
+ exports.SEND_SHORTCUT_PATTERN = /^send(?:\s+(?:last|playlist|the playlist))?(?:\s+to\s+(.+))?$/i;
78
+ /**
79
+ * Extract the target device from a sendMatch result, falling back to the CLI
80
+ * --device flag. Exported for testing.
81
+ */
82
+ function resolveSendShortcutDevice(match, cliDefault) {
83
+ return match[1]?.trim() || cliDefault;
84
+ }
85
+ /**
86
+ * Resolve the effective device name for a send operation.
87
+ * The intent (from NL parsing) takes precedence; CLI flag is the fallback.
88
+ * Exported for testing.
89
+ */
90
+ function resolveEffectiveDeviceName(fromIntent, fromCLI) {
91
+ return fromIntent || fromCLI;
92
+ }
93
+ /**
94
+ * Sanitize and resolve the device name for the direct send_playlist action path.
95
+ *
96
+ * The intent parser can emit literal "null" or "" as a device name string; these
97
+ * must be treated as absent so the CLI --device fallback is used instead.
98
+ * Exported for testing — mirrors the sanitization in confirmPlaylistForSending().
99
+ */
100
+ function resolveSendPlaylistDeviceName(intentDeviceName, cliDeviceName) {
101
+ const sanitized = intentDeviceName === 'null' ||
102
+ intentDeviceName === '' ||
103
+ intentDeviceName === null ||
104
+ intentDeviceName === undefined
105
+ ? undefined
106
+ : intentDeviceName;
107
+ return resolveEffectiveDeviceName(sanitized, cliDeviceName);
108
+ }
109
+ /**
110
+ * Validate and apply constraints to requirements
111
+ *
112
+ * @param {Array<Object>} requirements - Array of requirements
113
+ * @returns {Array<Object>} Validated requirements
114
+ */
115
+ function validateRequirements(requirements) {
116
+ if (!requirements || !Array.isArray(requirements) || requirements.length === 0) {
117
+ throw new Error('At least one requirement is needed');
118
+ }
119
+ return requirements.map((req, index) => {
120
+ // Validate based on requirement type
121
+ if (req.type === 'fetch_feed') {
122
+ // Feed playlist requirement - only needs playlistName and quantity
123
+ if (!req.playlistName) {
124
+ throw new Error(`Requirement ${index + 1}: playlistName is required for fetch_feed`);
125
+ }
126
+ const quantity = typeof req.quantity === 'number' ? Math.min(req.quantity, 20) : 5;
127
+ return {
128
+ ...req,
129
+ quantity,
130
+ };
131
+ }
132
+ // Query address requirement
133
+ if (req.type === 'query_address') {
134
+ // Query all NFTs from an owner address
135
+ if (!req.ownerAddress) {
136
+ throw new Error(`Requirement ${index + 1}: ownerAddress is required for query_address`);
137
+ }
138
+ // Allow "all" as a string, or cap numeric values
139
+ let quantity;
140
+ if (req.quantity === 'all') {
141
+ quantity = 'all';
142
+ }
143
+ else if (typeof req.quantity === 'number') {
144
+ quantity = Math.min(req.quantity, 100);
145
+ }
146
+ else {
147
+ quantity = undefined;
148
+ }
149
+ return {
150
+ ...req,
151
+ quantity,
152
+ };
153
+ }
154
+ // Build playlist requirement
155
+ if (req.type === 'build_playlist') {
156
+ if (!req.blockchain) {
157
+ throw new Error(`Requirement ${index + 1}: blockchain is required for build_playlist`);
158
+ }
159
+ if (!req.contractAddress) {
160
+ throw new Error(`Requirement ${index + 1}: contractAddress is required for build_playlist`);
161
+ }
162
+ // tokenIds is now optional - if not provided, query random tokens from contract
163
+ if (req.tokenIds && req.tokenIds.length > 0) {
164
+ // Specific token IDs provided
165
+ const quantity = typeof req.quantity === 'number'
166
+ ? Math.min(req.quantity, 20)
167
+ : Math.min(req.tokenIds.length, 20);
168
+ return {
169
+ ...req,
170
+ quantity,
171
+ tokenIds: req.tokenIds,
172
+ };
173
+ }
174
+ else {
175
+ // No token IDs - query random tokens from contract
176
+ const quantity = typeof req.quantity === 'number' ? Math.min(req.quantity, 100) : 100;
177
+ return {
178
+ ...req,
179
+ quantity,
180
+ tokenIds: undefined, // Explicitly set to undefined
181
+ };
182
+ }
183
+ }
184
+ throw new Error(`Requirement ${index + 1}: invalid type "${req.type}"`);
185
+ });
186
+ }
187
+ /**
188
+ * Apply playlist settings defaults
189
+ *
190
+ * @param {Object} settings - Playlist settings
191
+ * @returns {Object} Settings with defaults
192
+ */
193
+ function applyPlaylistDefaults(settings = {}) {
194
+ const config = (0, config_1.getConfig)();
195
+ return {
196
+ title: settings.title || null,
197
+ slug: settings.slug || null,
198
+ durationPerItem: settings.durationPerItem || config.defaultDuration || 10,
199
+ preserveOrder: settings.preserveOrder !== false,
200
+ deviceName: settings.deviceName,
201
+ };
202
+ }
203
+ /**
204
+ * Build playlist deterministically from structured parameters
205
+ * Path 1: Direct execution (no AI)
206
+ *
207
+ * @param {Object} params - Playlist parameters
208
+ * @param {Array<Object>} params.requirements - Array of requirements
209
+ * @param {Object} [params.playlistSettings] - Playlist settings
210
+ * @param {Object} options - Options
211
+ * @param {boolean} [options.verbose=false] - Verbose output
212
+ * @param {string} [options.outputPath='playlist.json'] - Output path
213
+ * @returns {Promise<Object>} Result with playlist
214
+ */
215
+ async function buildPlaylistDirect(params, options = {}) {
216
+ const requirements = validateRequirements(params.requirements);
217
+ const playlistSettings = applyPlaylistDefaults(params.playlistSettings);
218
+ const utilities = getUtilities();
219
+ const config = (0, config_1.getConfig)();
220
+ // Initialize utilities with config (indexer endpoint, API key, etc.)
221
+ utilities.initializeUtilities(config);
222
+ return await utilities.buildPlaylistDirect({ requirements, playlistSettings }, options);
223
+ }
224
+ /**
225
+ * Build playlist from natural language request
226
+ * Path 2: AI-driven execution (intent parser → AI orchestrator → utilities)
227
+ *
228
+ * @param {string} userRequest - Natural language request
229
+ * @param {Object} options - Options
230
+ * @param {boolean} [options.verbose=false] - Verbose output
231
+ * @param {string} [options.outputPath='playlist.json'] - Output path
232
+ * @param {string} [options.modelName] - AI model to use
233
+ * @param {boolean} [options.interactive=true] - Interactive mode (allow clarification prompts)
234
+ * @returns {Promise<Object>} Result with playlist
235
+ */
236
+ async function buildPlaylist(userRequest, options = {}) {
237
+ const { verbose = false, outputPath = 'playlist.json', modelName, interactive = true, deviceName: defaultDeviceName, } = options;
238
+ // Enable verbose logging if requested
239
+ if (verbose) {
240
+ logger.setVerbose(true);
241
+ }
242
+ // Initialize utilities with config (indexer endpoint, API key, etc.)
243
+ const utilities = getUtilities();
244
+ const config = (0, config_1.getConfig)();
245
+ utilities.initializeUtilities(config);
246
+ try {
247
+ const trimmedRequest = userRequest.trim();
248
+ const sendMatch = trimmedRequest.match(/^send(?:\s+(?:last|playlist|the playlist))?(?:\s+to\s+(.+))?$/i);
249
+ if (sendMatch) {
250
+ const deviceName = sendMatch[1]?.trim() || defaultDeviceName;
251
+ const { confirmPlaylistForSending } = await Promise.resolve().then(() => __importStar(require('./utilities/playlist-send')));
252
+ const confirmation = await confirmPlaylistForSending(outputPath, deviceName);
253
+ if (!confirmation.success) {
254
+ if (confirmation.message) {
255
+ console.log(chalk_1.default.red(`\n${confirmation.message}`));
256
+ }
257
+ return {
258
+ success: false,
259
+ error: confirmation.error || 'Failed to prepare playlist for sending',
260
+ action: 'send_playlist',
261
+ playlist: null,
262
+ };
263
+ }
264
+ const sendResult = await utilities.sendToDevice(confirmation.playlist, confirmation.deviceName);
265
+ if (sendResult.success) {
266
+ console.log(chalk_1.default.green('\nPlaylist sent'));
267
+ if (sendResult.deviceName) {
268
+ console.log(chalk_1.default.dim(` Device: ${sendResult.deviceName}`));
269
+ }
270
+ console.log();
271
+ return {
272
+ success: true,
273
+ playlist: confirmation.playlist,
274
+ action: 'send_playlist',
275
+ };
276
+ }
277
+ console.log();
278
+ console.error(chalk_1.default.red('Send failed'));
279
+ if (sendResult.error) {
280
+ console.error(chalk_1.default.red(` ${sendResult.error}`));
281
+ }
282
+ return {
283
+ success: false,
284
+ error: sendResult.error || 'Failed to send playlist',
285
+ playlist: null,
286
+ action: 'send_playlist',
287
+ };
288
+ }
289
+ // STEP 1: INTENT PARSER
290
+ // Parse user intent into structured requirements
291
+ const { processIntentParserRequest } = getIntentParser();
292
+ let intentParserResult = await processIntentParserRequest(userRequest, {
293
+ modelName,
294
+ defaultDeviceName,
295
+ });
296
+ // Handle interactive clarification loop
297
+ while (intentParserResult.needsMoreInfo) {
298
+ if (!interactive) {
299
+ // Non-interactive mode: cannot ask for clarification
300
+ console.error(chalk_1.default.red('\nNeed more information, but running in non-interactive mode. Provide a complete request.'));
301
+ if (intentParserResult.question) {
302
+ console.error(chalk_1.default.yellow('\nQuestion: ') + intentParserResult.question);
303
+ }
304
+ process.exit(1);
305
+ }
306
+ // Ask user for clarification
307
+ const rl = readline.createInterface({
308
+ input: process.stdin,
309
+ output: process.stdout,
310
+ });
311
+ // Display the AI's question before asking for input
312
+ if (intentParserResult.question) {
313
+ console.log(chalk_1.default.cyan('\n') + intentParserResult.question);
314
+ }
315
+ const userResponse = await new Promise((resolve) => {
316
+ rl.question(chalk_1.default.yellow('Your response: '), (answer) => {
317
+ rl.close();
318
+ resolve(answer.trim());
319
+ });
320
+ });
321
+ if (!userResponse) {
322
+ console.error(chalk_1.default.red('\nNo response provided. Exiting.'));
323
+ process.exit(1);
324
+ }
325
+ console.log();
326
+ // Continue intent parser conversation
327
+ intentParserResult = await processIntentParserRequest(userResponse, {
328
+ modelName,
329
+ defaultDeviceName,
330
+ conversationContext: {
331
+ messages: intentParserResult.messages,
332
+ },
333
+ });
334
+ }
335
+ if (!intentParserResult.approved) {
336
+ console.error(chalk_1.default.red('\nRequest not approved by intent parser'));
337
+ return null;
338
+ }
339
+ const params = intentParserResult.params;
340
+ // NOTE: do NOT merge defaultDeviceName into playlistSettings here.
341
+ // buildPlaylistDirect (src/utilities/index.js) sends whenever
342
+ // playlistSettings.deviceName is defined, so setting it here would turn
343
+ // every build-only `chat --device` invocation into an implicit network send.
344
+ // The CLI --device flag is used only in the two explicit send paths below
345
+ // (sendMatch shortcut and send_playlist action) via resolveEffectiveDeviceName.
346
+ // Check if this is a send_playlist action
347
+ if (params && params.action === 'send_playlist') {
348
+ // Handle playlist sending directly
349
+ const sendParams = params;
350
+ const utilities = getUtilities();
351
+ console.log();
352
+ console.log(chalk_1.default.cyan('Sending to device'));
353
+ const sendResult = await utilities.sendToDevice(sendParams.playlist, resolveSendPlaylistDeviceName(sendParams.deviceName, defaultDeviceName));
354
+ if (sendResult.success) {
355
+ console.log(chalk_1.default.green('\nPlaylist sent'));
356
+ if (sendResult.deviceName) {
357
+ console.log(chalk_1.default.dim(` Device: ${sendResult.deviceName}`));
358
+ }
359
+ console.log();
360
+ return {
361
+ success: true,
362
+ playlist: sendParams.playlist,
363
+ action: 'send_playlist',
364
+ };
365
+ }
366
+ else {
367
+ // Send failed - return error without showing the playlist summary
368
+ console.log();
369
+ console.error(chalk_1.default.red('Send failed'));
370
+ if (sendResult.error) {
371
+ console.error(chalk_1.default.red(` ${sendResult.error}`));
372
+ }
373
+ return {
374
+ success: false,
375
+ error: sendResult.error || 'Failed to send playlist',
376
+ playlist: null,
377
+ action: 'send_playlist',
378
+ };
379
+ }
380
+ }
381
+ // Check if this is a publish_playlist action
382
+ if (params && params.action === 'publish_playlist') {
383
+ // Publishing was already handled by intent parser, just return the result
384
+ const publishParams = params;
385
+ if (publishParams.success) {
386
+ return {
387
+ success: true,
388
+ action: 'publish_playlist',
389
+ playlistId: publishParams.playlistId,
390
+ feedServer: publishParams.feedServer,
391
+ };
392
+ }
393
+ else {
394
+ return {
395
+ success: false,
396
+ error: publishParams.error,
397
+ action: 'publish_playlist',
398
+ };
399
+ }
400
+ }
401
+ // STEP 2: AI ORCHESTRATOR (Function Calling)
402
+ // AI orchestrates function calls to build playlist
403
+ const { buildPlaylistWithAI } = getAIOrchestrator();
404
+ let result = await buildPlaylistWithAI(params, {
405
+ modelName,
406
+ verbose,
407
+ outputPath,
408
+ interactive,
409
+ defaultDeviceName,
410
+ });
411
+ // Handle confirmation loop in interactive mode
412
+ while (result.needsConfirmation && interactive) {
413
+ console.log(chalk_1.default.yellow('\n' + result.question));
414
+ console.log();
415
+ const rl = readline.createInterface({
416
+ input: process.stdin,
417
+ output: process.stdout,
418
+ });
419
+ const userResponse = await new Promise((resolve) => {
420
+ rl.question(chalk_1.default.cyan('Your response: '), (answer) => {
421
+ rl.close();
422
+ resolve(answer.trim());
423
+ });
424
+ });
425
+ if (!userResponse) {
426
+ console.error(chalk_1.default.red('\nNo response provided. Canceling.'));
427
+ return null;
428
+ }
429
+ console.log();
430
+ // Continue orchestrator with user's response
431
+ result = await buildPlaylistWithAI(result.params, {
432
+ modelName,
433
+ verbose,
434
+ outputPath,
435
+ interactive,
436
+ defaultDeviceName,
437
+ conversationContext: {
438
+ messages: result.messages,
439
+ userResponse,
440
+ },
441
+ });
442
+ }
443
+ // If no playlist was built, display the AI's message
444
+ if (!result.playlist && result.message) {
445
+ console.log(chalk_1.default.yellow('\n' + result.message));
446
+ }
447
+ else if (!result.playlist && result.error) {
448
+ console.error(chalk_1.default.red('\n' + result.error));
449
+ }
450
+ return result;
451
+ }
452
+ catch (error) {
453
+ console.error(chalk_1.default.red('\nError:'), error.message);
454
+ if (verbose) {
455
+ console.error(chalk_1.default.dim(error.stack));
456
+ }
457
+ throw error;
458
+ }
459
+ }
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ /**
3
+ * Type definitions for ff-cli
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });