@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,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.statusCommand = void 0;
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const config_files_1 = require("./helpers/config-files");
|
|
10
|
+
const config_1 = require("../config");
|
|
11
|
+
const ed25519_key_derive_1 = require("../utilities/ed25519-key-derive");
|
|
12
|
+
const playlist_signing_role_1 = require("../utilities/playlist-signing-role");
|
|
13
|
+
exports.statusCommand = new commander_1.Command('status')
|
|
14
|
+
.description('Show configuration status')
|
|
15
|
+
.action(async () => {
|
|
16
|
+
try {
|
|
17
|
+
const configPath = await (0, config_files_1.resolveExistingConfigPath)();
|
|
18
|
+
if (!configPath) {
|
|
19
|
+
console.log(chalk_1.default.red('config.json not found'));
|
|
20
|
+
console.log(chalk_1.default.dim('Run: ff1 setup'));
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
const config = await (0, config_files_1.readConfigFile)(configPath);
|
|
24
|
+
const modelNames = Object.keys(config.models || {});
|
|
25
|
+
const defaultModel = config.defaultModel && modelNames.includes(config.defaultModel)
|
|
26
|
+
? config.defaultModel
|
|
27
|
+
: modelNames[0];
|
|
28
|
+
const defaultModelLabel = defaultModel || 'unknown';
|
|
29
|
+
const defaultModelConfig = defaultModel ? config.models?.[defaultModel] : undefined;
|
|
30
|
+
const playlistConfig = (0, config_1.getPlaylistConfig)();
|
|
31
|
+
const hasApiKey = defaultModel ? !(0, config_files_1.isMissingConfigValue)(defaultModelConfig?.apiKey) : false;
|
|
32
|
+
const playlistKeyMaterial = playlistConfig.privateKey?.trim() || '';
|
|
33
|
+
const playlistKeyError = playlistKeyMaterial.length > 0 ? validatePlaylistPrivateKey(playlistKeyMaterial) : null;
|
|
34
|
+
const hasPlaylistSigningKey = playlistKeyMaterial.length > 0 && playlistKeyError === null;
|
|
35
|
+
let hasValidPlaylistRole = false;
|
|
36
|
+
let playlistRoleDetail;
|
|
37
|
+
let playlistRoleError;
|
|
38
|
+
const playlistRoleMaterial = playlistConfig.role?.trim() || '';
|
|
39
|
+
if (playlistRoleMaterial) {
|
|
40
|
+
hasValidPlaylistRole = (0, playlist_signing_role_1.isDp1PlaylistSigningRole)(playlistRoleMaterial);
|
|
41
|
+
playlistRoleDetail = playlistRoleMaterial;
|
|
42
|
+
if (!hasValidPlaylistRole) {
|
|
43
|
+
playlistRoleError = playlistRoleMaterial;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const statuses = [
|
|
47
|
+
{
|
|
48
|
+
label: 'Config file',
|
|
49
|
+
ok: true,
|
|
50
|
+
detail: configPath,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
label: `Default model (${defaultModelLabel}) API key`,
|
|
54
|
+
ok: hasApiKey,
|
|
55
|
+
optional: true,
|
|
56
|
+
hint: ' (needed for chat)',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
label: 'Playlist signing key',
|
|
60
|
+
ok: hasPlaylistSigningKey,
|
|
61
|
+
optional: false,
|
|
62
|
+
detail: playlistKeyError
|
|
63
|
+
? `${playlistKeyError} (from config/env)`
|
|
64
|
+
: playlistKeyMaterial
|
|
65
|
+
? 'from config/env'
|
|
66
|
+
: undefined,
|
|
67
|
+
hint: ' (needed for signing and legacy verification)',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
label: 'Playlist signing role',
|
|
71
|
+
ok: hasValidPlaylistRole,
|
|
72
|
+
optional: true,
|
|
73
|
+
detail: playlistRoleDetail,
|
|
74
|
+
invalid: Boolean(playlistRoleError),
|
|
75
|
+
hint: ' (used when signing playlists)',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
label: `FF1 devices (${config.ff1Devices?.devices?.length || 0})`,
|
|
79
|
+
ok: (config.ff1Devices?.devices?.length || 0) > 0 &&
|
|
80
|
+
(config.ff1Devices?.devices || []).every((d) => !(0, config_files_1.isMissingConfigValue)(d.host)),
|
|
81
|
+
detail: (config.ff1Devices?.devices || [])
|
|
82
|
+
.map((d) => `${d.name || 'unnamed'} → ${d.host}`)
|
|
83
|
+
.join(', ') || undefined,
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
console.log(chalk_1.default.blue('\n🔎 FF1 Status\n'));
|
|
87
|
+
statuses.forEach((status) => {
|
|
88
|
+
let label;
|
|
89
|
+
if (status.ok) {
|
|
90
|
+
label = chalk_1.default.green('OK');
|
|
91
|
+
}
|
|
92
|
+
else if (status.invalid) {
|
|
93
|
+
label = chalk_1.default.red('Invalid');
|
|
94
|
+
}
|
|
95
|
+
else if (status.optional) {
|
|
96
|
+
label = chalk_1.default.yellow('Not set');
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
label = chalk_1.default.red('Missing');
|
|
100
|
+
}
|
|
101
|
+
const detail = status.detail ? chalk_1.default.dim(` (${status.detail})`) : '';
|
|
102
|
+
const hint = status.ok || !status.hint
|
|
103
|
+
? ''
|
|
104
|
+
: chalk_1.default.dim(status.hint);
|
|
105
|
+
console.log(`${label} ${status.label}${detail}${hint}`);
|
|
106
|
+
});
|
|
107
|
+
const hasRequired = statuses.some((status) => !status.ok && (!status.optional || Boolean(status.invalid)));
|
|
108
|
+
if (hasRequired) {
|
|
109
|
+
console.log(chalk_1.default.dim('\nRun: ff1 setup'));
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
console.error(chalk_1.default.red('\nStatus check failed:'), error.message);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
function validatePlaylistPrivateKey(material) {
|
|
119
|
+
try {
|
|
120
|
+
(0, ed25519_key_derive_1.parsePlaylistPrivateKeyToKeyObject)(material);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
return error.message;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateCommand = exports.verifyCommand = void 0;
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const playlist_display_1 = require("./helpers/playlist-display");
|
|
6
|
+
exports.verifyCommand = new commander_1.Command('verify')
|
|
7
|
+
.description('Validate playlist structure and verify DP-1 signatures')
|
|
8
|
+
.argument('<file>', 'Path to the playlist file or hosted playlist URL')
|
|
9
|
+
.option('--public-key <publicKey>', 'Ed25519 public key for legacy verification (overrides deriving from playlist.privateKey / PLAYLIST_PRIVATE_KEY)')
|
|
10
|
+
.action(async (file, options) => {
|
|
11
|
+
await (0, playlist_display_1.runVerifyCommand)(file, options.publicKey);
|
|
12
|
+
});
|
|
13
|
+
exports.validateCommand = new commander_1.Command('validate')
|
|
14
|
+
.description('Validate playlist structure only')
|
|
15
|
+
.argument('<file>', 'Path to the playlist file or hosted playlist URL')
|
|
16
|
+
.action(async (file) => {
|
|
17
|
+
await (0, playlist_display_1.runValidateCommand)(file);
|
|
18
|
+
});
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getConfigPaths = getConfigPaths;
|
|
7
|
+
exports.getConfig = getConfig;
|
|
8
|
+
exports.sanitizationLevelToNumber = sanitizationLevelToNumber;
|
|
9
|
+
exports.getBrowserConfig = getBrowserConfig;
|
|
10
|
+
exports.getPlaylistConfig = getPlaylistConfig;
|
|
11
|
+
exports.getFeedConfig = getFeedConfig;
|
|
12
|
+
exports.getFF1DeviceConfig = getFF1DeviceConfig;
|
|
13
|
+
exports.getModelConfig = getModelConfig;
|
|
14
|
+
exports.validateConfig = validateConfig;
|
|
15
|
+
exports.createSampleConfig = createSampleConfig;
|
|
16
|
+
exports.listAvailableModels = listAvailableModels;
|
|
17
|
+
const fs_1 = __importDefault(require("fs"));
|
|
18
|
+
const path_1 = __importDefault(require("path"));
|
|
19
|
+
const os_1 = __importDefault(require("os"));
|
|
20
|
+
const playlist_signing_role_1 = require("./utilities/playlist-signing-role");
|
|
21
|
+
// One-shot legacy config migration: copy `$XDG_CONFIG_HOME/ff1/config.json` to
|
|
22
|
+
// `$XDG_CONFIG_HOME/ff-cli/config.json` on first read after upgrading from
|
|
23
|
+
// `ff1-cli`. The check is cheap (two fs.existsSync calls) and the
|
|
24
|
+
// `migrationChecked` flag keeps it to one attempt per process.
|
|
25
|
+
//
|
|
26
|
+
// Remove this block after a release cycle once telemetry suggests no remaining
|
|
27
|
+
// users still have only the legacy path populated.
|
|
28
|
+
let migrationChecked = false;
|
|
29
|
+
function migrateLegacyConfigIfNeeded(newUserPath) {
|
|
30
|
+
if (migrationChecked) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
migrationChecked = true;
|
|
34
|
+
const configBase = process.env.XDG_CONFIG_HOME || path_1.default.join(os_1.default.homedir(), '.config');
|
|
35
|
+
const legacyUserPath = path_1.default.join(configBase, 'ff1', 'config.json');
|
|
36
|
+
if (!fs_1.default.existsSync(legacyUserPath) || fs_1.default.existsSync(newUserPath)) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
fs_1.default.mkdirSync(path_1.default.dirname(newUserPath), { recursive: true });
|
|
41
|
+
fs_1.default.copyFileSync(legacyUserPath, newUserPath);
|
|
42
|
+
console.log(`Migrated config from ${legacyUserPath} to ${newUserPath}`);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
46
|
+
console.warn(`Warning: Failed to migrate legacy config from ${legacyUserPath}: ${message}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function getConfigPaths() {
|
|
50
|
+
const localPath = path_1.default.join(process.cwd(), 'config.json');
|
|
51
|
+
const configBase = process.env.XDG_CONFIG_HOME || path_1.default.join(os_1.default.homedir(), '.config');
|
|
52
|
+
const userPath = path_1.default.join(configBase, 'ff-cli', 'config.json');
|
|
53
|
+
migrateLegacyConfigIfNeeded(userPath);
|
|
54
|
+
return { localPath, userPath };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Load configuration from config.json or environment variables
|
|
58
|
+
* Priority: config.json > .env > defaults
|
|
59
|
+
*
|
|
60
|
+
* @returns {Object} Configuration object with model settings
|
|
61
|
+
* @returns {string} returns.defaultModel - Name of the default model to use
|
|
62
|
+
* @returns {Object} returns.models - Available models configuration
|
|
63
|
+
* @returns {number} returns.defaultDuration - Default duration per item in seconds
|
|
64
|
+
*/
|
|
65
|
+
function loadConfig() {
|
|
66
|
+
const { localPath, userPath } = getConfigPaths();
|
|
67
|
+
// Default configuration supporting Grok as default
|
|
68
|
+
const defaultConfig = {
|
|
69
|
+
defaultModel: process.env.DEFAULT_MODEL || 'claude',
|
|
70
|
+
models: {
|
|
71
|
+
claude: {
|
|
72
|
+
apiKey: process.env.ANTHROPIC_API_KEY || '',
|
|
73
|
+
baseURL: 'https://api.anthropic.com/v1/',
|
|
74
|
+
model: 'claude-sonnet-4-6',
|
|
75
|
+
availableModels: ['claude-opus-4-7', 'claude-sonnet-4-6', 'claude-haiku-4-5-20251001'],
|
|
76
|
+
timeout: 30000,
|
|
77
|
+
maxRetries: 3,
|
|
78
|
+
temperature: 0.3,
|
|
79
|
+
maxTokens: 4000,
|
|
80
|
+
supportsFunctionCalling: true,
|
|
81
|
+
},
|
|
82
|
+
grok: {
|
|
83
|
+
apiKey: process.env.GROK_API_KEY || '',
|
|
84
|
+
baseURL: process.env.GROK_API_BASE_URL || 'https://api.x.ai/v1',
|
|
85
|
+
model: process.env.GROK_MODEL || 'grok-beta',
|
|
86
|
+
availableModels: ['grok-beta', 'grok-2-1212', 'grok-2-vision-1212'],
|
|
87
|
+
timeout: parseInt(process.env.TIMEOUT || '30000', 10),
|
|
88
|
+
maxRetries: parseInt(process.env.MAX_RETRIES || '3', 10),
|
|
89
|
+
temperature: parseFloat(process.env.TEMPERATURE || '0.3'),
|
|
90
|
+
maxTokens: parseInt(process.env.MAX_TOKENS || '4000', 10),
|
|
91
|
+
supportsFunctionCalling: true,
|
|
92
|
+
},
|
|
93
|
+
gpt: {
|
|
94
|
+
apiKey: process.env.OPENAI_API_KEY || '',
|
|
95
|
+
baseURL: 'https://api.openai.com/v1',
|
|
96
|
+
model: 'gpt-4.1',
|
|
97
|
+
availableModels: ['gpt-4.1', 'gpt-4.1-mini', 'gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo'],
|
|
98
|
+
timeout: 30000,
|
|
99
|
+
maxRetries: 3,
|
|
100
|
+
temperature: 0.3,
|
|
101
|
+
maxTokens: 4000,
|
|
102
|
+
supportsFunctionCalling: true,
|
|
103
|
+
},
|
|
104
|
+
gemini: {
|
|
105
|
+
apiKey: process.env.GEMINI_API_KEY || '',
|
|
106
|
+
baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai/',
|
|
107
|
+
model: 'gemini-2.5-flash',
|
|
108
|
+
availableModels: ['gemini-2.5-flash-lite', 'gemini-2.5-flash', 'gemini-flash-lite-latest'],
|
|
109
|
+
timeout: 30000,
|
|
110
|
+
maxRetries: 3,
|
|
111
|
+
temperature: 0.3,
|
|
112
|
+
maxTokens: 4000,
|
|
113
|
+
supportsFunctionCalling: true,
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
defaultDuration: parseInt(process.env.DEFAULT_DURATION || '10', 10),
|
|
117
|
+
browser: {
|
|
118
|
+
timeout: parseInt(process.env.BROWSER_TIMEOUT || '90000', 10),
|
|
119
|
+
sanitizationLevel: process.env.SANITIZATION_LEVEL || 'medium',
|
|
120
|
+
},
|
|
121
|
+
feed: {
|
|
122
|
+
baseURLs: process.env.FEED_BASE_URLS
|
|
123
|
+
? process.env.FEED_BASE_URLS.split(',')
|
|
124
|
+
: ['https://dp1-feed-operator-api-prod.autonomy-system.workers.dev/api/v1'],
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
// Try to load config.json if it exists
|
|
128
|
+
const configPath = fs_1.default.existsSync(localPath) ? localPath : userPath;
|
|
129
|
+
if (fs_1.default.existsSync(configPath)) {
|
|
130
|
+
try {
|
|
131
|
+
const fileConfig = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
132
|
+
// Deep merge models configuration
|
|
133
|
+
const mergedModels = { ...defaultConfig.models };
|
|
134
|
+
if (fileConfig.models) {
|
|
135
|
+
Object.keys(fileConfig.models).forEach((modelName) => {
|
|
136
|
+
mergedModels[modelName] = {
|
|
137
|
+
...(defaultConfig.models[modelName] || {}),
|
|
138
|
+
...fileConfig.models[modelName],
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
// Merge with defaults, file config takes precedence
|
|
143
|
+
return {
|
|
144
|
+
...defaultConfig,
|
|
145
|
+
...fileConfig,
|
|
146
|
+
models: mergedModels,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
151
|
+
console.warn(`Warning: Failed to parse config at ${configPath}. ${message}. Using defaults.`);
|
|
152
|
+
return defaultConfig;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Return default config if no file exists
|
|
156
|
+
return defaultConfig;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Get current configuration
|
|
160
|
+
*
|
|
161
|
+
* @returns {Object} Current configuration
|
|
162
|
+
*/
|
|
163
|
+
function getConfig() {
|
|
164
|
+
return loadConfig();
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Convert sanitization level string to numeric value
|
|
168
|
+
*
|
|
169
|
+
* @param {string|number} level - Sanitization level ('none', 'low', 'medium', 'high') or number (0-3)
|
|
170
|
+
* @returns {number} Numeric level (0 = none, 1 = low, 2 = medium, 3 = high)
|
|
171
|
+
*/
|
|
172
|
+
function sanitizationLevelToNumber(level) {
|
|
173
|
+
if (typeof level === 'number') {
|
|
174
|
+
return level;
|
|
175
|
+
}
|
|
176
|
+
const levelMap = {
|
|
177
|
+
none: 0,
|
|
178
|
+
low: 1,
|
|
179
|
+
medium: 2,
|
|
180
|
+
high: 3,
|
|
181
|
+
};
|
|
182
|
+
return levelMap[level] !== undefined ? levelMap[level] : 2; // Default to medium (2)
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Get browser configuration
|
|
186
|
+
*
|
|
187
|
+
* @returns {Object} Browser configuration
|
|
188
|
+
* @returns {number} returns.timeout - Browser timeout in milliseconds
|
|
189
|
+
* @returns {number} returns.sanitizationLevel - Numeric sanitization level (0-3)
|
|
190
|
+
*/
|
|
191
|
+
function getBrowserConfig() {
|
|
192
|
+
const config = getConfig();
|
|
193
|
+
const browserConfig = config.browser || {
|
|
194
|
+
timeout: 90000,
|
|
195
|
+
sanitizationLevel: 'medium',
|
|
196
|
+
};
|
|
197
|
+
return {
|
|
198
|
+
timeout: browserConfig.timeout,
|
|
199
|
+
sanitizationLevel: sanitizationLevelToNumber(browserConfig.sanitizationLevel),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Get playlist configuration including signing private key and role.
|
|
204
|
+
*
|
|
205
|
+
* @returns {Object} Playlist configuration
|
|
206
|
+
* @returns {string|null} returns.privateKey - Ed25519 private key in base64 or hex format (null if not configured)
|
|
207
|
+
* @returns {string|null} returns.role - DP-1 signing role (null if not configured)
|
|
208
|
+
*/
|
|
209
|
+
function getPlaylistConfig() {
|
|
210
|
+
const config = getConfig();
|
|
211
|
+
const playlistConfig = config.playlist || {};
|
|
212
|
+
return {
|
|
213
|
+
privateKey: playlistConfig.privateKey || process.env.PLAYLIST_PRIVATE_KEY || null,
|
|
214
|
+
role: playlistConfig.role || process.env.PLAYLIST_ROLE || null,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Resolve the effective playlist signing role used by runtime paths.
|
|
219
|
+
*
|
|
220
|
+
* Validation must inspect the same value that setup and signing use so
|
|
221
|
+
* preflight checks do not accept combinations that will fail later, or reject
|
|
222
|
+
* whitespace-padded values that runtime trims before validation.
|
|
223
|
+
*
|
|
224
|
+
* @param {Config} config - Loaded configuration object.
|
|
225
|
+
* @returns {string|null} Effective role after config/env fallback and trimming.
|
|
226
|
+
*/
|
|
227
|
+
function resolveEffectivePlaylistRole(config) {
|
|
228
|
+
const playlistConfig = config.playlist || {};
|
|
229
|
+
const configuredRole = playlistConfig.role || process.env.PLAYLIST_ROLE || null;
|
|
230
|
+
if (configuredRole === null) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
return (0, playlist_signing_role_1.resolveDp1PlaylistSigningRole)(configuredRole);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Get feed configuration for DP1 feed API
|
|
237
|
+
*
|
|
238
|
+
* Supports both legacy (feed.baseURLs/apiKey) and new (feedServers array) formats.
|
|
239
|
+
*
|
|
240
|
+
* @returns {Object} Feed configuration
|
|
241
|
+
* @returns {string[]} returns.baseURLs - Array of base URLs for feed APIs
|
|
242
|
+
* @returns {string} [returns.apiKey] - Optional API key for authentication (legacy)
|
|
243
|
+
* @returns {Array<Object>} [returns.servers] - Array of feed servers with individual API keys (new)
|
|
244
|
+
*/
|
|
245
|
+
function getFeedConfig() {
|
|
246
|
+
const config = getConfig();
|
|
247
|
+
// Check for new feedServers format first
|
|
248
|
+
if (config.feedServers && Array.isArray(config.feedServers) && config.feedServers.length > 0) {
|
|
249
|
+
const baseURLs = config.feedServers.map((server) => server.baseUrl);
|
|
250
|
+
return {
|
|
251
|
+
baseURLs,
|
|
252
|
+
servers: config.feedServers,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
// Fall back to legacy feed format
|
|
256
|
+
const feedConfig = config.feed || {};
|
|
257
|
+
// Support both legacy baseURL and new baseURLs
|
|
258
|
+
let urls = [];
|
|
259
|
+
if (feedConfig.baseURLs && Array.isArray(feedConfig.baseURLs)) {
|
|
260
|
+
urls = feedConfig.baseURLs;
|
|
261
|
+
}
|
|
262
|
+
else if (feedConfig.baseURL) {
|
|
263
|
+
urls = [feedConfig.baseURL];
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
// Default feed URL
|
|
267
|
+
urls = ['https://dp1-feed-operator-api-prod.autonomy-system.workers.dev/api/v1'];
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
baseURLs: urls,
|
|
271
|
+
apiKey: feedConfig.apiKey,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Get FF1 device configuration for casting playlists
|
|
276
|
+
*
|
|
277
|
+
* @returns {Object} FF1 device configuration
|
|
278
|
+
* @returns {Array<Object>} returns.devices - Array of configured FF1 devices
|
|
279
|
+
* @returns {string} returns.devices[].host - Device host URL
|
|
280
|
+
* @returns {string} [returns.devices[].apiKey] - Optional device API key
|
|
281
|
+
* @returns {string} [returns.devices[].topicID] - Optional device topic ID
|
|
282
|
+
* @returns {string} [returns.devices[].name] - Optional device name
|
|
283
|
+
*/
|
|
284
|
+
function getFF1DeviceConfig() {
|
|
285
|
+
const config = getConfig();
|
|
286
|
+
const ff1Devices = config.ff1Devices || { devices: [] };
|
|
287
|
+
return {
|
|
288
|
+
devices: ff1Devices.devices || [],
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Get configuration for a specific model
|
|
293
|
+
*
|
|
294
|
+
* @param {string} [modelName] - Name of the model (defaults to defaultModel from config)
|
|
295
|
+
* @returns {Object} Model configuration
|
|
296
|
+
* @returns {string} returns.apiKey - API key for the model
|
|
297
|
+
* @returns {string} returns.baseURL - Base URL for the API
|
|
298
|
+
* @returns {string} returns.model - Model name/identifier
|
|
299
|
+
* @returns {number} returns.timeout - Request timeout in milliseconds
|
|
300
|
+
* @returns {number} returns.maxRetries - Maximum number of retries
|
|
301
|
+
* @returns {number} returns.temperature - Temperature for generation
|
|
302
|
+
* @returns {number} returns.maxTokens - Maximum tokens for generation
|
|
303
|
+
* @returns {boolean} returns.supportsFunctionCalling - Whether model supports function calling
|
|
304
|
+
* @throws {Error} If model is not configured or doesn't support function calling
|
|
305
|
+
*/
|
|
306
|
+
function getModelConfig(modelName) {
|
|
307
|
+
const config = getConfig();
|
|
308
|
+
const selectedModel = modelName || config.defaultModel;
|
|
309
|
+
if (!config.models[selectedModel]) {
|
|
310
|
+
throw new Error(`Model "${selectedModel}" is not configured. Available models: ${Object.keys(config.models).join(', ')}`);
|
|
311
|
+
}
|
|
312
|
+
const modelConfig = config.models[selectedModel];
|
|
313
|
+
if (!modelConfig.supportsFunctionCalling) {
|
|
314
|
+
throw new Error(`Model "${selectedModel}" does not support function calling`);
|
|
315
|
+
}
|
|
316
|
+
const normalizedBaseURL = modelConfig.baseURL?.replace(/\/+$/, '');
|
|
317
|
+
return {
|
|
318
|
+
...modelConfig,
|
|
319
|
+
baseURL: normalizedBaseURL,
|
|
320
|
+
defaultDuration: config.defaultDuration,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Validate configuration for a specific model
|
|
325
|
+
*
|
|
326
|
+
* @param {string} [modelName] - Name of the model to validate
|
|
327
|
+
* @returns {Object} Validation result
|
|
328
|
+
* @returns {boolean} returns.valid - Whether the configuration is valid
|
|
329
|
+
* @returns {Array<string>} returns.errors - List of validation errors
|
|
330
|
+
*/
|
|
331
|
+
function validateConfig(modelName) {
|
|
332
|
+
const errors = [];
|
|
333
|
+
try {
|
|
334
|
+
const config = getConfig();
|
|
335
|
+
const selectedModel = modelName || config.defaultModel;
|
|
336
|
+
if (!config.models[selectedModel]) {
|
|
337
|
+
errors.push(`Model "${selectedModel}" is not configured. Available: ${Object.keys(config.models).join(', ')}`);
|
|
338
|
+
return { valid: false, errors };
|
|
339
|
+
}
|
|
340
|
+
const modelConfig = config.models[selectedModel];
|
|
341
|
+
if (!modelConfig.apiKey || modelConfig.apiKey === 'your_api_key_here') {
|
|
342
|
+
errors.push(`API key for "${selectedModel}" is missing or not configured`);
|
|
343
|
+
}
|
|
344
|
+
if (!modelConfig.baseURL) {
|
|
345
|
+
errors.push(`Base URL for "${selectedModel}" is missing`);
|
|
346
|
+
}
|
|
347
|
+
if (!modelConfig.model) {
|
|
348
|
+
errors.push(`Model identifier for "${selectedModel}" is not set`);
|
|
349
|
+
}
|
|
350
|
+
if (!modelConfig.supportsFunctionCalling) {
|
|
351
|
+
errors.push(`Model "${selectedModel}" does not support function calling (required)`);
|
|
352
|
+
}
|
|
353
|
+
// Validate browser configuration
|
|
354
|
+
if (config.browser) {
|
|
355
|
+
if (config.browser.timeout && typeof config.browser.timeout !== 'number') {
|
|
356
|
+
errors.push('Browser timeout must be a number');
|
|
357
|
+
}
|
|
358
|
+
const validLevels = ['none', 'low', 'medium', 'high'];
|
|
359
|
+
if (config.browser.sanitizationLevel &&
|
|
360
|
+
!validLevels.includes(config.browser.sanitizationLevel) &&
|
|
361
|
+
typeof config.browser.sanitizationLevel !== 'number') {
|
|
362
|
+
errors.push(`Invalid browser.sanitizationLevel: "${config.browser.sanitizationLevel}". Must be one of: ${validLevels.join(', ')} or 0-3`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
// Validate playlist configuration (optional, but warn if configured incorrectly)
|
|
366
|
+
if (config.playlist && config.playlist.privateKey) {
|
|
367
|
+
const key = config.playlist.privateKey;
|
|
368
|
+
const placeholderPattern = /your_ed25519_private_key/i;
|
|
369
|
+
if (!placeholderPattern.test(key) && typeof key === 'string' && key.length > 0) {
|
|
370
|
+
const base64Regex = /^[A-Za-z0-9+/]+=*$/;
|
|
371
|
+
const hexRegex = /^(0x)?[0-9a-fA-F]+$/;
|
|
372
|
+
if (!base64Regex.test(key) && !hexRegex.test(key)) {
|
|
373
|
+
errors.push('playlist.privateKey must be a valid base64- or hex-encoded ed25519 private key');
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (config.playlist?.role !== undefined || process.env.PLAYLIST_ROLE !== undefined) {
|
|
378
|
+
try {
|
|
379
|
+
resolveEffectivePlaylistRole(config);
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
if (error instanceof Error &&
|
|
383
|
+
error.message.startsWith('Unsupported DP-1 playlist signing role')) {
|
|
384
|
+
errors.push(`playlist.role must be one of: ${playlist_signing_role_1.DP1_PLAYLIST_SIGNING_ROLES.join(', ')}`);
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
errors.push(error.message);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
valid: errors.length === 0,
|
|
393
|
+
errors,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
errors.push(error.message);
|
|
398
|
+
return { valid: false, errors };
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Create a sample config.json file from config.json.example
|
|
403
|
+
*
|
|
404
|
+
* Loads the bundled config.json.example template from the package directory
|
|
405
|
+
* and writes it to the user's current working directory.
|
|
406
|
+
*
|
|
407
|
+
* @returns {Promise<string>} Path to the created config file
|
|
408
|
+
* @throws {Error} If config.json already exists or example file is missing
|
|
409
|
+
*/
|
|
410
|
+
async function createSampleConfig(targetPath) {
|
|
411
|
+
const { userPath } = getConfigPaths();
|
|
412
|
+
const configPath = targetPath || userPath;
|
|
413
|
+
// Check if config.json already exists in user's directory
|
|
414
|
+
if (fs_1.default.existsSync(configPath)) {
|
|
415
|
+
throw new Error('config.json already exists');
|
|
416
|
+
}
|
|
417
|
+
// Look for config.json.example in the package directory
|
|
418
|
+
// When compiled, this file is in dist/src/config.js
|
|
419
|
+
// The template is at the package root: ../../config.json.example
|
|
420
|
+
const exampleCandidates = [
|
|
421
|
+
path_1.default.join(process.cwd(), 'config.json.example'),
|
|
422
|
+
path_1.default.join(__dirname, '../..', 'config.json.example'),
|
|
423
|
+
];
|
|
424
|
+
const examplePath = exampleCandidates.find((candidate) => fs_1.default.existsSync(candidate));
|
|
425
|
+
if (!examplePath) {
|
|
426
|
+
throw new Error('config.json.example not found. This is likely a package installation issue.');
|
|
427
|
+
}
|
|
428
|
+
const exampleConfig = fs_1.default.readFileSync(examplePath, 'utf-8');
|
|
429
|
+
fs_1.default.mkdirSync(path_1.default.dirname(configPath), { recursive: true });
|
|
430
|
+
fs_1.default.writeFileSync(configPath, exampleConfig, 'utf-8');
|
|
431
|
+
return configPath;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* List all available models
|
|
435
|
+
*
|
|
436
|
+
* @returns {Array<string>} List of available model names
|
|
437
|
+
*/
|
|
438
|
+
function listAvailableModels() {
|
|
439
|
+
const config = getConfig();
|
|
440
|
+
return Object.keys(config.models);
|
|
441
|
+
}
|