chore: import upstream snapshot with attribution
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:39:48 +08:00
commit 9b395f5cc3
2400 changed files with 376116 additions and 0 deletions
+94
View File
@@ -0,0 +1,94 @@
# Browser Bridge Setup
> **⚠️ Important**: Browser commands reuse your Chrome login session. You must be logged into the target website in Chrome before running commands.
OpenCLI connects to your browser through a lightweight **Browser Bridge** Chrome Extension + micro-daemon (zero config, auto-start).
## Extension Installation
### Method 1: Download Pre-built Release (Recommended)
1. Go to the GitHub [Releases page](https://github.com/jackwener/opencli/releases) and download the latest `opencli-extension-v{version}.zip`.
2. Unzip the file and open `chrome://extensions`, enable **Developer mode** (top-right toggle).
3. Click **Load unpacked** and select the unzipped folder.
### Method 2: Load Unpacked Source (For Developers)
1. Open `chrome://extensions` and enable **Developer mode**.
2. Click **Load unpacked** and select the `extension/` directory from the repository.
## Verification
That's it! The daemon auto-starts when you run any browser command. No tokens, no manual configuration.
```bash
opencli doctor # Check extension + daemon connectivity
```
## Tab Targeting
Browser commands require an explicit `<session>` positional immediately after `browser`. Use the same session name for a multi-step flow, and use different names to isolate parallel work.
```bash
opencli browser baidu open https://www.baidu.com/
opencli browser baidu tab list
opencli browser baidu tab new https://www.baidu.com/
opencli browser baidu eval --tab <targetId> 'document.title'
opencli browser baidu tab select <targetId>
opencli browser baidu get title
opencli browser baidu tab close <targetId>
```
Key rules:
- `opencli browser <session> open <url>` and `opencli browser <session> tab new [url]` return a `targetId`.
- `opencli browser <session> tab list` prints the `targetId` values of tabs that already exist.
- `--tab <targetId>` routes a single browser command to that specific tab.
- `tab new` creates a new tab but does not change the default browser target.
- `tab select <targetId>` makes that tab the default target for later untargeted `opencli browser ...` commands.
- `tab close <targetId>` removes the tab; if it was the current default target, the stored default is cleared.
## Session Lifecycle
Use a stable session name when you want multiple `opencli browser` commands to keep operating on the same page:
```bash
opencli browser my-session open https://example.com
opencli browser my-session state
opencli browser my-session extract "main"
```
Owned browser sessions use an interactive tab lease with a 10-minute idle timeout. Release it explicitly when done:
```bash
opencli browser my-session close
```
Use `opencli browser <session> bind` when you want to attach OpenCLI to a Chrome tab you already opened manually. Bound sessions do not have the owned-session idle close timer; they stay attached until `unbind`, tab close, window close, or daemon restart. For owned sessions, use `--window foreground` to watch OpenCLI work in a visible automation window, or `--window background` to keep that automation window out of the way.
The `OpenCLI Browser` and `OpenCLI Adapter` tab groups are extension-managed automation containers; avoid putting your own long-lived tabs in them or renaming them.
## How It Works
```
┌─────────────┐ WebSocket ┌──────────────┐ Chrome API ┌─────────┐
│ opencli │ ◄──────────────► │ micro-daemon │ ◄──────────────► │ Chrome │
│ (Node.js) │ localhost:19825 │ (auto-start) │ Extension │ Browser │
└─────────────┘ └──────────────┘ └─────────┘
```
The daemon manages the WebSocket connection between your CLI commands and the Chrome extension. The extension executes JavaScript in the context of web pages, with access to the logged-in session.
## Daemon Lifecycle
The daemon auto-starts on first browser command and stays alive persistently.
```bash
opencli daemon stop # Graceful shutdown
```
The daemon is persistent — it stays alive until you explicitly stop it (`opencli daemon stop`) or uninstall the package.
## Running OpenCLI from a remote machine
If you need to run `opencli` on a remote server (CI runner, agent host) but keep the browser session on your local machine, see [Remote Orchestration](/guide/remote-orchestration). It walks through the SSH reverse-tunnel pattern so the daemon never leaves localhost.
+199
View File
@@ -0,0 +1,199 @@
---
description: How to turn a new Electron desktop app into an OpenCLI adapter
---
# Add a New Electron App CLI
This guide is the **fast entry point** for turning a new Electron desktop application into an OpenCLI adapter.
If you want the full background and deeper SOP, read:
- [CLI-ifying Electron Applications](/advanced/electron)
- [Chrome DevTools Protocol](/advanced/cdp)
- [TypeScript Adapter Guide](/developer/ts-adapter)
## When to use this guide
Use this workflow when the target app:
- is built with **Electron**, or at least exposes a working **Chrome DevTools Protocol (CDP)** endpoint
- can be launched with `--remote-debugging-port=<port>`
- should be automated through its real UI instead of a public HTTP API
If the app is **not** Electron and does **not** expose CDP, use the native desktop automation pattern instead. See [CLI-ifying Electron Applications](/advanced/electron#non-electron-pattern-applescript).
## The shortest path
### 1. Confirm the app is Electron
Typical macOS check:
```bash
ls /Applications/AppName.app/Contents/Frameworks/Electron\ Framework.framework
```
If Electron is present, the next step is usually to launch the app with a debugging port.
### 2. Launch it with CDP enabled
```bash
/Applications/AppName.app/Contents/MacOS/AppName --remote-debugging-port=<unique-port>
```
Then point OpenCLI at that CDP endpoint:
```bash
export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:<unique-port>"
```
### 3. Start with the 5-command pattern
For a new Electron adapter, implement these commands first in `clis/<app>/`:
- `status.js` — verify the app is reachable through CDP
- `dump.js` — inspect DOM and snapshot structure before guessing selectors
- `read.js` — extract the visible context you actually need
- `send.js` — inject text and submit through the real editor
- `new.js` — create a new session, tab, thread, or document
This is the standard baseline because it gives you:
- a connection check
- a reverse-engineering tool
- one read path
- one write path
- one session reset path
The full rationale and examples are in [CLI-ifying Electron Applications](/advanced/electron).
## Recommended implementation workflow
### Step 1: Build `status`
Goal: prove CDP connectivity before touching app-specific logic.
Typical checks:
- current URL
- document title
- app shell presence
If `status` is unstable, stop there and fix connectivity first.
### Step 2: Build `dump`
Do **not** guess selectors from the rendered UI.
Dump:
- `document.body.innerHTML`
- accessibility snapshot
- any stable attributes such as `data-testid`, `role`, `aria-*`, framework-specific markers
Use the dump to identify real containers, buttons, composers, and conversation regions.
### Step 3: Build `read`
Target only the app region that matters.
Good targets:
- message list
- editor history
- visible thread content
- selected document panel
Avoid dumping the entire page text into the final command output.
### Step 4: Build `send`
Most Electron apps use React-style controlled editors, so direct `.value = ...` assignments are often ignored.
Prefer editor-aware input patterns such as:
- focus the editable region
- use `document.execCommand('insertText', false, text)` when applicable
- use real key presses like `Enter`, `Meta+Enter`, or app-specific shortcuts
### Step 5: Build `new`
Many desktop apps rely on keyboard shortcuts for “new chat”, “new tab”, or “new note”.
Typical pattern:
```ts
const isMac = process.platform === 'darwin';
await page.pressKey(isMac ? 'Meta+N' : 'Control+N');
await page.wait(1);
```
## Where to put files
For a desktop adapter, the usual layout is:
```text
clis/<app>/status.js
clis/<app>/dump.js
clis/<app>/read.js
clis/<app>/send.js
clis/<app>/new.js
clis/<app>/utils.js
```
If the app grows beyond the baseline, add higher-level commands such as:
- `ask`
- `history`
- `model`
- `screenshot`
- `export`
## What to document when you add a new app
When the adapter is ready, also add:
- an adapter doc under `docs/adapters/desktop/`
- command list and examples
- launch instructions with `--remote-debugging-port`
- any required environment variables
- platform-specific caveats
Examples to study:
- `docs/adapters/desktop/codex.md`
- `docs/adapters/desktop/chatwise.md`
- `docs/adapters/desktop/discord.md`
## Common failure modes
### CDP endpoint exists, but commands are flaky
Usually one of these:
- the wrong window/tab is selected
- the app has not finished rendering
- selectors were guessed instead of discovered from `dump`
- the editor is controlled and ignores direct value assignment
### The app is Chromium-based but not truly controllable
Some desktop apps embed Chromium but do not expose a usable CDP surface.
In that case, switch to the non-Electron desktop automation approach instead of forcing the Electron pattern.
### You already have a browser workflow and wonder whether to reuse it
If the app exposes a normal web URL and the browser flow is enough, a browser adapter is usually simpler.
Use an Electron adapter only when the desktop app is the real integration surface.
## Recommended reading order
If you are starting from zero:
1. This page
2. [CLI-ifying Electron Applications](/advanced/electron)
3. [Chrome DevTools Protocol](/advanced/cdp)
4. [TypeScript Adapter Guide](/developer/ts-adapter)
5. One concrete desktop adapter doc under `docs/adapters/desktop/`
## Practical rule
Do not start with a large feature surface.
Start with:
- `status`
- `dump`
- `read`
- `send`
- `new`
Once those are stable, extend outward.
+24
View File
@@ -0,0 +1,24 @@
# Exit Codes
`opencli` follows Unix `sysexits.h` conventions so it integrates naturally with shell pipelines and CI scripts.
| Code | Meaning | When |
|------|---------|------|
| `0` | Success | Command completed normally |
| `1` | Generic error | Unexpected / unclassified failure |
| `2` | Usage error | Bad arguments or unknown command |
| `66` | Empty result | No data returned (`EX_NOINPUT`) |
| `69` | Service unavailable | Browser Bridge not connected (`EX_UNAVAILABLE`) |
| `75` | Temporary failure | Command timed out — retry (`EX_TEMPFAIL`) |
| `77` | Auth required | Not logged in to target site (`EX_NOPERM`) |
| `78` | Config error | Missing credentials or bad config (`EX_CONFIG`) |
| `130` | Interrupted | Ctrl-C / SIGINT |
## Example: branch on exit code
```bash
opencli spotify status || echo "exit $?" # 69 if browser not running
opencli gh issue list 2>/dev/null
[ $? -eq 77 ] && opencli gh auth login # auto-auth if not logged in
```
+134
View File
@@ -0,0 +1,134 @@
# Extending OpenCLI
OpenCLI has five extension paths. Pick the path based on where you want the source code to live and how you want commands to be shared.
| Goal | Use | Source location | Command surface |
|------|-----|-----------------|-----------------|
| Build a personal website command in your own Git repo | Local plugin | Your project directory, symlinked into `~/.opencli/plugins/` | `opencli <plugin> <command>` |
| Quickly draft a private adapter on this machine | User adapter | `~/.opencli/clis/<site>/<command>.js` | `opencli <site> <command>` |
| Edit an official adapter locally | Adapter override | `~/.opencli/clis/<site>/` | `opencli <site> <command>` |
| Publish or install third-party commands | Plugin | Git repo, installed into `~/.opencli/plugins/` | `opencli <plugin> <command>` |
| Wrap an existing local binary | External CLI | `~/.opencli/external-clis.yaml` | `opencli <tool> ...` |
## Personal commands in your own Git repo
Use a local plugin when you want the code to stay in a normal project directory and be managed by Git.
```bash
opencli plugin create my-cnn
cd my-cnn
git init
opencli plugin install file://$(pwd)
opencli my-cnn hello
```
`plugin install file://...` creates a symlink under `~/.opencli/plugins/`. Your source files stay in your project directory, so edits and commits happen there.
This is the recommended path for custom commands you own long-term.
## Private adapters in `~/.opencli/clis`
Use a user adapter when you want the fastest local adapter loop and do not need a separate project directory.
```bash
opencli browser init cnn/top
# edit ~/.opencli/clis/cnn/top.js
opencli browser verify cnn/top
opencli cnn top
```
User adapters are loaded from:
```text
~/.opencli/clis/<site>/<command>.js
```
This path is convenient for quick local automation. For code you want to version, review, or share, prefer a plugin.
If the command takes required positional args and no fixture exists yet, seed the first verify run explicitly:
```bash
opencli browser verify instagram/collection-create --write-fixture --seed-args opencli-verify
opencli browser verify example/detail --write-fixture --seed-args '["https://example.com/item/1", "--limit", 3]'
```
`--seed-args` is only used when the fixture has no `args`. Once the fixture is written, `opencli browser verify` reads args from `~/.opencli/sites/<site>/verify/<command>.json`.
`browser verify` also enforces row shape before fixture checks: each row should
stay compact (at most 12 top-level keys), avoid nesting deeper than one level,
and keep id-shaped fields such as `id` / `user_id` at the top level.
## Local overrides for official adapters
Use `adapter eject` when you want to customize an existing official adapter.
```bash
opencli adapter eject twitter
# edit ~/.opencli/clis/twitter/*.js
opencli adapter reset twitter
```
Files in `~/.opencli/clis/<site>/<command>.js` override packaged adapters with the same `site/command` on this machine. `opencli browser verify <site>/<command>` also runs the local override, so a passing local verify does not prove that the packaged adapter was changed.
The packaged `cli-manifest.json` only describes bundled adapters. User adapters are discovered at runtime and do not need manifest entries.
After copying a local fix into the repository for a PR, remove the local copy or run `opencli adapter reset <site>` after merge. Otherwise the local file keeps shadowing future package updates. `opencli doctor` warns when it detects this shadowing.
## Plugins for sharing commands
Plugins are third-party command packages. They can be installed from GitHub, any git-cloneable URL, or a local directory.
```bash
opencli plugin install github:user/opencli-plugin-my-tool
opencli plugin install https://github.com/user/opencli-plugin-my-tool
opencli plugin install file:///absolute/path/to/plugin
opencli plugin list
opencli plugin update --all
opencli plugin uninstall my-tool
```
Each plugin directory is scanned for `.ts` and `.js` command files. TypeScript plugins are transpiled during install.
See [Plugins](./plugins.md) for manifest fields, TypeScript examples, update behavior, and monorepo publishing.
## Multiple custom sites in one repo
For a Git-hosted plugin collection, declare sub-plugins in `opencli-plugin.json` and install from GitHub:
```json
{
"plugins": {
"cnn": { "path": "packages/cnn" },
"reuters": { "path": "packages/reuters" }
}
}
```
```bash
opencli plugin install github:user/opencli-plugins
opencli plugin install github:user/opencli-plugins/cnn
```
For local development, install each sub-plugin directory directly:
```bash
opencli plugin install file:///absolute/path/opencli-plugins/packages/cnn
opencli plugin install file:///absolute/path/opencli-plugins/packages/reuters
```
Local `file://` installs expect the target directory itself to be a valid plugin with command files. For a monorepo root, push it to GitHub and install it with the GitHub monorepo flow.
## External CLI passthrough
Use external CLI registration when the command already exists as a binary on your machine and you want it available through `opencli`.
```bash
opencli external register my-tool \
--binary my-tool \
--install "npm i -g my-tool" \
--desc "My internal CLI"
opencli my-tool --help
```
External CLIs pass stdio and exit codes through to the underlying binary.
+81
View File
@@ -0,0 +1,81 @@
# Getting Started
> **Make any website or Electron App your CLI.**
> Zero risk · Reuse Chrome login · AI-powered discovery · Browser + Desktop automation
[![npm](https://img.shields.io/npm/v/@jackwener/opencli?style=flat-square)](https://www.npmjs.com/package/@jackwener/opencli)
[![Node.js Version](https://img.shields.io/node/v/@jackwener/opencli?style=flat-square)](https://nodejs.org)
[![License](https://img.shields.io/npm/l/@jackwener/opencli?style=flat-square)](https://github.com/jackwener/opencli/blob/main/LICENSE)
OpenCLI turns **any website** or **Electron app** into a command-line interface — Bilibili, Zhihu, 小红书, Twitter/X, Reddit, YouTube, Antigravity, and [many more](/adapters/) — powered by browser session reuse and AI-native discovery.
## Highlights
- **Desktop App Control** — Drive Electron apps (Cursor, Codex, ChatGPT, etc.) directly from the terminal via CDP.
- **Browser Automation** — `browser` gives AI agents direct browser control: click, type/fill, extract, screenshot — fully scriptable.
- **Website → CLI** — Turn any website into a deterministic CLI: 100+ site surfaces are already registered, or author your own with the `opencli-adapter-author` skill.
- **Account-safe** — Reuses Chrome's logged-in state; your credentials never leave the browser.
- **AI Agent ready** — `opencli browser *` primitives (`open` / `network` / `state` / `eval` / `init` / `verify`) drive the adapter-authoring loop.
- **Zero LLM cost** — No tokens consumed at runtime. Run 10,000 times and pay nothing.
- **Deterministic** — Same command, same output schema, every time. Pipeable, scriptable, CI-friendly.
## Quick Start
### Install via npm
```bash
npm install -g @jackwener/opencli
```
### Basic Usage
```bash
opencli list # See all commands
opencli hackernews top --limit 5 # Public API, no browser
opencli bilibili hot --limit 5 # Browser command
opencli zhihu hot -f json # JSON output
```
### Output Formats
All built-in commands support `--format` / `-f`:
```bash
opencli bilibili hot -f table # Default: rich terminal table
opencli bilibili hot -f json # JSON (pipe to jq or LLMs)
opencli bilibili hot -f yaml # YAML (human-readable)
opencli bilibili hot -f md # Markdown
opencli bilibili hot -f csv # CSV
opencli bilibili hot -v # Verbose: show pipeline debug
```
### Tab Completion
OpenCLI supports intelligent tab completion to speed up command input:
```bash
# Add shell completion to your startup config
echo 'eval "$(opencli completion zsh)"' >> ~/.zshrc # Zsh
echo 'eval "$(opencli completion bash)"' >> ~/.bashrc # Bash
echo 'opencli completion fish | source' >> ~/.config/fish/config.fish # Fish
# Restart your shell, then press Tab to complete:
opencli [Tab] # Complete site names (bilibili, zhihu, twitter...)
opencli bilibili [Tab] # Complete commands (hot, search, me, download...)
```
The completion includes:
- All available sites and adapters
- Built-in commands (list, validate, verify, browser, doctor, plugin...)
- Command aliases
- Real-time updates as you add new adapters
## Next Steps
- [Installation details](/guide/installation)
- [Browser Bridge setup](/guide/browser-bridge)
- [Extending OpenCLI — custom commands, plugins, and external CLIs](/guide/extending-opencli)
- [Plugins — extend with community adapters](/guide/plugins)
- [All available adapters](/adapters/)
- [For developers / AI agents](/developer/contributing)
- [Add a new Electron app CLI](/guide/electron-app-cli)
+52
View File
@@ -0,0 +1,52 @@
# Installation
## Requirements
- **Node.js**: >= 21.0.0, or **Bun** >= 1.0
- **Chrome** running and logged into the target site (for browser commands)
## Install via npm (Recommended)
```bash
npm install -g @jackwener/opencli
```
## Install from Source
```bash
git clone git@github.com:jackwener/opencli.git
cd opencli
npm install
npm run build
npm link # Link binary globally
opencli list # Now you can use it anywhere!
```
## Update
```bash
npm install -g @jackwener/opencli@latest
# If you use the packaged OpenCLI skills, refresh them too
npx skills add jackwener/opencli
```
Or refresh only the skills you actually use:
```bash
npx skills add jackwener/opencli --skill opencli-adapter-author
npx skills add jackwener/opencli --skill opencli-autofix
npx skills add jackwener/opencli --skill opencli-browser
npx skills add jackwener/opencli --skill opencli-browser-sitemap
npx skills add jackwener/opencli --skill opencli-sitemap-author
npx skills add jackwener/opencli --skill opencli-usage
npx skills add jackwener/opencli --skill smart-search
```
## Verify Installation
```bash
opencli --version # Check version
opencli list # List all commands
opencli doctor # Diagnose connectivity
```
+226
View File
@@ -0,0 +1,226 @@
# Plugins
OpenCLI supports community-contributed plugins. Install third-party adapters from GitHub, and they're automatically discovered alongside built-in commands.
## Quick Start
```bash
# Install a plugin
opencli plugin install github:ByteYue/opencli-plugin-github-trending
# List installed plugins
opencli plugin list
# Update one plugin
opencli plugin update github-trending
# Update all installed plugins
opencli plugin update --all
# Use the plugin (it's just a regular command)
opencli github-trending repos --limit 10
# Remove a plugin
opencli plugin uninstall github-trending
```
## How Plugins Work
Plugins live in `~/.opencli/plugins/<name>/`. Each subdirectory is scanned at startup for `.ts` or `.js` command files — the same formats used by built-in adapters.
### Supported Source Formats
```bash
# GitHub shorthand
opencli plugin install github:user/repo
opencli plugin install github:user/repo/subplugin # install specific sub-plugin from monorepo
opencli plugin install https://github.com/user/repo
# Any git-cloneable URL
opencli plugin install https://gitlab.example.com/team/repo.git
opencli plugin install ssh://git@gitlab.example.com/team/repo.git
opencli plugin install git@gitlab.example.com:team/repo.git
# Local plugin (for development)
opencli plugin install file:///path/to/plugin
opencli plugin install /path/to/plugin
```
The repo name prefix `opencli-plugin-` is automatically stripped for the local directory name. For example, `opencli-plugin-hot-digest` becomes `hot-digest`.
## Plugin Manifest (`opencli-plugin.json`)
Plugins can include an `opencli-plugin.json` manifest file at the repo root to declare metadata:
```json
{
"name": "my-plugin",
"version": "1.0.0",
"opencli": ">=1.0.0",
"description": "My awesome plugin"
}
```
| Field | Description |
|-------|-------------|
| `name` | Plugin name (overrides repo-derived name) |
| `version` | Semantic version |
| `opencli` | Required opencli version range (e.g. `>=1.0.0`, `^1.2.0`) |
| `description` | Human-readable description |
| `plugins` | Monorepo sub-plugin declarations (see below) |
The manifest is optional — plugins without one continue to work exactly as before.
## Monorepo Plugins
A single repository can contain multiple plugins by declaring a `plugins` field in `opencli-plugin.json`:
```json
{
"version": "1.0.0",
"opencli": ">=1.0.0",
"description": "My plugin collection",
"plugins": {
"polymarket": {
"path": "packages/polymarket",
"description": "Prediction market analysis",
"version": "1.2.0"
},
"defi": {
"path": "packages/defi",
"description": "DeFi protocol data",
"version": "0.8.0",
"opencli": ">=1.2.0"
},
"experimental": {
"path": "packages/experimental",
"disabled": true
}
}
}
```
### Installing
```bash
# Install ALL enabled sub-plugins from a monorepo
opencli plugin install github:user/opencli-plugins
# Install a SPECIFIC sub-plugin
opencli plugin install github:user/opencli-plugins/polymarket
```
### How It Works
- The monorepo is cloned once to `~/.opencli/monorepos/<repo>/`
- Each sub-plugin gets a symlink in `~/.opencli/plugins/<name>/` pointing to its subdirectory
- Command discovery works transparently — symlinks are scanned just like regular directories
- Disabled sub-plugins (with `"disabled": true`) are skipped during install
- Sub-plugins can specify their own `opencli` compatibility range
### Updating
Updating any sub-plugin from a monorepo pulls the entire repo and refreshes all sub-plugins:
```bash
opencli plugin update polymarket # updates the monorepo, refreshes all
```
### Uninstalling
```bash
opencli plugin uninstall polymarket # removes just this sub-plugin's symlink
```
When the last sub-plugin from a monorepo is uninstalled, the monorepo clone is automatically cleaned up.
## Version Tracking
OpenCLI records installed plugin versions in `~/.opencli/plugins.lock.json`. Each entry stores the plugin source, current git commit hash, install time, and last update time. `opencli plugin list` shows the short commit hash when version metadata is available.
## Creating a Plugin
### Creating a TypeScript Plugin
```
my-plugin/
├── package.json
├── my-command.ts
└── README.md
```
`package.json`:
```json
{
"name": "opencli-plugin-my-plugin",
"version": "0.1.0",
"type": "module",
"peerDependencies": {
"@jackwener/opencli": ">=1.0.0"
}
}
```
`my-command.ts`:
```typescript
import { cli, Strategy } from '@jackwener/opencli/registry';
cli({
site: 'my-plugin',
name: 'my-command',
description: 'My custom command',
access: 'read', // 'read' | 'write'
example: 'opencli my-plugin my-command -f yaml',
strategy: Strategy.PUBLIC,
browser: false,
args: [
{ name: 'limit', type: 'int', default: 10, help: 'Number of items' },
],
columns: ['title', 'score'],
func: async (kwargs) => {
const res = await fetch('https://api.example.com/data');
const data = await res.json();
return data.items.slice(0, kwargs.limit).map((item: any, i: number) => ({
title: item.title,
score: item.score,
}));
},
});
```
### TS Plugin Install Lifecycle
When you run `opencli plugin install`, TS plugins are automatically set up:
1. **Clone**`git clone --depth 1` from GitHub
2. **npm install** — Resolves regular dependencies
3. **Host symlink** — Links the running `@jackwener/opencli` into the plugin's `node_modules/` so `import from '@jackwener/opencli/registry'` always resolves against the host
4. **Transpile** — Compiles `.ts``.js` via `esbuild` (production `node` cannot load `.ts` directly)
On startup, if both `my-command.ts` and `my-command.js` exist, the `.js` version is loaded to avoid duplicate registration.
## Example Plugins
| Repo | Type | Description |
|------|------|-------------|
| [opencli-plugin-github-trending](https://github.com/ByteYue/opencli-plugin-github-trending) | TS | GitHub Trending repositories |
| [opencli-plugin-hot-digest](https://github.com/ByteYue/opencli-plugin-hot-digest) | TS | Multi-platform trending aggregator (zhihu, weibo, bilibili, v2ex, stackoverflow, reddit, linux-do) |
| [opencli-plugin-juejin](https://github.com/Astro-Han/opencli-plugin-juejin) | TS | 稀土掘金 (Juejin) hot articles, categories, and article feed |
| [opencli-plugin-rubysec](https://github.com/nullptrKey/opencli-plugin-rubysec) | TS | RubySec advisory archive and advisory article reader |
## Troubleshooting
### Command not found after install
Restart opencli (or open a new terminal) — plugins are discovered at startup.
### TS plugin import errors
If you see `Cannot find module '@jackwener/opencli/registry'`, the host symlink may be broken. Reinstall the plugin:
```bash
opencli plugin uninstall my-plugin
opencli plugin install github:user/opencli-plugin-my-plugin
```
+116
View File
@@ -0,0 +1,116 @@
# Remote Orchestration
Run an OpenCLI command from a remote machine (a CI runner, an agent server, a sandbox) while the **browser session stays on your local laptop**. The remote command sees `localhost:19825` like usual; behind the scenes, traffic is tunneled back to the daemon and Chrome on your machine.
## When you need this
- An autonomous agent (OpenClaw, a CI job, a server-side script) needs to drive a logged-in browser session, but only your local Chrome has the cookies.
- Target sites do IP-based throttling or risk control — you want web traffic to leave from your home network, not from the agent's data center.
- The remote machine has no display and no Chrome installed.
## What not to do (and why)
The first instinct is "let the extension connect to a remote daemon" — type a public host into the popup, expose port 19825 with frp, done. **Don't.** The daemon's WebSocket protocol has no built-in authentication. Anything that can reach the port can:
- Read cookies for every site you're logged into
- Execute arbitrary JavaScript in any of your tabs
- Take screenshots, send arbitrary HTTP requests, dump page content
Treat the daemon port the way you'd treat your unlocked desktop: never put it on a network you don't fully trust. Native "extension talks to remote daemon" support was [proposed in #636](https://github.com/jackwener/OpenCLI/pull/636) and deferred until daemon authentication exists.
## The pattern: reverse-tunnel a localhost daemon
Keep Chrome, the extension, and the daemon **all on your local machine**. Use a reverse port forward so the remote process can reach your daemon by connecting to its own `localhost:19825`. The daemon itself never leaves localhost.
```
┌─ Local ─────────────────────────────────┐ ┌─ Remote ──────────┐
│ Chrome ↔ Extension ↔ Daemon (127.0.0.1) │ ←┐ │ opencli-cli │
└──────────────────────────────────────────┘ │ │ (talks to │
│ │ localhost:19825)│
reverse tunnel ────────┘ └───────────────────┘
(SSH -R / frpc / VPN)
```
The remote `opencli` process needs **no flags, no env vars, no extension changes** — it connects to its own loopback, which the tunnel forwards to your laptop.
## Option A — SSH reverse port forward (recommended)
From your local machine, SSH to the remote with `-R` to expose your local daemon to the remote's loopback:
```bash
ssh -R 19825:127.0.0.1:19825 user@remote-server
```
While that session is open, anything on the remote connecting to `localhost:19825` is forwarded back to your local daemon. The remote-side workflow is unchanged:
```bash
# On the remote server
opencli twitter feed
opencli browser open https://example.com
```
::: tip
Use `127.0.0.1` (not `localhost`) on the `-R` clause to avoid IPv6 resolution stalls.
:::
For long-lived agent runs, use `autossh` so the tunnel reconnects automatically:
```bash
autossh -M 0 -N -R 19825:127.0.0.1:19825 user@remote-server
```
Or as a systemd unit / launchd plist on the local side.
### Why this is safe
- The daemon stays bound to `127.0.0.1` on your machine.
- The tunnel rides on SSH's authenticated transport — no new auth surface.
- If the SSH session drops, the tunnel drops; the remote `opencli` simply fails to connect rather than reaching some stale endpoint.
## Option B — frp reverse TCP proxy
If SSH from your local machine to the remote isn't an option (NAT, firewalls), use [frp](https://github.com/fatedier/frp) to expose the local daemon through a public relay. **This is more complex than SSH and has more failure modes — prefer Option A unless you have a hard reason not to.**
1. Run **frps** on a public relay you control.
2. Run **frpc** on your local machine, exposing daemon port 19825:
```toml
# ~/frpc.toml on your local machine
serverAddr = "<public-relay-ip>"
serverPort = 7000
auth.method = "token"
auth.token = "<long-random-token>"
[[proxies]]
name = "opencli-daemon"
type = "tcp"
localIP = "127.0.0.1"
localPort = 19825
remotePort = 19825
```
3. On the remote server, run a second **frpc** that maps the relay's exposed port back to the remote's `localhost:19825` (a `stcp` visitor or a plain `tcp` client+visitor pair). This way `opencli` on the remote keeps talking to its own loopback.
::: warning
- Always set a strong `auth.token` on frps and frpc. Without it, anyone who learns the relay address has full control of your browser.
- Bind the relay's exposed port to a private interface or restrict it with iptables / security groups. A daemon port on the public internet is the same risk as the rejected #636 design.
- If you find yourself debugging frp auth, revisit Option A — it's cheaper and safer.
:::
## Verification
After setting up the tunnel, confirm the remote sees the daemon:
```bash
# On the remote server
curl -sf http://127.0.0.1:19825/ping && echo "daemon reachable"
opencli doctor
```
`opencli doctor` from the remote should report the same extension version your local Chrome is running.
## Caveats
- **Local Chrome must be running** for the duration of the remote command. Closing Chrome closes the extension's WebSocket; remote `opencli` calls will fail with a "no daemon" error until Chrome is reopened.
- **Tunnel latency adds to every call**. Each browser command crosses the tunnel twice; expect 50200ms overhead per call on a typical SSH link, more on transcontinental links.
- **One tunnel per local daemon**. If you start multiple SSH sessions all forwarding 19825, only the first wins; the rest log a "remote port already in use" warning.
+69
View File
@@ -0,0 +1,69 @@
# Troubleshooting
## Common Issues
### "Extension not connected"
- Ensure the opencli Browser Bridge extension is installed and **enabled** in `chrome://extensions`.
- Run `opencli doctor` to diagnose connectivity.
### Empty data or 'Unauthorized' error
- Your login session in Chrome might have expired. Open a normal Chrome tab, navigate to the target site, and log in or refresh the page.
- Some sites have geographic restrictions (e.g., Bilibili, Zhihu from outside China).
### Browser command opens the page but still cannot read context
- A healthy Browser Bridge connection does not guarantee that the current page target exposes the data your adapter expects.
- Some browser adapters are sensitive to the active host or page context.
- Example: `opencli 1688 item` may fail with `did not expose product context` if the target is too broad.
- Retry on a real item page, refresh the page in Chrome, and if needed narrow the target, for example:
```bash
OPENCLI_CDP_TARGET=detail.1688.com opencli 1688 item 841141931191 -f json
```
### Node API errors
- Make sure you are using **Node.js >= 20**. Run `node --version` to verify.
### Daemon issues
```bash
# View extension logs
curl localhost:19825/logs
# Stop the daemon
opencli daemon stop
# Full diagnostics
opencli doctor
```
> The daemon is persistent and stays alive until explicitly stopped (`opencli daemon stop`) or the package is uninstalled.
> When the CLI detects a stale daemon (version mismatch after `npm install -g @jackwener/opencli@latest`), it first asks the daemon to shut down via `/shutdown`, then falls back to `SIGKILL` if the daemon does not release the port within 3 seconds. Manual `opencli daemon stop` is only needed if SIGKILL itself is rejected (cross-user owner / cross-machine PID file).
### Desktop adapter connection issues
For Electron/CDP-based adapters (Cursor, Codex, etc.):
1. Make sure the app is launched with `--remote-debugging-port=XXXX`
2. Verify the endpoint is set: `echo $OPENCLI_CDP_ENDPOINT`
3. Test the endpoint: `curl http://127.0.0.1:XXXX/json/version`
### Build errors
```bash
# Clean rebuild
rm -rf dist/
npm run build
# Type check
npx tsc --noEmit
```
## Getting Help
- [GitHub Issues](https://github.com/jackwener/opencli/issues) — Bug reports and feature requests
- Run `opencli doctor` for comprehensive diagnostics