@jackwener/opencli 1.0.0 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/build-extension.yml +62 -0
- package/.github/workflows/ci.yml +6 -6
- package/.github/workflows/e2e-headed.yml +2 -2
- package/.github/workflows/pkg-pr-new.yml +2 -2
- package/.github/workflows/release.yml +2 -5
- package/.github/workflows/security.yml +2 -2
- package/CDP.md +1 -1
- package/CDP.zh-CN.md +1 -1
- package/README.md +35 -8
- package/README.zh-CN.md +35 -8
- package/SKILL.md +3 -5
- package/dist/browser/cdp.d.ts +27 -0
- package/dist/browser/cdp.js +295 -0
- package/dist/browser/daemon-client.d.ts +1 -1
- package/dist/browser/index.d.ts +4 -2
- package/dist/browser/index.js +5 -5
- package/dist/browser/mcp.d.ts +5 -8
- package/dist/browser/mcp.js +9 -10
- package/dist/browser/page.d.ts +8 -1
- package/dist/browser/page.js +25 -40
- package/dist/browser/utils.d.ts +10 -0
- package/dist/browser/utils.js +27 -0
- package/dist/browser.test.js +48 -7
- package/dist/chaoxing.d.ts +58 -0
- package/dist/chaoxing.js +225 -0
- package/dist/chaoxing.test.d.ts +1 -0
- package/dist/chaoxing.test.js +38 -0
- package/dist/cli-manifest.json +597 -14
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +197 -0
- 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/boss/chatlist.d.ts +1 -0
- package/dist/clis/boss/chatlist.js +50 -0
- package/dist/clis/boss/chatmsg.d.ts +1 -0
- package/dist/clis/boss/chatmsg.js +73 -0
- package/dist/clis/boss/send.d.ts +1 -0
- package/dist/clis/boss/send.js +176 -0
- package/dist/clis/chaoxing/assignments.d.ts +1 -0
- package/dist/clis/chaoxing/assignments.js +74 -0
- package/dist/clis/chaoxing/exams.d.ts +1 -0
- package/dist/clis/chaoxing/exams.js +74 -0
- package/dist/clis/chatgpt/ask.js +15 -14
- package/dist/clis/chatgpt/ax.d.ts +1 -0
- package/dist/clis/chatgpt/ax.js +78 -0
- package/dist/clis/chatgpt/read.js +5 -6
- package/dist/clis/chatwise/history.js +18 -1
- package/dist/clis/discord-app/channels.js +33 -21
- 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/post.js +9 -2
- 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 +30 -11
- 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/clis/xiaohongshu/download.d.ts +1 -1
- package/dist/clis/xiaohongshu/download.js +1 -1
- package/dist/daemon.js +2 -2
- package/dist/doctor.d.ts +0 -21
- package/dist/doctor.js +2 -24
- package/dist/engine.js +24 -13
- package/dist/explore.js +46 -101
- package/dist/main.js +4 -203
- package/dist/output.d.ts +1 -1
- package/dist/registry.d.ts +3 -3
- package/dist/runtime.d.ts +1 -4
- package/dist/runtime.js +1 -4
- package/dist/scripts/framework.d.ts +4 -0
- package/dist/scripts/framework.js +21 -0
- package/dist/scripts/interact.d.ts +4 -0
- package/dist/scripts/interact.js +20 -0
- package/dist/scripts/store.d.ts +9 -0
- package/dist/scripts/store.js +44 -0
- package/dist/setup.js +2 -2
- package/dist/synthesize.js +1 -1
- package/extension/dist/background.js +392 -0
- package/extension/manifest.json +3 -3
- package/extension/package.json +1 -1
- package/extension/src/background.ts +101 -24
- package/extension/src/protocol.ts +1 -1
- package/package.json +1 -1
- package/src/browser/cdp.ts +295 -0
- package/src/browser/daemon-client.ts +1 -1
- package/src/browser/index.ts +5 -6
- package/src/browser/mcp.ts +14 -15
- package/src/browser/page.ts +25 -41
- package/src/browser/utils.ts +27 -0
- package/src/browser.test.ts +52 -6
- package/src/chaoxing.test.ts +45 -0
- package/src/chaoxing.ts +268 -0
- package/src/cli.ts +185 -0
- package/src/clis/antigravity/SKILL.md +5 -0
- 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/boss/chatlist.ts +50 -0
- package/src/clis/boss/chatmsg.ts +70 -0
- package/src/clis/boss/send.ts +193 -0
- package/src/clis/chaoxing/README.md +36 -0
- package/src/clis/chaoxing/README.zh-CN.md +35 -0
- package/src/clis/chaoxing/assignments.ts +88 -0
- package/src/clis/chaoxing/exams.ts +88 -0
- package/src/clis/chatgpt/ask.ts +14 -15
- package/src/clis/chatgpt/ax.ts +81 -0
- package/src/clis/chatgpt/read.ts +5 -7
- package/src/clis/chatwise/history.ts +15 -1
- package/src/clis/discord-app/channels.ts +33 -21
- 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/post.ts +9 -2
- package/src/clis/twitter/reply-dm.ts +193 -0
- package/src/clis/twitter/search.ts +34 -12
- 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/clis/xiaohongshu/download.ts +1 -1
- package/src/daemon.ts +2 -2
- package/src/doctor.ts +2 -19
- package/src/engine.ts +20 -13
- package/src/explore.ts +51 -100
- package/src/main.ts +4 -186
- package/src/output.ts +12 -12
- package/src/registry.ts +3 -3
- package/src/runtime.ts +2 -6
- package/src/scripts/framework.ts +20 -0
- package/src/scripts/interact.ts +22 -0
- package/src/scripts/store.ts +40 -0
- package/src/setup.ts +2 -2
- package/src/synthesize.ts +1 -1
- 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,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Injected script for detecting frontend frameworks (Vue, React, Next, Nuxt, etc.)
|
|
3
|
+
*/
|
|
4
|
+
export function detectFramework() {
|
|
5
|
+
const r = {};
|
|
6
|
+
try {
|
|
7
|
+
const app = document.querySelector('#app');
|
|
8
|
+
r.vue3 = !!(app && app.__vue_app__);
|
|
9
|
+
r.vue2 = !!(app && app.__vue__);
|
|
10
|
+
r.react = !!window.__REACT_DEVTOOLS_GLOBAL_HOOK__ || !!document.querySelector('[data-reactroot]');
|
|
11
|
+
r.nextjs = !!window.__NEXT_DATA__;
|
|
12
|
+
r.nuxt = !!window.__NUXT__;
|
|
13
|
+
if (r.vue3 && app.__vue_app__) {
|
|
14
|
+
const gp = app.__vue_app__.config?.globalProperties;
|
|
15
|
+
r.pinia = !!(gp && gp.$pinia);
|
|
16
|
+
r.vuex = !!(gp && gp.$store);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch { }
|
|
20
|
+
return r;
|
|
21
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Injected script for interactive fuzzing (clicking elements to trigger lazy loading)
|
|
3
|
+
*/
|
|
4
|
+
export async function interactFuzz() {
|
|
5
|
+
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
6
|
+
const clickables = Array.from(document.querySelectorAll('button, [role="button"], [role="tab"], .tab, .btn, a[href="javascript:void(0)"], a[href="#"]')).slice(0, 15); // limit to a small number to avoid endless loops
|
|
7
|
+
let clicked = 0;
|
|
8
|
+
for (const el of clickables) {
|
|
9
|
+
try {
|
|
10
|
+
const rect = el.getBoundingClientRect();
|
|
11
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
12
|
+
el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
|
|
13
|
+
clicked++;
|
|
14
|
+
await sleep(300); // give it time to trigger network
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
catch { }
|
|
18
|
+
}
|
|
19
|
+
return clicked;
|
|
20
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Injected script for discovering Pinia or Vuex stores and their actions/state representations
|
|
3
|
+
*/
|
|
4
|
+
export function discoverStores() {
|
|
5
|
+
const stores = [];
|
|
6
|
+
try {
|
|
7
|
+
const app = document.querySelector('#app');
|
|
8
|
+
if (!app?.__vue_app__)
|
|
9
|
+
return stores;
|
|
10
|
+
const gp = app.__vue_app__.config?.globalProperties;
|
|
11
|
+
// Pinia stores
|
|
12
|
+
const pinia = gp?.$pinia;
|
|
13
|
+
if (pinia?._s) {
|
|
14
|
+
pinia._s.forEach((store, id) => {
|
|
15
|
+
const actions = [];
|
|
16
|
+
const stateKeys = [];
|
|
17
|
+
for (const k in store) {
|
|
18
|
+
try {
|
|
19
|
+
if (k.startsWith('$') || k.startsWith('_'))
|
|
20
|
+
continue;
|
|
21
|
+
if (typeof store[k] === 'function')
|
|
22
|
+
actions.push(k);
|
|
23
|
+
else
|
|
24
|
+
stateKeys.push(k);
|
|
25
|
+
}
|
|
26
|
+
catch { }
|
|
27
|
+
}
|
|
28
|
+
stores.push({ type: 'pinia', id, actions: actions.slice(0, 20), stateKeys: stateKeys.slice(0, 15) });
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
// Vuex store modules
|
|
32
|
+
const vuex = gp?.$store;
|
|
33
|
+
if (vuex?._modules?.root?._children) {
|
|
34
|
+
const children = vuex._modules.root._children;
|
|
35
|
+
for (const [modName, mod] of Object.entries(children)) {
|
|
36
|
+
const actions = Object.keys(mod._rawModule?.actions ?? {}).slice(0, 20);
|
|
37
|
+
const stateKeys = Object.keys(mod.state ?? {}).slice(0, 15);
|
|
38
|
+
stores.push({ type: 'vuex', id: modName, actions, stateKeys });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch { }
|
|
43
|
+
return stores;
|
|
44
|
+
}
|
package/dist/setup.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
import { checkDaemonStatus } from './browser/discover.js';
|
|
9
9
|
import { checkConnectivity } from './doctor.js';
|
|
10
|
-
import {
|
|
10
|
+
import { BrowserBridge } from './browser/index.js';
|
|
11
11
|
export async function runSetup(opts = {}) {
|
|
12
12
|
console.log();
|
|
13
13
|
console.log(chalk.bold(' opencli setup') + chalk.dim(' — browser bridge configuration'));
|
|
@@ -23,7 +23,7 @@ export async function runSetup(opts = {}) {
|
|
|
23
23
|
console.log(chalk.dim(' The daemon starts automatically when you run a browser command.'));
|
|
24
24
|
console.log(chalk.dim(' Starting daemon now...'));
|
|
25
25
|
// Try to spawn daemon
|
|
26
|
-
const mcp = new
|
|
26
|
+
const mcp = new BrowserBridge();
|
|
27
27
|
try {
|
|
28
28
|
await mcp.connect({ timeout: 5 });
|
|
29
29
|
await mcp.close();
|
package/dist/synthesize.js
CHANGED
|
@@ -108,7 +108,7 @@ function buildEvaluateScript(url, itemPath, endpoint) {
|
|
|
108
108
|
}
|
|
109
109
|
return [
|
|
110
110
|
'(async () => {',
|
|
111
|
-
` const res = await fetch(
|
|
111
|
+
` const res = await fetch(${JSON.stringify(url)}, {`,
|
|
112
112
|
` credentials: 'include'`,
|
|
113
113
|
' });',
|
|
114
114
|
' const data = await res.json();',
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
const DAEMON_PORT = 19825;
|
|
2
|
+
const DAEMON_HOST = "localhost";
|
|
3
|
+
const DAEMON_WS_URL = `ws://${DAEMON_HOST}:${DAEMON_PORT}/ext`;
|
|
4
|
+
const WS_RECONNECT_BASE_DELAY = 2e3;
|
|
5
|
+
const WS_RECONNECT_MAX_DELAY = 6e4;
|
|
6
|
+
|
|
7
|
+
const attached = /* @__PURE__ */ new Set();
|
|
8
|
+
async function ensureAttached(tabId) {
|
|
9
|
+
if (attached.has(tabId)) return;
|
|
10
|
+
try {
|
|
11
|
+
await chrome.debugger.attach({ tabId }, "1.3");
|
|
12
|
+
} catch (e) {
|
|
13
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
14
|
+
if (msg.includes("Another debugger is already attached")) {
|
|
15
|
+
try {
|
|
16
|
+
await chrome.debugger.detach({ tabId });
|
|
17
|
+
} catch {
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
await chrome.debugger.attach({ tabId }, "1.3");
|
|
21
|
+
} catch {
|
|
22
|
+
throw new Error(`attach failed: ${msg}`);
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
throw new Error(`attach failed: ${msg}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
attached.add(tabId);
|
|
29
|
+
try {
|
|
30
|
+
await chrome.debugger.sendCommand({ tabId }, "Runtime.enable");
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function evaluate(tabId, expression) {
|
|
35
|
+
await ensureAttached(tabId);
|
|
36
|
+
const result = await chrome.debugger.sendCommand({ tabId }, "Runtime.evaluate", {
|
|
37
|
+
expression,
|
|
38
|
+
returnByValue: true,
|
|
39
|
+
awaitPromise: true
|
|
40
|
+
});
|
|
41
|
+
if (result.exceptionDetails) {
|
|
42
|
+
const errMsg = result.exceptionDetails.exception?.description || result.exceptionDetails.text || "Eval error";
|
|
43
|
+
throw new Error(errMsg);
|
|
44
|
+
}
|
|
45
|
+
return result.result?.value;
|
|
46
|
+
}
|
|
47
|
+
const evaluateAsync = evaluate;
|
|
48
|
+
async function screenshot(tabId, options = {}) {
|
|
49
|
+
await ensureAttached(tabId);
|
|
50
|
+
const format = options.format ?? "png";
|
|
51
|
+
if (options.fullPage) {
|
|
52
|
+
const metrics = await chrome.debugger.sendCommand({ tabId }, "Page.getLayoutMetrics");
|
|
53
|
+
const size = metrics.cssContentSize || metrics.contentSize;
|
|
54
|
+
if (size) {
|
|
55
|
+
await chrome.debugger.sendCommand({ tabId }, "Emulation.setDeviceMetricsOverride", {
|
|
56
|
+
mobile: false,
|
|
57
|
+
width: Math.ceil(size.width),
|
|
58
|
+
height: Math.ceil(size.height),
|
|
59
|
+
deviceScaleFactor: 1
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const params = { format };
|
|
65
|
+
if (format === "jpeg" && options.quality !== void 0) {
|
|
66
|
+
params.quality = Math.max(0, Math.min(100, options.quality));
|
|
67
|
+
}
|
|
68
|
+
const result = await chrome.debugger.sendCommand({ tabId }, "Page.captureScreenshot", params);
|
|
69
|
+
return result.data;
|
|
70
|
+
} finally {
|
|
71
|
+
if (options.fullPage) {
|
|
72
|
+
await chrome.debugger.sendCommand({ tabId }, "Emulation.clearDeviceMetricsOverride").catch(() => {
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function detach(tabId) {
|
|
78
|
+
if (!attached.has(tabId)) return;
|
|
79
|
+
attached.delete(tabId);
|
|
80
|
+
try {
|
|
81
|
+
chrome.debugger.detach({ tabId });
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function registerListeners() {
|
|
86
|
+
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
87
|
+
attached.delete(tabId);
|
|
88
|
+
});
|
|
89
|
+
chrome.debugger.onDetach.addListener((source) => {
|
|
90
|
+
if (source.tabId) attached.delete(source.tabId);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let ws = null;
|
|
95
|
+
let reconnectTimer = null;
|
|
96
|
+
let reconnectAttempts = 0;
|
|
97
|
+
const _origLog = console.log.bind(console);
|
|
98
|
+
const _origWarn = console.warn.bind(console);
|
|
99
|
+
const _origError = console.error.bind(console);
|
|
100
|
+
function forwardLog(level, args) {
|
|
101
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
102
|
+
try {
|
|
103
|
+
const msg = args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
|
|
104
|
+
ws.send(JSON.stringify({ type: "log", level, msg, ts: Date.now() }));
|
|
105
|
+
} catch {
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
console.log = (...args) => {
|
|
109
|
+
_origLog(...args);
|
|
110
|
+
forwardLog("info", args);
|
|
111
|
+
};
|
|
112
|
+
console.warn = (...args) => {
|
|
113
|
+
_origWarn(...args);
|
|
114
|
+
forwardLog("warn", args);
|
|
115
|
+
};
|
|
116
|
+
console.error = (...args) => {
|
|
117
|
+
_origError(...args);
|
|
118
|
+
forwardLog("error", args);
|
|
119
|
+
};
|
|
120
|
+
function connect() {
|
|
121
|
+
if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING) return;
|
|
122
|
+
try {
|
|
123
|
+
ws = new WebSocket(DAEMON_WS_URL);
|
|
124
|
+
} catch {
|
|
125
|
+
scheduleReconnect();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
ws.onopen = () => {
|
|
129
|
+
console.log("[opencli] Connected to daemon");
|
|
130
|
+
reconnectAttempts = 0;
|
|
131
|
+
if (reconnectTimer) {
|
|
132
|
+
clearTimeout(reconnectTimer);
|
|
133
|
+
reconnectTimer = null;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
ws.onmessage = async (event) => {
|
|
137
|
+
try {
|
|
138
|
+
const command = JSON.parse(event.data);
|
|
139
|
+
const result = await handleCommand(command);
|
|
140
|
+
ws?.send(JSON.stringify(result));
|
|
141
|
+
} catch (err) {
|
|
142
|
+
console.error("[opencli] Message handling error:", err);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
ws.onclose = () => {
|
|
146
|
+
console.log("[opencli] Disconnected from daemon");
|
|
147
|
+
ws = null;
|
|
148
|
+
scheduleReconnect();
|
|
149
|
+
};
|
|
150
|
+
ws.onerror = () => {
|
|
151
|
+
ws?.close();
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function scheduleReconnect() {
|
|
155
|
+
if (reconnectTimer) return;
|
|
156
|
+
reconnectAttempts++;
|
|
157
|
+
const delay = Math.min(WS_RECONNECT_BASE_DELAY * Math.pow(2, reconnectAttempts - 1), WS_RECONNECT_MAX_DELAY);
|
|
158
|
+
reconnectTimer = setTimeout(() => {
|
|
159
|
+
reconnectTimer = null;
|
|
160
|
+
connect();
|
|
161
|
+
}, delay);
|
|
162
|
+
}
|
|
163
|
+
let automationWindowId = null;
|
|
164
|
+
let windowIdleTimer = null;
|
|
165
|
+
const WINDOW_IDLE_TIMEOUT = 3e4;
|
|
166
|
+
function resetWindowIdleTimer() {
|
|
167
|
+
if (windowIdleTimer) clearTimeout(windowIdleTimer);
|
|
168
|
+
windowIdleTimer = setTimeout(async () => {
|
|
169
|
+
if (automationWindowId !== null) {
|
|
170
|
+
try {
|
|
171
|
+
await chrome.windows.remove(automationWindowId);
|
|
172
|
+
console.log(`[opencli] Automation window ${automationWindowId} closed (idle timeout)`);
|
|
173
|
+
} catch {
|
|
174
|
+
}
|
|
175
|
+
automationWindowId = null;
|
|
176
|
+
}
|
|
177
|
+
windowIdleTimer = null;
|
|
178
|
+
}, WINDOW_IDLE_TIMEOUT);
|
|
179
|
+
}
|
|
180
|
+
async function getAutomationWindow() {
|
|
181
|
+
if (automationWindowId !== null) {
|
|
182
|
+
try {
|
|
183
|
+
await chrome.windows.get(automationWindowId);
|
|
184
|
+
return automationWindowId;
|
|
185
|
+
} catch {
|
|
186
|
+
automationWindowId = null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const win = await chrome.windows.create({
|
|
190
|
+
url: "about:blank",
|
|
191
|
+
focused: false,
|
|
192
|
+
width: 1280,
|
|
193
|
+
height: 900,
|
|
194
|
+
type: "normal"
|
|
195
|
+
});
|
|
196
|
+
automationWindowId = win.id;
|
|
197
|
+
console.log(`[opencli] Created automation window ${automationWindowId}`);
|
|
198
|
+
return automationWindowId;
|
|
199
|
+
}
|
|
200
|
+
chrome.windows.onRemoved.addListener((windowId) => {
|
|
201
|
+
if (windowId === automationWindowId) {
|
|
202
|
+
console.log("[opencli] Automation window closed");
|
|
203
|
+
automationWindowId = null;
|
|
204
|
+
if (windowIdleTimer) {
|
|
205
|
+
clearTimeout(windowIdleTimer);
|
|
206
|
+
windowIdleTimer = null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
let initialized = false;
|
|
211
|
+
function initialize() {
|
|
212
|
+
if (initialized) return;
|
|
213
|
+
initialized = true;
|
|
214
|
+
chrome.alarms.create("keepalive", { periodInMinutes: 0.4 });
|
|
215
|
+
registerListeners();
|
|
216
|
+
connect();
|
|
217
|
+
console.log("[opencli] OpenCLI extension initialized");
|
|
218
|
+
}
|
|
219
|
+
chrome.runtime.onInstalled.addListener(() => {
|
|
220
|
+
initialize();
|
|
221
|
+
});
|
|
222
|
+
chrome.runtime.onStartup.addListener(() => {
|
|
223
|
+
initialize();
|
|
224
|
+
});
|
|
225
|
+
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
226
|
+
if (alarm.name === "keepalive") connect();
|
|
227
|
+
});
|
|
228
|
+
async function handleCommand(cmd) {
|
|
229
|
+
resetWindowIdleTimer();
|
|
230
|
+
try {
|
|
231
|
+
switch (cmd.action) {
|
|
232
|
+
case "exec":
|
|
233
|
+
return await handleExec(cmd);
|
|
234
|
+
case "navigate":
|
|
235
|
+
return await handleNavigate(cmd);
|
|
236
|
+
case "tabs":
|
|
237
|
+
return await handleTabs(cmd);
|
|
238
|
+
case "cookies":
|
|
239
|
+
return await handleCookies(cmd);
|
|
240
|
+
case "screenshot":
|
|
241
|
+
return await handleScreenshot(cmd);
|
|
242
|
+
case "close-window":
|
|
243
|
+
return await handleCloseWindow(cmd);
|
|
244
|
+
default:
|
|
245
|
+
return { id: cmd.id, ok: false, error: `Unknown action: ${cmd.action}` };
|
|
246
|
+
}
|
|
247
|
+
} catch (err) {
|
|
248
|
+
return {
|
|
249
|
+
id: cmd.id,
|
|
250
|
+
ok: false,
|
|
251
|
+
error: err instanceof Error ? err.message : String(err)
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function isWebUrl(url) {
|
|
256
|
+
if (!url) return false;
|
|
257
|
+
return !url.startsWith("chrome://") && !url.startsWith("chrome-extension://");
|
|
258
|
+
}
|
|
259
|
+
async function resolveTabId(tabId) {
|
|
260
|
+
if (tabId !== void 0) return tabId;
|
|
261
|
+
const windowId = await getAutomationWindow();
|
|
262
|
+
const tabs = await chrome.tabs.query({ windowId });
|
|
263
|
+
const webTab = tabs.find((t) => t.id && isWebUrl(t.url));
|
|
264
|
+
if (webTab?.id) return webTab.id;
|
|
265
|
+
if (tabs.length > 0 && tabs[0]?.id) return tabs[0].id;
|
|
266
|
+
const newTab = await chrome.tabs.create({ windowId, url: "about:blank", active: true });
|
|
267
|
+
if (!newTab.id) throw new Error("Failed to create tab in automation window");
|
|
268
|
+
return newTab.id;
|
|
269
|
+
}
|
|
270
|
+
async function handleExec(cmd) {
|
|
271
|
+
if (!cmd.code) return { id: cmd.id, ok: false, error: "Missing code" };
|
|
272
|
+
const tabId = await resolveTabId(cmd.tabId);
|
|
273
|
+
try {
|
|
274
|
+
const data = await evaluateAsync(tabId, cmd.code);
|
|
275
|
+
return { id: cmd.id, ok: true, data };
|
|
276
|
+
} catch (err) {
|
|
277
|
+
return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async function handleNavigate(cmd) {
|
|
281
|
+
if (!cmd.url) return { id: cmd.id, ok: false, error: "Missing url" };
|
|
282
|
+
const tabId = await resolveTabId(cmd.tabId);
|
|
283
|
+
await chrome.tabs.update(tabId, { url: cmd.url });
|
|
284
|
+
await new Promise((resolve) => {
|
|
285
|
+
chrome.tabs.get(tabId).then((tab2) => {
|
|
286
|
+
if (tab2.status === "complete") {
|
|
287
|
+
resolve();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const listener = (id, info) => {
|
|
291
|
+
if (id === tabId && info.status === "complete") {
|
|
292
|
+
chrome.tabs.onUpdated.removeListener(listener);
|
|
293
|
+
resolve();
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
chrome.tabs.onUpdated.addListener(listener);
|
|
297
|
+
setTimeout(() => {
|
|
298
|
+
chrome.tabs.onUpdated.removeListener(listener);
|
|
299
|
+
resolve();
|
|
300
|
+
}, 15e3);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
const tab = await chrome.tabs.get(tabId);
|
|
304
|
+
return { id: cmd.id, ok: true, data: { title: tab.title, url: tab.url, tabId } };
|
|
305
|
+
}
|
|
306
|
+
async function handleTabs(cmd) {
|
|
307
|
+
switch (cmd.op) {
|
|
308
|
+
case "list": {
|
|
309
|
+
const tabs = await chrome.tabs.query({});
|
|
310
|
+
const data = tabs.filter((t) => isWebUrl(t.url)).map((t, i) => ({
|
|
311
|
+
index: i,
|
|
312
|
+
tabId: t.id,
|
|
313
|
+
url: t.url,
|
|
314
|
+
title: t.title,
|
|
315
|
+
active: t.active
|
|
316
|
+
}));
|
|
317
|
+
return { id: cmd.id, ok: true, data };
|
|
318
|
+
}
|
|
319
|
+
case "new": {
|
|
320
|
+
const tab = await chrome.tabs.create({ url: cmd.url, active: true });
|
|
321
|
+
return { id: cmd.id, ok: true, data: { tabId: tab.id, url: tab.url } };
|
|
322
|
+
}
|
|
323
|
+
case "close": {
|
|
324
|
+
if (cmd.index !== void 0) {
|
|
325
|
+
const tabs = await chrome.tabs.query({});
|
|
326
|
+
const target = tabs[cmd.index];
|
|
327
|
+
if (!target?.id) return { id: cmd.id, ok: false, error: `Tab index ${cmd.index} not found` };
|
|
328
|
+
await chrome.tabs.remove(target.id);
|
|
329
|
+
detach(target.id);
|
|
330
|
+
return { id: cmd.id, ok: true, data: { closed: target.id } };
|
|
331
|
+
}
|
|
332
|
+
const tabId = await resolveTabId(cmd.tabId);
|
|
333
|
+
await chrome.tabs.remove(tabId);
|
|
334
|
+
detach(tabId);
|
|
335
|
+
return { id: cmd.id, ok: true, data: { closed: tabId } };
|
|
336
|
+
}
|
|
337
|
+
case "select": {
|
|
338
|
+
if (cmd.index === void 0 && cmd.tabId === void 0)
|
|
339
|
+
return { id: cmd.id, ok: false, error: "Missing index or tabId" };
|
|
340
|
+
if (cmd.tabId !== void 0) {
|
|
341
|
+
await chrome.tabs.update(cmd.tabId, { active: true });
|
|
342
|
+
return { id: cmd.id, ok: true, data: { selected: cmd.tabId } };
|
|
343
|
+
}
|
|
344
|
+
const tabs = await chrome.tabs.query({});
|
|
345
|
+
const target = tabs[cmd.index];
|
|
346
|
+
if (!target?.id) return { id: cmd.id, ok: false, error: `Tab index ${cmd.index} not found` };
|
|
347
|
+
await chrome.tabs.update(target.id, { active: true });
|
|
348
|
+
return { id: cmd.id, ok: true, data: { selected: target.id } };
|
|
349
|
+
}
|
|
350
|
+
default:
|
|
351
|
+
return { id: cmd.id, ok: false, error: `Unknown tabs op: ${cmd.op}` };
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
async function handleCookies(cmd) {
|
|
355
|
+
const details = {};
|
|
356
|
+
if (cmd.domain) details.domain = cmd.domain;
|
|
357
|
+
if (cmd.url) details.url = cmd.url;
|
|
358
|
+
const cookies = await chrome.cookies.getAll(details);
|
|
359
|
+
const data = cookies.map((c) => ({
|
|
360
|
+
name: c.name,
|
|
361
|
+
value: c.value,
|
|
362
|
+
domain: c.domain,
|
|
363
|
+
path: c.path,
|
|
364
|
+
secure: c.secure,
|
|
365
|
+
httpOnly: c.httpOnly,
|
|
366
|
+
expirationDate: c.expirationDate
|
|
367
|
+
}));
|
|
368
|
+
return { id: cmd.id, ok: true, data };
|
|
369
|
+
}
|
|
370
|
+
async function handleScreenshot(cmd) {
|
|
371
|
+
const tabId = await resolveTabId(cmd.tabId);
|
|
372
|
+
try {
|
|
373
|
+
const data = await screenshot(tabId, {
|
|
374
|
+
format: cmd.format,
|
|
375
|
+
quality: cmd.quality,
|
|
376
|
+
fullPage: cmd.fullPage
|
|
377
|
+
});
|
|
378
|
+
return { id: cmd.id, ok: true, data };
|
|
379
|
+
} catch (err) {
|
|
380
|
+
return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
async function handleCloseWindow(cmd) {
|
|
384
|
+
if (automationWindowId !== null) {
|
|
385
|
+
try {
|
|
386
|
+
await chrome.windows.remove(automationWindowId);
|
|
387
|
+
} catch {
|
|
388
|
+
}
|
|
389
|
+
automationWindowId = null;
|
|
390
|
+
}
|
|
391
|
+
return { id: cmd.id, ok: true, data: { closed: true } };
|
|
392
|
+
}
|
package/extension/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
|
-
"name": "
|
|
4
|
-
"version": "0.
|
|
3
|
+
"name": "OpenCLI",
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"description": "Bridge between opencli CLI and your browser — execute commands, read cookies, manage tabs.",
|
|
6
6
|
"permissions": [
|
|
7
7
|
"debugger",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"128": "icons/icon-128.png"
|
|
22
22
|
},
|
|
23
23
|
"action": {
|
|
24
|
-
"default_title": "
|
|
24
|
+
"default_title": "OpenCLI",
|
|
25
25
|
"default_icon": {
|
|
26
26
|
"16": "icons/icon-16.png",
|
|
27
27
|
"32": "icons/icon-32.png"
|