explorbot 0.5.7 → 0.5.8

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.
@@ -502,6 +502,7 @@ program
502
502
  .option('-g, --global', 'Configure explorbot in ~/.explorbot to run from anywhere')
503
503
  .option('--provider <name>', `AI provider for the global config: ${Object.keys(PROVIDERS).join(', ')}`)
504
504
  .option('--api-key <key>', 'API key stored in ~/.explorbot/.env')
505
+ .option('--url <url>', 'Web application URL written to web.url in the local config')
505
506
  .action(async (options) => {
506
507
  try {
507
508
  const { runInit } = await import('../src/commands/init-command.js');
@@ -512,6 +513,7 @@ program
512
513
  global: options.global,
513
514
  provider: options.provider,
514
515
  apiKey: options.apiKey,
516
+ url: options.url,
515
517
  });
516
518
  } catch (error) {
517
519
  console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
@@ -452,6 +452,7 @@ program
452
452
  .option('-g, --global', 'Configure explorbot in ~/.explorbot to run from anywhere')
453
453
  .option('--provider <name>', `AI provider for the global config: ${Object.keys(PROVIDERS).join(', ')}`)
454
454
  .option('--api-key <key>', 'API key stored in ~/.explorbot/.env')
455
+ .option('--url <url>', 'Web application URL written to web.url in the local config')
455
456
  .action(async (options) => {
456
457
  try {
457
458
  const { runInit } = await import('../src/commands/init-command.js');
@@ -462,6 +463,7 @@ program
462
463
  global: options.global,
463
464
  provider: options.provider,
464
465
  apiKey: options.apiKey,
466
+ url: options.url,
465
467
  });
466
468
  }
467
469
  catch (error) {
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "explorbot",
3
- "version": "0.5.7",
3
+ "version": "0.5.8",
4
4
  "description": "CLI app built with React Ink, CodeceptJS, and Playwright",
5
5
  "license": "Elastic-2.0",
6
6
  "type": "module",
@@ -576,6 +576,13 @@ export class ExploreCommand extends BaseCommand {
576
576
  commands,
577
577
  });
578
578
  }
579
+ sections.push({
580
+ label: 'Explore more',
581
+ commands: [
582
+ { label: 'Whole site', command: `${cli} explore /` },
583
+ { label: 'Docs', command: 'https://testomat.ai/docs/explorbot' },
584
+ ],
585
+ });
579
586
  printNextSteps(sections);
580
587
  }
581
588
  isLimitReached() {
@@ -11,5 +11,6 @@ type InitCommandOptions = {
11
11
  global?: boolean;
12
12
  provider?: string;
13
13
  apiKey?: string;
14
+ url?: string;
14
15
  };
15
16
  export {};
@@ -1,24 +1,29 @@
1
1
  import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, extname, join, resolve } from 'node:path';
3
3
  import chalk from 'chalk';
4
+ import figureSet from 'figures';
4
5
  import { ConfigParser, PROVIDERS, missingModelRoles } from "../config.js";
5
6
  import { findGlobalConfig, globalConfigPath, globalDir, globalEnvPath } from "../global-config.js";
6
7
  import { getCliName } from "../utils/cli-name.js";
7
8
  import { log, tag } from '../utils/logger.js';
8
- import { relativeToCwd } from "../utils/next-steps.js";
9
- function defaultConfigTemplate(provider) {
9
+ import { printNextSteps, relativeToCwd } from "../utils/next-steps.js";
10
+ function defaultConfigTemplate(provider, url) {
11
+ const appUrl = url || 'http://<your-app-host-here>';
10
12
  return `// 'provider/model-id' uses a bundled provider.
11
13
  // It is also possible to import provider as a module from Vercel AI SDK.
12
- // https://github.com/testomatio/explorbot/blob/main/docs/basics/providers.md
14
+ // https://testomat.ai/docs/explorbot
13
15
 
14
16
  const config = {
15
17
  web: {
16
18
  // use application host without path prefix (e.g., http://localhost:3000)
17
- url: 'http://<your-app-host-here>',
19
+ url: '${appUrl}',
18
20
  },
19
21
 
20
22
  ai: {
21
- ${modelLines(provider)}
23
+ // every role (model, visionModel, agenticModel) takes the recommended ${provider} models bundled with this version
24
+ // override a single role as 'provider/model-id':
25
+ // model: '${provider}/<model-id>',
26
+ recommendedModels: '${provider}',
22
27
  },
23
28
 
24
29
  reporter: {
@@ -35,9 +40,10 @@ export default config;
35
40
  `;
36
41
  }
37
42
  export function envTemplate(provider) {
43
+ const { keyUrl } = PROVIDERS[provider];
38
44
  const keyLines = Object.entries(PROVIDERS).map(([name, { envKey }]) => {
39
45
  if (name === provider)
40
- return `${envKey}=`;
46
+ return `# Create a key at ${keyUrl} and paste it after '='\n${envKey}=`;
41
47
  return `# ${envKey}=`;
42
48
  });
43
49
  return `# AI provider API keys
@@ -64,10 +70,10 @@ export async function runInit(options) {
64
70
  const choice = await renderInitWizard('choose');
65
71
  if (choice !== 'local')
66
72
  return;
67
- const provider = await renderLocalProviderWizard();
68
- if (!provider)
73
+ const setup = await renderLocalSetupWizard();
74
+ if (!setup)
69
75
  return;
70
- runInitCommand({ ...options, provider });
76
+ runInitCommand({ ...options, ...setup });
71
77
  }
72
78
  export function writeGlobalConfig(provider, apiKey) {
73
79
  if (!PROVIDERS[provider]) {
@@ -75,21 +81,25 @@ export function writeGlobalConfig(provider, apiKey) {
75
81
  }
76
82
  mkdirSync(globalDir(), { recursive: true });
77
83
  writeFileSync(globalConfigPath(), globalConfigTemplate(provider), 'utf8');
78
- log(`Created global config: ${globalConfigPath()}`);
84
+ tag('success').log(`${figureSet.tick} Created global config: ${globalConfigPath()}`);
79
85
  const envKey = PROVIDERS[provider].envKey;
80
86
  writeEnvKey(envKey, apiKey || '');
81
- log(`Stored ${envKey} in ${globalEnvPath()}`);
87
+ tag('success').log(`${figureSet.tick} Stored ${envKey} in ${globalEnvPath()}`);
82
88
  const missing = missingModelRoles(provider);
83
89
  if (missing.length) {
84
90
  tag('warning').log(`No recommended ${missing.join(' and ')} for ${provider} — set the model ids in ${globalConfigPath()}`);
85
91
  }
86
92
  if (!apiKey && !process.env[envKey]) {
87
- tag('warning').log(`Add your API key to ${globalEnvPath()}`);
93
+ printApiKeySteps(provider, globalEnvPath());
88
94
  }
89
95
  log('');
90
- log('Explorbot now runs from any directory:');
91
- tag('substep').log(chalk.yellow(`${getCliName()} explore https://your-app.example.com`));
92
- tag('substep').log(chalk.yellow(`${getCliName()} sites`));
96
+ log(`${figureSet.star} What's next:`);
97
+ printInitNextSteps({
98
+ siteCommands: [
99
+ { label: 'Any site', command: `${getCliName()} explore https://your-app.example.com` },
100
+ { label: 'Saved sites', command: `${getCliName()} sites` },
101
+ ],
102
+ });
93
103
  }
94
104
  export function runInitCommand(options) {
95
105
  const provider = options.provider || 'openrouter';
@@ -125,34 +135,38 @@ export function runInitCommand(options) {
125
135
  log('Use --force to overwrite existing file');
126
136
  process.exit(1);
127
137
  }
128
- writeFileSync(outPath, defaultConfigTemplate(provider), 'utf8');
129
- log(`Created config file: ${relativeToCwd(outPath)}`);
138
+ writeFileSync(outPath, defaultConfigTemplate(provider, options.url), 'utf8');
139
+ tag('success').log(`${figureSet.tick} Created config file: ${relativeToCwd(outPath)}`);
130
140
  const envPath = resolve(process.cwd(), '.env');
131
141
  if (!existsSync(envPath)) {
132
142
  writeFileSync(envPath, `${envTemplate(provider)}\n`, 'utf8');
133
- log(`Created env file: ${relativeToCwd(envPath)}`);
143
+ tag('success').log(`${figureSet.tick} Created env file: ${relativeToCwd(envPath)}`);
134
144
  }
135
145
  else {
136
146
  log(`Env file already exists: ${relativeToCwd(envPath)}`);
137
147
  }
148
+ const envKey = PROVIDERS[provider].envKey;
149
+ if (options.apiKey) {
150
+ writeEnvKey(envKey, options.apiKey, envPath);
151
+ tag('success').log(`${figureSet.tick} Stored ${envKey} in ${relativeToCwd(envPath)}`);
152
+ }
153
+ if (!options.apiKey && !process.env[envKey]) {
154
+ printApiKeySteps(provider, relativeToCwd(envPath));
155
+ }
156
+ if (!options.url) {
157
+ tag('warning').log(`Set your application URL in web.url: ${relativeToCwd(outPath)}`);
158
+ }
138
159
  const missing = missingModelRoles(provider);
139
160
  if (missing.length) {
140
161
  tag('warning').log(`No recommended ${missing.join(' and ')} for ${provider} — set the model ids in ${relativeToCwd(outPath)}`);
141
162
  }
142
- log('');
143
- log('Next steps:');
144
- log('1. Configure AI provider in .env');
145
- log('2. Set AI models config file');
146
- log('3. Set web application URL in the config file');
147
- log('4. Add initial knowledge (how to authorize to the application, etc.)');
148
- tag('substep').log(chalk.yellow(`${getCliName()} learn '*' 'to authorize use these credentials: admin@example.com / secret123'`));
149
- tag('substep').log('You can use ${env.LOGIN} and ${env.PASSWORD} to reference environment variables.');
150
- log('5. Launch application on a relative URL');
151
- tag('substep').log(chalk.yellow(`${getCliName()} start /dashboard`));
152
163
  if (!existsSync('./output')) {
153
164
  mkdirSync('./output', { recursive: true });
154
- log('Created directory: output');
165
+ tag('success').log(`${figureSet.tick} Created directory: output`);
155
166
  }
167
+ log('');
168
+ log(`${figureSet.star} What's next:`);
169
+ printInitNextSteps({ url: options.url });
156
170
  }
157
171
  catch (error) {
158
172
  log('Failed to create config file:', error);
@@ -198,12 +212,12 @@ async function renderInitWizard(mode) {
198
212
  }), { exitOnCtrlC: false, patchConsole: false });
