@feralfile/cli 1.6.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@ A small Node.js CLI for building DP-1 playlists of digital art.
4
4
 
5
5
  **Runtime:** Node.js 22 or newer (matches CI and the `dp1-js` dependency). That engine floor is a **breaking** change if you previously used Node 18 or 20—check the **GitHub Release** for the version you move to; release authors follow `docs/RELEASING.md` so the notes stay explicit.
6
6
 
7
- ff-cli turns a simple prompt into a DP-1–conformant playlist you can preview on an Art Computer. The model orchestrates; deterministic tools do the real work (schema validation, indexing, JSON‑LD). If something comes back invalid, validation rejects it and we loop until it’s right.
7
+ ff-cli is a set of deterministic commands for building DP-1–conformant playlists and playing them on an Art Computer: schema validation, indexing, JSON‑LD, signing, and device delivery. There's no built-in chat — natural language belongs in your coding agent (Claude Code, Codex), which drives ff-cli through the `ff-control` skill (see [Use with Claude Code](#use-with-claude-code)). No LLM API key required.
8
8
 
9
9
  ## Install
10
10
 
@@ -24,16 +24,14 @@ Installs a prebuilt binary for macOS/Linux (no Node.js required).
24
24
 
25
25
  ```bash
26
26
  npx @feralfile/cli setup
27
- npx @feralfile/cli chat
27
+ npx @feralfile/cli find https://www.artblocks.io/collection/ringers-by-dmitri-cherniak --play
28
28
  ```
29
29
 
30
30
  ## Quick Start
31
31
 
32
- **Set your LLM API key first (default Claude):** `export ANTHROPIC_API_KEY='sk-ant-your-api-key-here'`
33
-
34
32
  ```bash
35
33
  ff-cli setup
36
- ff-cli chat
34
+ ff-cli find https://objkt.com/tokens/hicetnunc/111068 --play
37
35
  ff-cli play "https://example.com/video.mp4" --skip-verify
38
36
  ```
39
37
 
@@ -96,12 +94,10 @@ Codex will surface it when you ask to build a playlist, play an artwork or URL o
96
94
 
97
95
  ## Dev Quick Start
98
96
 
99
- **Set your LLM API key first (default Claude):** `export ANTHROPIC_API_KEY='sk-ant-your-api-key-here'`
100
-
101
97
  ```bash
102
98
  npm ci
103
99
  npm run dev -- setup
104
- npm run dev -- chat
100
+ npm run dev -- find https://objkt.com/tokens/hicetnunc/111068 --play
105
101
  npm run dev -- play "https://example.com/video.mp4" --skip-verify
106
102
  ```
107
103
 
@@ -109,16 +105,15 @@ npm run dev -- play "https://example.com/video.mp4" --skip-verify
109
105
 
110
106
  - Getting started and usage: `./docs/README.md`
111
107
  - Configuration: `./docs/CONFIGURATION.md`
112
- - Function calling architecture: `./docs/FUNCTION_CALLING.md`
113
108
  - Examples: `./docs/EXAMPLES.md`
114
109
  - SSH access: `ff-cli ssh enable|disable` in `./docs/README.md`
115
110
 
116
111
  ## Verification
117
112
 
118
- GitHub Actions runs `.github/workflows/ci.yml` for pull requests, pushes to `main`/`master`, and reusable `workflow_call` jobs. CI uses Node.js 22, installs dependencies with `npm ci`, sets `ANTHROPIC_API_KEY=dummy`, and runs the repo-wide verification entrypoint:
113
+ GitHub Actions runs `.github/workflows/ci.yml` for pull requests, pushes to `main`/`master`, and reusable `workflow_call` jobs. CI uses Node.js 22, installs dependencies with `npm ci`, and runs the repo-wide verification entrypoint:
119
114
 
120
115
  ```bash
121
- ANTHROPIC_API_KEY=dummy npm run verify
116
+ npm run verify
122
117
  ```
123
118
 
