@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
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
//#region src/protocol.ts
|
|
2
|
+
/** Default daemon port */
|
|
3
|
+
var DAEMON_PORT = 19825;
|
|
4
|
+
var DAEMON_HOST = "localhost";
|
|
5
|
+
var DAEMON_WS_URL = `ws://${DAEMON_HOST}:${DAEMON_PORT}/ext`;
|
|
6
|
+
`${DAEMON_HOST}${DAEMON_PORT}`;
|
|
7
|
+
/** Base reconnect delay for extension WebSocket (ms) */
|
|
8
|
+
var WS_RECONNECT_BASE_DELAY = 2e3;
|
|
9
|
+
/** Max reconnect delay (ms) */
|
|
10
|
+
var WS_RECONNECT_MAX_DELAY = 6e4;
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/cdp.ts
|
|
13
|
+
/**
|
|
14
|
+
* CDP execution via chrome.debugger API.
|
|
15
|
+
*
|
|
16
|
+
* chrome.debugger only needs the "debugger" permission — no host_permissions.
|
|
17
|
+
* It can attach to any http/https tab. Avoid chrome:// and chrome-extension://
|
|
18
|
+
* tabs (resolveTabId in background.ts filters them).
|
|
19
|
+
*/
|
|
20
|
+
var attached = /* @__PURE__ */ new Set();
|
|
21
|
+
async function ensureAttached(tabId) {
|
|
22
|
+
if (attached.has(tabId)) return;
|
|
23
|
+
try {
|
|
24
|
+
await chrome.debugger.attach({ tabId }, "1.3");
|
|
25
|
+
} catch (e) {
|
|
26
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
27
|
+
if (msg.includes("Another debugger is already attached")) {
|
|
28
|
+
try {
|
|
29
|
+
await chrome.debugger.detach({ tabId });
|
|
30
|
+
} catch {}
|
|
31
|
+
try {
|
|
32
|
+
await chrome.debugger.attach({ tabId }, "1.3");
|
|
33
|
+
} catch {
|
|
34
|
+
throw new Error(`attach failed: ${msg}`);
|
|
35
|
+
}
|
|
36
|
+
} else throw new Error(`attach failed: ${msg}`);
|
|
37
|
+
}
|
|
38
|
+
attached.add(tabId);
|
|
39
|
+
try {
|
|
40
|
+
await chrome.debugger.sendCommand({ tabId }, "Runtime.enable");
|
|
41
|
+
} catch {}
|
|
42
|
+
}
|
|
43
|
+
async function evaluate(tabId, expression) {
|
|
44
|
+
await ensureAttached(tabId);
|
|
45
|
+
const result = await chrome.debugger.sendCommand({ tabId }, "Runtime.evaluate", {
|
|
46
|
+
expression,
|
|
47
|
+
returnByValue: true,
|
|
48
|
+
awaitPromise: true
|
|
49
|
+
});
|
|
50
|
+
if (result.exceptionDetails) {
|
|
51
|
+
const errMsg = result.exceptionDetails.exception?.description || result.exceptionDetails.text || "Eval error";
|
|
52
|
+
throw new Error(errMsg);
|
|
53
|
+
}
|
|
54
|
+
return result.result?.value;
|
|
55
|
+
}
|
|
56
|
+
var evaluateAsync = evaluate;
|
|
57
|
+
/**
|
|
58
|
+
* Capture a screenshot via CDP Page.captureScreenshot.
|
|
59
|
+
* Returns base64-encoded image data.
|
|
60
|
+
*/
|
|
61
|
+
async function screenshot(tabId, options = {}) {
|
|
62
|
+
await ensureAttached(tabId);
|
|
63
|
+
const format = options.format ?? "png";
|
|
64
|
+
if (options.fullPage) {
|
|
65
|
+
const metrics = await chrome.debugger.sendCommand({ tabId }, "Page.getLayoutMetrics");
|
|
66
|
+
const size = metrics.cssContentSize || metrics.contentSize;
|
|
67
|
+
if (size) await chrome.debugger.sendCommand({ tabId }, "Emulation.setDeviceMetricsOverride", {
|
|
68
|
+
mobile: false,
|
|
69
|
+
width: Math.ceil(size.width),
|
|
70
|
+
height: Math.ceil(size.height),
|
|
71
|
+
deviceScaleFactor: 1
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const params = { format };
|
|
76
|
+
if (format === "jpeg" && options.quality !== void 0) params.quality = Math.max(0, Math.min(100, options.quality));
|
|
77
|
+
return (await chrome.debugger.sendCommand({ tabId }, "Page.captureScreenshot", params)).data;
|
|
78
|
+
} finally {
|
|
79
|
+
if (options.fullPage) await chrome.debugger.sendCommand({ tabId }, "Emulation.clearDeviceMetricsOverride").catch(() => {});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function detach(tabId) {
|
|
83
|
+
if (!attached.has(tabId)) return;
|
|
84
|
+
attached.delete(tabId);
|
|
85
|
+
try {
|
|
86
|
+
chrome.debugger.detach({ tabId });
|
|
87
|
+
} catch {}
|
|
88
|
+
}
|
|
89
|
+
function registerListeners() {
|
|
90
|
+
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
91
|
+
attached.delete(tabId);
|
|
92
|
+
});
|
|
93
|
+
chrome.debugger.onDetach.addListener((source) => {
|
|
94
|
+
if (source.tabId) attached.delete(source.tabId);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
98
|
+
//#region src/background.ts
|
|
99
|
+
var ws = null;
|
|
100
|
+
var reconnectTimer = null;
|
|
101
|
+
var reconnectAttempts = 0;
|
|
102
|
+
var _origLog = console.log.bind(console);
|
|
103
|
+
var _origWarn = console.warn.bind(console);
|
|
104
|
+
var _origError = console.error.bind(console);
|
|
105
|
+
function forwardLog(level, args) {
|
|
106
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
107
|
+
try {
|
|
108
|
+
const msg = args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
|
|
109
|
+
ws.send(JSON.stringify({
|
|
110
|
+
type: "log",
|
|
111
|
+
level,
|
|
112
|
+
msg,
|
|
113
|
+
ts: Date.now()
|
|
114
|
+
}));
|
|
115
|
+
} catch {}
|
|
116
|
+
}
|
|
117
|
+
console.log = (...args) => {
|
|
118
|
+
_origLog(...args);
|
|
119
|
+
forwardLog("info", args);
|
|
120
|
+
};
|
|
121
|
+
console.warn = (...args) => {
|
|
122
|
+
_origWarn(...args);
|
|
123
|
+
forwardLog("warn", args);
|
|
124
|
+
};
|
|
125
|
+
console.error = (...args) => {
|
|
126
|
+
_origError(...args);
|
|
127
|
+
forwardLog("error", args);
|
|
128
|
+
};
|
|
129
|
+
function connect() {
|
|
130
|
+
if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING) return;
|
|
131
|
+
try {
|
|
132
|
+
ws = new WebSocket(DAEMON_WS_URL);
|
|
133
|
+
} catch {
|
|
134
|
+
scheduleReconnect();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
ws.onopen = () => {
|
|
138
|
+
console.log("[opencli] Connected to daemon");
|
|
139
|
+
reconnectAttempts = 0;
|
|
140
|
+
if (reconnectTimer) {
|
|
141
|
+
clearTimeout(reconnectTimer);
|
|
142
|
+
reconnectTimer = null;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
ws.onmessage = async (event) => {
|
|
146
|
+
try {
|
|
147
|
+
const result = await handleCommand(JSON.parse(event.data));
|
|
148
|
+
ws?.send(JSON.stringify(result));
|
|
149
|
+
} catch (err) {
|
|
150
|
+
console.error("[opencli] Message handling error:", err);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
ws.onclose = () => {
|
|
154
|
+
console.log("[opencli] Disconnected from daemon");
|
|
155
|
+
ws = null;
|
|
156
|
+
scheduleReconnect();
|
|
157
|
+
};
|
|
158
|
+
ws.onerror = () => {
|
|
159
|
+
ws?.close();
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function scheduleReconnect() {
|
|
163
|
+
if (reconnectTimer) return;
|
|
164
|
+
reconnectAttempts++;
|
|
165
|
+
const delay = Math.min(WS_RECONNECT_BASE_DELAY * Math.pow(2, reconnectAttempts - 1), WS_RECONNECT_MAX_DELAY);
|
|
166
|
+
reconnectTimer = setTimeout(() => {
|
|
167
|
+
reconnectTimer = null;
|
|
168
|
+
connect();
|
|
169
|
+
}, delay);
|
|
170
|
+
}
|
|
171
|
+
var automationWindowId = null;
|
|
172
|
+
var windowIdleTimer = null;
|
|
173
|
+
var WINDOW_IDLE_TIMEOUT = 3e4;
|
|
174
|
+
function resetWindowIdleTimer() {
|
|
175
|
+
if (windowIdleTimer) clearTimeout(windowIdleTimer);
|
|
176
|
+
windowIdleTimer = setTimeout(async () => {
|
|
177
|
+
if (automationWindowId !== null) {
|
|
178
|
+
try {
|
|
179
|
+
await chrome.windows.remove(automationWindowId);
|
|
180
|
+
console.log(`[opencli] Automation window ${automationWindowId} closed (idle timeout)`);
|
|
181
|
+
} catch {}
|
|
182
|
+
automationWindowId = null;
|
|
183
|
+
}
|
|
184
|
+
windowIdleTimer = null;
|
|
185
|
+
}, WINDOW_IDLE_TIMEOUT);
|
|
186
|
+
}
|
|
187
|
+
/** Get or create the dedicated automation window. */
|
|
188
|
+
async function getAutomationWindow() {
|
|
189
|
+
if (automationWindowId !== null) try {
|
|
190
|
+
await chrome.windows.get(automationWindowId);
|
|
191
|
+
return automationWindowId;
|
|
192
|
+
} catch {
|
|
193
|
+
automationWindowId = null;
|
|
194
|
+
}
|
|
195
|
+
automationWindowId = (await chrome.windows.create({
|
|
196
|
+
url: "about:blank",
|
|
197
|
+
focused: false,
|
|
198
|
+
width: 1280,
|
|
199
|
+
height: 900,
|
|
200
|
+
type: "normal"
|
|
201
|
+
})).id;
|
|
202
|
+
console.log(`[opencli] Created automation window ${automationWindowId}`);
|
|
203
|
+
return automationWindowId;
|
|
204
|
+
}
|
|
205
|
+
chrome.windows.onRemoved.addListener((windowId) => {
|
|
206
|
+
if (windowId === automationWindowId) {
|
|
207
|
+
console.log("[opencli] Automation window closed");
|
|
208
|
+
automationWindowId = null;
|
|
209
|
+
if (windowIdleTimer) {
|
|
210
|
+
clearTimeout(windowIdleTimer);
|
|
211
|
+
windowIdleTimer = null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
var initialized = false;
|
|
216
|
+
function initialize() {
|
|
217
|
+
if (initialized) return;
|
|
218
|
+
initialized = true;
|
|
219
|
+
chrome.alarms.create("keepalive", { periodInMinutes: .4 });
|
|
220
|
+
registerListeners();
|
|
221
|
+
connect();
|
|
222
|
+
console.log("[opencli] Browser Bridge extension initialized");
|
|
223
|
+
}
|
|
224
|
+
chrome.runtime.onInstalled.addListener(() => {
|
|
225
|
+
initialize();
|
|
226
|
+
});
|
|
227
|
+
chrome.runtime.onStartup.addListener(() => {
|
|
228
|
+
initialize();
|
|
229
|
+
});
|
|
230
|
+
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
231
|
+
if (alarm.name === "keepalive") connect();
|
|
232
|
+
});
|
|
233
|
+
async function handleCommand(cmd) {
|
|
234
|
+
resetWindowIdleTimer();
|
|
235
|
+
try {
|
|
236
|
+
switch (cmd.action) {
|
|
237
|
+
case "exec": return await handleExec(cmd);
|
|
238
|
+
case "navigate": return await handleNavigate(cmd);
|
|
239
|
+
case "tabs": return await handleTabs(cmd);
|
|
240
|
+
case "cookies": return await handleCookies(cmd);
|
|
241
|
+
case "screenshot": return await handleScreenshot(cmd);
|
|
242
|
+
case "close-window": return await handleCloseWindow(cmd);
|
|
243
|
+
default: return {
|
|
244
|
+
id: cmd.id,
|
|
245
|
+
ok: false,
|
|
246
|
+
error: `Unknown action: ${cmd.action}`
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
} catch (err) {
|
|
250
|
+
return {
|
|
251
|
+
id: cmd.id,
|
|
252
|
+
ok: false,
|
|
253
|
+
error: err instanceof Error ? err.message : String(err)
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/** Check if a URL is a debuggable web page (not chrome:// or extension page) */
|
|
258
|
+
function isWebUrl(url) {
|
|
259
|
+
if (!url) return false;
|
|
260
|
+
return !url.startsWith("chrome://") && !url.startsWith("chrome-extension://");
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Resolve target tab in the automation window.
|
|
264
|
+
* If explicit tabId is given, use that directly.
|
|
265
|
+
* Otherwise, find or create a tab in the dedicated automation window.
|
|
266
|
+
*/
|
|
267
|
+
async function resolveTabId(tabId) {
|
|
268
|
+
if (tabId !== void 0) return tabId;
|
|
269
|
+
const windowId = await getAutomationWindow();
|
|
270
|
+
const tabs = await chrome.tabs.query({ windowId });
|
|
271
|
+
const webTab = tabs.find((t) => t.id && isWebUrl(t.url));
|
|
272
|
+
if (webTab?.id) return webTab.id;
|
|
273
|
+
if (tabs.length > 0 && tabs[0]?.id) return tabs[0].id;
|
|
274
|
+
const newTab = await chrome.tabs.create({
|
|
275
|
+
windowId,
|
|
276
|
+
url: "about:blank",
|
|
277
|
+
active: true
|
|
278
|
+
});
|
|
279
|
+
if (!newTab.id) throw new Error("Failed to create tab in automation window");
|
|
280
|
+
return newTab.id;
|
|
281
|
+
}
|
|
282
|
+
async function handleExec(cmd) {
|
|
283
|
+
if (!cmd.code) return {
|
|
284
|
+
id: cmd.id,
|
|
285
|
+
ok: false,
|
|
286
|
+
error: "Missing code"
|
|
287
|
+
};
|
|
288
|
+
const tabId = await resolveTabId(cmd.tabId);
|
|
289
|
+
try {
|
|
290
|
+
const data = await evaluateAsync(tabId, cmd.code);
|
|
291
|
+
return {
|
|
292
|
+
id: cmd.id,
|
|
293
|
+
ok: true,
|
|
294
|
+
data
|
|
295
|
+
};
|
|
296
|
+
} catch (err) {
|
|
297
|
+
return {
|
|
298
|
+
id: cmd.id,
|
|
299
|
+
ok: false,
|
|
300
|
+
error: err instanceof Error ? err.message : String(err)
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async function handleNavigate(cmd) {
|
|
305
|
+
if (!cmd.url) return {
|
|
306
|
+
id: cmd.id,
|
|
307
|
+
ok: false,
|
|
308
|
+
error: "Missing url"
|
|
309
|
+
};
|
|
310
|
+
const tabId = await resolveTabId(cmd.tabId);
|
|
311
|
+
await chrome.tabs.update(tabId, { url: cmd.url });
|
|
312
|
+
await new Promise((resolve) => {
|
|
313
|
+
chrome.tabs.get(tabId).then((tab) => {
|
|
314
|
+
if (tab.status === "complete") {
|
|
315
|
+
resolve();
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const listener = (id, info) => {
|
|
319
|
+
if (id === tabId && info.status === "complete") {
|
|
320
|
+
chrome.tabs.onUpdated.removeListener(listener);
|
|
321
|
+
resolve();
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
chrome.tabs.onUpdated.addListener(listener);
|
|
325
|
+
setTimeout(() => {
|
|
326
|
+
chrome.tabs.onUpdated.removeListener(listener);
|
|
327
|
+
resolve();
|
|
328
|
+
}, 15e3);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
const tab = await chrome.tabs.get(tabId);
|
|
332
|
+
return {
|
|
333
|
+
id: cmd.id,
|
|
334
|
+
ok: true,
|
|
335
|
+
data: {
|
|
336
|
+
title: tab.title,
|
|
337
|
+
url: tab.url,
|
|
338
|
+
tabId
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
async function handleTabs(cmd) {
|
|
343
|
+
switch (cmd.op) {
|
|
344
|
+
case "list": {
|
|
345
|
+
const data = (await chrome.tabs.query({})).filter((t) => isWebUrl(t.url)).map((t, i) => ({
|
|
346
|
+
index: i,
|
|
347
|
+
tabId: t.id,
|
|
348
|
+
url: t.url,
|
|
349
|
+
title: t.title,
|
|
350
|
+
active: t.active
|
|
351
|
+
}));
|
|
352
|
+
return {
|
|
353
|
+
id: cmd.id,
|
|
354
|
+
ok: true,
|
|
355
|
+
data
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
case "new": {
|
|
359
|
+
const tab = await chrome.tabs.create({
|
|
360
|
+
url: cmd.url,
|
|
361
|
+
active: true
|
|
362
|
+
});
|
|
363
|
+
return {
|
|
364
|
+
id: cmd.id,
|
|
365
|
+
ok: true,
|
|
366
|
+
data: {
|
|
367
|
+
tabId: tab.id,
|
|
368
|
+
url: tab.url
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
case "close": {
|
|
373
|
+
if (cmd.index !== void 0) {
|
|
374
|
+
const target = (await chrome.tabs.query({}))[cmd.index];
|
|
375
|
+
if (!target?.id) return {
|
|
376
|
+
id: cmd.id,
|
|
377
|
+
ok: false,
|
|
378
|
+
error: `Tab index ${cmd.index} not found`
|
|
379
|
+
};
|
|
380
|
+
await chrome.tabs.remove(target.id);
|
|
381
|
+
detach(target.id);
|
|
382
|
+
return {
|
|
383
|
+
id: cmd.id,
|
|
384
|
+
ok: true,
|
|
385
|
+
data: { closed: target.id }
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
const tabId = await resolveTabId(cmd.tabId);
|
|
389
|
+
await chrome.tabs.remove(tabId);
|
|
390
|
+
detach(tabId);
|
|
391
|
+
return {
|
|
392
|
+
id: cmd.id,
|
|
393
|
+
ok: true,
|
|
394
|
+
data: { closed: tabId }
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
case "select": {
|
|
398
|
+
if (cmd.index === void 0 && cmd.tabId === void 0) return {
|
|
399
|
+
id: cmd.id,
|
|
400
|
+
ok: false,
|
|
401
|
+
error: "Missing index or tabId"
|
|
402
|
+
};
|
|
403
|
+
if (cmd.tabId !== void 0) {
|
|
404
|
+
await chrome.tabs.update(cmd.tabId, { active: true });
|
|
405
|
+
return {
|
|
406
|
+
id: cmd.id,
|
|
407
|
+
ok: true,
|
|
408
|
+
data: { selected: cmd.tabId }
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
const target = (await chrome.tabs.query({}))[cmd.index];
|
|
412
|
+
if (!target?.id) return {
|
|
413
|
+
id: cmd.id,
|
|
414
|
+
ok: false,
|
|
415
|
+
error: `Tab index ${cmd.index} not found`
|
|
416
|
+
};
|
|
417
|
+
await chrome.tabs.update(target.id, { active: true });
|
|
418
|
+
return {
|
|
419
|
+
id: cmd.id,
|
|
420
|
+
ok: true,
|
|
421
|
+
data: { selected: target.id }
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
default: return {
|
|
425
|
+
id: cmd.id,
|
|
426
|
+
ok: false,
|
|
427
|
+
error: `Unknown tabs op: ${cmd.op}`
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
async function handleCookies(cmd) {
|
|
432
|
+
const details = {};
|
|
433
|
+
if (cmd.domain) details.domain = cmd.domain;
|
|
434
|
+
if (cmd.url) details.url = cmd.url;
|
|
435
|
+
const data = (await chrome.cookies.getAll(details)).map((c) => ({
|
|
436
|
+
name: c.name,
|
|
437
|
+
value: c.value,
|
|
438
|
+
domain: c.domain,
|
|
439
|
+
path: c.path,
|
|
440
|
+
secure: c.secure,
|
|
441
|
+
httpOnly: c.httpOnly,
|
|
442
|
+
expirationDate: c.expirationDate
|
|
443
|
+
}));
|
|
444
|
+
return {
|
|
445
|
+
id: cmd.id,
|
|
446
|
+
ok: true,
|
|
447
|
+
data
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
async function handleScreenshot(cmd) {
|
|
451
|
+
const tabId = await resolveTabId(cmd.tabId);
|
|
452
|
+
try {
|
|
453
|
+
const data = await screenshot(tabId, {
|
|
454
|
+
format: cmd.format,
|
|
455
|
+
quality: cmd.quality,
|
|
456
|
+
fullPage: cmd.fullPage
|
|
457
|
+
});
|
|
458
|
+
return {
|
|
459
|
+
id: cmd.id,
|
|
460
|
+
ok: true,
|
|
461
|
+
data
|
|
462
|
+
};
|
|
463
|
+
} catch (err) {
|
|
464
|
+
return {
|
|
465
|
+
id: cmd.id,
|
|
466
|
+
ok: false,
|
|
467
|
+
error: err instanceof Error ? err.message : String(err)
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
async function handleCloseWindow(cmd) {
|
|
472
|
+
if (automationWindowId !== null) {
|
|
473
|
+
try {
|
|
474
|
+
await chrome.windows.remove(automationWindowId);
|
|
475
|
+
} catch {}
|
|
476
|
+
automationWindowId = null;
|
|
477
|
+
}
|
|
478
|
+
return {
|
|
479
|
+
id: cmd.id,
|
|
480
|
+
ok: true,
|
|
481
|
+
data: { closed: true }
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
//#endregion
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": 3,
|
|
3
|
+
"name": "opencli Browser Bridge",
|
|
4
|
+
"version": "0.2.0",
|
|
5
|
+
"description": "Bridge between opencli CLI and your browser — execute commands, read cookies, manage tabs.",
|
|
6
|
+
"permissions": [
|
|
7
|
+
"debugger",
|
|
8
|
+
"tabs",
|
|
9
|
+
"cookies",
|
|
10
|
+
"activeTab",
|
|
11
|
+
"alarms"
|
|
12
|
+
],
|
|
13
|
+
"background": {
|
|
14
|
+
"service_worker": "dist/background.js",
|
|
15
|
+
"type": "module"
|
|
16
|
+
},
|
|
17
|
+
"icons": {
|
|
18
|
+
"16": "icons/icon-16.png",
|
|
19
|
+
"32": "icons/icon-32.png",
|
|
20
|
+
"48": "icons/icon-48.png",
|
|
21
|
+
"128": "icons/icon-128.png"
|
|
22
|
+
},
|
|
23
|
+
"action": {
|
|
24
|
+
"default_title": "opencli Browser Bridge",
|
|
25
|
+
"default_icon": {
|
|
26
|
+
"16": "icons/icon-16.png",
|
|
27
|
+
"32": "icons/icon-32.png"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"homepage_url": "https://github.com/jackwener/opencli"
|
|
31
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencli-extension",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite build --watch",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"typecheck": "tsc --noEmit"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@types/chrome": "^0.0.287",
|
|
13
|
+
"typescript": "^5.7.0",
|
|
14
|
+
"vite": "^6.0.0"
|
|
15
|
+
}
|
|
16
|
+
}
|