199
213
  });
200
214
  }
201
- async function renderLocalProviderWizard() {
215
+ async function renderLocalSetupWizard() {
202
216
  const [{ render }, React, InitWizard] = await Promise.all([import('ink'), import('react'), import('../components/InitWizard.js').then((m) => m.default)]);
203
217
  return new Promise((resolve) => {
204
- const finish = (provider) => {
218
+ const finish = (setup) => {
205
219
  unmount();
206
- resolve(provider);
220
+ resolve(setup);
207
221
  };
208
222
  const { unmount } = render(React.createElement(InitWizard, {
209
223
  mode: 'local',
@@ -211,7 +225,7 @@ async function renderLocalProviderWizard() {
211
225
  onLocal: () => finish(null),
212
226
  onComplete: () => finish(null),
213
227
  onCancel: () => finish(null),
214
- onLocalProvider: (provider) => finish(provider),
228
+ onLocalSetup: (setup) => finish(setup),
215
229
  }), { exitOnCtrlC: false, patchConsole: false });
216
230
  });
217
231
  }
@@ -238,7 +252,7 @@ function globalConfigTemplate(provider) {
238
252
  // Models are written as 'provider/model-id' so they resolve without a local node_modules.
239
253
  // The key is read from ${envKey} in ~/.explorbot/.env
240
254
  // Model ids are snapshotted from the recommendations of this Explorbot version.
241
- // https://github.com/testomatio/explorbot/blob/main/docs/basics/providers.md
255
+ // https://testomat.ai/docs/explorbot
242
256
  const config = {
243
257
  ai: {
244
258
  ${modelLines(provider)}
@@ -255,8 +269,39 @@ ${modelLines(provider)}
255
269
  export default config;
256
270
  `;
257
271
  }
258
- function writeEnvKey(key, value) {
259
- const envPath = globalEnvPath();
272
+ function printInitNextSteps(options) {
273
+ const cli = getCliName();
274
+ const sections = [
275
+ {
276
+ label: 'Try the demo on a sample app',
277
+ commands: [{ command: `${cli} explore https://todomvc.com/examples/react/dist/` }],
278
+ },
279
+ ];
280
+ if (options.url) {
281
+ sections.push({
282
+ label: 'Explore your app',
283
+ commands: [
284
+ { label: 'Whole site', command: `${cli} explore ${options.url}` },
285
+ { label: 'One page', command: `${cli} explore /dashboard` },
286
+ ],
287
+ });
288
+ }
289
+ if (options.siteCommands)
290
+ sections.push({ label: 'Explore any site', commands: options.siteCommands });
291
+ sections.push({
292
+ label: 'Run it from VS Code',
293
+ commands: [{ command: 'https://testomat.ai/explorbot' }],
294
+ }, {
295
+ label: 'Docs',
296
+ commands: [{ command: 'https://testomat.ai/docs/explorbot' }],
297
+ });
298
+ printNextSteps(sections);
299
+ log('');
300
+ log('Add knowledge (logins, credentials) — created on first use in ./knowledge:');
301
+ tag('substep').log(chalk.yellow(`${cli} learn '*' 'to authorize use these credentials: admin@example.com / secret123'`));
302
+ tag('substep').log('You can use ${env.LOGIN} and ${env.PASSWORD} to reference environment variables.');
303
+ }
304
+ function writeEnvKey(key, value, envPath = globalEnvPath()) {
260
305
  let content = '# AI provider API keys';
261
306
  if (existsSync(envPath))
262
307
  content = readFileSync(envPath, 'utf8').trimEnd();
@@ -268,3 +313,9 @@ function writeEnvKey(key, value) {
268
313
  lines[index] = `${key}=${value}`;
269
314
  writeFileSync(envPath, `${lines.join('\n').trimEnd()}\n`, 'utf8');
270
315
  }
316
+ function printApiKeySteps(provider, envPath) {
317
+ const { envKey, keyUrl } = PROVIDERS[provider];
318
+ tag('warning').log(`No API key yet — explorbot needs one to connect to ${provider}`);
319
+ tag('substep').log(`1. Create a key at ${keyUrl}`);
320
+ tag('substep').log(`2. Paste it into ${envPath}: ${envKey}=<your-key>`);
321
+ }
@@ -5,7 +5,11 @@ interface InitWizardProps {
5
5
  onLocal: () => void;
6
6
  onComplete: () => void;
7
7
  onCancel: () => void;
8
- onLocalProvider?: (provider: string) => void;
8
+ onLocalSetup?: (setup: {
9
+ provider: string;
10
+ apiKey: string;
11
+ url: string;
12
+ }) => void;
9
13
  }
10
14
  declare const InitWizard: React.FC<InitWizardProps>;
11
15
  export default InitWizard;
@@ -7,16 +7,23 @@ import { ConfigParser, PROVIDERS, createModel } from '../config.js';
7
7
  import { globalDir } from '../global-config.js';
8
8
  import InputReadline from './InputReadline.js';
9
9
  const PROVIDER_NAMES = Object.keys(PROVIDERS);
10
- const InitWizard = ({ mode, globalConfigExists, onLocal, onComplete, onCancel, onLocalProvider }) => {
10
+ const InitWizard = ({ mode, globalConfigExists, onLocal, onComplete, onCancel, onLocalSetup }) => {
11
11
  const [step, setStep] = useState(mode === 'choose' ? 'target' : 'provider');
12
12
  const [targetIndex, setTargetIndex] = useState(0);
13
13
  const [providerIndex, setProviderIndex] = useState(0);
14
14
  const [apiKey, setApiKey] = useState('');
15
+ const [url, setUrl] = useState('');
16
+ const [urlError, setUrlError] = useState('');
15
17
  const [status, setStatus] = useState('');
16
18
  const [error, setError] = useState('');
17
19
  const provider = PROVIDER_NAMES[providerIndex];
18
20
  const envKey = PROVIDERS[provider].envKey;
19
21
  const save = () => {
22
+ if (mode === 'local') {
23
+ setStatus('');
24
+ setStep('url');
25
+ return;
26
+ }
20
27
  writeGlobalConfig(provider, apiKey.trim());
21
28
  onComplete();
22
29
  };
@@ -45,7 +52,7 @@ const InitWizard = ({ mode, globalConfigExists, onLocal, onComplete, onCancel, o
45
52
  onCancel();
46
53
  return;
47
54
  }
48
- if (step === 'key')
55
+ if (step === 'key' || step === 'url')
49
56
  return;
50
57
  if (key.escape) {
51
58
  onCancel();
@@ -67,12 +74,8 @@ const InitWizard = ({ mode, globalConfigExists, onLocal, onComplete, onCancel, o
67
74
  setProviderIndex((index) => Math.max(0, index - 1));
68
75
  if (key.downArrow)
69
76
  setProviderIndex((index) => Math.min(PROVIDER_NAMES.length - 1, index + 1));
70
- if (key.return) {
71
- if (mode === 'local')
72
- onLocalProvider?.(provider);
73
- else
74
- setStep('key');
75
- }
77
+ if (key.return)
78
+ setStep('key');
76
79
  return;
77
80
  }
78
81
  if (status)
@@ -109,10 +112,12 @@ const InitWizard = ({ mode, globalConfigExists, onLocal, onComplete, onCancel, o
109
112
  React.createElement(Text, null,
110
113
  "Enter the API key for ",
111
114
  provider,
112
- " ",
115
+ ' ',
113
116
  React.createElement(Text, { dimColor: true },
114
117
  "(stored as ",
115
118
  envKey,
119
+ ", create one at ",
120
+ PROVIDERS[provider].keyUrl,
116
121
  ")")),
117
122
  React.createElement(Box, { borderStyle: "single", borderColor: "blue", paddingX: 1 },
118
123
  React.createElement(InputReadline, { value: apiKey, onChange: setApiKey, onSubmit: () => setStep('validate'), placeholder: process.env[envKey] ? 'leave empty to keep the key from your environment' : 'paste the key', isActive: true, showPrompt: false, mask: true })))),
@@ -121,13 +126,26 @@ const InitWizard = ({ mode, globalConfigExists, onLocal, onComplete, onCancel, o
121
126
  status && React.createElement(Text, { color: "yellow" }, status),
122
127
  error && (React.createElement(Box, { flexDirection: "column" },
123
128
  React.createElement(Text, { color: "red" }, error),
124
- React.createElement(Text, { dimColor: true }, "r: re-enter the key | n: save anyway"))))),
129
+ React.createElement(Text, { dimColor: true }, "r: re-enter the key | n: continue anyway"))))),
130
+ step === 'url' && (React.createElement(Box, { flexDirection: "column" },
131
+ React.createElement(Text, null,
132
+ "What is the URL of the web application to test? ",
133
+ React.createElement(Text, { dimColor: true }, "(written to web.url in the config)")),
134
+ React.createElement(Box, { borderStyle: "single", borderColor: "blue", paddingX: 1 },
135
+ React.createElement(InputReadline, { value: url, onChange: setUrl, onSubmit: () => {
136
+ if (!URL.canParse(url.trim())) {
137
+ setUrlError('Enter a full URL, e.g. http://localhost:3000');
138
+ return;
139
+ }
140
+ onLocalSetup?.({ provider, apiKey: apiKey.trim(), url: url.trim() });
141
+ }, placeholder: "http://localhost:3000", isActive: true, showPrompt: false })),
142
+ urlError && React.createElement(Text, { color: "red" }, urlError))),
125
143
  React.createElement(Box, { marginTop: 1 },
126
144
  React.createElement(Text, { dimColor: true },
127
145
  "Config goes to ",
128
146
  mode === 'local' ? 'the current directory' : globalDir(),
129
147
  " | ",
130
- step === 'key' ? 'Enter: continue' : '↑↓: select | Enter: confirm',
148
+ step === 'key' || step === 'url' ? 'Enter: continue' : '↑↓: select | Enter: confirm',
131
149
  " | Ctrl+C: exit"))));
132
150
  };
133
151
  function defaultModelId(provider) {
@@ -141,6 +141,7 @@ interface AgentsConfig {
141
141
  }
142
142
  interface AIConfig {
143
143
  model: any;
144
+ recommendedModels?: string;
144
145
  apiKey?: string;
145
146
  config?: Record<string, any>;
146
147
  langfuse?: {
@@ -315,6 +316,7 @@ interface ConfiguredModel {
315
316
  }
316
317
  interface ProviderInfo {
317
318
  envKey: string;
319
+ keyUrl: string;
318
320
  load: () => Promise<(modelId: string) => any>;
319
321
  }
320
322
  interface EnvVar {
@@ -18,13 +18,13 @@ import { getCliName } from './utils/cli-name.js';
18
18
  import { deepMerge } from './utils/merge.js';
19
19
  import { log, tag } from './utils/logger.js';
20
20
  export const PROVIDERS = {
21
- openai: { envKey: 'OPENAI_API_KEY', load: async () => (await import('@ai-sdk/openai')).createOpenAI() },
22
- anthropic: { envKey: 'ANTHROPIC_API_KEY', load: async () => (await import('@ai-sdk/anthropic')).createAnthropic() },
23
- google: { envKey: 'GOOGLE_GENERATIVE_AI_API_KEY', load: async () => (await import('@ai-sdk/google')).createGoogleGenerativeAI() },
24
- groq: { envKey: 'GROQ_API_KEY', load: async () => (await import('@ai-sdk/groq')).createGroq() },
25
- mistral: { envKey: 'MISTRAL_API_KEY', load: async () => (await import('@ai-sdk/mistral')).createMistral() },
26
- openrouter: { envKey: 'OPENROUTER_API_KEY', load: async () => (await import('@openrouter/ai-sdk-provider')).createOpenRouter() },
27
- sambanova: { envKey: 'SAMBANOVA_API_KEY', load: async () => (await import('sambanova-ai-provider')).createSambaNova() },
21
+ openai: { envKey: 'OPENAI_API_KEY', keyUrl: 'https://platform.openai.com/api-keys', load: async () => (await import('@ai-sdk/openai')).createOpenAI() },
22
+ anthropic: { envKey: 'ANTHROPIC_API_KEY', keyUrl: 'https://console.anthropic.com/settings/keys', load: async () => (await import('@ai-sdk/anthropic')).createAnthropic() },
23
+ google: { envKey: 'GOOGLE_GENERATIVE_AI_API_KEY', keyUrl: 'https://aistudio.google.com/apikey', load: async () => (await import('@ai-sdk/google')).createGoogleGenerativeAI() },
24
+ groq: { envKey: 'GROQ_API_KEY', keyUrl: 'https://console.groq.com/keys', load: async () => (await import('@ai-sdk/groq')).createGroq() },
25
+ mistral: { envKey: 'MISTRAL_API_KEY', keyUrl: 'https://console.mistral.ai/api-keys', load: async () => (await import('@ai-sdk/mistral')).createMistral() },
26
+ openrouter: { envKey: 'OPENROUTER_API_KEY', keyUrl: 'https://openrouter.ai/keys', load: async () => (await import('@openrouter/ai-sdk-provider')).createOpenRouter() },
27
+ sambanova: { envKey: 'SAMBANOVA_API_KEY', keyUrl: 'https://cloud.sambanova.ai/apis', load: async () => (await import('sambanova-ai-provider')).createSambaNova() },
28
28
  };
29
29
  export const MODEL_ROLES = ['model', 'visionModel', 'agenticModel'];
30
30
  let cachedOutputRoot = null;
@@ -572,11 +572,33 @@ export async function resolveConfigModels(ai) {
572
572
  if (typeof ai[role] === 'string')
573
573
  ai[role] = await resolveModel(ai[role], role);
574
574
  }
575
+ await fillRecommendedModels(ai);
575
576
  for (const agent of Object.values(ai.agents || {})) {
576
577
  if (typeof agent?.model === 'string')
577
578
  agent.model = await resolveModel(agent.model);
578
579
  }
579
580
  }
581
+ async function fillRecommendedModels(ai) {
582
+ const provider = ai.recommendedModels;
583
+ if (!provider)
584
+ return;
585
+ const recommended = ConfigParser.recommendedModels()[provider];
586
+ if (!recommended) {
587
+ throw new Error(`No recommended models for "${provider}". Use a provider with recommendations: ${Object.keys(ConfigParser.recommendedModels()).join(', ')}`);
588
+ }
589
+ for (const role of MODEL_ROLES) {
590
+ if (ai[role])
591
+ continue;
592
+ if (!recommended[role])
593
+ continue;
594
+ ai[role] = await resolveModel(provider, role);
595
+ }
596
+ if (!ai.decisionModel && recommended.decisionModel)
597
+ ai.decisionModel = { provider, model: recommended.decisionModel };
598
+ const missing = missingModelRoles(provider);
599
+ if (missing.length)
600
+ tag('warning').log(`No recommended ${missing.join(' and ')} for ${provider} — set them explicitly as "provider/model-id"`);
601
+ }
580
602
  export function resolveLangfuse(ai) {
581
603
  if (!ai)
582
604
  return;
@@ -15,21 +15,28 @@ You need Node.js 24+ (or Bun), an AI provider key, and a modern terminal — iTe
15
15
 
16
16
  ## 2. Configure
17
17
 
18
- Create the config files:
18
+ Run the setup wizard:
19
19
 
20
20
  ```bash
21
21
  npx explorbot init
22
22
  ```
23
23
 
24
- This writes `explorbot.config.js`, an `.env` file for your keys, and an `output/` folder.
24
+ It asks for your AI provider, its API key, and your app's URL, then writes everything for you: `explorbot.config.js`, an `.env` file with the key stored, and an `output/` folder. No manual file editing.
25
25
 
26
- Open `.env` and add your provider key:
26
+ The generated config keeps models simple — one line picks the recommended set for your provider:
27
27
 
28
- ```bash
29
- OPENROUTER_API_KEY=sk-...
28
+ ```javascript
29
+ export default {
30
+ web: {
31
+ url: 'http://localhost:3000',
32
+ },
33
+ ai: {
34
+ recommendedModels: 'openrouter',
35
+ },
36
+ };
30
37
  ```
31
38
 
32
- Then open `explorbot.config.js` and set your app's base URL — the host only, no path:
39
+ `ai.recommendedModels` fills every model role from the provider's bundled recommendations. To pin a single role instead, write it as `'provider/model-id'` — it overrides the recommendation:
33
40
 
34
41
  ```javascript
35
42
  export default {
@@ -37,10 +44,8 @@ export default {
37
44
  url: 'http://localhost:3000',
38
45
  },
39
46
  ai: {
40
- model: 'openrouter/openai/gpt-oss-20b:nitro',
41
- visionModel: 'openrouter/openai/gpt-5.6-luna',
42
- agenticModel: 'openrouter/anthropic/claude-haiku-5.5',
43
- decisionModel: { provider: 'openrouter', model: 'typesafe/jev-latest' },
47
+ recommendedModels: 'openrouter',
48
+ agenticModel: 'groq/openai/gpt-oss-120b',
44
49
  },
45
50
  };
46
51
  ```
@@ -84,6 +89,14 @@ Explorbot uses three models. Pick each one for speed and cost:
84
89
 
85
90
  Captain and Pilot barely use tokens, so a smarter `agenticModel` improves results for almost no extra cost. OpenRouter is the simplest start — one key, many models. To use OpenAI, Anthropic, Groq, or others, see [Providers](./providers.md). For every config option, see [Configuration](../reference/configuration.md).
86
91
 
92
+ Want to see Explorbot work before pointing it at your app? Run it on the sample app:
93
+
94
+ ```bash
95
+ npx explorbot explore https://todomvc.com/examples/react/dist/
96
+ ```
97
+
98
+ You can also run it from the [VS Code extension](https://testomat.ai/explorbot) — pick a site, start a bot, and watch it test without leaving the editor. Full documentation lives at [testomat.ai/docs/explorbot](https://testomat.ai/docs/explorbot).
99
+
87
100
  ## 3. Tell Explorbot how to log in
88
101
 
89
102
  Most apps need a login. Give Explorbot the credentials once, and it signs in on its own:
@@ -4,6 +4,8 @@ Explorbot connects to AI providers through the [Vercel AI SDK](https://sdk.verce
4
4
 
5
5
  Every provider below is set up the classical way: install its package, import it, build the client. Explorbot bundles some of these packages — for those you can skip the install and name the model as `'provider/model-id'` instead. See [Getting Started](./getting-started.md#2-configure) for that list and the two styles side by side.
6
6
 
7
+ A third, shortest style names just the provider: `ai: { recommendedModels: 'openrouter' }` fills every unset role from that provider's bundled recommendations. An explicit role set alongside it wins, so `recommendedModels` with a pinned `agenticModel` mixes both styles.
8
+
7
9
  > The `export default` config block inside each `<!-- START/END provider -->` marker is generated from [`models.json`](../../models.json). After editing that file, run `bunosh docs:sync`. Everything else — including the import blocks — is hand-written.
8
10
 
9
11
  ## Requirements
@@ -920,7 +920,9 @@ npx explorbot init --config-path ./explorbot.config.js
920
920
  npx explorbot init --force
921
921
  ```
922
922
 
923
- Passing `--config-path` or `--path` means local, and so does running outside a terminal (agents, CI): the chooser is skipped and the project config is written as before.
923
+ The local wizard picks the provider, takes the API key (optionally checked with one test AI call), and asks for your app's URL — then writes everything: the key goes into `.env`, the URL into `web.url`, and the models come from `ai.recommendedModels: '<provider>'`. Nothing is left to edit by hand.
924
+
925
+ Passing `--config-path` or `--path` means local, and so does running outside a terminal (agents, CI): the chooser is skipped and the project config is written with placeholder values instead. Init then prints the exact paths of the files that still need attention, a demo command to try Explorbot on a sample app (`npx explorbot explore https://todomvc.com/examples/react/dist/`), and links to the [VS Code extension](https://testomat.ai/explorbot) and the [docs](https://testomat.ai/docs/explorbot).
924
926
 
925
927
  `--global` runs the global wizard instead — pick a provider, paste the API key, optionally check it with one test AI call. The wizard writes `~/.explorbot/config.js` with the recommended model ids of this Explorbot version and stores the key in `~/.explorbot/.env`.
926
928
 
@@ -939,7 +941,8 @@ The global config holds models and keys, never a site: every command names the s
939
941
  | `-p, --path <path>` | Working directory for initialization |
940
942
  | `-g, --global` | Configure `~/.explorbot` to run from anywhere |
941
943
  | `--provider <name>` | AI provider for the global config, skips the wizard |
942
- | `--api-key <key>` | API key stored in `~/.explorbot/.env` |
944
+ | `--api-key <key>` | API key stored in `~/.explorbot/.env` (local init: written to the project `.env`) |
945
+ | `--url <url>` | Web application URL written to `web.url` in the local config |
943
946
 
944
947
  See [Configuration](configuration.md#running-from-anywhere-the-global-installation) for the directory layout and how a site is resolved.
945
948
 
@@ -536,6 +536,7 @@ export default {
536
536
  // AI provider settings
537
537
  ai: {
538
538
  model: groq('openai/gpt-oss-20b'), // Default model instance (required)
539
+ recommendedModels: 'openrouter', // Fills every unset role from this provider's bundled recommendations; explicit roles win
539
540
  visionModel: groq('meta-llama/llama-4-scout-17b-16e-instruct'), // Model for screenshot analysis; setting it enables vision features
540
541
  decisionModel: { provider: 'openrouter', model: 'typesafe/jev-1.13' }, // Optional; see providers docs
541
542
  config: {}, // Additional provider config
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "explorbot",
3
- "version": "0.5.7",
3
+ "version": "0.5.8",
4
4
  "description": "CLI app built with React Ink, CodeceptJS, and Playwright",
5
5
  "license": "Elastic-2.0",
6
6
  "type": "module",
@@ -583,6 +583,14 @@ export class ExploreCommand extends BaseCommand {
583
583
  });
584
584
  }
585
585
 
586
+ sections.push({
587
+ label: 'Explore more',
588
+ commands: [
589
+ { label: 'Whole site', command: `${cli} explore /` },
590
+ { label: 'Docs', command: 'https://testomat.ai/docs/explorbot' },
591
+ ],
592
+ });
593
+
586
594
  printNextSteps(sections);
587
595
  }
588
596
 
@@ -1,25 +1,30 @@
1
1
  import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, extname, join, resolve } from 'node:path';
3
3
  import chalk from 'chalk';
4
+ import figureSet from 'figures';
4
5
  import { ConfigParser, type ModelRole, PROVIDERS, missingModelRoles } from '../config.ts';
5
6
  import { findGlobalConfig, globalConfigPath, globalDir, globalEnvPath } from '../global-config.ts';
6
7
  import { getCliName } from '../utils/cli-name.ts';
7
8
  import { log, tag } from '../utils/logger.js';
8
- import { relativeToCwd } from '../utils/next-steps.ts';
9
+ import { type NextStepCommand, type NextStepSection, printNextSteps, relativeToCwd } from '../utils/next-steps.ts';
9
10
 
10
- function defaultConfigTemplate(provider: string): string {
11
+ function defaultConfigTemplate(provider: string, url?: string): string {
12
+ const appUrl = url || 'http://<your-app-host-here>';
11
13
  return `// 'provider/model-id' uses a bundled provider.
12
14
  // It is also possible to import provider as a module from Vercel AI SDK.
13
- // https://github.com/testomatio/explorbot/blob/main/docs/basics/providers.md
15
+ // https://testomat.ai/docs/explorbot
14
16
 
15
17
  const config = {
16
18
  web: {
17
19
  // use application host without path prefix (e.g., http://localhost:3000)
18
- url: 'http://<your-app-host-here>',
20
+ url: '${appUrl}',
19
21
  },
20
22
 
21
23
  ai: {
22
- ${modelLines(provider)}
24
+ // every role (model, visionModel, agenticModel) takes the recommended ${provider} models bundled with this version
25
+ // override a single role as 'provider/model-id':
26
+ // model: '${provider}/<model-id>',
27
+ recommendedModels: '${provider}',
23
28
  },
24
29
 
25
30
  reporter: {
@@ -37,8 +42,9 @@ export default config;
37
42
  }
38
43
 
39
44
  export function envTemplate(provider: string): string {
45
+ const { keyUrl } = PROVIDERS[provider];
40
46
  const keyLines = Object.entries(PROVIDERS).map(([name, { envKey }]) => {
41
- if (name === provider) return `${envKey}=`;
47
+ if (name === provider) return `# Create a key at ${keyUrl} and paste it after '='\n${envKey}=`;
42
48
  return `# ${envKey}=`;
43
49
  });
44
50
 
@@ -69,10 +75,10 @@ export async function runInit(options: InitCommandOptions): Promise<void> {
69
75
  const choice = await renderInitWizard('choose');
70
76
  if (choice !== 'local') return;
71
77
 
72
- const provider = await renderLocalProviderWizard();
73
- if (!provider) return;
78
+ const setup = await renderLocalSetupWizard();
79
+ if (!setup) return;
74
80
 
75
- runInitCommand({ ...options, provider });
81
+ runInitCommand({ ...options, ...setup });
76
82
  }
77
83
 
78
84
  export function writeGlobalConfig(provider: string, apiKey?: string): void {
@@ -82,24 +88,28 @@ export function writeGlobalConfig(provider: string, apiKey?: string): void {
82
88
 
83
89
  mkdirSync(globalDir(), { recursive: true });
84
90
  writeFileSync(globalConfigPath(), globalConfigTemplate(provider), 'utf8');
85
- log(`Created global config: ${globalConfigPath()}`);
91
+ tag('success').log(`${figureSet.tick} Created global config: ${globalConfigPath()}`);
86
92
 
87
93
  const envKey = PROVIDERS[provider].envKey;
88
94
  writeEnvKey(envKey, apiKey || '');
89
- log(`Stored ${envKey} in ${globalEnvPath()}`);
95
+ tag('success').log(`${figureSet.tick} Stored ${envKey} in ${globalEnvPath()}`);
90
96
 
91
97
  const missing = missingModelRoles(provider);
92
98
  if (missing.length) {
93
99
  tag('warning').log(`No recommended ${missing.join(' and ')} for ${provider} — set the model ids in ${globalConfigPath()}`);
94
100
  }
95
101
  if (!apiKey && !process.env[envKey]) {
96
- tag('warning').log(`Add your API key to ${globalEnvPath()}`);
102
+ printApiKeySteps(provider, globalEnvPath());
97
103
  }
98
104
 
99
105
  log('');
100
- log('Explorbot now runs from any directory:');
101
- tag('substep').log(chalk.yellow(`${getCliName()} explore https://your-app.example.com`));
102
- tag('substep').log(chalk.yellow(`${getCliName()} sites`));
106
+ log(`${figureSet.star} What's next:`);
107
+ printInitNextSteps({
108
+ siteCommands: [
109
+ { label: 'Any site', command: `${getCliName()} explore https://your-app.example.com` },
110
+ { label: 'Saved sites', command: `${getCliName()} sites` },
111
+ ],
112
+ });
103
113
  }
104
114
 
105
115
  export function runInitCommand(options: InitCommandOptions): void {
@@ -141,38 +151,42 @@ export function runInitCommand(options: InitCommandOptions): void {
141
151
  process.exit(1);
142
152
  }
143
153
 
144
- writeFileSync(outPath, defaultConfigTemplate(provider), 'utf8');
145
- log(`Created config file: ${relativeToCwd(outPath)}`);
154
+ writeFileSync(outPath, defaultConfigTemplate(provider, options.url), 'utf8');
155
+ tag('success').log(`${figureSet.tick} Created config file: ${relativeToCwd(outPath)}`);
146
156
 
147
157
  const envPath = resolve(process.cwd(), '.env');
148
158
  if (!existsSync(envPath)) {
149
159
  writeFileSync(envPath, `${envTemplate(provider)}\n`, 'utf8');
150
- log(`Created env file: ${relativeToCwd(envPath)}`);
160
+ tag('success').log(`${figureSet.tick} Created env file: ${relativeToCwd(envPath)}`);
151
161
  } else {
152
162
  log(`Env file already exists: ${relativeToCwd(envPath)}`);
153
163
  }
154
164
 
165
+ const envKey = PROVIDERS[provider].envKey;
166
+ if (options.apiKey) {
167
+ writeEnvKey(envKey, options.apiKey, envPath);
168
+ tag('success').log(`${figureSet.tick} Stored ${envKey} in ${relativeToCwd(envPath)}`);
169
+ }
170
+ if (!options.apiKey && !process.env[envKey]) {
171
+ printApiKeySteps(provider, relativeToCwd(envPath));
172
+ }
173
+ if (!options.url) {
174
+ tag('warning').log(`Set your application URL in web.url: ${relativeToCwd(outPath)}`);
175
+ }
176
+
155
177
  const missing = missingModelRoles(provider);
156
178
  if (missing.length) {
157
179
  tag('warning').log(`No recommended ${missing.join(' and ')} for ${provider} — set the model ids in ${relativeToCwd(outPath)}`);
158
180
  }
159
181
 
160
- log('');
161
- log('Next steps:');
162
- log('1. Configure AI provider in .env');
163
- log('2. Set AI models config file');
164
- log('3. Set web application URL in the config file');
165
- log('4. Add initial knowledge (how to authorize to the application, etc.)');
166
- tag('substep').log(chalk.yellow(`${getCliName()} learn '*' 'to authorize use these credentials: admin@example.com / secret123'`));
167
- tag('substep').log('You can use ${env.LOGIN} and ${env.PASSWORD} to reference environment variables.');
168
-
169
- log('5. Launch application on a relative URL');
170
- tag('substep').log(chalk.yellow(`${getCliName()} start /dashboard`));
171
-
172
182
  if (!existsSync('./output')) {
173
183
  mkdirSync('./output', { recursive: true });
174
- log('Created directory: output');
184
+ tag('success').log(`${figureSet.tick} Created directory: output`);
175
185
  }
186
+
187
+ log('');
188
+ log(`${figureSet.star} What's next:`);
189
+ printInitNextSteps({ url: options.url });
176
190
  } catch (error) {
177
191
  log('Failed to create config file:', error);
178
192
  process.exit(1);
@@ -227,13 +241,13 @@ async function renderInitWizard(mode: 'choose' | 'global'): Promise<'local' | 'g
227
241
  });
228
242
  }
229
243
 
230
- async function renderLocalProviderWizard(): Promise<string | null> {
244
+ async function renderLocalSetupWizard(): Promise<LocalSetup | null> {
231
245
  const [{ render }, React, InitWizard] = await Promise.all([import('ink'), import('react'), import('../components/InitWizard.js').then((m) => m.default)]);
232
246
 
233
247
  return new Promise((resolve) => {
234
- const finish = (provider: string | null) => {
248
+ const finish = (setup: LocalSetup | null) => {
235
249
  unmount();
236
- resolve(provider);
250
+ resolve(setup);
237
251
  };
238
252
  const { unmount } = render(
239
253
  React.createElement(InitWizard, {
@@ -242,7 +256,7 @@ async function renderLocalProviderWizard(): Promise<string | null> {
242
256
  onLocal: () => finish(null),
243
257
  onComplete: () => finish(null),
244
258
  onCancel: () => finish(null),
245
- onLocalProvider: (provider: string) => finish(provider),
259
+ onLocalSetup: (setup) => finish(setup),
246
260
  }),
247
261
  { exitOnCtrlC: false, patchConsole: false }
248
262
  );
@@ -274,7 +288,7 @@ function globalConfigTemplate(provider: string): string {
274
288
  // Models are written as 'provider/model-id' so they resolve without a local node_modules.
275
289
  // The key is read from ${envKey} in ~/.explorbot/.env
276
290
  // Model ids are snapshotted from the recommendations of this Explorbot version.
277
- // https://github.com/testomatio/explorbot/blob/main/docs/basics/providers.md
291
+ // https://testomat.ai/docs/explorbot
278
292
  const config = {
279
293
  ai: {
280
294
  ${modelLines(provider)}
@@ -292,8 +306,47 @@ export default config;
292
306
  `;
293
307
  }
294
308
 
295
- function writeEnvKey(key: string, value: string): void {
296
- const envPath = globalEnvPath();
309
+ function printInitNextSteps(options: { url?: string; siteCommands?: NextStepCommand[] }): void {
310
+ const cli = getCliName();
311
+ const sections: NextStepSection[] = [
312
+ {
313
+ label: 'Try the demo on a sample app',
314
+ commands: [{ command: `${cli} explore https://todomvc.com/examples/react/dist/` }],
315
+ },
316
+ ];
317
+
318
+ if (options.url) {
319
+ sections.push({
320
+ label: 'Explore your app',
321
+ commands: [
322
+ { label: 'Whole site', command: `${cli} explore ${options.url}` },
323
+ { label: 'One page', command: `${cli} explore /dashboard` },
324
+ ],
325
+ });
326
+ }
327
+
328
+ if (options.siteCommands) sections.push({ label: 'Explore any site', commands: options.siteCommands });
329
+
330
+ sections.push(
331
+ {
332
+ label: 'Run it from VS Code',
333
+ commands: [{ command: 'https://testomat.ai/explorbot' }],
334
+ },
335
+ {
336
+ label: 'Docs',
337
+ commands: [{ command: 'https://testomat.ai/docs/explorbot' }],
338
+ }
339
+ );
340
+
341
+ printNextSteps(sections);
342
+
343
+ log('');
344
+ log('Add knowledge (logins, credentials) — created on first use in ./knowledge:');
345
+ tag('substep').log(chalk.yellow(`${cli} learn '*' 'to authorize use these credentials: admin@example.com / secret123'`));
346
+ tag('substep').log('You can use ${env.LOGIN} and ${env.PASSWORD} to reference environment variables.');
347
+ }
348
+
349
+ function writeEnvKey(key: string, value: string, envPath = globalEnvPath()): void {
297
350
  let content = '# AI provider API keys';
298
351
  if (existsSync(envPath)) content = readFileSync(envPath, 'utf8').trimEnd();
299
352
 
@@ -306,6 +359,19 @@ function writeEnvKey(key: string, value: string): void {
306
359
  writeFileSync(envPath, `${lines.join('\n').trimEnd()}\n`, 'utf8');
307
360
  }
308
361
 
362
+ function printApiKeySteps(provider: string, envPath: string): void {
363
+ const { envKey, keyUrl } = PROVIDERS[provider];
364
+ tag('warning').log(`No API key yet — explorbot needs one to connect to ${provider}`);
365
+ tag('substep').log(`1. Create a key at ${keyUrl}`);
366
+ tag('substep').log(`2. Paste it into ${envPath}: ${envKey}=<your-key>`);
367
+ }
368
+
369
+ type LocalSetup = {
370
+ provider: string;
371
+ apiKey: string;
372
+ url: string;
373
+ };
374
+
309
375
  type InitCommandOptions = {
310
376
  configPath?: string;
311
377
  force?: boolean;
@@ -313,4 +379,5 @@ type InitCommandOptions = {
313
379
  global?: boolean;
314
380
  provider?: string;
315
381
  apiKey?: string;
382
+ url?: string;
316
383
  };
@@ -15,14 +15,16 @@ interface InitWizardProps {
15
15
  onLocal: () => void;
16
16
  onComplete: () => void;
17
17
  onCancel: () => void;
18
- onLocalProvider?: (provider: string) => void;
18
+ onLocalSetup?: (setup: { provider: string; apiKey: string; url: string }) => void;
19
19
  }
20
20
 
21
- const InitWizard: React.FC<InitWizardProps> = ({ mode, globalConfigExists, onLocal, onComplete, onCancel, onLocalProvider }) => {
22
- const [step, setStep] = useState<'target' | 'provider' | 'key' | 'validate'>(mode === 'choose' ? 'target' : 'provider');
21
+ const InitWizard: React.FC<InitWizardProps> = ({ mode, globalConfigExists, onLocal, onComplete, onCancel, onLocalSetup }) => {
22
+ const [step, setStep] = useState<'target' | 'provider' | 'key' | 'validate' | 'url'>(mode === 'choose' ? 'target' : 'provider');
23
23
  const [targetIndex, setTargetIndex] = useState(0);
24
24
  const [providerIndex, setProviderIndex] = useState(0);
25
25
  const [apiKey, setApiKey] = useState('');
26
+ const [url, setUrl] = useState('');
27
+ const [urlError, setUrlError] = useState('');
26
28
  const [status, setStatus] = useState('');
27
29
  const [error, setError] = useState('');
28
30
 
@@ -30,6 +32,11 @@ const InitWizard: React.FC<InitWizardProps> = ({ mode, globalConfigExists, onLoc
30
32
  const envKey = PROVIDERS[provider].envKey;
31
33
 
32
34
  const save = () => {
35
+ if (mode === 'local') {
36
+ setStatus('');
37
+ setStep('url');
38
+ return;
39
+ }
33
40
  writeGlobalConfig(provider, apiKey.trim());
34
41
  onComplete();
35
42
  };
@@ -61,7 +68,7 @@ const InitWizard: React.FC<InitWizardProps> = ({ mode, globalConfigExists, onLoc
61
68
  return;
62
69
  }
63
70
 
64
- if (step === 'key') return;
71
+ if (step === 'key' || step === 'url') return;
65
72
 
66
73
  if (key.escape) {
67
74
  onCancel();
@@ -79,10 +86,7 @@ const InitWizard: React.FC<InitWizardProps> = ({ mode, globalConfigExists, onLoc
79
86
  if (step === 'provider') {
80
87
  if (key.upArrow) setProviderIndex((index) => Math.max(0, index - 1));
81
88
  if (key.downArrow) setProviderIndex((index) => Math.min(PROVIDER_NAMES.length - 1, index + 1));
82
- if (key.return) {
83
- if (mode === 'local') onLocalProvider?.(provider);
84
- else setStep('key');
85
- }
89
+ if (key.return) setStep('key');
86
90
  return;
87
91
  }
88
92
 
@@ -132,7 +136,10 @@ const InitWizard: React.FC<InitWizardProps> = ({ mode, globalConfigExists, onLoc
132
136
  {step === 'key' && (
133
137
  <Box flexDirection="column">
134
138
  <Text>
135
- Enter the API key for {provider} <Text dimColor>(stored as {envKey})</Text>
139
+ Enter the API key for {provider}{' '}
140
+ <Text dimColor>
141
+ (stored as {envKey}, create one at {PROVIDERS[provider].keyUrl})
142
+ </Text>
136
143
  </Text>
137
144
  <Box borderStyle="single" borderColor="blue" paddingX={1}>
138
145
  <InputReadline value={apiKey} onChange={setApiKey} onSubmit={() => setStep('validate')} placeholder={process.env[envKey] ? 'leave empty to keep the key from your environment' : 'paste the key'} isActive showPrompt={false} mask />
@@ -147,15 +154,40 @@ const InitWizard: React.FC<InitWizardProps> = ({ mode, globalConfigExists, onLoc
147
154
  {error && (
148
155
  <Box flexDirection="column">
149
156
  <Text color="red">{error}</Text>
150
- <Text dimColor>r: re-enter the key | n: save anyway</Text>
157
+ <Text dimColor>r: re-enter the key | n: continue anyway</Text>
151
158
  </Box>
152
159
  )}
153
160
  </Box>
154
161
  )}
155
162
 
163
+ {step === 'url' && (
164
+ <Box flexDirection="column">
165
+ <Text>
166
+ What is the URL of the web application to test? <Text dimColor>(written to web.url in the config)</Text>
167
+ </Text>
168
+ <Box borderStyle="single" borderColor="blue" paddingX={1}>
169
+ <InputReadline
170
+ value={url}
171
+ onChange={setUrl}
172
+ onSubmit={() => {
173
+ if (!URL.canParse(url.trim())) {
174
+ setUrlError('Enter a full URL, e.g. http://localhost:3000');
175
+ return;
176
+ }
177
+ onLocalSetup?.({ provider, apiKey: apiKey.trim(), url: url.trim() });
178
+ }}
179
+ placeholder="http://localhost:3000"
180
+ isActive
181
+ showPrompt={false}
182
+ />
183
+ </Box>
184
+ {urlError && <Text color="red">{urlError}</Text>}
185
+ </Box>
186
+ )}
187
+
156
188
  <Box marginTop={1}>
157
189
  <Text dimColor>
158
- Config goes to {mode === 'local' ? 'the current directory' : globalDir()} | {step === 'key' ? 'Enter: continue' : '↑↓: select | Enter: confirm'} | Ctrl+C: exit
190
+ Config goes to {mode === 'local' ? 'the current directory' : globalDir()} | {step === 'key' || step === 'url' ? 'Enter: continue' : '↑↓: select | Enter: confirm'} | Ctrl+C: exit
159
191
  </Text>
160
192
  </Box>
161
193
  </Box>
package/src/config.ts CHANGED
@@ -11,13 +11,13 @@ import { deepMerge } from './utils/merge.js';
11
11
  import { log, tag } from './utils/logger.js';
12
12
 
13
13
  export const PROVIDERS: Record<string, ProviderInfo> = {
14
- openai: { envKey: 'OPENAI_API_KEY', load: async () => (await import('@ai-sdk/openai')).createOpenAI() },
15
- anthropic: { envKey: 'ANTHROPIC_API_KEY', load: async () => (await import('@ai-sdk/anthropic')).createAnthropic() },
16
- google: { envKey: 'GOOGLE_GENERATIVE_AI_API_KEY', load: async () => (await import('@ai-sdk/google')).createGoogleGenerativeAI() },
17
- groq: { envKey: 'GROQ_API_KEY', load: async () => (await import('@ai-sdk/groq')).createGroq() },
18
- mistral: { envKey: 'MISTRAL_API_KEY', load: async () => (await import('@ai-sdk/mistral')).createMistral() },
19
- openrouter: { envKey: 'OPENROUTER_API_KEY', load: async () => (await import('@openrouter/ai-sdk-provider')).createOpenRouter() },
20
- sambanova: { envKey: 'SAMBANOVA_API_KEY', load: async () => (await import('sambanova-ai-provider')).createSambaNova() },
14
+ openai: { envKey: 'OPENAI_API_KEY', keyUrl: 'https://platform.openai.com/api-keys', load: async () => (await import('@ai-sdk/openai')).createOpenAI() },
15
+ anthropic: { envKey: 'ANTHROPIC_API_KEY', keyUrl: 'https://console.anthropic.com/settings/keys', load: async () => (await import('@ai-sdk/anthropic')).createAnthropic() },
16
+ google: { envKey: 'GOOGLE_GENERATIVE_AI_API_KEY', keyUrl: 'https://aistudio.google.com/apikey', load: async () => (await import('@ai-sdk/google')).createGoogleGenerativeAI() },
17
+ groq: { envKey: 'GROQ_API_KEY', keyUrl: 'https://console.groq.com/keys', load: async () => (await import('@ai-sdk/groq')).createGroq() },
18
+ mistral: { envKey: 'MISTRAL_API_KEY', keyUrl: 'https://console.mistral.ai/api-keys', load: async () => (await import('@ai-sdk/mistral')).createMistral() },
19
+ openrouter: { envKey: 'OPENROUTER_API_KEY', keyUrl: 'https://openrouter.ai/keys', load: async () => (await import('@openrouter/ai-sdk-provider')).createOpenRouter() },
20
+ sambanova: { envKey: 'SAMBANOVA_API_KEY', keyUrl: 'https://cloud.sambanova.ai/apis', load: async () => (await import('sambanova-ai-provider')).createSambaNova() },
21
21
  };
22
22
 
23
23
  export const MODEL_ROLES: ModelRole[] = ['model', 'visionModel', 'agenticModel'];
@@ -169,6 +169,7 @@ interface AgentsConfig {
169
169
 
170
170
  interface AIConfig {
171
171
  model: any;
172
+ recommendedModels?: string;
172
173
  apiKey?: string;
173
174
  config?: Record<string, any>;
174
175
  langfuse?: {
@@ -890,11 +891,34 @@ export async function resolveConfigModels(ai?: AIConfig): Promise<void> {
890
891
  if (typeof ai[role] === 'string') ai[role] = await resolveModel(ai[role], role);
891
892
  }
892
893
 
894
+ await fillRecommendedModels(ai);
895
+
893
896
  for (const agent of Object.values(ai.agents || {})) {
894
897
  if (typeof agent?.model === 'string') agent.model = await resolveModel(agent.model);
895
898
  }
896
899
  }
897
900
 
901
+ async function fillRecommendedModels(ai: AIConfig): Promise<void> {
902
+ const provider = ai.recommendedModels;
903
+ if (!provider) return;
904
+
905
+ const recommended = ConfigParser.recommendedModels()[provider];
906
+ if (!recommended) {
907
+ throw new Error(`No recommended models for "${provider}". Use a provider with recommendations: ${Object.keys(ConfigParser.recommendedModels()).join(', ')}`);
908
+ }
909
+
910
+ for (const role of MODEL_ROLES) {
911
+ if (ai[role]) continue;
912
+ if (!recommended[role]) continue;
913
+ ai[role] = await resolveModel(provider, role);
914
+ }
915
+
916
+ if (!ai.decisionModel && recommended.decisionModel) ai.decisionModel = { provider, model: recommended.decisionModel };
917
+
918
+ const missing = missingModelRoles(provider);
919
+ if (missing.length) tag('warning').log(`No recommended ${missing.join(' and ')} for ${provider} — set them explicitly as "provider/model-id"`);
920
+ }
921
+
898
922
  export function resolveLangfuse(ai?: AIConfig): void {
899
923
  if (!ai) return;
900
924
 
@@ -994,6 +1018,7 @@ interface ConfiguredModel {
994
1018
 
995
1019
  interface ProviderInfo {
996
1020
  envKey: string;
1021
+ keyUrl: string;
997
1022
  load: () => Promise<(modelId: string) => any>;
998
1023
  }
999
1024