@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.
- package/LICENSE +21 -0
- package/README.md +96 -0
- package/config.json.example +96 -0
- package/dist/index.js +54 -0
- package/dist/src/ai-orchestrator/index.js +1019 -0
- package/dist/src/ai-orchestrator/registry.js +96 -0
- package/dist/src/commands/build.js +69 -0
- package/dist/src/commands/chat.js +189 -0
- package/dist/src/commands/config.js +68 -0
- package/dist/src/commands/device.js +278 -0
- package/dist/src/commands/helpers/config-files.js +62 -0
- package/dist/src/commands/helpers/device-discovery.js +111 -0
- package/dist/src/commands/helpers/playlist-display.js +161 -0
- package/dist/src/commands/helpers/prompt.js +65 -0
- package/dist/src/commands/helpers/ssh-helpers.js +44 -0
- package/dist/src/commands/play.js +110 -0
- package/dist/src/commands/publish.js +115 -0
- package/dist/src/commands/setup.js +225 -0
- package/dist/src/commands/sign.js +41 -0
- package/dist/src/commands/ssh.js +108 -0
- package/dist/src/commands/status.js +126 -0
- package/dist/src/commands/validate.js +18 -0
- package/dist/src/config.js +441 -0
- package/dist/src/intent-parser/index.js +1382 -0
- package/dist/src/intent-parser/utils.js +108 -0
- package/dist/src/logger.js +82 -0
- package/dist/src/main.js +459 -0
- package/dist/src/types.js +5 -0
- package/dist/src/utilities/address-validator.js +242 -0
- package/dist/src/utilities/device-default.js +36 -0
- package/dist/src/utilities/device-lookup.js +107 -0
- package/dist/src/utilities/device-normalize.js +62 -0
- package/dist/src/utilities/device-upsert.js +91 -0
- package/dist/src/utilities/domain-resolver.js +291 -0
- package/dist/src/utilities/ed25519-key-derive.js +155 -0
- package/dist/src/utilities/feed-fetcher.js +471 -0
- package/dist/src/utilities/ff1-compatibility.js +269 -0
- package/dist/src/utilities/ff1-device.js +250 -0
- package/dist/src/utilities/ff1-discovery.js +330 -0
- package/dist/src/utilities/functions.js +308 -0
- package/dist/src/utilities/index.js +469 -0
- package/dist/src/utilities/nft-indexer.js +1024 -0
- package/dist/src/utilities/playlist-builder.js +523 -0
- package/dist/src/utilities/playlist-publisher.js +131 -0
- package/dist/src/utilities/playlist-send.js +260 -0
- package/dist/src/utilities/playlist-signer.js +204 -0
- package/dist/src/utilities/playlist-signing-role.js +41 -0
- package/dist/src/utilities/playlist-source.js +128 -0
- package/dist/src/utilities/playlist-verifier.js +274 -0
- package/dist/src/utilities/ssh-access.js +145 -0
- package/dist/src/utils.js +48 -0
- package/docs/CONFIGURATION.md +206 -0
- package/docs/EXAMPLES.md +390 -0
- package/docs/FUNCTION_CALLING.md +96 -0
- package/docs/PROJECT_SPEC.md +228 -0
- package/docs/README.md +348 -0
- package/docs/RELEASING.md +73 -0
- 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
|
+
}
|
package/dist/src/main.js
ADDED
|
@@ -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
|
+
}
|