@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
package/dist/doctor.js CHANGED
@@ -1,425 +1,22 @@
1
- import * as fs from 'node:fs';
2
- import * as os from 'node:os';
3
- import * as path from 'node:path';
4
- import { createInterface } from 'node:readline/promises';
5
- import { stdin as input, stdout as output } from 'node:process';
6
- import chalk from 'chalk';
7
- import { PlaywrightMCP, getTokenFingerprint } from './browser/index.js';
8
- const PLAYWRIGHT_SERVER_NAME = 'playwright';
9
- export const PLAYWRIGHT_TOKEN_ENV = 'PLAYWRIGHT_MCP_EXTENSION_TOKEN';
10
- const PLAYWRIGHT_EXTENSION_ID = 'mmlmfjhmonkocbjadbfplnigmagldckm';
11
- const TOKEN_LINE_RE = /^(\s*export\s+PLAYWRIGHT_MCP_EXTENSION_TOKEN=)(['"]?)([^'"\\\n]+)\2\s*$/m;
12
- function colorLabel(status) {
13
- switch (status) {
14
- case 'OK': return chalk.green('[OK]');
15
- case 'MISSING': return chalk.red('[MISSING]');
16
- case 'MISMATCH': return chalk.yellow('[MISMATCH]');
17
- case 'WARN': return chalk.yellow('[WARN]');
18
- }
19
- }
20
- function statusLine(status, text) {
21
- return `${colorLabel(status)} ${text}`;
22
- }
23
- function tokenSummary(token, fingerprint) {
24
- if (!token)
25
- return chalk.dim('missing');
26
- return `configured ${chalk.dim(`(${fingerprint})`)}`;
27
- }
28
- export function shortenPath(p) {
29
- const home = os.homedir();
30
- return home && p.startsWith(home) ? '~' + p.slice(home.length) : p;
31
- }
32
- export function toolName(p) {
33
- if (p.includes('.codex/'))
34
- return 'Codex';
35
- if (p.includes('.cursor/'))
36
- return 'Cursor';
37
- if (p.includes('.claude.json'))
38
- return 'Claude Code';
39
- if (p.includes('antigravity'))
40
- return 'Antigravity';
41
- if (p.includes('.gemini/settings'))
42
- return 'Gemini CLI';
43
- if (p.includes('opencode'))
44
- return 'OpenCode';
45
- if (p.includes('Claude/claude_desktop'))
46
- return 'Claude Desktop';
47
- if (p.includes('.vscode/'))
48
- return 'VS Code';
49
- if (p.includes('.mcp.json'))
50
- return 'Project MCP';
51
- if (p.includes('.zshrc') || p.includes('.bashrc') || p.includes('.profile'))
52
- return 'Shell';
53
- return '';
54
- }
55
- export function getDefaultShellRcPath() {
56
- const shell = process.env.SHELL ?? '';
57
- if (shell.endsWith('/bash'))
58
- return path.join(os.homedir(), '.bashrc');
59
- if (shell.endsWith('/fish'))
60
- return path.join(os.homedir(), '.config', 'fish', 'config.fish');
61
- return path.join(os.homedir(), '.zshrc');
62
- }
63
- function isFishConfig(filePath) {
64
- return filePath.endsWith('config.fish') || filePath.includes('/fish/');
65
- }
66
- /** Detect if a JSON config file uses OpenCode's `mcp` format vs standard `mcpServers` */
67
- function isOpenCodeConfig(filePath) {
68
- return filePath.includes('opencode');
69
- }
70
- export function getDefaultMcpConfigPaths(cwd = process.cwd()) {
71
- const home = os.homedir();
72
- const candidates = [
73
- path.join(home, '.codex', 'config.toml'),
74
- path.join(home, '.codex', 'mcp.json'),
75
- path.join(home, '.cursor', 'mcp.json'),
76
- path.join(home, '.claude.json'),
77
- path.join(home, '.gemini', 'settings.json'),
78
- path.join(home, '.gemini', 'antigravity', 'mcp_config.json'),
79
- path.join(home, '.config', 'opencode', 'opencode.json'),
80
- path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'),
81
- path.join(home, '.config', 'Claude', 'claude_desktop_config.json'),
82
- path.join(cwd, '.cursor', 'mcp.json'),
83
- path.join(cwd, '.vscode', 'mcp.json'),
84
- path.join(cwd, '.opencode', 'opencode.json'),
85
- path.join(cwd, '.mcp.json'),
86
- ];
87
- return [...new Set(candidates)];
88
- }
89
- export function readTokenFromShellContent(content) {
90
- const m = content.match(TOKEN_LINE_RE);
91
- return m?.[3] ?? null;
92
- }
93
- export function upsertShellToken(content, token, filePath) {
94
- if (filePath && isFishConfig(filePath)) {
95
- // Fish shell uses `set -gx` instead of `export`
96
- const fishLine = `set -gx ${PLAYWRIGHT_TOKEN_ENV} "${token}"`;
97
- const fishRe = /^\s*set\s+(-gx\s+)?PLAYWRIGHT_MCP_EXTENSION_TOKEN\s+.*/m;
98
- if (!content.trim())
99
- return `${fishLine}\n`;
100
- if (fishRe.test(content))
101
- return content.replace(fishRe, fishLine);
102
- return `${content.replace(/\s*$/, '')}\n${fishLine}\n`;
103
- }
104
- const nextLine = `export ${PLAYWRIGHT_TOKEN_ENV}="${token}"`;
105
- if (!content.trim())
106
- return `${nextLine}\n`;
107
- if (TOKEN_LINE_RE.test(content))
108
- return content.replace(TOKEN_LINE_RE, `$1"${token}"`);
109
- return `${content.replace(/\s*$/, '')}\n${nextLine}\n`;
110
- }
111
- function readJsonConfigToken(content) {
112
- try {
113
- const parsed = JSON.parse(content);
114
- return readTokenFromJsonObject(parsed);
115
- }
116
- catch {
117
- return null;
118
- }
119
- }
120
- function readTokenFromJsonObject(parsed) {
121
- const direct = parsed?.mcpServers?.[PLAYWRIGHT_SERVER_NAME]?.env?.[PLAYWRIGHT_TOKEN_ENV];
122
- if (typeof direct === 'string' && direct)
123
- return direct;
124
- const opencode = parsed?.mcp?.[PLAYWRIGHT_SERVER_NAME]?.environment?.[PLAYWRIGHT_TOKEN_ENV];
125
- if (typeof opencode === 'string' && opencode)
126
- return opencode;
127
- return null;
128
- }
129
- export function upsertJsonConfigToken(content, token, filePath) {
130
- const parsed = content.trim() ? JSON.parse(content) : {};
131
- // Determine format: use OpenCode format only if explicitly an opencode config,
132
- // or if the existing content already uses `mcp` key (not `mcpServers`)
133
- const useOpenCodeFormat = filePath
134
- ? isOpenCodeConfig(filePath)
135
- : (!parsed.mcpServers && parsed.mcp);
136
- if (useOpenCodeFormat) {
137
- parsed.mcp = parsed.mcp ?? {};
138
- parsed.mcp[PLAYWRIGHT_SERVER_NAME] = parsed.mcp[PLAYWRIGHT_SERVER_NAME] ?? {
139
- command: ['npx', '-y', '@playwright/mcp@latest', '--extension'],
140
- enabled: true,
141
- type: 'local',
142
- };
143
- parsed.mcp[PLAYWRIGHT_SERVER_NAME].environment = parsed.mcp[PLAYWRIGHT_SERVER_NAME].environment ?? {};
144
- parsed.mcp[PLAYWRIGHT_SERVER_NAME].environment[PLAYWRIGHT_TOKEN_ENV] = token;
145
- }
146
- else {
147
- parsed.mcpServers = parsed.mcpServers ?? {};
148
- parsed.mcpServers[PLAYWRIGHT_SERVER_NAME] = parsed.mcpServers[PLAYWRIGHT_SERVER_NAME] ?? {
149
- command: 'npx',
150
- args: ['-y', '@playwright/mcp@latest', '--extension'],
151
- };
152
- parsed.mcpServers[PLAYWRIGHT_SERVER_NAME].env = parsed.mcpServers[PLAYWRIGHT_SERVER_NAME].env ?? {};
153
- parsed.mcpServers[PLAYWRIGHT_SERVER_NAME].env[PLAYWRIGHT_TOKEN_ENV] = token;
154
- }
155
- return `${JSON.stringify(parsed, null, 2)}\n`;
156
- }
157
- export function readTomlConfigToken(content) {
158
- const sectionMatch = content.match(/\[mcp_servers\.playwright\.env\][\s\S]*?(?=\n\[|$)/);
159
- if (!sectionMatch)
160
- return null;
161
- const tokenMatch = sectionMatch[0].match(/^\s*PLAYWRIGHT_MCP_EXTENSION_TOKEN\s*=\s*"([^"\n]+)"/m);
162
- return tokenMatch?.[1] ?? null;
163
- }
164
- export function upsertTomlConfigToken(content, token) {
165
- const envSectionRe = /(\[mcp_servers\.playwright\.env\][\s\S]*?)(?=\n\[|$)/;
166
- const tokenLine = `PLAYWRIGHT_MCP_EXTENSION_TOKEN = "${token}"`;
167
- if (envSectionRe.test(content)) {
168
- return content.replace(envSectionRe, (section) => {
169
- if (/^\s*PLAYWRIGHT_MCP_EXTENSION_TOKEN\s*=/m.test(section)) {
170
- return section.replace(/^\s*PLAYWRIGHT_MCP_EXTENSION_TOKEN\s*=.*$/m, tokenLine);
171
- }
172
- return `${section.replace(/\s*$/, '')}\n${tokenLine}\n`;
173
- });
174
- }
175
- const baseSectionRe = /(\[mcp_servers\.playwright\][\s\S]*?)(?=\n\[|$)/;
176
- if (baseSectionRe.test(content)) {
177
- return content.replace(baseSectionRe, (section) => `${section.replace(/\s*$/, '')}\n\n[mcp_servers.playwright.env]\n${tokenLine}\n`);
178
- }
179
- const prefix = content.trim() ? `${content.replace(/\s*$/, '')}\n\n` : '';
180
- return `${prefix}[mcp_servers.playwright]\ntype = "stdio"\ncommand = "npx"\nargs = ["-y", "@playwright/mcp@latest", "--extension"]\n\n[mcp_servers.playwright.env]\n${tokenLine}\n`;
181
- }
182
- export function fileExists(filePath) {
183
- try {
184
- return fs.existsSync(filePath);
185
- }
186
- catch {
187
- return false;
188
- }
189
- }
190
- function canWrite(filePath) {
191
- try {
192
- if (fileExists(filePath)) {
193
- fs.accessSync(filePath, fs.constants.W_OK);
194
- return true;
195
- }
196
- fs.accessSync(path.dirname(filePath), fs.constants.W_OK);
197
- return true;
198
- }
199
- catch {
200
- return false;
201
- }
202
- }
203
- function readConfigStatus(filePath) {
204
- const format = filePath.endsWith('.toml') ? 'toml' : 'json';
205
- if (!fileExists(filePath)) {
206
- return { path: filePath, exists: false, format, token: null, fingerprint: null, writable: canWrite(filePath) };
207
- }
208
- try {
209
- const content = fs.readFileSync(filePath, 'utf-8');
210
- const token = format === 'toml' ? readTomlConfigToken(content) : readJsonConfigToken(content);
211
- return {
212
- path: filePath,
213
- exists: true,
214
- format,
215
- token,
216
- fingerprint: getTokenFingerprint(token ?? undefined),
217
- writable: canWrite(filePath),
218
- };
219
- }
220
- catch (error) {
221
- return {
222
- path: filePath,
223
- exists: true,
224
- format,
225
- token: null,
226
- fingerprint: null,
227
- writable: canWrite(filePath),
228
- parseError: error?.message ?? String(error),
229
- };
230
- }
231
- }
232
- /**
233
- * Dynamically enumerate Chrome profiles by scanning for 'Default' and 'Profile *'
234
- * directories across all browser base paths. Falls back to ['Default'] if none found.
235
- */
236
- function enumerateProfiles(baseDirs) {
237
- const profiles = new Set();
238
- for (const base of baseDirs) {
239
- if (!fileExists(base))
240
- continue;
241
- try {
242
- for (const entry of fs.readdirSync(base, { withFileTypes: true })) {
243
- if (!entry.isDirectory())
244
- continue;
245
- if (entry.name === 'Default' || /^Profile \d+$/.test(entry.name)) {
246
- profiles.add(entry.name);
247
- }
248
- }
249
- }
250
- catch { /* permission denied, etc. */ }
251
- }
252
- return profiles.size > 0 ? [...profiles].sort() : ['Default'];
253
- }
254
1
  /**
255
- * Discover the auth token stored by the Playwright MCP Bridge extension
256
- * by scanning Chrome's LevelDB localStorage files directly.
2
+ * opencli doctor diagnose and fix browser connectivity.
257
3
  *
258
- * Reads LevelDB .ldb/.log files as raw binary and searches for the
259
- * extension ID near base64url token values. This works reliably across
260
- * platforms because LevelDB's internal encoding can split ASCII strings
261
- * like "auth-token" and the extension ID across byte boundaries, making
262
- * text-based tools like `strings` + `grep` unreliable.
4
+ * Simplified for the daemon-based architecture. No more token management,
5
+ * MCP path discovery, or config file scanning.
263
6
  */
264
- export function discoverExtensionToken() {
265
- const home = os.homedir();
266
- const platform = os.platform();
267
- const bases = [];
268
- if (platform === 'darwin') {
269
- bases.push(path.join(home, 'Library', 'Application Support', 'Google', 'Chrome'), path.join(home, 'Library', 'Application Support', 'Google', 'Chrome Dev'), path.join(home, 'Library', 'Application Support', 'Google', 'Chrome Beta'), path.join(home, 'Library', 'Application Support', 'Google', 'Chrome Canary'), path.join(home, 'Library', 'Application Support', 'Chromium'), path.join(home, 'Library', 'Application Support', 'Microsoft Edge'));
270
- }
271
- else if (platform === 'linux') {
272
- bases.push(path.join(home, '.config', 'google-chrome'), path.join(home, '.config', 'google-chrome-unstable'), path.join(home, '.config', 'google-chrome-beta'), path.join(home, '.config', 'chromium'), path.join(home, '.config', 'microsoft-edge'));
273
- }
274
- else if (platform === 'win32') {
275
- const appData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
276
- bases.push(path.join(appData, 'Google', 'Chrome', 'User Data'), path.join(appData, 'Google', 'Chrome Dev', 'User Data'), path.join(appData, 'Google', 'Chrome Beta', 'User Data'), path.join(appData, 'Microsoft', 'Edge', 'User Data'));
277
- }
278
- const profiles = enumerateProfiles(bases);
279
- const tokenRe = /([A-Za-z0-9_-]{40,50})/;
280
- for (const base of bases) {
281
- for (const profile of profiles) {
282
- const dir = path.join(base, profile, 'Local Storage', 'leveldb');
283
- if (!fileExists(dir))
284
- continue;
285
- const token = extractTokenViaBinaryRead(dir, tokenRe);
286
- if (token)
287
- return token;
288
- }
289
- }
290
- return null;
291
- }
292
- function extractTokenViaBinaryRead(dir, tokenRe) {
293
- // LevelDB fragments strings across byte boundaries, so we can't search
294
- // for the full extension ID or "auth-token" as contiguous ASCII. Instead,
295
- // search for a short prefix of the extension ID that reliably appears as
296
- // contiguous bytes, then scan a window around each match for a base64url
297
- // token value.
298
- //
299
- // Observed LevelDB layout near the auth-token entry:
300
- // ... auth-t<binary> ... 4,mmlmfjh<binary>Pocbjadbfplnigmagldckm.7 ...
301
- // <binary> hqI86ncsD1QpcVcj-k9CyzTF-ieCQd_4KreZ_wy1WHA <binary> ...
302
- //
303
- // The extension ID prefix "mmlmfjh" appears ~44 bytes before the token.
304
- const extIdBuf = Buffer.from(PLAYWRIGHT_EXTENSION_ID);
305
- const extIdPrefix = Buffer.from(PLAYWRIGHT_EXTENSION_ID.slice(0, 7)); // "mmlmfjh"
306
- let files;
307
- try {
308
- files = fs.readdirSync(dir)
309
- .filter(f => f.endsWith('.ldb') || f.endsWith('.log'))
310
- .map(f => path.join(dir, f));
311
- }
312
- catch {
313
- return null;
314
- }
315
- // Sort by mtime descending so we find the freshest token first
316
- files.sort((a, b) => {
317
- try {
318
- return fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs;
319
- }
320
- catch {
321
- return 0;
322
- }
323
- });
324
- for (const file of files) {
325
- let data;
326
- try {
327
- data = fs.readFileSync(file);
328
- }
329
- catch {
330
- continue;
331
- }
332
- // Quick check: file must contain at least the prefix
333
- if (data.indexOf(extIdPrefix) === -1)
334
- continue;
335
- // Strategy 1: scan after each occurrence of the extension ID prefix
336
- // for base64url tokens within a 500-byte window
337
- let idx = 0;
338
- while (true) {
339
- const pos = data.indexOf(extIdPrefix, idx);
340
- if (pos === -1)
341
- break;
342
- const scanStart = pos;
343
- const scanEnd = Math.min(data.length, pos + 500);
344
- const window = data.subarray(scanStart, scanEnd).toString('latin1');
345
- const m = window.match(tokenRe);
346
- if (m && validateBase64urlToken(m[1])) {
347
- // Make sure this isn't another extension ID that happens to match
348
- if (m[1] !== PLAYWRIGHT_EXTENSION_ID)
349
- return m[1];
350
- }
351
- idx = pos + 1;
352
- }
353
- // Strategy 2 (fallback): original approach using full extension ID + auth-token key
354
- const keyBuf = Buffer.from('auth-token');
355
- idx = 0;
356
- while (true) {
357
- const kp = data.indexOf(keyBuf, idx);
358
- if (kp === -1)
359
- break;
360
- const contextStart = Math.max(0, kp - 500);
361
- if (data.indexOf(extIdBuf, contextStart) !== -1 && data.indexOf(extIdBuf, contextStart) < kp) {
362
- const after = data.subarray(kp + keyBuf.length, kp + keyBuf.length + 200).toString('latin1');
363
- const m = after.match(tokenRe);
364
- if (m && validateBase64urlToken(m[1]))
365
- return m[1];
366
- }
367
- idx = kp + 1;
368
- }
369
- }
370
- return null;
371
- }
372
- function validateBase64urlToken(token) {
373
- try {
374
- const b64 = token.replace(/-/g, '+').replace(/_/g, '/');
375
- const decoded = Buffer.from(b64, 'base64');
376
- return decoded.length >= 28 && decoded.length <= 36;
377
- }
378
- catch {
379
- return false;
380
- }
381
- }
382
- /**
383
- * Check whether the Playwright MCP Bridge extension is installed in any browser.
384
- * Scans Chrome/Chromium/Edge Extensions directories for the known extension ID.
385
- */
386
- export function checkExtensionInstalled() {
387
- const home = os.homedir();
388
- const platform = os.platform();
389
- const browserDirs = [];
390
- if (platform === 'darwin') {
391
- browserDirs.push({ name: 'Chrome', base: path.join(home, 'Library', 'Application Support', 'Google', 'Chrome') }, { name: 'Chrome Dev', base: path.join(home, 'Library', 'Application Support', 'Google', 'Chrome Dev') }, { name: 'Chrome Beta', base: path.join(home, 'Library', 'Application Support', 'Google', 'Chrome Beta') }, { name: 'Chrome Canary', base: path.join(home, 'Library', 'Application Support', 'Google', 'Chrome Canary') }, { name: 'Chromium', base: path.join(home, 'Library', 'Application Support', 'Chromium') }, { name: 'Edge', base: path.join(home, 'Library', 'Application Support', 'Microsoft Edge') });
392
- }
393
- else if (platform === 'linux') {
394
- browserDirs.push({ name: 'Chrome', base: path.join(home, '.config', 'google-chrome') }, { name: 'Chrome Dev', base: path.join(home, '.config', 'google-chrome-unstable') }, { name: 'Chrome Beta', base: path.join(home, '.config', 'google-chrome-beta') }, { name: 'Chromium', base: path.join(home, '.config', 'chromium') }, { name: 'Edge', base: path.join(home, '.config', 'microsoft-edge') });
395
- }
396
- else if (platform === 'win32') {
397
- const appData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
398
- browserDirs.push({ name: 'Chrome', base: path.join(appData, 'Google', 'Chrome', 'User Data') }, { name: 'Chrome Dev', base: path.join(appData, 'Google', 'Chrome Dev', 'User Data') }, { name: 'Chrome Beta', base: path.join(appData, 'Google', 'Chrome Beta', 'User Data') }, { name: 'Edge', base: path.join(appData, 'Microsoft', 'Edge', 'User Data') });
399
- }
400
- const profiles = enumerateProfiles(browserDirs.map(d => d.base));
401
- const foundBrowsers = [];
402
- for (const { name, base } of browserDirs) {
403
- for (const profile of profiles) {
404
- const extDir = path.join(base, profile, 'Extensions', PLAYWRIGHT_EXTENSION_ID);
405
- if (fileExists(extDir)) {
406
- foundBrowsers.push(name);
407
- break; // one match per browser is enough
408
- }
409
- }
410
- }
411
- return { installed: foundBrowsers.length > 0, browsers: [...new Set(foundBrowsers)] };
412
- }
7
+ import chalk from 'chalk';
8
+ import { checkDaemonStatus } from './browser/discover.js';
9
+ import { BrowserBridge } from './browser/index.js';
413
10
  /**
414
- * Test token connectivity by attempting a real MCP connection.
415
- * Connects, does the JSON-RPC handshake, and immediately closes.
11
+ * Test connectivity by attempting a real browser command.
416
12
  */
417
- export async function checkTokenConnectivity(opts) {
418
- const timeout = opts?.timeout ?? 8;
13
+ export async function checkConnectivity(opts) {
419
14
  const start = Date.now();
420
15
  try {
421
- const mcp = new PlaywrightMCP();
422
- await mcp.connect({ timeout });
16
+ const mcp = new BrowserBridge();
17
+ const page = await mcp.connect({ timeout: opts?.timeout ?? 8 });
18
+ // Try a simple eval to verify end-to-end connectivity
19
+ await page.evaluate('1 + 1');
423
20
  await mcp.close();
424
21
  return { ok: true, durationMs: Date.now() - start };
425
22
  }
@@ -428,210 +25,60 @@ export async function checkTokenConnectivity(opts) {
428
25
  }
429
26
  }
430
27
  export async function runBrowserDoctor(opts = {}) {
431
- const envToken = process.env[PLAYWRIGHT_TOKEN_ENV] ?? null;
432
- const shellPath = opts.shellRc ?? getDefaultShellRcPath();
433
- const shellFiles = [shellPath].map((filePath) => {
434
- if (!fileExists(filePath))
435
- return { path: filePath, exists: false, token: null, fingerprint: null };
436
- const content = fs.readFileSync(filePath, 'utf-8');
437
- const token = readTokenFromShellContent(content);
438
- return { path: filePath, exists: true, token, fingerprint: getTokenFingerprint(token ?? undefined) };
439
- });
440
- const configPaths = opts.configPaths?.length ? opts.configPaths : getDefaultMcpConfigPaths();
441
- const configs = configPaths.map(readConfigStatus);
442
- // Try to discover the token directly from the Chrome extension's localStorage
443
- const extensionToken = discoverExtensionToken();
444
- const allTokens = [
445
- opts.token ?? null,
446
- extensionToken,
447
- envToken,
448
- ...shellFiles.map(s => s.token),
449
- ...configs.map(c => c.token),
450
- ].filter((v) => !!v);
451
- const uniqueTokens = [...new Set(allTokens)];
452
- const recommendedToken = opts.token ?? extensionToken ?? envToken ?? (uniqueTokens.length === 1 ? uniqueTokens[0] : null) ?? null;
453
- // Check extension installation
454
- const extInstall = checkExtensionInstalled();
455
- // Connectivity test (only when --live)
28
+ const status = await checkDaemonStatus();
456
29
  let connectivity;
457
30
  if (opts.live) {
458
- connectivity = await checkTokenConnectivity();
31
+ connectivity = await checkConnectivity();
32
+ }
33
+ const issues = [];
34
+ if (!status.running) {
35
+ issues.push('Daemon is not running. It should start automatically when you run an opencli browser command.');
36
+ }
37
+ if (status.running && !status.extensionConnected) {
38
+ issues.push('Daemon is running but the Chrome extension is not connected.\n' +
39
+ 'Please install the opencli Browser Bridge extension:\n' +
40
+ ' 1. Download from GitHub Releases\n' +
41
+ ' 2. Open chrome://extensions/ → Enable Developer Mode\n' +
42
+ ' 3. Click "Load unpacked" → select the extension folder');
459
43
  }
460
- const report = {
44
+ if (connectivity && !connectivity.ok) {
45
+ issues.push(`Browser connectivity test failed: ${connectivity.error ?? 'unknown'}`);
46
+ }
47
+ return {
461
48
  cliVersion: opts.cliVersion,
462
- envToken,
463
- envFingerprint: getTokenFingerprint(envToken ?? undefined),
464
- extensionToken,
465
- extensionFingerprint: getTokenFingerprint(extensionToken ?? undefined),
466
- extensionInstalled: extInstall.installed,
467
- extensionBrowsers: extInstall.browsers,
468
- shellFiles,
469
- configs,
470
- recommendedToken,
471
- recommendedFingerprint: getTokenFingerprint(recommendedToken ?? undefined),
49
+ daemonRunning: status.running,
50
+ extensionConnected: status.extensionConnected,
472
51
  connectivity,
473
- warnings: [],
474
- issues: [],
52
+ issues,
475
53
  };
476
- if (!extInstall.installed)
477
- report.issues.push('Playwright MCP Bridge extension is not installed in any browser.');
478
- if (!envToken)
479
- report.issues.push(`Current environment is missing ${PLAYWRIGHT_TOKEN_ENV}.`);
480
- if (!shellFiles.some(s => s.token))
481
- report.issues.push('Shell startup file does not export PLAYWRIGHT_MCP_EXTENSION_TOKEN.');
482
- if (!configs.some(c => c.token))
483
- report.issues.push('No scanned MCP config currently contains a Playwright extension token.');
484
- if (uniqueTokens.length > 1)
485
- report.issues.push('Detected inconsistent Playwright MCP tokens across env/config files.');
486
- if (connectivity && !connectivity.ok)
487
- report.issues.push(`Browser connectivity test failed: ${connectivity.error ?? 'unknown'}`);
488
- for (const config of configs) {
489
- if (config.parseError)
490
- report.warnings.push(`Could not parse ${config.path}: ${config.parseError}`);
491
- }
492
- if (!recommendedToken) {
493
- report.warnings.push('No token source found.');
494
- }
495
- return report;
496
54
  }
497
55
  export function renderBrowserDoctorReport(report) {
498
- const tokenFingerprints = [
499
- report.extensionFingerprint,
500
- report.envFingerprint,
501
- ...report.shellFiles.map(shell => shell.fingerprint),
502
- ...report.configs.filter(config => config.exists).map(config => config.fingerprint),
503
- ].filter((value) => !!value);
504
- const uniqueFingerprints = [...new Set(tokenFingerprints)];
505
- const hasMismatch = uniqueFingerprints.length > 1;
506
56
  const lines = [chalk.bold(`opencli v${report.cliVersion ?? 'unknown'} doctor`), ''];
507
- // CDP endpoint mode (for remote/server environments)
508
- const cdpEndpoint = process.env.OPENCLI_CDP_ENDPOINT;
509
- if (cdpEndpoint) {
510
- lines.push(statusLine('OK', `CDP endpoint: ${chalk.cyan(cdpEndpoint)}`));
511
- lines.push(chalk.dim(' Remote Chrome mode: extension token not required'));
512
- lines.push('');
513
- return lines.join('\n');
514
- }
515
- const installStatus = report.extensionInstalled ? 'OK' : 'MISSING';
516
- const installDetail = report.extensionInstalled
517
- ? `Extension installed (${report.extensionBrowsers.join(', ')})`
518
- : 'Extension not installed in any browser';
519
- lines.push(statusLine(installStatus, installDetail));
520
- const extStatus = !report.extensionToken ? 'MISSING' : hasMismatch ? 'MISMATCH' : 'OK';
521
- lines.push(statusLine(extStatus, `Extension token (Chrome LevelDB): ${tokenSummary(report.extensionToken, report.extensionFingerprint)}`));
522
- const envStatus = !report.envToken ? 'MISSING' : hasMismatch ? 'MISMATCH' : 'OK';
523
- lines.push(statusLine(envStatus, `Environment token: ${tokenSummary(report.envToken, report.envFingerprint)}`));
524
- for (const shell of report.shellFiles) {
525
- const shellStatus = !shell.token ? 'MISSING' : hasMismatch ? 'MISMATCH' : 'OK';
526
- const tool = toolName(shell.path);
527
- const suffix = tool ? chalk.dim(` [${tool}]`) : '';
528
- lines.push(statusLine(shellStatus, `${shortenPath(shell.path)}${suffix}: ${tokenSummary(shell.token, shell.fingerprint)}`));
529
- }
530
- const existingConfigs = report.configs.filter(config => config.exists);
531
- const missingConfigCount = report.configs.length - existingConfigs.length;
532
- if (existingConfigs.length > 0) {
533
- for (const config of existingConfigs) {
534
- const parseSuffix = config.parseError ? chalk.red(` (parse error)`) : '';
535
- const configStatus = config.parseError
536
- ? 'WARN'
537
- : !config.token
538
- ? 'MISSING'
539
- : hasMismatch
540
- ? 'MISMATCH'
541
- : 'OK';
542
- const tool = toolName(config.path);
543
- const suffix = tool ? chalk.dim(` [${tool}]`) : '';
544
- lines.push(statusLine(configStatus, `${shortenPath(config.path)}${suffix}: ${tokenSummary(config.token, config.fingerprint)}${parseSuffix}`));
545
- }
546
- }
547
- else {
548
- lines.push(statusLine('MISSING', 'MCP config: no existing config files found'));
549
- }
550
- if (missingConfigCount > 0)
551
- lines.push(chalk.dim(` Other scanned config locations not present: ${missingConfigCount}`));
552
- lines.push('');
553
- // Connectivity result
57
+ // Daemon status
58
+ const daemonIcon = report.daemonRunning ? chalk.green('[OK]') : chalk.red('[MISSING]');
59
+ lines.push(`${daemonIcon} Daemon: ${report.daemonRunning ? 'running on port 19825' : 'not running'}`);
60
+ // Extension status
61
+ const extIcon = report.extensionConnected ? chalk.green('[OK]') : chalk.yellow('[MISSING]');
62
+ lines.push(`${extIcon} Extension: ${report.extensionConnected ? 'connected' : 'not connected'}`);
63
+ // Connectivity
554
64
  if (report.connectivity) {
555
- const connStatus = report.connectivity.ok ? 'OK' : 'WARN';
556
- const connDetail = report.connectivity.ok
557
- ? `Browser connectivity: connected in ${(report.connectivity.durationMs / 1000).toFixed(1)}s`
558
- : `Browser connectivity: failed (${report.connectivity.error ?? 'unknown'})`;
559
- lines.push(statusLine(connStatus, connDetail));
65
+ const connIcon = report.connectivity.ok ? chalk.green('[OK]') : chalk.red('[FAIL]');
66
+ const detail = report.connectivity.ok
67
+ ? `connected in ${(report.connectivity.durationMs / 1000).toFixed(1)}s`
68
+ : `failed (${report.connectivity.error ?? 'unknown'})`;
69
+ lines.push(`${connIcon} Connectivity: ${detail}`);
560
70
  }
561
71
  else {
562
- lines.push(statusLine('WARN', 'Browser connectivity: not tested (use --live)'));
72
+ lines.push(`${chalk.dim('[SKIP]')} Connectivity: not tested (use --live)`);
563
73
  }
564
- lines.push(statusLine(hasMismatch ? 'MISMATCH' : report.recommendedToken ? 'OK' : 'WARN', `Recommended token fingerprint: ${report.recommendedFingerprint ?? 'unavailable'}`));
565
74
  if (report.issues.length) {
566
75
  lines.push('', chalk.yellow('Issues:'));
567
- for (const issue of report.issues)
76
+ for (const issue of report.issues) {
568
77
  lines.push(chalk.dim(` • ${issue}`));
78
+ }
569
79
  }
570
- if (report.warnings.length) {
571
- lines.push('', chalk.yellow('Warnings:'));
572
- for (const warning of report.warnings)
573
- lines.push(chalk.dim(` • ${warning}`));
80
+ else if (report.daemonRunning && report.extensionConnected) {
81
+ lines.push('', chalk.green('Everything looks good!'));
574
82
  }
575
83
  return lines.join('\n');
576
84
  }
577
- async function confirmPrompt(question) {
578
- const rl = createInterface({ input, output });
579
- try {
580
- const answer = (await rl.question(`${question} [y/N] `)).trim().toLowerCase();
581
- return answer === 'y' || answer === 'yes';
582
- }
583
- finally {
584
- rl.close();
585
- }
586
- }
587
- export function writeFileWithMkdir(filePath, content) {
588
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
589
- fs.writeFileSync(filePath, content, 'utf-8');
590
- }
591
- export async function applyBrowserDoctorFix(report, opts = {}) {
592
- const token = opts.token ?? report.recommendedToken;
593
- if (!token)
594
- throw new Error('No Playwright MCP token is available to write. Provide --token first.');
595
- const fp = getTokenFingerprint(token);
596
- const plannedWrites = [];
597
- const shellPath = opts.shellRc ?? report.shellFiles[0]?.path ?? getDefaultShellRcPath();
598
- const shellStatus = report.shellFiles.find(s => s.path === shellPath);
599
- if (shellStatus?.fingerprint !== fp)
600
- plannedWrites.push(shellPath);
601
- for (const config of report.configs) {
602
- if (!config.writable)
603
- continue;
604
- if (config.fingerprint === fp)
605
- continue; // already correct
606
- plannedWrites.push(config.path);
607
- }
608
- if (plannedWrites.length === 0) {
609
- console.log(chalk.green('All config files are already up to date.'));
610
- return [];
611
- }
612
- if (!opts.yes) {
613
- const ok = await confirmPrompt(`Update ${plannedWrites.length} file(s) with Playwright MCP token fingerprint ${fp}?`);
614
- if (!ok)
615
- return [];
616
- }
617
- const written = [];
618
- if (plannedWrites.includes(shellPath)) {
619
- const shellBefore = fileExists(shellPath) ? fs.readFileSync(shellPath, 'utf-8') : '';
620
- writeFileWithMkdir(shellPath, upsertShellToken(shellBefore, token, shellPath));
621
- written.push(shellPath);
622
- }
623
- for (const config of report.configs) {
624
- if (!plannedWrites.includes(config.path))
625
- continue;
626
- if (config.parseError)
627
- continue;
628
- const before = fileExists(config.path) ? fs.readFileSync(config.path, 'utf-8') : '';
629
- const next = config.format === 'toml'
630
- ? upsertTomlConfigToken(before, token)
631
- : upsertJsonConfigToken(before, token, config.path);
632
- writeFileWithMkdir(config.path, next);
633
- written.push(config.path);
634
- }
635
- process.env[PLAYWRIGHT_TOKEN_ENV] = token;
636
- return written;
637
- }