@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.
Files changed (171) hide show
  1. package/.github/workflows/build-extension.yml +62 -0
  2. package/.github/workflows/ci.yml +6 -6
  3. package/.github/workflows/e2e-headed.yml +2 -2
  4. package/.github/workflows/pkg-pr-new.yml +2 -2
  5. package/.github/workflows/release.yml +2 -5
  6. package/.github/workflows/security.yml +2 -2
  7. package/CDP.md +1 -1
  8. package/CDP.zh-CN.md +1 -1
  9. package/README.md +35 -8
  10. package/README.zh-CN.md +35 -8
  11. package/SKILL.md +3 -5
  12. package/dist/browser/cdp.d.ts +27 -0
  13. package/dist/browser/cdp.js +295 -0
  14. package/dist/browser/daemon-client.d.ts +1 -1
  15. package/dist/browser/index.d.ts +4 -2
  16. package/dist/browser/index.js +5 -5
  17. package/dist/browser/mcp.d.ts +5 -8
  18. package/dist/browser/mcp.js +9 -10
  19. package/dist/browser/page.d.ts +8 -1
  20. package/dist/browser/page.js +25 -40
  21. package/dist/browser/utils.d.ts +10 -0
  22. package/dist/browser/utils.js +27 -0
  23. package/dist/browser.test.js +48 -7
  24. package/dist/chaoxing.d.ts +58 -0
  25. package/dist/chaoxing.js +225 -0
  26. package/dist/chaoxing.test.d.ts +1 -0
  27. package/dist/chaoxing.test.js +38 -0
  28. package/dist/cli-manifest.json +597 -14
  29. package/dist/cli.d.ts +1 -0
  30. package/dist/cli.js +197 -0
  31. package/dist/clis/apple-podcasts/episodes.d.ts +1 -0
  32. package/dist/clis/apple-podcasts/episodes.js +28 -0
  33. package/dist/clis/apple-podcasts/search.d.ts +1 -0
  34. package/dist/clis/apple-podcasts/search.js +29 -0
  35. package/dist/clis/apple-podcasts/top.d.ts +1 -0
  36. package/dist/clis/apple-podcasts/top.js +34 -0
  37. package/dist/clis/apple-podcasts/utils.d.ts +11 -0
  38. package/dist/clis/apple-podcasts/utils.js +30 -0
  39. package/dist/clis/apple-podcasts/utils.test.d.ts +1 -0
  40. package/dist/clis/apple-podcasts/utils.test.js +57 -0
  41. package/dist/clis/boss/chatlist.d.ts +1 -0
  42. package/dist/clis/boss/chatlist.js +50 -0
  43. package/dist/clis/boss/chatmsg.d.ts +1 -0
  44. package/dist/clis/boss/chatmsg.js +73 -0
  45. package/dist/clis/boss/send.d.ts +1 -0
  46. package/dist/clis/boss/send.js +176 -0
  47. package/dist/clis/chaoxing/assignments.d.ts +1 -0
  48. package/dist/clis/chaoxing/assignments.js +74 -0
  49. package/dist/clis/chaoxing/exams.d.ts +1 -0
  50. package/dist/clis/chaoxing/exams.js +74 -0
  51. package/dist/clis/chatgpt/ask.js +15 -14
  52. package/dist/clis/chatgpt/ax.d.ts +1 -0
  53. package/dist/clis/chatgpt/ax.js +78 -0
  54. package/dist/clis/chatgpt/read.js +5 -6
  55. package/dist/clis/chatwise/history.js +18 -1
  56. package/dist/clis/discord-app/channels.js +33 -21
  57. package/dist/clis/twitter/accept.d.ts +1 -0
  58. package/dist/clis/twitter/accept.js +202 -0
  59. package/dist/clis/twitter/followers.js +30 -22
  60. package/dist/clis/twitter/following.js +19 -14
  61. package/dist/clis/twitter/notifications.js +29 -22
  62. package/dist/clis/twitter/post.js +9 -2
  63. package/dist/clis/twitter/reply-dm.d.ts +1 -0
  64. package/dist/clis/twitter/reply-dm.js +181 -0
  65. package/dist/clis/twitter/search.js +30 -11
  66. package/dist/clis/weread/book.d.ts +1 -0
  67. package/dist/clis/weread/book.js +26 -0
  68. package/dist/clis/weread/highlights.d.ts +1 -0
  69. package/dist/clis/weread/highlights.js +23 -0
  70. package/dist/clis/weread/notebooks.d.ts +1 -0
  71. package/dist/clis/weread/notebooks.js +21 -0
  72. package/dist/clis/weread/notes.d.ts +1 -0
  73. package/dist/clis/weread/notes.js +29 -0
  74. package/dist/clis/weread/ranking.d.ts +1 -0
  75. package/dist/clis/weread/ranking.js +28 -0
  76. package/dist/clis/weread/search.d.ts +1 -0
  77. package/dist/clis/weread/search.js +25 -0
  78. package/dist/clis/weread/shelf.d.ts +1 -0
  79. package/dist/clis/weread/shelf.js +24 -0
  80. package/dist/clis/weread/utils.d.ts +20 -0
  81. package/dist/clis/weread/utils.js +72 -0
  82. package/dist/clis/weread/utils.test.d.ts +1 -0
  83. package/dist/clis/weread/utils.test.js +85 -0
  84. package/dist/clis/xiaohongshu/download.d.ts +1 -1
  85. package/dist/clis/xiaohongshu/download.js +1 -1
  86. package/dist/daemon.js +2 -2
  87. package/dist/doctor.d.ts +0 -21
  88. package/dist/doctor.js +2 -24
  89. package/dist/engine.js +24 -13
  90. package/dist/explore.js +46 -101
  91. package/dist/main.js +4 -203
  92. package/dist/output.d.ts +1 -1
  93. package/dist/registry.d.ts +3 -3
  94. package/dist/runtime.d.ts +1 -4
  95. package/dist/runtime.js +1 -4
  96. package/dist/scripts/framework.d.ts +4 -0
  97. package/dist/scripts/framework.js +21 -0
  98. package/dist/scripts/interact.d.ts +4 -0
  99. package/dist/scripts/interact.js +20 -0
  100. package/dist/scripts/store.d.ts +9 -0
  101. package/dist/scripts/store.js +44 -0
  102. package/dist/setup.js +2 -2
  103. package/dist/synthesize.js +1 -1
  104. package/extension/dist/background.js +392 -0
  105. package/extension/manifest.json +3 -3
  106. package/extension/package.json +1 -1
  107. package/extension/src/background.ts +101 -24
  108. package/extension/src/protocol.ts +1 -1
  109. package/package.json +1 -1
  110. package/src/browser/cdp.ts +295 -0
  111. package/src/browser/daemon-client.ts +1 -1
  112. package/src/browser/index.ts +5 -6
  113. package/src/browser/mcp.ts +14 -15
  114. package/src/browser/page.ts +25 -41
  115. package/src/browser/utils.ts +27 -0
  116. package/src/browser.test.ts +52 -6
  117. package/src/chaoxing.test.ts +45 -0
  118. package/src/chaoxing.ts +268 -0
  119. package/src/cli.ts +185 -0
  120. package/src/clis/antigravity/SKILL.md +5 -0
  121. package/src/clis/apple-podcasts/episodes.ts +28 -0
  122. package/src/clis/apple-podcasts/search.ts +29 -0
  123. package/src/clis/apple-podcasts/top.ts +34 -0
  124. package/src/clis/apple-podcasts/utils.test.ts +72 -0
  125. package/src/clis/apple-podcasts/utils.ts +37 -0
  126. package/src/clis/boss/chatlist.ts +50 -0
  127. package/src/clis/boss/chatmsg.ts +70 -0
  128. package/src/clis/boss/send.ts +193 -0
  129. package/src/clis/chaoxing/README.md +36 -0
  130. package/src/clis/chaoxing/README.zh-CN.md +35 -0
  131. package/src/clis/chaoxing/assignments.ts +88 -0
  132. package/src/clis/chaoxing/exams.ts +88 -0
  133. package/src/clis/chatgpt/ask.ts +14 -15
  134. package/src/clis/chatgpt/ax.ts +81 -0
  135. package/src/clis/chatgpt/read.ts +5 -7
  136. package/src/clis/chatwise/history.ts +15 -1
  137. package/src/clis/discord-app/channels.ts +33 -21
  138. package/src/clis/twitter/accept.ts +213 -0
  139. package/src/clis/twitter/followers.ts +36 -29
  140. package/src/clis/twitter/following.ts +25 -20
  141. package/src/clis/twitter/notifications.ts +34 -27
  142. package/src/clis/twitter/post.ts +9 -2
  143. package/src/clis/twitter/reply-dm.ts +193 -0
  144. package/src/clis/twitter/search.ts +34 -12
  145. package/src/clis/weread/book.ts +28 -0
  146. package/src/clis/weread/highlights.ts +25 -0
  147. package/src/clis/weread/notebooks.ts +23 -0
  148. package/src/clis/weread/notes.ts +31 -0
  149. package/src/clis/weread/ranking.ts +29 -0
  150. package/src/clis/weread/search.ts +26 -0
  151. package/src/clis/weread/shelf.ts +26 -0
  152. package/src/clis/weread/utils.test.ts +104 -0
  153. package/src/clis/weread/utils.ts +74 -0
  154. package/src/clis/xiaohongshu/download.ts +1 -1
  155. package/src/daemon.ts +2 -2
  156. package/src/doctor.ts +2 -19
  157. package/src/engine.ts +20 -13
  158. package/src/explore.ts +51 -100
  159. package/src/main.ts +4 -186
  160. package/src/output.ts +12 -12
  161. package/src/registry.ts +3 -3
  162. package/src/runtime.ts +2 -6
  163. package/src/scripts/framework.ts +20 -0
  164. package/src/scripts/interact.ts +22 -0
  165. package/src/scripts/store.ts +40 -0
  166. package/src/setup.ts +2 -2
  167. package/src/synthesize.ts +1 -1
  168. package/tests/e2e/public-commands.test.ts +68 -1
  169. package/dist/clis/grok/debug.d.ts +0 -1
  170. package/dist/clis/grok/debug.js +0 -45
  171. 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,4 @@
