explorbot 0.5.7 → 0.5.9
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/bin/explorbot-cli.ts +25 -3
- package/boat/prima/README.md +3 -0
- package/boat/prima/src/cli.ts +7 -1
- package/boat/prima/src/envelope.ts +18 -4
- package/boat/prima/src/help.ts +15 -4
- package/boat/prima/src/prima.ts +181 -25
- package/dist/bin/explorbot-cli.js +31 -3
- package/dist/boat/prima/src/cli.js +6 -1
- package/dist/boat/prima/src/envelope.js +16 -3
- package/dist/boat/prima/src/help.js +14 -4
- package/dist/boat/prima/src/prima.js +191 -27
- package/dist/package.json +1 -1
- package/dist/rules/navigator/verification-actions.md +5 -3
- package/dist/rules/planner/ignored-pages.md +6 -0
- package/dist/rules/quartermaster/ux/accessibility.md +8 -0
- package/dist/rules/quartermaster/ux/charts.md +6 -0
- package/dist/rules/quartermaster/ux/copy.md +9 -0
- package/dist/rules/quartermaster/ux/forms.md +7 -0
- package/dist/rules/quartermaster/ux/interaction.md +7 -0
- package/dist/rules/quartermaster/ux/motion.md +5 -0
- package/dist/rules/quartermaster/ux/navigation.md +7 -0
- package/dist/rules/quartermaster/ux/readability.md +6 -0
- package/dist/rules/quartermaster/ux/responsive.md +7 -0
- package/dist/src/ai/judge.d.ts +3 -1
- package/dist/src/ai/judge.js +13 -3
- package/dist/src/ai/navigator.js +3 -2
- package/dist/src/ai/pilot.js +10 -4
- package/dist/src/ai/planner.js +9 -1
- package/dist/src/ai/provider.d.ts +4 -0
- package/dist/src/ai/provider.js +27 -5
- package/dist/src/ai/quartermaster.d.ts +31 -3
- package/dist/src/ai/quartermaster.js +262 -17
- package/dist/src/ai/tester.js +6 -3
- package/dist/src/commands/explore-command.js +7 -0
- package/dist/src/commands/init-command.d.ts +1 -0
- package/dist/src/commands/init-command.js +87 -36
- package/dist/src/components/InitWizard.d.ts +5 -1
- package/dist/src/components/InitWizard.js +29 -11
- package/dist/src/config.d.ts +2 -0
- package/dist/src/config.js +29 -7
- package/dist/src/explorbot.d.ts +1 -1
- package/dist/src/explorbot.js +8 -13
- package/dist/src/explorer.js +1 -0
- package/dist/src/global-config.js +6 -2
- package/dist/src/utils/ux-probe.d.ts +25 -0
- package/dist/src/utils/ux-probe.js +155 -0
- package/docs/basics/getting-started.md +23 -10
- package/docs/basics/providers.md +2 -0
- package/docs/reference/commands.md +5 -2
- package/docs/reference/configuration.md +1 -0
- package/package.json +1 -1
- package/rules/navigator/verification-actions.md +5 -3
- package/rules/planner/ignored-pages.md +6 -0
- package/rules/quartermaster/ux/accessibility.md +8 -0
- package/rules/quartermaster/ux/charts.md +6 -0
- package/rules/quartermaster/ux/copy.md +9 -0
- package/rules/quartermaster/ux/forms.md +7 -0
- package/rules/quartermaster/ux/interaction.md +7 -0
- package/rules/quartermaster/ux/motion.md +5 -0
- package/rules/quartermaster/ux/navigation.md +7 -0
- package/rules/quartermaster/ux/readability.md +6 -0
- package/rules/quartermaster/ux/responsive.md +7 -0
- package/src/ai/judge.ts +11 -3
- package/src/ai/navigator.ts +2 -2
- package/src/ai/pilot.ts +10 -4
- package/src/ai/planner.ts +10 -1
- package/src/ai/provider.ts +29 -6
- package/src/ai/quartermaster.ts +300 -18
- package/src/ai/tester.ts +5 -3
- package/src/commands/explore-command.ts +8 -0
- package/src/commands/init-command.ts +105 -38
- package/src/components/InitWizard.tsx +43 -11
- package/src/config.ts +32 -7
- package/src/explorbot.ts +9 -13
- package/src/explorer.ts +1 -0
- package/src/global-config.ts +5 -2
- package/src/utils/ux-probe.ts +163 -0
|
@@ -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://
|
|
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: '
|
|
19
|
+
url: '${appUrl}',
|
|
18
20
|
},
|
|
19
21
|
|
|
20
22
|
ai: {
|
|
21
|
-
${
|
|
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
|
|
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
|
|
68
|
-
if (!
|
|
73
|
+
const setup = await renderLocalSetupWizard();
|
|
74
|
+
if (!setup)
|
|
69
75
|
return;
|
|
70
|
-
runInitCommand({ ...options,
|
|
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(
|
|
84
|
+
tag('success').log(`${figureSet.tick} Created global config: ${globalConfigPath()}`);
|
|
79
85
|
const envKey = PROVIDERS[provider].envKey;
|
|
80
86
|
writeEnvKey(envKey, apiKey || '');
|
|
81
|
-
log(
|
|
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
|
-
|
|
93
|
+
printApiKeySteps(provider, globalEnvPath());
|
|
88
94
|
}
|
|
89
95
|
log('');
|
|
90
|
-
log('
|
|
91
|
-
|
|
92
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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 = (
|
|
218
|
+
const finish = (setup) => {
|
|
205
219
|
unmount();
|
|
206
|
-
resolve(
|
|
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
|
-
|
|
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://
|
|
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
|
|
259
|
-
const
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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:
|
|
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) {
|
package/dist/src/config.d.ts
CHANGED
|
@@ -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 {
|
package/dist/src/config.js
CHANGED
|
@@ -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;
|
package/dist/src/explorbot.d.ts
CHANGED
|
@@ -97,7 +97,7 @@ export declare class ExplorBot {
|
|
|
97
97
|
agentTester(): Tester;
|
|
98
98
|
agentCaptain(): Captain;
|
|
99
99
|
agentExperienceCompactor(): ExperienceCompactor;
|
|
100
|
-
agentQuartermaster(): Quartermaster
|
|
100
|
+
agentQuartermaster(): Quartermaster;
|
|
101
101
|
agentHistorian(): Historian;
|
|
102
102
|
agentRerunner(): Rerunner;
|
|
103
103
|
agentDriller(): Driller;
|
package/dist/src/explorbot.js
CHANGED
|
@@ -111,6 +111,7 @@ export class ExplorBot {
|
|
|
111
111
|
if (this.options.optionalAi)
|
|
112
112
|
return this.bootstrapOptionalProvider();
|
|
113
113
|
this.provider = new AIProvider(this.config.ai);
|
|
114
|
+
this.provider.setJudge(this.judge());
|
|
114
115
|
await this.provider.validateConnection();
|
|
115
116
|
}
|
|
116
117
|
aiFailureReason() {
|
|
@@ -217,9 +218,11 @@ export class ExplorBot {
|
|
|
217
218
|
const tools = createAgentTools({ ...deps, researcher, navigator, withExperience: false });
|
|
218
219
|
return new Tester(deps, researcher, navigator, tools);
|
|
219
220
|
});
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
this.
|
|
221
|
+
if (this.config.ai?.agents?.quartermaster?.enabled === true) {
|
|
222
|
+
const quartermaster = this.agentQuartermaster();
|
|
223
|
+
quartermaster.start(this.stateManager());
|
|
224
|
+
this.agents.tester.setQuartermaster(quartermaster);
|
|
225
|
+
}
|
|
223
226
|
if (this.isHistorianEnabled())
|
|
224
227
|
this.agents.tester.setHistorian(this.agentHistorian());
|
|
225
228
|
this.agents.tester.setPilot(this.agentPilot());
|
|
@@ -240,16 +243,7 @@ export class ExplorBot {
|
|
|
240
243
|
return (this.agents.experienceCompactor ||= new ExperienceCompactor(this.provider, this.experienceTracker()));
|
|
241
244
|
}
|
|
242
245
|
agentQuartermaster() {
|
|
243
|
-
|
|
244
|
-
if (config?.enabled !== true)
|
|
245
|
-
return null;
|
|
246
|
-
if (!this.agents.quartermaster) {
|
|
247
|
-
this.agents.quartermaster = new Quartermaster(this.provider, {
|
|
248
|
-
model: config?.model,
|
|
249
|
-
});
|
|
250
|
-
this.agents.quartermaster.start(this.explorer, this.stateManager());
|
|
251
|
-
}
|
|
252
|
-
return this.agents.quartermaster;
|
|
246
|
+
return (this.agents.quartermaster ||= new Quartermaster(this.provider, this.explorer, { model: this.config.ai?.agents?.quartermaster?.model }));
|
|
253
247
|
}
|
|
254
248
|
agentHistorian() {
|
|
255
249
|
return (this.agents.historian ||= this.createAgent(({ ai, explorer, config, stateManager, playwrightRecorder }) => {
|
|
@@ -500,6 +494,7 @@ export class ExplorBot {
|
|
|
500
494
|
async bootstrapOptionalProvider() {
|
|
501
495
|
try {
|
|
502
496
|
const provider = new AIProvider(this.config.ai);
|
|
497
|
+
provider.setJudge(this.judge());
|
|
503
498
|
await provider.validateConnection();
|
|
504
499
|
this.provider = provider;
|
|
505
500
|
}
|
package/dist/src/explorer.js
CHANGED
|
@@ -74,6 +74,7 @@ class Explorer {
|
|
|
74
74
|
throw new Error('Playwright helper not available');
|
|
75
75
|
}
|
|
76
76
|
this.applyBasePathToHelper();
|
|
77
|
+
codeceptjs.store.visibleLocator = this.playwrightHelper.options?.visibleLocator;
|
|
77
78
|
await this.connectOrLaunchBrowser();
|
|
78
79
|
const hasSession = !this.options?.attachedBrowser && this.options?.session && existsSync(this.options.session);
|
|
79
80
|
await this.openContextPage();
|
|
@@ -13,6 +13,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
13
13
|
import dedent from 'dedent';
|
|
14
14
|
const GLOBAL_CONFIG_NAMES = ['config.js', 'config.mjs', 'config.ts'];
|
|
15
15
|
const SITE_DIRS = ['knowledge', 'experience', 'output'];
|
|
16
|
+
const LOOPBACK_HOSTS = ['localhost', '127.0.0.1', '[::1]'];
|
|
16
17
|
export const EXPLORBOT_CONFIG_PATHS = ['explorbot.config.js', 'explorbot.config.mjs', 'explorbot.config.ts'];
|
|
17
18
|
export function globalDir() {
|
|
18
19
|
return join(os.homedir(), '.explorbot');
|
|
@@ -33,7 +34,10 @@ export function sitesDir() {
|
|
|
33
34
|
return join(globalDir(), 'sites');
|
|
34
35
|
}
|
|
35
36
|
export function siteFolderName(url) {
|
|
36
|
-
|
|
37
|
+
const parsed = new URL(url);
|
|
38
|
+
if (LOOPBACK_HOSTS.includes(parsed.hostname))
|
|
39
|
+
parsed.hostname = 'localhost';
|
|
40
|
+
return parsed.host.toLowerCase().replace(/[^a-z0-9._-]/g, '_');
|
|
37
41
|
}
|
|
38
42
|
export function listSites() {
|
|
39
43
|
if (!existsSync(sitesDir()))
|
|
@@ -74,7 +78,7 @@ function validateSiteConfig(config, configPath, baseUrl) {
|
|
|
74
78
|
`);
|
|
75
79
|
}
|
|
76
80
|
const declared = URL.parse(url)?.origin;
|
|
77
|
-
if (declared === baseUrl)
|
|
81
|
+
if (declared && siteFolderName(declared) === siteFolderName(baseUrl))
|
|
78
82
|
return;
|
|
79
83
|
throw new Error(dedent `
|
|
80
84
|
Site config declares a different site.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare const UX_FOCUS_ATTR = "data-explorbot-ux";
|
|
2
|
+
declare const UX_LIMITS: {
|
|
3
|
+
touchTarget: number;
|
|
4
|
+
pointerTarget: number;
|
|
5
|
+
touchWidth: number;
|
|
6
|
+
targetGap: number;
|
|
7
|
+
minFontSize: number;
|
|
8
|
+
minLineHeight: number;
|
|
9
|
+
maxEntries: number;
|
|
10
|
+
minOpacity: 0.1;
|
|
11
|
+
};
|
|
12
|
+
export declare function uxProbeArgs(scope: string): UxProbeArgs;
|
|
13
|
+
export declare function renderUxMeasurements(measurements: UxMeasurements): string;
|
|
14
|
+
export declare function measureUx({ scope, controlsSelector, limits }: UxProbeArgs): UxMeasurements;
|
|
15
|
+
export declare function countRunningAnimations(scope: string): number;
|
|
16
|
+
interface UxProbeArgs {
|
|
17
|
+
scope: string;
|
|
18
|
+
controlsSelector: string;
|
|
19
|
+
limits: typeof UX_LIMITS;
|
|
20
|
+
}
|
|
21
|
+
export interface UxMeasurements {
|
|
22
|
+
width: number;
|
|
23
|
+
checks: Record<string, string[]>;
|
|
24
|
+
}
|
|
25
|
+
export {};
|