@xbrowser/cli 1.0.5 → 1.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # xbrowser
2
2
 
3
- > **Browser automation CLI** for web scraping, headless browsing, SEO analysis, and AI agent workflows. 49 commands, 69 plugins. A command-line alternative to Playwright, Puppeteer, and Selenium — **no code required**.
3
+ > **Browser automation CLI** for web scraping, headless browsing, SEO analysis, and AI agent workflows. 49 commands, 20+ plugins. A command-line alternative to Playwright, Puppeteer, and Selenium — **no code required**.
4
4
 
5
5
  [![CI Status](https://github.com/dyyz1993/xbrowser/workflows/CI/badge.svg)](https://github.com/dyyz1993/xbrowser/actions)
6
6
  [![codecov](https://codecov.io/gh/dyyz1993/xbrowser/branch/master/graph/badge.svg)](https://codecov.io/gh/dyyz1993/xbrowser)
@@ -13,7 +13,7 @@
13
13
  - **命令链** — 用 `&&`、`,`、`+`、`->`、`;` 串联多个命令,一行搞定复杂流程
14
14
  - **管道 & Heredoc** — 支持 stdin 管道和 heredoc 批量执行
15
15
  - **录制 / 回放** — 录制浏览器操作为 YAML,随时回放,可转换为 JS/Python/Bash 脚本
16
- - **插件系统** — 基于 `@dyyz1993/xcli-core`,用 TypeScript 编写站点插件
16
+ - **插件系统** — 基于 `@dyyz1993/xcli-core`,用 TypeScript 编写站点插件。快速开始:`xbrowser create my-plugin --template static`
17
17
  - **CDP 连接** — 连接已运行的 Chrome/Chromium,无需重新启动浏览器
18
18
  - **会话管理** — 多会话并行,独立上下文
19
19
  - **Daemon 模式** — 后台常驻,快速响应
@@ -256,7 +256,7 @@ xbrowser --cdp auto "goto https://example.com , title"
256
256
  | `html --selector "#main"` | 获取指定元素 HTML | `xbrowser html --selector "#main"` |
257
257
  | `text` | 获取页面文本 | `xbrowser text` |
258
258
  | `text --selector "#article"` | 获取指定元素文本 | `xbrowser text --selector "#article"` |
259
- | `getProperty <selector> <property>` | 获取元素属性 | `xbrowser getProperty "#link" href` |
259
+ | `eval "expression"` | 执行 JS 获取元素属性 | `xbrowser eval "document.querySelector('#link').href"` |
260
260
 
261
261
  ### 截图与快照
262
262
 
@@ -282,20 +282,20 @@ xbrowser --cdp auto "goto https://example.com , title"
282
282
  |------|------|------|
283
283
  | `eval <expression>` | 执行 JS 表达式 | `xbrowser eval "document.title"` |
284
284
  | `eval "1 + 2"` | 计算表达式 | `xbrowser eval "1 + 2"` |
285
- | `evaluateFn <fn> --args 1 2` | 执行带参数的函数 | `xbrowser evaluateFn "return args[0] + args[1]" --args 1 2` |
285
+ | `eval "(a, b) => a + b" --args 1 2` | 执行带参数的函数 | `xbrowser eval "(a, b) => a + b" --args 1 2` |
286
286
 
287
287
  ### 存储
288
288
 
289
289
  | 命令 | 说明 | 示例 |
290
290
  |------|------|------|
291
- | `getCookies` | 获取所有 Cookie | `xbrowser getCookies` |
292
- | `setCookie <name> <value>` | 设置 Cookie | `xbrowser setCookie session abc123` |
293
- | `setCookie <name> <value> --domain .example.com` | 指定域名 | `xbrowser setCookie session abc123 --domain .example.com` |
294
- | `clearCookies` | 清除所有 Cookie | `xbrowser clearCookies` |
295
- | `getLocalStorage` | 获取所有 localStorage | `xbrowser getLocalStorage` |
296
- | `getLocalStorage --key token` | 获取指定 key | `xbrowser getLocalStorage --key token` |
297
- | `setLocalStorage <key> <value>` | 设置 localStorage | `xbrowser setLocalStorage token "abc"` |
298
- | `clearLocalStorage` | 清除 localStorage | `xbrowser clearLocalStorage` |
291
+ | `get-cookies` | 获取所有 Cookie | `xbrowser get-cookies` |
292
+ | `set-cookie <name> <value>` | 设置 Cookie | `xbrowser set-cookie session abc123` |
293
+ | `set-cookie <name> <value> --domain .example.com` | 指定域名 | `xbrowser set-cookie session abc123 --domain .example.com` |
294
+ | `clear-cookies` | 清除所有 Cookie | `xbrowser clear-cookies` |
295
+ | `get-local-storage` | 获取所有 localStorage | `xbrowser get-local-storage` |
296
+ | `get-local-storage --key token` | 获取指定 key | `xbrowser get-local-storage --key token` |
297
+ | `set-local-storage <key> <value>` | 设置 localStorage | `xbrowser set-local-storage token "abc"` |
298
+ | `clear-local-storage` | 清除 localStorage | `xbrowser clear-local-storage` |
299
299
 
300
300
  ### 帧操作
301
301
 
@@ -309,8 +309,8 @@ xbrowser --cdp auto "goto https://example.com , title"
309
309
 
310
310
  | 命令 | 说明 | 示例 |
311
311
  |------|------|------|
312
- | `setViewport <width> <height>` | 设置视口大小 | `xbrowser setViewport 1920 1080` |
313
- | `setViewport 375 812 --isMobile true` | 移动设备模式 | `xbrowser setViewport 375 812 --isMobile true` |
312
+ | `set-viewport <width> <height>` | 设置视口大小 | `xbrowser set-viewport 1920 1080` |
313
+ | `set-viewport 375 812 --isMobile true` | 移动设备模式 | `xbrowser set-viewport 375 812 --isMobile true` |
314
314
 
315
315
  ### 配置管理
316
316
 
@@ -416,7 +416,10 @@ xbrowser create my-plugin --template static --force
416
416
  | `;` | 分割管线(前一个完成后开始下一个) | `goto https://example.com ; goto https://other.com` |
417
417
  | `\|\|` | 前一步成功则跳过后续 | `goto https://example.com \|\| goto https://fallback.com` |
418
418
 
419
- **注意**:命令链中如果包含特殊字符(如 `#`、`>`),需要用引号包裹整个命令链或对单个参数加引号。
419
+ **注意**:
420
+ - 命令链中如果包含特殊字符(如 `#`、`>`),需要用引号包裹整个命令链或对单个参数加引号。
421
+ - `->` 和 `+` 分隔符要求两边有空格(`goto url -> title` ✅,`goto url->title` ❌)。
422
+ - `,` 和 `;` 不要求空格。
420
423
 
421
424
  ```bash
422
425
  # 正确 — 整体引号
@@ -440,7 +443,7 @@ project > browser > page > element
440
443
  | Scope | 说明 | 需要的条件 | 典型命令 |
441
444
  |-------|------|------------|----------|
442
445
  | **project** | 项目级别 | 无 | config, daemon, plugin |
443
- | **browser** | 浏览器级别 | 浏览器实例 | setViewport, session |
446
+ | **browser** | 浏览器级别 | 浏览器实例 | set-viewport, session |
444
447
  | **page** | 页面级别 | 活跃页面 | goto, wait, scroll, screenshot |
445
448
  | **element** | 元素级别 | 页面中的元素 | click, fill, type, hover |
446
449
 
@@ -20,8 +20,8 @@ import {
20
20
  saveSessionDiskMeta,
21
21
  setActivePage,
22
22
  touchSession
23
- } from "./chunk-LRBSUKUZ.js";
24
- import "./chunk-N2JFPWMI.js";
23
+ } from "./chunk-MXG2H3HJ.js";
24
+ import "./chunk-SEFIJY2M.js";
25
25
  import "./chunk-TNEN6VQ2.js";
26
26
  import "./chunk-GDKLH7ZY.js";
27
27
  import "./chunk-KFQGP6VL.js";
@@ -20,7 +20,7 @@ import {
20
20
  saveSessionDiskMeta,
21
21
  setActivePage,
22
22
  touchSession
23
- } from "./chunk-DKWR54XQ.js";
23
+ } from "./chunk-7POCCXIB.js";
24
24
  import "./chunk-TNEN6VQ2.js";
25
25
  import "./chunk-GDKLH7ZY.js";
26
26
  import "./chunk-KFQGP6VL.js";
@@ -20,8 +20,8 @@ import {
20
20
  saveSessionDiskMeta,
21
21
  setActivePage,
22
22
  touchSession
23
- } from "./chunk-TWWOIJM7.js";
24
- import "./chunk-N2JFPWMI.js";
23
+ } from "./chunk-5QAYN5EZ.js";
24
+ import "./chunk-SEFIJY2M.js";
25
25
  import "./chunk-TNEN6VQ2.js";
26
26
  import "./chunk-GDKLH7ZY.js";
27
27
  import "./chunk-ABXMBNQ6.js";
@@ -2179,8 +2179,14 @@ var XBContextImpl = class {
2179
2179
  // ── Cookies ─────────────────────────────────────────────────
2180
2180
  async cookies(urls) {
2181
2181
  const urlList = typeof urls === "string" ? [urls] : urls;
2182
- const result = await this.conn.send("Network.getCookies", urlList ? { urls: urlList } : void 0);
2183
- return result.cookies;
2182
+ const params = urlList ? { urls: urlList } : void 0;
2183
+ try {
2184
+ const result = await this.conn.send("Storage.getCookies", params);
2185
+ return result.cookies;
2186
+ } catch {
2187
+ const result = await this.conn.send("Network.getCookies", params);
2188
+ return result.cookies;
2189
+ }
2184
2190
  }
2185
2191
  async addCookies(cookies) {
2186
2192
  const cdpCookies = cookies.map((c) => ({
@@ -2193,10 +2199,18 @@ var XBContextImpl = class {
2193
2199
  secure: c.secure,
2194
2200
  sameSite: c.sameSite
2195
2201
  }));
2196
- await this.conn.send("Network.setCookies", { cookies: cdpCookies });
2202
+ try {
2203
+ await this.conn.send("Storage.setCookies", { cookies: cdpCookies });
2204
+ } catch {
2205
+ await this.conn.send("Network.setCookies", { cookies: cdpCookies });
2206
+ }
2197
2207
  }
2198
2208
  async clearCookies() {
2199
- await this.conn.send("Network.clearBrowserCookies");
2209
+ try {
2210
+ await this.conn.send("Storage.clearCookies");
2211
+ } catch {
2212
+ await this.conn.send("Network.clearBrowserCookies");
2213
+ }
2200
2214
  }
2201
2215
  on(event, handler) {
2202
2216
  this._emitter.on(event, handler);
@@ -14,7 +14,7 @@ import {
14
14
  scrollIntoView,
15
15
  waitForActionable,
16
16
  waitForNetworkIdle
17
- } from "./chunk-N2JFPWMI.js";
17
+ } from "./chunk-SEFIJY2M.js";
18
18
  import {
19
19
  connectToCDP,
20
20
  findChrome,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  launch
3
- } from "./chunk-N2JFPWMI.js";
3
+ } from "./chunk-SEFIJY2M.js";
4
4
  import {
5
5
  errMsg
6
6
  } from "./chunk-GDKLH7ZY.js";
@@ -36,6 +36,15 @@ async function fetchNoProxy(url) {
36
36
  async function resolveCDPEndpoint(raw) {
37
37
  if (raw === "auto") {
38
38
  const httpResp = await fetchNoProxy("http://localhost:9222/json/version");
39
+ if (!httpResp.ok) {
40
+ throw new Error(
41
+ `CDP port 9222 responded with ${httpResp.status} ${httpResp.statusText}. \u53EF\u80FD\u539F\u56E0\uFF1A9222 \u88AB\u50F5\u6B7B\u7684 Chrome \u5360\u7528\uFF0C\u6216\u6CA1\u6709 Chrome \u4EE5 --remote-debugging-port \u542F\u52A8\u3002
42
+ \u89E3\u51B3\u65B9\u6CD5\uFF1A
43
+ 1. \u6740\u6389\u6B8B\u7559 Chrome: pkill -f "remote-debugging-port"
44
+ 2. \u91CD\u542F Chrome: npx cdp-tunnel setup
45
+ 3. \u6216\u6307\u5B9A\u7AEF\u53E3: --cdp <port>`
46
+ );
47
+ }
39
48
  const data = await httpResp.json();
40
49
  if (!data.webSocketDebuggerUrl) {
41
50
  throw new Error("Could not auto-discover CDP endpoint from localhost:9222");
@@ -2184,8 +2184,14 @@ var XBContextImpl = class {
2184
2184
  // ── Cookies ─────────────────────────────────────────────────
2185
2185
  async cookies(urls) {
2186
2186
  const urlList = typeof urls === "string" ? [urls] : urls;
2187
- const result = await this.conn.send("Network.getCookies", urlList ? { urls: urlList } : void 0);
2188
- return result.cookies;
2187
+ const params = urlList ? { urls: urlList } : void 0;
2188
+ try {
2189
+ const result = await this.conn.send("Storage.getCookies", params);
2190
+ return result.cookies;
2191
+ } catch {
2192
+ const result = await this.conn.send("Network.getCookies", params);
2193
+ return result.cookies;
2194
+ }
2189
2195
  }
2190
2196
  async addCookies(cookies) {
2191
2197
  const cdpCookies = cookies.map((c) => ({
@@ -2198,10 +2204,18 @@ var XBContextImpl = class {
2198
2204
  secure: c.secure,
2199
2205
  sameSite: c.sameSite
2200
2206
  }));
2201
- await this.conn.send("Network.setCookies", { cookies: cdpCookies });
2207
+ try {
2208
+ await this.conn.send("Storage.setCookies", { cookies: cdpCookies });
2209
+ } catch {
2210
+ await this.conn.send("Network.setCookies", { cookies: cdpCookies });
2211
+ }
2202
2212
  }
2203
2213
  async clearCookies() {
2204
- await this.conn.send("Network.clearBrowserCookies");
2214
+ try {
2215
+ await this.conn.send("Storage.clearCookies");
2216
+ } catch {
2217
+ await this.conn.send("Network.clearBrowserCookies");
2218
+ }
2205
2219
  }
2206
2220
  on(event, handler) {
2207
2221
  this._emitter.on(event, handler);
@@ -4129,6 +4143,15 @@ async function fetchNoProxy(url) {
4129
4143
  async function resolveCDPEndpoint(raw) {
4130
4144
  if (raw === "auto") {
4131
4145
  const httpResp = await fetchNoProxy("http://localhost:9222/json/version");
4146
+ if (!httpResp.ok) {
4147
+ throw new Error(
4148
+ `CDP port 9222 responded with ${httpResp.status} ${httpResp.statusText}. \u53EF\u80FD\u539F\u56E0\uFF1A9222 \u88AB\u50F5\u6B7B\u7684 Chrome \u5360\u7528\uFF0C\u6216\u6CA1\u6709 Chrome \u4EE5 --remote-debugging-port \u542F\u52A8\u3002
4149
+ \u89E3\u51B3\u65B9\u6CD5\uFF1A
4150
+ 1. \u6740\u6389\u6B8B\u7559 Chrome: pkill -f "remote-debugging-port"
4151
+ 2. \u91CD\u542F Chrome: npx cdp-tunnel setup
4152
+ 3. \u6216\u6307\u5B9A\u7AEF\u53E3: --cdp <port>`
4153
+ );
4154
+ }
4132
4155
  const data = await httpResp.json();
4133
4156
  if (!data.webSocketDebuggerUrl) {
4134
4157
  throw new Error("Could not auto-discover CDP endpoint from localhost:9222");
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  launch
3
- } from "./chunk-N2JFPWMI.js";
3
+ } from "./chunk-SEFIJY2M.js";
4
4
  import {
5
5
  errMsg
6
6
  } from "./chunk-GDKLH7ZY.js";
@@ -1401,6 +1401,15 @@ async function fetchNoProxy(url) {
1401
1401
  async function resolveCDPEndpoint(raw) {
1402
1402
  if (raw === "auto") {
1403
1403
  const httpResp = await fetchNoProxy("http://localhost:9222/json/version");
1404
+ if (!httpResp.ok) {
1405
+ throw new Error(
1406
+ `CDP port 9222 responded with ${httpResp.status} ${httpResp.statusText}. \u53EF\u80FD\u539F\u56E0\uFF1A9222 \u88AB\u50F5\u6B7B\u7684 Chrome \u5360\u7528\uFF0C\u6216\u6CA1\u6709 Chrome \u4EE5 --remote-debugging-port \u542F\u52A8\u3002
1407
+ \u89E3\u51B3\u65B9\u6CD5\uFF1A
1408
+ 1. \u6740\u6389\u6B8B\u7559 Chrome: pkill -f "remote-debugging-port"
1409
+ 2. \u91CD\u542F Chrome: npx cdp-tunnel setup
1410
+ 3. \u6216\u6307\u5B9A\u7AEF\u53E3: --cdp <port>`
1411
+ );
1412
+ }
1404
1413
  const data = await httpResp.json();
1405
1414
  if (!data.webSocketDebuggerUrl) {
1406
1415
  throw new Error("Could not auto-discover CDP endpoint from localhost:9222");
@@ -2178,8 +2178,14 @@ var XBContextImpl = class {
2178
2178
  // ── Cookies ─────────────────────────────────────────────────
2179
2179
  async cookies(urls) {
2180
2180
  const urlList = typeof urls === "string" ? [urls] : urls;
2181
- const result = await this.conn.send("Network.getCookies", urlList ? { urls: urlList } : void 0);
2182
- return result.cookies;
2181
+ const params = urlList ? { urls: urlList } : void 0;
2182
+ try {
2183
+ const result = await this.conn.send("Storage.getCookies", params);
2184
+ return result.cookies;
2185
+ } catch {
2186
+ const result = await this.conn.send("Network.getCookies", params);
2187
+ return result.cookies;
2188
+ }
2183
2189
  }
2184
2190
  async addCookies(cookies) {
2185
2191
  const cdpCookies = cookies.map((c) => ({
@@ -2192,10 +2198,18 @@ var XBContextImpl = class {
2192
2198
  secure: c.secure,
2193
2199
  sameSite: c.sameSite
2194
2200
  }));
2195
- await this.conn.send("Network.setCookies", { cookies: cdpCookies });
2201
+ try {
2202
+ await this.conn.send("Storage.setCookies", { cookies: cdpCookies });
2203
+ } catch {
2204
+ await this.conn.send("Network.setCookies", { cookies: cdpCookies });
2205
+ }
2196
2206
  }
2197
2207
  async clearCookies() {
2198
- await this.conn.send("Network.clearBrowserCookies");
2208
+ try {
2209
+ await this.conn.send("Storage.clearCookies");
2210
+ } catch {
2211
+ await this.conn.send("Network.clearBrowserCookies");
2212
+ }
2199
2213
  }
2200
2214
  on(event, handler) {
2201
2215
  this._emitter.on(event, handler);
package/dist/cli.js CHANGED
@@ -25,7 +25,7 @@ import {
25
25
  resolveLaunchOpts,
26
26
  saveSessionDiskMeta,
27
27
  setActivePage
28
- } from "./chunk-DKWR54XQ.js";
28
+ } from "./chunk-7POCCXIB.js";
29
29
  import "./chunk-TNEN6VQ2.js";
30
30
  import {
31
31
  forwardCommandLog,
@@ -346,8 +346,13 @@ var gotoCommand = registerCommand({
346
346
  }),
347
347
  handler: async (p, ctx) => {
348
348
  let url = p.url;
349
- if (!/^https?:\/\//i.test(url) && !/^wss?:\/\//i.test(url)) {
350
- url = "https://" + url;
349
+ const hasScheme = /^(https?|wss?|file|about|data|chrome|blob):/i.test(url);
350
+ if (!hasScheme) {
351
+ if (/^[\w-]+(\.[\w-]+)+/.test(url) || url.startsWith("localhost")) {
352
+ url = "https://" + url;
353
+ } else {
354
+ throw new Error(`Invalid URL: "${url}". Expected http(s)://, file://, about:, data:, or a domain name.`);
355
+ }
351
356
  }
352
357
  const response = await ctx.page.goto(url, {
353
358
  waitUntil: p.waitUntil || "domcontentloaded"
@@ -402,7 +407,7 @@ var urlCommand = registerCommand({
402
407
  scope: "page",
403
408
  result: z.object({ url: z.string() }),
404
409
  handler: async (_p, ctx) => {
405
- return ok({ url: ctx.page.url() });
410
+ return ok({ url: ctx.page.url() || "about:blank" });
406
411
  }
407
412
  });
408
413
  registerCommand({
@@ -565,7 +570,8 @@ var fillCommand = registerCommand({
565
570
  }),
566
571
  handler: async (p, ctx) => {
567
572
  const page = ctx.page;
568
- if (p.clear) {
573
+ const shouldClear = p.clear !== false;
574
+ if (shouldClear) {
569
575
  await page.fill(p.selector, "", { force: true, timeout: 1e4 });
570
576
  }
571
577
  const isReact = await page.evaluate(() => {
@@ -591,7 +597,7 @@ var fillCommand = registerCommand({
591
597
  } else {
592
598
  await page.fill(p.selector, p.value, { force: true, timeout: 1e4 });
593
599
  }
594
- return ok2({ selector: p.selector, value: p.value, cleared: p.clear || false, reactMode: !!isReact });
600
+ return ok2({ selector: p.selector, value: p.value, cleared: shouldClear, reactMode: !!isReact });
595
601
  }
596
602
  });
597
603
  var typeCommand = registerCommand({
@@ -2125,6 +2131,7 @@ function createTurndown() {
2125
2131
  return turndown;
2126
2132
  }
2127
2133
  function postClean(md) {
2134
+ md = md.replace(/<(?:table|div|tbody|thead|tr|td|th|span|colgroup|col)\b[^>]*(?:>[\s\S]{200,}?<\/(?:table|div|tbody|thead|tr|td|th|span|colgroup|col)>)/g, "\n[\u26A0\uFE0F HTML block removed \u2014 complex table/layout not converted to Markdown]\n");
2128
2135
  md = md.replace(/\n{3,}/g, "\n\n");
2129
2136
  md = md.replace(/!\[[^\]]*\]\(\s*\)/g, "");
2130
2137
  md = md.replace(/\[([^\]]*)\]\(\s*\)/g, "$1");
@@ -6960,7 +6967,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
6960
6967
  }
6961
6968
  let targetPageOverride = null;
6962
6969
  if (_target && extraOpts?.cdpEndpoint) {
6963
- const { findTargetPage } = await import("./browser-5CTOA2WS.js");
6970
+ const { findTargetPage } = await import("./browser-U4VWPTS2.js");
6964
6971
  targetPageOverride = await findTargetPage(extraOpts.cdpEndpoint, _target);
6965
6972
  if (!targetPageOverride) {
6966
6973
  return errorResult(`Target "${_target}" not found. Use 'xbrowser targets --cdp ${extraOpts.cdpEndpoint}' to list available pages.`);
@@ -8804,6 +8811,8 @@ var TEMPLATES = {
8804
8811
  {
8805
8812
  path: "index.ts",
8806
8813
  content: [
8814
+ `import { z } from 'zod';`,
8815
+ `import { ok } from '@dyyz1993/xcli-core';`,
8807
8816
  `import type { XCLIAPI } from '@dyyz1993/xcli-core';`,
8808
8817
  ``,
8809
8818
  `export default function (xcli: XCLIAPI): void {`,
@@ -8821,7 +8830,7 @@ var TEMPLATES = {
8821
8830
  ` (sel: string) => document.querySelector(sel)?.textContent ?? '',`,
8822
8831
  ` params.selector || 'body'`,
8823
8832
  ` );`,
8824
- ` return { ok: true, text };`,
8833
+ ` return ok({ text });`,
8825
8834
  ` },`,
8826
8835
  ` });`,
8827
8836
  `}`
@@ -8829,14 +8838,21 @@ var TEMPLATES = {
8829
8838
  },
8830
8839
  {
8831
8840
  path: "package.json",
8832
- content: `{
8833
- "name": "{{projectName}}",
8834
- "version": "1.0.0",
8835
- "dependencies": {
8836
- "zod": "^3.24.0"
8837
- }
8838
- }
8839
- `
8841
+ content: [
8842
+ `{`,
8843
+ ` "name": "xbrowser-plugin-{{projectName}}",`,
8844
+ ` "version": "1.0.0",`,
8845
+ ` "main": "index.ts",`,
8846
+ ` "type": "module",`,
8847
+ ` "dependencies": { "zod": "^3.24.0" },`,
8848
+ ` "peerDependencies": { "@dyyz1993/xcli-core": ">=1.0.0" },`,
8849
+ ` "xbrowser": {`,
8850
+ ` "name": "{{projectName}}",`,
8851
+ ` "description": "A static page plugin",`,
8852
+ ` "commands": ["scrape"]`,
8853
+ ` }`,
8854
+ `}`
8855
+ ].join("\n")
8840
8856
  }
8841
8857
  ]
8842
8858
  },
@@ -8847,6 +8863,8 @@ var TEMPLATES = {
8847
8863
  {
8848
8864
  path: "index.ts",
8849
8865
  content: [
8866
+ `import { z } from 'zod';`,
8867
+ `import { ok } from '@dyyz1993/xcli-core';`,
8850
8868
  `import type { XCLIAPI } from '@dyyz1993/xcli-core';`,
8851
8869
  ``,
8852
8870
  `export default function (xcli: XCLIAPI): void {`,
@@ -8861,7 +8879,7 @@ var TEMPLATES = {
8861
8879
  ` parameters: z.object({ url: z.string() }),`,
8862
8880
  ` handler: async (params, ctx) => {`,
8863
8881
  ` await ctx.page.goto(params.url);`,
8864
- ` return { ok: true };`,
8882
+ ` return ok({ url: params.url });`,
8865
8883
  ` },`,
8866
8884
  ` });`,
8867
8885
  ``,
@@ -8876,7 +8894,7 @@ var TEMPLATES = {
8876
8894
  ` case 'fill': await page.fill(params.selector, ''); break;`,
8877
8895
  ` case 'hover': await page.hover(params.selector); break;`,
8878
8896
  ` }`,
8879
- ` return { ok: true };`,
8897
+ ` return ok({ action: params.action, selector: params.selector });`,
8880
8898
  ` },`,
8881
8899
  ` });`,
8882
8900
  `}`
@@ -8884,14 +8902,21 @@ var TEMPLATES = {
8884
8902
  },
8885
8903
  {
8886
8904
  path: "package.json",
8887
- content: `{
8888
- "name": "{{projectName}}",
8889
- "version": "1.0.0",
8890
- "dependencies": {
8891
- "zod": "^3.24.0"
8892
- }
8893
- }
8894
- `
8905
+ content: [
8906
+ `{`,
8907
+ ` "name": "xbrowser-plugin-{{projectName}}",`,
8908
+ ` "version": "1.0.0",`,
8909
+ ` "main": "index.ts",`,
8910
+ ` "type": "module",`,
8911
+ ` "dependencies": { "zod": "^3.24.0" },`,
8912
+ ` "peerDependencies": { "@dyyz1993/xcli-core": ">=1.0.0" },`,
8913
+ ` "xbrowser": {`,
8914
+ ` "name": "{{projectName}}",`,
8915
+ ` "description": "A dynamic page plugin",`,
8916
+ ` "commands": ["navigate", "interact"]`,
8917
+ ` }`,
8918
+ `}`
8919
+ ].join("\n")
8895
8920
  }
8896
8921
  ]
8897
8922
  },
@@ -8902,6 +8927,8 @@ var TEMPLATES = {
8902
8927
  {
8903
8928
  path: "index.ts",
8904
8929
  content: [
8930
+ `import { z } from 'zod';`,
8931
+ `import { ok } from '@dyyz1993/xcli-core';`,
8905
8932
  `import type { XCLIAPI } from '@dyyz1993/xcli-core';`,
8906
8933
  ``,
8907
8934
  `export default function (xcli: XCLIAPI): void {`,
@@ -8915,14 +8942,13 @@ var TEMPLATES = {
8915
8942
  ` description: 'Check login status',`,
8916
8943
  ` scope: 'project',`,
8917
8944
  ` parameters: z.object({}),`,
8918
- ` handler: async (_params, ctx) => {`,
8945
+ ` handler: async () => {`,
8919
8946
  ` const loggedIn = await site.isLoggedIn();`,
8920
- ` return { ok: true, loggedIn };`,
8947
+ ` return ok({ loggedIn });`,
8921
8948
  ` },`,
8922
8949
  ` });`,
8923
8950
  ``,
8924
8951
  ` site.login(async (ctx) => {`,
8925
- ` console.log('Login handler for {{projectName}}');`,
8926
8952
  ` await ctx.storage.set('auth_token', 'dummy');`,
8927
8953
  ` });`,
8928
8954
  ``,
@@ -8934,14 +8960,22 @@ var TEMPLATES = {
8934
8960
  },
8935
8961
  {
8936
8962
  path: "package.json",
8937
- content: `{
8938
- "name": "{{projectName}}",
8939
- "version": "1.0.0",
8940
- "dependencies": {
8941
- "zod": "^3.24.0"
8942
- }
8943
- }
8944
- `
8963
+ content: [
8964
+ `{`,
8965
+ ` "name": "xbrowser-plugin-{{projectName}}",`,
8966
+ ` "version": "1.0.0",`,
8967
+ ` "main": "index.ts",`,
8968
+ ` "type": "module",`,
8969
+ ` "dependencies": { "zod": "^3.24.0" },`,
8970
+ ` "peerDependencies": { "@dyyz1993/xcli-core": ">=1.0.0" },`,
8971
+ ` "xbrowser": {`,
8972
+ ` "name": "{{projectName}}",`,
8973
+ ` "description": "A plugin with login/logout support",`,
8974
+ ` "requiresLogin": true,`,
8975
+ ` "commands": ["check"]`,
8976
+ ` }`,
8977
+ `}`
8978
+ ].join("\n")
8945
8979
  }
8946
8980
  ]
8947
8981
  },
@@ -8952,6 +8986,8 @@ var TEMPLATES = {
8952
8986
  {
8953
8987
  path: "index.ts",
8954
8988
  content: [
8989
+ `import { z } from 'zod';`,
8990
+ `import { ok } from '@dyyz1993/xcli-core';`,
8955
8991
  `import type { XCLIAPI } from '@dyyz1993/xcli-core';`,
8956
8992
  ``,
8957
8993
  `export default function (xcli: XCLIAPI): void {`,
@@ -8964,9 +9000,9 @@ var TEMPLATES = {
8964
9000
  ` description: 'Fetch data from API',`,
8965
9001
  ` scope: 'project',`,
8966
9002
  ` parameters: z.object({ endpoint: z.string(), method: z.enum(['GET', 'POST']).optional() }),`,
8967
- ` handler: async (params, _ctx) => {`,
9003
+ ` handler: async (params) => {`,
8968
9004
  ` const method = params.method || 'GET';`,
8969
- ` return { ok: true, endpoint: params.endpoint, method };`,
9005
+ ` return ok({ endpoint: params.endpoint, method });`,
8970
9006
  ` },`,
8971
9007
  ` });`,
8972
9008
  ``,
@@ -8974,8 +9010,8 @@ var TEMPLATES = {
8974
9010
  ` description: 'List available endpoints',`,
8975
9011
  ` scope: 'project',`,
8976
9012
  ` parameters: z.object({}),`,
8977
- ` handler: async (_params, _ctx) => {`,
8978
- ` return { ok: true, endpoints: ['/api/data', '/api/status'] };`,
9013
+ ` handler: async () => {`,
9014
+ ` return ok({ endpoints: ['/api/data', '/api/status'] });`,
8979
9015
  ` },`,
8980
9016
  ` });`,
8981
9017
  `}`
@@ -8983,14 +9019,21 @@ var TEMPLATES = {
8983
9019
  },
8984
9020
  {
8985
9021
  path: "package.json",
8986
- content: `{
8987
- "name": "{{projectName}}",
8988
- "version": "1.0.0",
8989
- "dependencies": {
8990
- "zod": "^3.24.0"
8991
- }
8992
- }
8993
- `
9022
+ content: [
9023
+ `{`,
9024
+ ` "name": "xbrowser-plugin-{{projectName}}",`,
9025
+ ` "version": "1.0.0",`,
9026
+ ` "main": "index.ts",`,
9027
+ ` "type": "module",`,
9028
+ ` "dependencies": { "zod": "^3.24.0" },`,
9029
+ ` "peerDependencies": { "@dyyz1993/xcli-core": ">=1.0.0" },`,
9030
+ ` "xbrowser": {`,
9031
+ ` "name": "{{projectName}}",`,
9032
+ ` "description": "An API integration plugin",`,
9033
+ ` "commands": ["fetch", "list-endpoints"]`,
9034
+ ` }`,
9035
+ `}`
9036
+ ].join("\n")
8994
9037
  }
8995
9038
  ]
8996
9039
  }
@@ -9497,10 +9540,10 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
9497
9540
  break;
9498
9541
  }
9499
9542
  case "press": {
9500
- const key = parsed.value || parsed.remaining[1];
9501
- if (!sel && !key) outputError("Usage: xbrowser press [selector] <key>");
9543
+ const key = parsed.value || (sel ? parsed.remaining[1] : parsed.remaining[0]);
9544
+ if (!key) outputError("Usage: xbrowser press [selector] <key>");
9502
9545
  cmdName = "press";
9503
- params = { ...sel ? { selector: sel } : {}, key: key || sel };
9546
+ params = { ...sel ? { selector: sel } : {}, key };
9504
9547
  break;
9505
9548
  }
9506
9549
  case "select": {
@@ -9569,13 +9612,22 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
9569
9612
  params = { expression: args.join(" ") };
9570
9613
  break;
9571
9614
  case "scroll": {
9572
- const direction = args[0] || "down";
9573
- if (!["up", "down", "left", "right"].includes(direction))
9574
- outputError("Direction must be: up, down, left, right");
9615
+ let direction = "down";
9616
+ let distance;
9617
+ const firstArg = args[0];
9618
+ if (firstArg && ["up", "down", "left", "right"].includes(firstArg)) {
9619
+ direction = firstArg;
9620
+ if (args[1] && /^\d+$/.test(args[1])) distance = Number(args[1]);
9621
+ } else if (firstArg && /^\d+$/.test(firstArg)) {
9622
+ distance = Number(firstArg);
9623
+ } else if (firstArg) {
9624
+ outputError(`Invalid scroll argument: "${firstArg}". Use up/down/left/right or a pixel number.`);
9625
+ }
9626
+ distance = distance ?? (options.distance ? Number(options.distance) : options.amount ? Number(options.amount) : void 0);
9575
9627
  cmdName = "scroll";
9576
9628
  params = {
9577
9629
  direction,
9578
- distance: options.distance ? Number(options.distance) : options.amount ? Number(options.amount) : void 0,
9630
+ distance,
9579
9631
  selector: options.selector || options.s
9580
9632
  };
9581
9633
  break;
@@ -9588,6 +9640,14 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
9588
9640
  cmdName = "url";
9589
9641
  params = {};
9590
9642
  break;
9643
+ case "set-viewport": {
9644
+ const width = options.width ? Number(options.width) : args[0] ? Number(args[0]) : void 0;
9645
+ const height = options.height ? Number(options.height) : args[1] ? Number(args[1]) : void 0;
9646
+ if (!width || !height) outputError("Usage: xbrowser set-viewport <width> <height>");
9647
+ cmdName = "set-viewport";
9648
+ params = { width, height };
9649
+ break;
9650
+ }
9591
9651
  case "html":
9592
9652
  cmdName = "html";
9593
9653
  params = { selector: options.selector || options.s };
@@ -10124,6 +10184,11 @@ async function handlePlugin(args, options, mode) {
10124
10184
  case "uninstall": {
10125
10185
  const name = subArgs[0];
10126
10186
  if (!name) outputError("Usage: xbrowser plugin uninstall <name>");
10187
+ const installed = await installer.list();
10188
+ const exists = installed.some((p) => p.name === name || p.metadata?.name === name);
10189
+ if (!exists) {
10190
+ outputError(`Plugin "${name}" is not installed. Use 'xbrowser plugin list' to see installed plugins.`);
10191
+ }
10127
10192
  await installer.uninstall(name);
10128
10193
  outputResult({ ok: true, name }, mode);
10129
10194
  break;
@@ -12650,7 +12715,7 @@ async function main() {
12650
12715
  const command = process.argv[2];
12651
12716
  const isLongRunning = command === "preview" || command === "serve";
12652
12717
  if (!isLongRunning) {
12653
- const { ensureProcessCanExit } = await import("./browser-5CTOA2WS.js");
12718
+ const { ensureProcessCanExit } = await import("./browser-U4VWPTS2.js");
12654
12719
  await ensureProcessCanExit().catch(() => {
12655
12720
  });
12656
12721
  process.exit(exitCode);
@@ -21,8 +21,8 @@ import {
21
21
  resolveLaunchOpts,
22
22
  saveSessionDiskMeta,
23
23
  setActivePage
24
- } from "./chunk-LRBSUKUZ.js";
25
- import "./chunk-N2JFPWMI.js";
24
+ } from "./chunk-MXG2H3HJ.js";
25
+ import "./chunk-SEFIJY2M.js";
26
26
  import "./chunk-TNEN6VQ2.js";
27
27
  import {
28
28
  getDaemonConfig,
@@ -304,8 +304,13 @@ var gotoCommand = registerCommand({
304
304
  }),
305
305
  handler: async (p, ctx) => {
306
306
  let url = p.url;
307
- if (!/^https?:\/\//i.test(url) && !/^wss?:\/\//i.test(url)) {
308
- url = "https://" + url;
307
+ const hasScheme = /^(https?|wss?|file|about|data|chrome|blob):/i.test(url);
308
+ if (!hasScheme) {
309
+ if (/^[\w-]+(\.[\w-]+)+/.test(url) || url.startsWith("localhost")) {
310
+ url = "https://" + url;
311
+ } else {
312
+ throw new Error(`Invalid URL: "${url}". Expected http(s)://, file://, about:, data:, or a domain name.`);
313
+ }
309
314
  }
310
315
  const response = await ctx.page.goto(url, {
311
316
  waitUntil: p.waitUntil || "domcontentloaded"
@@ -360,7 +365,7 @@ var urlCommand = registerCommand({
360
365
  scope: "page",
361
366
  result: z.object({ url: z.string() }),
362
367
  handler: async (_p, ctx) => {
363
- return ok({ url: ctx.page.url() });
368
+ return ok({ url: ctx.page.url() || "about:blank" });
364
369
  }
365
370
  });
366
371
  registerCommand({
@@ -523,7 +528,8 @@ var fillCommand = registerCommand({
523
528
  }),
524
529
  handler: async (p, ctx) => {
525
530
  const page = ctx.page;
526
- if (p.clear) {
531
+ const shouldClear = p.clear !== false;
532
+ if (shouldClear) {
527
533
  await page.fill(p.selector, "", { force: true, timeout: 1e4 });
528
534
  }
529
535
  const isReact = await page.evaluate(() => {
@@ -549,7 +555,7 @@ var fillCommand = registerCommand({
549
555
  } else {
550
556
  await page.fill(p.selector, p.value, { force: true, timeout: 1e4 });
551
557
  }
552
- return ok2({ selector: p.selector, value: p.value, cleared: p.clear || false, reactMode: !!isReact });
558
+ return ok2({ selector: p.selector, value: p.value, cleared: shouldClear, reactMode: !!isReact });
553
559
  }
554
560
  });
555
561
  var typeCommand = registerCommand({
@@ -2083,6 +2089,7 @@ function createTurndown() {
2083
2089
  return turndown;
2084
2090
  }
2085
2091
  function postClean(md) {
2092
+ md = md.replace(/<(?:table|div|tbody|thead|tr|td|th|span|colgroup|col)\b[^>]*(?:>[\s\S]{200,}?<\/(?:table|div|tbody|thead|tr|td|th|span|colgroup|col)>)/g, "\n[\u26A0\uFE0F HTML block removed \u2014 complex table/layout not converted to Markdown]\n");
2086
2093
  md = md.replace(/\n{3,}/g, "\n\n");
2087
2094
  md = md.replace(/!\[[^\]]*\]\(\s*\)/g, "");
2088
2095
  md = md.replace(/\[([^\]]*)\]\(\s*\)/g, "$1");
@@ -6918,7 +6925,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
6918
6925
  }
6919
6926
  let targetPageOverride = null;
6920
6927
  if (_target && extraOpts?.cdpEndpoint) {
6921
- const { findTargetPage } = await import("./browser-ITLZZDHJ.js");
6928
+ const { findTargetPage } = await import("./browser-AXCKBSWS.js");
6922
6929
  targetPageOverride = await findTargetPage(extraOpts.cdpEndpoint, _target);
6923
6930
  if (!targetPageOverride) {
6924
6931
  return errorResult(`Target "${_target}" not found. Use 'xbrowser targets --cdp ${extraOpts.cdpEndpoint}' to list available pages.`);
@@ -8602,7 +8609,7 @@ function createRPCHandler() {
8602
8609
  const isNewFormat = Array.isArray(parsed.actions);
8603
8610
  if (isNewFormat) {
8604
8611
  try {
8605
- const { SessionReplayer } = await import("./session-replayer-MY27H4DX.js");
8612
+ const { SessionReplayer } = await import("./session-replayer-GCGY6KFK.js");
8606
8613
  const replayer = new SessionReplayer({
8607
8614
  page: session.page,
8608
8615
  stepDelay: slowMo * 500,
package/dist/index.js CHANGED
@@ -81,8 +81,8 @@ import {
81
81
  resolveLaunchOpts,
82
82
  saveSessionDiskMeta,
83
83
  setActivePage
84
- } from "./chunk-TWWOIJM7.js";
85
- import "./chunk-N2JFPWMI.js";
84
+ } from "./chunk-5QAYN5EZ.js";
85
+ import "./chunk-SEFIJY2M.js";
86
86
  import "./chunk-TNEN6VQ2.js";
87
87
  import {
88
88
  errMsg
@@ -386,8 +386,13 @@ var gotoCommand = registerCommand({
386
386
  }),
387
387
  handler: async (p, ctx) => {
388
388
  let url = p.url;
389
- if (!/^https?:\/\//i.test(url) && !/^wss?:\/\//i.test(url)) {
390
- url = "https://" + url;
389
+ const hasScheme = /^(https?|wss?|file|about|data|chrome|blob):/i.test(url);
390
+ if (!hasScheme) {
391
+ if (/^[\w-]+(\.[\w-]+)+/.test(url) || url.startsWith("localhost")) {
392
+ url = "https://" + url;
393
+ } else {
394
+ throw new Error(`Invalid URL: "${url}". Expected http(s)://, file://, about:, data:, or a domain name.`);
395
+ }
391
396
  }
392
397
  const response = await ctx.page.goto(url, {
393
398
  waitUntil: p.waitUntil || "domcontentloaded"
@@ -442,7 +447,7 @@ var urlCommand = registerCommand({
442
447
  scope: "page",
443
448
  result: z.object({ url: z.string() }),
444
449
  handler: async (_p, ctx) => {
445
- return ok({ url: ctx.page.url() });
450
+ return ok({ url: ctx.page.url() || "about:blank" });
446
451
  }
447
452
  });
448
453
  registerCommand({
@@ -605,7 +610,8 @@ var fillCommand = registerCommand({
605
610
  }),
606
611
  handler: async (p, ctx) => {
607
612
  const page = ctx.page;
608
- if (p.clear) {
613
+ const shouldClear = p.clear !== false;
614
+ if (shouldClear) {
609
615
  await page.fill(p.selector, "", { force: true, timeout: 1e4 });
610
616
  }
611
617
  const isReact = await page.evaluate(() => {
@@ -631,7 +637,7 @@ var fillCommand = registerCommand({
631
637
  } else {
632
638
  await page.fill(p.selector, p.value, { force: true, timeout: 1e4 });
633
639
  }
634
- return ok2({ selector: p.selector, value: p.value, cleared: p.clear || false, reactMode: !!isReact });
640
+ return ok2({ selector: p.selector, value: p.value, cleared: shouldClear, reactMode: !!isReact });
635
641
  }
636
642
  });
637
643
  var typeCommand = registerCommand({
@@ -2165,6 +2171,7 @@ function createTurndown() {
2165
2171
  return turndown;
2166
2172
  }
2167
2173
  function postClean(md) {
2174
+ md = md.replace(/<(?:table|div|tbody|thead|tr|td|th|span|colgroup|col)\b[^>]*(?:>[\s\S]{200,}?<\/(?:table|div|tbody|thead|tr|td|th|span|colgroup|col)>)/g, "\n[\u26A0\uFE0F HTML block removed \u2014 complex table/layout not converted to Markdown]\n");
2168
2175
  md = md.replace(/\n{3,}/g, "\n\n");
2169
2176
  md = md.replace(/!\[[^\]]*\]\(\s*\)/g, "");
2170
2177
  md = md.replace(/\[([^\]]*)\]\(\s*\)/g, "$1");
@@ -7280,7 +7287,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
7280
7287
  }
7281
7288
  let targetPageOverride = null;
7282
7289
  if (_target && extraOpts?.cdpEndpoint) {
7283
- const { findTargetPage } = await import("./browser-IUJXXNBT.js");
7290
+ const { findTargetPage } = await import("./browser-X7OVRKJH.js");
7284
7291
  targetPageOverride = await findTargetPage(extraOpts.cdpEndpoint, _target);
7285
7292
  if (!targetPageOverride) {
7286
7293
  return errorResult(`Target "${_target}" not found. Use 'xbrowser targets --cdp ${extraOpts.cdpEndpoint}' to list available pages.`);
@@ -9139,6 +9146,8 @@ var TEMPLATES = {
9139
9146
  {
9140
9147
  path: "index.ts",
9141
9148
  content: [
9149
+ `import { z } from 'zod';`,
9150
+ `import { ok } from '@dyyz1993/xcli-core';`,
9142
9151
  `import type { XCLIAPI } from '@dyyz1993/xcli-core';`,
9143
9152
  ``,
9144
9153
  `export default function (xcli: XCLIAPI): void {`,
@@ -9156,7 +9165,7 @@ var TEMPLATES = {
9156
9165
  ` (sel: string) => document.querySelector(sel)?.textContent ?? '',`,
9157
9166
  ` params.selector || 'body'`,
9158
9167
  ` );`,
9159
- ` return { ok: true, text };`,
9168
+ ` return ok({ text });`,
9160
9169
  ` },`,
9161
9170
  ` });`,
9162
9171
  `}`
@@ -9164,14 +9173,21 @@ var TEMPLATES = {
9164
9173
  },
9165
9174
  {
9166
9175
  path: "package.json",
9167
- content: `{
9168
- "name": "{{projectName}}",
9169
- "version": "1.0.0",
9170
- "dependencies": {
9171
- "zod": "^3.24.0"
9172
- }
9173
- }
9174
- `
9176
+ content: [
9177
+ `{`,
9178
+ ` "name": "xbrowser-plugin-{{projectName}}",`,
9179
+ ` "version": "1.0.0",`,
9180
+ ` "main": "index.ts",`,
9181
+ ` "type": "module",`,
9182
+ ` "dependencies": { "zod": "^3.24.0" },`,
9183
+ ` "peerDependencies": { "@dyyz1993/xcli-core": ">=1.0.0" },`,
9184
+ ` "xbrowser": {`,
9185
+ ` "name": "{{projectName}}",`,
9186
+ ` "description": "A static page plugin",`,
9187
+ ` "commands": ["scrape"]`,
9188
+ ` }`,
9189
+ `}`
9190
+ ].join("\n")
9175
9191
  }
9176
9192
  ]
9177
9193
  },
@@ -9182,6 +9198,8 @@ var TEMPLATES = {
9182
9198
  {
9183
9199
  path: "index.ts",
9184
9200
  content: [
9201
+ `import { z } from 'zod';`,
9202
+ `import { ok } from '@dyyz1993/xcli-core';`,
9185
9203
  `import type { XCLIAPI } from '@dyyz1993/xcli-core';`,
9186
9204
  ``,
9187
9205
  `export default function (xcli: XCLIAPI): void {`,
@@ -9196,7 +9214,7 @@ var TEMPLATES = {
9196
9214
  ` parameters: z.object({ url: z.string() }),`,
9197
9215
  ` handler: async (params, ctx) => {`,
9198
9216
  ` await ctx.page.goto(params.url);`,
9199
- ` return { ok: true };`,
9217
+ ` return ok({ url: params.url });`,
9200
9218
  ` },`,
9201
9219
  ` });`,
9202
9220
  ``,
@@ -9211,7 +9229,7 @@ var TEMPLATES = {
9211
9229
  ` case 'fill': await page.fill(params.selector, ''); break;`,
9212
9230
  ` case 'hover': await page.hover(params.selector); break;`,
9213
9231
  ` }`,
9214
- ` return { ok: true };`,
9232
+ ` return ok({ action: params.action, selector: params.selector });`,
9215
9233
  ` },`,
9216
9234
  ` });`,
9217
9235
  `}`
@@ -9219,14 +9237,21 @@ var TEMPLATES = {
9219
9237
  },
9220
9238
  {
9221
9239
  path: "package.json",
9222
- content: `{
9223
- "name": "{{projectName}}",
9224
- "version": "1.0.0",
9225
- "dependencies": {
9226
- "zod": "^3.24.0"
9227
- }
9228
- }
9229
- `
9240
+ content: [
9241
+ `{`,
9242
+ ` "name": "xbrowser-plugin-{{projectName}}",`,
9243
+ ` "version": "1.0.0",`,
9244
+ ` "main": "index.ts",`,
9245
+ ` "type": "module",`,
9246
+ ` "dependencies": { "zod": "^3.24.0" },`,
9247
+ ` "peerDependencies": { "@dyyz1993/xcli-core": ">=1.0.0" },`,
9248
+ ` "xbrowser": {`,
9249
+ ` "name": "{{projectName}}",`,
9250
+ ` "description": "A dynamic page plugin",`,
9251
+ ` "commands": ["navigate", "interact"]`,
9252
+ ` }`,
9253
+ `}`
9254
+ ].join("\n")
9230
9255
  }
9231
9256
  ]
9232
9257
  },
@@ -9237,6 +9262,8 @@ var TEMPLATES = {
9237
9262
  {
9238
9263
  path: "index.ts",
9239
9264
  content: [
9265
+ `import { z } from 'zod';`,
9266
+ `import { ok } from '@dyyz1993/xcli-core';`,
9240
9267
  `import type { XCLIAPI } from '@dyyz1993/xcli-core';`,
9241
9268
  ``,
9242
9269
  `export default function (xcli: XCLIAPI): void {`,
@@ -9250,14 +9277,13 @@ var TEMPLATES = {
9250
9277
  ` description: 'Check login status',`,
9251
9278
  ` scope: 'project',`,
9252
9279
  ` parameters: z.object({}),`,
9253
- ` handler: async (_params, ctx) => {`,
9280
+ ` handler: async () => {`,
9254
9281
  ` const loggedIn = await site.isLoggedIn();`,
9255
- ` return { ok: true, loggedIn };`,
9282
+ ` return ok({ loggedIn });`,
9256
9283
  ` },`,
9257
9284
  ` });`,
9258
9285
  ``,
9259
9286
  ` site.login(async (ctx) => {`,
9260
- ` console.log('Login handler for {{projectName}}');`,
9261
9287
  ` await ctx.storage.set('auth_token', 'dummy');`,
9262
9288
  ` });`,
9263
9289
  ``,
@@ -9269,14 +9295,22 @@ var TEMPLATES = {
9269
9295
  },
9270
9296
  {
9271
9297
  path: "package.json",
9272
- content: `{
9273
- "name": "{{projectName}}",
9274
- "version": "1.0.0",
9275
- "dependencies": {
9276
- "zod": "^3.24.0"
9277
- }
9278
- }
9279
- `
9298
+ content: [
9299
+ `{`,
9300
+ ` "name": "xbrowser-plugin-{{projectName}}",`,
9301
+ ` "version": "1.0.0",`,
9302
+ ` "main": "index.ts",`,
9303
+ ` "type": "module",`,
9304
+ ` "dependencies": { "zod": "^3.24.0" },`,
9305
+ ` "peerDependencies": { "@dyyz1993/xcli-core": ">=1.0.0" },`,
9306
+ ` "xbrowser": {`,
9307
+ ` "name": "{{projectName}}",`,
9308
+ ` "description": "A plugin with login/logout support",`,
9309
+ ` "requiresLogin": true,`,
9310
+ ` "commands": ["check"]`,
9311
+ ` }`,
9312
+ `}`
9313
+ ].join("\n")
9280
9314
  }
9281
9315
  ]
9282
9316
  },
@@ -9287,6 +9321,8 @@ var TEMPLATES = {
9287
9321
  {
9288
9322
  path: "index.ts",
9289
9323
  content: [
9324
+ `import { z } from 'zod';`,
9325
+ `import { ok } from '@dyyz1993/xcli-core';`,
9290
9326
  `import type { XCLIAPI } from '@dyyz1993/xcli-core';`,
9291
9327
  ``,
9292
9328
  `export default function (xcli: XCLIAPI): void {`,
@@ -9299,9 +9335,9 @@ var TEMPLATES = {
9299
9335
  ` description: 'Fetch data from API',`,
9300
9336
  ` scope: 'project',`,
9301
9337
  ` parameters: z.object({ endpoint: z.string(), method: z.enum(['GET', 'POST']).optional() }),`,
9302
- ` handler: async (params, _ctx) => {`,
9338
+ ` handler: async (params) => {`,
9303
9339
  ` const method = params.method || 'GET';`,
9304
- ` return { ok: true, endpoint: params.endpoint, method };`,
9340
+ ` return ok({ endpoint: params.endpoint, method });`,
9305
9341
  ` },`,
9306
9342
  ` });`,
9307
9343
  ``,
@@ -9309,8 +9345,8 @@ var TEMPLATES = {
9309
9345
  ` description: 'List available endpoints',`,
9310
9346
  ` scope: 'project',`,
9311
9347
  ` parameters: z.object({}),`,
9312
- ` handler: async (_params, _ctx) => {`,
9313
- ` return { ok: true, endpoints: ['/api/data', '/api/status'] };`,
9348
+ ` handler: async () => {`,
9349
+ ` return ok({ endpoints: ['/api/data', '/api/status'] });`,
9314
9350
  ` },`,
9315
9351
  ` });`,
9316
9352
  `}`
@@ -9318,14 +9354,21 @@ var TEMPLATES = {
9318
9354
  },
9319
9355
  {
9320
9356
  path: "package.json",
9321
- content: `{
9322
- "name": "{{projectName}}",
9323
- "version": "1.0.0",
9324
- "dependencies": {
9325
- "zod": "^3.24.0"
9326
- }
9327
- }
9328
- `
9357
+ content: [
9358
+ `{`,
9359
+ ` "name": "xbrowser-plugin-{{projectName}}",`,
9360
+ ` "version": "1.0.0",`,
9361
+ ` "main": "index.ts",`,
9362
+ ` "type": "module",`,
9363
+ ` "dependencies": { "zod": "^3.24.0" },`,
9364
+ ` "peerDependencies": { "@dyyz1993/xcli-core": ">=1.0.0" },`,
9365
+ ` "xbrowser": {`,
9366
+ ` "name": "{{projectName}}",`,
9367
+ ` "description": "An API integration plugin",`,
9368
+ ` "commands": ["fetch", "list-endpoints"]`,
9369
+ ` }`,
9370
+ `}`
9371
+ ].join("\n")
9329
9372
  }
9330
9373
  ]
9331
9374
  }
@@ -9837,10 +9880,10 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
9837
9880
  break;
9838
9881
  }
9839
9882
  case "press": {
9840
- const key = parsed.value || parsed.remaining[1];
9841
- if (!sel && !key) outputError("Usage: xbrowser press [selector] <key>");
9883
+ const key = parsed.value || (sel ? parsed.remaining[1] : parsed.remaining[0]);
9884
+ if (!key) outputError("Usage: xbrowser press [selector] <key>");
9842
9885
  cmdName = "press";
9843
- params = { ...sel ? { selector: sel } : {}, key: key || sel };
9886
+ params = { ...sel ? { selector: sel } : {}, key };
9844
9887
  break;
9845
9888
  }
9846
9889
  case "select": {
@@ -9909,13 +9952,22 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
9909
9952
  params = { expression: args.join(" ") };
9910
9953
  break;
9911
9954
  case "scroll": {
9912
- const direction = args[0] || "down";
9913
- if (!["up", "down", "left", "right"].includes(direction))
9914
- outputError("Direction must be: up, down, left, right");
9955
+ let direction = "down";
9956
+ let distance;
9957
+ const firstArg = args[0];
9958
+ if (firstArg && ["up", "down", "left", "right"].includes(firstArg)) {
9959
+ direction = firstArg;
9960
+ if (args[1] && /^\d+$/.test(args[1])) distance = Number(args[1]);
9961
+ } else if (firstArg && /^\d+$/.test(firstArg)) {
9962
+ distance = Number(firstArg);
9963
+ } else if (firstArg) {
9964
+ outputError(`Invalid scroll argument: "${firstArg}". Use up/down/left/right or a pixel number.`);
9965
+ }
9966
+ distance = distance ?? (options.distance ? Number(options.distance) : options.amount ? Number(options.amount) : void 0);
9915
9967
  cmdName = "scroll";
9916
9968
  params = {
9917
9969
  direction,
9918
- distance: options.distance ? Number(options.distance) : options.amount ? Number(options.amount) : void 0,
9970
+ distance,
9919
9971
  selector: options.selector || options.s
9920
9972
  };
9921
9973
  break;
@@ -9928,6 +9980,14 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
9928
9980
  cmdName = "url";
9929
9981
  params = {};
9930
9982
  break;
9983
+ case "set-viewport": {
9984
+ const width = options.width ? Number(options.width) : args[0] ? Number(args[0]) : void 0;
9985
+ const height = options.height ? Number(options.height) : args[1] ? Number(args[1]) : void 0;
9986
+ if (!width || !height) outputError("Usage: xbrowser set-viewport <width> <height>");
9987
+ cmdName = "set-viewport";
9988
+ params = { width, height };
9989
+ break;
9990
+ }
9931
9991
  case "html":
9932
9992
  cmdName = "html";
9933
9993
  params = { selector: options.selector || options.s };
@@ -10464,6 +10524,11 @@ async function handlePlugin(args, options, mode) {
10464
10524
  case "uninstall": {
10465
10525
  const name = subArgs[0];
10466
10526
  if (!name) outputError("Usage: xbrowser plugin uninstall <name>");
10527
+ const installed = await installer.list();
10528
+ const exists = installed.some((p) => p.name === name || p.metadata?.name === name);
10529
+ if (!exists) {
10530
+ outputError(`Plugin "${name}" is not installed. Use 'xbrowser plugin list' to see installed plugins.`);
10531
+ }
10467
10532
  await installer.uninstall(name);
10468
10533
  outputResult({ ok: true, name }, mode);
10469
10534
  break;
@@ -15777,7 +15842,7 @@ var DataCollector = class {
15777
15842
  return results;
15778
15843
  }
15779
15844
  async createBrowserContext() {
15780
- const { launch } = await import("./cdp-driver-D6WMSMWX.js");
15845
+ const { launch } = await import("./cdp-driver-ZAVN7GRB.js");
15781
15846
  const { browser } = await launch({
15782
15847
  headless: true,
15783
15848
  args: ["--no-sandbox", "--disable-setuid-sandbox"]
@@ -31,7 +31,7 @@ var SessionReplayer = class {
31
31
  if (this.opts.page) {
32
32
  this.page = this.opts.page;
33
33
  } else if (this.opts.cdpUrl) {
34
- const { launch } = await import("./cdp-driver-D6WMSMWX.js");
34
+ const { launch } = await import("./cdp-driver-ZAVN7GRB.js");
35
35
  const { browser } = await launch({ cdpEndpoint: this.opts.cdpUrl });
36
36
  let contexts = browser.contexts();
37
37
  for (let i = 0; i < 10 && contexts.length === 0; i++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xbrowser/cli",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Browser automation CLI for web scraping, headless browsing, SEO analysis, and AI agent workflows. A command-line alternative to Playwright, Puppeteer, and Selenium.",
5
5
  "type": "module",
6
6
  "bin": {