@jackwener/opencli 1.5.9 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +18 -0
- package/SKILL.md +59 -0
- package/autoresearch/baseline-browse.txt +1 -0
- package/autoresearch/baseline-skill.txt +1 -0
- package/autoresearch/browse-tasks.json +688 -0
- package/autoresearch/eval-browse.ts +185 -0
- package/autoresearch/eval-skill.ts +248 -0
- package/autoresearch/run-browse.sh +9 -0
- package/autoresearch/run-skill.sh +9 -0
- package/dist/browser/daemon-client.d.ts +20 -1
- package/dist/browser/daemon-client.js +37 -30
- package/dist/browser/daemon-client.test.d.ts +1 -0
- package/dist/browser/daemon-client.test.js +77 -0
- package/dist/browser/discover.js +8 -19
- package/dist/browser/page.d.ts +4 -0
- package/dist/browser/page.js +48 -1
- package/dist/cli.js +392 -0
- package/dist/clis/twitter/article.js +28 -1
- package/dist/clis/xiaohongshu/note.js +11 -0
- package/dist/clis/xiaohongshu/note.test.js +49 -0
- package/dist/commanderAdapter.js +1 -1
- package/dist/commanderAdapter.test.js +43 -0
- package/dist/commands/daemon.js +7 -46
- package/dist/commands/daemon.test.js +44 -69
- package/dist/discovery.js +27 -0
- package/dist/types.d.ts +8 -0
- package/docs/guide/getting-started.md +21 -0
- package/docs/superpowers/specs/2026-04-02-browse-skill-testing-design.md +144 -0
- package/docs/zh/guide/getting-started.md +21 -0
- package/extension/package-lock.json +2 -2
- package/extension/src/background.ts +51 -4
- package/extension/src/cdp.ts +77 -124
- package/extension/src/protocol.ts +5 -1
- package/package.json +1 -1
- package/skills/opencli-explorer/SKILL.md +6 -0
- package/skills/opencli-oneshot/SKILL.md +6 -0
- package/skills/opencli-operate/SKILL.md +213 -0
- package/skills/opencli-usage/SKILL.md +113 -32
- package/src/browser/daemon-client.test.ts +103 -0
- package/src/browser/daemon-client.ts +53 -30
- package/src/browser/discover.ts +8 -17
- package/src/browser/page.ts +48 -1
- package/src/cli.ts +392 -0
- package/src/clis/twitter/article.ts +31 -1
- package/src/clis/xiaohongshu/note.test.ts +51 -0
- package/src/clis/xiaohongshu/note.ts +18 -0
- package/src/commanderAdapter.test.ts +62 -0
- package/src/commanderAdapter.ts +1 -1
- package/src/commands/daemon.test.ts +49 -83
- package/src/commands/daemon.ts +7 -55
- package/src/discovery.ts +22 -0
- package/src/doctor.ts +1 -1
- package/src/types.ts +8 -0
- package/extension/dist/background.js +0 -681
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: opencli-usage
|
|
3
|
-
description: "OpenCLI
|
|
4
|
-
version: 1.
|
|
3
|
+
description: "Use when running OpenCLI commands to interact with websites (Bilibili, Twitter, Reddit, Xiaohongshu, etc.), desktop apps (Cursor, Notion), or public APIs (HackerNews, arXiv). Covers installation, command reference, and output formats for 60+ adapters."
|
|
4
|
+
version: 1.6.0
|
|
5
5
|
author: jackwener
|
|
6
|
-
tags: [cli, browser, web, chrome-extension, cdp, AI, agent]
|
|
6
|
+
tags: [opencli, cli, browser, web, chrome-extension, cdp, bilibili, twitter, reddit, xiaohongshu, github, youtube, AI, agent, automation]
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# OpenCLI Usage Guide
|
|
@@ -36,36 +36,117 @@ Browser commands require:
|
|
|
36
36
|
|
|
37
37
|
Public API commands (`hackernews`, `v2ex`) need no browser.
|
|
38
38
|
|
|
39
|
-
##
|
|
39
|
+
## Quick Lookup by Capability
|
|
40
|
+
|
|
41
|
+
| Capability | Platforms (partial list) | File |
|
|
42
|
+
|-----------|--------------------------|------|
|
|
43
|
+
| **search** | Bilibili, Twitter, Reddit, Xiaohongshu, Zhihu, YouTube, Google, arXiv, LinkedIn, Pixiv, etc. | browser.md / public-api.md |
|
|
44
|
+
| **hot/trending** | Bilibili, Twitter, Weibo, HackerNews, Reddit, V2EX, Xueqiu, Lobsters, Douban | browser.md / public-api.md |
|
|
45
|
+
| **feed/timeline** | Twitter, Reddit, Xiaohongshu, Xueqiu, Jike, Facebook, Instagram, Medium | browser.md |
|
|
46
|
+
| **user/profile** | Twitter, Reddit, Instagram, TikTok, Facebook, Bilibili, Pixiv | browser.md |
|
|
47
|
+
| **post/create** | Twitter, Jike | browser.md |
|
|
48
|
+
| **AI chat** | Grok, Doubao, Kimi, DeepSeek, Qwen, ChatGPT, Cursor, Codex | browser.md / desktop.md |
|
|
49
|
+
| **finance/stock** | Xueqiu, Yahoo Finance, Barchart, Sina Finance, Bloomberg | browser.md / public-api.md |
|
|
50
|
+
| **web scraping** | `opencli web read --url <url>` — any URL to Markdown | browser.md |
|
|
51
|
+
|
|
52
|
+
## Command Quick Reference
|
|
53
|
+
|
|
54
|
+
Usage: `opencli <site> <command> [args] [--limit N] [-f json|yaml|md|csv|table]`
|
|
55
|
+
|
|
56
|
+
### Browser-based (login required)
|
|
57
|
+
|
|
58
|
+
| Site | Commands |
|
|
59
|
+
|------|----------|
|
|
60
|
+
| **bilibili** | `hot` `search` `me` `favorite` `history` `feed` `user-videos` `subtitle` `dynamic` `ranking` `following` |
|
|
61
|
+
| **zhihu** | `hot` `search` `question` |
|
|
62
|
+
| **xiaohongshu** | `search` `notifications` `feed` `user` `creator-notes` `creator-note-detail` `creator-notes-summary` `creator-profile` `creator-stats` |
|
|
63
|
+
| **xueqiu** | `hot-stock` `stock` `watchlist` `feed` `hot` `search` `earnings-date` `fund-holdings` `fund-snapshot` |
|
|
64
|
+
| **twitter** | `trending` `bookmarks` `search` `profile` `timeline` `thread` `article` `follow` `unfollow` `bookmark` `unbookmark` `post` `like` `reply` `delete` `block` `unblock` `followers` `following` `notifications` `hide-reply` `download` `accept` `reply-dm` |
|
|
65
|
+
| **reddit** | `hot` `frontpage` `popular` `search` `subreddit` `read` `user` `user-posts` `user-comments` `upvote` `save` `comment` `subscribe` `saved` `upvoted` |
|
|
66
|
+
| **youtube** | `search` `video` `transcript` |
|
|
67
|
+
| **facebook** | `feed` `profile` `search` `friends` `groups` `events` `notifications` `memories` `add-friend` `join-group` |
|
|
68
|
+
| **instagram** | `explore` `profile` `search` `user` `followers` `following` `follow` `unfollow` `like` `unlike` `comment` `save` `unsave` `saved` |
|
|
69
|
+
| **tiktok** | `explore` `search` `profile` `user` `following` `follow` `unfollow` `like` `unlike` `comment` `save` `unsave` `live` `notifications` `friends` |
|
|
70
|
+
| **linkedin** | `search` `timeline` |
|
|
71
|
+
| **medium** | `feed` `search` `user` |
|
|
72
|
+
| **substack** | `feed` `search` `publication` |
|
|
73
|
+
| **sinablog** | `hot` `search` `article` `user` |
|
|
74
|
+
| **weibo** | `hot` |
|
|
75
|
+
| **boss** | `search` `detail` `recommend` `joblist` `greet` `batchgreet` `send` `chatlist` `chatmsg` `invite` `mark` `exchange` `resume` `stats` |
|
|
76
|
+
| **douban** | `search` `top250` `subject` `photos` `download` `marks` `reviews` |
|
|
77
|
+
| **pixiv** | `ranking` `search` `user` `illusts` `detail` `download` |
|
|
78
|
+
| **jike** | `feed` `search` `create` `like` `comment` `repost` `notifications` |
|
|
79
|
+
| **yahoo-finance** | `quote` |
|
|
80
|
+
| **barchart** | `quote` `options` `greeks` `flow` |
|
|
81
|
+
| **sinafinance** | `news` |
|
|
82
|
+
| **reuters** | `search` |
|
|
83
|
+
| **coupang** | `search` `add-to-cart` |
|
|
84
|
+
| **jd** | `item` |
|
|
85
|
+
| **smzdm** | `search` |
|
|
86
|
+
| **ctrip** | `search` |
|
|
87
|
+
| **weread** | `shelf` `search` `book` `highlights` `notes` `ranking` |
|
|
88
|
+
| **chaoxing** | `assignments` `exams` |
|
|
89
|
+
| **jimeng** | `generate` `history` |
|
|
90
|
+
| **yollomi** | `models` `generate` `video` `upload` `remove-bg` `edit` `background` `face-swap` `object-remover` `restore` `try-on` `upscale` |
|
|
91
|
+
| **web** | `read` — any URL to Markdown |
|
|
92
|
+
| **weixin** | `download` — 公众号 article to Markdown |
|
|
93
|
+
| **v2ex** (browser) | `daily` `me` `notifications` |
|
|
94
|
+
| **linux-do** (browser) | `categories` `category` |
|
|
95
|
+
| **bloomberg** (browser) | `news` — full article reader |
|
|
96
|
+
| **grok** | `ask` |
|
|
97
|
+
| **doubao** | `status` `new` `send` `read` `ask` |
|
|
98
|
+
| **kimi** | `status` `new` `ask` |
|
|
99
|
+
| **deepseek** | `status` `new` `ask` |
|
|
100
|
+
| **qwen** | `status` `new` `ask` |
|
|
101
|
+
|
|
102
|
+
### Desktop (CDP/Electron)
|
|
103
|
+
|
|
104
|
+
| Site | Commands |
|
|
105
|
+
|------|----------|
|
|
106
|
+
| **gh** | `repo` `pr` `issue` — passthrough to gh CLI |
|
|
107
|
+
| **cursor** | `status` `send` `read` `new` `dump` `composer` `model` `extract-code` `ask` `screenshot` `history` `export` |
|
|
108
|
+
| **codex** | `status` `send` `read` `new` `dump` `extract-diff` `model` `ask` `screenshot` `history` `export` |
|
|
109
|
+
| **chatgpt** | `status` `new` `send` `read` `ask` |
|
|
110
|
+
| **chatwise** | `status` `new` `send` `read` `ask` `model` `history` `export` `screenshot` |
|
|
111
|
+
| **notion** | `status` `search` `read` `new` `write` `sidebar` `favorites` `export` |
|
|
112
|
+
| **discord-app** | `status` `send` `read` `channels` `servers` `search` `members` |
|
|
113
|
+
| **doubao-app** | `status` `new` `send` `read` `ask` `screenshot` `dump` |
|
|
114
|
+
| **antigravity** | `status` `send` `read` `new` `dump` `extract-code` `model` `watch` |
|
|
115
|
+
|
|
116
|
+
### Public API (no browser)
|
|
117
|
+
|
|
118
|
+
| Site | Commands |
|
|
119
|
+
|------|----------|
|
|
120
|
+
| **hackernews** | `top` `new` `best` `ask` `show` `jobs` `search` `user` |
|
|
121
|
+
| **v2ex** (public) | `hot` `latest` `topic` `node` `nodes` `member` `user` `replies` |
|
|
122
|
+
| **bbc** | `news` |
|
|
123
|
+
| **lobsters** | `hot` `newest` `active` `tag` |
|
|
124
|
+
| **google** | `news` `search` `suggest` `trends` |
|
|
125
|
+
| **devto** | `top` `tag` `user` |
|
|
126
|
+
| **steam** | `top-sellers` |
|
|
127
|
+
| **apple-podcasts** | `top` `search` `episodes` |
|
|
128
|
+
| **arxiv** | `search` `paper` |
|
|
129
|
+
| **bloomberg** (RSS) | `main` `markets` `tech` `politics` `economics` `opinions` `industries` `businessweek` `feeds` |
|
|
130
|
+
| **dictionary** | `search` `synonyms` `examples` |
|
|
131
|
+
| **hf** | `top` |
|
|
132
|
+
| **stackoverflow** | `hot` `search` `bounties` |
|
|
133
|
+
| **xiaoyuzhou** | `podcast` `podcast-episodes` `episode` |
|
|
134
|
+
| **wikipedia** | `search` `summary` |
|
|
135
|
+
| **producthunt** | `today` `week` `month` `search` |
|
|
136
|
+
|
|
137
|
+
### Management
|
|
40
138
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
- Twitter/X, Reddit, Facebook, Instagram, TikTok, LinkedIn
|
|
49
|
-
- YouTube, Medium, Substack, Sinablog (新浪博客)
|
|
50
|
-
- V2EX (browser features), Weibo (微博), Jike (即刻), Linux.do (browser features)
|
|
51
|
-
- BOSS直聘, Coupang (쿠팡), JD (京东), SMZDM (什么值得买), Ctrip (携程)
|
|
52
|
-
- Yahoo Finance, Sina Finance, Reuters, Barchart, Bloomberg (full article)
|
|
53
|
-
- Douban (豆瓣), Chaoxing (超星学习通), WeRead (微信读书), Pixiv
|
|
54
|
-
- Yollomi, Jimeng (即梦 AI), Web, Weixin (微信公众号)
|
|
55
|
-
- Grok, Doubao Web (豆包), Kimi, DeepSeek, Qwen (通义千问)
|
|
56
|
-
|
|
57
|
-
### 🖥️ Desktop Adapter Commands
|
|
58
|
-
Commands that interact with desktop applications via CDP or external CLI tools. See [desktop.md](./desktop.md) for full reference.
|
|
139
|
+
```bash
|
|
140
|
+
opencli list [-f json|yaml] # List all commands
|
|
141
|
+
opencli validate [site] # Validate adapter definitions
|
|
142
|
+
opencli doctor # Diagnose browser bridge
|
|
143
|
+
opencli explore <url> # AI-powered API discovery
|
|
144
|
+
opencli record <url> # Record API calls manually
|
|
145
|
+
```
|
|
59
146
|
|
|
60
|
-
|
|
61
|
-
- GitHub (via gh CLI), Cursor, Codex, ChatGPT, ChatWise
|
|
62
|
-
- Notion, Discord App, Doubao App (豆包桌面版), Antigravity
|
|
147
|
+
All commands support: `--format` / `-f` with `table` `json` `yaml` `md` `csv`
|
|
63
148
|
|
|
64
|
-
|
|
65
|
-
Commands that work without browser or authentication. See [public-api.md](./public-api.md) for full reference.
|
|
149
|
+
## Related Skills
|
|
66
150
|
|
|
67
|
-
**
|
|
68
|
-
-
|
|
69
|
-
- Lobsters, Google, DEV.to, Steam, Apple Podcasts
|
|
70
|
-
- arXiv, Bloomberg (RSS), Dictionary, HuggingFace
|
|
71
|
-
- StackOverflow, Xiaoyuzhou (小宇宙), Wikipedia, Product Hunt
|
|
151
|
+
- **opencli-explorer** — Full guide for creating new adapters (API discovery, auth strategy, YAML/TS writing)
|
|
152
|
+
- **opencli-oneshot** — Quick 4-step template for adding a single command from a URL
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
fetchDaemonStatus,
|
|
5
|
+
isDaemonRunning,
|
|
6
|
+
isExtensionConnected,
|
|
7
|
+
requestDaemonShutdown,
|
|
8
|
+
} from './daemon-client.js';
|
|
9
|
+
|
|
10
|
+
describe('daemon-client', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
vi.stubGlobal('fetch', vi.fn());
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
vi.restoreAllMocks();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('fetchDaemonStatus sends the shared status request and returns parsed data', async () => {
|
|
20
|
+
const status = {
|
|
21
|
+
ok: true,
|
|
22
|
+
pid: 123,
|
|
23
|
+
uptime: 10,
|
|
24
|
+
extensionConnected: true,
|
|
25
|
+
extensionVersion: '1.2.3',
|
|
26
|
+
pending: 0,
|
|
27
|
+
lastCliRequestTime: Date.now(),
|
|
28
|
+
memoryMB: 32,
|
|
29
|
+
port: 19825,
|
|
30
|
+
};
|
|
31
|
+
const fetchMock = vi.mocked(fetch);
|
|
32
|
+
fetchMock.mockResolvedValue({
|
|
33
|
+
ok: true,
|
|
34
|
+
json: () => Promise.resolve(status),
|
|
35
|
+
} as Response);
|
|
36
|
+
|
|
37
|
+
await expect(fetchDaemonStatus()).resolves.toEqual(status);
|
|
38
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
39
|
+
expect.stringMatching(/\/status$/),
|
|
40
|
+
expect.objectContaining({
|
|
41
|
+
headers: expect.objectContaining({ 'X-OpenCLI': '1' }),
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('fetchDaemonStatus returns null on network failure', async () => {
|
|
47
|
+
vi.mocked(fetch).mockRejectedValue(new Error('ECONNREFUSED'));
|
|
48
|
+
|
|
49
|
+
await expect(fetchDaemonStatus()).resolves.toBeNull();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('requestDaemonShutdown POSTs to the shared shutdown endpoint', async () => {
|
|
53
|
+
const fetchMock = vi.mocked(fetch);
|
|
54
|
+
fetchMock.mockResolvedValue({ ok: true } as Response);
|
|
55
|
+
|
|
56
|
+
await expect(requestDaemonShutdown()).resolves.toBe(true);
|
|
57
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
58
|
+
expect.stringMatching(/\/shutdown$/),
|
|
59
|
+
expect.objectContaining({
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: expect.objectContaining({ 'X-OpenCLI': '1' }),
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('isDaemonRunning reflects shared status availability', async () => {
|
|
67
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
68
|
+
ok: true,
|
|
69
|
+
json: () =>
|
|
70
|
+
Promise.resolve({
|
|
71
|
+
ok: true,
|
|
72
|
+
pid: 123,
|
|
73
|
+
uptime: 10,
|
|
74
|
+
extensionConnected: false,
|
|
75
|
+
pending: 0,
|
|
76
|
+
lastCliRequestTime: Date.now(),
|
|
77
|
+
memoryMB: 16,
|
|
78
|
+
port: 19825,
|
|
79
|
+
}),
|
|
80
|
+
} as Response);
|
|
81
|
+
|
|
82
|
+
await expect(isDaemonRunning()).resolves.toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('isExtensionConnected reflects shared status payload', async () => {
|
|
86
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
87
|
+
ok: true,
|
|
88
|
+
json: () =>
|
|
89
|
+
Promise.resolve({
|
|
90
|
+
ok: true,
|
|
91
|
+
pid: 123,
|
|
92
|
+
uptime: 10,
|
|
93
|
+
extensionConnected: false,
|
|
94
|
+
pending: 0,
|
|
95
|
+
lastCliRequestTime: Date.now(),
|
|
96
|
+
memoryMB: 16,
|
|
97
|
+
port: 19825,
|
|
98
|
+
}),
|
|
99
|
+
} as Response);
|
|
100
|
+
|
|
101
|
+
await expect(isExtensionConnected()).resolves.toBe(false);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -11,6 +11,7 @@ import { isTransientBrowserError } from './errors.js';
|
|
|
11
11
|
|
|
12
12
|
const DAEMON_PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
|
|
13
13
|
const DAEMON_URL = `http://127.0.0.1:${DAEMON_PORT}`;
|
|
14
|
+
const OPENCLI_HEADERS = { 'X-OpenCLI': '1' };
|
|
14
15
|
|
|
15
16
|
let _idCounter = 0;
|
|
16
17
|
|
|
@@ -20,7 +21,7 @@ function generateId(): string {
|
|
|
20
21
|
|
|
21
22
|
export interface DaemonCommand {
|
|
22
23
|
id: string;
|
|
23
|
-
action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions' | 'set-file-input';
|
|
24
|
+
action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions' | 'set-file-input' | 'cdp';
|
|
24
25
|
tabId?: number;
|
|
25
26
|
code?: string;
|
|
26
27
|
workspace?: string;
|
|
@@ -31,10 +32,13 @@ export interface DaemonCommand {
|
|
|
31
32
|
format?: 'png' | 'jpeg';
|
|
32
33
|
quality?: number;
|
|
33
34
|
fullPage?: boolean;
|
|
35
|
+
|
|
34
36
|
/** Local file paths for set-file-input action */
|
|
35
37
|
files?: string[];
|
|
36
38
|
/** CSS selector for file input element (set-file-input action) */
|
|
37
39
|
selector?: string;
|
|
40
|
+
cdpMethod?: string;
|
|
41
|
+
cdpParams?: Record<string, unknown>;
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
export interface DaemonResult {
|
|
@@ -44,42 +48,65 @@ export interface DaemonResult {
|
|
|
44
48
|
error?: string;
|
|
45
49
|
}
|
|
46
50
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
export interface DaemonStatus {
|
|
52
|
+
ok: boolean;
|
|
53
|
+
pid: number;
|
|
54
|
+
uptime: number;
|
|
55
|
+
extensionConnected: boolean;
|
|
56
|
+
extensionVersion?: string;
|
|
57
|
+
pending: number;
|
|
58
|
+
lastCliRequestTime: number;
|
|
59
|
+
memoryMB: number;
|
|
60
|
+
port: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function requestDaemon(pathname: string, init?: RequestInit & { timeout?: number }): Promise<Response> {
|
|
64
|
+
const { timeout = 2000, headers, ...rest } = init ?? {};
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
51
67
|
try {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
headers: { 'X-OpenCLI': '1' },
|
|
68
|
+
return await fetch(`${DAEMON_URL}${pathname}`, {
|
|
69
|
+
...rest,
|
|
70
|
+
headers: { ...OPENCLI_HEADERS, ...headers },
|
|
56
71
|
signal: controller.signal,
|
|
57
72
|
});
|
|
73
|
+
} finally {
|
|
58
74
|
clearTimeout(timer);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function fetchDaemonStatus(opts?: { timeout?: number }): Promise<DaemonStatus | null> {
|
|
79
|
+
try {
|
|
80
|
+
const res = await requestDaemon('/status', { timeout: opts?.timeout ?? 2000 });
|
|
81
|
+
if (!res.ok) return null;
|
|
82
|
+
return await res.json() as DaemonStatus;
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function requestDaemonShutdown(opts?: { timeout?: number }): Promise<boolean> {
|
|
89
|
+
try {
|
|
90
|
+
const res = await requestDaemon('/shutdown', { method: 'POST', timeout: opts?.timeout ?? 5000 });
|
|
59
91
|
return res.ok;
|
|
60
92
|
} catch {
|
|
61
93
|
return false;
|
|
62
94
|
}
|
|
63
95
|
}
|
|
64
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Check if daemon is running.
|
|
99
|
+
*/
|
|
100
|
+
export async function isDaemonRunning(): Promise<boolean> {
|
|
101
|
+
return (await fetchDaemonStatus()) !== null;
|
|
102
|
+
}
|
|
103
|
+
|
|
65
104
|
/**
|
|
66
105
|
* Check if daemon is running AND the extension is connected.
|
|
67
106
|
*/
|
|
68
107
|
export async function isExtensionConnected(): Promise<boolean> {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const timer = setTimeout(() => controller.abort(), 2000);
|
|
72
|
-
const res = await fetch(`${DAEMON_URL}/status`, {
|
|
73
|
-
headers: { 'X-OpenCLI': '1' },
|
|
74
|
-
signal: controller.signal,
|
|
75
|
-
});
|
|
76
|
-
clearTimeout(timer);
|
|
77
|
-
if (!res.ok) return false;
|
|
78
|
-
const data = await res.json() as { extensionConnected?: boolean };
|
|
79
|
-
return !!data.extensionConnected;
|
|
80
|
-
} catch {
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
108
|
+
const status = await fetchDaemonStatus();
|
|
109
|
+
return !!status?.extensionConnected;
|
|
83
110
|
}
|
|
84
111
|
|
|
85
112
|
/**
|
|
@@ -98,16 +125,12 @@ export async function sendCommand(
|
|
|
98
125
|
const id = generateId();
|
|
99
126
|
const command: DaemonCommand = { id, action, ...params };
|
|
100
127
|
try {
|
|
101
|
-
const
|
|
102
|
-
const timer = setTimeout(() => controller.abort(), 30000);
|
|
103
|
-
|
|
104
|
-
const res = await fetch(`${DAEMON_URL}/command`, {
|
|
128
|
+
const res = await requestDaemon('/command', {
|
|
105
129
|
method: 'POST',
|
|
106
|
-
headers: { 'Content-Type': 'application/json'
|
|
130
|
+
headers: { 'Content-Type': 'application/json' },
|
|
107
131
|
body: JSON.stringify(command),
|
|
108
|
-
|
|
132
|
+
timeout: 30000,
|
|
109
133
|
});
|
|
110
|
-
clearTimeout(timer);
|
|
111
134
|
|
|
112
135
|
const result = (await res.json()) as DaemonResult;
|
|
113
136
|
|
package/src/browser/discover.ts
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
* Daemon discovery — checks if the daemon is running.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import { isDaemonRunning } from './daemon-client.js';
|
|
5
|
+
import { fetchDaemonStatus, isDaemonRunning } from './daemon-client.js';
|
|
7
6
|
|
|
8
7
|
export { isDaemonRunning };
|
|
9
8
|
|
|
@@ -15,21 +14,13 @@ export async function checkDaemonStatus(opts?: { timeout?: number }): Promise<{
|
|
|
15
14
|
extensionConnected: boolean;
|
|
16
15
|
extensionVersion?: string;
|
|
17
16
|
}> {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const controller = new AbortController();
|
|
21
|
-
const timer = setTimeout(() => controller.abort(), opts?.timeout ?? 2000);
|
|
22
|
-
try {
|
|
23
|
-
const res = await fetch(`http://127.0.0.1:${port}/status`, {
|
|
24
|
-
headers: { 'X-OpenCLI': '1' },
|
|
25
|
-
signal: controller.signal,
|
|
26
|
-
});
|
|
27
|
-
const data = await res.json() as { ok: boolean; extensionConnected: boolean; extensionVersion?: string };
|
|
28
|
-
return { running: true, extensionConnected: data.extensionConnected, extensionVersion: data.extensionVersion };
|
|
29
|
-
} finally {
|
|
30
|
-
clearTimeout(timer);
|
|
31
|
-
}
|
|
32
|
-
} catch {
|
|
17
|
+
const status = await fetchDaemonStatus({ timeout: opts?.timeout ?? 2000 });
|
|
18
|
+
if (!status) {
|
|
33
19
|
return { running: false, extensionConnected: false };
|
|
34
20
|
}
|
|
21
|
+
return {
|
|
22
|
+
running: true,
|
|
23
|
+
extensionConnected: status.extensionConnected,
|
|
24
|
+
extensionVersion: status.extensionVersion,
|
|
25
|
+
};
|
|
35
26
|
}
|
package/src/browser/page.ts
CHANGED
|
@@ -179,6 +179,53 @@ export class Page extends BasePage {
|
|
|
179
179
|
throw new Error('setFileInput returned no count — command may not be supported by the extension');
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
|
+
|
|
183
|
+
async cdp(method: string, params: Record<string, unknown> = {}): Promise<unknown> {
|
|
184
|
+
return sendCommand('cdp', {
|
|
185
|
+
cdpMethod: method,
|
|
186
|
+
cdpParams: params,
|
|
187
|
+
...this._cmdOpts(),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async nativeClick(x: number, y: number): Promise<void> {
|
|
192
|
+
await this.cdp('Input.dispatchMouseEvent', {
|
|
193
|
+
type: 'mousePressed',
|
|
194
|
+
x, y,
|
|
195
|
+
button: 'left',
|
|
196
|
+
clickCount: 1,
|
|
197
|
+
});
|
|
198
|
+
await this.cdp('Input.dispatchMouseEvent', {
|
|
199
|
+
type: 'mouseReleased',
|
|
200
|
+
x, y,
|
|
201
|
+
button: 'left',
|
|
202
|
+
clickCount: 1,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async nativeType(text: string): Promise<void> {
|
|
207
|
+
// Use Input.insertText for reliable Unicode/CJK text insertion
|
|
208
|
+
await this.cdp('Input.insertText', { text });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async nativeKeyPress(key: string, modifiers: string[] = []): Promise<void> {
|
|
212
|
+
let modifierFlags = 0;
|
|
213
|
+
for (const mod of modifiers) {
|
|
214
|
+
if (mod === 'Alt') modifierFlags |= 1;
|
|
215
|
+
if (mod === 'Ctrl') modifierFlags |= 2;
|
|
216
|
+
if (mod === 'Meta') modifierFlags |= 4;
|
|
217
|
+
if (mod === 'Shift') modifierFlags |= 8;
|
|
218
|
+
}
|
|
219
|
+
await this.cdp('Input.dispatchKeyEvent', {
|
|
220
|
+
type: 'keyDown',
|
|
221
|
+
key,
|
|
222
|
+
modifiers: modifierFlags,
|
|
223
|
+
});
|
|
224
|
+
await this.cdp('Input.dispatchKeyEvent', {
|
|
225
|
+
type: 'keyUp',
|
|
226
|
+
key,
|
|
227
|
+
modifiers: modifierFlags,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
182
230
|
}
|
|
183
231
|
|
|
184
|
-
// (End of file)
|