@jackwener/opencli 1.5.9 → 1.6.1

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.
Files changed (61) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +18 -0
  3. package/SKILL.md +59 -0
  4. package/autoresearch/baseline-browse.txt +1 -0
  5. package/autoresearch/baseline-skill.txt +1 -0
  6. package/autoresearch/browse-tasks.json +688 -0
  7. package/autoresearch/eval-browse.ts +185 -0
  8. package/autoresearch/eval-skill.ts +248 -0
  9. package/autoresearch/run-browse.sh +9 -0
  10. package/autoresearch/run-skill.sh +9 -0
  11. package/bun.lock +615 -0
  12. package/dist/browser/daemon-client.d.ts +20 -1
  13. package/dist/browser/daemon-client.js +37 -30
  14. package/dist/browser/daemon-client.test.d.ts +1 -0
  15. package/dist/browser/daemon-client.test.js +77 -0
  16. package/dist/browser/discover.js +8 -19
  17. package/dist/browser/page.d.ts +4 -0
  18. package/dist/browser/page.js +48 -1
  19. package/dist/cli-manifest.json +2 -2
  20. package/dist/cli.js +392 -0
  21. package/dist/clis/twitter/article.js +28 -1
  22. package/dist/clis/twitter/search.js +67 -5
  23. package/dist/clis/twitter/search.test.js +83 -5
  24. package/dist/clis/xiaohongshu/note.js +11 -0
  25. package/dist/clis/xiaohongshu/note.test.js +49 -0
  26. package/dist/commanderAdapter.js +1 -1
  27. package/dist/commanderAdapter.test.js +43 -0
  28. package/dist/commands/daemon.js +7 -46
  29. package/dist/commands/daemon.test.js +44 -69
  30. package/dist/discovery.js +27 -0
  31. package/dist/types.d.ts +8 -0
  32. package/docs/guide/getting-started.md +21 -0
  33. package/docs/superpowers/specs/2026-04-02-browse-skill-testing-design.md +144 -0
  34. package/docs/zh/guide/getting-started.md +21 -0
  35. package/extension/package-lock.json +2 -2
  36. package/extension/src/background.ts +51 -4
  37. package/extension/src/cdp.ts +77 -124
  38. package/extension/src/protocol.ts +5 -1
  39. package/package.json +1 -1
  40. package/skills/opencli-explorer/SKILL.md +6 -0
  41. package/skills/opencli-oneshot/SKILL.md +6 -0
  42. package/skills/opencli-operate/SKILL.md +213 -0
  43. package/skills/opencli-usage/SKILL.md +113 -32
  44. package/src/browser/daemon-client.test.ts +103 -0
  45. package/src/browser/daemon-client.ts +53 -30
  46. package/src/browser/discover.ts +8 -17
  47. package/src/browser/page.ts +48 -1
  48. package/src/cli.ts +392 -0
  49. package/src/clis/twitter/article.ts +31 -1
  50. package/src/clis/twitter/search.test.ts +88 -5
  51. package/src/clis/twitter/search.ts +68 -5
  52. package/src/clis/xiaohongshu/note.test.ts +51 -0
  53. package/src/clis/xiaohongshu/note.ts +18 -0
  54. package/src/commanderAdapter.test.ts +62 -0
  55. package/src/commanderAdapter.ts +1 -1
  56. package/src/commands/daemon.test.ts +49 -83
  57. package/src/commands/daemon.ts +7 -55
  58. package/src/discovery.ts +22 -0
  59. package/src/doctor.ts +1 -1
  60. package/src/types.ts +8 -0
  61. package/extension/dist/background.js +0 -681
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: opencli-usage
3
- description: "OpenCLI usage guide installation, prerequisites, and command reference organized by platform"
4
- version: 1.5.9
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
- ## Command Categories
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
- OpenCLI commands are organized by platform type:
42
-
43
- ### 📱 Browser-based Commands
44
- Commands that require Chrome browser with login state. See [browser.md](./browser.md) for full reference.
45
-
46
- **Supported platforms:**
47
- - Bilibili (哔哩哔哩), Zhihu (知乎), Xiaohongshu (小红书), Xueqiu (雪球)
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
- **Supported platforms:**
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
- ### 🌐 Public API Commands
65
- Commands that work without browser or authentication. See [public-api.md](./public-api.md) for full reference.
149
+ ## Related Skills
66
150
 
67
- **Supported platforms:**
68
- - Hacker News, V2EX (public features), BBC News, Sina Finance
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
- * Check if daemon is running.
49
- */
50
- export async function isDaemonRunning(): Promise<boolean> {
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
- const controller = new AbortController();
53
- const timer = setTimeout(() => controller.abort(), 2000);
54
- const res = await fetch(`${DAEMON_URL}/status`, {
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
- try {
70
- const controller = new AbortController();
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 controller = new AbortController();
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', 'X-OpenCLI': '1' },
130
+ headers: { 'Content-Type': 'application/json' },
107
131
  body: JSON.stringify(command),
108
- signal: controller.signal,
132
+ timeout: 30000,
109
133
  });
110
- clearTimeout(timer);
111
134
 
112
135
  const result = (await res.json()) as DaemonResult;
113
136
 
@@ -2,8 +2,7 @@
2
2
  * Daemon discovery — checks if the daemon is running.
3
3
  */
4
4
 
5
- import { DEFAULT_DAEMON_PORT } from '../constants.js';
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
- try {
19
- const port = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
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
  }
@@ -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)