ff1-cli 1.0.1 → 1.0.3
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/README.md +12 -2
- package/config.json.example +11 -9
- package/dist/index.js +271 -82
- package/dist/src/ai-orchestrator/index.js +62 -5
- package/dist/src/config.js +16 -38
- package/dist/src/intent-parser/index.js +58 -16
- package/dist/src/intent-parser/utils.js +5 -2
- package/dist/src/logger.js +10 -0
- package/dist/src/utilities/ff1-compatibility.js +269 -0
- package/dist/src/utilities/ff1-device.js +9 -27
- package/dist/src/utilities/ff1-discovery.js +147 -0
- package/dist/src/utilities/functions.js +8 -26
- package/dist/src/utilities/index.js +9 -3
- package/dist/src/utilities/playlist-send.js +36 -17
- package/dist/src/utilities/playlist-source.js +77 -0
- package/dist/src/utilities/ssh-access.js +145 -0
- package/docs/CONFIGURATION.md +22 -8
- package/docs/EXAMPLES.md +5 -1
- package/docs/README.md +24 -4
- package/docs/RELEASING.md +33 -4
- package/package.json +3 -10
package/dist/src/config.js
CHANGED
|
@@ -52,8 +52,8 @@ function loadConfig() {
|
|
|
52
52
|
gpt: {
|
|
53
53
|
apiKey: process.env.OPENAI_API_KEY || '',
|
|
54
54
|
baseURL: 'https://api.openai.com/v1',
|
|
55
|
-
model: 'gpt-
|
|
56
|
-
availableModels: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo'],
|
|
55
|
+
model: 'gpt-4.1',
|
|
56
|
+
availableModels: ['gpt-4.1', 'gpt-4.1-mini', 'gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo'],
|
|
57
57
|
timeout: 30000,
|
|
58
58
|
maxRetries: 3,
|
|
59
59
|
temperature: 0.3,
|
|
@@ -80,7 +80,7 @@ function loadConfig() {
|
|
|
80
80
|
feed: {
|
|
81
81
|
baseURLs: process.env.FEED_BASE_URLS
|
|
82
82
|
? process.env.FEED_BASE_URLS.split(',')
|
|
83
|
-
: ['https://feed.
|
|
83
|
+
: ['https://dp1-feed-operator-api-prod.autonomy-system.workers.dev/api/v1'],
|
|
84
84
|
},
|
|
85
85
|
};
|
|
86
86
|
// Try to load config.json if it exists
|
|
@@ -105,8 +105,9 @@ function loadConfig() {
|
|
|
105
105
|
models: mergedModels,
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
|
-
catch (
|
|
109
|
-
|
|
108
|
+
catch (error) {
|
|
109
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
110
|
+
console.warn(`Warning: Failed to parse config at ${configPath}. ${message}. Using defaults.`);
|
|
110
111
|
return defaultConfig;
|
|
111
112
|
}
|
|
112
113
|
}
|
|
@@ -161,7 +162,7 @@ function getBrowserConfig() {
|
|
|
161
162
|
* Get playlist configuration including private key for signing
|
|
162
163
|
*
|
|
163
164
|
* @returns {Object} Playlist configuration
|
|
164
|
-
* @returns {string|null} returns.privateKey - Ed25519 private key in base64 format (null if not configured)
|
|
165
|
+
* @returns {string|null} returns.privateKey - Ed25519 private key in base64 or hex format (null if not configured)
|
|
165
166
|
*/
|
|
166
167
|
function getPlaylistConfig() {
|
|
167
168
|
const config = getConfig();
|
|
@@ -202,7 +203,7 @@ function getFeedConfig() {
|
|
|
202
203
|
}
|
|
203
204
|
else {
|
|
204
205
|
// Default feed URL
|
|
205
|
-
urls = ['https://feed.
|
|
206
|
+
urls = ['https://dp1-feed-operator-api-prod.autonomy-system.workers.dev/api/v1'];
|
|
206
207
|
}
|
|
207
208
|
return {
|
|
208
209
|
baseURLs: urls,
|
|
@@ -241,24 +242,9 @@ function getFF1DeviceConfig() {
|
|
|
241
242
|
* @returns {boolean} returns.supportsFunctionCalling - Whether model supports function calling
|
|
242
243
|
* @throws {Error} If model is not configured or doesn't support function calling
|
|
243
244
|
*/
|
|
244
|
-
function resolveModelName(config, modelName) {
|
|
245
|
-
const selectedModel = modelName || config.defaultModel;
|
|
246
|
-
if (config.models[selectedModel]) {
|
|
247
|
-
return selectedModel;
|
|
248
|
-
}
|
|
249
|
-
const aliasMap = {
|
|
250
|
-
gpt: 'chatgpt',
|
|
251
|
-
chatgpt: 'gpt',
|
|
252
|
-
};
|
|
253
|
-
const alias = aliasMap[selectedModel];
|
|
254
|
-
if (alias && config.models[alias]) {
|
|
255
|
-
return alias;
|
|
256
|
-
}
|
|
257
|
-
return selectedModel;
|
|
258
|
-
}
|
|
259
245
|
function getModelConfig(modelName) {
|
|
260
246
|
const config = getConfig();
|
|
261
|
-
const selectedModel =
|
|
247
|
+
const selectedModel = modelName || config.defaultModel;
|
|
262
248
|
if (!config.models[selectedModel]) {
|
|
263
249
|
throw new Error(`Model "${selectedModel}" is not configured. Available models: ${Object.keys(config.models).join(', ')}`);
|
|
264
250
|
}
|
|
@@ -285,7 +271,7 @@ function validateConfig(modelName) {
|
|
|
285
271
|
const errors = [];
|
|
286
272
|
try {
|
|
287
273
|
const config = getConfig();
|
|
288
|
-
const selectedModel =
|
|
274
|
+
const selectedModel = modelName || config.defaultModel;
|
|
289
275
|
if (!config.models[selectedModel]) {
|
|
290
276
|
errors.push(`Model "${selectedModel}" is not configured. Available: ${Object.keys(config.models).join(', ')}`);
|
|
291
277
|
return { valid: false, errors };
|
|
@@ -318,13 +304,12 @@ function validateConfig(modelName) {
|
|
|
318
304
|
// Validate playlist configuration (optional, but warn if configured incorrectly)
|
|
319
305
|
if (config.playlist && config.playlist.privateKey) {
|
|
320
306
|
const key = config.playlist.privateKey;
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
key.length > 0) {
|
|
324
|
-
// Check if it looks like valid base64
|
|
307
|
+
const placeholderPattern = /your_ed25519_private_key/i;
|
|
308
|
+
if (!placeholderPattern.test(key) && typeof key === 'string' && key.length > 0) {
|
|
325
309
|
const base64Regex = /^[A-Za-z0-9+/]+=*$/;
|
|
326
|
-
|
|
327
|
-
|
|
310
|
+
const hexRegex = /^(0x)?[0-9a-fA-F]+$/;
|
|
311
|
+
if (!base64Regex.test(key) && !hexRegex.test(key)) {
|
|
312
|
+
errors.push('playlist.privateKey must be a valid base64- or hex-encoded ed25519 private key');
|
|
328
313
|
}
|
|
329
314
|
}
|
|
330
315
|
}
|
|
@@ -377,12 +362,5 @@ async function createSampleConfig(targetPath) {
|
|
|
377
362
|
*/
|
|
378
363
|
function listAvailableModels() {
|
|
379
364
|
const config = getConfig();
|
|
380
|
-
|
|
381
|
-
if (modelNames.has('gpt')) {
|
|
382
|
-
modelNames.add('chatgpt');
|
|
383
|
-
}
|
|
384
|
-
if (modelNames.has('chatgpt')) {
|
|
385
|
-
modelNames.add('gpt');
|
|
386
|
-
}
|
|
387
|
-
return Array.from(modelNames);
|
|
365
|
+
return Object.keys(config.models);
|
|
388
366
|
}
|
|
@@ -48,6 +48,7 @@ const openai_1 = __importDefault(require("openai"));
|
|
|
48
48
|
const chalk_1 = __importDefault(require("chalk"));
|
|
49
49
|
const config_1 = require("../config");
|
|
50
50
|
const utils_1 = require("./utils");
|
|
51
|
+
const logger = __importStar(require("../logger"));
|
|
51
52
|
// Cache for AI clients
|
|
52
53
|
const clientCache = new Map();
|
|
53
54
|
/**
|
|
@@ -210,7 +211,7 @@ PUBLISH INTENT (CRITICAL)
|
|
|
210
211
|
2. If only 1 server → use it directly in playlistSettings.feedServer
|
|
211
212
|
3. If 2+ servers → ask user "Which feed server?" with numbered list (e.g., "1) https://feed.feralfile.com 2) http://localhost:8787")
|
|
212
213
|
4. After selection, set playlistSettings.feedServer = { baseUrl, apiKey } from selected server
|
|
213
|
-
5. Acknowledge in Settings bullets (e.g., "publish to: https://feed.
|
|
214
|
+
5. Acknowledge in Settings bullets (e.g., "publish to: https://dp1-feed-operator-api-prod.autonomy-system.workers.dev/api/v1")
|
|
214
215
|
- User can request both device display AND publishing (e.g., "send to FF1 and publish to feed") → set both deviceName and feedServer
|
|
215
216
|
- Publishing happens automatically after playlist verification passes
|
|
216
217
|
|
|
@@ -369,13 +370,13 @@ const intentParserFunctionSchemas = [
|
|
|
369
370
|
type: 'function',
|
|
370
371
|
function: {
|
|
371
372
|
name: 'confirm_send_playlist',
|
|
372
|
-
description: 'Confirm the playlist file path and device name for sending. This function is called after the user mentions "send" or similar phrases.',
|
|
373
|
+
description: 'Confirm the playlist file path or hosted URL and device name for sending. This function is called after the user mentions "send" or similar phrases.',
|
|
373
374
|
parameters: {
|
|
374
375
|
type: 'object',
|
|
375
376
|
properties: {
|
|
376
377
|
filePath: {
|
|
377
378
|
type: 'string',
|
|
378
|
-
description: 'Path to
|
|
379
|
+
description: 'Path to playlist file or playlist URL (default: "./playlist.json")',
|
|
379
380
|
},
|
|
380
381
|
deviceName: {
|
|
381
382
|
type: 'string',
|
|
@@ -483,16 +484,53 @@ function printMarkdownContent(content) {
|
|
|
483
484
|
const lines = content.split('\n');
|
|
484
485
|
for (const line of lines) {
|
|
485
486
|
if (line.trim()) {
|
|
486
|
-
|
|
487
|
+
logger.verbose(formatMarkdown(line));
|
|
487
488
|
}
|
|
488
489
|
else if (line === '') {
|
|
489
|
-
|
|
490
|
+
logger.verbose();
|
|
490
491
|
}
|
|
491
492
|
}
|
|
492
493
|
}
|
|
493
494
|
function sleep(ms) {
|
|
494
495
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
495
496
|
}
|
|
497
|
+
function extractDomains(text) {
|
|
498
|
+
if (!text) {
|
|
499
|
+
return [];
|
|
500
|
+
}
|
|
501
|
+
const matches = text.match(/[a-z0-9-]+\.(eth|tez)/gi);
|
|
502
|
+
return matches ? matches.map((match) => match.toLowerCase()) : [];
|
|
503
|
+
}
|
|
504
|
+
function normalizeDomainInput(address, userDomains) {
|
|
505
|
+
if (!address || userDomains.length === 0) {
|
|
506
|
+
return address;
|
|
507
|
+
}
|
|
508
|
+
const lower = address.toLowerCase();
|
|
509
|
+
if (!lower.endsWith('.eth') && !lower.endsWith('.tez')) {
|
|
510
|
+
return address;
|
|
511
|
+
}
|
|
512
|
+
const baseName = lower.replace(/\.(eth|tez)$/i, '');
|
|
513
|
+
const match = userDomains.find((domain) => domain.replace(/\.(eth|tez)$/i, '') === baseName);
|
|
514
|
+
return match || address;
|
|
515
|
+
}
|
|
516
|
+
function normalizeRequirementDomains(params, userDomains) {
|
|
517
|
+
if (userDomains.length === 0) {
|
|
518
|
+
return params;
|
|
519
|
+
}
|
|
520
|
+
const normalized = params.requirements.map((req) => {
|
|
521
|
+
if (req.type !== 'query_address' || typeof req.ownerAddress !== 'string') {
|
|
522
|
+
return req;
|
|
523
|
+
}
|
|
524
|
+
return {
|
|
525
|
+
...req,
|
|
526
|
+
ownerAddress: normalizeDomainInput(req.ownerAddress, userDomains),
|
|
527
|
+
};
|
|
528
|
+
});
|
|
529
|
+
return {
|
|
530
|
+
...params,
|
|
531
|
+
requirements: normalized,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
496
534
|
function buildToolResponseMessages(toolCalls, responses) {
|
|
497
535
|
return toolCalls
|
|
498
536
|
.filter((toolCall) => toolCall.id)
|
|
@@ -514,7 +552,7 @@ async function processNonStreamingResponse(response) {
|
|
|
514
552
|
}
|
|
515
553
|
if (message.content) {
|
|
516
554
|
printMarkdownContent(message.content);
|
|
517
|
-
|
|
555
|
+
logger.verbose();
|
|
518
556
|
}
|
|
519
557
|
return { message };
|
|
520
558
|
}
|
|
@@ -586,10 +624,10 @@ async function processStreamingResponse(stream) {
|
|
|
586
624
|
for (const line of lines) {
|
|
587
625
|
if (line.trim()) {
|
|
588
626
|
const formatted = formatMarkdown(line);
|
|
589
|
-
|
|
627
|
+
logger.verbose(formatted);
|
|
590
628
|
}
|
|
591
629
|
else if (line === '') {
|
|
592
|
-
|
|
630
|
+
logger.verbose();
|
|
593
631
|
}
|
|
594
632
|
}
|
|
595
633
|
printedUpTo = lastNewlineIndex + 1;
|
|
@@ -635,11 +673,11 @@ async function processStreamingResponse(stream) {
|
|
|
635
673
|
const remainingText = contentBuffer.substring(printedUpTo);
|
|
636
674
|
if (remainingText.trim()) {
|
|
637
675
|
const formatted = formatMarkdown(remainingText);
|
|
638
|
-
|
|
676
|
+
logger.verbose(formatted);
|
|
639
677
|
}
|
|
640
678
|
}
|
|
641
679
|
if (contentBuffer.length > 0) {
|
|
642
|
-
|
|
680
|
+
logger.verbose(); // Extra newline after AI response
|
|
643
681
|
}
|
|
644
682
|
// Convert toolCallsMap to array
|
|
645
683
|
toolCalls = Object.values(toolCallsMap).filter((tc) => tc.id);
|
|
@@ -664,6 +702,7 @@ async function processStreamingResponse(stream) {
|
|
|
664
702
|
*/
|
|
665
703
|
async function processIntentParserRequest(userRequest, options = {}) {
|
|
666
704
|
const { modelName, conversationContext } = options;
|
|
705
|
+
const userDomains = extractDomains(userRequest);
|
|
667
706
|
const client = createIntentParserClient(modelName);
|
|
668
707
|
const modelConfig = (0, config_1.getModelConfig)(modelName);
|
|
669
708
|
const config = (0, config_1.getConfig)();
|
|
@@ -734,7 +773,7 @@ async function processIntentParserRequest(userRequest, options = {}) {
|
|
|
734
773
|
if (followUpMessage.tool_calls && followUpMessage.tool_calls.length > 0) {
|
|
735
774
|
const followUpToolCall = followUpMessage.tool_calls[0];
|
|
736
775
|
if (followUpToolCall.function.name === 'parse_requirements') {
|
|
737
|
-
const params = JSON.parse(followUpToolCall.function.arguments);
|
|
776
|
+
const params = normalizeRequirementDomains(JSON.parse(followUpToolCall.function.arguments), userDomains);
|
|
738
777
|
// Apply constraints and defaults
|
|
739
778
|
const validatedParams = (0, utils_1.applyConstraints)(params, config);
|
|
740
779
|
return {
|
|
@@ -786,7 +825,7 @@ async function processIntentParserRequest(userRequest, options = {}) {
|
|
|
786
825
|
if (feedFollowUpMessage.tool_calls && feedFollowUpMessage.tool_calls.length > 0) {
|
|
787
826
|
const feedToolCall = feedFollowUpMessage.tool_calls[0];
|
|
788
827
|
if (feedToolCall.function.name === 'parse_requirements') {
|
|
789
|
-
const params = JSON.parse(feedToolCall.function.arguments);
|
|
828
|
+
const params = normalizeRequirementDomains(JSON.parse(feedToolCall.function.arguments), userDomains);
|
|
790
829
|
// Apply constraints and defaults
|
|
791
830
|
const validatedParams = (0, utils_1.applyConstraints)(params, config);
|
|
792
831
|
return {
|
|
@@ -951,7 +990,7 @@ async function processIntentParserRequest(userRequest, options = {}) {
|
|
|
951
990
|
if (followUpMessage.tool_calls && followUpMessage.tool_calls.length > 0) {
|
|
952
991
|
const followUpToolCall = followUpMessage.tool_calls[0];
|
|
953
992
|
if (followUpToolCall.function.name === 'parse_requirements') {
|
|
954
|
-
const params = JSON.parse(followUpToolCall.function.arguments);
|
|
993
|
+
const params = normalizeRequirementDomains(JSON.parse(followUpToolCall.function.arguments), userDomains);
|
|
955
994
|
// Apply constraints and defaults
|
|
956
995
|
const validatedParams = (0, utils_1.applyConstraints)(params, config);
|
|
957
996
|
return {
|
|
@@ -1014,7 +1053,7 @@ async function processIntentParserRequest(userRequest, options = {}) {
|
|
|
1014
1053
|
};
|
|
1015
1054
|
}
|
|
1016
1055
|
else if (toolCall.function.name === 'parse_requirements') {
|
|
1017
|
-
const params = JSON.parse(toolCall.function.arguments);
|
|
1056
|
+
const params = normalizeRequirementDomains(JSON.parse(toolCall.function.arguments), userDomains);
|
|
1018
1057
|
// Apply constraints and defaults
|
|
1019
1058
|
const validatedParams = (0, utils_1.applyConstraints)(params, config);
|
|
1020
1059
|
return {
|
|
@@ -1117,6 +1156,9 @@ async function processIntentParserRequest(userRequest, options = {}) {
|
|
|
1117
1156
|
}
|
|
1118
1157
|
else if (toolCall.function.name === 'verify_addresses') {
|
|
1119
1158
|
const args = JSON.parse(toolCall.function.arguments);
|
|
1159
|
+
if (Array.isArray(args.addresses) && userDomains.length > 0) {
|
|
1160
|
+
args.addresses = args.addresses.map((address) => normalizeDomainInput(address, userDomains));
|
|
1161
|
+
}
|
|
1120
1162
|
const { verifyAddresses } = await Promise.resolve().then(() => __importStar(require('../utilities/functions')));
|
|
1121
1163
|
const verificationResult = await verifyAddresses({ addresses: args.addresses });
|
|
1122
1164
|
if (!verificationResult.valid) {
|
|
@@ -1168,7 +1210,7 @@ async function processIntentParserRequest(userRequest, options = {}) {
|
|
|
1168
1210
|
if (followUpMessage.tool_calls && followUpMessage.tool_calls.length > 0) {
|
|
1169
1211
|
const followUpToolCall = followUpMessage.tool_calls[0];
|
|
1170
1212
|
if (followUpToolCall.function.name === 'parse_requirements') {
|
|
1171
|
-
const params = JSON.parse(followUpToolCall.function.arguments);
|
|
1213
|
+
const params = normalizeRequirementDomains(JSON.parse(followUpToolCall.function.arguments), userDomains);
|
|
1172
1214
|
// Apply constraints and defaults
|
|
1173
1215
|
const validatedParams = (0, utils_1.applyConstraints)(params, config);
|
|
1174
1216
|
return {
|
|
@@ -1221,7 +1263,7 @@ async function processIntentParserRequest(userRequest, options = {}) {
|
|
|
1221
1263
|
if (feedFollowUpMessage.tool_calls && feedFollowUpMessage.tool_calls.length > 0) {
|
|
1222
1264
|
const feedToolCall = feedFollowUpMessage.tool_calls[0];
|
|
1223
1265
|
if (feedToolCall.function.name === 'parse_requirements') {
|
|
1224
|
-
const params = JSON.parse(feedToolCall.function.arguments);
|
|
1266
|
+
const params = normalizeRequirementDomains(JSON.parse(feedToolCall.function.arguments), userDomains);
|
|
1225
1267
|
// Apply constraints and defaults
|
|
1226
1268
|
const validatedParams = (0, utils_1.applyConstraints)(params, config);
|
|
1227
1269
|
return {
|
|
@@ -57,8 +57,11 @@ function applyConstraints(params, config) {
|
|
|
57
57
|
// Set default quantity if not provided
|
|
58
58
|
// Allow "all" as a string value for query_address type
|
|
59
59
|
let quantity;
|
|
60
|
-
if (r.quantity === 'all'
|
|
61
|
-
quantity =
|
|
60
|
+
if (r.quantity === 'all') {
|
|
61
|
+
quantity = 'all';
|
|
62
|
+
}
|
|
63
|
+
else if (r.quantity === null || r.quantity === undefined) {
|
|
64
|
+
quantity = 5;
|
|
62
65
|
}
|
|
63
66
|
else if (typeof r.quantity === 'string') {
|
|
64
67
|
// Try to parse string numbers
|
package/dist/src/logger.js
CHANGED
|
@@ -10,6 +10,7 @@ exports.setVerbose = setVerbose;
|
|
|
10
10
|
exports.debug = debug;
|
|
11
11
|
exports.info = info;
|
|
12
12
|
exports.warn = warn;
|
|
13
|
+
exports.verbose = verbose;
|
|
13
14
|
exports.error = error;
|
|
14
15
|
exports.always = always;
|
|
15
16
|
const chalk_1 = __importDefault(require("chalk"));
|
|
@@ -49,6 +50,15 @@ function warn(...args) {
|
|
|
49
50
|
console.warn(chalk_1.default.yellow('[WARN]'), ...args);
|
|
50
51
|
}
|
|
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
|
+
}
|
|
52
62
|
/**
|
|
53
63
|
* Log error message (always shown, but with more details in verbose mode)
|
|
54
64
|
* @param {...any} args - Arguments to log
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* FF1 device compatibility helpers for command preflight checks.
|
|
4
|
+
*/
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
+
}) : function(o, v) {
|
|
19
|
+
o["default"] = v;
|
|
20
|
+
});
|
|
21
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
+
var ownKeys = function(o) {
|
|
23
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
+
var ar = [];
|
|
25
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
+
return ar;
|
|
27
|
+
};
|
|
28
|
+
return ownKeys(o);
|
|
29
|
+
};
|
|
30
|
+
return function (mod) {
|
|
31
|
+
if (mod && mod.__esModule) return mod;
|
|
32
|
+
var result = {};
|
|
33
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
+
__setModuleDefault(result, mod);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
})();
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.resolveConfiguredDevice = resolveConfiguredDevice;
|
|
40
|
+
exports.assertFF1CommandCompatibility = assertFF1CommandCompatibility;
|
|
41
|
+
const config_1 = require("../config");
|
|
42
|
+
const logger = __importStar(require("../logger"));
|
|
43
|
+
const FF1_COMMAND_POLICIES = {
|
|
44
|
+
displayPlaylist: {
|
|
45
|
+
minimumVersion: '1.0.0',
|
|
46
|
+
},
|
|
47
|
+
sshAccess: {
|
|
48
|
+
minimumVersion: '1.0.9',
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Load and validate the configured FF1 device selected by name.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} [deviceName] - Optional device name, exact match required
|
|
55
|
+
* @param {Object} [options] - Optional dependency overrides
|
|
56
|
+
* @param {Function} [options.getFF1DeviceConfigFn] - Optional config loader override
|
|
57
|
+
* @returns {FF1DeviceSelectionResult} Selected device or reason for failure
|
|
58
|
+
* @throws {Error} Never throws; malformed configuration is returned as an error result
|
|
59
|
+
* @example
|
|
60
|
+
* const result = resolveConfiguredDevice('Living Room');
|
|
61
|
+
*/
|
|
62
|
+
function resolveConfiguredDevice(deviceName, options = {}) {
|
|
63
|
+
const getFF1DeviceConfigFn = options.getFF1DeviceConfigFn || config_1.getFF1DeviceConfig;
|
|
64
|
+
const deviceConfig = getFF1DeviceConfigFn();
|
|
65
|
+
if (!deviceConfig.devices || deviceConfig.devices.length === 0) {
|
|
66
|
+
return {
|
|
67
|
+
success: false,
|
|
68
|
+
error: 'No FF1 devices configured. Add devices to config.json under "ff1Devices"',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
let device = deviceConfig.devices[0];
|
|
72
|
+
if (deviceName) {
|
|
73
|
+
device = deviceConfig.devices.find((item) => item.name === deviceName);
|
|
74
|
+
if (!device) {
|
|
75
|
+
const availableNames = deviceConfig.devices
|
|
76
|
+
.map((item) => item.name)
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
.join(', ');
|
|
79
|
+
return {
|
|
80
|
+
success: false,
|
|
81
|
+
error: `Device "${deviceName}" not found. Available devices: ${availableNames || 'none with names'}`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
logger.info(`Found device by name: ${deviceName}`);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
logger.info('Using first configured device');
|
|
88
|
+
}
|
|
89
|
+
if (!device.host) {
|
|
90
|
+
return {
|
|
91
|
+
success: false,
|
|
92
|
+
error: 'Invalid device configuration: must include host',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
success: true,
|
|
97
|
+
device,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Ensure the target device supports the requested FF1 command.
|
|
102
|
+
*
|
|
103
|
+
* @param {Object} device - FF1 device configuration
|
|
104
|
+
* @param {FF1Command} command - Command to execute
|
|
105
|
+
* @param {Object} [options] - Optional dependency overrides
|
|
106
|
+
* @param {Function} [options.fetchFn] - Optional fetch implementation
|
|
107
|
+
* @returns {Promise<FF1CompatibilityResult>} Compatibility result
|
|
108
|
+
* @throws {Error} Never throws; network and parsing failures produce a compatible result
|
|
109
|
+
* @example
|
|
110
|
+
* const result = await assertFF1CommandCompatibility(device, 'displayPlaylist');
|
|
111
|
+
*/
|
|
112
|
+
async function assertFF1CommandCompatibility(device, command, options = {}) {
|
|
113
|
+
const fetchFn = options.fetchFn || globalThis.fetch.bind(globalThis);
|
|
114
|
+
const policy = getCommandPolicy(command);
|
|
115
|
+
const versionResult = await detectFF1VersionSafely(device.host, buildVersionHeaders(device), fetchFn);
|
|
116
|
+
return resolveCompatibility(device, command, policy, versionResult);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Return command compatibility requirements.
|
|
120
|
+
*
|
|
121
|
+
* @param {FF1Command} command - Command to check
|
|
122
|
+
* @returns {FF1CommandPolicy} Policy metadata
|
|
123
|
+
* @example
|
|
124
|
+
* getCommandPolicy('sshAccess'); // { minimumVersion: '1.0.0' }
|
|
125
|
+
*/
|
|
126
|
+
function getCommandPolicy(command) {
|
|
127
|
+
return FF1_COMMAND_POLICIES[command];
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Detect FF1 version and recover compatibility when detection fails.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} host - Device host URL
|
|
133
|
+
* @param {Object} headers - Request headers
|
|
134
|
+
* @param {Function} fetchFn - Fetch implementation
|
|
135
|
+
* @returns {Promise<FF1VersionProbe | null>} Detected version metadata
|
|
136
|
+
*/
|
|
137
|
+
async function detectFF1VersionSafely(host, headers, fetchFn) {
|
|
138
|
+
try {
|
|
139
|
+
return await detectFF1Version(host, headers, fetchFn);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
logger.debug('FF1 version detection failed; continuing with command', error.message);
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Resolve final compatibility decision from detected version and policy.
|
|
148
|
+
*
|
|
149
|
+
* @param {FF1Device} device - Target device
|
|
150
|
+
* @param {FF1Command} command - Command requested
|
|
151
|
+
* @param {FF1CommandPolicy} policy - Version policy
|
|
152
|
+
* @param {FF1VersionProbe | null} versionResult - Detected version probe
|
|
153
|
+
* @returns {FF1CompatibilityResult} Compatibility decision
|
|
154
|
+
*/
|
|
155
|
+
function resolveCompatibility(device, command, policy, versionResult) {
|
|
156
|
+
if (!versionResult) {
|
|
157
|
+
logger.warn(`Could not verify FF1 OS version for ${device.name || device.host}`);
|
|
158
|
+
return { compatible: true };
|
|
159
|
+
}
|
|
160
|
+
const normalizedVersion = normalizeVersion(versionResult.version);
|
|
161
|
+
if (!normalizedVersion) {
|
|
162
|
+
return {
|
|
163
|
+
compatible: true,
|
|
164
|
+
version: versionResult.version,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (compareVersions(normalizedVersion, policy.minimumVersion) < 0) {
|
|
168
|
+
return {
|
|
169
|
+
compatible: false,
|
|
170
|
+
version: normalizedVersion,
|
|
171
|
+
error: `Unsupported FF1 OS ${normalizedVersion} for ${command}. FF1 OS must be ${policy.minimumVersion} or newer.`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
compatible: true,
|
|
176
|
+
version: normalizedVersion,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Detect FF1 OS version via POST /api/cast with getDeviceStatus command.
|
|
181
|
+
*
|
|
182
|
+
* Reads `message.installedVersion` from the device status response.
|
|
183
|
+
*
|
|
184
|
+
* @param {string} host - Device host URL
|
|
185
|
+
* @param {Record<string, string>} headers - Request headers (e.g. API-KEY)
|
|
186
|
+
* @param {FetchFunction} fetchFn - Fetch implementation
|
|
187
|
+
* @returns {Promise<FF1VersionProbe | null>} Version probe or null if unavailable
|
|
188
|
+
* @example
|
|
189
|
+
* const probe = await detectFF1Version('http://ff1.local', {}, fetch);
|
|
190
|
+
*/
|
|
191
|
+
async function detectFF1Version(host, headers, fetchFn) {
|
|
192
|
+
try {
|
|
193
|
+
const response = await fetchFn(`${host}/api/cast`, {
|
|
194
|
+
method: 'POST',
|
|
195
|
+
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
196
|
+
body: JSON.stringify({ command: 'getDeviceStatus', request: {} }),
|
|
197
|
+
});
|
|
198
|
+
if (!response.ok) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
const data = (await response.json());
|
|
202
|
+
const version = data?.message?.installedVersion;
|
|
203
|
+
if (!version) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
return { version };
|
|
207
|
+
}
|
|
208
|
+
catch (_error) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Build headers shared by cast requests.
|
|
214
|
+
*
|
|
215
|
+
* @param {FF1Device} device - Target device
|
|
216
|
+
* @returns {Record<string, string>} Headers map
|
|
217
|
+
* @example
|
|
218
|
+
* const headers = buildVersionHeaders(device);
|
|
219
|
+
*/
|
|
220
|
+
function buildVersionHeaders(device) {
|
|
221
|
+
const headers = {};
|
|
222
|
+
if (device.apiKey) {
|
|
223
|
+
headers['API-KEY'] = device.apiKey;
|
|
224
|
+
}
|
|
225
|
+
return headers;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Parse and normalize a version string to x.y.z format.
|
|
229
|
+
*
|
|
230
|
+
* @param {string} version - Raw version string
|
|
231
|
+
* @returns {string | null} Normalized semver-like version
|
|
232
|
+
* @example
|
|
233
|
+
* normalizeVersion('v1.2') // '1.2.0'
|
|
234
|
+
*/
|
|
235
|
+
function normalizeVersion(version) {
|
|
236
|
+
const raw = version.trim();
|
|
237
|
+
const match = raw.match(/(?:v)?(\d+)\.(\d+)(?:\.(\d+))?/);
|
|
238
|
+
if (!match) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
const major = match[1];
|
|
242
|
+
const minor = match[2];
|
|
243
|
+
const patch = match[3] || '0';
|
|
244
|
+
return `${major}.${minor}.${patch}`;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Compare two semantic versions in x.y.z format.
|
|
248
|
+
*
|
|
249
|
+
* @param {string} left - First version
|
|
250
|
+
* @param {string} right - Second version
|
|
251
|
+
* @returns {number} 1 if left > right, -1 if left < right, 0 if equal
|
|
252
|
+
* @example
|
|
253
|
+
* compareVersions('1.2.1', '1.2.0'); // 1
|
|
254
|
+
*/
|
|
255
|
+
function compareVersions(left, right) {
|
|
256
|
+
const leftParts = left.split('.').map((value) => Number.parseInt(value, 10));
|
|
257
|
+
const rightParts = right.split('.').map((value) => Number.parseInt(value, 10));
|
|
258
|
+
for (let i = 0; i < 3; i++) {
|
|
259
|
+
const leftPart = leftParts[i] || 0;
|
|
260
|
+
const rightPart = rightParts[i] || 0;
|
|
261
|
+
if (leftPart > rightPart) {
|
|
262
|
+
return 1;
|
|
263
|
+
}
|
|
264
|
+
if (leftPart < rightPart) {
|
|
265
|
+
return -1;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return 0;
|
|
269
|
+
}
|
|
@@ -38,8 +38,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
38
38
|
})();
|
|
39
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
40
|
exports.sendPlaylistToDevice = sendPlaylistToDevice;
|
|
41
|
-
const config_1 = require("../config");
|
|
42
41
|
const logger = __importStar(require("../logger"));
|
|
42
|
+
const ff1_compatibility_1 = require("./ff1-compatibility");
|
|
43
43
|
/**
|
|
44
44
|
* Send a DP1 playlist to an FF1 device using the cast API
|
|
45
45
|
*
|
|
@@ -80,38 +80,20 @@ async function sendPlaylistToDevice({ playlist, deviceName, }) {
|
|
|
80
80
|
error: 'Invalid playlist: must provide a valid DP1 playlist object',
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (!deviceConfig.devices || deviceConfig.devices.length === 0) {
|
|
83
|
+
const resolved = (0, ff1_compatibility_1.resolveConfiguredDevice)(deviceName);
|
|
84
|
+
if (!resolved.success || !resolved.device) {
|
|
86
85
|
return {
|
|
87
86
|
success: false,
|
|
88
|
-
error:
|
|
87
|
+
error: resolved.error || 'FF1 device is not configured correctly',
|
|
89
88
|
};
|
|
90
89
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (
|
|
94
|
-
device = deviceConfig.devices.find((d) => d.name === deviceName);
|
|
95
|
-
if (!device) {
|
|
96
|
-
const availableNames = deviceConfig.devices
|
|
97
|
-
.map((d) => d.name)
|
|
98
|
-
.filter(Boolean)
|
|
99
|
-
.join(', ');
|
|
100
|
-
return {
|
|
101
|
-
success: false,
|
|
102
|
-
error: `Device "${deviceName}" not found. Available devices: ${availableNames || 'none with names'}`,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
logger.info(`Found device by name: ${deviceName}`);
|
|
106
|
-
}
|
|
107
|
-
else {
|
|
108
|
-
device = deviceConfig.devices[0];
|
|
109
|
-
logger.info('Using first configured device');
|
|
110
|
-
}
|
|
111
|
-
if (!device.host) {
|
|
90
|
+
const device = resolved.device;
|
|
91
|
+
const compatibility = await (0, ff1_compatibility_1.assertFF1CommandCompatibility)(device, 'displayPlaylist');
|
|
92
|
+
if (!compatibility.compatible) {
|
|
112
93
|
return {
|
|
113
94
|
success: false,
|
|
114
|
-
error: '
|
|
95
|
+
error: compatibility.error || 'FF1 OS does not support playlist casting',
|
|
96
|
+
details: compatibility.version ? `Detected version ${compatibility.version}` : undefined,
|
|
115
97
|
};
|
|
116
98
|
}
|
|
117
99
|
logger.info(`Sending playlist to FF1 device: ${device.host}`);
|