@jackwener/opencli 0.4.2 → 0.4.3

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 (64) hide show
  1. package/CLI-CREATOR.md +10 -10
  2. package/LICENSE +28 -0
  3. package/README.md +113 -63
  4. package/README.zh-CN.md +114 -63
  5. package/SKILL.md +21 -4
  6. package/dist/browser.d.ts +21 -2
  7. package/dist/browser.js +269 -15
  8. package/dist/browser.test.d.ts +1 -0
  9. package/dist/browser.test.js +43 -0
  10. package/dist/build-manifest.js +4 -0
  11. package/dist/cli-manifest.json +279 -3
  12. package/dist/clis/boss/search.js +186 -30
  13. package/dist/clis/twitter/delete.d.ts +1 -0
  14. package/dist/clis/twitter/delete.js +73 -0
  15. package/dist/clis/twitter/followers.d.ts +1 -0
  16. package/dist/clis/twitter/followers.js +104 -0
  17. package/dist/clis/twitter/following.d.ts +1 -0
  18. package/dist/clis/twitter/following.js +90 -0
  19. package/dist/clis/twitter/like.d.ts +1 -0
  20. package/dist/clis/twitter/like.js +69 -0
  21. package/dist/clis/twitter/notifications.d.ts +1 -0
  22. package/dist/clis/twitter/notifications.js +109 -0
  23. package/dist/clis/twitter/post.d.ts +1 -0
  24. package/dist/clis/twitter/post.js +63 -0
  25. package/dist/clis/twitter/reply.d.ts +1 -0
  26. package/dist/clis/twitter/reply.js +57 -0
  27. package/dist/clis/v2ex/daily.d.ts +1 -0
  28. package/dist/clis/v2ex/daily.js +98 -0
  29. package/dist/clis/v2ex/me.d.ts +1 -0
  30. package/dist/clis/v2ex/me.js +99 -0
  31. package/dist/clis/v2ex/notifications.d.ts +1 -0
  32. package/dist/clis/v2ex/notifications.js +72 -0
  33. package/dist/doctor.d.ts +50 -0
  34. package/dist/doctor.js +372 -0
  35. package/dist/doctor.test.d.ts +1 -0
  36. package/dist/doctor.test.js +114 -0
  37. package/dist/main.js +47 -5
  38. package/dist/output.test.d.ts +1 -0
  39. package/dist/output.test.js +20 -0
  40. package/dist/registry.d.ts +4 -0
  41. package/dist/registry.js +1 -0
  42. package/dist/runtime.d.ts +3 -1
  43. package/dist/runtime.js +2 -2
  44. package/package.json +2 -2
  45. package/src/browser.test.ts +51 -0
  46. package/src/browser.ts +318 -22
  47. package/src/build-manifest.ts +4 -0
  48. package/src/clis/boss/search.ts +196 -29
  49. package/src/clis/twitter/delete.ts +78 -0
  50. package/src/clis/twitter/followers.ts +119 -0
  51. package/src/clis/twitter/following.ts +105 -0
  52. package/src/clis/twitter/like.ts +74 -0
  53. package/src/clis/twitter/notifications.ts +119 -0
  54. package/src/clis/twitter/post.ts +68 -0
  55. package/src/clis/twitter/reply.ts +62 -0
  56. package/src/clis/v2ex/daily.ts +105 -0
  57. package/src/clis/v2ex/me.ts +103 -0
  58. package/src/clis/v2ex/notifications.ts +77 -0
  59. package/src/doctor.test.ts +133 -0
  60. package/src/doctor.ts +424 -0
  61. package/src/main.ts +47 -4
  62. package/src/output.test.ts +27 -0
  63. package/src/registry.ts +5 -0
  64. package/src/runtime.ts +2 -1
