browse-agent-cli 0.0.5 → 0.0.7

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/clear.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as BASE_DIR, u as loadSession } from "./config.js";
1
+ import { d as loadSession, t as BASE_DIR } from "./config.js";
2
2
  import { existsSync, rmSync } from "node:fs";
3
3
  //#region src/scripts/clear.ts
4
4
  async function clear() {
package/dist/cli.js CHANGED
@@ -1,68 +1,83 @@
1
1
  #!/usr/bin/env node
2
- import { l as loadServiceSession, n as DEFAULT_SERVICE_PORT, t as BASE_DIR } from "./config.js";
2
+ import { f as resolveOptions, n as DEFAULT_PROFILE_PATHS, r as DEFAULT_SERVICE_PORT, s as findBrowser, u as loadServiceSession } from "./config.js";
3
3
  import { t as clear } from "./clear.js";
4
- import { execSync, spawn } from "node:child_process";
5
- import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, statSync, unlinkSync } from "node:fs";
4
+ import { spawn } from "node:child_process";
5
+ import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, statSync } from "node:fs";
6
6
  import { dirname, join, resolve } from "node:path";
7
7
  import { createInterface } from "node:readline/promises";
8
8
  import { fileURLToPath } from "node:url";
9
9
  //#region src/scripts/setup.ts
