@jackwener/opencli 0.7.5 → 0.7.8

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 (56) hide show
  1. package/.agents/skills/cross-project-adapter-migration/SKILL.md +249 -0
  2. package/.agents/workflows/cross-project-adapter-migration.md +54 -0
  3. package/dist/browser/discover.d.ts +8 -0
  4. package/dist/browser/discover.js +83 -0
  5. package/dist/browser/errors.d.ts +21 -0
  6. package/dist/browser/errors.js +54 -0
  7. package/dist/browser/index.d.ts +22 -0
  8. package/dist/browser/index.js +22 -0
  9. package/dist/browser/mcp.d.ts +33 -0
  10. package/dist/browser/mcp.js +304 -0
  11. package/dist/browser/page.d.ts +41 -0
  12. package/dist/browser/page.js +142 -0
  13. package/dist/browser/tabs.d.ts +13 -0
  14. package/dist/browser/tabs.js +70 -0
  15. package/dist/browser.test.js +1 -1
  16. package/dist/completion.d.ts +21 -0
  17. package/dist/completion.js +116 -0
  18. package/dist/doctor.js +7 -7
  19. package/dist/engine.js +6 -4
  20. package/dist/errors.d.ts +25 -0
  21. package/dist/errors.js +42 -0
  22. package/dist/logger.d.ts +22 -0
  23. package/dist/logger.js +47 -0
  24. package/dist/main.js +37 -2
  25. package/dist/pipeline/executor.js +8 -8
  26. package/dist/pipeline/steps/browser.d.ts +7 -7
  27. package/dist/pipeline/steps/intercept.d.ts +1 -1
  28. package/dist/pipeline/steps/tap.d.ts +1 -1
  29. package/dist/setup.js +1 -1
  30. package/package.json +4 -3
  31. package/scripts/clean-yaml.cjs +19 -0
  32. package/scripts/copy-yaml.cjs +21 -0
  33. package/scripts/postinstall.js +200 -0
  34. package/src/bilibili.ts +1 -1
  35. package/src/browser/discover.ts +90 -0
  36. package/src/browser/errors.ts +89 -0
  37. package/src/browser/index.ts +26 -0
  38. package/src/browser/mcp.ts +305 -0
  39. package/src/browser/page.ts +152 -0
  40. package/src/browser/tabs.ts +76 -0
  41. package/src/browser.test.ts +1 -1
  42. package/src/completion.ts +129 -0
  43. package/src/doctor.ts +13 -1
  44. package/src/engine.ts +9 -4
  45. package/src/errors.ts +48 -0
  46. package/src/logger.ts +57 -0
  47. package/src/main.ts +39 -3
  48. package/src/pipeline/executor.ts +8 -7
  49. package/src/pipeline/steps/browser.ts +18 -18
  50. package/src/pipeline/steps/intercept.ts +8 -8
  51. package/src/pipeline/steps/tap.ts +2 -2
  52. package/src/setup.ts +1 -1
  53. package/tsconfig.json +1 -2
  54. package/dist/browser.d.ts +0 -105
  55. package/dist/browser.js +0 -644
  56. package/src/browser.ts +0 -698
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * postinstall script — automatically install shell completion files.
5
+ *
6
+ * Detects the user's default shell and writes the completion script to the
7
+ * standard system completion directory so that tab-completion works immediately
8
+ * after `npm install -g`.
9
+ *
10
+ * Supported shells: bash, zsh, fish.
11
+ *
12
+ * This script is intentionally plain Node.js (no TypeScript, no imports from
13
+ * the main source tree) so that it can run without a build step.
14
+ */
15
+
16
+ import { mkdirSync, writeFileSync, existsSync, readFileSync, appendFileSync } from 'node:fs';
17
+ import { join } from 'node:path';
18
+ import { homedir } from 'node:os';
19
+
20
+
21
+ // ── Completion script content ──────────────────────────────────────────────
22
+
23
+ const BASH_COMPLETION = `# Bash completion for opencli (auto-installed)
24
+ _opencli_completions() {
25
+ local cur words cword
26
+ _get_comp_words_by_ref -n : cur words cword
27
+
28
+ local completions
29
+ completions=$(opencli --get-completions --cursor "$cword" "\${words[@]:1}" 2>/dev/null)
30
+
31
+ COMPREPLY=( $(compgen -W "$completions" -- "$cur") )
32
+ __ltrim_colon_completions "$cur"
33
+ }
34
+ complete -F _opencli_completions opencli
35
+ `;
36
+
37
+ const ZSH_COMPLETION = `#compdef opencli
38
+ # Zsh completion for opencli (auto-installed)
39
+ _opencli() {
40
+ local -a completions
41
+ local cword=$((CURRENT - 1))
42
+ completions=(\${(f)"$(opencli --get-completions --cursor "$cword" "\${words[@]:1}" 2>/dev/null)"})
43
+ compadd -a completions
44
+ }
45
+ _opencli
46
+ `;
47
+
48
+ const FISH_COMPLETION = `# Fish completion for opencli (auto-installed)
49
+ complete -c opencli -f -a '(
50
+ set -l tokens (commandline -cop)
51
+ set -l cursor (count (commandline -cop))
52
+ opencli --get-completions --cursor $cursor $tokens[2..] 2>/dev/null
53
+ )'
54
+ `;
55
+
56
+ // ── Helpers ────────────────────────────────────────────────────────────────
57
+
58
+ function detectShell() {
59
+ const shell = process.env.SHELL || '';
60
+ if (shell.includes('zsh')) return 'zsh';
61
+ if (shell.includes('bash')) return 'bash';
62
+ if (shell.includes('fish')) return 'fish';
63
+ return null;
64
+ }
65
+
66
+ function ensureDir(dir) {
67
+ if (!existsSync(dir)) {
68
+ mkdirSync(dir, { recursive: true });
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Ensure fpath contains the custom completions directory in .zshrc.
74
+ *
75
+ * Key detail: the fpath line MUST appear BEFORE the first `compinit` call,
76
+ * otherwise compinit won't scan our completions directory. This is critical
77
+ * for oh-my-zsh users (source $ZSH/oh-my-zsh.sh calls compinit internally).
78
+ */
79
+ function ensureZshFpath(completionsDir, zshrcPath) {
80
+ const fpathLine = `fpath=(${completionsDir} $fpath)`;
81
+ const autoloadLine = `autoload -Uz compinit && compinit`;
82
+ const marker = '# opencli completion';
83
+
84
+ if (!existsSync(zshrcPath)) {
85
+ writeFileSync(zshrcPath, `${marker}\n${fpathLine}\n${autoloadLine}\n`, 'utf8');
86
+ return;
87
+ }
88
+
89
+ const content = readFileSync(zshrcPath, 'utf8');
90
+
91
+ // Already configured — nothing to do
92
+ if (content.includes(completionsDir)) {
93
+ return;
94
+ }
95
+
96
+ // Find the first line that triggers compinit (direct call or oh-my-zsh source)
97
+ const lines = content.split('\n');
98
+ let insertIdx = -1;
99
+ for (let i = 0; i < lines.length; i++) {
100
+ const trimmed = lines[i].trim();
101
+ // Skip comment-only lines
102
+ if (trimmed.startsWith('#')) continue;
103
+ if (/compinit/.test(trimmed) || /source\s+.*oh-my-zsh\.sh/.test(trimmed)) {
104
+ insertIdx = i;
105
+ break;
106
+ }
107
+ }
108
+
109
+ if (insertIdx !== -1) {
110
+ // Insert fpath BEFORE the compinit / oh-my-zsh source line
111
+ lines.splice(insertIdx, 0, marker, fpathLine);
112
+ writeFileSync(zshrcPath, lines.join('\n'), 'utf8');
113
+ } else {
114
+ // No compinit found — append fpath + compinit at the end
115
+ let addition = `\n${marker}\n${fpathLine}\n${autoloadLine}\n`;
116
+ appendFileSync(zshrcPath, addition, 'utf8');
117
+ }
118
+ }
119
+
120
+ // ── Main ───────────────────────────────────────────────────────────────────
121
+
122
+ function main() {
123
+ // Skip in CI environments
124
+ if (process.env.CI || process.env.CONTINUOUS_INTEGRATION) {
125
+ return;
126
+ }
127
+
128
+ // Only install completion for global installs and npm link
129
+ const isGlobal = process.env.npm_config_global === 'true';
130
+ if (!isGlobal) {
131
+ return;
132
+ }
133
+
134
+ const shell = detectShell();
135
+ if (!shell) {
136
+ // Cannot determine shell; silently skip
137
+ return;
138
+ }
139
+
140
+ const home = homedir();
141
+
142
+ try {
143
+ switch (shell) {
144
+ case 'zsh': {
145
+ const completionsDir = join(home, '.zsh', 'completions');
146
+ const completionFile = join(completionsDir, '_opencli');
147
+ ensureDir(completionsDir);
148
+ writeFileSync(completionFile, ZSH_COMPLETION, 'utf8');
149
+
150
+ // Ensure fpath is set up in .zshrc
151
+ const zshrcPath = join(home, '.zshrc');
152
+ ensureZshFpath(completionsDir, zshrcPath);
153
+
154
+ console.log(`✓ Zsh completion installed to ${completionFile}`);
155
+ console.log(` Restart your shell or run: source ~/.zshrc`);
156
+ break;
157
+ }
158
+ case 'bash': {
159
+ // Try system-level first, fall back to user-level
160
+ const userCompDir = join(home, '.bash_completion.d');
161
+ const completionFile = join(userCompDir, 'opencli');
162
+ ensureDir(userCompDir);
163
+ writeFileSync(completionFile, BASH_COMPLETION, 'utf8');
164
+
165
+ // Ensure .bashrc sources the completion directory
166
+ const bashrcPath = join(home, '.bashrc');
167
+ if (existsSync(bashrcPath)) {
168
+ const content = readFileSync(bashrcPath, 'utf8');
169
+ if (!content.includes('.bash_completion.d/opencli')) {
170
+ appendFileSync(bashrcPath,
171
+ `\n# opencli completion\n[ -f "${completionFile}" ] && source "${completionFile}"\n`,
172
+ 'utf8'
173
+ );
174
+ }
175
+ }
176
+
177
+ console.log(`✓ Bash completion installed to ${completionFile}`);
178
+ console.log(` Restart your shell or run: source ~/.bashrc`);
179
+ break;
180
+ }
181
+ case 'fish': {
182
+ const completionsDir = join(home, '.config', 'fish', 'completions');
183
+ const completionFile = join(completionsDir, 'opencli.fish');
184
+ ensureDir(completionsDir);
185
+ writeFileSync(completionFile, FISH_COMPLETION, 'utf8');
186
+
187
+ console.log(`✓ Fish completion installed to ${completionFile}`);
188
+ console.log(` Restart your shell to activate.`);
189
+ break;
190
+ }
191
+ }
192
+ } catch (err) {
193
+ // Completion install is best-effort; never fail the package install
194
+ if (process.env.OPENCLI_VERBOSE) {
195
+ console.error(`Warning: Could not install shell completion: ${err.message}`);
196
+ }
197
+ }
198
+ }
199
+
200
+ main();
package/src/bilibili.ts CHANGED
@@ -56,7 +56,7 @@ export async function wbiSign(
56
56
  const mixinKey = getMixinKey(imgKey, subKey);
57
57
  const wts = Math.floor(Date.now() / 1000);
58
58
  const sorted: Record<string, string> = {};
59
- const allParams = { ...params, wts: String(wts) };
59
+ const allParams: Record<string, any> = { ...params, wts: String(wts) };
60
60
  for (const key of Object.keys(allParams).sort()) {
61
61
  sorted[key] = String(allParams[key]).replace(/[!'()*]/g, '');
62
62
  }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * MCP server path discovery and argument building.
3
+ */
4
+
5
+ import { execSync } from 'node:child_process';
6
+ import { fileURLToPath } from 'node:url';
7
+ import * as fs from 'node:fs';
8
+ import * as os from 'node:os';
9
+ import * as path from 'node:path';
10
+
11
+ let _cachedMcpServerPath: string | null | undefined;
12
+
13
+ export function findMcpServerPath(): string | null {
14
+ if (_cachedMcpServerPath !== undefined) return _cachedMcpServerPath;
15
+
16
+ const envMcp = process.env.OPENCLI_MCP_SERVER_PATH;
17
+ if (envMcp && fs.existsSync(envMcp)) {
18
+ _cachedMcpServerPath = envMcp;
19
+ return _cachedMcpServerPath;
20
+ }
21
+
22
+ // Check local node_modules first (@playwright/mcp is the modern package)
23
+ const localMcp = path.resolve('node_modules', '@playwright', 'mcp', 'cli.js');
24
+ if (fs.existsSync(localMcp)) {
25
+ _cachedMcpServerPath = localMcp;
26
+ return _cachedMcpServerPath;
27
+ }
28
+
29
+ // Check project-relative path
30
+ const __dirname2 = path.dirname(fileURLToPath(import.meta.url));
31
+ const projectMcp = path.resolve(__dirname2, '..', '..', 'node_modules', '@playwright', 'mcp', 'cli.js');
32
+ if (fs.existsSync(projectMcp)) {
33
+ _cachedMcpServerPath = projectMcp;
34
+ return _cachedMcpServerPath;
35
+ }
36
+
37
+ // Check common locations
38
+ const candidates = [
39
+ path.join(os.homedir(), '.npm', '_npx'),
40
+ path.join(os.homedir(), 'node_modules', '.bin'),
41
+ '/usr/local/lib/node_modules',
42
+ ];
43
+
44
+ // Try npx resolution (legacy package name)
45
+ try {
46
+ const result = execSync('npx -y --package=@playwright/mcp which mcp-server-playwright 2>/dev/null', { encoding: 'utf-8', timeout: 10000 }).trim();
47
+ if (result && fs.existsSync(result)) {
48
+ _cachedMcpServerPath = result;
49
+ return _cachedMcpServerPath;
50
+ }
51
+ } catch {}
52
+
53
+ // Try which
54
+ try {
55
+ const result = execSync('which mcp-server-playwright 2>/dev/null', { encoding: 'utf-8', timeout: 5000 }).trim();
56
+ if (result && fs.existsSync(result)) {
57
+ _cachedMcpServerPath = result;
58
+ return _cachedMcpServerPath;
59
+ }
60
+ } catch {}
61
+
62
+ // Search in common npx cache
63
+ for (const base of candidates) {
64
+ if (!fs.existsSync(base)) continue;
65
+ try {
66
+ const found = execSync(`find "${base}" -name "cli.js" -path "*playwright*mcp*" 2>/dev/null | head -1`, { encoding: 'utf-8', timeout: 5000 }).trim();
67
+ if (found) {
68
+ _cachedMcpServerPath = found;
69
+ return _cachedMcpServerPath;
70
+ }
71
+ } catch {}
72
+ }
73
+
74
+ _cachedMcpServerPath = null;
75
+ return _cachedMcpServerPath;
76
+ }
77
+
78
+ export function buildMcpArgs(input: { mcpPath: string; executablePath?: string | null }): string[] {
79
+ const args = [input.mcpPath];
80
+ if (!process.env.CI) {
81
+ // Local: always connect to user's running Chrome via MCP Bridge extension
82
+ args.push('--extension');
83
+ }
84
+ // CI: standalone mode — @playwright/mcp launches its own browser (headed by default).
85
+ // xvfb provides a virtual display for headed mode in GitHub Actions.
86
+ if (input.executablePath) {
87
+ args.push('--executable-path', input.executablePath);
88
+ }
89
+ return args;
90
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Browser connection error classification and formatting.
3
+ */
4
+
5
+ import { createHash } from 'node:crypto';
6
+
7
+ export type ConnectFailureKind = 'missing-token' | 'extension-timeout' | 'extension-not-installed' | 'mcp-init' | 'process-exit' | 'unknown';
8
+
9
+ export type ConnectFailureInput = {
10
+ kind: ConnectFailureKind;
11
+ timeout: number;
12
+ hasExtensionToken: boolean;
13
+ tokenFingerprint?: string | null;
14
+ stderr?: string;
15
+ exitCode?: number | null;
16
+ rawMessage?: string;
17
+ };
18
+
19
+ export function getTokenFingerprint(token: string | undefined): string | null {
20
+ if (!token) return null;
21
+ return createHash('sha256').update(token).digest('hex').slice(0, 8);
22
+ }
23
+
24
+ export function formatBrowserConnectError(input: ConnectFailureInput): Error {
25
+ const stderr = input.stderr?.trim();
26
+ const suffix = stderr ? `\n\nMCP stderr:\n${stderr}` : '';
27
+ const tokenHint = input.tokenFingerprint ? ` Token fingerprint: ${input.tokenFingerprint}.` : '';
28
+
29
+ if (input.kind === 'missing-token') {
30
+ return new Error(
31
+ 'Failed to connect to Playwright MCP Bridge: PLAYWRIGHT_MCP_EXTENSION_TOKEN is not set.\n\n' +
32
+ 'Without this token, Chrome will show a manual approval dialog for every new MCP connection. ' +
33
+ 'Copy the token from the Playwright MCP Bridge extension and set it in BOTH your shell environment and MCP client config.' +
34
+ suffix,
35
+ );
36
+ }
37
+
38
+ if (input.kind === 'extension-not-installed') {
39
+ return new Error(
40
+ 'Failed to connect to Playwright MCP Bridge: the browser extension did not attach.\n\n' +
41
+ 'Make sure Chrome is running and the "Playwright MCP Bridge" extension is installed and enabled. ' +
42
+ 'If Chrome shows an approval dialog, click Allow.' +
43
+ suffix,
44
+ );
45
+ }
46
+
47
+ if (input.kind === 'extension-timeout') {
48
+ const likelyCause = input.hasExtensionToken
49
+ ? `The most likely cause is that PLAYWRIGHT_MCP_EXTENSION_TOKEN does not match the token currently shown by the browser extension.${tokenHint} Re-copy the token from the extension and update BOTH your shell environment and MCP client config.`
50
+ : 'PLAYWRIGHT_MCP_EXTENSION_TOKEN is not configured, so the extension may be waiting for manual approval.';
51
+ return new Error(
52
+ `Timed out connecting to Playwright MCP Bridge (${input.timeout}s).\n\n` +
53
+ `${likelyCause} If a browser prompt is visible, click Allow.` +
54
+ suffix,
55
+ );
56
+ }
57
+
58
+ if (input.kind === 'mcp-init') {
59
+ return new Error(`Failed to initialize Playwright MCP: ${input.rawMessage ?? 'unknown error'}${suffix}`);
60
+ }
61
+
62
+ if (input.kind === 'process-exit') {
63
+ return new Error(
64
+ `Playwright MCP process exited before the browser connection was established${input.exitCode == null ? '' : ` (code ${input.exitCode})`}.` +
65
+ suffix,
66
+ );
67
+ }
68
+
69
+ return new Error(input.rawMessage ?? 'Failed to connect to browser');
70
+ }
71
+
72
+ export function inferConnectFailureKind(args: {
73
+ hasExtensionToken: boolean;
74
+ stderr: string;
75
+ rawMessage?: string;
76
+ exited?: boolean;
77
+ }): ConnectFailureKind {
78
+ const haystack = `${args.rawMessage ?? ''}\n${args.stderr}`.toLowerCase();
79
+
80
+ if (!args.hasExtensionToken)
81
+ return 'missing-token';
82
+ if (haystack.includes('extension connection timeout') || haystack.includes('playwright mcp bridge'))
83
+ return 'extension-not-installed';
84
+ if (args.rawMessage?.startsWith('MCP init failed:'))
85
+ return 'mcp-init';
86
+ if (args.exited)
87
+ return 'process-exit';
88
+ return 'extension-timeout';
89
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Browser module — public API re-exports.
3
+ *
4
+ * This barrel replaces the former monolithic browser.ts.
5
+ * External code should import from './browser/index.js' (or './browser.js' via Node resolution).
6
+ */
7
+
8
+ export { Page } from './page.js';
9
+ export { PlaywrightMCP } from './mcp.js';
10
+ export { getTokenFingerprint, formatBrowserConnectError } from './errors.js';
11
+ export type { ConnectFailureKind, ConnectFailureInput } from './errors.js';
12
+
13
+ // Test-only helpers — exposed for unit tests
14
+ import { createJsonRpcRequest } from './mcp.js';
15
+ import { extractTabEntries, diffTabIndexes, appendLimited } from './tabs.js';
16
+ import { buildMcpArgs } from './discover.js';
17
+ import { withTimeoutMs } from '../runtime.js';
18
+
19
+ export const __test__ = {
20
+ createJsonRpcRequest,
21
+ extractTabEntries,
22
+ diffTabIndexes,
23
+ appendLimited,
24
+ buildMcpArgs,
25
+ withTimeoutMs,
26
+ };