124
119
  Run the same command locally before opening a PR. It checks formatting, lint, tests, TypeScript build, playlist validation smoke, and config validation smoke without mutating source files.
@@ -1,69 +1,4 @@
1
1
  {
2
- "defaultModel": "claude",
3
- "models": {
4
- "claude": {
5
- "apiKey": "YOUR_ANTHROPIC_API_KEY_FROM_CONSOLE_ANTHROPIC_COM",
6
- "baseURL": "https://api.anthropic.com/v1/",
7
- "model": "claude-sonnet-4-6",
8
- "availableModels": [
9
- "claude-opus-4-7",
10
- "claude-sonnet-4-6",
11
- "claude-haiku-4-5-20251001"
12
- ],
13
- "timeout": 30000,
14
- "maxRetries": 3,
15
- "temperature": 0.3,
16
- "maxTokens": 4000,
17
- "supportsFunctionCalling": true
18
- },
19
- "grok": {
20
- "apiKey": "YOUR_GROK_API_KEY_FROM_X_AI_CONSOLE",
21
- "baseURL": "https://api.x.ai/v1",
22
- "model": "grok-beta",
23
- "availableModels": [
24
- "grok-beta",
25
- "grok-2-1212",
26
- "grok-2-vision-1212"
27
- ],
28
- "timeout": 120000,
29
- "maxRetries": 3,
30
- "temperature": 0.3,
31
- "maxTokens": 4000,
32
- "supportsFunctionCalling": true
33
- },
34
- "gpt": {
35
- "apiKey": "YOUR_OPENAI_API_KEY_FROM_PLATFORM_OPENAI_COM",
36
- "baseURL": "https://api.openai.com/v1",
37
- "model": "gpt-4.1",
38
- "availableModels": [
39
- "gpt-4.1",
40
- "gpt-4.1-mini",
41
- "gpt-4o",
42
- "gpt-4o-mini",
43
- "gpt-4-turbo"
44
- ],
45
- "timeout": 30000,
46
- "maxRetries": 3,
47
- "temperature": 1,
48
- "maxTokens": 4000,
49
- "supportsFunctionCalling": true
50
- },
51
- "gemini": {
52
- "apiKey": "YOUR_GOOGLE_AI_STUDIO_API_KEY_FROM_AISTUDIO_GOOGLE_COM",
53
- "baseURL": "https://generativelanguage.googleapis.com/v1beta/openai/",
54
- "model": "gemini-2.5-flash",
55
- "availableModels": [
56
- "gemini-2.5-flash-lite",
57
- "gemini-2.5-flash",
58
- "gemini-flash-lite-latest"
59
- ],
60
- "timeout": 30000,
61
- "maxRetries": 3,
62
- "temperature": 0.3,
63
- "maxTokens": 4000,
64
- "supportsFunctionCalling": true
65
- }
66
- },
67
2
  "defaultDuration": 10,
68
3
  "browser": {
69
4
  "timeout": 90000,
package/dist/index.js CHANGED
@@ -14,7 +14,6 @@ const fs_1 = require("fs");
14
14
  const path_1 = require("path");
15
15
  const setup_1 = require("./src/commands/setup");
16
16
  const status_1 = require("./src/commands/status");
17
- const chat_1 = require("./src/commands/chat");
18
17
  const validate_1 = require("./src/commands/validate");
19
18
  const sign_1 = require("./src/commands/sign");
20
19
  const play_1 = require("./src/commands/play");
@@ -39,10 +38,9 @@ program
39
38
  .name('ff-cli')
40
39
  .description(packageDescription)
41
40
  .version(packageVersion)
42
- .addHelpText('after', `\nQuick start:\n 1) ff-cli setup\n 2) ff-cli chat\n\nDocs: https://github.com/feral-file/ff-cli\n`);
41
+ .addHelpText('after', `\nQuick start:\n 1) ff-cli setup\n 2) ff-cli play <url-or-playlist>\n\nDocs: https://github.com/feral-file/ff-cli\n`);
43
42
  program.addCommand(setup_1.setupCommand);
44
43
  program.addCommand(status_1.statusCommand);
45
- program.addCommand(chat_1.chatCommand);
46
44
  program.addCommand(validate_1.verifyCommand);
47
45
  program.addCommand(validate_1.validateCommand);
48
46
  program.addCommand(sign_1.signCommand);
package/dist/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@feralfile/cli",
3
- "version": "1.6.0",
4
- "description": "CLI for building DP-1 playlists of digital art using AI (Claude, Grok, ChatGPT, Gemini)",
3
+ "version": "2.0.0",
4
+ "description": "CLI for building, signing, and playing DP-1 playlists of digital art on Feral File devices",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/feral-file/ff-cli.git"
@@ -51,7 +51,7 @@
51
51
  "nft",
52
52
  "playlist",
53
53
  "dp1",
54
- "grok",
54
+ "feralfile",
55
55
  "cli"
56
56
  ],
57
57
  "author": "",
@@ -65,7 +65,6 @@
65
65
  "dp1-js": "2.0.0",
66
66
  "ff1": "^1.0.0",
67
67
  "fuzzysort": "^3.1.0",
68
- "openai": "^4.58.1",
69
68
  "viem": "^2.38.2"
70
69
  },
