@jackwener/opencli 0.9.8 → 1.0.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 (165) hide show
  1. package/CDP.md +1 -1
  2. package/CDP.zh-CN.md +1 -1
  3. package/CLI-ELECTRON.md +2 -2
  4. package/CLI-EXPLORER.md +4 -4
  5. package/README.md +35 -58
  6. package/README.zh-CN.md +36 -60
  7. package/SKILL.md +10 -8
  8. package/TESTING.md +7 -7
  9. package/dist/browser/daemon-client.d.ts +37 -0
  10. package/dist/browser/daemon-client.js +82 -0
  11. package/dist/browser/discover.d.ts +11 -34
  12. package/dist/browser/discover.js +15 -205
  13. package/dist/browser/errors.d.ts +6 -20
  14. package/dist/browser/errors.js +24 -63
  15. package/dist/browser/index.d.ts +2 -12
  16. package/dist/browser/index.js +2 -12
  17. package/dist/browser/mcp.d.ts +9 -21
  18. package/dist/browser/mcp.js +70 -285
  19. package/dist/browser/page.d.ts +36 -7
  20. package/dist/browser/page.js +212 -81
  21. package/dist/browser.test.js +10 -231
  22. package/dist/cli-manifest.json +561 -14
  23. package/dist/clis/apple-podcasts/episodes.d.ts +1 -0
  24. package/dist/clis/apple-podcasts/episodes.js +28 -0
  25. package/dist/clis/apple-podcasts/search.d.ts +1 -0
  26. package/dist/clis/apple-podcasts/search.js +29 -0
  27. package/dist/clis/apple-podcasts/top.d.ts +1 -0
  28. package/dist/clis/apple-podcasts/top.js +34 -0
  29. package/dist/clis/apple-podcasts/utils.d.ts +11 -0
  30. package/dist/clis/apple-podcasts/utils.js +30 -0
  31. package/dist/clis/apple-podcasts/utils.test.d.ts +1 -0
  32. package/dist/clis/apple-podcasts/utils.test.js +57 -0
  33. package/dist/clis/chatwise/history.js +18 -1
  34. package/dist/clis/discord-app/channels.js +33 -21
  35. package/dist/clis/neteasemusic/like.d.ts +1 -0
  36. package/dist/clis/neteasemusic/like.js +25 -0
  37. package/dist/clis/neteasemusic/lyrics.d.ts +1 -0
  38. package/dist/clis/neteasemusic/lyrics.js +47 -0
  39. package/dist/clis/neteasemusic/next.d.ts +1 -0
  40. package/dist/clis/neteasemusic/next.js +26 -0
  41. package/dist/clis/neteasemusic/play.d.ts +1 -0
  42. package/dist/clis/neteasemusic/play.js +26 -0
  43. package/dist/clis/neteasemusic/playing.d.ts +1 -0
  44. package/dist/clis/neteasemusic/playing.js +59 -0
  45. package/dist/clis/neteasemusic/playlist.d.ts +1 -0
  46. package/dist/clis/neteasemusic/playlist.js +46 -0
  47. package/dist/clis/neteasemusic/prev.d.ts +1 -0
  48. package/dist/clis/neteasemusic/prev.js +25 -0
  49. package/dist/clis/neteasemusic/search.d.ts +1 -0
  50. package/dist/clis/neteasemusic/search.js +52 -0
  51. package/dist/clis/neteasemusic/status.d.ts +1 -0
  52. package/dist/clis/neteasemusic/status.js +16 -0
  53. package/dist/clis/neteasemusic/volume.d.ts +1 -0
  54. package/dist/clis/neteasemusic/volume.js +54 -0
  55. package/dist/clis/twitter/accept.d.ts +1 -0
  56. package/dist/clis/twitter/accept.js +202 -0
  57. package/dist/clis/twitter/followers.js +30 -22
  58. package/dist/clis/twitter/following.js +19 -14
  59. package/dist/clis/twitter/notifications.js +29 -22
  60. package/dist/clis/twitter/reply-dm.d.ts +1 -0
  61. package/dist/clis/twitter/reply-dm.js +181 -0
  62. package/dist/clis/twitter/search.js +50 -12
  63. package/dist/clis/weread/book.d.ts +1 -0
  64. package/dist/clis/weread/book.js +26 -0
  65. package/dist/clis/weread/highlights.d.ts +1 -0
  66. package/dist/clis/weread/highlights.js +23 -0
  67. package/dist/clis/weread/notebooks.d.ts +1 -0
  68. package/dist/clis/weread/notebooks.js +21 -0
  69. package/dist/clis/weread/notes.d.ts +1 -0
  70. package/dist/clis/weread/notes.js +29 -0
  71. package/dist/clis/weread/ranking.d.ts +1 -0
  72. package/dist/clis/weread/ranking.js +28 -0
  73. package/dist/clis/weread/search.d.ts +1 -0
  74. package/dist/clis/weread/search.js +25 -0
  75. package/dist/clis/weread/shelf.d.ts +1 -0
  76. package/dist/clis/weread/shelf.js +24 -0
  77. package/dist/clis/weread/utils.d.ts +20 -0
  78. package/dist/clis/weread/utils.js +72 -0
  79. package/dist/clis/weread/utils.test.d.ts +1 -0
  80. package/dist/clis/weread/utils.test.js +85 -0
  81. package/dist/daemon.d.ts +13 -0
  82. package/dist/daemon.js +187 -0
  83. package/dist/doctor.d.ts +10 -65
  84. package/dist/doctor.js +49 -602
  85. package/dist/doctor.test.js +30 -170
  86. package/dist/main.js +12 -41
  87. package/dist/pipeline/executor.test.js +1 -0
  88. package/dist/pipeline/steps/browser.js +2 -2
  89. package/dist/pipeline/steps/intercept.js +1 -2
  90. package/dist/runtime.d.ts +1 -4
  91. package/dist/runtime.js +1 -4
  92. package/dist/setup.d.ts +6 -0
  93. package/dist/setup.js +46 -160
  94. package/dist/types.d.ts +6 -0
  95. package/extension/dist/background.js +484 -0
  96. package/extension/icons/icon-128.png +0 -0
  97. package/extension/icons/icon-16.png +0 -0
  98. package/extension/icons/icon-32.png +0 -0
  99. package/extension/icons/icon-48.png +0 -0
  100. package/extension/manifest.json +31 -0
  101. package/extension/package.json +16 -0
  102. package/extension/src/background.ts +370 -0
  103. package/extension/src/cdp.ts +125 -0
  104. package/extension/src/protocol.ts +57 -0
  105. package/extension/store-assets/screenshot-1280x800.png +0 -0
  106. package/extension/tsconfig.json +15 -0
  107. package/extension/vite.config.ts +18 -0
  108. package/package.json +5 -5
  109. package/src/browser/daemon-client.ts +113 -0
  110. package/src/browser/discover.ts +18 -232
  111. package/src/browser/errors.ts +30 -100
  112. package/src/browser/index.ts +2 -13
  113. package/src/browser/mcp.ts +81 -282
  114. package/src/browser/page.ts +223 -83
  115. package/src/browser.test.ts +9 -239
  116. package/src/clis/apple-podcasts/episodes.ts +28 -0
  117. package/src/clis/apple-podcasts/search.ts +29 -0
  118. package/src/clis/apple-podcasts/top.ts +34 -0
  119. package/src/clis/apple-podcasts/utils.test.ts +72 -0
  120. package/src/clis/apple-podcasts/utils.ts +37 -0
  121. package/src/clis/chatgpt/README.md +1 -1
  122. package/src/clis/chatgpt/README.zh-CN.md +1 -1
  123. package/src/clis/chatwise/history.ts +15 -1
  124. package/src/clis/discord-app/channels.ts +33 -21
  125. package/src/clis/neteasemusic/README.md +31 -0
  126. package/src/clis/neteasemusic/README.zh-CN.md +31 -0
  127. package/src/clis/neteasemusic/like.ts +28 -0
  128. package/src/clis/neteasemusic/lyrics.ts +53 -0
  129. package/src/clis/neteasemusic/next.ts +30 -0
  130. package/src/clis/neteasemusic/play.ts +30 -0
  131. package/src/clis/neteasemusic/playing.ts +62 -0
  132. package/src/clis/neteasemusic/playlist.ts +51 -0
  133. package/src/clis/neteasemusic/prev.ts +29 -0
  134. package/src/clis/neteasemusic/search.ts +58 -0
  135. package/src/clis/neteasemusic/status.ts +18 -0
  136. package/src/clis/neteasemusic/volume.ts +61 -0
  137. package/src/clis/twitter/accept.ts +213 -0
  138. package/src/clis/twitter/followers.ts +36 -29
  139. package/src/clis/twitter/following.ts +25 -20
  140. package/src/clis/twitter/notifications.ts +34 -27
  141. package/src/clis/twitter/reply-dm.ts +193 -0
  142. package/src/clis/twitter/search.ts +53 -13
  143. package/src/clis/weread/book.ts +28 -0
  144. package/src/clis/weread/highlights.ts +25 -0
  145. package/src/clis/weread/notebooks.ts +23 -0
  146. package/src/clis/weread/notes.ts +31 -0
  147. package/src/clis/weread/ranking.ts +29 -0
  148. package/src/clis/weread/search.ts +26 -0
  149. package/src/clis/weread/shelf.ts +26 -0
  150. package/src/clis/weread/utils.test.ts +104 -0
  151. package/src/clis/weread/utils.ts +74 -0
  152. package/src/daemon.ts +217 -0
  153. package/src/doctor.test.ts +32 -193
  154. package/src/doctor.ts +58 -669
  155. package/src/main.ts +11 -34
  156. package/src/pipeline/executor.test.ts +1 -0
  157. package/src/pipeline/steps/browser.ts +2 -2
  158. package/src/pipeline/steps/intercept.ts +1 -2
  159. package/src/runtime.ts +2 -6
  160. package/src/setup.ts +47 -183
  161. package/src/types.ts +1 -0
  162. package/tests/e2e/public-commands.test.ts +68 -1
  163. package/dist/clis/grok/debug.d.ts +0 -1
  164. package/dist/clis/grok/debug.js +0 -45
  165. package/src/clis/grok/debug.ts +0 -49