@@ -0,0 +1,114 @@
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.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN).toBe('abc123');
70
+ });
71
+ });
72
+ describe('doctor report rendering', () => {
73
+ it('renders OK-style report when tokens match', () => {
74
+ const text = renderBrowserDoctorReport({
75
+ envToken: 'abc123',
76
+ envFingerprint: 'fp1',
77
+ shellFiles: [{ path: '/tmp/.zshrc', exists: true, token: 'abc123', fingerprint: 'fp1' }],
78
+ configs: [{ path: '/tmp/mcp.json', exists: true, format: 'json', token: 'abc123', fingerprint: 'fp1', writable: true }],
79
+ remoteDebuggingEnabled: true,
80
+ remoteDebuggingEndpoint: 'ws://127.0.0.1:9222/devtools/browser/test',
81
+ cdpEnabled: false,
82
+ cdpToken: null,
83
+ cdpFingerprint: null,
84
+ recommendedToken: 'abc123',
85
+ recommendedFingerprint: 'fp1',
86
+ warnings: [],
87
+ issues: [],
88
+ });
89
+ expect(text).toContain('[OK] Chrome remote debugging: enabled');
90
+ expect(text).toContain('[OK] Environment token: configured (fp1)');
91
+ expect(text).toContain('[OK] MCP config /tmp/mcp.json: configured (fp1)');
92
+ });
93
+ it('renders MISMATCH-style report when fingerprints differ', () => {
94
+ const text = renderBrowserDoctorReport({
95
+ envToken: 'abc123',
96
+ envFingerprint: 'fp1',
97
+ shellFiles: [{ path: '/tmp/.zshrc', exists: true, token: 'def456', fingerprint: 'fp2' }],
98
+ configs: [{ path: '/tmp/mcp.json', exists: true, format: 'json', token: 'abc123', fingerprint: 'fp1', writable: true }],
99
+ remoteDebuggingEnabled: false,
100
+ remoteDebuggingEndpoint: null,
101
+ cdpEnabled: false,
102
+ cdpToken: null,
103
+ cdpFingerprint: null,
104
+ recommendedToken: 'abc123',
105
+ recommendedFingerprint: 'fp1',
106
+ warnings: ['Chrome remote debugging appears to be disabled or Chrome is not currently exposing a DevTools endpoint.'],
107
+ issues: ['Detected inconsistent Playwright MCP tokens across env/config files.'],
108
+ });
109
+ expect(text).toContain('[WARN] Chrome remote debugging: disabled');
110
+ expect(text).toContain('[MISMATCH] Environment token: configured (fp1)');
111
+ expect(text).toContain('[MISMATCH] Shell file /tmp/.zshrc: configured (fp2)');
112
+ expect(text).toContain('[MISMATCH] Recommended token fingerprint: fp1');
113
+ });
114
+ });
package/dist/main.js CHANGED
@@ -24,12 +24,27 @@ await discoverClis(BUILTIN_CLIS, USER_CLIS);
24
24
  const program = new Command();
25
25
  program.name('opencli').description('Make any website your CLI. Zero setup. AI-powered.').version(PKG_VERSION);
26
26
  // ── Built-in commands ──────────────────────────────────────────────────────
27
- program.command('list').description('List all available CLI commands').option('--json', 'JSON output')
27
+ program.command('list').description('List all available CLI commands').option('-f, --format <fmt>', 'Output format: table, json, yaml, md, csv', 'table').option('--json', 'JSON output (deprecated)')
28
28
  .action((opts) => {
29
29
  const registry = getRegistry();
30
30
  const commands = [...registry.values()].sort((a, b) => fullName(a).localeCompare(fullName(b)));
31
- if (opts.json) {
32
- console.log(JSON.stringify(commands.map(c => ({ command: fullName(c), site: c.site, name: c.name, description: c.description, strategy: strategyLabel(c), browser: c.browser, args: c.args.map(a => a.name) })), null, 2));
31
+ const rows = commands.map(c => ({
32
+ command: fullName(c),
33
+ site: c.site,
34
+ name: c.name,
35
+ description: c.description,
36
+ strategy: strategyLabel(c),
37
+ browser: c.browser,
38
+ args: c.args.map(a => a.name).join(', '),
39
+ }));
40
+ const fmt = opts.json && opts.format === 'table' ? 'json' : opts.format;
41
+ if (fmt !== 'table') {
42
+ renderOutput(rows, {
43
+ fmt,
44
+ columns: ['command', 'site', 'name', 'description', 'strategy', 'browser', 'args'],
45
+ title: 'opencli/list',
46
+ source: 'opencli list',
47
+ });
33
48
  return;
34
49
  }
35
50
  const sites = new Map();
@@ -77,6 +92,31 @@ program.command('cascade').description('Strategy cascade: find simplest working
77
92
  });
78
93
  console.log(renderCascadeResult(result));
79
94
  });
