browser-use 0.4.0 → 0.6.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/dist/agent/service.js +2 -0
- package/dist/agent/system_prompt.md +269 -0
- package/dist/agent/system_prompt_anthropic_flash.md +240 -0
- package/dist/agent/system_prompt_browser_use.md +18 -0
- package/dist/agent/system_prompt_browser_use_flash.md +15 -0
- package/dist/agent/system_prompt_browser_use_no_thinking.md +17 -0
- package/dist/agent/system_prompt_flash.md +16 -0
- package/dist/agent/system_prompt_flash_anthropic.md +30 -0
- package/dist/agent/system_prompt_no_thinking.md +245 -0
- package/dist/browser/cloud/index.d.ts +1 -0
- package/dist/browser/cloud/index.js +1 -0
- package/dist/browser/cloud/management.d.ts +130 -0
- package/dist/browser/cloud/management.js +140 -0
- package/dist/browser/events.d.ts +61 -3
- package/dist/browser/events.js +66 -0
- package/dist/browser/profile.d.ts +1 -0
- package/dist/browser/profile.js +25 -8
- package/dist/browser/session.d.ts +59 -2
- package/dist/browser/session.js +943 -131
- package/dist/browser/watchdogs/base.js +34 -1
- package/dist/browser/watchdogs/captcha-watchdog.d.ts +26 -0
- package/dist/browser/watchdogs/captcha-watchdog.js +151 -0
- package/dist/browser/watchdogs/index.d.ts +1 -0
- package/dist/browser/watchdogs/index.js +1 -0
- package/dist/browser/watchdogs/screenshot-watchdog.js +4 -3
- package/dist/cli.d.ts +120 -0
- package/dist/cli.js +1816 -4
- package/dist/controller/service.js +106 -362
- package/dist/controller/views.d.ts +9 -6
- package/dist/controller/views.js +8 -5
- package/dist/dom/dom_tree/index.js +24 -11
- package/dist/filesystem/file-system.js +1 -1
- package/dist/llm/litellm/chat.d.ts +11 -0
- package/dist/llm/litellm/chat.js +16 -0
- package/dist/llm/litellm/index.d.ts +1 -0
- package/dist/llm/litellm/index.js +1 -0
- package/dist/llm/models.js +29 -3
- package/dist/llm/oci-raw/chat.d.ts +64 -0
- package/dist/llm/oci-raw/chat.js +350 -0
- package/dist/llm/oci-raw/index.d.ts +2 -0
- package/dist/llm/oci-raw/index.js +2 -0
- package/dist/llm/oci-raw/serializer.d.ts +12 -0
- package/dist/llm/oci-raw/serializer.js +128 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +62 -13
- package/dist/skill-cli/direct.d.ts +100 -0
- package/dist/skill-cli/direct.js +984 -0
- package/dist/skill-cli/index.d.ts +2 -0
- package/dist/skill-cli/index.js +2 -0
- package/dist/skill-cli/server.d.ts +2 -0
- package/dist/skill-cli/server.js +472 -11
- package/dist/skill-cli/tunnel.d.ts +61 -0
- package/dist/skill-cli/tunnel.js +257 -0
- package/dist/sync/auth.d.ts +8 -0
- package/dist/sync/auth.js +12 -0
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +2 -1
- package/package.json +22 -4
package/dist/cli.js
CHANGED
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
import { promises as fs } from 'node:fs';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
5
7
|
import { stdin, stdout } from 'node:process';
|
|
6
8
|
import { createInterface } from 'node:readline/promises';
|
|
7
9
|
import { Agent } from './agent/service.js';
|
|
8
10
|
import { BrowserProfile, } from './browser/profile.js';
|
|
9
|
-
import { BrowserSession } from './browser/session.js';
|
|
11
|
+
import { BrowserSession, systemChrome } from './browser/session.js';
|
|
10
12
|
import { CONFIG } from './config.js';
|
|
13
|
+
import { CloudBrowserClient } from './browser/cloud/cloud.js';
|
|
14
|
+
import { CloudManagementClient, } from './browser/cloud/management.js';
|
|
11
15
|
import { ChatOpenAI } from './llm/openai/chat.js';
|
|
12
16
|
import { ChatAnthropic } from './llm/anthropic/chat.js';
|
|
13
17
|
import { ChatGoogle } from './llm/google/chat.js';
|
|
@@ -17,6 +21,7 @@ import { ChatOpenRouter } from './llm/openrouter/chat.js';
|
|
|
17
21
|
import { ChatAzure } from './llm/azure/chat.js';
|
|
18
22
|
import { ChatOllama } from './llm/ollama/chat.js';
|
|
19
23
|
import { ChatMistral } from './llm/mistral/chat.js';
|
|
24
|
+
import { ChatOCIRaw } from './llm/oci-raw/chat.js';
|
|
20
25
|
import { ChatCerebras } from './llm/cerebras/chat.js';
|
|
21
26
|
import { ChatVercel } from './llm/vercel/chat.js';
|
|
22
27
|
import { ChatAnthropicBedrock } from './llm/aws/chat-anthropic.js';
|
|
@@ -25,8 +30,11 @@ import { ChatBrowserUse } from './llm/browser-use/chat.js';
|
|
|
25
30
|
import { MCPServer } from './mcp/server.js';
|
|
26
31
|
import { get_browser_use_version } from './utils.js';
|
|
27
32
|
import { setupLogging } from './logging-config.js';
|
|
33
|
+
import { get_tunnel_manager } from './skill-cli/tunnel.js';
|
|
34
|
+
import { DeviceAuthClient, save_cloud_api_token } from './sync/auth.js';
|
|
28
35
|
import dotenv from 'dotenv';
|
|
29
36
|
dotenv.config();
|
|
37
|
+
const require = createRequire(import.meta.url);
|
|
30
38
|
const CLI_PROVIDER_ALIASES = {
|
|
31
39
|
openai: 'openai',
|
|
32
40
|
anthropic: 'anthropic',
|
|
@@ -128,6 +136,10 @@ export const parseCliArgs = (argv) => {
|
|
|
128
136
|
provider: null,
|
|
129
137
|
prompt: null,
|
|
130
138
|
mcp: false,
|
|
139
|
+
json: false,
|
|
140
|
+
yes: false,
|
|
141
|
+
setup_mode: null,
|
|
142
|
+
api_key: null,
|
|
131
143
|
positional: [],
|
|
132
144
|
};
|
|
133
145
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -156,6 +168,14 @@ export const parseCliArgs = (argv) => {
|
|
|
156
168
|
parsed.mcp = true;
|
|
157
169
|
continue;
|
|
158
170
|
}
|
|
171
|
+
if (arg === '--json') {
|
|
172
|
+
parsed.json = true;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (arg === '-y' || arg === '--yes') {
|
|
176
|
+
parsed.yes = true;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
159
179
|
if (arg === '-p' || arg === '--prompt' || arg.startsWith('--prompt=')) {
|
|
160
180
|
const { value, nextIndex } = takeOptionValue(arg, i, argv);
|
|
161
181
|
parsed.prompt = value;
|
|
@@ -174,6 +194,18 @@ export const parseCliArgs = (argv) => {
|
|
|
174
194
|
i = nextIndex;
|
|
175
195
|
continue;
|
|
176
196
|
}
|
|
197
|
+
if (arg === '--mode' || arg.startsWith('--mode=')) {
|
|
198
|
+
const { value, nextIndex } = takeOptionValue(arg, i, argv);
|
|
199
|
+
parsed.setup_mode = value.trim();
|
|
200
|
+
i = nextIndex;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (arg === '--api-key' || arg.startsWith('--api-key=')) {
|
|
204
|
+
const { value, nextIndex } = takeOptionValue(arg, i, argv);
|
|
205
|
+
parsed.api_key = value.trim();
|
|
206
|
+
i = nextIndex;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
177
209
|
if (arg === '--window-width' || arg.startsWith('--window-width=')) {
|
|
178
210
|
const { value, nextIndex } = takeOptionValue(arg, i, argv);
|
|
179
211
|
parsed.window_width = parsePositiveInt('--window-width', value);
|
|
@@ -512,7 +544,9 @@ const createLlmForProvider = (provider, model) => {
|
|
|
512
544
|
baseURL: process.env.VERCEL_BASE_URL,
|
|
513
545
|
});
|
|
514
546
|
case 'oci':
|
|
515
|
-
|
|
547
|
+
return new ChatOCIRaw({
|
|
548
|
+
model,
|
|
549
|
+
});
|
|
516
550
|
case 'ollama': {
|
|
517
551
|
const host = process.env.OLLAMA_HOST || 'http://localhost:11434';
|
|
518
552
|
return new ChatOllama(model, host);
|
|
@@ -741,6 +775,14 @@ const runInteractiveMode = async (args, llm) => {
|
|
|
741
775
|
};
|
|
742
776
|
export const getCliUsage = () => `Usage:
|
|
743
777
|
browser-use # interactive mode (TTY)
|
|
778
|
+
browser-use doctor
|
|
779
|
+
browser-use install
|
|
780
|
+
browser-use setup [--mode <local|remote|full>]
|
|
781
|
+
browser-use tunnel <port>
|
|
782
|
+
browser-use task <list|status|stop|logs>
|
|
783
|
+
browser-use session <list|get|stop|create|share>
|
|
784
|
+
browser-use profile <list|get|create|update|delete|cookies|sync>
|
|
785
|
+
browser-use run --remote <task>
|
|
744
786
|
browser-use <task>
|
|
745
787
|
browser-use -p "<task>"
|
|
746
788
|
browser-use [options] <task>
|
|
@@ -750,9 +792,13 @@ Options:
|
|
|
750
792
|
-h, --help Show this help message
|
|
751
793
|
--version Print version and exit
|
|
752
794
|
--mcp Run as MCP server
|
|
795
|
+
--json Output command results as JSON when supported
|
|
796
|
+
-y, --yes Skip optional setup prompts where supported
|
|
753
797
|
--provider <name> Force provider (openai|anthropic|google|deepseek|groq|openrouter|azure|mistral|cerebras|vercel|oci|ollama|browser-use|aws|aws-anthropic)
|
|
754
798
|
--model <model> Set model (e.g., gpt-5-mini, claude-4-sonnet, gemini-2.5-pro)
|
|
755
799
|
-p, --prompt <task> Run a single task
|
|
800
|
+
--mode <name> Setup mode for setup command (local|remote|full)
|
|
801
|
+
--api-key <value> Browser Use API key for setup or cloud operations
|
|
756
802
|
--headless Run browser in headless mode
|
|
757
803
|
--allowed-domains <items> Comma-separated allowlist (e.g., example.com,*.example.org)
|
|
758
804
|
--window-width <px> Browser window width
|
|
@@ -765,6 +811,1696 @@ Options:
|
|
|
765
811
|
--proxy-password <value> Proxy password
|
|
766
812
|
--cdp-url <url> Connect to an existing Chromium instance via CDP
|
|
767
813
|
--debug Enable debug logging`;
|
|
814
|
+
const resolvePlaywrightCliPath = () => require.resolve('playwright/cli');
|
|
815
|
+
export const runInstallCommand = (options = {}) => {
|
|
816
|
+
const playwrightCliPath = options.playwright_cli_path ?? resolvePlaywrightCliPath();
|
|
817
|
+
const spawnImpl = options.spawn_impl ?? spawnSync;
|
|
818
|
+
const result = spawnImpl(process.execPath, [playwrightCliPath, 'install', 'chromium'], {
|
|
819
|
+
stdio: 'inherit',
|
|
820
|
+
});
|
|
821
|
+
if (result.error) {
|
|
822
|
+
throw result.error;
|
|
823
|
+
}
|
|
824
|
+
if (result.status !== 0) {
|
|
825
|
+
throw new Error(`Playwright browser install failed with exit code ${result.status ?? 1}`);
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
const writeLine = (stream, value) => {
|
|
829
|
+
stream.write(`${value}\n`);
|
|
830
|
+
};
|
|
831
|
+
const parseTunnelPort = (value) => {
|
|
832
|
+
const port = Number.parseInt(String(value ?? ''), 10);
|
|
833
|
+
if (!Number.isFinite(port) || port <= 0) {
|
|
834
|
+
throw new Error(`Invalid port: ${value ?? ''}`);
|
|
835
|
+
}
|
|
836
|
+
return port;
|
|
837
|
+
};
|
|
838
|
+
export const runTunnelCommand = async (argv, options = {}) => {
|
|
839
|
+
const manager = options.manager ?? get_tunnel_manager();
|
|
840
|
+
const output = options.stdout ?? process.stdout;
|
|
841
|
+
const errorOutput = options.stderr ?? process.stderr;
|
|
842
|
+
const json_output = Boolean(options.json_output);
|
|
843
|
+
const render = (value) => {
|
|
844
|
+
if (json_output) {
|
|
845
|
+
writeLine(output, JSON.stringify(value, null, 2));
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
try {
|
|
849
|
+
const first = argv[0] ?? null;
|
|
850
|
+
if (!first) {
|
|
851
|
+
writeLine(errorOutput, 'Usage: browser-use tunnel <port> | list | stop <port> | stop --all');
|
|
852
|
+
return 1;
|
|
853
|
+
}
|
|
854
|
+
if (first === 'list') {
|
|
855
|
+
const result = manager.list_tunnels();
|
|
856
|
+
if (json_output) {
|
|
857
|
+
render(result);
|
|
858
|
+
}
|
|
859
|
+
else if (result.tunnels.length > 0) {
|
|
860
|
+
for (const tunnel of result.tunnels) {
|
|
861
|
+
writeLine(output, `${tunnel.port}: ${tunnel.url}`);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
else {
|
|
865
|
+
writeLine(output, 'No active tunnels');
|
|
866
|
+
}
|
|
867
|
+
return 0;
|
|
868
|
+
}
|
|
869
|
+
if (first === 'stop' || first === 'stop-all') {
|
|
870
|
+
const stopAll = first === 'stop-all' || argv.includes('--all');
|
|
871
|
+
if (stopAll) {
|
|
872
|
+
const result = await manager.stop_all_tunnels();
|
|
873
|
+
if (json_output) {
|
|
874
|
+
render(result);
|
|
875
|
+
}
|
|
876
|
+
else if (result.count > 0) {
|
|
877
|
+
writeLine(output, `Stopped ${result.count} tunnel(s): ${result.stopped.join(', ')}`);
|
|
878
|
+
}
|
|
879
|
+
else {
|
|
880
|
+
writeLine(output, 'No tunnels to stop');
|
|
881
|
+
}
|
|
882
|
+
return 0;
|
|
883
|
+
}
|
|
884
|
+
const port = parseTunnelPort(argv[1]);
|
|
885
|
+
const result = await manager.stop_tunnel(port);
|
|
886
|
+
if ('error' in result) {
|
|
887
|
+
writeLine(errorOutput, result.error);
|
|
888
|
+
return 1;
|
|
889
|
+
}
|
|
890
|
+
if (json_output) {
|
|
891
|
+
render(result);
|
|
892
|
+
}
|
|
893
|
+
else {
|
|
894
|
+
writeLine(output, `Stopped tunnel on port ${result.stopped}`);
|
|
895
|
+
}
|
|
896
|
+
return 0;
|
|
897
|
+
}
|
|
898
|
+
const port = parseTunnelPort(first);
|
|
899
|
+
const result = await manager.start_tunnel(port);
|
|
900
|
+
if ('error' in result) {
|
|
901
|
+
writeLine(errorOutput, result.error);
|
|
902
|
+
return 1;
|
|
903
|
+
}
|
|
904
|
+
if (json_output) {
|
|
905
|
+
render(result);
|
|
906
|
+
}
|
|
907
|
+
else if (result.existing) {
|
|
908
|
+
writeLine(output, `Tunnel already running: http://localhost:${result.port} -> ${result.url}`);
|
|
909
|
+
}
|
|
910
|
+
else {
|
|
911
|
+
writeLine(output, `Tunnel started: http://localhost:${result.port} -> ${result.url}`);
|
|
912
|
+
}
|
|
913
|
+
return 0;
|
|
914
|
+
}
|
|
915
|
+
catch (error) {
|
|
916
|
+
writeLine(errorOutput, error.message);
|
|
917
|
+
return 1;
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
const validateSetupMode = (mode) => {
|
|
921
|
+
const normalized = (mode ?? 'local').trim().toLowerCase();
|
|
922
|
+
if (normalized === 'local' ||
|
|
923
|
+
normalized === 'remote' ||
|
|
924
|
+
normalized === 'full') {
|
|
925
|
+
return normalized;
|
|
926
|
+
}
|
|
927
|
+
throw new Error(`Invalid setup mode "${mode ?? ''}". Expected local, remote, or full.`);
|
|
928
|
+
};
|
|
929
|
+
const renderSetupChecks = (mode, report) => {
|
|
930
|
+
const checks = {
|
|
931
|
+
browser_use_package: report.checks.package,
|
|
932
|
+
};
|
|
933
|
+
if (mode === 'local' || mode === 'full') {
|
|
934
|
+
checks.browser = report.checks.browser;
|
|
935
|
+
}
|
|
936
|
+
if (mode === 'remote' || mode === 'full') {
|
|
937
|
+
checks.api_key = report.checks.api_key;
|
|
938
|
+
checks.cloudflared = report.checks.cloudflared;
|
|
939
|
+
}
|
|
940
|
+
return checks;
|
|
941
|
+
};
|
|
942
|
+
const planSetupActions = (mode, checks, yes, api_key) => {
|
|
943
|
+
const actions = [];
|
|
944
|
+
if ((mode === 'local' || mode === 'full') &&
|
|
945
|
+
checks.browser?.status !== 'ok') {
|
|
946
|
+
actions.push({
|
|
947
|
+
type: 'install_browser',
|
|
948
|
+
description: 'Install browser (Chromium)',
|
|
949
|
+
required: true,
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
if ((mode === 'remote' || mode === 'full') &&
|
|
953
|
+
checks.api_key?.status !== 'ok') {
|
|
954
|
+
if (api_key?.trim()) {
|
|
955
|
+
actions.push({
|
|
956
|
+
type: 'configure_api_key',
|
|
957
|
+
description: 'Configure API key',
|
|
958
|
+
required: true,
|
|
959
|
+
api_key: api_key.trim(),
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
else if (!yes) {
|
|
963
|
+
actions.push({
|
|
964
|
+
type: 'prompt_api_key',
|
|
965
|
+
description: 'Prompt for API key',
|
|
966
|
+
required: false,
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
if ((mode === 'remote' || mode === 'full') &&
|
|
971
|
+
checks.cloudflared?.status !== 'ok') {
|
|
972
|
+
actions.push({
|
|
973
|
+
type: 'install_cloudflared',
|
|
974
|
+
description: 'Install cloudflared (for tunneling)',
|
|
975
|
+
required: true,
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
return actions;
|
|
979
|
+
};
|
|
980
|
+
const logSetupChecks = (stream, checks) => {
|
|
981
|
+
writeLine(stream, '');
|
|
982
|
+
writeLine(stream, 'Running checks...');
|
|
983
|
+
writeLine(stream, '');
|
|
984
|
+
for (const [name, check] of Object.entries(checks)) {
|
|
985
|
+
const icon = check.status === 'ok' ? '✓' : check.status === 'missing' ? '⚠' : '✗';
|
|
986
|
+
writeLine(stream, ` ${icon} ${name.replace(/_/g, ' ')}: ${check.message}`);
|
|
987
|
+
}
|
|
988
|
+
writeLine(stream, '');
|
|
989
|
+
};
|
|
990
|
+
const logSetupActions = (stream, actions) => {
|
|
991
|
+
if (actions.length === 0) {
|
|
992
|
+
writeLine(stream, 'No additional setup needed.');
|
|
993
|
+
writeLine(stream, '');
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
writeLine(stream, '');
|
|
997
|
+
writeLine(stream, 'Setup actions:');
|
|
998
|
+
writeLine(stream, '');
|
|
999
|
+
actions.forEach((action, index) => {
|
|
1000
|
+
writeLine(stream, ` ${index + 1}. ${action.description} ${action.required ? '(required)' : '(optional)'}`);
|
|
1001
|
+
});
|
|
1002
|
+
writeLine(stream, '');
|
|
1003
|
+
};
|
|
1004
|
+
const logSetupValidation = (stream, validation) => {
|
|
1005
|
+
writeLine(stream, '');
|
|
1006
|
+
writeLine(stream, 'Validation:');
|
|
1007
|
+
writeLine(stream, '');
|
|
1008
|
+
for (const [name, result] of Object.entries(validation)) {
|
|
1009
|
+
const normalized = String(result);
|
|
1010
|
+
const ok = normalized === 'ok' || normalized === 'true';
|
|
1011
|
+
writeLine(stream, ` ${ok ? '✓' : '✗'} ${name.replace(/_/g, ' ')}: ${normalized}`);
|
|
1012
|
+
}
|
|
1013
|
+
writeLine(stream, '');
|
|
1014
|
+
};
|
|
1015
|
+
export const runSetupCommand = async (params, options = {}) => {
|
|
1016
|
+
const mode = validateSetupMode(params.mode);
|
|
1017
|
+
const yes = Boolean(params.yes);
|
|
1018
|
+
const api_key = params.api_key?.trim() || null;
|
|
1019
|
+
const runDoctor = options.run_doctor_checks ??
|
|
1020
|
+
((doctorOptions) => runDoctorChecks(doctorOptions));
|
|
1021
|
+
const installCommand = options.install_command ?? runInstallCommand;
|
|
1022
|
+
const saveApiKey = options.save_api_key ?? save_cloud_api_token;
|
|
1023
|
+
const output = options.stdout ?? process.stdout;
|
|
1024
|
+
const json_output = Boolean(options.json_output);
|
|
1025
|
+
const initialReport = await runDoctor();
|
|
1026
|
+
const checks = renderSetupChecks(mode, initialReport);
|
|
1027
|
+
const actions = planSetupActions(mode, checks, yes, api_key);
|
|
1028
|
+
if (!json_output) {
|
|
1029
|
+
logSetupChecks(output, checks);
|
|
1030
|
+
logSetupActions(output, actions);
|
|
1031
|
+
}
|
|
1032
|
+
for (const action of actions) {
|
|
1033
|
+
if (action.type === 'install_browser') {
|
|
1034
|
+
if (!json_output) {
|
|
1035
|
+
writeLine(output, 'Installing Chromium browser...');
|
|
1036
|
+
}
|
|
1037
|
+
await installCommand();
|
|
1038
|
+
continue;
|
|
1039
|
+
}
|
|
1040
|
+
if (action.type === 'configure_api_key') {
|
|
1041
|
+
if (!json_output) {
|
|
1042
|
+
writeLine(output, 'Configuring API key...');
|
|
1043
|
+
}
|
|
1044
|
+
saveApiKey(action.api_key);
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
if (action.type === 'prompt_api_key' && !json_output) {
|
|
1048
|
+
writeLine(output, 'API key not configured');
|
|
1049
|
+
writeLine(output, ' Set via: export BROWSER_USE_API_KEY=your_key');
|
|
1050
|
+
writeLine(output, ' Or: browser-use setup --api-key <key>');
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
if (action.type === 'install_cloudflared' && !json_output) {
|
|
1054
|
+
writeLine(output, 'cloudflared not installed');
|
|
1055
|
+
writeLine(output, ' Install via:');
|
|
1056
|
+
writeLine(output, ' macOS: brew install cloudflared');
|
|
1057
|
+
writeLine(output, ' Linux: curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o ~/.local/bin/cloudflared && chmod +x ~/.local/bin/cloudflared');
|
|
1058
|
+
writeLine(output, ' Windows: winget install Cloudflare.cloudflared');
|
|
1059
|
+
writeLine(output, '');
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
const validationReport = await runDoctor();
|
|
1063
|
+
const validation = {
|
|
1064
|
+
browser_use_import: 'ok',
|
|
1065
|
+
};
|
|
1066
|
+
if (mode === 'local' || mode === 'full') {
|
|
1067
|
+
validation.browser_available =
|
|
1068
|
+
validationReport.checks.browser.status === 'ok'
|
|
1069
|
+
? 'ok'
|
|
1070
|
+
: `failed: ${validationReport.checks.browser.message}`;
|
|
1071
|
+
}
|
|
1072
|
+
if (mode === 'remote' || mode === 'full') {
|
|
1073
|
+
validation.api_key_available =
|
|
1074
|
+
validationReport.checks.api_key.status === 'ok';
|
|
1075
|
+
validation.cloudflared_available =
|
|
1076
|
+
validationReport.checks.cloudflared.status === 'ok';
|
|
1077
|
+
}
|
|
1078
|
+
const result = {
|
|
1079
|
+
status: 'success',
|
|
1080
|
+
mode,
|
|
1081
|
+
checks,
|
|
1082
|
+
validation,
|
|
1083
|
+
};
|
|
1084
|
+
if (json_output) {
|
|
1085
|
+
writeLine(output, JSON.stringify(result, null, 2));
|
|
1086
|
+
}
|
|
1087
|
+
else {
|
|
1088
|
+
logSetupValidation(output, validation);
|
|
1089
|
+
}
|
|
1090
|
+
return 0;
|
|
1091
|
+
};
|
|
1092
|
+
const formatDuration = (startedAt, finishedAt) => {
|
|
1093
|
+
if (!startedAt) {
|
|
1094
|
+
return '';
|
|
1095
|
+
}
|
|
1096
|
+
const start = new Date(startedAt).getTime();
|
|
1097
|
+
const end = finishedAt ? new Date(finishedAt).getTime() : Date.now();
|
|
1098
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) {
|
|
1099
|
+
return '';
|
|
1100
|
+
}
|
|
1101
|
+
const totalSeconds = Math.floor((end - start) / 1000);
|
|
1102
|
+
if (totalSeconds < 60) {
|
|
1103
|
+
return `${totalSeconds}s`;
|
|
1104
|
+
}
|
|
1105
|
+
if (totalSeconds < 3600) {
|
|
1106
|
+
return `${Math.floor(totalSeconds / 60)}m ${totalSeconds % 60}s`;
|
|
1107
|
+
}
|
|
1108
|
+
return `${Math.floor(totalSeconds / 3600)}h ${Math.floor((totalSeconds % 3600) / 60)}m`;
|
|
1109
|
+
};
|
|
1110
|
+
const printTaskStep = (stream, step, verbose) => {
|
|
1111
|
+
const stepNumber = step.number ?? '?';
|
|
1112
|
+
const memory = String(step.memory ?? '');
|
|
1113
|
+
if (verbose) {
|
|
1114
|
+
const url = String(step.url ?? '');
|
|
1115
|
+
const shortUrl = url.length > 60 ? `${url.slice(0, 57)}...` : url;
|
|
1116
|
+
writeLine(stream, ` [${stepNumber}] ${shortUrl}`);
|
|
1117
|
+
if (memory) {
|
|
1118
|
+
const shortMemory = memory.length > 100 ? `${memory.slice(0, 97)}...` : memory;
|
|
1119
|
+
writeLine(stream, ` Reasoning: ${shortMemory}`);
|
|
1120
|
+
}
|
|
1121
|
+
const actions = Array.isArray(step.actions) ? step.actions : [];
|
|
1122
|
+
actions.slice(0, 2).forEach((action) => {
|
|
1123
|
+
const text = String(action);
|
|
1124
|
+
const shortAction = text.length > 70 ? `${text.slice(0, 67)}...` : text;
|
|
1125
|
+
writeLine(stream, ` Action: ${shortAction}`);
|
|
1126
|
+
});
|
|
1127
|
+
if (actions.length > 2) {
|
|
1128
|
+
writeLine(stream, ` ... and ${actions.length - 2} more actions`);
|
|
1129
|
+
}
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
const shortMemory = memory
|
|
1133
|
+
? memory.length > 80
|
|
1134
|
+
? `${memory.slice(0, 77)}...`
|
|
1135
|
+
: memory
|
|
1136
|
+
: '(no reasoning)';
|
|
1137
|
+
writeLine(stream, ` ${stepNumber}. ${shortMemory}`);
|
|
1138
|
+
};
|
|
1139
|
+
const requireCommandTarget = (value, usage) => {
|
|
1140
|
+
const target = value?.trim();
|
|
1141
|
+
if (!target || target.startsWith('-')) {
|
|
1142
|
+
throw new Error(usage);
|
|
1143
|
+
}
|
|
1144
|
+
return target;
|
|
1145
|
+
};
|
|
1146
|
+
const rejectUnexpectedPositionals = (positionals, usage) => {
|
|
1147
|
+
if (positionals.length > 0) {
|
|
1148
|
+
throw new Error(usage);
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
const markUsedOption = (used_options, option) => {
|
|
1152
|
+
if (!used_options.includes(option)) {
|
|
1153
|
+
used_options.push(option);
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
const rejectUnsupportedFlags = (used_options, allowed_options, usage) => {
|
|
1157
|
+
const allowed = new Set(allowed_options);
|
|
1158
|
+
const unsupported = used_options.find((option) => !allowed.has(option));
|
|
1159
|
+
if (unsupported) {
|
|
1160
|
+
throw new Error(usage);
|
|
1161
|
+
}
|
|
1162
|
+
};
|
|
1163
|
+
const parseTaskCommandFlags = (argv) => {
|
|
1164
|
+
const flags = {
|
|
1165
|
+
json: false,
|
|
1166
|
+
limit: 10,
|
|
1167
|
+
status: null,
|
|
1168
|
+
session: null,
|
|
1169
|
+
compact: false,
|
|
1170
|
+
verbose: false,
|
|
1171
|
+
last: null,
|
|
1172
|
+
reverse: false,
|
|
1173
|
+
step: null,
|
|
1174
|
+
positionals: [],
|
|
1175
|
+
used_options: [],
|
|
1176
|
+
};
|
|
1177
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
1178
|
+
const arg = argv[index] ?? '';
|
|
1179
|
+
if (arg === '--json') {
|
|
1180
|
+
flags.json = true;
|
|
1181
|
+
markUsedOption(flags.used_options, '--json');
|
|
1182
|
+
continue;
|
|
1183
|
+
}
|
|
1184
|
+
if (arg === '--compact' || arg === '-c') {
|
|
1185
|
+
flags.compact = true;
|
|
1186
|
+
markUsedOption(flags.used_options, '--compact');
|
|
1187
|
+
continue;
|
|
1188
|
+
}
|
|
1189
|
+
if (arg === '--verbose' || arg === '-v') {
|
|
1190
|
+
flags.verbose = true;
|
|
1191
|
+
markUsedOption(flags.used_options, '--verbose');
|
|
1192
|
+
continue;
|
|
1193
|
+
}
|
|
1194
|
+
if (arg === '--reverse' || arg === '-r') {
|
|
1195
|
+
flags.reverse = true;
|
|
1196
|
+
markUsedOption(flags.used_options, '--reverse');
|
|
1197
|
+
continue;
|
|
1198
|
+
}
|
|
1199
|
+
if (arg === '--limit' || arg.startsWith('--limit=')) {
|
|
1200
|
+
const { value, nextIndex } = takeOptionValue(arg, index, argv);
|
|
1201
|
+
flags.limit = parsePositiveInt('--limit', value);
|
|
1202
|
+
markUsedOption(flags.used_options, '--limit');
|
|
1203
|
+
index = nextIndex;
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
if (arg === '--status' || arg.startsWith('--status=')) {
|
|
1207
|
+
const { value, nextIndex } = takeOptionValue(arg, index, argv);
|
|
1208
|
+
flags.status = value;
|
|
1209
|
+
markUsedOption(flags.used_options, '--status');
|
|
1210
|
+
index = nextIndex;
|
|
1211
|
+
continue;
|
|
1212
|
+
}
|
|
1213
|
+
if (arg === '--session' || arg.startsWith('--session=')) {
|
|
1214
|
+
const { value, nextIndex } = takeOptionValue(arg, index, argv);
|
|
1215
|
+
flags.session = value;
|
|
1216
|
+
markUsedOption(flags.used_options, '--session');
|
|
1217
|
+
index = nextIndex;
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
if (arg === '--last' || arg === '-n' || arg.startsWith('--last=')) {
|
|
1221
|
+
const { value, nextIndex } = takeOptionValue(arg, index, argv);
|
|
1222
|
+
flags.last = parsePositiveInt('--last', value);
|
|
1223
|
+
markUsedOption(flags.used_options, '--last');
|
|
1224
|
+
index = nextIndex;
|
|
1225
|
+
continue;
|
|
1226
|
+
}
|
|
1227
|
+
if (arg === '--step' || arg === '-s' || arg.startsWith('--step=')) {
|
|
1228
|
+
const { value, nextIndex } = takeOptionValue(arg, index, argv);
|
|
1229
|
+
flags.step = parsePositiveInt('--step', value);
|
|
1230
|
+
markUsedOption(flags.used_options, '--step');
|
|
1231
|
+
index = nextIndex;
|
|
1232
|
+
continue;
|
|
1233
|
+
}
|
|
1234
|
+
if (arg.startsWith('-')) {
|
|
1235
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
1236
|
+
}
|
|
1237
|
+
flags.positionals.push(arg);
|
|
1238
|
+
}
|
|
1239
|
+
return flags;
|
|
1240
|
+
};
|
|
1241
|
+
export const runTaskCommand = async (argv, options = {}) => {
|
|
1242
|
+
const client = options.client ?? new CloudManagementClient();
|
|
1243
|
+
const output = options.stdout ?? process.stdout;
|
|
1244
|
+
const errorOutput = options.stderr ?? process.stderr;
|
|
1245
|
+
const subcommand = argv[0] ?? '';
|
|
1246
|
+
try {
|
|
1247
|
+
if (subcommand === 'list') {
|
|
1248
|
+
const flags = parseTaskCommandFlags(argv.slice(1));
|
|
1249
|
+
rejectUnexpectedPositionals(flags.positionals, 'Usage: browser-use task list [options]');
|
|
1250
|
+
rejectUnsupportedFlags(flags.used_options, ['--json', '--limit', '--status', '--session'], 'Usage: browser-use task list [options]');
|
|
1251
|
+
const result = await client.list_tasks({
|
|
1252
|
+
pageSize: flags.limit,
|
|
1253
|
+
filterBy: flags.status,
|
|
1254
|
+
sessionId: flags.session,
|
|
1255
|
+
});
|
|
1256
|
+
if (flags.json) {
|
|
1257
|
+
writeLine(output, JSON.stringify(result.items, null, 2));
|
|
1258
|
+
return 0;
|
|
1259
|
+
}
|
|
1260
|
+
if (result.items.length === 0) {
|
|
1261
|
+
writeLine(output, 'No tasks found');
|
|
1262
|
+
return 0;
|
|
1263
|
+
}
|
|
1264
|
+
writeLine(output, `Tasks (${result.items.length}):`);
|
|
1265
|
+
for (const task of result.items) {
|
|
1266
|
+
const emoji = {
|
|
1267
|
+
created: '🕒',
|
|
1268
|
+
started: '🔄',
|
|
1269
|
+
running: '🔄',
|
|
1270
|
+
finished: '✅',
|
|
1271
|
+
stopped: '⏹️',
|
|
1272
|
+
failed: '❌',
|
|
1273
|
+
}[task.status] ?? '❓';
|
|
1274
|
+
const text = task.task.length > 50 ? `${task.task.slice(0, 47)}...` : task.task;
|
|
1275
|
+
writeLine(output, ` ${emoji} ${task.id.slice(0, 8)}... [${task.status}] ${text}`);
|
|
1276
|
+
}
|
|
1277
|
+
return 0;
|
|
1278
|
+
}
|
|
1279
|
+
if (subcommand === 'status') {
|
|
1280
|
+
const taskId = requireCommandTarget(argv[1], 'Usage: browser-use task status <task-id>');
|
|
1281
|
+
const flags = parseTaskCommandFlags(argv.slice(2));
|
|
1282
|
+
rejectUnexpectedPositionals(flags.positionals, 'Usage: browser-use task status <task-id>');
|
|
1283
|
+
rejectUnsupportedFlags(flags.used_options, ['--json', '--compact', '--verbose', '--reverse', '--last', '--step'], 'Usage: browser-use task status <task-id>');
|
|
1284
|
+
const task = await client.get_task(taskId);
|
|
1285
|
+
if (flags.json) {
|
|
1286
|
+
writeLine(output, JSON.stringify(task, null, 2));
|
|
1287
|
+
return 0;
|
|
1288
|
+
}
|
|
1289
|
+
const parts = [
|
|
1290
|
+
`${{
|
|
1291
|
+
created: '🕒',
|
|
1292
|
+
started: '🔄',
|
|
1293
|
+
running: '🔄',
|
|
1294
|
+
finished: '✅',
|
|
1295
|
+
stopped: '⏹️',
|
|
1296
|
+
failed: '❌',
|
|
1297
|
+
}[task.status] ?? '❓'} ${task.id.slice(0, 8)}... [${task.status}]`,
|
|
1298
|
+
];
|
|
1299
|
+
const duration = formatDuration(task.startedAt, task.finishedAt);
|
|
1300
|
+
if (duration) {
|
|
1301
|
+
parts.push(duration);
|
|
1302
|
+
}
|
|
1303
|
+
writeLine(output, parts.join(' '));
|
|
1304
|
+
let steps = [...(task.steps ?? [])];
|
|
1305
|
+
const showAllSteps = flags.compact || flags.verbose;
|
|
1306
|
+
if (flags.step !== null) {
|
|
1307
|
+
steps = steps.filter((step) => Number(step.number) === flags.step);
|
|
1308
|
+
}
|
|
1309
|
+
else if (!showAllSteps && steps.length > 1) {
|
|
1310
|
+
writeLine(output, ` ... ${steps.length - 1} earlier steps`);
|
|
1311
|
+
steps = [steps[steps.length - 1]];
|
|
1312
|
+
}
|
|
1313
|
+
else if (flags.last !== null && flags.last < steps.length) {
|
|
1314
|
+
writeLine(output, ` ... ${steps.length - flags.last} earlier steps`);
|
|
1315
|
+
steps = steps.slice(-flags.last);
|
|
1316
|
+
}
|
|
1317
|
+
if (flags.reverse) {
|
|
1318
|
+
steps.reverse();
|
|
1319
|
+
}
|
|
1320
|
+
steps.forEach((step) => printTaskStep(output, step, flags.verbose));
|
|
1321
|
+
if (task.output) {
|
|
1322
|
+
writeLine(output, '');
|
|
1323
|
+
writeLine(output, `Output: ${task.output}`);
|
|
1324
|
+
}
|
|
1325
|
+
return 0;
|
|
1326
|
+
}
|
|
1327
|
+
if (subcommand === 'stop') {
|
|
1328
|
+
const taskId = requireCommandTarget(argv[1], 'Usage: browser-use task stop <task-id>');
|
|
1329
|
+
const flags = parseTaskCommandFlags(argv.slice(2));
|
|
1330
|
+
rejectUnexpectedPositionals(flags.positionals, 'Usage: browser-use task stop <task-id>');
|
|
1331
|
+
rejectUnsupportedFlags(flags.used_options, ['--json'], 'Usage: browser-use task stop <task-id>');
|
|
1332
|
+
await client.update_task(taskId, 'stop');
|
|
1333
|
+
if (flags.json) {
|
|
1334
|
+
writeLine(output, JSON.stringify({ stopped: taskId }, null, 2));
|
|
1335
|
+
}
|
|
1336
|
+
else {
|
|
1337
|
+
writeLine(output, `Stopped task: ${taskId}`);
|
|
1338
|
+
}
|
|
1339
|
+
return 0;
|
|
1340
|
+
}
|
|
1341
|
+
if (subcommand === 'logs') {
|
|
1342
|
+
const taskId = requireCommandTarget(argv[1], 'Usage: browser-use task logs <task-id>');
|
|
1343
|
+
const flags = parseTaskCommandFlags(argv.slice(2));
|
|
1344
|
+
rejectUnexpectedPositionals(flags.positionals, 'Usage: browser-use task logs <task-id>');
|
|
1345
|
+
rejectUnsupportedFlags(flags.used_options, ['--json'], 'Usage: browser-use task logs <task-id>');
|
|
1346
|
+
const result = await client.get_task_logs(taskId);
|
|
1347
|
+
if (flags.json) {
|
|
1348
|
+
writeLine(output, JSON.stringify(result, null, 2));
|
|
1349
|
+
}
|
|
1350
|
+
else if (result.downloadUrl) {
|
|
1351
|
+
writeLine(output, `Download logs: ${result.downloadUrl}`);
|
|
1352
|
+
}
|
|
1353
|
+
else {
|
|
1354
|
+
writeLine(output, 'No logs available for this task');
|
|
1355
|
+
}
|
|
1356
|
+
return 0;
|
|
1357
|
+
}
|
|
1358
|
+
writeLine(errorOutput, 'Usage: browser-use task <list|status|stop|logs> [options]');
|
|
1359
|
+
return 1;
|
|
1360
|
+
}
|
|
1361
|
+
catch (error) {
|
|
1362
|
+
writeLine(errorOutput, `Error: ${error.message}`);
|
|
1363
|
+
return 1;
|
|
1364
|
+
}
|
|
1365
|
+
};
|
|
1366
|
+
const parseSessionCommandFlags = (argv) => {
|
|
1367
|
+
const flags = {
|
|
1368
|
+
json: false,
|
|
1369
|
+
limit: 10,
|
|
1370
|
+
status: null,
|
|
1371
|
+
all: false,
|
|
1372
|
+
delete: false,
|
|
1373
|
+
profile: null,
|
|
1374
|
+
proxy_country: null,
|
|
1375
|
+
start_url: null,
|
|
1376
|
+
screen_size: null,
|
|
1377
|
+
positionals: [],
|
|
1378
|
+
used_options: [],
|
|
1379
|
+
};
|
|
1380
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
1381
|
+
const arg = argv[index] ?? '';
|
|
1382
|
+
if (arg === '--json') {
|
|
1383
|
+
flags.json = true;
|
|
1384
|
+
markUsedOption(flags.used_options, '--json');
|
|
1385
|
+
continue;
|
|
1386
|
+
}
|
|
1387
|
+
if (arg === '--all') {
|
|
1388
|
+
flags.all = true;
|
|
1389
|
+
markUsedOption(flags.used_options, '--all');
|
|
1390
|
+
continue;
|
|
1391
|
+
}
|
|
1392
|
+
if (arg === '--delete') {
|
|
1393
|
+
flags.delete = true;
|
|
1394
|
+
markUsedOption(flags.used_options, '--delete');
|
|
1395
|
+
continue;
|
|
1396
|
+
}
|
|
1397
|
+
if (arg === '--limit' || arg.startsWith('--limit=')) {
|
|
1398
|
+
const { value, nextIndex } = takeOptionValue(arg, index, argv);
|
|
1399
|
+
flags.limit = parsePositiveInt('--limit', value);
|
|
1400
|
+
markUsedOption(flags.used_options, '--limit');
|
|
1401
|
+
index = nextIndex;
|
|
1402
|
+
continue;
|
|
1403
|
+
}
|
|
1404
|
+
if (arg === '--status' || arg.startsWith('--status=')) {
|
|
1405
|
+
const { value, nextIndex } = takeOptionValue(arg, index, argv);
|
|
1406
|
+
flags.status = value;
|
|
1407
|
+
markUsedOption(flags.used_options, '--status');
|
|
1408
|
+
index = nextIndex;
|
|
1409
|
+
continue;
|
|
1410
|
+
}
|
|
1411
|
+
if (arg === '--profile' || arg.startsWith('--profile=')) {
|
|
1412
|
+
const { value, nextIndex } = takeOptionValue(arg, index, argv);
|
|
1413
|
+
flags.profile = value;
|
|
1414
|
+
markUsedOption(flags.used_options, '--profile');
|
|
1415
|
+
index = nextIndex;
|
|
1416
|
+
continue;
|
|
1417
|
+
}
|
|
1418
|
+
if (arg === '--proxy-country' || arg.startsWith('--proxy-country=')) {
|
|
1419
|
+
const { value, nextIndex } = takeOptionValue(arg, index, argv);
|
|
1420
|
+
flags.proxy_country = value;
|
|
1421
|
+
markUsedOption(flags.used_options, '--proxy-country');
|
|
1422
|
+
index = nextIndex;
|
|
1423
|
+
continue;
|
|
1424
|
+
}
|
|
1425
|
+
if (arg === '--start-url' || arg.startsWith('--start-url=')) {
|
|
1426
|
+
const { value, nextIndex } = takeOptionValue(arg, index, argv);
|
|
1427
|
+
flags.start_url = value;
|
|
1428
|
+
markUsedOption(flags.used_options, '--start-url');
|
|
1429
|
+
index = nextIndex;
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
if (arg === '--screen-size' || arg.startsWith('--screen-size=')) {
|
|
1433
|
+
const { value, nextIndex } = takeOptionValue(arg, index, argv);
|
|
1434
|
+
flags.screen_size = value;
|
|
1435
|
+
markUsedOption(flags.used_options, '--screen-size');
|
|
1436
|
+
index = nextIndex;
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1439
|
+
if (arg.startsWith('-')) {
|
|
1440
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
1441
|
+
}
|
|
1442
|
+
flags.positionals.push(arg);
|
|
1443
|
+
}
|
|
1444
|
+
return flags;
|
|
1445
|
+
};
|
|
1446
|
+
export const runSessionCommand = async (argv, options = {}) => {
|
|
1447
|
+
const client = options.client ?? new CloudManagementClient();
|
|
1448
|
+
const output = options.stdout ?? process.stdout;
|
|
1449
|
+
const errorOutput = options.stderr ?? process.stderr;
|
|
1450
|
+
const subcommand = argv[0] ?? '';
|
|
1451
|
+
try {
|
|
1452
|
+
if (subcommand === 'list') {
|
|
1453
|
+
const flags = parseSessionCommandFlags(argv.slice(1));
|
|
1454
|
+
rejectUnexpectedPositionals(flags.positionals, 'Usage: browser-use session list [options]');
|
|
1455
|
+
rejectUnsupportedFlags(flags.used_options, ['--json', '--limit', '--status'], 'Usage: browser-use session list [options]');
|
|
1456
|
+
const result = await client.list_sessions({
|
|
1457
|
+
pageSize: flags.limit,
|
|
1458
|
+
filterBy: flags.status,
|
|
1459
|
+
});
|
|
1460
|
+
if (flags.json) {
|
|
1461
|
+
writeLine(output, JSON.stringify(result.items, null, 2));
|
|
1462
|
+
return 0;
|
|
1463
|
+
}
|
|
1464
|
+
if (result.items.length === 0) {
|
|
1465
|
+
writeLine(output, 'No sessions found');
|
|
1466
|
+
return 0;
|
|
1467
|
+
}
|
|
1468
|
+
writeLine(output, `Sessions (${result.items.length}):`);
|
|
1469
|
+
for (const session of result.items) {
|
|
1470
|
+
const emoji = session.status === 'active' ? '🟢' : '⏹️';
|
|
1471
|
+
const duration = formatDuration(session.startedAt, session.finishedAt);
|
|
1472
|
+
const details = duration ? ` ${duration}` : '';
|
|
1473
|
+
writeLine(output, ` ${emoji} ${session.id.slice(0, 8)}... [${session.status}]${details}`);
|
|
1474
|
+
}
|
|
1475
|
+
return 0;
|
|
1476
|
+
}
|
|
1477
|
+
if (subcommand === 'get') {
|
|
1478
|
+
const sessionId = requireCommandTarget(argv[1], 'Usage: browser-use session get <session-id>');
|
|
1479
|
+
const flags = parseSessionCommandFlags(argv.slice(2));
|
|
1480
|
+
rejectUnexpectedPositionals(flags.positionals, 'Usage: browser-use session get <session-id>');
|
|
1481
|
+
rejectUnsupportedFlags(flags.used_options, ['--json'], 'Usage: browser-use session get <session-id>');
|
|
1482
|
+
const session = await client.get_session(sessionId);
|
|
1483
|
+
if (flags.json) {
|
|
1484
|
+
writeLine(output, JSON.stringify(session, null, 2));
|
|
1485
|
+
}
|
|
1486
|
+
else {
|
|
1487
|
+
writeLine(output, `${session.id} [${session.status}]`);
|
|
1488
|
+
if (session.liveUrl) {
|
|
1489
|
+
writeLine(output, `Live URL: ${session.liveUrl}`);
|
|
1490
|
+
}
|
|
1491
|
+
const duration = formatDuration(session.startedAt, session.finishedAt);
|
|
1492
|
+
if (duration) {
|
|
1493
|
+
writeLine(output, `Duration: ${duration}`);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
return 0;
|
|
1497
|
+
}
|
|
1498
|
+
if (subcommand === 'stop') {
|
|
1499
|
+
const flags = parseSessionCommandFlags(argv.slice(1));
|
|
1500
|
+
rejectUnsupportedFlags(flags.used_options, ['--json', '--all'], 'Usage: browser-use session stop <session-id> | --all');
|
|
1501
|
+
if (flags.all) {
|
|
1502
|
+
rejectUnexpectedPositionals(flags.positionals, 'Usage: browser-use session stop <session-id> | --all');
|
|
1503
|
+
const sessions = await client.list_sessions({
|
|
1504
|
+
pageSize: 100,
|
|
1505
|
+
filterBy: 'active',
|
|
1506
|
+
});
|
|
1507
|
+
const stopped = [];
|
|
1508
|
+
for (const session of sessions.items) {
|
|
1509
|
+
await client.update_session(session.id, 'stop');
|
|
1510
|
+
stopped.push(session.id);
|
|
1511
|
+
}
|
|
1512
|
+
if (flags.json) {
|
|
1513
|
+
writeLine(output, JSON.stringify({ stopped }, null, 2));
|
|
1514
|
+
}
|
|
1515
|
+
else if (stopped.length > 0) {
|
|
1516
|
+
writeLine(output, `Stopped ${stopped.length} session(s): ${stopped.join(', ')}`);
|
|
1517
|
+
}
|
|
1518
|
+
else {
|
|
1519
|
+
writeLine(output, 'No sessions to stop');
|
|
1520
|
+
}
|
|
1521
|
+
return 0;
|
|
1522
|
+
}
|
|
1523
|
+
const [sessionIdCandidate, ...unexpectedPositionals] = flags.positionals;
|
|
1524
|
+
rejectUnexpectedPositionals(unexpectedPositionals, 'Usage: browser-use session stop <session-id> | --all');
|
|
1525
|
+
const sessionId = requireCommandTarget(sessionIdCandidate ?? argv[1], 'Usage: browser-use session stop <session-id> | --all');
|
|
1526
|
+
await client.update_session(sessionId, 'stop');
|
|
1527
|
+
if (flags.json) {
|
|
1528
|
+
writeLine(output, JSON.stringify({ stopped: sessionId }, null, 2));
|
|
1529
|
+
}
|
|
1530
|
+
else {
|
|
1531
|
+
writeLine(output, `Stopped session: ${sessionId}`);
|
|
1532
|
+
}
|
|
1533
|
+
return 0;
|
|
1534
|
+
}
|
|
1535
|
+
if (subcommand === 'create') {
|
|
1536
|
+
const flags = parseSessionCommandFlags(argv.slice(1));
|
|
1537
|
+
rejectUnexpectedPositionals(flags.positionals, 'Usage: browser-use session create [options]');
|
|
1538
|
+
rejectUnsupportedFlags(flags.used_options, [
|
|
1539
|
+
'--json',
|
|
1540
|
+
'--profile',
|
|
1541
|
+
'--proxy-country',
|
|
1542
|
+
'--start-url',
|
|
1543
|
+
'--screen-size',
|
|
1544
|
+
], 'Usage: browser-use session create [options]');
|
|
1545
|
+
let browserScreenWidth = null;
|
|
1546
|
+
let browserScreenHeight = null;
|
|
1547
|
+
if (flags.screen_size) {
|
|
1548
|
+
const match = flags.screen_size.match(/^(\d+)x(\d+)$/i);
|
|
1549
|
+
if (!match) {
|
|
1550
|
+
throw new Error('Expected --screen-size WIDTHxHEIGHT');
|
|
1551
|
+
}
|
|
1552
|
+
browserScreenWidth = Number.parseInt(match[1], 10);
|
|
1553
|
+
browserScreenHeight = Number.parseInt(match[2], 10);
|
|
1554
|
+
}
|
|
1555
|
+
const session = await client.create_session({
|
|
1556
|
+
profileId: flags.profile,
|
|
1557
|
+
proxyCountryCode: flags.proxy_country,
|
|
1558
|
+
startUrl: flags.start_url,
|
|
1559
|
+
browserScreenWidth,
|
|
1560
|
+
browserScreenHeight,
|
|
1561
|
+
});
|
|
1562
|
+
if (flags.json) {
|
|
1563
|
+
writeLine(output, JSON.stringify(session, null, 2));
|
|
1564
|
+
}
|
|
1565
|
+
else {
|
|
1566
|
+
writeLine(output, `Created session: ${session.id}`);
|
|
1567
|
+
if (session.liveUrl) {
|
|
1568
|
+
writeLine(output, `Live URL: ${session.liveUrl}`);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
return 0;
|
|
1572
|
+
}
|
|
1573
|
+
if (subcommand === 'share') {
|
|
1574
|
+
const sessionId = requireCommandTarget(argv[1], 'Usage: browser-use session share <session-id> [--delete]');
|
|
1575
|
+
const flags = parseSessionCommandFlags(argv.slice(2));
|
|
1576
|
+
rejectUnexpectedPositionals(flags.positionals, 'Usage: browser-use session share <session-id> [--delete]');
|
|
1577
|
+
rejectUnsupportedFlags(flags.used_options, ['--json', '--delete'], 'Usage: browser-use session share <session-id> [--delete]');
|
|
1578
|
+
if (flags.delete) {
|
|
1579
|
+
await client.delete_session_public_share(sessionId);
|
|
1580
|
+
if (flags.json) {
|
|
1581
|
+
writeLine(output, JSON.stringify({ deleted: sessionId }, null, 2));
|
|
1582
|
+
}
|
|
1583
|
+
else {
|
|
1584
|
+
writeLine(output, `Deleted public share for session: ${sessionId}`);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
else {
|
|
1588
|
+
const share = await client.create_session_public_share(sessionId);
|
|
1589
|
+
if (flags.json) {
|
|
1590
|
+
writeLine(output, JSON.stringify(share, null, 2));
|
|
1591
|
+
}
|
|
1592
|
+
else {
|
|
1593
|
+
writeLine(output, `Public share URL: ${share.shareUrl}`);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
return 0;
|
|
1597
|
+
}
|
|
1598
|
+
writeLine(errorOutput, 'Usage: browser-use session <list|get|stop|create|share> [options]');
|
|
1599
|
+
return 1;
|
|
1600
|
+
}
|
|
1601
|
+
catch (error) {
|
|
1602
|
+
writeLine(errorOutput, `Error: ${error.message}`);
|
|
1603
|
+
return 1;
|
|
1604
|
+
}
|
|
1605
|
+
};
|
|
1606
|
+
const parseProfileCommandFlags = (argv) => {
|
|
1607
|
+
const flags = {
|
|
1608
|
+
json: false,
|
|
1609
|
+
remote: false,
|
|
1610
|
+
limit: 20,
|
|
1611
|
+
name: null,
|
|
1612
|
+
domain: null,
|
|
1613
|
+
from_profile: null,
|
|
1614
|
+
positionals: [],
|
|
1615
|
+
used_options: [],
|
|
1616
|
+
};
|
|
1617
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
1618
|
+
const arg = argv[index] ?? '';
|
|
1619
|
+
if (arg === '--json') {
|
|
1620
|
+
flags.json = true;
|
|
1621
|
+
markUsedOption(flags.used_options, '--json');
|
|
1622
|
+
continue;
|
|
1623
|
+
}
|
|
1624
|
+
if (arg === '--remote') {
|
|
1625
|
+
flags.remote = true;
|
|
1626
|
+
markUsedOption(flags.used_options, '--remote');
|
|
1627
|
+
continue;
|
|
1628
|
+
}
|
|
1629
|
+
if (arg === '--limit' || arg.startsWith('--limit=')) {
|
|
1630
|
+
const { value, nextIndex } = takeOptionValue(arg, index, argv);
|
|
1631
|
+
flags.limit = parsePositiveInt('--limit', value);
|
|
1632
|
+
markUsedOption(flags.used_options, '--limit');
|
|
1633
|
+
index = nextIndex;
|
|
1634
|
+
continue;
|
|
1635
|
+
}
|
|
1636
|
+
if (arg === '--name' || arg.startsWith('--name=')) {
|
|
1637
|
+
const { value, nextIndex } = takeOptionValue(arg, index, argv);
|
|
1638
|
+
flags.name = value;
|
|
1639
|
+
markUsedOption(flags.used_options, '--name');
|
|
1640
|
+
index = nextIndex;
|
|
1641
|
+
continue;
|
|
1642
|
+
}
|
|
1643
|
+
if (arg === '--domain' || arg.startsWith('--domain=')) {
|
|
1644
|
+
const { value, nextIndex } = takeOptionValue(arg, index, argv);
|
|
1645
|
+
flags.domain = value;
|
|
1646
|
+
markUsedOption(flags.used_options, '--domain');
|
|
1647
|
+
index = nextIndex;
|
|
1648
|
+
continue;
|
|
1649
|
+
}
|
|
1650
|
+
if (arg === '--from' || arg.startsWith('--from=')) {
|
|
1651
|
+
const { value, nextIndex } = takeOptionValue(arg, index, argv);
|
|
1652
|
+
flags.from_profile = value;
|
|
1653
|
+
markUsedOption(flags.used_options, '--from');
|
|
1654
|
+
index = nextIndex;
|
|
1655
|
+
continue;
|
|
1656
|
+
}
|
|
1657
|
+
if (arg.startsWith('-')) {
|
|
1658
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
1659
|
+
}
|
|
1660
|
+
flags.positionals.push(arg);
|
|
1661
|
+
}
|
|
1662
|
+
return flags;
|
|
1663
|
+
};
|
|
1664
|
+
const normalizeProfileCookieDomain = (value) => String(value ?? '')
|
|
1665
|
+
.trim()
|
|
1666
|
+
.replace(/^\./, '')
|
|
1667
|
+
.toLowerCase();
|
|
1668
|
+
const cookieMatchesDomainFilter = (cookie, domainFilter) => {
|
|
1669
|
+
if (!domainFilter) {
|
|
1670
|
+
return true;
|
|
1671
|
+
}
|
|
1672
|
+
const cookieDomain = normalizeProfileCookieDomain(cookie.domain);
|
|
1673
|
+
const normalizedFilter = normalizeProfileCookieDomain(domainFilter);
|
|
1674
|
+
return Boolean(cookieDomain &&
|
|
1675
|
+
normalizedFilter &&
|
|
1676
|
+
(cookieDomain === normalizedFilter ||
|
|
1677
|
+
cookieDomain.endsWith(`.${normalizedFilter}`) ||
|
|
1678
|
+
normalizedFilter.endsWith(`.${cookieDomain}`)));
|
|
1679
|
+
};
|
|
1680
|
+
export const runProfileCommand = async (argv, options = {}) => {
|
|
1681
|
+
const client = options.client ?? new CloudManagementClient();
|
|
1682
|
+
const profileLister = options.profile_lister ?? (() => systemChrome.listProfiles());
|
|
1683
|
+
const localSessionFactory = options.local_session_factory ??
|
|
1684
|
+
((profile_directory) => BrowserSession.from_system_chrome({
|
|
1685
|
+
profile_directory,
|
|
1686
|
+
profile: { headless: true },
|
|
1687
|
+
}));
|
|
1688
|
+
const remoteSessionFactory = options.remote_session_factory ??
|
|
1689
|
+
((init) => new BrowserSession({
|
|
1690
|
+
cdp_url: init.cdp_url,
|
|
1691
|
+
}));
|
|
1692
|
+
const cloudBrowserClientFactory = options.cloud_browser_client_factory ?? (() => new CloudBrowserClient());
|
|
1693
|
+
const output = options.stdout ?? process.stdout;
|
|
1694
|
+
const errorOutput = options.stderr ?? process.stderr;
|
|
1695
|
+
const subcommand = argv[0] ?? '';
|
|
1696
|
+
try {
|
|
1697
|
+
if (subcommand === 'list') {
|
|
1698
|
+
const flags = parseProfileCommandFlags(argv.slice(1));
|
|
1699
|
+
rejectUnexpectedPositionals(flags.positionals, 'Usage: browser-use profile list [--remote] [options]');
|
|
1700
|
+
rejectUnsupportedFlags(flags.used_options, ['--json', '--remote', '--limit'], 'Usage: browser-use profile list [--remote] [options]');
|
|
1701
|
+
if (flags.remote) {
|
|
1702
|
+
const result = await client.list_profiles({
|
|
1703
|
+
pageSize: flags.limit,
|
|
1704
|
+
});
|
|
1705
|
+
if (flags.json) {
|
|
1706
|
+
writeLine(output, JSON.stringify(result.items, null, 2));
|
|
1707
|
+
}
|
|
1708
|
+
else if (result.items.length === 0) {
|
|
1709
|
+
writeLine(output, 'No cloud profiles found');
|
|
1710
|
+
}
|
|
1711
|
+
else {
|
|
1712
|
+
writeLine(output, `Cloud profiles (${result.items.length}):`);
|
|
1713
|
+
result.items.forEach((profile) => {
|
|
1714
|
+
writeLine(output, ` ${profile.id}: ${profile.name || 'Unnamed'}`);
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
return 0;
|
|
1718
|
+
}
|
|
1719
|
+
const profiles = profileLister();
|
|
1720
|
+
if (flags.json) {
|
|
1721
|
+
writeLine(output, JSON.stringify({ profiles }, null, 2));
|
|
1722
|
+
}
|
|
1723
|
+
else if (profiles.length === 0) {
|
|
1724
|
+
writeLine(output, 'No Chrome profiles found');
|
|
1725
|
+
}
|
|
1726
|
+
else {
|
|
1727
|
+
writeLine(output, 'Local Chrome profiles:');
|
|
1728
|
+
profiles.forEach((profile) => {
|
|
1729
|
+
const emailSuffix = profile.email ? ` (${profile.email})` : '';
|
|
1730
|
+
writeLine(output, ` ${profile.directory}: ${profile.name}${emailSuffix}`);
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
return 0;
|
|
1734
|
+
}
|
|
1735
|
+
if (subcommand === 'get') {
|
|
1736
|
+
const profileId = requireCommandTarget(argv[1], 'Usage: browser-use profile get <profile-id> [--remote]');
|
|
1737
|
+
const flags = parseProfileCommandFlags(argv.slice(2));
|
|
1738
|
+
rejectUnexpectedPositionals(flags.positionals, 'Usage: browser-use profile get <profile-id> [--remote]');
|
|
1739
|
+
rejectUnsupportedFlags(flags.used_options, ['--json', '--remote'], 'Usage: browser-use profile get <profile-id> [--remote]');
|
|
1740
|
+
if (flags.remote) {
|
|
1741
|
+
const profile = await client.get_profile(profileId);
|
|
1742
|
+
if (flags.json) {
|
|
1743
|
+
writeLine(output, JSON.stringify(profile, null, 2));
|
|
1744
|
+
}
|
|
1745
|
+
else {
|
|
1746
|
+
writeLine(output, `Profile: ${profile.id}`);
|
|
1747
|
+
if (profile.name) {
|
|
1748
|
+
writeLine(output, ` Name: ${profile.name}`);
|
|
1749
|
+
}
|
|
1750
|
+
writeLine(output, ` Updated: ${profile.updatedAt}`);
|
|
1751
|
+
}
|
|
1752
|
+
return 0;
|
|
1753
|
+
}
|
|
1754
|
+
const profile = profileLister().find((entry) => entry.directory === profileId || entry.name === profileId);
|
|
1755
|
+
if (!profile) {
|
|
1756
|
+
throw new Error(`Profile "${profileId}" not found`);
|
|
1757
|
+
}
|
|
1758
|
+
if (flags.json) {
|
|
1759
|
+
writeLine(output, JSON.stringify(profile, null, 2));
|
|
1760
|
+
}
|
|
1761
|
+
else {
|
|
1762
|
+
writeLine(output, `Profile: ${profile.directory}`);
|
|
1763
|
+
writeLine(output, ` Name: ${profile.name}`);
|
|
1764
|
+
if (profile.email) {
|
|
1765
|
+
writeLine(output, ` Email: ${profile.email}`);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
return 0;
|
|
1769
|
+
}
|
|
1770
|
+
if (subcommand === 'create') {
|
|
1771
|
+
const flags = parseProfileCommandFlags(argv.slice(1));
|
|
1772
|
+
rejectUnexpectedPositionals(flags.positionals, 'Usage: browser-use profile create --remote [--name <name>] [--json]');
|
|
1773
|
+
rejectUnsupportedFlags(flags.used_options, ['--json', '--remote', '--name'], 'Usage: browser-use profile create --remote [--name <name>] [--json]');
|
|
1774
|
+
if (!flags.remote) {
|
|
1775
|
+
throw new Error('Profile create is only supported with --remote');
|
|
1776
|
+
}
|
|
1777
|
+
const profile = await client.create_profile({ name: flags.name });
|
|
1778
|
+
if (flags.json) {
|
|
1779
|
+
writeLine(output, JSON.stringify(profile, null, 2));
|
|
1780
|
+
}
|
|
1781
|
+
else {
|
|
1782
|
+
writeLine(output, `Created cloud profile: ${profile.id}`);
|
|
1783
|
+
}
|
|
1784
|
+
return 0;
|
|
1785
|
+
}
|
|
1786
|
+
if (subcommand === 'delete') {
|
|
1787
|
+
const profileId = requireCommandTarget(argv[1], 'Usage: browser-use profile delete <profile-id> --remote');
|
|
1788
|
+
const flags = parseProfileCommandFlags(argv.slice(2));
|
|
1789
|
+
rejectUnexpectedPositionals(flags.positionals, 'Usage: browser-use profile delete <profile-id> --remote');
|
|
1790
|
+
rejectUnsupportedFlags(flags.used_options, ['--json', '--remote'], 'Usage: browser-use profile delete <profile-id> --remote');
|
|
1791
|
+
if (!flags.remote) {
|
|
1792
|
+
throw new Error('Profile delete is only supported with --remote');
|
|
1793
|
+
}
|
|
1794
|
+
await client.delete_profile(profileId);
|
|
1795
|
+
if (flags.json) {
|
|
1796
|
+
writeLine(output, JSON.stringify({ deleted: profileId }, null, 2));
|
|
1797
|
+
}
|
|
1798
|
+
else {
|
|
1799
|
+
writeLine(output, `Deleted cloud profile: ${profileId}`);
|
|
1800
|
+
}
|
|
1801
|
+
return 0;
|
|
1802
|
+
}
|
|
1803
|
+
if (subcommand === 'cookies') {
|
|
1804
|
+
const profileId = requireCommandTarget(argv[1], 'Usage: browser-use profile cookies <profile-id>');
|
|
1805
|
+
const flags = parseProfileCommandFlags(argv.slice(2));
|
|
1806
|
+
rejectUnexpectedPositionals(flags.positionals, 'Usage: browser-use profile cookies <profile-id>');
|
|
1807
|
+
rejectUnsupportedFlags(flags.used_options, ['--json'], 'Usage: browser-use profile cookies <profile-id>');
|
|
1808
|
+
if (flags.remote) {
|
|
1809
|
+
throw new Error('Profile cookies is only supported for local Chrome profiles');
|
|
1810
|
+
}
|
|
1811
|
+
const profile = profileLister().find((entry) => entry.directory === profileId || entry.name === profileId);
|
|
1812
|
+
if (!profile) {
|
|
1813
|
+
throw new Error(`Profile "${profileId}" not found`);
|
|
1814
|
+
}
|
|
1815
|
+
const session = localSessionFactory(profile.directory);
|
|
1816
|
+
await session.start();
|
|
1817
|
+
try {
|
|
1818
|
+
const cookies = (await session.get_cookies?.()) ?? [];
|
|
1819
|
+
const domains = new Map();
|
|
1820
|
+
for (const cookie of cookies) {
|
|
1821
|
+
const domain = normalizeProfileCookieDomain(cookie.domain) || 'unknown';
|
|
1822
|
+
domains.set(domain, (domains.get(domain) ?? 0) + 1);
|
|
1823
|
+
}
|
|
1824
|
+
const sortedDomains = Array.from(domains.entries()).sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]));
|
|
1825
|
+
if (flags.json) {
|
|
1826
|
+
writeLine(output, JSON.stringify({
|
|
1827
|
+
domains: Object.fromEntries(sortedDomains),
|
|
1828
|
+
total_cookies: cookies.length,
|
|
1829
|
+
}, null, 2));
|
|
1830
|
+
}
|
|
1831
|
+
else {
|
|
1832
|
+
const emailSuffix = profile.email ? ` (${profile.email})` : '';
|
|
1833
|
+
writeLine(output, `Loading cookies from: ${profile.name}${emailSuffix}`);
|
|
1834
|
+
writeLine(output, '');
|
|
1835
|
+
writeLine(output, `Cookies by domain (${cookies.length} total):`);
|
|
1836
|
+
sortedDomains.slice(0, 20).forEach(([domain, count]) => {
|
|
1837
|
+
writeLine(output, ` ${domain}: ${count}`);
|
|
1838
|
+
});
|
|
1839
|
+
if (sortedDomains.length > 20) {
|
|
1840
|
+
writeLine(output, ` ... and ${sortedDomains.length - 20} more domains`);
|
|
1841
|
+
}
|
|
1842
|
+
writeLine(output, '');
|
|
1843
|
+
writeLine(output, 'To sync cookies to cloud:');
|
|
1844
|
+
writeLine(output, ` browser-use profile sync --from "${profile.directory}" --domain <domain>`);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
finally {
|
|
1848
|
+
await session.stop?.();
|
|
1849
|
+
}
|
|
1850
|
+
return 0;
|
|
1851
|
+
}
|
|
1852
|
+
if (subcommand === 'sync') {
|
|
1853
|
+
const flags = parseProfileCommandFlags(argv.slice(1));
|
|
1854
|
+
rejectUnexpectedPositionals(flags.positionals, 'Usage: browser-use profile sync --from <profile-id> [--name <name>] [--domain <domain>] [--json]');
|
|
1855
|
+
rejectUnsupportedFlags(flags.used_options, ['--json', '--from', '--name', '--domain'], 'Usage: browser-use profile sync --from <profile-id> [--name <name>] [--domain <domain>] [--json]');
|
|
1856
|
+
const profiles = profileLister();
|
|
1857
|
+
const fromProfile = flags.from_profile?.trim();
|
|
1858
|
+
if (!fromProfile) {
|
|
1859
|
+
writeLine(errorOutput, 'Usage: browser-use profile sync --from <profile-id> [--name <name>] [--domain <domain>] [--json]');
|
|
1860
|
+
if (profiles.length > 0) {
|
|
1861
|
+
writeLine(errorOutput, 'Available local profiles:');
|
|
1862
|
+
profiles.forEach((profile) => {
|
|
1863
|
+
const emailSuffix = profile.email ? ` (${profile.email})` : '';
|
|
1864
|
+
writeLine(errorOutput, ` ${profile.directory}: ${profile.name}${emailSuffix}`);
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
return 1;
|
|
1868
|
+
}
|
|
1869
|
+
const profile = profiles.find((entry) => entry.directory === fromProfile || entry.name === fromProfile);
|
|
1870
|
+
if (!profile) {
|
|
1871
|
+
throw new Error(`Profile "${fromProfile}" not found`);
|
|
1872
|
+
}
|
|
1873
|
+
const progress = flags.json ? errorOutput : output;
|
|
1874
|
+
const logProgress = (message) => writeLine(progress, message);
|
|
1875
|
+
const cloudName = flags.name ??
|
|
1876
|
+
(flags.domain
|
|
1877
|
+
? `Chrome - ${profile.name} (${flags.domain})`
|
|
1878
|
+
: `Chrome - ${profile.name}`);
|
|
1879
|
+
let cloudProfileId = null;
|
|
1880
|
+
let cloudBrowserSessionId = null;
|
|
1881
|
+
const cloudBrowserClient = cloudBrowserClientFactory();
|
|
1882
|
+
try {
|
|
1883
|
+
logProgress(`Syncing: ${profile.name}${profile.email ? ` (${profile.email})` : ''}`);
|
|
1884
|
+
logProgress(' Creating cloud profile...');
|
|
1885
|
+
const cloudProfile = await client.create_profile({ name: cloudName });
|
|
1886
|
+
cloudProfileId = cloudProfile.id;
|
|
1887
|
+
logProgress(` Created: ${cloudProfileId}`);
|
|
1888
|
+
logProgress(' Exporting cookies from local profile...');
|
|
1889
|
+
const localSession = localSessionFactory(profile.directory);
|
|
1890
|
+
let filteredCookies = [];
|
|
1891
|
+
await localSession.start();
|
|
1892
|
+
try {
|
|
1893
|
+
const cookies = ((await localSession.get_cookies?.()) ??
|
|
1894
|
+
[]);
|
|
1895
|
+
filteredCookies = cookies.filter((cookie) => cookieMatchesDomainFilter(cookie, flags.domain));
|
|
1896
|
+
}
|
|
1897
|
+
finally {
|
|
1898
|
+
await localSession.stop?.();
|
|
1899
|
+
}
|
|
1900
|
+
if (filteredCookies.length === 0) {
|
|
1901
|
+
throw new Error(flags.domain
|
|
1902
|
+
? `No cookies found for domain: ${flags.domain}`
|
|
1903
|
+
: 'No cookies found in local profile');
|
|
1904
|
+
}
|
|
1905
|
+
logProgress(` Found ${filteredCookies.length} cookies`);
|
|
1906
|
+
logProgress(' Importing cookies to cloud profile...');
|
|
1907
|
+
const cloudBrowser = await cloudBrowserClient.create_browser({
|
|
1908
|
+
profile_id: cloudProfileId,
|
|
1909
|
+
});
|
|
1910
|
+
cloudBrowserSessionId = cloudBrowser.id;
|
|
1911
|
+
if (!cloudBrowser.cdpUrl) {
|
|
1912
|
+
throw new Error('Cloud browser did not return a CDP URL');
|
|
1913
|
+
}
|
|
1914
|
+
const remoteSession = remoteSessionFactory({
|
|
1915
|
+
cdp_url: cloudBrowser.cdpUrl,
|
|
1916
|
+
});
|
|
1917
|
+
await remoteSession.start();
|
|
1918
|
+
try {
|
|
1919
|
+
if (!remoteSession.browser_context?.addCookies) {
|
|
1920
|
+
throw new Error('Remote browser context does not support addCookies');
|
|
1921
|
+
}
|
|
1922
|
+
await remoteSession.browser_context.addCookies(filteredCookies);
|
|
1923
|
+
}
|
|
1924
|
+
finally {
|
|
1925
|
+
await remoteSession.stop?.();
|
|
1926
|
+
}
|
|
1927
|
+
await cloudBrowserClient.stop_browser(cloudBrowserSessionId);
|
|
1928
|
+
cloudBrowserSessionId = null;
|
|
1929
|
+
if (flags.json) {
|
|
1930
|
+
writeLine(output, JSON.stringify({
|
|
1931
|
+
success: true,
|
|
1932
|
+
profile_id: cloudProfileId,
|
|
1933
|
+
cookies_synced: filteredCookies.length,
|
|
1934
|
+
}, null, 2));
|
|
1935
|
+
}
|
|
1936
|
+
else {
|
|
1937
|
+
logProgress('Profile synced successfully!');
|
|
1938
|
+
logProgress(` Cloud profile ID: ${cloudProfileId}`);
|
|
1939
|
+
}
|
|
1940
|
+
return 0;
|
|
1941
|
+
}
|
|
1942
|
+
catch (error) {
|
|
1943
|
+
if (cloudBrowserSessionId) {
|
|
1944
|
+
try {
|
|
1945
|
+
await cloudBrowserClient.stop_browser(cloudBrowserSessionId);
|
|
1946
|
+
}
|
|
1947
|
+
catch {
|
|
1948
|
+
// Ignore cleanup failures.
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
if (cloudProfileId) {
|
|
1952
|
+
try {
|
|
1953
|
+
await client.delete_profile(cloudProfileId);
|
|
1954
|
+
}
|
|
1955
|
+
catch {
|
|
1956
|
+
// Ignore cleanup failures.
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
throw error;
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
if (subcommand === 'update') {
|
|
1963
|
+
const profileId = requireCommandTarget(argv[1], 'Usage: browser-use profile update <profile-id> --remote --name <name>');
|
|
1964
|
+
const flags = parseProfileCommandFlags(argv.slice(2));
|
|
1965
|
+
rejectUnexpectedPositionals(flags.positionals, 'Usage: browser-use profile update <profile-id> --remote --name <name>');
|
|
1966
|
+
rejectUnsupportedFlags(flags.used_options, ['--json', '--remote', '--name'], 'Usage: browser-use profile update <profile-id> --remote --name <name>');
|
|
1967
|
+
if (!flags.remote) {
|
|
1968
|
+
throw new Error('Profile update is only supported with --remote');
|
|
1969
|
+
}
|
|
1970
|
+
if (!flags.name) {
|
|
1971
|
+
throw new Error('Usage: browser-use profile update <profile-id> --remote --name <name>');
|
|
1972
|
+
}
|
|
1973
|
+
const profile = await client.update_profile(profileId, {
|
|
1974
|
+
name: flags.name,
|
|
1975
|
+
});
|
|
1976
|
+
if (flags.json) {
|
|
1977
|
+
writeLine(output, JSON.stringify(profile, null, 2));
|
|
1978
|
+
}
|
|
1979
|
+
else {
|
|
1980
|
+
writeLine(output, `Updated cloud profile: ${profile.id}`);
|
|
1981
|
+
}
|
|
1982
|
+
return 0;
|
|
1983
|
+
}
|
|
1984
|
+
writeLine(errorOutput, 'Usage: browser-use profile <list|get|create|update|delete|cookies|sync> [--remote] [options]');
|
|
1985
|
+
return 1;
|
|
1986
|
+
}
|
|
1987
|
+
catch (error) {
|
|
1988
|
+
writeLine(errorOutput, `Error: ${error.message}`);
|
|
1989
|
+
return 1;
|
|
1990
|
+
}
|
|
1991
|
+
};
|
|
1992
|
+
const CLOUD_RUN_FLAGS = new Set([
|
|
1993
|
+
'--remote',
|
|
1994
|
+
'--llm',
|
|
1995
|
+
'--session-id',
|
|
1996
|
+
'--proxy-country',
|
|
1997
|
+
'--wait',
|
|
1998
|
+
'--stream',
|
|
1999
|
+
'--flash',
|
|
2000
|
+
'--thinking',
|
|
2001
|
+
'--vision',
|
|
2002
|
+
'--no-vision',
|
|
2003
|
+
'--start-url',
|
|
2004
|
+
'--metadata',
|
|
2005
|
+
'--secret',
|
|
2006
|
+
'--allowed-domain',
|
|
2007
|
+
'--skill-id',
|
|
2008
|
+
'--structured-output',
|
|
2009
|
+
'--judge',
|
|
2010
|
+
'--judge-ground-truth',
|
|
2011
|
+
'--max-steps',
|
|
2012
|
+
'--profile',
|
|
2013
|
+
]);
|
|
2014
|
+
const CLOUD_RUN_VALUE_FLAGS = new Set([
|
|
2015
|
+
'--llm',
|
|
2016
|
+
'--session-id',
|
|
2017
|
+
'--proxy-country',
|
|
2018
|
+
'--start-url',
|
|
2019
|
+
'--structured-output',
|
|
2020
|
+
'--judge-ground-truth',
|
|
2021
|
+
'--max-steps',
|
|
2022
|
+
'--profile',
|
|
2023
|
+
'--metadata',
|
|
2024
|
+
'--secret',
|
|
2025
|
+
'--allowed-domain',
|
|
2026
|
+
'--skill-id',
|
|
2027
|
+
]);
|
|
2028
|
+
export const hasCloudRunFlags = (argv) => {
|
|
2029
|
+
for (const arg of argv) {
|
|
2030
|
+
if (arg === '--') {
|
|
2031
|
+
break;
|
|
2032
|
+
}
|
|
2033
|
+
if (CLOUD_RUN_FLAGS.has(arg)) {
|
|
2034
|
+
return true;
|
|
2035
|
+
}
|
|
2036
|
+
const separator = arg.indexOf('=');
|
|
2037
|
+
if (separator > 0 && CLOUD_RUN_VALUE_FLAGS.has(arg.slice(0, separator))) {
|
|
2038
|
+
return true;
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
return false;
|
|
2042
|
+
};
|
|
2043
|
+
const hasExplicitRemoteRunFlag = (argv) => {
|
|
2044
|
+
for (const arg of argv) {
|
|
2045
|
+
if (arg === '--') {
|
|
2046
|
+
break;
|
|
2047
|
+
}
|
|
2048
|
+
if (arg === '--remote') {
|
|
2049
|
+
return true;
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
return false;
|
|
2053
|
+
};
|
|
2054
|
+
const PREFIXED_SUBCOMMAND_VALUE_FLAGS = [
|
|
2055
|
+
'--api-key',
|
|
2056
|
+
'--provider',
|
|
2057
|
+
'--model',
|
|
2058
|
+
'--window-width',
|
|
2059
|
+
'--window-height',
|
|
2060
|
+
'--user-data-dir',
|
|
2061
|
+
'--profile-directory',
|
|
2062
|
+
'--allowed-domains',
|
|
2063
|
+
'--proxy-url',
|
|
2064
|
+
'--no-proxy',
|
|
2065
|
+
'--proxy-username',
|
|
2066
|
+
'--proxy-password',
|
|
2067
|
+
'--cdp-url',
|
|
2068
|
+
];
|
|
2069
|
+
const matchesOptionWithValue = (arg, option) => arg === option || arg.startsWith(`${option}=`);
|
|
2070
|
+
export const extractPrefixedSubcommand = (argv) => {
|
|
2071
|
+
const forwardedArgs = [];
|
|
2072
|
+
let debug = false;
|
|
2073
|
+
let index = 0;
|
|
2074
|
+
while (index < argv.length) {
|
|
2075
|
+
const arg = argv[index] ?? '';
|
|
2076
|
+
if (arg === '--debug') {
|
|
2077
|
+
debug = true;
|
|
2078
|
+
index += 1;
|
|
2079
|
+
continue;
|
|
2080
|
+
}
|
|
2081
|
+
if (arg === '--json') {
|
|
2082
|
+
forwardedArgs.push(arg);
|
|
2083
|
+
index += 1;
|
|
2084
|
+
continue;
|
|
2085
|
+
}
|
|
2086
|
+
if (arg === '--headless') {
|
|
2087
|
+
index += 1;
|
|
2088
|
+
continue;
|
|
2089
|
+
}
|
|
2090
|
+
const valueOption = PREFIXED_SUBCOMMAND_VALUE_FLAGS.find((option) => matchesOptionWithValue(arg, option));
|
|
2091
|
+
if (valueOption) {
|
|
2092
|
+
if (arg === valueOption) {
|
|
2093
|
+
if (index + 1 >= argv.length) {
|
|
2094
|
+
break;
|
|
2095
|
+
}
|
|
2096
|
+
index += 2;
|
|
2097
|
+
}
|
|
2098
|
+
else {
|
|
2099
|
+
index += 1;
|
|
2100
|
+
}
|
|
2101
|
+
continue;
|
|
2102
|
+
}
|
|
2103
|
+
break;
|
|
2104
|
+
}
|
|
2105
|
+
const command = argv[index];
|
|
2106
|
+
if (command !== 'run' &&
|
|
2107
|
+
command !== 'task' &&
|
|
2108
|
+
command !== 'session' &&
|
|
2109
|
+
command !== 'profile') {
|
|
2110
|
+
return null;
|
|
2111
|
+
}
|
|
2112
|
+
return {
|
|
2113
|
+
command,
|
|
2114
|
+
argv: argv.slice(index + 1),
|
|
2115
|
+
debug,
|
|
2116
|
+
forwardedArgs,
|
|
2117
|
+
};
|
|
2118
|
+
};
|
|
2119
|
+
const parseKeyValuePairs = (values, option) => {
|
|
2120
|
+
const result = {};
|
|
2121
|
+
values.forEach((value) => {
|
|
2122
|
+
const separator = value.indexOf('=');
|
|
2123
|
+
if (separator <= 0) {
|
|
2124
|
+
throw new Error(`Invalid value for ${option}: expected KEY=VALUE`);
|
|
2125
|
+
}
|
|
2126
|
+
const key = value.slice(0, separator).trim();
|
|
2127
|
+
if (!key) {
|
|
2128
|
+
throw new Error(`Invalid value for ${option}: expected KEY=VALUE`);
|
|
2129
|
+
}
|
|
2130
|
+
result[key] = value.slice(separator + 1);
|
|
2131
|
+
});
|
|
2132
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
2133
|
+
};
|
|
2134
|
+
const parseCloudRunArgs = (argv) => {
|
|
2135
|
+
const flags = {
|
|
2136
|
+
remote: false,
|
|
2137
|
+
wait: false,
|
|
2138
|
+
stream: false,
|
|
2139
|
+
flash: false,
|
|
2140
|
+
thinking: false,
|
|
2141
|
+
vision: null,
|
|
2142
|
+
llm: null,
|
|
2143
|
+
session_id: null,
|
|
2144
|
+
proxy_country: null,
|
|
2145
|
+
start_url: null,
|
|
2146
|
+
structured_output: null,
|
|
2147
|
+
judge: false,
|
|
2148
|
+
judge_ground_truth: null,
|
|
2149
|
+
max_steps: null,
|
|
2150
|
+
profile: null,
|
|
2151
|
+
metadata: [],
|
|
2152
|
+
secret: [],
|
|
2153
|
+
allowed_domain: [],
|
|
2154
|
+
skill_id: [],
|
|
2155
|
+
task_parts: [],
|
|
2156
|
+
};
|
|
2157
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
2158
|
+
const rawArg = argv[index] ?? '';
|
|
2159
|
+
if (rawArg === '--') {
|
|
2160
|
+
flags.task_parts.push(...argv.slice(index + 1));
|
|
2161
|
+
break;
|
|
2162
|
+
}
|
|
2163
|
+
const separator = rawArg.indexOf('=');
|
|
2164
|
+
const hasInlineValue = separator > 0;
|
|
2165
|
+
const arg = hasInlineValue ? rawArg.slice(0, separator) : rawArg;
|
|
2166
|
+
const inlineValue = hasInlineValue
|
|
2167
|
+
? rawArg.slice(separator + 1).trim()
|
|
2168
|
+
: '';
|
|
2169
|
+
if (arg === '--remote') {
|
|
2170
|
+
flags.remote = true;
|
|
2171
|
+
continue;
|
|
2172
|
+
}
|
|
2173
|
+
if (arg === '--wait') {
|
|
2174
|
+
flags.wait = true;
|
|
2175
|
+
continue;
|
|
2176
|
+
}
|
|
2177
|
+
if (arg === '--stream') {
|
|
2178
|
+
flags.stream = true;
|
|
2179
|
+
continue;
|
|
2180
|
+
}
|
|
2181
|
+
if (arg === '--flash') {
|
|
2182
|
+
flags.flash = true;
|
|
2183
|
+
continue;
|
|
2184
|
+
}
|
|
2185
|
+
if (arg === '--thinking') {
|
|
2186
|
+
flags.thinking = true;
|
|
2187
|
+
continue;
|
|
2188
|
+
}
|
|
2189
|
+
if (arg === '--vision') {
|
|
2190
|
+
flags.vision = true;
|
|
2191
|
+
continue;
|
|
2192
|
+
}
|
|
2193
|
+
if (arg === '--no-vision') {
|
|
2194
|
+
flags.vision = false;
|
|
2195
|
+
continue;
|
|
2196
|
+
}
|
|
2197
|
+
if (arg === '--judge') {
|
|
2198
|
+
flags.judge = true;
|
|
2199
|
+
continue;
|
|
2200
|
+
}
|
|
2201
|
+
if (arg === '--llm' ||
|
|
2202
|
+
arg === '--session-id' ||
|
|
2203
|
+
arg === '--proxy-country' ||
|
|
2204
|
+
arg === '--start-url' ||
|
|
2205
|
+
arg === '--structured-output' ||
|
|
2206
|
+
arg === '--judge-ground-truth' ||
|
|
2207
|
+
arg === '--max-steps' ||
|
|
2208
|
+
arg === '--profile' ||
|
|
2209
|
+
arg === '--metadata' ||
|
|
2210
|
+
arg === '--secret' ||
|
|
2211
|
+
arg === '--allowed-domain' ||
|
|
2212
|
+
arg === '--skill-id') {
|
|
2213
|
+
const { value: next, nextIndex } = takeOptionValue(rawArg, index, argv);
|
|
2214
|
+
if (arg === '--llm') {
|
|
2215
|
+
flags.llm = next;
|
|
2216
|
+
}
|
|
2217
|
+
else if (arg === '--session-id') {
|
|
2218
|
+
flags.session_id = next;
|
|
2219
|
+
}
|
|
2220
|
+
else if (arg === '--proxy-country') {
|
|
2221
|
+
flags.proxy_country = next;
|
|
2222
|
+
}
|
|
2223
|
+
else if (arg === '--start-url') {
|
|
2224
|
+
flags.start_url = next;
|
|
2225
|
+
}
|
|
2226
|
+
else if (arg === '--structured-output') {
|
|
2227
|
+
flags.structured_output = next;
|
|
2228
|
+
}
|
|
2229
|
+
else if (arg === '--judge-ground-truth') {
|
|
2230
|
+
flags.judge_ground_truth = next;
|
|
2231
|
+
}
|
|
2232
|
+
else if (arg === '--max-steps') {
|
|
2233
|
+
flags.max_steps = parsePositiveInt('--max-steps', next);
|
|
2234
|
+
}
|
|
2235
|
+
else if (arg === '--profile') {
|
|
2236
|
+
flags.profile = next;
|
|
2237
|
+
}
|
|
2238
|
+
else if (arg === '--metadata') {
|
|
2239
|
+
flags.metadata.push(next);
|
|
2240
|
+
}
|
|
2241
|
+
else if (arg === '--secret') {
|
|
2242
|
+
flags.secret.push(next);
|
|
2243
|
+
}
|
|
2244
|
+
else if (arg === '--allowed-domain') {
|
|
2245
|
+
flags.allowed_domain.push(next);
|
|
2246
|
+
}
|
|
2247
|
+
else {
|
|
2248
|
+
flags.skill_id.push(next);
|
|
2249
|
+
}
|
|
2250
|
+
index = nextIndex;
|
|
2251
|
+
continue;
|
|
2252
|
+
}
|
|
2253
|
+
if (rawArg.startsWith('-')) {
|
|
2254
|
+
throw new Error(`Unknown option: ${rawArg}`);
|
|
2255
|
+
}
|
|
2256
|
+
flags.task_parts.push(rawArg);
|
|
2257
|
+
}
|
|
2258
|
+
return flags;
|
|
2259
|
+
};
|
|
2260
|
+
export const runCloudTaskCommand = async (argv, options = {}) => {
|
|
2261
|
+
const client = options.client ?? new CloudManagementClient();
|
|
2262
|
+
const output = options.stdout ?? process.stdout;
|
|
2263
|
+
const errorOutput = options.stderr ?? process.stderr;
|
|
2264
|
+
const sleepImpl = options.sleep_impl ??
|
|
2265
|
+
((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
2266
|
+
let autoCreatedSessionId = null;
|
|
2267
|
+
let taskCreated = false;
|
|
2268
|
+
try {
|
|
2269
|
+
const flags = parseCloudRunArgs(argv);
|
|
2270
|
+
if (!flags.remote) {
|
|
2271
|
+
throw new Error('Usage: browser-use run --remote <task>');
|
|
2272
|
+
}
|
|
2273
|
+
const task = flags.task_parts.join(' ').trim();
|
|
2274
|
+
if (!task) {
|
|
2275
|
+
throw new Error('Usage: browser-use run --remote <task>');
|
|
2276
|
+
}
|
|
2277
|
+
let sessionId = flags.session_id;
|
|
2278
|
+
if (!sessionId && (flags.profile || flags.proxy_country)) {
|
|
2279
|
+
const session = await client.create_session({
|
|
2280
|
+
profileId: flags.profile,
|
|
2281
|
+
proxyCountryCode: flags.proxy_country,
|
|
2282
|
+
startUrl: flags.start_url,
|
|
2283
|
+
});
|
|
2284
|
+
sessionId = session.id;
|
|
2285
|
+
autoCreatedSessionId = session.id;
|
|
2286
|
+
writeLine(output, `Created cloud session: ${sessionId}`);
|
|
2287
|
+
}
|
|
2288
|
+
const created = await client.create_task({
|
|
2289
|
+
task,
|
|
2290
|
+
llm: flags.llm,
|
|
2291
|
+
sessionId,
|
|
2292
|
+
startUrl: flags.start_url,
|
|
2293
|
+
maxSteps: flags.max_steps,
|
|
2294
|
+
structuredOutput: flags.structured_output,
|
|
2295
|
+
metadata: parseKeyValuePairs(flags.metadata, '--metadata'),
|
|
2296
|
+
secrets: parseKeyValuePairs(flags.secret, '--secret'),
|
|
2297
|
+
allowedDomains: flags.allowed_domain.length > 0 ? flags.allowed_domain : null,
|
|
2298
|
+
flashMode: flags.flash,
|
|
2299
|
+
thinking: flags.thinking,
|
|
2300
|
+
vision: flags.vision,
|
|
2301
|
+
judge: flags.judge,
|
|
2302
|
+
judgeGroundTruth: flags.judge_ground_truth,
|
|
2303
|
+
skillIds: flags.skill_id.length > 0 ? flags.skill_id : null,
|
|
2304
|
+
});
|
|
2305
|
+
taskCreated = true;
|
|
2306
|
+
if (!flags.wait) {
|
|
2307
|
+
writeLine(output, `Task started: ${created.id} (session: ${created.sessionId})`);
|
|
2308
|
+
writeLine(output, `Use "browser-use task status ${created.id}" to check progress.`);
|
|
2309
|
+
return 0;
|
|
2310
|
+
}
|
|
2311
|
+
let lastStatus = null;
|
|
2312
|
+
while (true) {
|
|
2313
|
+
const taskView = await client.get_task(created.id);
|
|
2314
|
+
if (flags.stream && taskView.status !== lastStatus) {
|
|
2315
|
+
writeLine(output, `Status: ${taskView.status}`);
|
|
2316
|
+
lastStatus = taskView.status;
|
|
2317
|
+
}
|
|
2318
|
+
if (taskView.status === 'finished' ||
|
|
2319
|
+
taskView.status === 'stopped' ||
|
|
2320
|
+
taskView.status === 'failed') {
|
|
2321
|
+
writeLine(output, `Task ${taskView.status}: ${taskView.id}`);
|
|
2322
|
+
if (taskView.output) {
|
|
2323
|
+
writeLine(output, taskView.output);
|
|
2324
|
+
}
|
|
2325
|
+
return taskView.status === 'finished' ? 0 : 1;
|
|
2326
|
+
}
|
|
2327
|
+
await sleepImpl(1000);
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
catch (error) {
|
|
2331
|
+
if (autoCreatedSessionId && !taskCreated) {
|
|
2332
|
+
try {
|
|
2333
|
+
await client.update_session(autoCreatedSessionId, 'stop');
|
|
2334
|
+
}
|
|
2335
|
+
catch {
|
|
2336
|
+
// Ignore cleanup failures.
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
writeLine(errorOutput, `Error: ${error.message}`);
|
|
2340
|
+
return 1;
|
|
2341
|
+
}
|
|
2342
|
+
};
|
|
2343
|
+
const enableDebugLogging = () => {
|
|
2344
|
+
process.env.BROWSER_USE_LOGGING_LEVEL = 'debug';
|
|
2345
|
+
setupLogging({ logLevel: 'debug', forceSetup: true });
|
|
2346
|
+
};
|
|
2347
|
+
const summarizeDoctorChecks = (checks) => {
|
|
2348
|
+
const values = Object.values(checks);
|
|
2349
|
+
const total = values.length;
|
|
2350
|
+
const ok = values.filter((check) => check.status === 'ok').length;
|
|
2351
|
+
const warning = values.filter((check) => check.status === 'warning').length;
|
|
2352
|
+
const missing = values.filter((check) => check.status === 'missing').length;
|
|
2353
|
+
const error = values.filter((check) => check.status === 'error').length;
|
|
2354
|
+
const parts = [`${ok}/${total} checks passed`];
|
|
2355
|
+
if (warning > 0) {
|
|
2356
|
+
parts.push(`${warning} warnings`);
|
|
2357
|
+
}
|
|
2358
|
+
if (missing > 0) {
|
|
2359
|
+
parts.push(`${missing} missing`);
|
|
2360
|
+
}
|
|
2361
|
+
if (error > 0) {
|
|
2362
|
+
parts.push(`${error} errors`);
|
|
2363
|
+
}
|
|
2364
|
+
return parts.join(', ');
|
|
2365
|
+
};
|
|
2366
|
+
const findSystemBinary = (binary) => {
|
|
2367
|
+
const command = process.platform === 'win32' ? 'where' : 'which';
|
|
2368
|
+
const result = spawnSync(command, [binary], {
|
|
2369
|
+
encoding: 'utf8',
|
|
2370
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
2371
|
+
});
|
|
2372
|
+
if (result.status !== 0) {
|
|
2373
|
+
return null;
|
|
2374
|
+
}
|
|
2375
|
+
const firstLine = result.stdout
|
|
2376
|
+
.split(/\r?\n/)
|
|
2377
|
+
.map((line) => line.trim())
|
|
2378
|
+
.find(Boolean);
|
|
2379
|
+
return firstLine ?? null;
|
|
2380
|
+
};
|
|
2381
|
+
export const runDoctorChecks = async (options = {}) => {
|
|
2382
|
+
const fetchImpl = options.fetch_impl ?? fetch;
|
|
2383
|
+
const configuredApiKey = options.api_key !== undefined
|
|
2384
|
+
? options.api_key?.trim() || null
|
|
2385
|
+
: process.env.BROWSER_USE_API_KEY?.trim() ||
|
|
2386
|
+
new DeviceAuthClient().api_token?.trim() ||
|
|
2387
|
+
null;
|
|
2388
|
+
const apiKeySource = options.api_key !== undefined
|
|
2389
|
+
? 'argument'
|
|
2390
|
+
: process.env.BROWSER_USE_API_KEY?.trim()
|
|
2391
|
+
? 'environment'
|
|
2392
|
+
: new DeviceAuthClient().api_token?.trim()
|
|
2393
|
+
? 'cloud_auth'
|
|
2394
|
+
: null;
|
|
2395
|
+
const tunnelStatus = options.cloudflared_path !== undefined
|
|
2396
|
+
? options.cloudflared_path
|
|
2397
|
+
? {
|
|
2398
|
+
available: true,
|
|
2399
|
+
source: 'system',
|
|
2400
|
+
path: options.cloudflared_path,
|
|
2401
|
+
note: 'cloudflared installed',
|
|
2402
|
+
}
|
|
2403
|
+
: {
|
|
2404
|
+
available: false,
|
|
2405
|
+
source: null,
|
|
2406
|
+
path: null,
|
|
2407
|
+
note: 'cloudflared not installed - install it manually before using tunnel',
|
|
2408
|
+
}
|
|
2409
|
+
: get_tunnel_manager().get_status();
|
|
2410
|
+
const checks = {
|
|
2411
|
+
package: {
|
|
2412
|
+
status: 'ok',
|
|
2413
|
+
message: `browser-use ${options.version ?? get_browser_use_version()}`,
|
|
2414
|
+
},
|
|
2415
|
+
browser: (() => {
|
|
2416
|
+
const executable = options.browser_executable !== undefined
|
|
2417
|
+
? options.browser_executable
|
|
2418
|
+
: systemChrome.findExecutable();
|
|
2419
|
+
if (executable) {
|
|
2420
|
+
return {
|
|
2421
|
+
status: 'ok',
|
|
2422
|
+
message: `Chrome detected at ${executable}`,
|
|
2423
|
+
};
|
|
2424
|
+
}
|
|
2425
|
+
return {
|
|
2426
|
+
status: 'warning',
|
|
2427
|
+
message: 'Chrome executable not detected',
|
|
2428
|
+
note: 'Local browser launch may fail until Chrome or Chromium is installed.',
|
|
2429
|
+
};
|
|
2430
|
+
})(),
|
|
2431
|
+
api_key: (() => {
|
|
2432
|
+
if (configuredApiKey) {
|
|
2433
|
+
return {
|
|
2434
|
+
status: 'ok',
|
|
2435
|
+
message: apiKeySource === 'cloud_auth'
|
|
2436
|
+
? 'Browser Use API key is configured in cloud auth'
|
|
2437
|
+
: 'BROWSER_USE_API_KEY is configured',
|
|
2438
|
+
};
|
|
2439
|
+
}
|
|
2440
|
+
return {
|
|
2441
|
+
status: 'missing',
|
|
2442
|
+
message: 'BROWSER_USE_API_KEY is not configured',
|
|
2443
|
+
note: 'Required for browser-use cloud browser features.',
|
|
2444
|
+
};
|
|
2445
|
+
})(),
|
|
2446
|
+
cloudflared: (() => {
|
|
2447
|
+
if (tunnelStatus.available && tunnelStatus.path) {
|
|
2448
|
+
return {
|
|
2449
|
+
status: 'ok',
|
|
2450
|
+
message: `cloudflared detected at ${tunnelStatus.path}`,
|
|
2451
|
+
};
|
|
2452
|
+
}
|
|
2453
|
+
return {
|
|
2454
|
+
status: 'missing',
|
|
2455
|
+
message: 'cloudflared not found',
|
|
2456
|
+
note: tunnelStatus.note ||
|
|
2457
|
+
'Tunnel features remain unavailable until cloudflared is installed.',
|
|
2458
|
+
};
|
|
2459
|
+
})(),
|
|
2460
|
+
network: {
|
|
2461
|
+
status: 'warning',
|
|
2462
|
+
message: 'Network connectivity check inconclusive',
|
|
2463
|
+
note: 'Some remote features may not work offline.',
|
|
2464
|
+
},
|
|
2465
|
+
};
|
|
2466
|
+
try {
|
|
2467
|
+
const response = await fetchImpl('https://api.github.com', {
|
|
2468
|
+
method: 'HEAD',
|
|
2469
|
+
});
|
|
2470
|
+
if (response.ok || response.status < 500) {
|
|
2471
|
+
checks.network = {
|
|
2472
|
+
status: 'ok',
|
|
2473
|
+
message: 'Network connectivity OK',
|
|
2474
|
+
};
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
catch {
|
|
2478
|
+
checks.network = {
|
|
2479
|
+
status: 'warning',
|
|
2480
|
+
message: 'Network connectivity check inconclusive',
|
|
2481
|
+
note: 'Some remote features may not work offline.',
|
|
2482
|
+
};
|
|
2483
|
+
}
|
|
2484
|
+
const allOk = Object.values(checks).every((check) => check.status === 'ok');
|
|
2485
|
+
return {
|
|
2486
|
+
status: allOk ? 'healthy' : 'issues_found',
|
|
2487
|
+
checks,
|
|
2488
|
+
summary: summarizeDoctorChecks(checks),
|
|
2489
|
+
};
|
|
2490
|
+
};
|
|
2491
|
+
const printDoctorReport = (report) => {
|
|
2492
|
+
console.log(`status: ${report.status}`);
|
|
2493
|
+
console.log(`summary: ${report.summary}`);
|
|
2494
|
+
for (const [name, check] of Object.entries(report.checks)) {
|
|
2495
|
+
console.log(`${name}: ${check.status} - ${check.message}`);
|
|
2496
|
+
if (check.note) {
|
|
2497
|
+
console.log(` note: ${check.note}`);
|
|
2498
|
+
}
|
|
2499
|
+
if (check.fix) {
|
|
2500
|
+
console.log(` fix: ${check.fix}`);
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
};
|
|
768
2504
|
async function runMcpServer() {
|
|
769
2505
|
const server = new MCPServer('browser-use', get_browser_use_version());
|
|
770
2506
|
await server.start();
|
|
@@ -777,6 +2513,50 @@ async function runMcpServer() {
|
|
|
777
2513
|
await new Promise(() => { });
|
|
778
2514
|
}
|
|
779
2515
|
export async function main(argv = process.argv.slice(2)) {
|
|
2516
|
+
const prefixedSubcommand = extractPrefixedSubcommand(argv);
|
|
2517
|
+
if (prefixedSubcommand) {
|
|
2518
|
+
if (prefixedSubcommand.debug) {
|
|
2519
|
+
enableDebugLogging();
|
|
2520
|
+
}
|
|
2521
|
+
if (prefixedSubcommand.command === 'run') {
|
|
2522
|
+
if (!hasCloudRunFlags(prefixedSubcommand.argv) ||
|
|
2523
|
+
!hasExplicitRemoteRunFlag(prefixedSubcommand.argv)) {
|
|
2524
|
+
await main([
|
|
2525
|
+
...prefixedSubcommand.forwardedArgs,
|
|
2526
|
+
...prefixedSubcommand.argv,
|
|
2527
|
+
]);
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2530
|
+
const exitCode = await runCloudTaskCommand(prefixedSubcommand.argv);
|
|
2531
|
+
if (exitCode !== 0) {
|
|
2532
|
+
process.exit(exitCode);
|
|
2533
|
+
}
|
|
2534
|
+
return;
|
|
2535
|
+
}
|
|
2536
|
+
const subcommandArgv = [
|
|
2537
|
+
...prefixedSubcommand.argv,
|
|
2538
|
+
...prefixedSubcommand.forwardedArgs,
|
|
2539
|
+
];
|
|
2540
|
+
if (prefixedSubcommand.command === 'task') {
|
|
2541
|
+
const exitCode = await runTaskCommand(subcommandArgv);
|
|
2542
|
+
if (exitCode !== 0) {
|
|
2543
|
+
process.exit(exitCode);
|
|
2544
|
+
}
|
|
2545
|
+
return;
|
|
2546
|
+
}
|
|
2547
|
+
if (prefixedSubcommand.command === 'session') {
|
|
2548
|
+
const exitCode = await runSessionCommand(subcommandArgv);
|
|
2549
|
+
if (exitCode !== 0) {
|
|
2550
|
+
process.exit(exitCode);
|
|
2551
|
+
}
|
|
2552
|
+
return;
|
|
2553
|
+
}
|
|
2554
|
+
const exitCode = await runProfileCommand(subcommandArgv);
|
|
2555
|
+
if (exitCode !== 0) {
|
|
2556
|
+
process.exit(exitCode);
|
|
2557
|
+
}
|
|
2558
|
+
return;
|
|
2559
|
+
}
|
|
780
2560
|
let args;
|
|
781
2561
|
try {
|
|
782
2562
|
args = parseCliArgs(argv);
|
|
@@ -796,13 +2576,45 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
796
2576
|
return;
|
|
797
2577
|
}
|
|
798
2578
|
if (args.debug) {
|
|
799
|
-
|
|
800
|
-
setupLogging({ logLevel: 'debug', forceSetup: true });
|
|
2579
|
+
enableDebugLogging();
|
|
801
2580
|
}
|
|
802
2581
|
if (args.mcp) {
|
|
803
2582
|
await runMcpServer();
|
|
804
2583
|
return;
|
|
805
2584
|
}
|
|
2585
|
+
if (args.prompt == null && args.positional[0] === 'doctor') {
|
|
2586
|
+
const report = await runDoctorChecks();
|
|
2587
|
+
printDoctorReport(report);
|
|
2588
|
+
return;
|
|
2589
|
+
}
|
|
2590
|
+
if (args.prompt == null && args.positional[0] === 'install') {
|
|
2591
|
+
console.log('Installing Chromium browser...');
|
|
2592
|
+
runInstallCommand();
|
|
2593
|
+
console.log('Chromium browser installed.');
|
|
2594
|
+
return;
|
|
2595
|
+
}
|
|
2596
|
+
if (args.prompt == null && args.positional[0] === 'setup') {
|
|
2597
|
+
const exitCode = await runSetupCommand({
|
|
2598
|
+
mode: args.setup_mode,
|
|
2599
|
+
yes: args.yes,
|
|
2600
|
+
api_key: args.api_key,
|
|
2601
|
+
}, {
|
|
2602
|
+
json_output: args.json,
|
|
2603
|
+
});
|
|
2604
|
+
if (exitCode !== 0) {
|
|
2605
|
+
process.exit(exitCode);
|
|
2606
|
+
}
|
|
2607
|
+
return;
|
|
2608
|
+
}
|
|
2609
|
+
if (args.prompt == null && args.positional[0] === 'tunnel') {
|
|
2610
|
+
const exitCode = await runTunnelCommand(args.positional.slice(1), {
|
|
2611
|
+
json_output: args.json,
|
|
2612
|
+
});
|
|
2613
|
+
if (exitCode !== 0) {
|
|
2614
|
+
process.exit(exitCode);
|
|
2615
|
+
}
|
|
2616
|
+
return;
|
|
2617
|
+
}
|
|
806
2618
|
const task = resolveTask(args);
|
|
807
2619
|
const shouldStartInteractive = shouldStartInteractiveMode(task);
|
|
808
2620
|
if (!task && !shouldStartInteractive) {
|