1
+ /**
2
+ * Injected script for interactive fuzzing (clicking elements to trigger lazy loading)
3
+ */
4
+ export declare function interactFuzz(): Promise<number>;
@@ -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,9 @@
1
+ /**
2
+ * Injected script for discovering Pinia or Vuex stores and their actions/state representations
3
+ */
4
+ export declare function discoverStores(): {
5
+ type: string;
6
+ id: string;
7
+ actions: string[];
8
+ stateKeys: string[];
9
+ }[];
@@ -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 { PlaywrightMCP } from './browser/index.js';
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 PlaywrightMCP();
26
+ const mcp = new BrowserBridge();
27
27
  try {
28
28
  await mcp.connect({ timeout: 5 });
29
29
  await mcp.close();
@@ -108,7 +108,7 @@ function buildEvaluateScript(url, itemPath, endpoint) {
108
108
  }
109
109
  return [
110
110
  '(async () => {',
111
- ` const res = await fetch('${url}', {`,
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
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
- "name": "opencli Browser Bridge",
4
- "version": "0.1.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": "opencli Browser Bridge",
24
+ "default_title": "OpenCLI",
25
25
  "default_icon": {
26
26
  "16": "icons/icon-16.png",
27
27
  "32": "icons/icon-32.png"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencli-extension",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {