@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.
- package/CDP.md +1 -1
- package/CDP.zh-CN.md +1 -1
- package/CLI-ELECTRON.md +2 -2
- package/CLI-EXPLORER.md +4 -4
- package/README.md +35 -58
- package/README.zh-CN.md +36 -60
- package/SKILL.md +10 -8
- package/TESTING.md +7 -7
- package/dist/browser/daemon-client.d.ts +37 -0
- package/dist/browser/daemon-client.js +82 -0
- package/dist/browser/discover.d.ts +11 -34
- package/dist/browser/discover.js +15 -205
- package/dist/browser/errors.d.ts +6 -20
- package/dist/browser/errors.js +24 -63
- package/dist/browser/index.d.ts +2 -12
- package/dist/browser/index.js +2 -12
- package/dist/browser/mcp.d.ts +9 -21
- package/dist/browser/mcp.js +70 -285
- package/dist/browser/page.d.ts +36 -7
- package/dist/browser/page.js +212 -81
- package/dist/browser.test.js +10 -231
- package/dist/cli-manifest.json +561 -14
- package/dist/clis/apple-podcasts/episodes.d.ts +1 -0
- package/dist/clis/apple-podcasts/episodes.js +28 -0
- package/dist/clis/apple-podcasts/search.d.ts +1 -0
- package/dist/clis/apple-podcasts/search.js +29 -0
- package/dist/clis/apple-podcasts/top.d.ts +1 -0
- package/dist/clis/apple-podcasts/top.js +34 -0
- package/dist/clis/apple-podcasts/utils.d.ts +11 -0
- package/dist/clis/apple-podcasts/utils.js +30 -0
- package/dist/clis/apple-podcasts/utils.test.d.ts +1 -0
- package/dist/clis/apple-podcasts/utils.test.js +57 -0
- package/dist/clis/chatwise/history.js +18 -1
- package/dist/clis/discord-app/channels.js +33 -21
- package/dist/clis/neteasemusic/like.d.ts +1 -0
- package/dist/clis/neteasemusic/like.js +25 -0
- package/dist/clis/neteasemusic/lyrics.d.ts +1 -0
- package/dist/clis/neteasemusic/lyrics.js +47 -0
- package/dist/clis/neteasemusic/next.d.ts +1 -0
- package/dist/clis/neteasemusic/next.js +26 -0
- package/dist/clis/neteasemusic/play.d.ts +1 -0
- package/dist/clis/neteasemusic/play.js +26 -0
- package/dist/clis/neteasemusic/playing.d.ts +1 -0
- package/dist/clis/neteasemusic/playing.js +59 -0
- package/dist/clis/neteasemusic/playlist.d.ts +1 -0
- package/dist/clis/neteasemusic/playlist.js +46 -0
- package/dist/clis/neteasemusic/prev.d.ts +1 -0
- package/dist/clis/neteasemusic/prev.js +25 -0
- package/dist/clis/neteasemusic/search.d.ts +1 -0
- package/dist/clis/neteasemusic/search.js +52 -0
- package/dist/clis/neteasemusic/status.d.ts +1 -0
- package/dist/clis/neteasemusic/status.js +16 -0
- package/dist/clis/neteasemusic/volume.d.ts +1 -0
- package/dist/clis/neteasemusic/volume.js +54 -0
- package/dist/clis/twitter/accept.d.ts +1 -0
- package/dist/clis/twitter/accept.js +202 -0
- package/dist/clis/twitter/followers.js +30 -22
- package/dist/clis/twitter/following.js +19 -14
- package/dist/clis/twitter/notifications.js +29 -22
- package/dist/clis/twitter/reply-dm.d.ts +1 -0
- package/dist/clis/twitter/reply-dm.js +181 -0
- package/dist/clis/twitter/search.js +50 -12
- package/dist/clis/weread/book.d.ts +1 -0
- package/dist/clis/weread/book.js +26 -0
- package/dist/clis/weread/highlights.d.ts +1 -0
- package/dist/clis/weread/highlights.js +23 -0
- package/dist/clis/weread/notebooks.d.ts +1 -0
- package/dist/clis/weread/notebooks.js +21 -0
- package/dist/clis/weread/notes.d.ts +1 -0
- package/dist/clis/weread/notes.js +29 -0
- package/dist/clis/weread/ranking.d.ts +1 -0
- package/dist/clis/weread/ranking.js +28 -0
- package/dist/clis/weread/search.d.ts +1 -0
- package/dist/clis/weread/search.js +25 -0
- package/dist/clis/weread/shelf.d.ts +1 -0
- package/dist/clis/weread/shelf.js +24 -0
- package/dist/clis/weread/utils.d.ts +20 -0
- package/dist/clis/weread/utils.js +72 -0
- package/dist/clis/weread/utils.test.d.ts +1 -0
- package/dist/clis/weread/utils.test.js +85 -0
- package/dist/daemon.d.ts +13 -0
- package/dist/daemon.js +187 -0
- package/dist/doctor.d.ts +10 -65
- package/dist/doctor.js +49 -602
- package/dist/doctor.test.js +30 -170
- package/dist/main.js +12 -41
- package/dist/pipeline/executor.test.js +1 -0
- package/dist/pipeline/steps/browser.js +2 -2
- package/dist/pipeline/steps/intercept.js +1 -2
- package/dist/runtime.d.ts +1 -4
- package/dist/runtime.js +1 -4
- package/dist/setup.d.ts +6 -0
- package/dist/setup.js +46 -160
- package/dist/types.d.ts +6 -0
- package/extension/dist/background.js +484 -0
- package/extension/icons/icon-128.png +0 -0
- package/extension/icons/icon-16.png +0 -0
- package/extension/icons/icon-32.png +0 -0
- package/extension/icons/icon-48.png +0 -0
- package/extension/manifest.json +31 -0
- package/extension/package.json +16 -0
- package/extension/src/background.ts +370 -0
- package/extension/src/cdp.ts +125 -0
- package/extension/src/protocol.ts +57 -0
- package/extension/store-assets/screenshot-1280x800.png +0 -0
- package/extension/tsconfig.json +15 -0
- package/extension/vite.config.ts +18 -0
- package/package.json +5 -5
- package/src/browser/daemon-client.ts +113 -0
- package/src/browser/discover.ts +18 -232
- package/src/browser/errors.ts +30 -100
- package/src/browser/index.ts +2 -13
- package/src/browser/mcp.ts +81 -282
- package/src/browser/page.ts +223 -83
- package/src/browser.test.ts +9 -239
- package/src/clis/apple-podcasts/episodes.ts +28 -0
- package/src/clis/apple-podcasts/search.ts +29 -0
- package/src/clis/apple-podcasts/top.ts +34 -0
- package/src/clis/apple-podcasts/utils.test.ts +72 -0
- package/src/clis/apple-podcasts/utils.ts +37 -0
- package/src/clis/chatgpt/README.md +1 -1
- package/src/clis/chatgpt/README.zh-CN.md +1 -1
- package/src/clis/chatwise/history.ts +15 -1
- package/src/clis/discord-app/channels.ts +33 -21
- package/src/clis/neteasemusic/README.md +31 -0
- package/src/clis/neteasemusic/README.zh-CN.md +31 -0
- package/src/clis/neteasemusic/like.ts +28 -0
- package/src/clis/neteasemusic/lyrics.ts +53 -0
- package/src/clis/neteasemusic/next.ts +30 -0
- package/src/clis/neteasemusic/play.ts +30 -0
- package/src/clis/neteasemusic/playing.ts +62 -0
- package/src/clis/neteasemusic/playlist.ts +51 -0
- package/src/clis/neteasemusic/prev.ts +29 -0
- package/src/clis/neteasemusic/search.ts +58 -0
- package/src/clis/neteasemusic/status.ts +18 -0
- package/src/clis/neteasemusic/volume.ts +61 -0
- package/src/clis/twitter/accept.ts +213 -0
- package/src/clis/twitter/followers.ts +36 -29
- package/src/clis/twitter/following.ts +25 -20
- package/src/clis/twitter/notifications.ts +34 -27
- package/src/clis/twitter/reply-dm.ts +193 -0
- package/src/clis/twitter/search.ts +53 -13
- package/src/clis/weread/book.ts +28 -0
- package/src/clis/weread/highlights.ts +25 -0
- package/src/clis/weread/notebooks.ts +23 -0
- package/src/clis/weread/notes.ts +31 -0
- package/src/clis/weread/ranking.ts +29 -0
- package/src/clis/weread/search.ts +26 -0
- package/src/clis/weread/shelf.ts +26 -0
- package/src/clis/weread/utils.test.ts +104 -0
- package/src/clis/weread/utils.ts +74 -0
- package/src/daemon.ts +217 -0
- package/src/doctor.test.ts +32 -193
- package/src/doctor.ts +58 -669
- package/src/main.ts +11 -34
- package/src/pipeline/executor.test.ts +1 -0
- package/src/pipeline/steps/browser.ts +2 -2
- package/src/pipeline/steps/intercept.ts +1 -2
- package/src/runtime.ts +2 -6
- package/src/setup.ts +47 -183
- package/src/types.ts +1 -0
- package/tests/e2e/public-commands.test.ts +68 -1
- package/dist/clis/grok/debug.d.ts +0 -1
- package/dist/clis/grok/debug.js +0 -45
- package/src/clis/grok/debug.ts +0 -49
package/src/main.ts
CHANGED
|
@@ -11,7 +11,7 @@ import chalk from 'chalk';
|
|
|
11
11
|
import { discoverClis, executeCommand } from './engine.js';
|
|
12
12
|
import { Strategy, type CliCommand, fullName, getRegistry, strategyLabel } from './registry.js';
|
|
13
13
|
import { render as renderOutput } from './output.js';
|
|
14
|
-
import {
|
|
14
|
+
import { BrowserBridge } from './browser/index.js';
|
|
15
15
|
import { browserSession, DEFAULT_BROWSER_COMMAND_TIMEOUT, runWithTimeout } from './runtime.js';
|
|
16
16
|
import { PKG_VERSION } from './version.js';
|
|
17
17
|
import { getCompletions, printCompletionScript } from './completion.js';
|
|
@@ -99,18 +99,18 @@ program.command('verify').description('Validate + smoke test').argument('[target
|
|
|
99
99
|
});
|
|
100
100
|
|
|
101
101
|
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,评论")')
|
|
102
|
-
.action(async (url, opts) => { const { exploreUrl, renderExploreSummary } = await import('./explore.js'); const clickLabels = opts.click ? opts.click.split(',').map((s: string) => s.trim()) : undefined; console.log(renderExploreSummary(await exploreUrl(url, { BrowserFactory:
|
|
102
|
+
.action(async (url, opts) => { const { exploreUrl, renderExploreSummary } = await import('./explore.js'); const clickLabels = opts.click ? opts.click.split(',').map((s: string) => 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 }))); });
|
|
103
103
|
|
|
104
104
|
program.command('synthesize').description('Synthesize CLIs from explore').argument('<target>').option('--top <n>', '', '3')
|
|
105
105
|
.action(async (target, opts) => { const { synthesizeFromExplore, renderSynthesizeSummary } = await import('./synthesize.js'); console.log(renderSynthesizeSummary(synthesizeFromExplore(target, { top: parseInt(opts.top) }))); });
|
|
106
106
|
|
|
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:
|
|
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
|
|
|
110
110
|
program.command('cascade').description('Strategy cascade: find simplest working strategy').argument('<url>').option('--site <name>')
|
|
111
111
|
.action(async (url, opts) => {
|
|
112
112
|
const { cascadeProbe, renderCascadeResult } = await import('./cascade.js');
|
|
113
|
-
const result = await browserSession(
|
|
113
|
+
const result = await browserSession(BrowserBridge, async (page) => {
|
|
114
114
|
// Navigate to the site first for cookie context
|
|
115
115
|
try { const siteUrl = new URL(url); await page.goto(`${siteUrl.protocol}//${siteUrl.host}`); await page.wait(2); } catch {}
|
|
116
116
|
return cascadeProbe(page, url);
|
|
@@ -119,36 +119,19 @@ program.command('cascade').description('Strategy cascade: find simplest working
|
|
|
119
119
|
});
|
|
120
120
|
|
|
121
121
|
program.command('doctor')
|
|
122
|
-
.description('Diagnose
|
|
123
|
-
.option('--fix', 'Apply suggested fixes to shell rc and detected MCP configs', false)
|
|
124
|
-
.option('-y, --yes', 'Skip confirmation prompts when applying fixes', false)
|
|
125
|
-
.option('--token <token>', 'Override token to write instead of auto-detecting')
|
|
122
|
+
.description('Diagnose opencli browser bridge connectivity')
|
|
126
123
|
.option('--live', 'Test browser connectivity (requires Chrome running)', false)
|
|
127
|
-
.option('--shell-rc <path>', 'Shell startup file to update')
|
|
128
|
-
.option('--mcp-config <paths>', 'Comma-separated MCP config paths to scan/update')
|
|
129
124
|
.action(async (opts) => {
|
|
130
|
-
const { runBrowserDoctor, renderBrowserDoctorReport
|
|
131
|
-
const
|
|
132
|
-
const report = await runBrowserDoctor({ token: opts.token, live: opts.live, shellRc: opts.shellRc, configPaths, cliVersion: PKG_VERSION });
|
|
125
|
+
const { runBrowserDoctor, renderBrowserDoctorReport } = await import('./doctor.js');
|
|
126
|
+
const report = await runBrowserDoctor({ live: opts.live, cliVersion: PKG_VERSION });
|
|
133
127
|
console.log(renderBrowserDoctorReport(report));
|
|
134
|
-
if (opts.fix) {
|
|
135
|
-
const written = await applyBrowserDoctorFix(report, { fix: true, yes: opts.yes, token: opts.token, shellRc: opts.shellRc, configPaths });
|
|
136
|
-
console.log();
|
|
137
|
-
if (written.length > 0) {
|
|
138
|
-
console.log(chalk.green('Updated files:'));
|
|
139
|
-
for (const filePath of written) console.log(`- ${filePath}`);
|
|
140
|
-
} else {
|
|
141
|
-
console.log(chalk.yellow('No files were changed.'));
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
128
|
});
|
|
145
129
|
|
|
146
130
|
program.command('setup')
|
|
147
|
-
.description('Interactive setup:
|
|
148
|
-
.
|
|
149
|
-
.action(async (opts) => {
|
|
131
|
+
.description('Interactive setup: verify browser bridge connectivity')
|
|
132
|
+
.action(async () => {
|
|
150
133
|
const { runSetup } = await import('./setup.js');
|
|
151
|
-
await runSetup({ cliVersion: PKG_VERSION
|
|
134
|
+
await runSetup({ cliVersion: PKG_VERSION });
|
|
152
135
|
});
|
|
153
136
|
|
|
154
137
|
program.command('completion')
|
|
@@ -209,13 +192,7 @@ for (const [, cmd] of registry) {
|
|
|
209
192
|
if (actionOpts.verbose) process.env.OPENCLI_VERBOSE = '1';
|
|
210
193
|
let result: any;
|
|
211
194
|
if (cmd.browser) {
|
|
212
|
-
result = await browserSession(
|
|
213
|
-
// Cookie/header strategies require same-origin context for credentialed fetch.
|
|
214
|
-
// In CDP mode the active tab may be on an unrelated domain, causing CORS failures.
|
|
215
|
-
// Navigate to the command's domain first (mirrors cascade command behavior).
|
|
216
|
-
if ((cmd.strategy === Strategy.COOKIE || cmd.strategy === Strategy.HEADER) && cmd.domain) {
|
|
217
|
-
try { await page.goto(`https://${cmd.domain}`); await page.wait(2); } catch {}
|
|
218
|
-
}
|
|
195
|
+
result = await browserSession(BrowserBridge, async (page) => {
|
|
219
196
|
return runWithTimeout(executeCommand(cmd, page, kwargs, actionOpts.verbose), { timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT, label: fullName(cmd) });
|
|
220
197
|
});
|
|
221
198
|
} else { result = await executeCommand(cmd, null, kwargs, actionOpts.verbose); }
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { IPage } from '../../types.js';
|
|
7
|
-
import { render
|
|
7
|
+
import { render } from '../template.js';
|
|
8
8
|
|
|
9
9
|
export async function stepNavigate(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any> {
|
|
10
10
|
const url = render(params, { args, data });
|
|
@@ -52,7 +52,7 @@ export async function stepSnapshot(page: IPage | null, params: any, _data: any,
|
|
|
52
52
|
|
|
53
53
|
export async function stepEvaluate(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any> {
|
|
54
54
|
const js = String(render(params, { args, data }));
|
|
55
|
-
let result = await page!.evaluate(
|
|
55
|
+
let result = await page!.evaluate(js);
|
|
56
56
|
// MCP may return JSON as a string — auto-parse it
|
|
57
57
|
if (typeof result === 'string') {
|
|
58
58
|
const trimmed = result.trim();
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { IPage } from '../../types.js';
|
|
6
|
-
import { render } from '../template.js';
|
|
6
|
+
import { render, normalizeEvaluateSource } from '../template.js';
|
|
7
7
|
import { generateInterceptorJs, generateReadInterceptedJs } from '../../interceptor.js';
|
|
8
8
|
|
|
9
9
|
export async function stepIntercept(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any> {
|
|
@@ -24,7 +24,6 @@ export async function stepIntercept(page: IPage | null, params: any, data: any,
|
|
|
24
24
|
await page!.goto(String(url));
|
|
25
25
|
} else if (trigger.startsWith('evaluate:')) {
|
|
26
26
|
const js = trigger.slice('evaluate:'.length);
|
|
27
|
-
const { normalizeEvaluateSource } = await import('../template.js');
|
|
28
27
|
await page!.evaluate(normalizeEvaluateSource(render(js, { args, data }) as string));
|
|
29
28
|
} else if (trigger.startsWith('click:')) {
|
|
30
29
|
const ref = render(trigger.slice('click:'.length), { args, data });
|
package/src/runtime.ts
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Runtime utilities: timeouts and browser session management.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
1
|
import type { IPage } from './types.js';
|
|
6
2
|
|
|
7
3
|
export const DEFAULT_BROWSER_CONNECT_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_CONNECT_TIMEOUT ?? '30', 10);
|
|
8
|
-
export const DEFAULT_BROWSER_COMMAND_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_COMMAND_TIMEOUT ?? '
|
|
4
|
+
export const DEFAULT_BROWSER_COMMAND_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_COMMAND_TIMEOUT ?? '60', 10);
|
|
9
5
|
export const DEFAULT_BROWSER_EXPLORE_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_EXPLORE_TIMEOUT ?? '120', 10);
|
|
10
6
|
export const DEFAULT_BROWSER_SMOKE_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_SMOKE_TIMEOUT ?? '60', 10);
|
|
11
7
|
|
|
@@ -32,7 +28,7 @@ export function withTimeoutMs<T>(promise: Promise<T>, timeoutMs: number, message
|
|
|
32
28
|
});
|
|
33
29
|
}
|
|
34
30
|
|
|
35
|
-
/** Interface for browser factory (
|
|
31
|
+
/** Interface for browser factory (BrowserBridge or test mocks) */
|
|
36
32
|
export interface IBrowserFactory {
|
|
37
33
|
connect(opts?: { timeout?: number }): Promise<IPage>;
|
|
38
34
|
close(): Promise<void>;
|
package/src/setup.ts
CHANGED
|
@@ -1,205 +1,69 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* setup.ts — Interactive
|
|
2
|
+
* setup.ts — Interactive browser setup for opencli
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Simplified for daemon-based architecture. No more token management.
|
|
5
|
+
* Just verifies daemon + extension connectivity.
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
|
|
8
8
|
import chalk from 'chalk';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
type DoctorReport,
|
|
13
|
-
PLAYWRIGHT_TOKEN_ENV,
|
|
14
|
-
checkExtensionInstalled,
|
|
15
|
-
checkTokenConnectivity,
|
|
16
|
-
discoverExtensionToken,
|
|
17
|
-
fileExists,
|
|
18
|
-
getDefaultShellRcPath,
|
|
19
|
-
runBrowserDoctor,
|
|
20
|
-
shortenPath,
|
|
21
|
-
toolName,
|
|
22
|
-
upsertJsonConfigToken,
|
|
23
|
-
upsertShellToken,
|
|
24
|
-
upsertTomlConfigToken,
|
|
25
|
-
writeFileWithMkdir,
|
|
26
|
-
} from './doctor.js';
|
|
27
|
-
import { getTokenFingerprint } from './browser/index.js';
|
|
28
|
-
import { type CheckboxItem, checkboxPrompt } from './tui.js';
|
|
9
|
+
import { checkDaemonStatus } from './browser/discover.js';
|
|
10
|
+
import { checkConnectivity } from './doctor.js';
|
|
11
|
+
import { BrowserBridge } from './browser/index.js';
|
|
29
12
|
|
|
30
13
|
export async function runSetup(opts: { cliVersion?: string; token?: string } = {}) {
|
|
31
14
|
console.log();
|
|
32
|
-
console.log(chalk.bold(' opencli setup') + chalk.dim(' —
|
|
15
|
+
console.log(chalk.bold(' opencli setup') + chalk.dim(' — browser bridge configuration'));
|
|
33
16
|
console.log();
|
|
34
17
|
|
|
35
|
-
// Step 1:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (!token) {
|
|
39
|
-
const extensionToken = discoverExtensionToken();
|
|
40
|
-
const envToken = process.env[PLAYWRIGHT_TOKEN_ENV] ?? null;
|
|
18
|
+
// Step 1: Check daemon
|
|
19
|
+
console.log(chalk.dim(' Checking daemon status...'));
|
|
20
|
+
const status = await checkDaemonStatus();
|
|
41
21
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
console.log(` ${chalk.green('✓')} Token auto-discovered from Chrome extension`);
|
|
45
|
-
console.log(` Fingerprint: ${chalk.bold(getTokenFingerprint(token) ?? 'unknown')}`);
|
|
46
|
-
} else if (extensionToken) {
|
|
47
|
-
token = extensionToken;
|
|
48
|
-
console.log(` ${chalk.green('✓')} Token discovered from Chrome extension ` +
|
|
49
|
-
chalk.dim(`(${getTokenFingerprint(token)})`));
|
|
50
|
-
if (envToken && envToken !== extensionToken) {
|
|
51
|
-
console.log(` ${chalk.yellow('!')} Environment has different token ` +
|
|
52
|
-
chalk.dim(`(${getTokenFingerprint(envToken)})`));
|
|
53
|
-
}
|
|
54
|
-
} else if (envToken) {
|
|
55
|
-
token = envToken;
|
|
56
|
-
console.log(` ${chalk.green('✓')} Token from environment variable ` +
|
|
57
|
-
chalk.dim(`(${getTokenFingerprint(token)})`));
|
|
58
|
-
}
|
|
22
|
+
if (status.running) {
|
|
23
|
+
console.log(` ${chalk.green('✓')} Daemon is running on port 19825`);
|
|
59
24
|
} else {
|
|
60
|
-
console.log(` ${chalk.
|
|
61
|
-
|
|
25
|
+
console.log(` ${chalk.yellow('!')} Daemon is not running`);
|
|
26
|
+
console.log(chalk.dim(' The daemon starts automatically when you run a browser command.'));
|
|
27
|
+
console.log(chalk.dim(' Starting daemon now...'));
|
|
28
|
+
|
|
29
|
+
// Try to spawn daemon
|
|
30
|
+
const mcp = new BrowserBridge();
|
|
31
|
+
try {
|
|
32
|
+
await mcp.connect({ timeout: 5 });
|
|
33
|
+
await mcp.close();
|
|
34
|
+
console.log(` ${chalk.green('✓')} Daemon started successfully`);
|
|
35
|
+
} catch {
|
|
36
|
+
console.log(` ${chalk.yellow('!')} Could not start daemon automatically`);
|
|
37
|
+
}
|
|
62
38
|
}
|
|
63
39
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
console.log(chalk.dim(' Cause: Playwright MCP Bridge extension is not installed'));
|
|
71
|
-
console.log(chalk.dim(' Fix: Install from https://chromewebstore.google.com/detail/'));
|
|
72
|
-
console.log(chalk.dim(' playwright-mcp-bridge/mmlmfjhmonkocbjadbfplnigmagldckm'));
|
|
73
|
-
} else {
|
|
74
|
-
console.log(chalk.dim(` Cause: Extension is installed (${extInstall.browsers.join(', ')}) but token not found in LevelDB`));
|
|
75
|
-
console.log(chalk.dim(' Fix: 1) Open the extension popup and verify the token is generated'));
|
|
76
|
-
console.log(chalk.dim(' 2) Close Chrome completely, then re-run setup'));
|
|
77
|
-
}
|
|
40
|
+
// Step 2: Check extension
|
|
41
|
+
const statusAfter = await checkDaemonStatus();
|
|
42
|
+
if (statusAfter.extensionConnected) {
|
|
43
|
+
console.log(` ${chalk.green('✓')} Chrome extension connected`);
|
|
44
|
+
} else {
|
|
45
|
+
console.log(` ${chalk.red('✗')} Chrome extension not connected`);
|
|
78
46
|
console.log();
|
|
79
|
-
console.log(
|
|
47
|
+
console.log(chalk.dim(' To install the opencli Browser Bridge extension:'));
|
|
48
|
+
console.log(chalk.dim(' 1. Download from GitHub Releases'));
|
|
49
|
+
console.log(chalk.dim(' 2. Open chrome://extensions/ → Enable Developer Mode'));
|
|
50
|
+
console.log(chalk.dim(' 3. Click "Load unpacked" → select the extension folder'));
|
|
51
|
+
console.log(chalk.dim(' 4. Make sure Chrome is running'));
|
|
80
52
|
console.log();
|
|
81
|
-
const rl = createInterface({ input, output });
|
|
82
|
-
const answer = await rl.question(' Token (press Enter to abort): ');
|
|
83
|
-
rl.close();
|
|
84
|
-
token = answer.trim();
|
|
85
|
-
if (!token) {
|
|
86
|
-
console.log(chalk.red('\n No token provided. Aborting.\n'));
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const fingerprint = getTokenFingerprint(token) ?? 'unknown';
|
|
92
|
-
console.log();
|
|
93
|
-
|
|
94
|
-
// Step 2: Scan all config locations
|
|
95
|
-
const report = await runBrowserDoctor({ token, cliVersion: opts.cliVersion });
|
|
96
|
-
|
|
97
|
-
// Step 3: Build checkbox items
|
|
98
|
-
const items: CheckboxItem[] = [];
|
|
99
|
-
|
|
100
|
-
// Shell file
|
|
101
|
-
const shellPath = report.shellFiles[0]?.path ?? getDefaultShellRcPath();
|
|
102
|
-
const shellStatus = report.shellFiles[0];
|
|
103
|
-
const shellFp = shellStatus?.fingerprint;
|
|
104
|
-
const shellOk = shellFp === fingerprint;
|
|
105
|
-
const shellTool = toolName(shellPath) || 'Shell';
|
|
106
|
-
items.push({
|
|
107
|
-
label: padRight(shortenPath(shellPath), 50) + chalk.dim(` [${shellTool}]`),
|
|
108
|
-
value: `shell:${shellPath}`,
|
|
109
|
-
checked: !shellOk,
|
|
110
|
-
status: shellOk ? `configured (${shellFp})` : shellFp ? `mismatch (${shellFp})` : 'missing',
|
|
111
|
-
statusColor: shellOk ? 'green' : shellFp ? 'yellow' : 'red',
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
// Config files
|
|
115
|
-
for (const config of report.configs) {
|
|
116
|
-
const fp = config.fingerprint;
|
|
117
|
-
const ok = fp === fingerprint;
|
|
118
|
-
const tool = toolName(config.path);
|
|
119
|
-
items.push({
|
|
120
|
-
label: padRight(shortenPath(config.path), 50) + chalk.dim(tool ? ` [${tool}]` : ''),
|
|
121
|
-
value: `config:${config.path}`,
|
|
122
|
-
checked: false, // let user explicitly select which tools to configure
|
|
123
|
-
status: ok ? `configured (${fp})` : !config.exists ? 'will create' : fp ? `mismatch (${fp})` : 'missing',
|
|
124
|
-
statusColor: ok ? 'green' : 'yellow',
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Step 4: Show interactive checkbox
|
|
129
|
-
console.clear();
|
|
130
|
-
const selected = await checkboxPrompt(items, {
|
|
131
|
-
title: ` ${chalk.bold('opencli setup')} — token ${chalk.cyan(fingerprint)}`,
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
if (selected.length === 0) {
|
|
135
|
-
console.log(chalk.dim(' No changes made.\n'));
|
|
136
53
|
return;
|
|
137
54
|
}
|
|
138
55
|
|
|
139
|
-
// Step
|
|
140
|
-
const written: string[] = [];
|
|
141
|
-
let wroteShell = false;
|
|
142
|
-
|
|
143
|
-
for (const sel of selected) {
|
|
144
|
-
if (sel.startsWith('shell:')) {
|
|
145
|
-
const p = sel.slice('shell:'.length);
|
|
146
|
-
const before = fileExists(p) ? fs.readFileSync(p, 'utf-8') : '';
|
|
147
|
-
writeFileWithMkdir(p, upsertShellToken(before, token, p));
|
|
148
|
-
written.push(p);
|
|
149
|
-
wroteShell = true;
|
|
150
|
-
} else if (sel.startsWith('config:')) {
|
|
151
|
-
const p = sel.slice('config:'.length);
|
|
152
|
-
const config = report.configs.find(c => c.path === p);
|
|
153
|
-
if (config && config.parseError) continue;
|
|
154
|
-
const before = fileExists(p) ? fs.readFileSync(p, 'utf-8') : '';
|
|
155
|
-
const format = config?.format ?? (p.endsWith('.toml') ? 'toml' : 'json');
|
|
156
|
-
const next = format === 'toml' ? upsertTomlConfigToken(before, token) : upsertJsonConfigToken(before, token, p);
|
|
157
|
-
writeFileWithMkdir(p, next);
|
|
158
|
-
written.push(p);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
process.env[PLAYWRIGHT_TOKEN_ENV] = token;
|
|
163
|
-
|
|
164
|
-
// Step 6: Summary
|
|
165
|
-
if (written.length > 0) {
|
|
166
|
-
console.log(chalk.green.bold(` ✓ Updated ${written.length} file(s):`));
|
|
167
|
-
for (const p of written) {
|
|
168
|
-
const tool = toolName(p);
|
|
169
|
-
console.log(` ${chalk.dim('•')} ${shortenPath(p)}${tool ? chalk.dim(` [${tool}]`) : ''}`);
|
|
170
|
-
}
|
|
171
|
-
if (wroteShell) {
|
|
172
|
-
console.log();
|
|
173
|
-
console.log(chalk.cyan(` 💡 Run ${chalk.bold(`source ${shortenPath(shellPath)}`)} to apply token to current shell.`));
|
|
174
|
-
}
|
|
175
|
-
} else {
|
|
176
|
-
console.log(chalk.yellow(' No files were changed.'));
|
|
177
|
-
}
|
|
56
|
+
// Step 3: Test connectivity
|
|
178
57
|
console.log();
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
console.log(` ${chalk.yellow('!')} Browser connectivity test failed: ${result.error ?? 'unknown'}`);
|
|
189
|
-
console.log(chalk.dim(' Token configuration is complete. To use opencli, make sure Chrome'));
|
|
190
|
-
console.log(chalk.dim(' is running with the Playwright MCP Bridge extension enabled.'));
|
|
191
|
-
console.log(chalk.dim(` Run ${chalk.bold('opencli doctor --live')} to re-test connectivity.`));
|
|
192
|
-
}
|
|
193
|
-
} catch {
|
|
194
|
-
console.log(` ${chalk.green('✓')} Token saved successfully.`);
|
|
195
|
-
console.log(` ${chalk.yellow('!')} Browser connectivity test skipped (Chrome may not be running).`);
|
|
196
|
-
console.log(chalk.dim(' Token configuration is complete. Start Chrome to begin using opencli.'));
|
|
197
|
-
console.log(chalk.dim(` Run ${chalk.bold('opencli doctor --live')} to re-test connectivity.`));
|
|
58
|
+
console.log(chalk.dim(' Testing browser connectivity...'));
|
|
59
|
+
const conn = await checkConnectivity({ timeout: 5 });
|
|
60
|
+
if (conn.ok) {
|
|
61
|
+
console.log(` ${chalk.green('✓')} Browser connected in ${(conn.durationMs / 1000).toFixed(1)}s`);
|
|
62
|
+
console.log();
|
|
63
|
+
console.log(chalk.green.bold(' ✓ Setup complete! You can now use opencli browser commands.'));
|
|
64
|
+
} else {
|
|
65
|
+
console.log(` ${chalk.yellow('!')} Connectivity test failed: ${conn.error ?? 'unknown'}`);
|
|
66
|
+
console.log(chalk.dim(` Run ${chalk.bold('opencli doctor --live')} to diagnose.`));
|
|
198
67
|
}
|
|
199
68
|
console.log();
|
|
200
69
|
}
|
|
201
|
-
|
|
202
|
-
function padRight(s: string, n: number): string {
|
|
203
|
-
const visible = s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
204
|
-
return visible.length >= n ? s : s + ' '.repeat(n - visible.length);
|
|
205
|
-
}
|
package/src/types.ts
CHANGED
|
@@ -23,4 +23,5 @@ export interface IPage {
|
|
|
23
23
|
autoScroll(options?: { times?: number; delayMs?: number }): Promise<void>;
|
|
24
24
|
installInterceptor(pattern: string): Promise<void>;
|
|
25
25
|
getInterceptedRequests(): Promise<any[]>;
|
|
26
|
+
screenshot(options?: { format?: 'png' | 'jpeg'; quality?: number; fullPage?: boolean; path?: string }): Promise<string>;
|
|
26
27
|
}
|
|
@@ -6,12 +6,49 @@
|
|
|
6
6
|
import { describe, it, expect } from 'vitest';
|
|
7
7
|
import { runCli, parseJsonOutput } from './helpers.js';
|
|
8
8
|
|
|
9
|
-
function
|
|
9
|
+
function isExpectedChineseSiteRestriction(code: number, stderr: string): boolean {
|
|
10
10
|
if (code === 0) return false;
|
|
11
11
|
return /Error \[FETCH_ERROR\]: HTTP (403|429|451|503)\b/.test(stderr);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
// Keep old name as alias for existing tests
|
|
15
|
+
const isExpectedXiaoyuzhouRestriction = isExpectedChineseSiteRestriction;
|
|
16
|
+
|
|
14
17
|
describe('public commands E2E', () => {
|
|
18
|
+
// ── apple-podcasts ──
|
|
19
|
+
it('apple-podcasts search returns structured podcast results', async () => {
|
|
20
|
+
const { stdout, code } = await runCli(['apple-podcasts', 'search', 'technology', '--limit', '3', '-f', 'json']);
|
|
21
|
+
expect(code).toBe(0);
|
|
22
|
+
const data = parseJsonOutput(stdout);
|
|
23
|
+
expect(Array.isArray(data)).toBe(true);
|
|
24
|
+
expect(data.length).toBeGreaterThanOrEqual(1);
|
|
25
|
+
expect(data[0]).toHaveProperty('id');
|
|
26
|
+
expect(data[0]).toHaveProperty('title');
|
|
27
|
+
expect(data[0]).toHaveProperty('author');
|
|
28
|
+
}, 30_000);
|
|
29
|
+
|
|
30
|
+
it('apple-podcasts episodes returns episode list from a known show', async () => {
|
|
31
|
+
const { stdout, code } = await runCli(['apple-podcasts', 'episodes', '275699983', '--limit', '3', '-f', 'json']);
|
|
32
|
+
expect(code).toBe(0);
|
|
33
|
+
const data = parseJsonOutput(stdout);
|
|
34
|
+
expect(Array.isArray(data)).toBe(true);
|
|
35
|
+
expect(data.length).toBeGreaterThanOrEqual(1);
|
|
36
|
+
expect(data[0]).toHaveProperty('title');
|
|
37
|
+
expect(data[0]).toHaveProperty('duration');
|
|
38
|
+
expect(data[0]).toHaveProperty('date');
|
|
39
|
+
}, 30_000);
|
|
40
|
+
|
|
41
|
+
it('apple-podcasts top returns ranked podcasts', async () => {
|
|
42
|
+
const { stdout, code } = await runCli(['apple-podcasts', 'top', '--limit', '3', '--country', 'us', '-f', 'json']);
|
|
43
|
+
expect(code).toBe(0);
|
|
44
|
+
const data = parseJsonOutput(stdout);
|
|
45
|
+
expect(Array.isArray(data)).toBe(true);
|
|
46
|
+
expect(data.length).toBe(3);
|
|
47
|
+
expect(data[0]).toHaveProperty('rank');
|
|
48
|
+
expect(data[0]).toHaveProperty('title');
|
|
49
|
+
expect(data[0]).toHaveProperty('id');
|
|
50
|
+
}, 30_000);
|
|
51
|
+
|
|
15
52
|
// ── hackernews ──
|
|
16
53
|
it('hackernews top returns structured data', async () => {
|
|
17
54
|
const { stdout, code } = await runCli(['hackernews', 'top', '--limit', '3', '-f', 'json']);
|
|
@@ -115,4 +152,34 @@ describe('public commands E2E', () => {
|
|
|
115
152
|
expect(code).not.toBe(0);
|
|
116
153
|
expect(stderr).toMatch(/limit must be a positive integer|Argument "limit" must be a valid number/);
|
|
117
154
|
}, 30_000);
|
|
155
|
+
|
|
156
|
+
// ── weread (Chinese site — may return empty on overseas CI runners) ──
|
|
157
|
+
it('weread search returns books', async () => {
|
|
158
|
+
const { stdout, stderr, code } = await runCli(['weread', 'search', 'python', '--limit', '3', '-f', 'json']);
|
|
159
|
+
if (isExpectedChineseSiteRestriction(code, stderr)) {
|
|
160
|
+
console.warn(`weread search skipped: ${stderr.trim()}`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
expect(code).toBe(0);
|
|
164
|
+
const data = parseJsonOutput(stdout);
|
|
165
|
+
expect(Array.isArray(data)).toBe(true);
|
|
166
|
+
expect(data.length).toBeGreaterThanOrEqual(1);
|
|
167
|
+
expect(data[0]).toHaveProperty('title');
|
|
168
|
+
expect(data[0]).toHaveProperty('bookId');
|
|
169
|
+
}, 30_000);
|
|
170
|
+
|
|
171
|
+
it('weread ranking returns books', async () => {
|
|
172
|
+
const { stdout, stderr, code } = await runCli(['weread', 'ranking', 'all', '--limit', '3', '-f', 'json']);
|
|
173
|
+
if (isExpectedChineseSiteRestriction(code, stderr)) {
|
|
174
|
+
console.warn(`weread ranking skipped: ${stderr.trim()}`);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
expect(code).toBe(0);
|
|
178
|
+
const data = parseJsonOutput(stdout);
|
|
179
|
+
expect(Array.isArray(data)).toBe(true);
|
|
180
|
+
expect(data.length).toBeGreaterThanOrEqual(1);
|
|
181
|
+
expect(data[0]).toHaveProperty('title');
|
|
182
|
+
expect(data[0]).toHaveProperty('readingCount');
|
|
183
|
+
expect(data[0]).toHaveProperty('bookId');
|
|
184
|
+
}, 30_000);
|
|
118
185
|
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const debugCommand: import("../../registry.js").CliCommand;
|
package/dist/clis/grok/debug.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { cli, Strategy } from '../../registry.js';
|
|
2
|
-
export const debugCommand = cli({
|
|
3
|
-
site: 'grok',
|
|
4
|
-
name: 'debug',
|
|
5
|
-
description: 'Debug grok page structure',
|
|
6
|
-
domain: 'grok.com',
|
|
7
|
-
strategy: Strategy.COOKIE,
|
|
8
|
-
browser: true,
|
|
9
|
-
columns: ['data'],
|
|
10
|
-
func: async (page, _kwargs) => {
|
|
11
|
-
await page.goto('https://grok.com');
|
|
12
|
-
await page.wait(3);
|
|
13
|
-
// Get all button-like elements near textarea
|
|
14
|
-
const debug = await page.evaluate(`(() => {
|
|
15
|
-
const ta = document.querySelector('textarea');
|
|
16
|
-
if (!ta) return { error: 'no textarea' };
|
|
17
|
-
|
|
18
|
-
// Get parent containers
|
|
19
|
-
let parent = ta.parentElement;
|
|
20
|
-
const parents = [];
|
|
21
|
-
for (let i = 0; i < 5 && parent; i++) {
|
|
22
|
-
parents.push({
|
|
23
|
-
tag: parent.tagName,
|
|
24
|
-
class: parent.className?.substring(0, 80),
|
|
25
|
-
childCount: parent.children.length,
|
|
26
|
-
});
|
|
27
|
-
parent = parent.parentElement;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Find buttons in the form/container near textarea
|
|
31
|
-
const form = ta.closest('form') || ta.closest('[class*="composer"]') || ta.closest('[class*="input"]') || ta.parentElement?.parentElement;
|
|
32
|
-
const buttons = form ? [...form.querySelectorAll('button')].map(b => ({
|
|
33
|
-
testid: b.getAttribute('data-testid'),
|
|
34
|
-
type: b.type,
|
|
35
|
-
disabled: b.disabled,
|
|
36
|
-
text: (b.textContent || '').substring(0, 30),
|
|
37
|
-
html: b.outerHTML.substring(0, 200),
|
|
38
|
-
rect: b.getBoundingClientRect().toJSON(),
|
|
39
|
-
})) : [];
|
|
40
|
-
|
|
41
|
-
return { parents, buttons, formTag: form?.tagName, formClass: form?.className?.substring(0, 80) };
|
|
42
|
-
})()`);
|
|
43
|
-
return [{ data: JSON.stringify(debug, null, 2) }];
|
|
44
|
-
},
|
|
45
|
-
});
|
package/src/clis/grok/debug.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { cli, Strategy } from '../../registry.js';
|
|
2
|
-
import type { IPage } from '../../types.js';
|
|
3
|
-
|
|
4
|
-
export const debugCommand = cli({
|
|
5
|
-
site: 'grok',
|
|
6
|
-
name: 'debug',
|
|
7
|
-
description: 'Debug grok page structure',
|
|
8
|
-
domain: 'grok.com',
|
|
9
|
-
strategy: Strategy.COOKIE,
|
|
10
|
-
browser: true,
|
|
11
|
-
columns: ['data'],
|
|
12
|
-
func: async (page: IPage, _kwargs: Record<string, any>) => {
|
|
13
|
-
await page.goto('https://grok.com');
|
|
14
|
-
await page.wait(3);
|
|
15
|
-
|
|
16
|
-
// Get all button-like elements near textarea
|
|
17
|
-
const debug = await page.evaluate(`(() => {
|
|
18
|
-
const ta = document.querySelector('textarea');
|
|
19
|
-
if (!ta) return { error: 'no textarea' };
|
|
20
|
-
|
|
21
|
-
// Get parent containers
|
|
22
|
-
let parent = ta.parentElement;
|
|
23
|
-
const parents = [];
|
|
24
|
-
for (let i = 0; i < 5 && parent; i++) {
|
|
25
|
-
parents.push({
|
|
26
|
-
tag: parent.tagName,
|
|
27
|
-
class: parent.className?.substring(0, 80),
|
|
28
|
-
childCount: parent.children.length,
|
|
29
|
-
});
|
|
30
|
-
parent = parent.parentElement;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Find buttons in the form/container near textarea
|
|
34
|
-
const form = ta.closest('form') || ta.closest('[class*="composer"]') || ta.closest('[class*="input"]') || ta.parentElement?.parentElement;
|
|
35
|
-
const buttons = form ? [...form.querySelectorAll('button')].map(b => ({
|
|
36
|
-
testid: b.getAttribute('data-testid'),
|
|
37
|
-
type: b.type,
|
|
38
|
-
disabled: b.disabled,
|
|
39
|
-
text: (b.textContent || '').substring(0, 30),
|
|
40
|
-
html: b.outerHTML.substring(0, 200),
|
|
41
|
-
rect: b.getBoundingClientRect().toJSON(),
|
|
42
|
-
})) : [];
|
|
43
|
-
|
|
44
|
-
return { parents, buttons, formTag: form?.tagName, formClass: form?.className?.substring(0, 80) };
|
|
45
|
-
})()`);
|
|
46
|
-
|
|
47
|
-
return [{ data: JSON.stringify(debug, null, 2) }];
|
|
48
|
-
},
|
|
49
|
-
});
|