@jackwener/opencli 0.9.8 → 1.0.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/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 +15 -57
- package/README.zh-CN.md +16 -59
- 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 -11
- package/dist/browser/index.js +5 -11
- package/dist/browser/mcp.d.ts +9 -18
- package/dist/browser/mcp.js +70 -284
- package/dist/browser/page.d.ts +28 -6
- package/dist/browser/page.js +210 -85
- package/dist/browser.test.js +4 -225
- package/dist/cli-manifest.json +167 -0
- 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/daemon.d.ts +13 -0
- package/dist/daemon.js +187 -0
- package/dist/doctor.d.ts +27 -61
- package/dist/doctor.js +70 -601
- package/dist/doctor.test.js +30 -170
- package/dist/main.js +6 -25
- 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/setup.d.ts +6 -0
- package/dist/setup.js +46 -160
- package/dist/types.d.ts +6 -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 +293 -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 +6 -12
- package/src/browser/mcp.ts +78 -278
- package/src/browser/page.ts +222 -88
- package/src/browser.test.ts +3 -233
- package/src/clis/chatgpt/README.md +1 -1
- package/src/clis/chatgpt/README.zh-CN.md +1 -1
- 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/daemon.ts +217 -0
- package/src/doctor.test.ts +32 -193
- package/src/doctor.ts +74 -668
- package/src/main.ts +6 -23
- 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/setup.ts +47 -183
- package/src/types.ts +1 -0
package/dist/daemon.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opencli micro-daemon — HTTP + WebSocket bridge between CLI and Chrome Extension.
|
|
3
|
+
*
|
|
4
|
+
* Architecture:
|
|
5
|
+
* CLI → HTTP POST /command → daemon → WebSocket → Extension
|
|
6
|
+
* Extension → WebSocket result → daemon → HTTP response → CLI
|
|
7
|
+
*
|
|
8
|
+
* Lifecycle:
|
|
9
|
+
* - Auto-spawned by opencli on first browser command
|
|
10
|
+
* - Auto-exits after 5 minutes of idle
|
|
11
|
+
* - Listens on localhost:19825
|
|
12
|
+
*/
|
|
13
|
+
import { createServer } from 'node:http';
|
|
14
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
15
|
+
const PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? '19825', 10);
|
|
16
|
+
const IDLE_TIMEOUT = 5 * 60 * 1000; // 5 minutes
|
|
17
|
+
// ─── State ───────────────────────────────────────────────────────────
|
|
18
|
+
let extensionWs = null;
|
|
19
|
+
const pending = new Map();
|
|
20
|
+
let idleTimer = null;
|
|
21
|
+
const LOG_BUFFER_SIZE = 200;
|
|
22
|
+
const logBuffer = [];
|
|
23
|
+
function pushLog(entry) {
|
|
24
|
+
logBuffer.push(entry);
|
|
25
|
+
if (logBuffer.length > LOG_BUFFER_SIZE)
|
|
26
|
+
logBuffer.shift();
|
|
27
|
+
}
|
|
28
|
+
// ─── Idle auto-exit ──────────────────────────────────────────────────
|
|
29
|
+
function resetIdleTimer() {
|
|
30
|
+
if (idleTimer)
|
|
31
|
+
clearTimeout(idleTimer);
|
|
32
|
+
idleTimer = setTimeout(() => {
|
|
33
|
+
console.error('[daemon] Idle timeout, shutting down');
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}, IDLE_TIMEOUT);
|
|
36
|
+
}
|
|
37
|
+
// ─── HTTP Server ─────────────────────────────────────────────────────
|
|
38
|
+
function readBody(req) {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const chunks = [];
|
|
41
|
+
req.on('data', (c) => chunks.push(c));
|
|
42
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
43
|
+
req.on('error', reject);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function jsonResponse(res, status, data) {
|
|
47
|
+
res.writeHead(status, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
48
|
+
res.end(JSON.stringify(data));
|
|
49
|
+
}
|
|
50
|
+
async function handleRequest(req, res) {
|
|
51
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
52
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
53
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
54
|
+
if (req.method === 'OPTIONS') {
|
|
55
|
+
res.writeHead(204);
|
|
56
|
+
res.end();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const url = req.url ?? '/';
|
|
60
|
+
const pathname = url.split('?')[0];
|
|
61
|
+
if (req.method === 'GET' && pathname === '/status') {
|
|
62
|
+
jsonResponse(res, 200, {
|
|
63
|
+
ok: true,
|
|
64
|
+
extensionConnected: extensionWs?.readyState === WebSocket.OPEN,
|
|
65
|
+
pending: pending.size,
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (req.method === 'GET' && pathname === '/logs') {
|
|
70
|
+
const params = new URL(url, `http://localhost:${PORT}`).searchParams;
|
|
71
|
+
const level = params.get('level');
|
|
72
|
+
const filtered = level
|
|
73
|
+
? logBuffer.filter(e => e.level === level)
|
|
74
|
+
: logBuffer;
|
|
75
|
+
jsonResponse(res, 200, { ok: true, logs: filtered });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (req.method === 'DELETE' && pathname === '/logs') {
|
|
79
|
+
logBuffer.length = 0;
|
|
80
|
+
jsonResponse(res, 200, { ok: true });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (req.method === 'POST' && url === '/command') {
|
|
84
|
+
resetIdleTimer();
|
|
85
|
+
try {
|
|
86
|
+
const body = JSON.parse(await readBody(req));
|
|
87
|
+
if (!body.id) {
|
|
88
|
+
jsonResponse(res, 400, { ok: false, error: 'Missing command id' });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!extensionWs || extensionWs.readyState !== WebSocket.OPEN) {
|
|
92
|
+
jsonResponse(res, 503, { id: body.id, ok: false, error: 'Extension not connected. Please install the opencli Browser Bridge extension.' });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const result = await new Promise((resolve, reject) => {
|
|
96
|
+
const timer = setTimeout(() => {
|
|
97
|
+
pending.delete(body.id);
|
|
98
|
+
reject(new Error('Command timeout (30s)'));
|
|
99
|
+
}, 30000);
|
|
100
|
+
pending.set(body.id, { resolve, reject, timer });
|
|
101
|
+
extensionWs.send(JSON.stringify(body));
|
|
102
|
+
});
|
|
103
|
+
jsonResponse(res, 200, result);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
jsonResponse(res, err instanceof Error && err.message.includes('timeout') ? 408 : 400, {
|
|
107
|
+
ok: false,
|
|
108
|
+
error: err instanceof Error ? err.message : 'Invalid request',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
jsonResponse(res, 404, { error: 'Not found' });
|
|
114
|
+
}
|
|
115
|
+
// ─── WebSocket for Extension ─────────────────────────────────────────
|
|
116
|
+
const httpServer = createServer((req, res) => { handleRequest(req, res).catch(() => { res.writeHead(500); res.end(); }); });
|
|
117
|
+
const wss = new WebSocketServer({ server: httpServer, path: '/ext' });
|
|
118
|
+
wss.on('connection', (ws) => {
|
|
119
|
+
console.error('[daemon] Extension connected');
|
|
120
|
+
extensionWs = ws;
|
|
121
|
+
ws.on('message', (data) => {
|
|
122
|
+
try {
|
|
123
|
+
const msg = JSON.parse(data.toString());
|
|
124
|
+
// Handle log messages from extension
|
|
125
|
+
if (msg.type === 'log') {
|
|
126
|
+
const prefix = msg.level === 'error' ? '❌' : msg.level === 'warn' ? '⚠️' : '📋';
|
|
127
|
+
console.error(`${prefix} [ext] ${msg.msg}`);
|
|
128
|
+
pushLog({ level: msg.level, msg: msg.msg, ts: msg.ts ?? Date.now() });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
// Handle command results
|
|
132
|
+
const p = pending.get(msg.id);
|
|
133
|
+
if (p) {
|
|
134
|
+
clearTimeout(p.timer);
|
|
135
|
+
pending.delete(msg.id);
|
|
136
|
+
p.resolve(msg);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Ignore malformed messages
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
ws.on('close', () => {
|
|
144
|
+
console.error('[daemon] Extension disconnected');
|
|
145
|
+
if (extensionWs === ws) {
|
|
146
|
+
extensionWs = null;
|
|
147
|
+
// Reject all pending requests since the extension is gone
|
|
148
|
+
for (const [id, p] of pending) {
|
|
149
|
+
clearTimeout(p.timer);
|
|
150
|
+
p.reject(new Error('Extension disconnected'));
|
|
151
|
+
}
|
|
152
|
+
pending.clear();
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
ws.on('error', () => {
|
|
156
|
+
if (extensionWs === ws)
|
|
157
|
+
extensionWs = null;
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
// ─── Start ───────────────────────────────────────────────────────────
|
|
161
|
+
httpServer.listen(PORT, '127.0.0.1', () => {
|
|
162
|
+
console.error(`[daemon] Listening on http://127.0.0.1:${PORT}`);
|
|
163
|
+
resetIdleTimer();
|
|
164
|
+
});
|
|
165
|
+
httpServer.on('error', (err) => {
|
|
166
|
+
if (err.code === 'EADDRINUSE') {
|
|
167
|
+
console.error(`[daemon] Port ${PORT} already in use — another daemon is likely running. Exiting.`);
|
|
168
|
+
process.exit(0);
|
|
169
|
+
}
|
|
170
|
+
console.error('[daemon] Server error:', err.message);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
});
|
|
173
|
+
// Graceful shutdown
|
|
174
|
+
function shutdown() {
|
|
175
|
+
// Reject all pending requests so CLI doesn't hang
|
|
176
|
+
for (const [, p] of pending) {
|
|
177
|
+
clearTimeout(p.timer);
|
|
178
|
+
p.reject(new Error('Daemon shutting down'));
|
|
179
|
+
}
|
|
180
|
+
pending.clear();
|
|
181
|
+
if (extensionWs)
|
|
182
|
+
extensionWs.close();
|
|
183
|
+
httpServer.close();
|
|
184
|
+
process.exit(0);
|
|
185
|
+
}
|
|
186
|
+
process.on('SIGTERM', shutdown);
|
|
187
|
+
process.on('SIGINT', shutdown);
|
package/dist/doctor.d.ts
CHANGED
|
@@ -1,29 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* opencli doctor — diagnose and fix browser connectivity.
|
|
3
|
+
*
|
|
4
|
+
* Simplified for the daemon-based architecture. No more token management,
|
|
5
|
+
* MCP path discovery, or config file scanning.
|
|
6
|
+
*/
|
|
2
7
|
export type DoctorOptions = {
|
|
3
8
|
fix?: boolean;
|
|
4
9
|
yes?: boolean;
|
|
5
10
|
live?: boolean;
|
|
6
|
-
shellRc?: string;
|
|
7
|
-
configPaths?: string[];
|
|
8
|
-
token?: string;
|
|
9
11
|
cliVersion?: string;
|
|
10
12
|
};
|
|
11
|
-
export type ShellFileStatus = {
|
|
12
|
-
path: string;
|
|
13
|
-
exists: boolean;
|
|
14
|
-
token: string | null;
|
|
15
|
-
fingerprint: string | null;
|
|
16
|
-
};
|
|
17
|
-
export type McpConfigFormat = 'json' | 'toml';
|
|
18
|
-
export type McpConfigStatus = {
|
|
19
|
-
path: string;
|
|
20
|
-
exists: boolean;
|
|
21
|
-
format: McpConfigFormat;
|
|
22
|
-
token: string | null;
|
|
23
|
-
fingerprint: string | null;
|
|
24
|
-
writable: boolean;
|
|
25
|
-
parseError?: string;
|
|
26
|
-
};
|
|
27
13
|
export type ConnectivityResult = {
|
|
28
14
|
ok: boolean;
|
|
29
15
|
error?: string;
|
|
@@ -31,57 +17,37 @@ export type ConnectivityResult = {
|
|
|
31
17
|
};
|
|
32
18
|
export type DoctorReport = {
|
|
33
19
|
cliVersion?: string;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
extensionToken: string | null;
|
|
37
|
-
extensionFingerprint: string | null;
|
|
38
|
-
extensionInstalled: boolean;
|
|
39
|
-
extensionBrowsers: string[];
|
|
40
|
-
shellFiles: ShellFileStatus[];
|
|
41
|
-
configs: McpConfigStatus[];
|
|
42
|
-
recommendedToken: string | null;
|
|
43
|
-
recommendedFingerprint: string | null;
|
|
20
|
+
daemonRunning: boolean;
|
|
21
|
+
extensionConnected: boolean;
|
|
44
22
|
connectivity?: ConnectivityResult;
|
|
45
|
-
warnings: string[];
|
|
46
23
|
issues: string[];
|
|
47
24
|
};
|
|
48
|
-
export declare function shortenPath(p: string): string;
|
|
49
|
-
export declare function toolName(p: string): string;
|
|
50
|
-
export declare function getDefaultShellRcPath(): string;
|
|
51
|
-
export declare function getDefaultMcpConfigPaths(cwd?: string): string[];
|
|
52
|
-
export declare function readTokenFromShellContent(content: string): string | null;
|
|
53
|
-
export declare function upsertShellToken(content: string, token: string, filePath?: string): string;
|
|
54
|
-
export declare function upsertJsonConfigToken(content: string, token: string, filePath?: string): string;
|
|
55
|
-
export declare function readTomlConfigToken(content: string): string | null;
|
|
56
|
-
export declare function upsertTomlConfigToken(content: string, token: string): string;
|
|
57
|
-
export declare function fileExists(filePath: string): boolean;
|
|
58
25
|
/**
|
|
59
|
-
*
|
|
60
|
-
* by scanning Chrome's LevelDB localStorage files directly.
|
|
61
|
-
*
|
|
62
|
-
* Reads LevelDB .ldb/.log files as raw binary and searches for the
|
|
63
|
-
* extension ID near base64url token values. This works reliably across
|
|
64
|
-
* platforms because LevelDB's internal encoding can split ASCII strings
|
|
65
|
-
* like "auth-token" and the extension ID across byte boundaries, making
|
|
66
|
-
* text-based tools like `strings` + `grep` unreliable.
|
|
26
|
+
* Test connectivity by attempting a real browser command.
|
|
67
27
|
*/
|
|
28
|
+
export declare function checkConnectivity(opts?: {
|
|
29
|
+
timeout?: number;
|
|
30
|
+
}): Promise<ConnectivityResult>;
|
|
31
|
+
export declare function runBrowserDoctor(opts?: DoctorOptions): Promise<DoctorReport>;
|
|
32
|
+
export declare function renderBrowserDoctorReport(report: DoctorReport): string;
|
|
33
|
+
export declare const PLAYWRIGHT_TOKEN_ENV = "PLAYWRIGHT_MCP_EXTENSION_TOKEN";
|
|
68
34
|
export declare function discoverExtensionToken(): string | null;
|
|
69
|
-
/**
|
|
70
|
-
* Check whether the Playwright MCP Bridge extension is installed in any browser.
|
|
71
|
-
* Scans Chrome/Chromium/Edge Extensions directories for the known extension ID.
|
|
72
|
-
*/
|
|
73
35
|
export declare function checkExtensionInstalled(): {
|
|
74
36
|
installed: boolean;
|
|
75
37
|
browsers: string[];
|
|
76
38
|
};
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
39
|
+
export declare function applyBrowserDoctorFix(): Promise<string[]>;
|
|
40
|
+
export declare function getDefaultShellRcPath(): string;
|
|
41
|
+
export declare function getDefaultMcpConfigPaths(): string[];
|
|
42
|
+
export declare function readTokenFromShellContent(_content: string): string | null;
|
|
43
|
+
export declare function upsertShellToken(content: string): string;
|
|
44
|
+
export declare function upsertJsonConfigToken(content: string): string;
|
|
45
|
+
export declare function readTomlConfigToken(_content: string): string | null;
|
|
46
|
+
export declare function upsertTomlConfigToken(content: string): string;
|
|
47
|
+
export declare function shortenPath(p: string): string;
|
|
48
|
+
export declare function toolName(_p: string): string;
|
|
49
|
+
export declare function fileExists(filePath: string): boolean;
|
|
50
|
+
export declare function writeFileWithMkdir(_p: string, _c: string): void;
|
|
81
51
|
export declare function checkTokenConnectivity(opts?: {
|
|
82
52
|
timeout?: number;
|
|
83
53
|
}): Promise<ConnectivityResult>;
|
|
84
|
-
export declare function runBrowserDoctor(opts?: DoctorOptions): Promise<DoctorReport>;
|
|
85
|
-
export declare function renderBrowserDoctorReport(report: DoctorReport): string;
|
|
86
|
-
export declare function writeFileWithMkdir(filePath: string, content: string): void;
|
|
87
|
-
export declare function applyBrowserDoctorFix(report: DoctorReport, opts?: DoctorOptions): Promise<string[]>;
|