@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
@@ -6,7 +6,7 @@
6
6
  import type { BrowserSessionInfo } from '../types.js';
7
7
  export interface DaemonCommand {
8
8
  id: string;
9
- action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions' | 'set-file-input';
9
+ action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions' | 'set-file-input' | 'cdp';
10
10
  tabId?: number;
11
11
  code?: string;
12
12
  workspace?: string;
@@ -21,6 +21,8 @@ export interface DaemonCommand {
21
21
  files?: string[];
22
22
  /** CSS selector for file input element (set-file-input action) */
23
23
  selector?: string;
24
+ cdpMethod?: string;
25
+ cdpParams?: Record<string, unknown>;
24
26
  }
25
27
  export interface DaemonResult {
26
28
  id: string;
@@ -28,6 +30,23 @@ export interface DaemonResult {
28
30
  data?: unknown;
29
31
  error?: string;
30
32
  }
33
+ export interface DaemonStatus {
34
+ ok: boolean;
35
+ pid: number;
36
+ uptime: number;
37
+ extensionConnected: boolean;
38
+ extensionVersion?: string;
39
+ pending: number;
40
+ lastCliRequestTime: number;
41
+ memoryMB: number;
42
+ port: number;
43
+ }
44
+ export declare function fetchDaemonStatus(opts?: {
45
+ timeout?: number;
46
+ }): Promise<DaemonStatus | null>;
47
+ export declare function requestDaemonShutdown(opts?: {
48
+ timeout?: number;
49
+ }): Promise<boolean>;
31
50
  /**
32
51
  * Check if daemon is running.
33
52
  */
@@ -8,48 +8,58 @@ import { sleep } from '../utils.js';
8
8
  import { isTransientBrowserError } from './errors.js';
9
9
  const DAEMON_PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
10
10
  const DAEMON_URL = `http://127.0.0.1:${DAEMON_PORT}`;
11
+ const OPENCLI_HEADERS = { 'X-OpenCLI': '1' };
11
12
  let _idCounter = 0;
12
13
  function generateId() {
13
14
  return `cmd_${Date.now()}_${++_idCounter}`;
14
15
  }
15
- /**
16
- * Check if daemon is running.
17
- */
18
- export async function isDaemonRunning() {
16
+ async function requestDaemon(pathname, init) {
17
+ const { timeout = 2000, headers, ...rest } = init ?? {};
18
+ const controller = new AbortController();
19
+ const timer = setTimeout(() => controller.abort(), timeout);
19
20
  try {
20
- const controller = new AbortController();
21
- const timer = setTimeout(() => controller.abort(), 2000);
22
- const res = await fetch(`${DAEMON_URL}/status`, {
23
- headers: { 'X-OpenCLI': '1' },
21
+ return await fetch(`${DAEMON_URL}${pathname}`, {
22
+ ...rest,
23
+ headers: { ...OPENCLI_HEADERS, ...headers },
24
24
  signal: controller.signal,
25
25
  });
26
+ }
27
+ finally {
26
28
  clearTimeout(timer);
29
+ }
30
+ }
31
+ export async function fetchDaemonStatus(opts) {
32
+ try {
33
+ const res = await requestDaemon('/status', { timeout: opts?.timeout ?? 2000 });
34
+ if (!res.ok)
35
+ return null;
36
+ return await res.json();
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ export async function requestDaemonShutdown(opts) {
43
+ try {
44
+ const res = await requestDaemon('/shutdown', { method: 'POST', timeout: opts?.timeout ?? 5000 });
27
45
  return res.ok;
28
46
  }
29
47
  catch {
30
48
  return false;
31
49
  }
32
50
  }
51
+ /**
52
+ * Check if daemon is running.
53
+ */
54
+ export async function isDaemonRunning() {
55
+ return (await fetchDaemonStatus()) !== null;
56
+ }
33
57
  /**
34
58
  * Check if daemon is running AND the extension is connected.
35
59
  */
36
60
  export async function isExtensionConnected() {
37
- try {
38
- const controller = new AbortController();
39
- const timer = setTimeout(() => controller.abort(), 2000);
40
- const res = await fetch(`${DAEMON_URL}/status`, {
41
- headers: { 'X-OpenCLI': '1' },
42
- signal: controller.signal,
43
- });
44
- clearTimeout(timer);
45
- if (!res.ok)
46
- return false;
47
- const data = await res.json();
48
- return !!data.extensionConnected;
49
- }
50
- catch {
51
- return false;
52
- }
61
+ const status = await fetchDaemonStatus();
62
+ return !!status?.extensionConnected;
53
63
  }
54
64
  /**
55
65
  * Send a command to the daemon and wait for a result.
@@ -63,15 +73,12 @@ export async function sendCommand(action, params = {}) {
63
73
  const id = generateId();
64
74
  const command = { id, action, ...params };
65
75
  try {
66
- const controller = new AbortController();
67
- const timer = setTimeout(() => controller.abort(), 30000);
68
- const res = await fetch(`${DAEMON_URL}/command`, {
76
+ const res = await requestDaemon('/command', {
69
77
  method: 'POST',
70
- headers: { 'Content-Type': 'application/json', 'X-OpenCLI': '1' },
78
+ headers: { 'Content-Type': 'application/json' },
71
79
  body: JSON.stringify(command),
72
- signal: controller.signal,
80
+ timeout: 30000,
73
81
  });
74
- clearTimeout(timer);
75
82
  const result = (await res.json());
76
83
  if (!result.ok) {
77
84
  // Check if error is a transient extension issue worth retrying
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,77 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { fetchDaemonStatus, isDaemonRunning, isExtensionConnected, requestDaemonShutdown, } from './daemon-client.js';
3
+ describe('daemon-client', () => {
4
+ beforeEach(() => {
5
+ vi.stubGlobal('fetch', vi.fn());
6
+ });
7
+ afterEach(() => {
8
+ vi.restoreAllMocks();
9
+ });
10
+ it('fetchDaemonStatus sends the shared status request and returns parsed data', async () => {
11
+ const status = {
12
+ ok: true,
13
+ pid: 123,
14
+ uptime: 10,
15
+ extensionConnected: true,
16
+ extensionVersion: '1.2.3',
17
+ pending: 0,
18
+ lastCliRequestTime: Date.now(),
19
+ memoryMB: 32,
20
+ port: 19825,
21
+ };
22
+ const fetchMock = vi.mocked(fetch);
23
+ fetchMock.mockResolvedValue({
24
+ ok: true,
25
+ json: () => Promise.resolve(status),
26
+ });
27
+ await expect(fetchDaemonStatus()).resolves.toEqual(status);
28
+ expect(fetchMock).toHaveBeenCalledWith(expect.stringMatching(/\/status$/), expect.objectContaining({
29
+ headers: expect.objectContaining({ 'X-OpenCLI': '1' }),
30
+ }));
31
+ });
32
+ it('fetchDaemonStatus returns null on network failure', async () => {
33
+ vi.mocked(fetch).mockRejectedValue(new Error('ECONNREFUSED'));
34
+ await expect(fetchDaemonStatus()).resolves.toBeNull();
35
+ });
36
+ it('requestDaemonShutdown POSTs to the shared shutdown endpoint', async () => {
37
+ const fetchMock = vi.mocked(fetch);
38
+ fetchMock.mockResolvedValue({ ok: true });
39
+ await expect(requestDaemonShutdown()).resolves.toBe(true);
40
+ expect(fetchMock).toHaveBeenCalledWith(expect.stringMatching(/\/shutdown$/), expect.objectContaining({
41
+ method: 'POST',
42
+ headers: expect.objectContaining({ 'X-OpenCLI': '1' }),
43
+ }));
44
+ });
45
+ it('isDaemonRunning reflects shared status availability', async () => {
46
+ vi.mocked(fetch).mockResolvedValue({
47
+ ok: true,
48
+ json: () => Promise.resolve({
49
+ ok: true,
50
+ pid: 123,
51
+ uptime: 10,
52
+ extensionConnected: false,
53
+ pending: 0,
54
+ lastCliRequestTime: Date.now(),
55
+ memoryMB: 16,
56
+ port: 19825,
57
+ }),
58
+ });
59
+ await expect(isDaemonRunning()).resolves.toBe(true);
60
+ });
61
+ it('isExtensionConnected reflects shared status payload', async () => {
62
+ vi.mocked(fetch).mockResolvedValue({
63
+ ok: true,
64
+ json: () => Promise.resolve({
65
+ ok: true,
66
+ pid: 123,
67
+ uptime: 10,
68
+ extensionConnected: false,
69
+ pending: 0,
70
+ lastCliRequestTime: Date.now(),
71
+ memoryMB: 16,
72
+ port: 19825,
73
+ }),
74
+ });
75
+ await expect(isExtensionConnected()).resolves.toBe(false);
76
+ });
77
+ });
@@ -1,30 +1,19 @@
1
1
  /**
2
2
  * Daemon discovery — checks if the daemon is running.
3
3
  */
4
- import { DEFAULT_DAEMON_PORT } from '../constants.js';
5
- import { isDaemonRunning } from './daemon-client.js';
4
+ import { fetchDaemonStatus, isDaemonRunning } from './daemon-client.js';
6
5
  export { isDaemonRunning };
7
6
  /**
8
7
  * Check daemon status and return connection info.
9
8
  */
10
9
  export async function checkDaemonStatus(opts) {
11
- try {
12
- const port = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
13
- const controller = new AbortController();
14
- const timer = setTimeout(() => controller.abort(), opts?.timeout ?? 2000);
15
- try {
16
- const res = await fetch(`http://127.0.0.1:${port}/status`, {
17
- headers: { 'X-OpenCLI': '1' },
18
- signal: controller.signal,
19
- });
20
- const data = await res.json();
21
- return { running: true, extensionConnected: data.extensionConnected, extensionVersion: data.extensionVersion };
22
- }
23
- finally {
24
- clearTimeout(timer);
25
- }
26
- }
27
- catch {
10
+ const status = await fetchDaemonStatus({ timeout: opts?.timeout ?? 2000 });
11
+ if (!status) {
28
12
  return { running: false, extensionConnected: false };
29
13
  }
14
+ return {
15
+ running: true,
16
+ extensionConnected: status.extensionConnected,
17
+ extensionVersion: status.extensionVersion,
18
+ };
30
19
  }
@@ -50,4 +50,8 @@ export declare class Page extends BasePage {
50
50
  * payload size limits of base64-in-evaluate.
51
51
  */
52
52
  setFileInput(files: string[], selector?: string): Promise<void>;
53
+ cdp(method: string, params?: Record<string, unknown>): Promise<unknown>;
54
+ nativeClick(x: number, y: number): Promise<void>;
55
+ nativeType(text: string): Promise<void>;
56
+ nativeKeyPress(key: string, modifiers?: string[]): Promise<void>;
53
57
  }
@@ -171,5 +171,52 @@ export class Page extends BasePage {
171
171
  throw new Error('setFileInput returned no count — command may not be supported by the extension');
172
172
  }
173
173
  }
174
+ async cdp(method, params = {}) {
175
+ return sendCommand('cdp', {
176
+ cdpMethod: method,
177
+ cdpParams: params,
178
+ ...this._cmdOpts(),
179
+ });
180
+ }
181
+ async nativeClick(x, y) {
182
+ await this.cdp('Input.dispatchMouseEvent', {
183
+ type: 'mousePressed',
184
+ x, y,
185
+ button: 'left',
186
+ clickCount: 1,
187
+ });
188
+ await this.cdp('Input.dispatchMouseEvent', {
189
+ type: 'mouseReleased',
190
+ x, y,
191
+ button: 'left',
192
+ clickCount: 1,
193
+ });
194
+ }
195
+ async nativeType(text) {
196
+ // Use Input.insertText for reliable Unicode/CJK text insertion
197
+ await this.cdp('Input.insertText', { text });
198
+ }
199
+ async nativeKeyPress(key, modifiers = []) {
200
+ let modifierFlags = 0;
201
+ for (const mod of modifiers) {
202
+ if (mod === 'Alt')
203
+ modifierFlags |= 1;
204
+ if (mod === 'Ctrl')
205
+ modifierFlags |= 2;
206
+ if (mod === 'Meta')
207
+ modifierFlags |= 4;
208
+ if (mod === 'Shift')
209
+ modifierFlags |= 8;
210
+ }
211
+ await this.cdp('Input.dispatchKeyEvent', {
212
+ type: 'keyDown',
213
+ key,
214
+ modifiers: modifierFlags,
215
+ });
216
+ await this.cdp('Input.dispatchKeyEvent', {
217
+ type: 'keyUp',
218
+ key,
219
+ modifiers: modifierFlags,
220
+ });
221
+ }
174
222
  }
175
- // (End of file)
@@ -5445,7 +5445,7 @@
5445
5445
  {
5446
5446
  "name": "op",
5447
5447
  "type": "str",
5448
- "default": "/home/runner/tmp/gemini-images",
5448
+ "default": "/Users/jakevin/tmp/gemini-images",
5449
5449
  "required": false,
5450
5450
  "help": "Output directory shorthand"
5451
5451
  },
@@ -6432,7 +6432,7 @@
6432
6432
  {
6433
6433
  "name": "path",
6434
6434
  "type": "str",
6435
- "default": "/home/runner/Downloads/Instagram",
6435
+ "default": "/Users/jakevin/Downloads/Instagram",
6436
6436
  "required": false,
6437
6437
  "help": "Download directory"
6438
6438
  }