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
|
@@ -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
|
+
}
|
package/dist/src/main.js
ADDED
|
@@ -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
|
+
}
|