10
+ const BROWSE_AGENT_WEBSTORE_URL = "https://chromewebstore.google.com/detail/browse-agent/amfbnjpgfgappenkkklkogngmoeofeae?authuser=0&hl=zh-CN";
11
+ const BROWSE_AGENT_EXTENSION_ID = "amfbnjpgfgappenkkklkogngmoeofeae";
12
+ const INSTALL_WAIT_TIMEOUT_MS = 300 * 1e3;
13
+ const INSTALL_POLL_INTERVAL_MS = 1500;
14
+ function openUrlInSelectedBrowser(url) {
15
+ const opts = resolveOptions();
16
+ const proc = spawn(findBrowser(opts.browser), ["--new-window", url], {
17
+ stdio: "ignore",
18
+ detached: true
19
+ });
20
+ proc.on("error", (err) => {
21
+ throw new Error(`${opts.browser} launch failed: ${err.message}`);
22
+ });
23
+ proc.unref();
24
+ }
25
+ function listProfileDirs(userDataDir) {
26
+ const candidates = ["Default"];
27
+ try {
28
+ for (const name of readdirSync(userDataDir)) if (name.startsWith("Profile ")) candidates.push(name);
29
+ } catch {}
30
+ return [...new Set(candidates)].map((name) => join(userDataDir, name));
31
+ }
32
+ function isExtensionInstalledInBrowser(browser) {
33
+ const userDataDir = DEFAULT_PROFILE_PATHS[browser]?.[process.platform];
34
+ if (!userDataDir || !existsSync(userDataDir)) return false;
35
+ for (const profileDir of listProfileDirs(userDataDir)) {
36
+ const extDir = join(profileDir, "Extensions", BROWSE_AGENT_EXTENSION_ID);
37
+ if (!existsSync(extDir)) continue;
38
+ try {
39
+ if (readdirSync(extDir).length > 0) return true;
40
+ } catch {}
41
+ }
42
+ return false;
43
+ }
44
+ function sleep$1(ms) {
45
+ return new Promise((resolve) => setTimeout(resolve, ms));
46
+ }
47
+ async function waitForExtensionInstall(browser, timeoutMs) {
48
+ const deadline = Date.now() + timeoutMs;
49
+ while (Date.now() < deadline) {
50
+ if (isExtensionInstalledInBrowser(browser)) return true;
51
+ await sleep$1(INSTALL_POLL_INTERVAL_MS);
52
+ }
53
+ return false;
54
+ }
10
55
  async function setup() {
11
- const baseDir = BASE_DIR;
12
- const extensionDir = join(baseDir, "extension");
13
- const zipPath = join(baseDir, "extension.zip");
14
- const extensionInstalled = existsSync(join(extensionDir, "manifest.json"));
15
- if (extensionInstalled) {
16
- console.log("browse-agent is already set up.");
17
- console.log(` Extension path: ${extensionDir}`);
18
- console.log(" Extension: installed\n");
56
+ const browser = resolveOptions().browser;
57
+ if (isExtensionInstalledInBrowser(browser)) {
58
+ console.log(`Browse Agent extension is already installed in ${browser}.`);
19
59
  return;
20
60
  }
21
- console.log("Setting up browse-agent...\n");
22
- mkdirSync(baseDir, { recursive: true });
23
- if (extensionInstalled) {
24
- console.log("\n[1/2] Downloading Chrome extension from latest release...");
25
- console.log(" Skipped: extension is already installed.");
26
- console.log("\n[2/2] Extracting extension...");
27
- console.log(" Skipped: extension is already installed.");
28
- } else {
29
- console.log("\n[1/2] Downloading Chrome extension from latest release...");
30
- const releaseJson = execSync(`curl -s "https://api.github.com/repos/imlinhanchao/browse-agent/releases/latest"`).toString();
31
- const asset = JSON.parse(releaseJson).assets?.find((item) => item.name.endsWith(".zip"));
32
- if (!asset) {
33
- console.error("Error: No extension zip found in latest release.");
34
- console.error("Visit https://github.com/imlinhanchao/browse-agent/releases to check.");
35
- process.exit(1);
36
- }
37
- console.log(` Downloading ${asset.name} (${(asset.size / 1024).toFixed(1)} KB)...`);
38
- execSync(`curl -sL -o "${zipPath}" "${asset.browser_download_url}"`);
39
- console.log("\n[2/2] Extracting extension...");
40
- if (existsSync(extensionDir)) execSync(`rm -rf "${extensionDir}"`);
41
- mkdirSync(extensionDir, { recursive: true });
42
- execSync(`unzip -o "${zipPath}" -d "${extensionDir}"`, { stdio: "pipe" });
43
- unlinkSync(zipPath);
44
- const files = readdirSync(extensionDir);
45
- if (!files.includes("manifest.json")) {
46
- const subdirs = files.filter((file) => {
47
- try {
48
- return readdirSync(join(extensionDir, file)).includes("manifest.json");
49
- } catch {
50
- return false;
51
- }
52
- });
53
- if (subdirs.length > 0) {
54
- const subdir = join(extensionDir, subdirs[0]);
55
- execSync(`mv "${subdir}"/* "${extensionDir}"/`);
56
- execSync(`rmdir "${subdir}"`);
57
- } else {
58
- console.error("Error: Extension extraction failed - manifest.json not found.");
59
- process.exit(1);
60
- }
61
+ console.log(`Opening Chrome Web Store install page in ${browser}...`);
62
+ try {
63
+ openUrlInSelectedBrowser(BROWSE_AGENT_WEBSTORE_URL);
64
+ console.log("Please click \"Add to Chrome\" to install Browse Agent.");
65
+ console.log("Browser extensions cannot be silently installed from CLI without enterprise policy.");
66
+ console.log(`If it did not open automatically, visit:\n${BROWSE_AGENT_WEBSTORE_URL}\n`);
67
+ console.log("Waiting for extension installation...");
68
+ if (await waitForExtensionInstall(browser, INSTALL_WAIT_TIMEOUT_MS)) {
69
+ console.log(`Browse Agent extension installation detected in ${browser}.`);
70
+ return;
61
71
  }
72
+ console.error(`Timed out after ${Math.round(INSTALL_WAIT_TIMEOUT_MS / 1e3)}s waiting for extension install in ${browser}.`);
73
+ console.error("Please finish installation in browser, then run \"browse-agent setup\" again.");
74
+ process.exit(1);
75
+ } catch (err) {
76
+ const msg = err instanceof Error ? err.message : String(err);
77
+ console.error(`Failed to open browser automatically: ${msg}`);
78
+ console.error(`Please open manually:\n${BROWSE_AGENT_WEBSTORE_URL}\n`);
79
+ process.exit(1);
62
80
  }
63
- console.log("\nSetup complete!");
64
- console.log(` Extension path: ${extensionDir}`);
65
- console.log(" Extension: installed\n");
66
81
  }
67
82
  //#endregion
68
83
  //#region src/cli.ts
@@ -160,7 +175,7 @@ Examples:
160
175
  `.trim());
161
176
  }
162
177
  function resolveSkillSourceDir() {
163
- return join(dirname(fileURLToPath(import.meta.url)), "..", "skill");
178
+ return join(dirname(fileURLToPath(import.meta.url)), "..", "skills");
164
179
  }
165
180
  function isNonEmptyDirectory(path) {
166
181
  if (!existsSync(path)) return false;
@@ -316,7 +331,7 @@ try {
316
331
  }
317
332
  if (!service.newlyStarted) {
318
333
  const status = await requestService(servicePort, "/status", "GET");
319
- if (status.running === true) {
334
+ if (status.running === true && status.connected === true) {
320
335
  console.log(JSON.stringify({
321
336
  servicePort,
322
337
  skipped: "service-and-browser-already-running",
package/dist/config.js CHANGED
@@ -1,10 +1,14 @@
1
1
  import { execSync } from "node:child_process";
2
- import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
5
5
  //#region src/scripts/config.ts
6
6
  const CLI_RUNTIME_DIR = dirname(fileURLToPath(import.meta.url));
7
7
  const HOME = process.env.HOME || process.env.USERPROFILE || "";
8
+ const DEFAULT_BASE_DIR = HOME ? join(HOME, ".browse-agent") : join(process.cwd(), ".browse-agent");
9
+ function unique(paths) {
10
+ return [...new Set(paths.filter(Boolean))];
11
+ }
8
12
  const BROWSER_PATHS = {
9
13
  chrome: {
10
14
  darwin: ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"],
@@ -63,8 +67,13 @@ const BROWSER_PATHS = {
63
67
  netbsd: []
64
68
  }
65
69
  };
66
- const BASE_DIR = join(CLI_RUNTIME_DIR, ".browse-agent");
70
+ const BASE_DIR = process.env.BROWSE_AGENT_HOME || DEFAULT_BASE_DIR;
67
71
  const DEFAULT_SERVICE_PORT = Number(process.env.BROWSE_AGENT_SERVICE_PORT || 9316);
72
+ unique([
73
+ join(CLI_RUNTIME_DIR, ".browse-agent"),
74
+ join(dirname(CLI_RUNTIME_DIR), ".browse-agent"),
75
+ join(process.cwd(), ".browse-agent")
76
+ ]).filter((dir) => dir !== BASE_DIR);
68
77
  async function importSdk() {
69
78
  try {
70
79
  return await import("browse-agent-sdk");
@@ -187,17 +196,6 @@ function getProfileDir(browser, useUserProfile) {
187
196
  mkdirSync(dir, { recursive: true });
188
197
  return dir;
189
198
  }
190
- function patchExtension(port, secret) {
191
- const extensionSrc = join(BASE_DIR, "extension");
192
- const extensionWork = join(BASE_DIR, "_ext_work");
193
- if (!existsSync(join(extensionSrc, "manifest.json"))) throw new Error(`Extension is not installed at ${extensionSrc}. Run "browse-agent setup" first.`);
194
- if (existsSync(extensionWork)) rmSync(extensionWork, { recursive: true });
195
- cpSync(extensionSrc, extensionWork, { recursive: true });
196
- const swPath = join(extensionWork, "service-worker.js");
197
- const originalSW = readFileSync(swPath, "utf8");
198
- writeFileSync(swPath, [`chrome.storage.local.set({ wsUrl: 'ws://127.0.0.1:${port}', secret: '${secret}' });`, originalSW].join("\n"));
199
- return extensionWork;
200
- }
201
199
  function cleanExtensionWork() {
202
200
  const extensionWork = join(BASE_DIR, "_ext_work");
203
201
  if (existsSync(extensionWork)) rmSync(extensionWork, { recursive: true });
@@ -206,13 +204,13 @@ function resolveOptions(options = {}) {
206
204
  return {
207
205
  browser: options.browser ?? process.env.BROWSER ?? "chrome",
208
206
  headless: options.headless ?? process.env.HEADLESS === "true",
209
- useUserProfile: options.useUserProfile ?? process.env.USE_USER_PROFILE === "true",
207
+ useUserProfile: options.useUserProfile ?? process.env.USE_USER_PROFILE !== "false",
210
208
  port: options.port ?? Number(process.env.BROWSE_AGENT_PORT || 9315),
211
209
  timeout: options.timeout ?? Number(process.env.CONNECTION_TIMEOUT || 3e4),
212
210
  secret: options.secret ?? process.env.SHARED_SECRET ?? ""
213
211
  };
214
212
  }
215
213
  //#endregion
216
- export { clearSession as a, importSdk as c, patchExtension as d, resolveOptions as f, clearServiceSession as i, loadServiceSession as l, saveSession as m, DEFAULT_SERVICE_PORT as n, findBrowser as o, saveServiceSession as p, cleanExtensionWork as r, getProfileDir as s, BASE_DIR as t, loadSession as u };
214
+ export { clearServiceSession as a, getProfileDir as c, loadSession as d, resolveOptions as f, cleanExtensionWork as i, importSdk as l, saveSession as m, DEFAULT_PROFILE_PATHS as n, clearSession as o, saveServiceSession as p, DEFAULT_SERVICE_PORT as r, findBrowser as s, BASE_DIR as t, loadServiceSession as u };
217
215
 
218
216
  //# sourceMappingURL=config.js.map
package/dist/script.js CHANGED
@@ -1,4 +1,4 @@
1
- import { c as importSdk, f as resolveOptions, r as cleanExtensionWork, u as loadSession } from "./config.js";
1
+ import { d as loadSession, f as resolveOptions, i as cleanExtensionWork, l as importSdk } from "./config.js";
2
2
  import { t as clear } from "./clear.js";
3
3
  import { a as injectCSS, c as getDOM, d as closeBrowser, f as launchBrowser, i as screenshot, l as getContent, n as closeTab, o as injectScript, r as listTabs, s as evaluate, t as activateTab, u as navigate } from "./tabs.js";
4
4
  //#region src/scripts/connect.ts
package/dist/service.js CHANGED
@@ -1,4 +1,4 @@
1
- import { f as resolveOptions, i as clearServiceSession, n as DEFAULT_SERVICE_PORT, p as saveServiceSession } from "./config.js";
1
+ import { a as clearServiceSession, f as resolveOptions, p as saveServiceSession, r as DEFAULT_SERVICE_PORT } from "./config.js";
2
2
  import { a as injectCSS, c as getDOM, d as closeBrowser, f as launchBrowser, i as screenshot, l as getContent, n as closeTab, o as injectScript, r as listTabs, s as evaluate, t as activateTab, u as navigate } from "./tabs.js";
3
3
  import { createServer } from "node:http";
4
4
  //#region src/service.ts
@@ -13,6 +13,15 @@ function toCommandRequest(value) {
13
13
  }
14
14
  let currentSession = null;
15
15
  let currentAgent = null;
16
+ function isProcessAlive(pid) {
17
+ if (!pid) return false;
18
+ try {
19
+ process.kill(pid, 0);
20
+ return true;
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
16
25
  function getServicePort() {
17
26
  const index = process.argv.findIndex((arg) => arg === "--service-port");
18
27
  if (index !== -1 && process.argv[index + 1]) {
@@ -39,11 +48,17 @@ function sessionInfo(session) {
39
48
  const { _agent, _proc, ...info } = session;
40
49
  return {
41
50
  running: true,
51
+ connected: Boolean(_agent?.isConnected),
42
52
  ...info
43
53
  };
44
54
  }
45
55
  async function handleLaunch(body) {
46
- if (currentSession && currentAgent) return sessionInfo(currentSession);
56
+ if (currentSession && currentAgent) {
57
+ const connected = Boolean(currentAgent?.isConnected);
58
+ const browserAlive = isProcessAlive(currentSession.pid);
59
+ if (connected && browserAlive) return sessionInfo(currentSession);
60
+ await stopBrowserSession();
61
+ }
47
62
  const options = {
48
63
  browser: body.browser,
49
64
  headless: body.headless,
package/dist/tabs.js CHANGED
@@ -1,11 +1,27 @@
1
- import { a as clearSession, c as importSdk, d as patchExtension, f as resolveOptions, m as saveSession, o as findBrowser, r as cleanExtensionWork, s as getProfileDir, u as loadSession } from "./config.js";
2
- import { spawn } from "node:child_process";
1
+ import { c as getProfileDir, d as loadSession, f as resolveOptions, i as cleanExtensionWork, l as importSdk, m as saveSession, o as clearSession, s as findBrowser } from "./config.js";
2
+ import { execSync, spawn } from "node:child_process";
3
3
  //#region src/scripts/launch-browser.ts
4
+ const BROWSE_AGENT_WEBSTORE_URL = "https://chromewebstore.google.com/detail/browse-agent/amfbnjpgfgappenkkklkogngmoeofeae?authuser=0&hl=zh-CN";
4
5
  async function launchBrowser(options = {}) {
5
6
  const { BrowserAgent } = await importSdk();
6
7
  const opts = resolveOptions(options);
7
- const extensionWork = patchExtension(opts.port, opts.secret);
8
8
  const profileDir = getProfileDir(opts.browser, opts.useUserProfile ?? true);
9
+ try {
10
+ if (process.platform === "win32") execSync("taskkill /F /IM chrome.exe /IM msedge.exe /IM brave.exe 2>nul", { stdio: "ignore" });
11
+ else if (opts.useUserProfile) for (const processName of {
12
+ chrome: ["Google Chrome"],
13
+ chromium: ["Chromium"],
14
+ edge: ["Microsoft Edge"],
15
+ brave: ["Brave Browser"]
16
+ }[opts.browser] || []) execSync(`pkill -x "${processName}" 2>/dev/null || true`, {
17
+ stdio: "pipe",
18
+ shell: "/bin/sh"
19
+ });
20
+ else execSync(`pkill -f "user-data-dir=${profileDir}" 2>/dev/null || true`, {
21
+ stdio: "pipe",
22
+ shell: "/bin/sh"
23
+ });
24
+ } catch {}
9
25
  const agent = new BrowserAgent({
10
26
  secret: opts.secret,
11
27
  port: opts.port
@@ -13,9 +29,11 @@ async function launchBrowser(options = {}) {
13
29
  await agent.start();
14
30
  console.error(`[browse-agent] Server started on port ${opts.port}`);
15
31
  const launchArgs = [
16
- `--load-extension=${extensionWork}`,
32
+ `--user-data-dir=${profileDir}`,
17
33
  "--no-first-run",
18
- "--no-default-browser-check"
34
+ "--no-default-browser-check",
35
+ "--new-window",
36
+ BROWSE_AGENT_WEBSTORE_URL
19
37
  ];
20
38
  if (opts.headless) launchArgs.push("--headless=new");
21
39
  const proc = spawn(findBrowser(opts.browser), launchArgs, {
@@ -33,13 +51,14 @@ async function launchBrowser(options = {}) {
33
51
  secret: opts.secret,
34
52
  browser: opts.browser,
35
53
  profileDir,
36
- extensionWork,
54
+ extensionWork: "chrome-webstore:amfbnjpgfgappenkkklkogngmoeofeae",
37
55
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
38
56
  _agent: agent,
39
57
  _proc: proc
40
58
  };
41
59
  saveSession(session);
42
60
  console.error(`[browse-agent] Browser launched (PID: ${proc.pid})`);
61
+ console.error("[browse-agent] Opened Chrome Web Store page. Please click \"Add to Chrome\" if not installed.");
43
62
  return session;
44
63
  }
45
64
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browse-agent-cli",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "type": "module",
5
5
  "description": "TypeScript CLI for browse-agent",
6
6
  "main": "./dist/cli.js",
package/skills/SKILL.md CHANGED
@@ -19,7 +19,13 @@ Control a real Chrome browser to visit web pages, extract content, take screensh
19
19
  browse-agent setup
20
20
  ```
21
21
 
22
- Auto-detects local vs global mode. The setup checks existing state if already installed, it exits immediately.
22
+ `setup` now opens the Chrome Web Store page in the selected browser and waits for installation detection.
23
+
24
+ Important for AI agents:
25
+ - Always remind the user to run `browse-agent setup` before first use.
26
+ - Explicitly remind the user to click **Add to Chrome** on the Web Store page (installation cannot be silent in normal user environments).
27
+ - `setup` exits immediately if it detects the extension is already installed.
28
+ - Do NOT use `--headless` unless the user explicitly asks for headless/silent/background mode. Default launch should always open a visible browser window.
23
29
 
24
30
  ## Usage Procedure — Step-by-Step (Recommended)
25
31
 
@@ -79,8 +85,7 @@ Each step is independent. If the page content isn't what you expected, you can n
79
85
 
80
86
  | Option | Applies to | Description |
81
87
  |---|---|---|
82
- | `--global` | setup, clear | Use global installation (`~/.browse-agent/`) |
83
- | `--browser <name>` | launch | `chrome` \| `chromium` \| `edge` \| `brave` (default: chrome) |
88
+ | `--browser <name>` | setup, launch | `chrome` \| `chromium` \| `edge` \| `brave` (default: chrome) |
84
89
  | `--headless` | launch | Run without visible window |
85
90
  | `--port <number>` | launch, connect, feature cmds | WebSocket port (default: 9315) |
86
91
  | `--tabId <id>` | all feature cmds | Target a specific tab (ID from `navigate` or `tabs list`) |
@@ -115,8 +120,7 @@ For full API and script examples, see [API Reference](./references/api.md) and [
115
120
  ## Cleanup
116
121
 
117
122
  ```bash
118
- browse-agent clear # remove local installation
119
- browse-agent clear --global # remove global installation
123
+ browse-agent clear # remove local runtime data
120
124
  ```
121
125
 
122
126
  ## References
@@ -11,11 +11,11 @@ browse-agent <command> [options]
11
11
  ```bash
12
12
  # Setup (install SDK + extension)
13
13
  browse-agent setup
14
- browse-agent setup --global
15
14
 
16
15
  # Launch browser
17
16
  browse-agent launch
18
17
  browse-agent launch --browser edge --headless
18
+ browse-agent launch --server-only
19
19
 
20
20
  # Check connection
21
21
  browse-agent connect
@@ -25,7 +25,6 @@ browse-agent close
25
25
 
26
26
  # Remove installation
27
27
  browse-agent clear
28
- browse-agent clear --global
29
28
  ```
30
29
 
31
30
  ## Feature Commands
@@ -64,9 +63,9 @@ browse-agent tabs close 123
64
63
 
65
64
  | Option | Applies to | Description |
66
65
  |---|---|---|
67
- | `--global` | setup, clear | Use global installation (`~/.browse-agent/`) |
68
- | `--browser <name>` | launch | Browser: `chrome` \| `chromium` \| `edge` \| `brave` |
66
+ | `--browser <name>` | launch, setup | Browser: `chrome` \| `chromium` \| `edge` \| `brave` |
69
67
  | `--headless` | launch | Run in headless mode |
68
+ | `--server-only` | launch | Start service only, skip browser launch |
70
69
  | `--port <number>` | launch, connect, feature cmds | WebSocket port (default: 9315) |
71
70
  | `--tabId <id>` | all feature cmds | Target a specific tab (ID from `navigate` or `tabs list`) |
72
71
  | `--format <type>` | get-content, screenshot | Content format (`text`/`html`) or screenshot format (`png`/`jpeg`) |