@jackwener/opencli 0.5.1 → 0.6.0
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/README.md +3 -2
- package/README.zh-CN.md +4 -3
- package/SKILL.md +7 -4
- package/dist/browser.d.ts +7 -3
- package/dist/browser.js +25 -92
- package/dist/browser.test.js +18 -1
- package/dist/cascade.d.ts +1 -1
- package/dist/cascade.js +42 -75
- package/dist/cli-manifest.json +80 -0
- package/dist/clis/coupang/add-to-cart.d.ts +1 -0
- package/dist/clis/coupang/add-to-cart.js +141 -0
- package/dist/clis/coupang/search.d.ts +1 -0
- package/dist/clis/coupang/search.js +453 -0
- package/dist/constants.d.ts +13 -0
- package/dist/constants.js +30 -0
- package/dist/coupang.d.ts +24 -0
- package/dist/coupang.js +262 -0
- package/dist/coupang.test.d.ts +1 -0
- package/dist/coupang.test.js +62 -0
- package/dist/doctor.d.ts +15 -0
- package/dist/doctor.js +226 -25
- package/dist/doctor.test.js +13 -6
- package/dist/engine.js +3 -3
- package/dist/engine.test.d.ts +4 -0
- package/dist/engine.test.js +67 -0
- package/dist/explore.js +1 -15
- package/dist/interceptor.d.ts +42 -0
- package/dist/interceptor.js +138 -0
- package/dist/main.js +8 -4
- package/dist/output.js +0 -5
- package/dist/pipeline/steps/intercept.js +4 -54
- package/dist/pipeline/steps/tap.js +11 -51
- package/dist/registry.d.ts +3 -1
- package/dist/registry.test.d.ts +4 -0
- package/dist/registry.test.js +90 -0
- package/dist/runtime.d.ts +15 -1
- package/dist/runtime.js +11 -6
- package/dist/setup.d.ts +4 -0
- package/dist/setup.js +145 -0
- package/dist/synthesize.js +5 -5
- package/dist/tui.d.ts +22 -0
- package/dist/tui.js +139 -0
- package/dist/validate.js +21 -0
- package/dist/verify.d.ts +7 -0
- package/dist/verify.js +7 -1
- package/dist/version.d.ts +4 -0
- package/dist/version.js +16 -0
- package/package.json +1 -1
- package/src/browser.test.ts +20 -1
- package/src/browser.ts +25 -87
- package/src/cascade.ts +47 -75
- package/src/clis/coupang/add-to-cart.ts +149 -0
- package/src/clis/coupang/search.ts +466 -0
- package/src/constants.ts +35 -0
- package/src/coupang.test.ts +78 -0
- package/src/coupang.ts +302 -0
- package/src/doctor.test.ts +15 -6
- package/src/doctor.ts +221 -25
- package/src/engine.test.ts +77 -0
- package/src/engine.ts +5 -5
- package/src/explore.ts +2 -15
- package/src/interceptor.ts +153 -0
- package/src/main.ts +9 -5
- package/src/output.ts +0 -4
- package/src/pipeline/executor.ts +15 -15
- package/src/pipeline/steps/intercept.ts +4 -55
- package/src/pipeline/steps/tap.ts +12 -51
- package/src/registry.test.ts +106 -0
- package/src/registry.ts +4 -1
- package/src/runtime.ts +22 -8
- package/src/setup.ts +169 -0
- package/src/synthesize.ts +5 -5
- package/src/tui.ts +171 -0
- package/src/validate.ts +22 -0
- package/src/verify.ts +10 -1
- package/src/version.ts +18 -0
package/dist/doctor.js
CHANGED
|
@@ -1,23 +1,57 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as os from 'node:os';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
|
+
import { execSync } from 'node:child_process';
|
|
4
5
|
import { createInterface } from 'node:readline/promises';
|
|
5
6
|
import { stdin as input, stdout as output } from 'node:process';
|
|
7
|
+
import chalk from 'chalk';
|
|
6
8
|
import { getTokenFingerprint } from './browser.js';
|
|
7
9
|
const PLAYWRIGHT_SERVER_NAME = 'playwright';
|
|
8
|
-
const PLAYWRIGHT_TOKEN_ENV = 'PLAYWRIGHT_MCP_EXTENSION_TOKEN';
|
|
10
|
+
export const PLAYWRIGHT_TOKEN_ENV = 'PLAYWRIGHT_MCP_EXTENSION_TOKEN';
|
|
9
11
|
const PLAYWRIGHT_EXTENSION_ID = 'mmlmfjhmonkocbjadbfplnigmagldckm';
|
|
10
12
|
const TOKEN_LINE_RE = /^(\s*export\s+PLAYWRIGHT_MCP_EXTENSION_TOKEN=)(['"]?)([^'"\\\n]+)\2\s*$/m;
|
|
11
|
-
function
|
|
12
|
-
|
|
13
|
+
function colorLabel(status) {
|
|
14
|
+
switch (status) {
|
|
15
|
+
case 'OK': return chalk.green('[OK]');
|
|
16
|
+
case 'MISSING': return chalk.red('[MISSING]');
|
|
17
|
+
case 'MISMATCH': return chalk.yellow('[MISMATCH]');
|
|
18
|
+
case 'WARN': return chalk.yellow('[WARN]');
|
|
19
|
+
}
|
|
13
20
|
}
|
|
14
21
|
function statusLine(status, text) {
|
|
15
|
-
return `${
|
|
22
|
+
return `${colorLabel(status)} ${text}`;
|
|
16
23
|
}
|
|
17
24
|
function tokenSummary(token, fingerprint) {
|
|
18
25
|
if (!token)
|
|
19
|
-
return 'missing';
|
|
20
|
-
return `configured (${fingerprint})`;
|
|
26
|
+
return chalk.dim('missing');
|
|
27
|
+
return `configured ${chalk.dim(`(${fingerprint})`)}`;
|
|
28
|
+
}
|
|
29
|
+
export function shortenPath(p) {
|
|
30
|
+
const home = os.homedir();
|
|
31
|
+
return home && p.startsWith(home) ? '~' + p.slice(home.length) : p;
|
|
32
|
+
}
|
|
33
|
+
export function toolName(p) {
|
|
34
|
+
if (p.includes('.codex/'))
|
|
35
|
+
return 'Codex';
|
|
36
|
+
if (p.includes('.cursor/'))
|
|
37
|
+
return 'Cursor';
|
|
38
|
+
if (p.includes('.claude.json'))
|
|
39
|
+
return 'Claude Code';
|
|
40
|
+
if (p.includes('antigravity'))
|
|
41
|
+
return 'Antigravity';
|
|
42
|
+
if (p.includes('.gemini/settings'))
|
|
43
|
+
return 'Gemini CLI';
|
|
44
|
+
if (p.includes('opencode'))
|
|
45
|
+
return 'OpenCode';
|
|
46
|
+
if (p.includes('Claude/claude_desktop'))
|
|
47
|
+
return 'Claude Desktop';
|
|
48
|
+
if (p.includes('.vscode/'))
|
|
49
|
+
return 'VS Code';
|
|
50
|
+
if (p.includes('.mcp.json'))
|
|
51
|
+
return 'Project MCP';
|
|
52
|
+
if (p.includes('.zshrc') || p.includes('.bashrc') || p.includes('.profile'))
|
|
53
|
+
return 'Shell';
|
|
54
|
+
return '';
|
|
21
55
|
}
|
|
22
56
|
export function getDefaultShellRcPath() {
|
|
23
57
|
const shell = process.env.SHELL ?? '';
|
|
@@ -33,12 +67,16 @@ export function getDefaultMcpConfigPaths(cwd = process.cwd()) {
|
|
|
33
67
|
path.join(home, '.codex', 'config.toml'),
|
|
34
68
|
path.join(home, '.codex', 'mcp.json'),
|
|
35
69
|
path.join(home, '.cursor', 'mcp.json'),
|
|
70
|
+
path.join(home, '.claude.json'),
|
|
71
|
+
path.join(home, '.gemini', 'settings.json'),
|
|
72
|
+
path.join(home, '.gemini', 'antigravity', 'mcp_config.json'),
|
|
36
73
|
path.join(home, '.config', 'opencode', 'opencode.json'),
|
|
37
74
|
path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'),
|
|
38
75
|
path.join(home, '.config', 'Claude', 'claude_desktop_config.json'),
|
|
39
76
|
path.join(cwd, '.cursor', 'mcp.json'),
|
|
40
77
|
path.join(cwd, '.vscode', 'mcp.json'),
|
|
41
78
|
path.join(cwd, '.opencode', 'opencode.json'),
|
|
79
|
+
path.join(cwd, '.mcp.json'),
|
|
42
80
|
];
|
|
43
81
|
return [...new Set(candidates)];
|
|
44
82
|
}
|
|
@@ -119,7 +157,7 @@ export function upsertTomlConfigToken(content, token) {
|
|
|
119
157
|
const prefix = content.trim() ? `${content.replace(/\s*$/, '')}\n\n` : '';
|
|
120
158
|
return `${prefix}[mcp_servers.playwright]\ntype = "stdio"\ncommand = "npx"\nargs = ["-y", "@playwright/mcp@latest", "--extension"]\n\n[mcp_servers.playwright.env]\n${tokenLine}\n`;
|
|
121
159
|
}
|
|
122
|
-
function fileExists(filePath) {
|
|
160
|
+
export function fileExists(filePath) {
|
|
123
161
|
try {
|
|
124
162
|
return fs.existsSync(filePath);
|
|
125
163
|
}
|
|
@@ -169,6 +207,144 @@ function readConfigStatus(filePath) {
|
|
|
169
207
|
};
|
|
170
208
|
}
|
|
171
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Discover the auth token stored by the Playwright MCP Bridge extension
|
|
212
|
+
* by scanning Chrome's LevelDB localStorage files directly.
|
|
213
|
+
*
|
|
214
|
+
* Uses `strings` + `grep` for fast binary scanning on macOS/Linux,
|
|
215
|
+
* with a pure-Node fallback on Windows.
|
|
216
|
+
*/
|
|
217
|
+
export function discoverExtensionToken() {
|
|
218
|
+
const home = os.homedir();
|
|
219
|
+
const platform = os.platform();
|
|
220
|
+
const bases = [];
|
|
221
|
+
if (platform === 'darwin') {
|
|
222
|
+
bases.push(path.join(home, 'Library', 'Application Support', 'Google', 'Chrome'), path.join(home, 'Library', 'Application Support', 'Google', 'Chrome Canary'), path.join(home, 'Library', 'Application Support', 'Chromium'), path.join(home, 'Library', 'Application Support', 'Microsoft Edge'));
|
|
223
|
+
}
|
|
224
|
+
else if (platform === 'linux') {
|
|
225
|
+
bases.push(path.join(home, '.config', 'google-chrome'), path.join(home, '.config', 'chromium'), path.join(home, '.config', 'microsoft-edge'));
|
|
226
|
+
}
|
|
227
|
+
else if (platform === 'win32') {
|
|
228
|
+
const appData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
229
|
+
bases.push(path.join(appData, 'Google', 'Chrome', 'User Data'), path.join(appData, 'Microsoft', 'Edge', 'User Data'));
|
|
230
|
+
}
|
|
231
|
+
const profiles = ['Default', 'Profile 1', 'Profile 2', 'Profile 3'];
|
|
232
|
+
// Token is 43 chars of base64url (from 32 random bytes)
|
|
233
|
+
const tokenRe = /([A-Za-z0-9_-]{40,50})/;
|
|
234
|
+
for (const base of bases) {
|
|
235
|
+
for (const profile of profiles) {
|
|
236
|
+
const dir = path.join(base, profile, 'Local Storage', 'leveldb');
|
|
237
|
+
if (!fileExists(dir))
|
|
238
|
+
continue;
|
|
239
|
+
// Fast path: use strings + grep to find candidate files and extract token
|
|
240
|
+
if (platform !== 'win32') {
|
|
241
|
+
const token = extractTokenViaStrings(dir, tokenRe);
|
|
242
|
+
if (token)
|
|
243
|
+
return token;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
// Slow path (Windows): read binary files directly
|
|
247
|
+
const token = extractTokenViaBinaryRead(dir, tokenRe);
|
|
248
|
+
if (token)
|
|
249
|
+
return token;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
function extractTokenViaStrings(dir, tokenRe) {
|
|
255
|
+
try {
|
|
256
|
+
// Single shell pipeline: for each LevelDB file, extract strings, find lines
|
|
257
|
+
// after the extension ID, and filter for base64url token pattern.
|
|
258
|
+
//
|
|
259
|
+
// LevelDB `strings` output for the extension's auth-token entry:
|
|
260
|
+
// auth-token ← key name
|
|
261
|
+
// 4,mmlmfjhmonkocbjadbfplnigmagldckm.7 ← LevelDB internal key
|
|
262
|
+
// hqI86ncsD1QpcVcj-k9CyzTF-ieCQd_4KreZ_wy1WHA ← token value
|
|
263
|
+
//
|
|
264
|
+
// We get the line immediately after any EXTENSION_ID mention and check
|
|
265
|
+
// if it looks like a base64url token (40-50 chars, [A-Za-z0-9_-]).
|
|
266
|
+
const shellDir = dir.replace(/'/g, "'\\''");
|
|
267
|
+
const cmd = `for f in '${shellDir}'/*.ldb '${shellDir}'/*.log; do ` +
|
|
268
|
+
`[ -f "$f" ] && strings "$f" 2>/dev/null | ` +
|
|
269
|
+
`grep -A1 '${PLAYWRIGHT_EXTENSION_ID}' | ` +
|
|
270
|
+
`grep -v '${PLAYWRIGHT_EXTENSION_ID}' | ` +
|
|
271
|
+
`grep -E '^[A-Za-z0-9_-]{40,50}$' | head -1; ` +
|
|
272
|
+
`done 2>/dev/null`;
|
|
273
|
+
const result = execSync(cmd, { encoding: 'utf-8', timeout: 10000 }).trim();
|
|
274
|
+
// Take the first non-empty line
|
|
275
|
+
for (const line of result.split('\n')) {
|
|
276
|
+
const token = line.trim();
|
|
277
|
+
if (token && validateBase64urlToken(token))
|
|
278
|
+
return token;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch { }
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
function extractTokenViaBinaryRead(dir, tokenRe) {
|
|
285
|
+
const extIdBuf = Buffer.from(PLAYWRIGHT_EXTENSION_ID);
|
|
286
|
+
const keyBuf = Buffer.from('auth-token');
|
|
287
|
+
let files;
|
|
288
|
+
try {
|
|
289
|
+
files = fs.readdirSync(dir)
|
|
290
|
+
.filter(f => f.endsWith('.ldb') || f.endsWith('.log'))
|
|
291
|
+
.map(f => path.join(dir, f));
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
// Sort by mtime descending
|
|
297
|
+
files.sort((a, b) => {
|
|
298
|
+
try {
|
|
299
|
+
return fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs;
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
return 0;
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
for (const file of files) {
|
|
306
|
+
let data;
|
|
307
|
+
try {
|
|
308
|
+
data = fs.readFileSync(file);
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
// Quick check: does file contain both the extension ID and auth-token key?
|
|
314
|
+
const extPos = data.indexOf(extIdBuf);
|
|
315
|
+
if (extPos === -1)
|
|
316
|
+
continue;
|
|
317
|
+
const keyPos = data.indexOf(keyBuf, Math.max(0, extPos - 500));
|
|
318
|
+
if (keyPos === -1)
|
|
319
|
+
continue;
|
|
320
|
+
// Scan for token value after auth-token key
|
|
321
|
+
let idx = 0;
|
|
322
|
+
while (true) {
|
|
323
|
+
const kp = data.indexOf(keyBuf, idx);
|
|
324
|
+
if (kp === -1)
|
|
325
|
+
break;
|
|
326
|
+
const contextStart = Math.max(0, kp - 500);
|
|
327
|
+
if (data.indexOf(extIdBuf, contextStart) !== -1 && data.indexOf(extIdBuf, contextStart) < kp) {
|
|
328
|
+
const after = data.subarray(kp + keyBuf.length, kp + keyBuf.length + 200).toString('latin1');
|
|
329
|
+
const m = after.match(tokenRe);
|
|
330
|
+
if (m && validateBase64urlToken(m[1]))
|
|
331
|
+
return m[1];
|
|
332
|
+
}
|
|
333
|
+
idx = kp + 1;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
function validateBase64urlToken(token) {
|
|
339
|
+
try {
|
|
340
|
+
const b64 = token.replace(/-/g, '+').replace(/_/g, '/');
|
|
341
|
+
const decoded = Buffer.from(b64, 'base64');
|
|
342
|
+
return decoded.length >= 28 && decoded.length <= 36;
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
172
348
|
export async function runBrowserDoctor(opts = {}) {
|
|
173
349
|
const envToken = process.env[PLAYWRIGHT_TOKEN_ENV] ?? null;
|
|
174
350
|
const shellPath = opts.shellRc ?? getDefaultShellRcPath();
|
|
@@ -181,18 +357,23 @@ export async function runBrowserDoctor(opts = {}) {
|
|
|
181
357
|
});
|
|
182
358
|
const configPaths = opts.configPaths?.length ? opts.configPaths : getDefaultMcpConfigPaths();
|
|
183
359
|
const configs = configPaths.map(readConfigStatus);
|
|
360
|
+
// Try to discover the token directly from the Chrome extension's localStorage
|
|
361
|
+
const extensionToken = discoverExtensionToken();
|
|
184
362
|
const allTokens = [
|
|
185
363
|
opts.token ?? null,
|
|
364
|
+
extensionToken,
|
|
186
365
|
envToken,
|
|
187
366
|
...shellFiles.map(s => s.token),
|
|
188
367
|
...configs.map(c => c.token),
|
|
189
368
|
].filter((v) => !!v);
|
|
190
369
|
const uniqueTokens = [...new Set(allTokens)];
|
|
191
|
-
const recommendedToken = opts.token ?? envToken ?? (uniqueTokens.length === 1 ? uniqueTokens[0] : null) ?? null;
|
|
370
|
+
const recommendedToken = opts.token ?? extensionToken ?? envToken ?? (uniqueTokens.length === 1 ? uniqueTokens[0] : null) ?? null;
|
|
192
371
|
const report = {
|
|
193
372
|
cliVersion: opts.cliVersion,
|
|
194
373
|
envToken,
|
|
195
374
|
envFingerprint: getTokenFingerprint(envToken ?? undefined),
|
|
375
|
+
extensionToken,
|
|
376
|
+
extensionFingerprint: getTokenFingerprint(extensionToken ?? undefined),
|
|
196
377
|
shellFiles,
|
|
197
378
|
configs,
|
|
198
379
|
recommendedToken,
|
|
@@ -219,24 +400,29 @@ export async function runBrowserDoctor(opts = {}) {
|
|
|
219
400
|
}
|
|
220
401
|
export function renderBrowserDoctorReport(report) {
|
|
221
402
|
const tokenFingerprints = [
|
|
403
|
+
report.extensionFingerprint,
|
|
222
404
|
report.envFingerprint,
|
|
223
405
|
...report.shellFiles.map(shell => shell.fingerprint),
|
|
224
406
|
...report.configs.filter(config => config.exists).map(config => config.fingerprint),
|
|
225
407
|
].filter((value) => !!value);
|
|
226
408
|
const uniqueFingerprints = [...new Set(tokenFingerprints)];
|
|
227
409
|
const hasMismatch = uniqueFingerprints.length > 1;
|
|
228
|
-
const lines = [`opencli v${report.cliVersion ?? 'unknown'} doctor
|
|
410
|
+
const lines = [chalk.bold(`opencli v${report.cliVersion ?? 'unknown'} doctor`), ''];
|
|
411
|
+
const extStatus = !report.extensionToken ? 'MISSING' : hasMismatch ? 'MISMATCH' : 'OK';
|
|
412
|
+
lines.push(statusLine(extStatus, `Extension token (Chrome LevelDB): ${tokenSummary(report.extensionToken, report.extensionFingerprint)}`));
|
|
229
413
|
const envStatus = !report.envToken ? 'MISSING' : hasMismatch ? 'MISMATCH' : 'OK';
|
|
230
414
|
lines.push(statusLine(envStatus, `Environment token: ${tokenSummary(report.envToken, report.envFingerprint)}`));
|
|
231
415
|
for (const shell of report.shellFiles) {
|
|
232
416
|
const shellStatus = !shell.token ? 'MISSING' : hasMismatch ? 'MISMATCH' : 'OK';
|
|
233
|
-
|
|
417
|
+
const tool = toolName(shell.path);
|
|
418
|
+
const suffix = tool ? chalk.dim(` [${tool}]`) : '';
|
|
419
|
+
lines.push(statusLine(shellStatus, `${shortenPath(shell.path)}${suffix}: ${tokenSummary(shell.token, shell.fingerprint)}`));
|
|
234
420
|
}
|
|
235
421
|
const existingConfigs = report.configs.filter(config => config.exists);
|
|
236
422
|
const missingConfigCount = report.configs.length - existingConfigs.length;
|
|
237
423
|
if (existingConfigs.length > 0) {
|
|
238
424
|
for (const config of existingConfigs) {
|
|
239
|
-
const parseSuffix = config.parseError ? ` (parse error
|
|
425
|
+
const parseSuffix = config.parseError ? chalk.red(` (parse error)`) : '';
|
|
240
426
|
const configStatus = config.parseError
|
|
241
427
|
? 'WARN'
|
|
242
428
|
: !config.token
|
|
@@ -244,25 +430,27 @@ export function renderBrowserDoctorReport(report) {
|
|
|
244
430
|
: hasMismatch
|
|
245
431
|
? 'MISMATCH'
|
|
246
432
|
: 'OK';
|
|
247
|
-
|
|
433
|
+
const tool = toolName(config.path);
|
|
434
|
+
const suffix = tool ? chalk.dim(` [${tool}]`) : '';
|
|
435
|
+
lines.push(statusLine(configStatus, `${shortenPath(config.path)}${suffix}: ${tokenSummary(config.token, config.fingerprint)}${parseSuffix}`));
|
|
248
436
|
}
|
|
249
437
|
}
|
|
250
438
|
else {
|
|
251
|
-
lines.push(statusLine('MISSING', 'MCP config: no existing config files found
|
|
439
|
+
lines.push(statusLine('MISSING', 'MCP config: no existing config files found'));
|
|
252
440
|
}
|
|
253
441
|
if (missingConfigCount > 0)
|
|
254
|
-
lines.push(` Other scanned config locations not present: ${missingConfigCount}`);
|
|
442
|
+
lines.push(chalk.dim(` Other scanned config locations not present: ${missingConfigCount}`));
|
|
255
443
|
lines.push('');
|
|
256
444
|
lines.push(statusLine(hasMismatch ? 'MISMATCH' : report.recommendedToken ? 'OK' : 'WARN', `Recommended token fingerprint: ${report.recommendedFingerprint ?? 'unavailable'}`));
|
|
257
445
|
if (report.issues.length) {
|
|
258
|
-
lines.push('', 'Issues:');
|
|
446
|
+
lines.push('', chalk.yellow('Issues:'));
|
|
259
447
|
for (const issue of report.issues)
|
|
260
|
-
lines.push(
|
|
448
|
+
lines.push(chalk.dim(` • ${issue}`));
|
|
261
449
|
}
|
|
262
450
|
if (report.warnings.length) {
|
|
263
|
-
lines.push('', 'Warnings:');
|
|
451
|
+
lines.push('', chalk.yellow('Warnings:'));
|
|
264
452
|
for (const warning of report.warnings)
|
|
265
|
-
lines.push(
|
|
453
|
+
lines.push(chalk.dim(` • ${warning}`));
|
|
266
454
|
}
|
|
267
455
|
return lines.join('\n');
|
|
268
456
|
}
|
|
@@ -276,7 +464,7 @@ async function confirmPrompt(question) {
|
|
|
276
464
|
rl.close();
|
|
277
465
|
}
|
|
278
466
|
}
|
|
279
|
-
function writeFileWithMkdir(filePath, content) {
|
|
467
|
+
export function writeFileWithMkdir(filePath, content) {
|
|
280
468
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
281
469
|
fs.writeFileSync(filePath, content, 'utf-8');
|
|
282
470
|
}
|
|
@@ -284,25 +472,38 @@ export async function applyBrowserDoctorFix(report, opts = {}) {
|
|
|
284
472
|
const token = opts.token ?? report.recommendedToken;
|
|
285
473
|
if (!token)
|
|
286
474
|
throw new Error('No Playwright MCP token is available to write. Provide --token first.');
|
|
475
|
+
const fp = getTokenFingerprint(token);
|
|
287
476
|
const plannedWrites = [];
|
|
288
477
|
const shellPath = opts.shellRc ?? report.shellFiles[0]?.path ?? getDefaultShellRcPath();
|
|
289
|
-
|
|
478
|
+
const shellStatus = report.shellFiles.find(s => s.path === shellPath);
|
|
479
|
+
if (shellStatus?.fingerprint !== fp)
|
|
480
|
+
plannedWrites.push(shellPath);
|
|
290
481
|
for (const config of report.configs) {
|
|
291
482
|
if (!config.writable)
|
|
292
483
|
continue;
|
|
484
|
+
if (config.fingerprint === fp)
|
|
485
|
+
continue; // already correct
|
|
293
486
|
plannedWrites.push(config.path);
|
|
294
487
|
}
|
|
488
|
+
if (plannedWrites.length === 0) {
|
|
489
|
+
console.log(chalk.green('All config files are already up to date.'));
|
|
490
|
+
return [];
|
|
491
|
+
}
|
|
295
492
|
if (!opts.yes) {
|
|
296
|
-
const ok = await confirmPrompt(`Update ${plannedWrites.length} file(s) with Playwright MCP token fingerprint ${
|
|
493
|
+
const ok = await confirmPrompt(`Update ${plannedWrites.length} file(s) with Playwright MCP token fingerprint ${fp}?`);
|
|
297
494
|
if (!ok)
|
|
298
495
|
return [];
|
|
299
496
|
}
|
|
300
497
|
const written = [];
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
498
|
+
if (plannedWrites.includes(shellPath)) {
|
|
499
|
+
const shellBefore = fileExists(shellPath) ? fs.readFileSync(shellPath, 'utf-8') : '';
|
|
500
|
+
writeFileWithMkdir(shellPath, upsertShellToken(shellBefore, token));
|
|
501
|
+
written.push(shellPath);
|
|
502
|
+
}
|
|
304
503
|
for (const config of report.configs) {
|
|
305
|
-
if (!
|
|
504
|
+
if (!plannedWrites.includes(config.path))
|
|
505
|
+
continue;
|
|
506
|
+
if (config.parseError)
|
|
306
507
|
continue;
|
|
307
508
|
const before = fileExists(config.path) ? fs.readFileSync(config.path, 'utf-8') : '';
|
|
308
509
|
const next = config.format === 'toml' ? upsertTomlConfigToken(before, token) : upsertJsonConfigToken(before, token);
|
package/dist/doctor.test.js
CHANGED
|
@@ -70,33 +70,40 @@ describe('json token helpers', () => {
|
|
|
70
70
|
});
|
|
71
71
|
});
|
|
72
72
|
describe('doctor report rendering', () => {
|
|
73
|
+
const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
73
74
|
it('renders OK-style report when tokens match', () => {
|
|
74
|
-
const text = renderBrowserDoctorReport({
|
|
75
|
+
const text = strip(renderBrowserDoctorReport({
|
|
75
76
|
envToken: 'abc123',
|
|
76
77
|
envFingerprint: 'fp1',
|
|
78
|
+
extensionToken: 'abc123',
|
|
79
|
+
extensionFingerprint: 'fp1',
|
|
77
80
|
shellFiles: [{ path: '/tmp/.zshrc', exists: true, token: 'abc123', fingerprint: 'fp1' }],
|
|
78
81
|
configs: [{ path: '/tmp/mcp.json', exists: true, format: 'json', token: 'abc123', fingerprint: 'fp1', writable: true }],
|
|
79
82
|
recommendedToken: 'abc123',
|
|
80
83
|
recommendedFingerprint: 'fp1',
|
|
81
84
|
warnings: [],
|
|
82
85
|
issues: [],
|
|
83
|
-
});
|
|
86
|
+
}));
|
|
84
87
|
expect(text).toContain('[OK] Environment token: configured (fp1)');
|
|
85
|
-
expect(text).toContain('[OK]
|
|
88
|
+
expect(text).toContain('[OK] /tmp/mcp.json');
|
|
89
|
+
expect(text).toContain('configured (fp1)');
|
|
86
90
|
});
|
|
87
91
|
it('renders MISMATCH-style report when fingerprints differ', () => {
|
|
88
|
-
const text = renderBrowserDoctorReport({
|
|
92
|
+
const text = strip(renderBrowserDoctorReport({
|
|
89
93
|
envToken: 'abc123',
|
|
90
94
|
envFingerprint: 'fp1',
|
|
95
|
+
extensionToken: null,
|
|
96
|
+
extensionFingerprint: null,
|
|
91
97
|
shellFiles: [{ path: '/tmp/.zshrc', exists: true, token: 'def456', fingerprint: 'fp2' }],
|
|
92
98
|
configs: [{ path: '/tmp/mcp.json', exists: true, format: 'json', token: 'abc123', fingerprint: 'fp1', writable: true }],
|
|
93
99
|
recommendedToken: 'abc123',
|
|
94
100
|
recommendedFingerprint: 'fp1',
|
|
95
101
|
warnings: [],
|
|
96
102
|
issues: ['Detected inconsistent Playwright MCP tokens across env/config files.'],
|
|
97
|
-
});
|
|
103
|
+
}));
|
|
98
104
|
expect(text).toContain('[MISMATCH] Environment token: configured (fp1)');
|
|
99
|
-
expect(text).toContain('[MISMATCH]
|
|
105
|
+
expect(text).toContain('[MISMATCH] /tmp/.zshrc');
|
|
106
|
+
expect(text).toContain('configured (fp2)');
|
|
100
107
|
expect(text).toContain('[MISMATCH] Recommended token fingerprint: fp1');
|
|
101
108
|
});
|
|
102
109
|
});
|
package/dist/engine.js
CHANGED
|
@@ -73,7 +73,6 @@ function loadFromManifest(manifestPath, clisDir) {
|
|
|
73
73
|
columns: entry.columns,
|
|
74
74
|
timeoutSeconds: entry.timeout,
|
|
75
75
|
source: modulePath,
|
|
76
|
-
// Mark as lazy — executeCommand will load the module before running
|
|
77
76
|
_lazy: true,
|
|
78
77
|
_modulePath: modulePath,
|
|
79
78
|
};
|
|
@@ -158,8 +157,9 @@ function registerYamlCli(filePath, defaultSite) {
|
|
|
158
157
|
*/
|
|
159
158
|
export async function executeCommand(cmd, page, kwargs, debug = false) {
|
|
160
159
|
// Lazy-load TS module on first execution
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
const internal = cmd;
|
|
161
|
+
if (internal._lazy && internal._modulePath) {
|
|
162
|
+
const modulePath = internal._modulePath;
|
|
163
163
|
if (!_loadedModules.has(modulePath)) {
|
|
164
164
|
try {
|
|
165
165
|
await import(`file://${modulePath}`);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for engine.ts: CLI discovery and command execution.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import { discoverClis, executeCommand } from './engine.js';
|
|
6
|
+
import { cli, Strategy } from './registry.js';
|
|
7
|
+
describe('discoverClis', () => {
|
|
8
|
+
it('handles non-existent directories gracefully', async () => {
|
|
9
|
+
// Should not throw for missing directories
|
|
10
|
+
await expect(discoverClis('/tmp/nonexistent-opencli-test-dir')).resolves.not.toThrow();
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
describe('executeCommand', () => {
|
|
14
|
+
it('executes a command with func', async () => {
|
|
15
|
+
const cmd = cli({
|
|
16
|
+
site: 'test-engine',
|
|
17
|
+
name: 'func-test',
|
|
18
|
+
description: 'test command with func',
|
|
19
|
+
browser: false,
|
|
20
|
+
strategy: Strategy.PUBLIC,
|
|
21
|
+
func: async (_page, kwargs) => {
|
|
22
|
+
return [{ title: kwargs.query ?? 'default' }];
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
const result = await executeCommand(cmd, null, { query: 'hello' });
|
|
26
|
+
expect(result).toEqual([{ title: 'hello' }]);
|
|
27
|
+
});
|
|
28
|
+
it('executes a command with pipeline', async () => {
|
|
29
|
+
const cmd = cli({
|
|
30
|
+
site: 'test-engine',
|
|
31
|
+
name: 'pipe-test',
|
|
32
|
+
description: 'test command with pipeline',
|
|
33
|
+
browser: false,
|
|
34
|
+
strategy: Strategy.PUBLIC,
|
|
35
|
+
pipeline: [
|
|
36
|
+
{ evaluate: '() => [{ n: 1 }, { n: 2 }, { n: 3 }]' },
|
|
37
|
+
{ limit: '2' },
|
|
38
|
+
],
|
|
39
|
+
});
|
|
40
|
+
// Pipeline commands require page for evaluate step, so we'll test the error path
|
|
41
|
+
await expect(executeCommand(cmd, null, {})).rejects.toThrow();
|
|
42
|
+
});
|
|
43
|
+
it('throws for command with no func or pipeline', async () => {
|
|
44
|
+
const cmd = cli({
|
|
45
|
+
site: 'test-engine',
|
|
46
|
+
name: 'empty-test',
|
|
47
|
+
description: 'empty command',
|
|
48
|
+
browser: false,
|
|
49
|
+
});
|
|
50
|
+
await expect(executeCommand(cmd, null, {})).rejects.toThrow('has no func or pipeline');
|
|
51
|
+
});
|
|
52
|
+
it('passes debug flag to func', async () => {
|
|
53
|
+
let receivedDebug = false;
|
|
54
|
+
const cmd = cli({
|
|
55
|
+
site: 'test-engine',
|
|
56
|
+
name: 'debug-test',
|
|
57
|
+
description: 'debug test',
|
|
58
|
+
browser: false,
|
|
59
|
+
func: async (_page, _kwargs, debug) => {
|
|
60
|
+
receivedDebug = debug ?? false;
|
|
61
|
+
return [];
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
await executeCommand(cmd, null, {}, true);
|
|
65
|
+
expect(receivedDebug).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
});
|
package/dist/explore.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import * as fs from 'node:fs';
|
|
9
9
|
import * as path from 'node:path';
|
|
10
10
|
import { DEFAULT_BROWSER_EXPLORE_TIMEOUT, browserSession, runWithTimeout } from './runtime.js';
|
|
11
|
+
import { VOLATILE_PARAMS, SEARCH_PARAMS, PAGINATION_PARAMS, LIMIT_PARAMS, FIELD_ROLES } from './constants.js';
|
|
11
12
|
// ── Site name detection ────────────────────────────────────────────────────
|
|
12
13
|
const KNOWN_SITE_ALIASES = {
|
|
13
14
|
'x.com': 'twitter', 'twitter.com': 'twitter',
|
|
@@ -39,21 +40,6 @@ export function detectSiteName(url) {
|
|
|
39
40
|
export function slugify(value) {
|
|
40
41
|
return value.trim().toLowerCase().replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-|-$/g, '') || 'site';
|
|
41
42
|
}
|
|
42
|
-
// ── Field & capability inference ───────────────────────────────────────────
|
|
43
|
-
const FIELD_ROLES = {
|
|
44
|
-
title: ['title', 'name', 'text', 'content', 'desc', 'description', 'headline', 'subject'],
|
|
45
|
-
url: ['url', 'uri', 'link', 'href', 'permalink', 'jump_url', 'web_url', 'share_url'],
|
|
46
|
-
author: ['author', 'username', 'user_name', 'nickname', 'nick', 'owner', 'creator', 'up_name', 'uname'],
|
|
47
|
-
score: ['score', 'hot', 'heat', 'likes', 'like_count', 'view_count', 'views', 'play', 'favorite_count', 'reply_count'],
|
|
48
|
-
time: ['time', 'created_at', 'publish_time', 'pub_time', 'date', 'ctime', 'mtime', 'pubdate', 'created'],
|
|
49
|
-
id: ['id', 'aid', 'bvid', 'mid', 'uid', 'oid', 'note_id', 'item_id'],
|
|
50
|
-
cover: ['cover', 'pic', 'image', 'thumbnail', 'poster', 'avatar'],
|
|
51
|
-
category: ['category', 'tag', 'type', 'tname', 'channel', 'section'],
|
|
52
|
-
};
|
|
53
|
-
const SEARCH_PARAMS = new Set(['q', 'query', 'keyword', 'search', 'wd', 'kw', 'search_query', 'w']);
|
|
54
|
-
const PAGINATION_PARAMS = new Set(['page', 'pn', 'offset', 'cursor', 'next', 'page_num']);
|
|
55
|
-
const LIMIT_PARAMS = new Set(['limit', 'count', 'size', 'per_page', 'page_size', 'ps', 'num']);
|
|
56
|
-
const VOLATILE_PARAMS = new Set(['w_rid', 'wts', '_', 'callback', 'timestamp', 't', 'nonce', 'sign']);
|
|
57
43
|
/**
|
|
58
44
|
* Parse raw network output from Playwright MCP.
|
|
59
45
|
* Handles text format: [GET] url => [200]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared XHR/Fetch interceptor JavaScript generators.
|
|
3
|
+
*
|
|
4
|
+
* Provides a single source of truth for monkey-patching browser
|
|
5
|
+
* fetch() and XMLHttpRequest to capture API responses matching
|
|
6
|
+
* a URL pattern. Used by:
|
|
7
|
+
* - Page.installInterceptor() (browser.ts)
|
|
8
|
+
* - stepIntercept (pipeline/steps/intercept.ts)
|
|
9
|
+
* - stepTap (pipeline/steps/tap.ts)
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Generate JavaScript source that installs a fetch/XHR interceptor.
|
|
13
|
+
* Captured responses are pushed to `window.__opencli_intercepted`.
|
|
14
|
+
*
|
|
15
|
+
* @param patternExpr - JS expression resolving to a URL substring to match (e.g. a JSON.stringify'd string)
|
|
16
|
+
* @param opts.arrayName - Global array name for captured data (default: '__opencli_intercepted')
|
|
17
|
+
* @param opts.patchGuard - Global boolean name to prevent double-patching (default: '__opencli_interceptor_patched')
|
|
18
|
+
*/
|
|
19
|
+
export declare function generateInterceptorJs(patternExpr: string, opts?: {
|
|
20
|
+
arrayName?: string;
|
|
21
|
+
patchGuard?: string;
|
|
22
|
+
}): string;
|
|
23
|
+
/**
|
|
24
|
+
* Generate JavaScript source to read and clear intercepted data.
|
|
25
|
+
*/
|
|
26
|
+
export declare function generateReadInterceptedJs(arrayName?: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Generate a self-contained tap interceptor for store-action bridge.
|
|
29
|
+
* Unlike the global interceptor, this one:
|
|
30
|
+
* - Installs temporarily, restores originals in finally block
|
|
31
|
+
* - Resolves a promise on first capture (for immediate await)
|
|
32
|
+
* - Returns captured data directly
|
|
33
|
+
*/
|
|
34
|
+
export declare function generateTapInterceptorJs(patternExpr: string): {
|
|
35
|
+
setupVar: string;
|
|
36
|
+
capturedVar: string;
|
|
37
|
+
promiseVar: string;
|
|
38
|
+
resolveVar: string;
|
|
39
|
+
fetchPatch: string;
|
|
40
|
+
xhrPatch: string;
|
|
41
|
+
restorePatch: string;
|
|
42
|
+
};
|