95
+ program.command('doctor')
96
+ .description('Diagnose Playwright MCP Bridge, token consistency, and Chrome remote debugging')
97
+ .option('--fix', 'Apply suggested fixes to shell rc and detected MCP configs', false)
98
+ .option('-y, --yes', 'Skip confirmation prompts when applying fixes', false)
99
+ .option('--token <token>', 'Override token to write instead of auto-detecting')
100
+ .option('--shell-rc <path>', 'Shell startup file to update')
101
+ .option('--mcp-config <paths>', 'Comma-separated MCP config paths to scan/update')
102
+ .action(async (opts) => {
103
+ const { runBrowserDoctor, renderBrowserDoctorReport, applyBrowserDoctorFix } = await import('./doctor.js');
104
+ const configPaths = opts.mcpConfig ? String(opts.mcpConfig).split(',').map((s) => s.trim()).filter(Boolean) : undefined;
105
+ const report = await runBrowserDoctor({ token: opts.token, shellRc: opts.shellRc, configPaths, cliVersion: PKG_VERSION });
106
+ console.log(renderBrowserDoctorReport(report));
107
+ if (opts.fix) {
108
+ const written = await applyBrowserDoctorFix(report, { fix: true, yes: opts.yes, token: opts.token, shellRc: opts.shellRc, configPaths });
109
+ console.log();
110
+ if (written.length > 0) {
111
+ console.log(chalk.green('Updated files:'));
112
+ for (const filePath of written)
113
+ console.log(`- ${filePath}`);
114
+ }
115
+ else {
116
+ console.log(chalk.yellow('No files were changed.'));
117
+ }
118
+ }
119
+ });
80
120
  // ── Dynamic site commands ──────────────────────────────────────────────────
81
121
  const registry = getRegistry();
82
122
  const siteGroups = new Map();
@@ -96,7 +136,7 @@ for (const [, cmd] of registry) {
96
136
  else
97
137
  subCmd.option(flag, arg.help ?? '');
98
138
  }