71
70
  "devDependencies": {
@@ -25,20 +25,22 @@ exports.configCommand = new commander_1.Command('config')
25
25
  else if (action === 'show') {
26
26
  const config = (0, config_1.getConfig)();
27
27
  console.log(chalk_1.default.blue('\nCurrent configuration\n'));
28
- console.log(chalk_1.default.bold('Default model:'), chalk_1.default.white(config.defaultModel));
29
28
  console.log(chalk_1.default.bold('Default duration:'), chalk_1.default.white(config.defaultDuration + 's'));
30
- console.log(chalk_1.default.bold('\nAvailable models:\n'));
31
- const models = (0, config_1.listAvailableModels)();
32
- models.forEach((modelName) => {
33
- const modelConfig = config.models[modelName];
34
- const isCurrent = modelName === config.defaultModel;
35
- console.log(` ${isCurrent ? chalk_1.default.green('→') : ' '} ${chalk_1.default.bold(modelName)}`);
36
- console.log(` API key: ${modelConfig.apiKey && modelConfig.apiKey !== 'your_api_key_here' ? chalk_1.default.green('Set') : chalk_1.default.red('Missing')}`);
37
- console.log(` Base URL: ${chalk_1.default.dim(modelConfig.baseURL)}`);
38
- console.log(` Model: ${chalk_1.default.dim(modelConfig.model)}`);
39
- console.log(` Function calling: ${modelConfig.supportsFunctionCalling ? chalk_1.default.green('Supported') : chalk_1.default.red('Not supported')}`);
40
- console.log();
41
- });
29
+ const feedServers = config.feedServers || [];
30
+ if (feedServers.length > 0) {
31
+ console.log(chalk_1.default.bold('\nFeed servers:\n'));
32
+ feedServers.forEach((server) => {
33
+ console.log(` ${chalk_1.default.dim(server.baseUrl)}`);
34
+ });
35
+ }
36
+ const devices = config.ff1Devices?.devices || [];
37
+ if (devices.length > 0) {
38
+ console.log(chalk_1.default.bold('\nFF1 devices:\n'));
39
+ devices.forEach((device) => {
40
+ console.log(` ${chalk_1.default.bold(device.name || 'unnamed')} ${chalk_1.default.dim(`→ ${device.host}`)}`);
41
+ });
42
+ }
43
+ console.log();
42
44
  }
43
45
  else if (action === 'validate') {
44
46
  const validation = (0, config_1.validateConfig)();
@@ -37,16 +37,60 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.playCommand = void 0;
40
+ exports.describeDiscoveryInput = describeDiscoveryInput;
40
41
  const commander_1 = require("commander");
41
42
  const chalk_1 = __importDefault(require("chalk"));
42
43
  const config_1 = require("../config");
43
44
  const playlist_source_1 = require("../utilities/playlist-source");
45
+ const marketplace_url_1 = require("../utilities/marketplace-url");
44
46
  const playlist_display_1 = require("./helpers/playlist-display");
45
47
  // ff1-device is still CommonJS; require keeps the interop simple.
46
48
  // eslint-disable-next-line @typescript-eslint/no-require-imports
47
49
  const { sendPlaylistToDevice } = require('../utilities/ff1-device');
48
50
  // eslint-disable-next-line @typescript-eslint/no-require-imports
49
51
  const { signPlaylist } = require('../utilities/playlist-signer');
52
+ /**
53
+ * Human label for a discovery input that `play` cannot cast directly.
54
+ *
55
+ * `play` casts an exact, already-playable source (a playlist file, a hosted
56
+ * playlist URL, or a direct media/web URL). A marketplace URL, on-chain
57
+ * coordinates, or a wallet address must be *resolved* through the indexer
58
+ * first — that is `find`'s job. Returns a label for those inputs so `play`
59
+ * can redirect the user instead of silently wrapping, say, an Art Blocks
60
+ * collection *page* as a web item. Returns null for anything `play` should
61
+ * handle itself (plain URLs and file paths parse to null here).
62
+ */
63
+ function describeDiscoveryInput(source) {
64
+ const parsed = (0, marketplace_url_1.parseFindInput)(source);
65
+ if (!parsed || parsed.kind === 'unsupported') {
66
+ return null;
67
+ }
68
+ switch (parsed.kind) {
69
+ case 'token':
70
+ return 'an on-chain token reference';
71
+ case 'address':
72
+ return 'a wallet address';
73
+ case 'ff-url':
74
+ return 'a Feral File URL';
75
+ case 'objkt-alias':
76
+ return 'an Objkt token';
77
+ case 'ab-collection':
78
+ return 'an Art Blocks collection';
79
+ case 'os-collection':
80
+ return 'an OpenSea collection';
81
+ case 'fxhash-iteration':
82
+ case 'fxhash-project':
83
+ return 'an fxhash work';
84
+ case 'neort-art':
85
+ return 'a Neort work';
86
+ case 'verse-series':
87
+ return 'a Verse series';
88
+ case 'raster-artwork':
89
+ return 'a Raster artwork';
90
+ default:
91
+ return 'an artwork reference';
92
+ }
93
+ }
50
94
  exports.playCommand = new commander_1.Command('play')
51
95
  .description('Play a playlist or media URL on an FF1 device')
52
96
  .argument('<source>', 'Playlist file, playlist URL, or media URL')
@@ -54,6 +98,17 @@ exports.playCommand = new commander_1.Command('play')
54
98
  .option('--skip-verify', 'Skip signature verification and auto-sign gating before playing; use only when you accept malformed envelopes')
55
99
  .action(async (source, options) => {
56
100
  try {
101
+ // `play` casts an exact playable source. A marketplace URL / on-chain
102
+ // coords / wallet must be resolved through the indexer first — redirect
103
+ // to `find` rather than silently wrapping the *page* as a web item.
104
+ const discovery = describeDiscoveryInput(source);
105
+ if (discovery) {
106
+ console.error(chalk_1.default.red('\n`play` casts an exact playlist or media URL — it does not search marketplaces.'));
107
+ console.error(chalk_1.default.dim(` "${source}" looks like ${discovery} to resolve first.`));
108
+ const deviceFlag = options.device ? ` -d "${options.device}"` : '';
109
+ console.log(chalk_1.default.yellow(`\nTry: ff-cli find "${source}" --play${deviceFlag}`));
110
+ process.exit(1);
111
+ }
57
112
  const config = (0, config_1.getConfig)();
58
113
  // No explicit duration: a direct video/audio URL plays its natural
59
114
  // length (DP-1 §4.1); static media uses config.defaultDuration.
@@ -10,7 +10,6 @@ const crypto_1 = __importDefault(require("crypto"));
10
10
  const fs_1 = require("fs");
11
11
  const device_lookup_1 = require("../utilities/device-lookup");
12
12
  const device_upsert_1 = require("../utilities/device-upsert");
13
- const config_1 = require("../config");
14
13
  const config_files_1 = require("./helpers/config-files");
15
14
  const device_discovery_1 = require("./helpers/device-discovery");
16
15
  const prompt_1 = require("./helpers/prompt");
@@ -26,76 +25,9 @@ exports.setupCommand = new commander_1.Command('setup')
26
25
  console.log(chalk_1.default.green(`Created ${configPath}`));
27
26
  }
28
27
  const config = await (0, config_files_1.readConfigFile)(configPath);
29
- const mergedDefaults = (0, config_1.getConfig)();
30
- // Show every known provider in the menu (in-code defaults first, then any
31
- // custom providers the user has added to the file) so adding a new provider
32
- // to defaults shows up for existing users without rerunning createSampleConfig.
33
- const modelNames = Array.from(new Set([...(0, config_1.listAvailableModels)(), ...Object.keys(config.models || {})]));
34
- if (modelNames.length === 0) {
35
- console.error(chalk_1.default.red('No models found in config.json'));
36
- process.exit(1);
37
- }
38
28
  console.log(chalk_1.default.blue('\nff-cli setup\n'));
39
29
  prompt = (0, prompt_1.createPrompt)();
40
30
  const ask = prompt.ask;
41
- const currentModel = config.defaultModel && modelNames.includes(config.defaultModel)
42
- ? config.defaultModel
43
- : modelNames[0];
44
- let selectedModel = currentModel;
45
- while (true) {
46
- const modelAnswer = await ask(`Default model (${modelNames.join(', ')}) [${currentModel}]: `);
47
- if (!modelAnswer) {
48
- selectedModel = currentModel;
49
- break;
50
- }
51
- if (modelNames.includes(modelAnswer)) {
52
- selectedModel = modelAnswer;
53
- break;
54
- }
55
- console.log(chalk_1.default.red(`Unknown model: ${modelAnswer}`));
56
- }
57
- config.defaultModel = selectedModel;
58
- if (!config.models) {
59
- config.models = {};
60
- }
61
- // Seed missing entries from in-code defaults so baseURL/model are populated,
62
- // but blank the apiKey so the user is prompted instead of silently inheriting
63
- // an env-derived value.
64
- const providerDefaults = mergedDefaults.models[selectedModel];
65
- const selectedModelConfig = config.models[selectedModel] ||
66
- (providerDefaults
67
- ? { ...providerDefaults, apiKey: '' }
68
- : {
69
- apiKey: '',
70
- baseURL: '',
71
- model: '',
72
- timeout: 0,
73
- maxRetries: 0,
74
- temperature: 0,
75
- maxTokens: 0,
76
- supportsFunctionCalling: true,
77
- });
78
- const hasApiKeyForModel = !(0, config_files_1.isMissingConfigValue)(selectedModelConfig.apiKey);
79
- const keyHelpUrls = {
80
- grok: 'https://console.x.ai/',
81
- gpt: 'https://platform.openai.com/api-keys',
82
- gemini: 'https://aistudio.google.com/app/apikey',
83
- claude: 'https://console.anthropic.com/settings/keys',
84
- };
85
- if (!hasApiKeyForModel) {
86
- const helpUrl = keyHelpUrls[selectedModel];
87
- if (helpUrl) {
88
- console.log(chalk_1.default.dim(helpUrl));
89
- }
90
- }
91
- const apiKeyPrompt = hasApiKeyForModel
92
- ? `API key for ${selectedModel} (leave blank to keep current): `
93
- : `API key for ${selectedModel} (optional, only needed for chat): `;
94
- const apiKeyAnswer = await ask(apiKeyPrompt);
95
- if (apiKeyAnswer) {
96
- selectedModelConfig.apiKey = apiKeyAnswer;
97
- }
98
- config.models[selectedModel] = selectedModelConfig;
99
31
  const currentKey = config.playlist?.privateKey || '';
100
32
  const currentRole = config.playlist?.role || '';
101
33
  let signingKey = currentKey;
@@ -202,7 +134,6 @@ exports.setupCommand = new commander_1.Command('setup')
202
134
  prompt = null;
203
135
  console.log(chalk_1.default.green('\nSetup complete'));
204
136
  console.log(chalk_1.default.dim(` Config: ${configPath}`));
205
- const hasApiKey = !(0, config_files_1.isMissingConfigValue)(config.models[selectedModel]?.apiKey);
206
137
  const hasSigningKey = !(0, config_files_1.isMissingConfigValue)(config.playlist?.privateKey || '');
207
138
  const hasDevice = (0, config_placeholders_1.configuredFF1Devices)(config.ff1Devices?.devices || []).length > 0;
208
139
  if (!hasSigningKey || !hasDevice) {
@@ -214,10 +145,7 @@ exports.setupCommand = new commander_1.Command('setup')
214
145
  console.log(chalk_1.default.yellow(' • Add an FF1 device host'));
215
146
  }
216
147
  }
217
- if (!hasApiKey) {
218
- console.log(chalk_1.default.dim(`\nTo use ff-cli chat, add an API key for ${selectedModel}`));
219
- }
220
- console.log(chalk_1.default.dim('\nRun: ff-cli play'));
148
+ console.log(chalk_1.default.dim('\nRun: ff-cli play <url-or-playlist>'));
221
149
  }
222
150
  catch (error) {
223
151
  console.error(chalk_1.default.red('\nSetup failed:'), error.message);
@@ -22,14 +22,7 @@ exports.statusCommand = new commander_1.Command('status')
22
22
  process.exit(1);
23
23
  }
24
24
  const config = await (0, config_files_1.readConfigFile)(configPath);
25
- const modelNames = Object.keys(config.models || {});
26
- const defaultModel = config.defaultModel && modelNames.includes(config.defaultModel)
27
- ? config.defaultModel
28
- : modelNames[0];
29
- const defaultModelLabel = defaultModel || 'unknown';
30
- const defaultModelConfig = defaultModel ? config.models?.[defaultModel] : undefined;
31
25
  const playlistConfig = (0, config_1.getPlaylistConfig)();
32
- const hasApiKey = defaultModel ? !(0, config_files_1.isMissingConfigValue)(defaultModelConfig?.apiKey) : false;
33
26
  const playlistKeyMaterial = playlistConfig.privateKey?.trim() || '';
34
27
  const playlistKeyError = playlistKeyMaterial.length > 0 ? validatePlaylistPrivateKey(playlistKeyMaterial) : null;
35
28
  const hasPlaylistSigningKey = playlistKeyMaterial.length > 0 && playlistKeyError === null;
@@ -51,12 +44,6 @@ exports.statusCommand = new commander_1.Command('status')
51
44
  ok: true,
52
45
  detail: configPath,
53
46
  },
54
- {
55
- label: `Default model (${defaultModelLabel}) API key`,
56
- ok: hasApiKey,
57
- optional: true,
58
- hint: ' (needed for chat)',
59
- },
60
47
  {
61
48
  label: 'Playlist signing key',
62
49
  ok: hasPlaylistSigningKey,
@@ -10,10 +10,8 @@ exports.getBrowserConfig = getBrowserConfig;
10
10
  exports.getPlaylistConfig = getPlaylistConfig;
11
11
  exports.getFeedConfig = getFeedConfig;
12
12
  exports.getFF1DeviceConfig = getFF1DeviceConfig;
13
- exports.getModelConfig = getModelConfig;
14
13
  exports.validateConfig = validateConfig;
15
14
  exports.createSampleConfig = createSampleConfig;
16
- exports.listAvailableModels = listAvailableModels;
17
15
  const fs_1 = __importDefault(require("fs"));
18
16
  const path_1 = __importDefault(require("path"));
19
17
  const os_1 = __importDefault(require("os"));
@@ -58,62 +56,12 @@ function getConfigPaths() {
58
56
  * Load configuration from config.json or environment variables
59
57
  * Priority: config.json > .env > defaults
60
58
  *
61
- * @returns {Object} Configuration object with model settings
62
- * @returns {string} returns.defaultModel - Name of the default model to use
63
- * @returns {Object} returns.models - Available models configuration
59
+ * @returns {Object} Configuration object
64
60
  * @returns {number} returns.defaultDuration - Default duration per item in seconds
65
61
  */
66
62
  function loadConfig() {
67
63
  const { localPath, userPath } = getConfigPaths();
68
- // Default configuration supporting Grok as default
69
64
  const defaultConfig = {
70
- defaultModel: process.env.DEFAULT_MODEL || 'claude',
71
- models: {
72
- claude: {
73
- apiKey: process.env.ANTHROPIC_API_KEY || '',
74
- baseURL: 'https://api.anthropic.com/v1/',
75
- model: 'claude-sonnet-4-6',
76
- availableModels: ['claude-opus-4-7', 'claude-sonnet-4-6', 'claude-haiku-4-5-20251001'],
77
- timeout: 30000,
78
- maxRetries: 3,
79
- temperature: 0.3,
80
- maxTokens: 4000,
81
- supportsFunctionCalling: true,
82
- },
83
- grok: {
84
- apiKey: process.env.GROK_API_KEY || '',
85
- baseURL: process.env.GROK_API_BASE_URL || 'https://api.x.ai/v1',
86
- model: process.env.GROK_MODEL || 'grok-beta',
87
- availableModels: ['grok-beta', 'grok-2-1212', 'grok-2-vision-1212'],
88
- timeout: parseInt(process.env.TIMEOUT || '30000', 10),
89
- maxRetries: parseInt(process.env.MAX_RETRIES || '3', 10),
90
- temperature: parseFloat(process.env.TEMPERATURE || '0.3'),
91
- maxTokens: parseInt(process.env.MAX_TOKENS || '4000', 10),
92
- supportsFunctionCalling: true,
93
- },
94
- gpt: {
95
- apiKey: process.env.OPENAI_API_KEY || '',
96
- baseURL: 'https://api.openai.com/v1',
97
- model: 'gpt-4.1',
98
- availableModels: ['gpt-4.1', 'gpt-4.1-mini', 'gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo'],
99
- timeout: 30000,
100
- maxRetries: 3,
101
- temperature: 0.3,
102
- maxTokens: 4000,
103
- supportsFunctionCalling: true,
104
- },
105
- gemini: {
106
- apiKey: process.env.GEMINI_API_KEY || '',
107
- baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai/',
108
- model: 'gemini-2.5-flash',
109
- availableModels: ['gemini-2.5-flash-lite', 'gemini-2.5-flash', 'gemini-flash-lite-latest'],
110
- timeout: 30000,
111
- maxRetries: 3,
112
- temperature: 0.3,
113
- maxTokens: 4000,
114
- supportsFunctionCalling: true,
115
- },
116
- },
117
65
  defaultDuration: parseInt(process.env.DEFAULT_DURATION || '10', 10),
118
66
  browser: {
119
67
  timeout: parseInt(process.env.BROWSER_TIMEOUT || '90000', 10),
@@ -130,21 +78,10 @@ function loadConfig() {
130
78
  if (fs_1.default.existsSync(configPath)) {
131
79
  try {
132
80
  const fileConfig = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
133
- // Deep merge models configuration
134
- const mergedModels = { ...defaultConfig.models };
135
- if (fileConfig.models) {
136
- Object.keys(fileConfig.models).forEach((modelName) => {
137
- mergedModels[modelName] = {
138
- ...(defaultConfig.models[modelName] || {}),
139
- ...fileConfig.models[modelName],
140
- };
141
- });
142
- }
143
81
  // Merge with defaults, file config takes precedence
144
82
  return {
145
83
  ...defaultConfig,
146
84
  ...fileConfig,
147
- models: mergedModels,
148
85
  };
149
86
  }
150
87
  catch (error) {
@@ -290,67 +227,16 @@ function getFF1DeviceConfig() {
290
227
  };
291
228
  }
292
229
  /**
293
- * Get configuration for a specific model
294
- *
295
- * @param {string} [modelName] - Name of the model (defaults to defaultModel from config)
296
- * @returns {Object} Model configuration
297
- * @returns {string} returns.apiKey - API key for the model
298
- * @returns {string} returns.baseURL - Base URL for the API
299
- * @returns {string} returns.model - Model name/identifier
300
- * @returns {number} returns.timeout - Request timeout in milliseconds
301
- * @returns {number} returns.maxRetries - Maximum number of retries
302
- * @returns {number} returns.temperature - Temperature for generation
303
- * @returns {number} returns.maxTokens - Maximum tokens for generation
304
- * @returns {boolean} returns.supportsFunctionCalling - Whether model supports function calling
305
- * @throws {Error} If model is not configured or doesn't support function calling
306
- */
307
- function getModelConfig(modelName) {
308
- const config = getConfig();
309
- const selectedModel = modelName || config.defaultModel;
310
- if (!config.models[selectedModel]) {
311
- throw new Error(`Model "${selectedModel}" is not configured. Available models: ${Object.keys(config.models).join(', ')}`);
312
- }
313
- const modelConfig = config.models[selectedModel];
314
- if (!modelConfig.supportsFunctionCalling) {
315
- throw new Error(`Model "${selectedModel}" does not support function calling`);
316
- }
317
- const normalizedBaseURL = modelConfig.baseURL?.replace(/\/+$/, '');
318
- return {
319
- ...modelConfig,
320
- baseURL: normalizedBaseURL,
321
- defaultDuration: config.defaultDuration,
322
- };
323
- }
324
- /**
325
- * Validate configuration for a specific model
230
+ * Validate configuration
326
231
  *
327
- * @param {string} [modelName] - Name of the model to validate
328
232
  * @returns {Object} Validation result
329
233
  * @returns {boolean} returns.valid - Whether the configuration is valid
330
234
  * @returns {Array<string>} returns.errors - List of validation errors
331
235
  */
332
- function validateConfig(modelName) {
236
+ function validateConfig() {
333
237
  const errors = [];
334
238
  try {
335
239
  const config = getConfig();
336
- const selectedModel = modelName || config.defaultModel;
337
- if (!config.models[selectedModel]) {
338
- errors.push(`Model "${selectedModel}" is not configured. Available: ${Object.keys(config.models).join(', ')}`);
339
- return { valid: false, errors };
340
- }
341
- const modelConfig = config.models[selectedModel];
342
- if ((0, config_placeholders_1.isMissingConfigValue)(modelConfig.apiKey)) {
343
- errors.push(`API key for "${selectedModel}" is missing or not configured`);
344
- }
345
- if (!modelConfig.baseURL) {
346
- errors.push(`Base URL for "${selectedModel}" is missing`);
347
- }
348
- if (!modelConfig.model) {
349
- errors.push(`Model identifier for "${selectedModel}" is not set`);
350
- }
351
- if (!modelConfig.supportsFunctionCalling) {
352
- errors.push(`Model "${selectedModel}" does not support function calling (required)`);
353
- }
354
240
  // Validate browser configuration
355
241
  if (config.browser) {
356
242
  if (config.browser.timeout && typeof config.browser.timeout !== 'number') {
@@ -431,12 +317,3 @@ async function createSampleConfig(targetPath) {
431
317
  fs_1.default.writeFileSync(configPath, exampleConfig, 'utf-8');
432
318
  return configPath;
433
319
  }
434
- /**
435
- * List all available models
436
- *
437
- * @returns {Array<string>} List of available model names
438
- */
439
- function listAvailableModels() {
440
- const config = getConfig();
441
- return Object.keys(config.models);
442
- }