@feralfile/cli 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +96 -0
  3. package/config.json.example +96 -0
  4. package/dist/index.js +54 -0
  5. package/dist/src/ai-orchestrator/index.js +1019 -0
  6. package/dist/src/ai-orchestrator/registry.js +96 -0
  7. package/dist/src/commands/build.js +69 -0
  8. package/dist/src/commands/chat.js +189 -0
  9. package/dist/src/commands/config.js +68 -0
  10. package/dist/src/commands/device.js +278 -0
  11. package/dist/src/commands/helpers/config-files.js +62 -0
  12. package/dist/src/commands/helpers/device-discovery.js +111 -0
  13. package/dist/src/commands/helpers/playlist-display.js +161 -0
  14. package/dist/src/commands/helpers/prompt.js +65 -0
  15. package/dist/src/commands/helpers/ssh-helpers.js +44 -0
  16. package/dist/src/commands/play.js +110 -0
  17. package/dist/src/commands/publish.js +115 -0
  18. package/dist/src/commands/setup.js +225 -0
  19. package/dist/src/commands/sign.js +41 -0
  20. package/dist/src/commands/ssh.js +108 -0
  21. package/dist/src/commands/status.js +126 -0
  22. package/dist/src/commands/validate.js +18 -0
  23. package/dist/src/config.js +441 -0
  24. package/dist/src/intent-parser/index.js +1382 -0
  25. package/dist/src/intent-parser/utils.js +108 -0
  26. package/dist/src/logger.js +82 -0
  27. package/dist/src/main.js +459 -0
  28. package/dist/src/types.js +5 -0
  29. package/dist/src/utilities/address-validator.js +242 -0
  30. package/dist/src/utilities/device-default.js +36 -0
  31. package/dist/src/utilities/device-lookup.js +107 -0
  32. package/dist/src/utilities/device-normalize.js +62 -0
  33. package/dist/src/utilities/device-upsert.js +91 -0
  34. package/dist/src/utilities/domain-resolver.js +291 -0
  35. package/dist/src/utilities/ed25519-key-derive.js +155 -0
  36. package/dist/src/utilities/feed-fetcher.js +471 -0
  37. package/dist/src/utilities/ff1-compatibility.js +269 -0
  38. package/dist/src/utilities/ff1-device.js +250 -0
  39. package/dist/src/utilities/ff1-discovery.js +330 -0
  40. package/dist/src/utilities/functions.js +308 -0
  41. package/dist/src/utilities/index.js +469 -0
  42. package/dist/src/utilities/nft-indexer.js +1024 -0
  43. package/dist/src/utilities/playlist-builder.js +523 -0
  44. package/dist/src/utilities/playlist-publisher.js +131 -0
  45. package/dist/src/utilities/playlist-send.js +260 -0
  46. package/dist/src/utilities/playlist-signer.js +204 -0
  47. package/dist/src/utilities/playlist-signing-role.js +41 -0
  48. package/dist/src/utilities/playlist-source.js +128 -0
  49. package/dist/src/utilities/playlist-verifier.js +274 -0
  50. package/dist/src/utilities/ssh-access.js +145 -0
  51. package/dist/src/utils.js +48 -0
  52. package/docs/CONFIGURATION.md +206 -0
  53. package/docs/EXAMPLES.md +390 -0
  54. package/docs/FUNCTION_CALLING.md +96 -0
  55. package/docs/PROJECT_SPEC.md +228 -0
  56. package/docs/README.md +348 -0
  57. package/docs/RELEASING.md +73 -0
  58. package/package.json +76 -0
@@ -0,0 +1,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
+ }