99
- subCmd.option('-f, --format <fmt>', 'Output format: table, json, md, csv', 'table').option('-v, --verbose', 'Debug output', false);
139
+ subCmd.option('-f, --format <fmt>', 'Output format: table, json, yaml, md, csv', 'table').option('-v, --verbose', 'Debug output', false);
100
140
  subCmd.action(async (actionOpts) => {
101
141
  const startTime = Date.now();
102
142
  const kwargs = {};
@@ -108,9 +148,11 @@ for (const [, cmd] of registry) {
108
148
  kwargs[arg.name] = arg.default;
109
149
  }
110
150
  try {
151
+ if (actionOpts.verbose)
152
+ process.env.OPENCLI_VERBOSE = '1';
111
153
  let result;
112
154
  if (cmd.browser) {
113
- result = await browserSession(PlaywrightMCP, async (page) => runWithTimeout(executeCommand(cmd, page, kwargs, actionOpts.verbose), { timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT, label: fullName(cmd) }));
155
+ result = await browserSession(PlaywrightMCP, async (page) => runWithTimeout(executeCommand(cmd, page, kwargs, actionOpts.verbose), { timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT, label: fullName(cmd) }), { forceExtension: cmd.forceExtension });
114
156
  }
115
157
  else {
116
158
  result = await executeCommand(cmd, null, kwargs, actionOpts.verbose);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,20 @@
1
+ import { describe, it, expect, vi, afterEach } from 'vitest';
2
+ import { render } from './output.js';
3
+ afterEach(() => {
4
+ vi.restoreAllMocks();
5
+ });
6
+ describe('render', () => {
7
+ it('renders YAML output', () => {
8
+ const log = vi.spyOn(console, 'log').mockImplementation(() => { });
9
+ render([{ title: 'Hello', rank: 1 }], { fmt: 'yaml' });
10
+ expect(log).toHaveBeenCalledOnce();
11
+ expect(log.mock.calls[0]?.[0]).toContain('- title: Hello');
12
+ expect(log.mock.calls[0]?.[0]).toContain('rank: 1');
13
+ });
14
+ it('renders yml alias as YAML output', () => {
15
+ const log = vi.spyOn(console, 'log').mockImplementation(() => { });
16
+ render({ title: 'Hello' }, { fmt: 'yml' });
17
+ expect(log).toHaveBeenCalledOnce();
18
+ expect(log.mock.calls[0]?.[0]).toContain('title: Hello');
19
+ });
20
+ });
@@ -33,6 +33,8 @@ export interface CliCommand {
33
33
  /** Internal: lazy-loaded TS module support */
34
34
  _lazy?: boolean;
35
35
  _modulePath?: string;
36
+ /** Force extension bridge mode (bypass CDP), for anti-bot sites */
37
+ forceExtension?: boolean;
36
38
  }
37
39
  export interface CliOptions {
38
40
  site: string;
@@ -46,6 +48,8 @@ export interface CliOptions {
46
48
  func?: (page: IPage | null, kwargs: Record<string, any>, debug?: boolean) => Promise<any>;
47
49
  pipeline?: any[];
48
50
  timeoutSeconds?: number;
51
+ /** Force extension bridge mode (bypass CDP), for anti-bot sites */
52
+ forceExtension?: boolean;
49
53
  }
50
54
  export declare function cli(opts: CliOptions): CliCommand;
51
55
  export declare function getRegistry(): Map<string, CliCommand>;
package/dist/registry.js CHANGED
@@ -23,6 +23,7 @@ export function cli(opts) {
23
23
  func: opts.func,
24
24
  pipeline: opts.pipeline,
25
25
  timeoutSeconds: opts.timeoutSeconds,
26
+ forceExtension: opts.forceExtension,
26
27
  };
27
28
  const key = fullName(cmd);
28
29
  _registry.set(key, cmd);
package/dist/runtime.d.ts CHANGED
@@ -10,4 +10,6 @@ export declare function runWithTimeout<T>(promise: Promise<T>, opts: {
10
10
  timeout: number;
11
11
  label?: string;
12
12
  }): Promise<T>;
13
- export declare function browserSession<T>(BrowserFactory: new () => any, fn: (page: IPage) => Promise<T>): Promise<T>;
13
+ export declare function browserSession<T>(BrowserFactory: new () => any, fn: (page: IPage) => Promise<T>, opts?: {
14
+ forceExtension?: boolean;
15
+ }): Promise<T>;
package/dist/runtime.js CHANGED
@@ -15,10 +15,10 @@ export async function runWithTimeout(promise, opts) {
15
15
  .catch((err) => { clearTimeout(timer); reject(err); });
16
16
  });
17
17
  }
18
- export async function browserSession(BrowserFactory, fn) {
18
+ export async function browserSession(BrowserFactory, fn, opts) {
19
19
  const mcp = new BrowserFactory();
20
20
  try {
21
- const page = await mcp.connect({ timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT });
21
+ const page = await mcp.connect({ timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT, forceExtension: opts?.forceExtension });
22
22
  return await fn(page);
23
23
  }
24
24
  finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jackwener/opencli",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -31,7 +31,7 @@
31
31
  "playwright"
32
32
  ],
33
33
  "author": "jackwener",
34
- "license": "MIT",
34
+ "license": "BSD-3-Clause",
35
35
  "repository": {
36
36
  "type": "git",
37
37
  "url": "git+https://github.com/jackwener/opencli.git"
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { formatBrowserConnectError, getTokenFingerprint } from './browser.js';
3
+
4
+ describe('getTokenFingerprint', () => {
5
+ it('returns null for empty token', () => {
6
+ expect(getTokenFingerprint(undefined)).toBeNull();
7
+ });
8
+
9
+ it('returns stable short fingerprint for token', () => {
10
+ expect(getTokenFingerprint('abc123')).toBe('6ca13d52');
11
+ });
12
+ });
13
+
14
+ describe('formatBrowserConnectError', () => {
15
+ it('explains missing extension token clearly', () => {
16
+ const err = formatBrowserConnectError({
17
+ kind: 'missing-token',
18
+ mode: 'extension',
19
+ timeout: 30,
20
+ hasExtensionToken: false,
21
+ });
22
+
23
+ expect(err.message).toContain('PLAYWRIGHT_MCP_EXTENSION_TOKEN is not set');
24
+ expect(err.message).toContain('manual approval dialog');
25
+ });
26
+
27
+ it('mentions token mismatch as likely cause for extension timeout', () => {
28
+ const err = formatBrowserConnectError({
29
+ kind: 'extension-timeout',
30
+ mode: 'extension',
31
+ timeout: 30,
32
+ hasExtensionToken: true,
33
+ tokenFingerprint: 'deadbeef',
34
+ });
35
+
36
+ expect(err.message).toContain('does not match the token currently shown by the browser extension');
37
+ expect(err.message).toContain('deadbeef');
38
+ });
39
+
40
+ it('keeps CDP timeout guidance separate', () => {
41
+ const err = formatBrowserConnectError({
42
+ kind: 'cdp-timeout',
43
+ mode: 'cdp',
44
+ timeout: 30,
45
+ hasExtensionToken: false,
46
+ });
47
+
48
+ expect(err.message).toContain('via CDP');
49
+ expect(err.message).toContain('chrome://inspect#remote-debugging');
50
+ });
51
+ });