@@ -1,191 +1,51 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { readTokenFromShellContent, renderBrowserDoctorReport, upsertShellToken, readTomlConfigToken, upsertTomlConfigToken, upsertJsonConfigToken, } from './doctor.js';
3
- describe('shell token helpers', () => {
4
- it('reads token from shell export', () => {
5
- expect(readTokenFromShellContent('export PLAYWRIGHT_MCP_EXTENSION_TOKEN="abc123"\n')).toBe('abc123');
6
- });
7
- it('appends token export when missing', () => {
8
- const next = upsertShellToken('export PATH="/usr/bin"\n', 'abc123');
9
- expect(next).toContain('export PLAYWRIGHT_MCP_EXTENSION_TOKEN="abc123"');
10
- });
11
- it('replaces token export when present', () => {
12
- const next = upsertShellToken('export PLAYWRIGHT_MCP_EXTENSION_TOKEN="old"\n', 'new');
13
- expect(next).toContain('export PLAYWRIGHT_MCP_EXTENSION_TOKEN="new"');
14
- expect(next).not.toContain('"old"');
15
- });
16
- });
17
- describe('toml token helpers', () => {
18
- it('reads token from playwright env section', () => {
19
- const content = `
20
- [mcp_servers.playwright.env]
21
- PLAYWRIGHT_MCP_EXTENSION_TOKEN = "abc123"
22
- `;
23
- expect(readTomlConfigToken(content)).toBe('abc123');
24
- });
25
- it('updates token inside existing env section', () => {
26
- const content = `
27
- [mcp_servers.playwright.env]
28
- PLAYWRIGHT_MCP_EXTENSION_TOKEN = "old"
29
- `;
30
- const next = upsertTomlConfigToken(content, 'new');
31
- expect(next).toContain('PLAYWRIGHT_MCP_EXTENSION_TOKEN = "new"');
32
- expect(next).not.toContain('"old"');
33
- });
34
- it('creates env section when missing', () => {
35
- const content = `
36
- [mcp_servers.playwright]
37
- type = "stdio"
38
- `;
39
- const next = upsertTomlConfigToken(content, 'abc123');
40
- expect(next).toContain('[mcp_servers.playwright.env]');
41
- expect(next).toContain('PLAYWRIGHT_MCP_EXTENSION_TOKEN = "abc123"');
42
- });
43
- });
44
- describe('json token helpers', () => {
45
- it('writes token into standard mcpServers config', () => {
46
- const next = upsertJsonConfigToken(JSON.stringify({
47
- mcpServers: {
48
- playwright: {
49
- command: 'npx',
50
- args: ['-y', '@playwright/mcp@latest', '--extension'],
51
- },
52
- },
53
- }), 'abc123');
54
- const parsed = JSON.parse(next);
55
- expect(parsed.mcpServers.playwright.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN).toBe('abc123');
56
- });
57
- it('writes token into opencode mcp config', () => {
58
- const next = upsertJsonConfigToken(JSON.stringify({
59
- $schema: 'https://opencode.ai/config.json',
60
- mcp: {
61
- playwright: {
62
- command: ['npx', '-y', '@playwright/mcp@latest', '--extension'],
63
- enabled: true,
64
- type: 'local',
65
- },
66
- },
67
- }), 'abc123');
68
- const parsed = JSON.parse(next);
69
- expect(parsed.mcp.playwright.environment.PLAYWRIGHT_MCP_EXTENSION_TOKEN).toBe('abc123');
70
- });
71
- it('creates standard mcpServers format for empty file (not OpenCode)', () => {
72
- const next = upsertJsonConfigToken('', 'abc123');
73
- const parsed = JSON.parse(next);
74
- expect(parsed.mcpServers.playwright.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN).toBe('abc123');
75
- expect(parsed.mcp).toBeUndefined();
76
- });
77
- it('creates OpenCode format when filePath contains opencode', () => {
78
- const next = upsertJsonConfigToken('', 'abc123', '/home/user/.config/opencode/opencode.json');
79
- const parsed = JSON.parse(next);
80
- expect(parsed.mcp.playwright.environment.PLAYWRIGHT_MCP_EXTENSION_TOKEN).toBe('abc123');
81
- expect(parsed.mcpServers).toBeUndefined();
82
- });
83
- it('creates standard format when filePath is claude.json', () => {
84
- const next = upsertJsonConfigToken('', 'abc123', '/home/user/.claude.json');
85
- const parsed = JSON.parse(next);
86
- expect(parsed.mcpServers.playwright.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN).toBe('abc123');
87
- });
88
- });
89
- describe('fish shell support', () => {
90
- it('generates fish set -gx syntax for fish config path', () => {
91
- const next = upsertShellToken('', 'abc123', '/home/user/.config/fish/config.fish');
92
- expect(next).toContain('set -gx PLAYWRIGHT_MCP_EXTENSION_TOKEN "abc123"');
93
- expect(next).not.toContain('export');
94
- });
95
- it('replaces existing fish set line', () => {
96
- const content = 'set -gx PLAYWRIGHT_MCP_EXTENSION_TOKEN "old"\n';
97
- const next = upsertShellToken(content, 'new', '/home/user/.config/fish/config.fish');
98
- expect(next).toContain('set -gx PLAYWRIGHT_MCP_EXTENSION_TOKEN "new"');
99
- expect(next).not.toContain('"old"');
100
- });
101
- it('appends fish syntax to existing fish config', () => {
102
- const content = 'set -gx PATH /usr/bin\n';
103
- const next = upsertShellToken(content, 'abc123', '/home/user/.config/fish/config.fish');
104
- expect(next).toContain('set -gx PLAYWRIGHT_MCP_EXTENSION_TOKEN "abc123"');
105
- expect(next).toContain('set -gx PATH /usr/bin');
106
- });
107
- it('uses export syntax for zshrc even with filePath', () => {
108
- const next = upsertShellToken('', 'abc123', '/home/user/.zshrc');
109
- expect(next).toContain('export PLAYWRIGHT_MCP_EXTENSION_TOKEN="abc123"');
110
- expect(next).not.toContain('set -gx');
111
- });
112
- });
2
+ import { renderBrowserDoctorReport } from './doctor.js';
113
3
  describe('doctor report rendering', () => {
114
4
  const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
115
- it('renders OK-style report when tokens match', () => {
5
+ it('renders OK-style report when daemon and extension connected', () => {
116
6
  const text = strip(renderBrowserDoctorReport({
117
- envToken: 'abc123',
118
- envFingerprint: 'fp1',
119
- extensionToken: 'abc123',
120
- extensionFingerprint: 'fp1',
121
- extensionInstalled: true,
122
- extensionBrowsers: ['Chrome'],
123
- shellFiles: [{ path: '/tmp/.zshrc', exists: true, token: 'abc123', fingerprint: 'fp1' }],
124
- configs: [{ path: '/tmp/mcp.json', exists: true, format: 'json', token: 'abc123', fingerprint: 'fp1', writable: true }],
125
- recommendedToken: 'abc123',
126
- recommendedFingerprint: 'fp1',
127
- warnings: [],
7
+ daemonRunning: true,
8
+ extensionConnected: true,
128
9
  issues: [],
129
10
  }));
130
- expect(text).toContain('[OK] Extension installed (Chrome)');
131
- expect(text).toContain('[OK] Environment token: configured (fp1)');
132
- expect(text).toContain('[OK] /tmp/mcp.json');
133
- expect(text).toContain('configured (fp1)');
11
+ expect(text).toContain('[OK] Daemon: running on port 19825');
12
+ expect(text).toContain('[OK] Extension: connected');
13
+ expect(text).toContain('Everything looks good!');
14
+ });
15
+ it('renders MISSING when daemon not running', () => {
16
+ const text = strip(renderBrowserDoctorReport({
17
+ daemonRunning: false,
18
+ extensionConnected: false,
19
+ issues: ['Daemon is not running.'],
20
+ }));
21
+ expect(text).toContain('[MISSING] Daemon: not running');
22
+ expect(text).toContain('[MISSING] Extension: not connected');
23
+ expect(text).toContain('Daemon is not running.');
134
24
  });
135
- it('renders MISMATCH-style report when fingerprints differ', () => {
25
+ it('renders extension not connected when daemon is running', () => {
136
26
  const text = strip(renderBrowserDoctorReport({
137
- envToken: 'abc123',
138
- envFingerprint: 'fp1',
139
- extensionToken: null,
140
- extensionFingerprint: null,
141
- extensionInstalled: false,
142
- extensionBrowsers: [],
143
- shellFiles: [{ path: '/tmp/.zshrc', exists: true, token: 'def456', fingerprint: 'fp2' }],
144
- configs: [{ path: '/tmp/mcp.json', exists: true, format: 'json', token: 'abc123', fingerprint: 'fp1', writable: true }],
145
- recommendedToken: 'abc123',
146
- recommendedFingerprint: 'fp1',
147
- warnings: [],
148
- issues: ['Detected inconsistent Playwright MCP tokens across env/config files.'],
27
+ daemonRunning: true,
28
+ extensionConnected: false,
29
+ issues: ['Daemon is running but the Chrome extension is not connected.'],
149
30
  }));
150
- expect(text).toContain('[MISSING] Extension not installed in any browser');
151
- expect(text).toContain('[MISMATCH] Environment token: configured (fp1)');
152
- expect(text).toContain('[MISMATCH] /tmp/.zshrc');
153
- expect(text).toContain('configured (fp2)');
154
- expect(text).toContain('[MISMATCH] Recommended token fingerprint: fp1');
31
+ expect(text).toContain('[OK] Daemon: running on port 19825');
32
+ expect(text).toContain('[MISSING] Extension: not connected');
155
33
  });
156
34
  it('renders connectivity OK when live test succeeds', () => {
157
35
  const text = strip(renderBrowserDoctorReport({
158
- envToken: 'abc123',
159
- envFingerprint: 'fp1',
160
- extensionToken: 'abc123',
161
- extensionFingerprint: 'fp1',
162
- extensionInstalled: true,
163
- extensionBrowsers: ['Chrome'],
164
- shellFiles: [],
165
- configs: [],
166
- recommendedToken: 'abc123',
167
- recommendedFingerprint: 'fp1',
36
+ daemonRunning: true,
37
+ extensionConnected: true,
168
38
  connectivity: { ok: true, durationMs: 1234 },
169
- warnings: [],
170
39
  issues: [],
171
40
  }));
172
- expect(text).toContain('[OK] Browser connectivity: connected in 1.2s');
41
+ expect(text).toContain('[OK] Connectivity: connected in 1.2s');
173
42
  });
174
- it('renders connectivity WARN when not tested', () => {
43
+ it('renders connectivity SKIP when not tested', () => {
175
44
  const text = strip(renderBrowserDoctorReport({
176
- envToken: 'abc123',
177
- envFingerprint: 'fp1',
178
- extensionToken: 'abc123',
179
- extensionFingerprint: 'fp1',
180
- extensionInstalled: true,
181
- extensionBrowsers: ['Chrome'],
182
- shellFiles: [],
183
- configs: [],
184
- recommendedToken: 'abc123',
185
- recommendedFingerprint: 'fp1',
186
- warnings: [],
45
+ daemonRunning: true,
46
+ extensionConnected: true,
187
47
  issues: [],
188
48
  }));
189
- expect(text).toContain('[WARN] Browser connectivity: not tested (use --live)');
49
+ expect(text).toContain('[SKIP] Connectivity: not tested (use --live)');
190
50
  });
191
51
  });
package/dist/main.js CHANGED
@@ -8,9 +8,9 @@ import { fileURLToPath } from 'node:url';
8
8
  import { Command } from 'commander';
9
9
  import chalk from 'chalk';
10
10
  import { discoverClis, executeCommand } from './engine.js';
11
- import { Strategy, fullName, getRegistry, strategyLabel } from './registry.js';
11
+ import { fullName, getRegistry, strategyLabel } from './registry.js';
12
12
  import { render as renderOutput } from './output.js';
13
- import { PlaywrightMCP } from './browser/index.js';
13
+ import { BrowserBridge } from './browser/index.js';
14
14
  import { browserSession, DEFAULT_BROWSER_COMMAND_TIMEOUT, runWithTimeout } from './runtime.js';
15
15
  import { PKG_VERSION } from './version.js';
16
16
  import { getCompletions, printCompletionScript } from './completion.js';
@@ -101,15 +101,15 @@ program.command('verify').description('Validate + smoke test').argument('[target
101
101
  process.exitCode = r.ok ? 0 : 1;
102
102
  });
103
103
  program.command('explore').alias('probe').description('Explore a website: discover APIs, stores, and recommend strategies').argument('<url>').option('--site <name>').option('--goal <text>').option('--wait <s>', '', '3').option('--auto', 'Enable interactive fuzzing (simulate clicks to trigger lazy APIs)').option('--click <labels>', 'Comma-separated labels to click before fuzzing (e.g. "字幕,CC,评论")')
104
- .action(async (url, opts) => { const { exploreUrl, renderExploreSummary } = await import('./explore.js'); const clickLabels = opts.click ? opts.click.split(',').map((s) => s.trim()) : undefined; console.log(renderExploreSummary(await exploreUrl(url, { BrowserFactory: PlaywrightMCP, site: opts.site, goal: opts.goal, waitSeconds: parseFloat(opts.wait), auto: opts.auto, clickLabels }))); });
104
+ .action(async (url, opts) => { const { exploreUrl, renderExploreSummary } = await import('./explore.js'); const clickLabels = opts.click ? opts.click.split(',').map((s) => s.trim()) : undefined; console.log(renderExploreSummary(await exploreUrl(url, { BrowserFactory: BrowserBridge, site: opts.site, goal: opts.goal, waitSeconds: parseFloat(opts.wait), auto: opts.auto, clickLabels }))); });
105
105
  program.command('synthesize').description('Synthesize CLIs from explore').argument('<target>').option('--top <n>', '', '3')
106
106
  .action(async (target, opts) => { const { synthesizeFromExplore, renderSynthesizeSummary } = await import('./synthesize.js'); console.log(renderSynthesizeSummary(synthesizeFromExplore(target, { top: parseInt(opts.top) }))); });
107
107
  program.command('generate').description('One-shot: explore → synthesize → register').argument('<url>').option('--goal <text>').option('--site <name>')
108
- .action(async (url, opts) => { const { generateCliFromUrl, renderGenerateSummary } = await import('./generate.js'); const r = await generateCliFromUrl({ url, BrowserFactory: PlaywrightMCP, builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, goal: opts.goal, site: opts.site }); console.log(renderGenerateSummary(r)); process.exitCode = r.ok ? 0 : 1; });
108
+ .action(async (url, opts) => { const { generateCliFromUrl, renderGenerateSummary } = await import('./generate.js'); const r = await generateCliFromUrl({ url, BrowserFactory: BrowserBridge, builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, goal: opts.goal, site: opts.site }); console.log(renderGenerateSummary(r)); process.exitCode = r.ok ? 0 : 1; });
109
109
  program.command('cascade').description('Strategy cascade: find simplest working strategy').argument('<url>').option('--site <name>')
110
110
  .action(async (url, opts) => {
111
111
  const { cascadeProbe, renderCascadeResult } = await import('./cascade.js');
112
- const result = await browserSession(PlaywrightMCP, async (page) => {
112
+ const result = await browserSession(BrowserBridge, async (page) => {
113
113
  // Navigate to the site first for cookie context
114
114
  try {
115
115
  const siteUrl = new URL(url);
@@ -122,37 +122,18 @@ program.command('cascade').description('Strategy cascade: find simplest working
122
122
  console.log(renderCascadeResult(result));
123
123
  });
124
124
  program.command('doctor')
125
- .description('Diagnose Playwright MCP Bridge, token consistency, and Chrome remote debugging')
126
- .option('--fix', 'Apply suggested fixes to shell rc and detected MCP configs', false)
127
- .option('-y, --yes', 'Skip confirmation prompts when applying fixes', false)
128
- .option('--token <token>', 'Override token to write instead of auto-detecting')
125
+ .description('Diagnose opencli browser bridge connectivity')
129
126
  .option('--live', 'Test browser connectivity (requires Chrome running)', false)
130
- .option('--shell-rc <path>', 'Shell startup file to update')
131
- .option('--mcp-config <paths>', 'Comma-separated MCP config paths to scan/update')
132
127
  .action(async (opts) => {
133
- const { runBrowserDoctor, renderBrowserDoctorReport, applyBrowserDoctorFix } = await import('./doctor.js');
134
- const configPaths = opts.mcpConfig ? String(opts.mcpConfig).split(',').map((s) => s.trim()).filter(Boolean) : undefined;
135
- const report = await runBrowserDoctor({ token: opts.token, live: opts.live, shellRc: opts.shellRc, configPaths, cliVersion: PKG_VERSION });
128
+ const { runBrowserDoctor, renderBrowserDoctorReport } = await import('./doctor.js');
129
+ const report = await runBrowserDoctor({ live: opts.live, cliVersion: PKG_VERSION });
136
130
  console.log(renderBrowserDoctorReport(report));
137
- if (opts.fix) {
138
- const written = await applyBrowserDoctorFix(report, { fix: true, yes: opts.yes, token: opts.token, shellRc: opts.shellRc, configPaths });
139
- console.log();
140
- if (written.length > 0) {
141
- console.log(chalk.green('Updated files:'));
142
- for (const filePath of written)
143
- console.log(`- ${filePath}`);
144
- }
145
- else {
146
- console.log(chalk.yellow('No files were changed.'));
147
- }
148
- }
149
131
  });
150
132
  program.command('setup')
151
- .description('Interactive setup: configure Playwright MCP token across all detected tools')
152
- .option('--token <token>', 'Provide token directly instead of auto-detecting')
153
- .action(async (opts) => {
133
+ .description('Interactive setup: verify browser bridge connectivity')
134
+ .action(async () => {
154
135
  const { runSetup } = await import('./setup.js');
155
- await runSetup({ cliVersion: PKG_VERSION, token: opts.token });
136
+ await runSetup({ cliVersion: PKG_VERSION });
156
137
  });
157
138
  program.command('completion')
158
139
  .description('Output shell completion script')
@@ -215,17 +196,7 @@ for (const [, cmd] of registry) {
215
196
  process.env.OPENCLI_VERBOSE = '1';
216
197
  let result;
217
198
  if (cmd.browser) {
218
- result = await browserSession(PlaywrightMCP, async (page) => {
219
- // Cookie/header strategies require same-origin context for credentialed fetch.
220
- // In CDP mode the active tab may be on an unrelated domain, causing CORS failures.
221
- // Navigate to the command's domain first (mirrors cascade command behavior).
222
- if ((cmd.strategy === Strategy.COOKIE || cmd.strategy === Strategy.HEADER) && cmd.domain) {
223
- try {
224
- await page.goto(`https://${cmd.domain}`);
225
- await page.wait(2);
226
- }
227
- catch { }
228
- }
199
+ result = await browserSession(BrowserBridge, async (page) => {
229
200
  return runWithTimeout(executeCommand(cmd, page, kwargs, actionOpts.verbose), { timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT, label: fullName(cmd) });
230
201
  });
231
202
  }
@@ -23,6 +23,7 @@ function createMockPage(overrides = {}) {
23
23
  autoScroll: vi.fn(),
24
24
  installInterceptor: vi.fn(),
25
25
  getInterceptedRequests: vi.fn().mockResolvedValue([]),
26
+ screenshot: vi.fn().mockResolvedValue(''),
26
27
  ...overrides,
27
28
  };
28
29
  }
@@ -2,7 +2,7 @@
2
2
  * Pipeline step: navigate, click, type, wait, press, snapshot.
3
3
  * Browser interaction primitives.
4
4
  */
5
- import { render, normalizeEvaluateSource } from '../template.js';
5
+ import { render } from '../template.js';
6
6
  export async function stepNavigate(page, params, data, args) {
7
7
  const url = render(params, { args, data });
8
8
  await page.goto(String(url));
@@ -49,7 +49,7 @@ export async function stepSnapshot(page, params, _data, _args) {
49
49
  }
50
50
  export async function stepEvaluate(page, params, data, args) {
51
51
  const js = String(render(params, { args, data }));
52
- let result = await page.evaluate(normalizeEvaluateSource(js));
52
+ let result = await page.evaluate(js);
53
53
  // MCP may return JSON as a string — auto-parse it
54
54
  if (typeof result === 'string') {
55
55
  const trimmed = result.trim();
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Pipeline step: intercept — declarative XHR interception.
3
3
  */
4
- import { render } from '../template.js';
4
+ import { render, normalizeEvaluateSource } from '../template.js';
5
5
  import { generateInterceptorJs, generateReadInterceptedJs } from '../../interceptor.js';
6
6
  export async function stepIntercept(page, params, data, args) {
7
7
  const cfg = typeof params === 'object' ? params : {};
@@ -20,7 +20,6 @@ export async function stepIntercept(page, params, data, args) {
20
20
  }
21
21
  else if (trigger.startsWith('evaluate:')) {
22
22
  const js = trigger.slice('evaluate:'.length);
23
- const { normalizeEvaluateSource } = await import('../template.js');
24
23
  await page.evaluate(normalizeEvaluateSource(render(js, { args, data })));
25
24
  }
26
25
  else if (trigger.startsWith('click:')) {
package/dist/runtime.d.ts CHANGED
@@ -1,6 +1,3 @@
1
- /**
2
- * Runtime utilities: timeouts and browser session management.
3
- */
4
1
  import type { IPage } from './types.js';
5
2
  export declare const DEFAULT_BROWSER_CONNECT_TIMEOUT: number;
6
3
  export declare const DEFAULT_BROWSER_COMMAND_TIMEOUT: number;
@@ -17,7 +14,7 @@ export declare function runWithTimeout<T>(promise: Promise<T>, opts: {
17
14
  * Timeout with milliseconds unit. Used for low-level internal timeouts.
18
15
  */
19
16
  export declare function withTimeoutMs<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T>;
20
- /** Interface for browser factory (PlaywrightMCP or test mocks) */
17
+ /** Interface for browser factory (BrowserBridge or test mocks) */
21
18
  export interface IBrowserFactory {
22
19
  connect(opts?: {
23
20
  timeout?: number;
package/dist/runtime.js CHANGED
@@ -1,8 +1,5 @@
1
- /**
2
- * Runtime utilities: timeouts and browser session management.
3
- */
4
1
  export const DEFAULT_BROWSER_CONNECT_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_CONNECT_TIMEOUT ?? '30', 10);
5
- export const DEFAULT_BROWSER_COMMAND_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_COMMAND_TIMEOUT ?? '45', 10);
2
+ export const DEFAULT_BROWSER_COMMAND_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_COMMAND_TIMEOUT ?? '60', 10);
6
3
  export const DEFAULT_BROWSER_EXPLORE_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_EXPLORE_TIMEOUT ?? '120', 10);
7
4
  export const DEFAULT_BROWSER_SMOKE_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_SMOKE_TIMEOUT ?? '60', 10);
8
5
  /**
package/dist/setup.d.ts CHANGED
@@ -1,3 +1,9 @@
1
+ /**
2
+ * setup.ts — Interactive browser setup for opencli
3
+ *
4
+ * Simplified for daemon-based architecture. No more token management.
5
+ * Just verifies daemon + extension connectivity.
6
+ */
1
7
  export declare function runSetup(opts?: {
2
8
  cliVersion?: string;
3
9
  token?: string;
package/dist/setup.js CHANGED
@@ -1,180 +1,66 @@
1
1
  /**
2
- * setup.ts — Interactive Playwright MCP token setup
2
+ * setup.ts — Interactive browser setup for opencli
3
3
  *
4
- * Discovers the extension token, shows an interactive checkbox
5
- * for selecting which config files to update, and applies changes.
4
+ * Simplified for daemon-based architecture. No more token management.
5
+ * Just verifies daemon + extension connectivity.
6
6
  */
7
- import * as fs from 'node:fs';
8
7
  import chalk from 'chalk';
9
- import { createInterface } from 'node:readline/promises';
10
- import { stdin as input, stdout as output } from 'node:process';
11
- import { PLAYWRIGHT_TOKEN_ENV, checkExtensionInstalled, checkTokenConnectivity, discoverExtensionToken, fileExists, getDefaultShellRcPath, runBrowserDoctor, shortenPath, toolName, upsertJsonConfigToken, upsertShellToken, upsertTomlConfigToken, writeFileWithMkdir, } from './doctor.js';
12
- import { getTokenFingerprint } from './browser/index.js';
13
- import { checkboxPrompt } from './tui.js';
8
+ import { checkDaemonStatus } from './browser/discover.js';
9
+ import { checkConnectivity } from './doctor.js';
10
+ import { BrowserBridge } from './browser/index.js';
14
11
  export async function runSetup(opts = {}) {
15
12
  console.log();
16
- console.log(chalk.bold(' opencli setup') + chalk.dim(' — Playwright MCP token configuration'));
13
+ console.log(chalk.bold(' opencli setup') + chalk.dim(' — browser bridge configuration'));
17
14
  console.log();
18
- // Step 1: Discover token
19
- let token = opts.token ?? null;
20
- if (!token) {
21
- const extensionToken = discoverExtensionToken();
22
- const envToken = process.env[PLAYWRIGHT_TOKEN_ENV] ?? null;
23
- if (extensionToken && envToken && extensionToken === envToken) {
24
- token = extensionToken;
25
- console.log(` ${chalk.green('✓')} Token auto-discovered from Chrome extension`);
26
- console.log(` Fingerprint: ${chalk.bold(getTokenFingerprint(token) ?? 'unknown')}`);
27
- }
28
- else if (extensionToken) {
29
- token = extensionToken;
30
- console.log(` ${chalk.green('✓')} Token discovered from Chrome extension ` +
31
- chalk.dim(`(${getTokenFingerprint(token)})`));
32
- if (envToken && envToken !== extensionToken) {
33
- console.log(` ${chalk.yellow('!')} Environment has different token ` +
34
- chalk.dim(`(${getTokenFingerprint(envToken)})`));
35
- }
36
- }
37
- else if (envToken) {
38
- token = envToken;
39
- console.log(` ${chalk.green('✓')} Token from environment variable ` +
40
- chalk.dim(`(${getTokenFingerprint(token)})`));
41
- }
15
+ // Step 1: Check daemon
16
+ console.log(chalk.dim(' Checking daemon status...'));
17
+ const status = await checkDaemonStatus();
18
+ if (status.running) {
19
+ console.log(` ${chalk.green('✓')} Daemon is running on port 19825`);
42
20
  }
43
21
  else {
44
- console.log(` ${chalk.green('')} Using provided token ` +
45
- chalk.dim(`(${getTokenFingerprint(token)})`));
46
- }
47
- if (!token) {
48
- // Give precise diagnosis of why token scan failed
49
- const extInstall = checkExtensionInstalled();
50
- console.log(` ${chalk.red('✗')} Browser token scan failed\n`);
51
- if (!extInstall.installed) {
52
- console.log(chalk.dim(' Cause: Playwright MCP Bridge extension is not installed'));
53
- console.log(chalk.dim(' Fix: Install from https://chromewebstore.google.com/detail/'));
54
- console.log(chalk.dim(' playwright-mcp-bridge/mmlmfjhmonkocbjadbfplnigmagldckm'));
55
- }
56
- else {
57
- console.log(chalk.dim(` Cause: Extension is installed (${extInstall.browsers.join(', ')}) but token not found in LevelDB`));
58
- console.log(chalk.dim(' Fix: 1) Open the extension popup and verify the token is generated'));
59
- console.log(chalk.dim(' 2) Close Chrome completely, then re-run setup'));
60
- }
61
- console.log();
62
- console.log(` You can enter the token manually, or fix the above and re-run ${chalk.bold('opencli setup')}.`);
63
- console.log();
64
- const rl = createInterface({ input, output });
65
- const answer = await rl.question(' Token (press Enter to abort): ');
66
- rl.close();
67
- token = answer.trim();
68
- if (!token) {
69
- console.log(chalk.red('\n No token provided. Aborting.\n'));
70
- return;
71
- }
72
- }
73
- const fingerprint = getTokenFingerprint(token) ?? 'unknown';
74
- console.log();
75
- // Step 2: Scan all config locations
76
- const report = await runBrowserDoctor({ token, cliVersion: opts.cliVersion });
77
- // Step 3: Build checkbox items
78
- const items = [];
79
- // Shell file
80
- const shellPath = report.shellFiles[0]?.path ?? getDefaultShellRcPath();
81
- const shellStatus = report.shellFiles[0];
82
- const shellFp = shellStatus?.fingerprint;
83
- const shellOk = shellFp === fingerprint;
84
- const shellTool = toolName(shellPath) || 'Shell';
85
- items.push({
86
- label: padRight(shortenPath(shellPath), 50) + chalk.dim(` [${shellTool}]`),
87
- value: `shell:${shellPath}`,
88
- checked: !shellOk,
89
- status: shellOk ? `configured (${shellFp})` : shellFp ? `mismatch (${shellFp})` : 'missing',
90
- statusColor: shellOk ? 'green' : shellFp ? 'yellow' : 'red',
91
- });
92
- // Config files
93
- for (const config of report.configs) {
94
- const fp = config.fingerprint;
95
- const ok = fp === fingerprint;
96
- const tool = toolName(config.path);
97
- items.push({
98
- label: padRight(shortenPath(config.path), 50) + chalk.dim(tool ? ` [${tool}]` : ''),
99
- value: `config:${config.path}`,
100
- checked: false, // let user explicitly select which tools to configure
101
- status: ok ? `configured (${fp})` : !config.exists ? 'will create' : fp ? `mismatch (${fp})` : 'missing',
102
- statusColor: ok ? 'green' : 'yellow',
103
- });
104
- }
105
- // Step 4: Show interactive checkbox
106
- console.clear();
107
- const selected = await checkboxPrompt(items, {
108
- title: ` ${chalk.bold('opencli setup')} — token ${chalk.cyan(fingerprint)}`,
109
- });
110
- if (selected.length === 0) {
111
- console.log(chalk.dim(' No changes made.\n'));
112
- return;
113
- }
114
- // Step 5: Apply changes
115
- const written = [];
116
- let wroteShell = false;
117
- for (const sel of selected) {
118
- if (sel.startsWith('shell:')) {
119
- const p = sel.slice('shell:'.length);
120
- const before = fileExists(p) ? fs.readFileSync(p, 'utf-8') : '';
121
- writeFileWithMkdir(p, upsertShellToken(before, token, p));
122
- written.push(p);
123
- wroteShell = true;
22
+ console.log(` ${chalk.yellow('!')} Daemon is not running`);
23
+ console.log(chalk.dim(' The daemon starts automatically when you run a browser command.'));
24
+ console.log(chalk.dim(' Starting daemon now...'));
25
+ // Try to spawn daemon
26
+ const mcp = new BrowserBridge();
27
+ try {
28
+ await mcp.connect({ timeout: 5 });
29
+ await mcp.close();
30
+ console.log(` ${chalk.green('✓')} Daemon started successfully`);
124
31
  }
125
- else if (sel.startsWith('config:')) {
126
- const p = sel.slice('config:'.length);
127
- const config = report.configs.find(c => c.path === p);
128
- if (config && config.parseError)
129
- continue;
130
- const before = fileExists(p) ? fs.readFileSync(p, 'utf-8') : '';
131
- const format = config?.format ?? (p.endsWith('.toml') ? 'toml' : 'json');
132
- const next = format === 'toml' ? upsertTomlConfigToken(before, token) : upsertJsonConfigToken(before, token, p);
133
- writeFileWithMkdir(p, next);
134
- written.push(p);
32
+ catch {
33
+ console.log(` ${chalk.yellow('!')} Could not start daemon automatically`);
135
34
  }
136
35
  }
137
- process.env[PLAYWRIGHT_TOKEN_ENV] = token;
138
- // Step 6: Summary
139
- if (written.length > 0) {
140
- console.log(chalk.green.bold(` Updated ${written.length} file(s):`));
141
- for (const p of written) {
142
- const tool = toolName(p);
143
- console.log(` ${chalk.dim('•')} ${shortenPath(p)}${tool ? chalk.dim(` [${tool}]`) : ''}`);
144
- }
145
- if (wroteShell) {
146
- console.log();
147
- console.log(chalk.cyan(` 💡 Run ${chalk.bold(`source ${shortenPath(shellPath)}`)} to apply token to current shell.`));
148
- }
36
+ // Step 2: Check extension
37
+ const statusAfter = await checkDaemonStatus();
38
+ if (statusAfter.extensionConnected) {
39
+ console.log(` ${chalk.green('')} Chrome extension connected`);
149
40
  }
150
41
  else {
151
- console.log(chalk.yellow(' No files were changed.'));
42
+ console.log(` ${chalk.red('✗')} Chrome extension not connected`);
43
+ console.log();
44
+ console.log(chalk.dim(' To install the opencli Browser Bridge extension:'));
45
+ console.log(chalk.dim(' 1. Download from GitHub Releases'));
46
+ console.log(chalk.dim(' 2. Open chrome://extensions/ → Enable Developer Mode'));
47
+ console.log(chalk.dim(' 3. Click "Load unpacked" → select the extension folder'));
48
+ console.log(chalk.dim(' 4. Make sure Chrome is running'));
49
+ console.log();
50
+ return;
152
51
  }
52
+ // Step 3: Test connectivity
153
53
  console.log();
154
- // Step 7: Auto-verify browser connectivity
155
- console.log(chalk.dim(' Verifying browser connectivity...'));
156
- try {
157
- const result = await checkTokenConnectivity({ timeout: 5 });
158
- if (result.ok) {
159
- console.log(` ${chalk.green('✓')} Browser connected in ${(result.durationMs / 1000).toFixed(1)}s`);
160
- }
161
- else {
162
- console.log(` ${chalk.green('✓')} Token saved successfully.`);
163
- console.log(` ${chalk.yellow('!')} Browser connectivity test failed: ${result.error ?? 'unknown'}`);
164
- console.log(chalk.dim(' Token configuration is complete. To use opencli, make sure Chrome'));
165
- console.log(chalk.dim(' is running with the Playwright MCP Bridge extension enabled.'));
166
- console.log(chalk.dim(` Run ${chalk.bold('opencli doctor --live')} to re-test connectivity.`));
167
- }
54
+ console.log(chalk.dim(' Testing browser connectivity...'));
55
+ const conn = await checkConnectivity({ timeout: 5 });
56
+ if (conn.ok) {
57
+ console.log(` ${chalk.green('✓')} Browser connected in ${(conn.durationMs / 1000).toFixed(1)}s`);
58
+ console.log();
59
+ console.log(chalk.green.bold(' Setup complete! You can now use opencli browser commands.'));
168
60
  }
169
- catch {
170
- console.log(` ${chalk.green('')} Token saved successfully.`);
171
- console.log(` ${chalk.yellow('!')} Browser connectivity test skipped (Chrome may not be running).`);
172
- console.log(chalk.dim(' Token configuration is complete. Start Chrome to begin using opencli.'));
173
- console.log(chalk.dim(` Run ${chalk.bold('opencli doctor --live')} to re-test connectivity.`));
61
+ else {
62
+ console.log(` ${chalk.yellow('!')} Connectivity test failed: ${conn.error ?? 'unknown'}`);
63
+ console.log(chalk.dim(` Run ${chalk.bold('opencli doctor --live')} to diagnose.`));
174
64
  }
175
65
  console.log();
176
66
  }
177
- function padRight(s, n) {
178
- const visible = s.replace(/\x1b\[[0-9;]*m/g, '');
179
- return visible.length >= n ? s : s + ' '.repeat(n - visible.length);
180
- }
package/dist/types.d.ts CHANGED
@@ -34,4 +34,10 @@ export interface IPage {
34
34
  }): Promise<void>;
35
35
  installInterceptor(pattern: string): Promise<void>;
36
36
  getInterceptedRequests(): Promise<any[]>;
37
+ screenshot(options?: {
38
+ format?: 'png' | 'jpeg';
39
+ quality?: number;
40
+ fullPage?: boolean;
41
+ path?: string;
42
+ }): Promise<string>;
37
43
  }