@jackwener/opencli 1.4.0 → 1.4.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.
Files changed (209) hide show
  1. package/.github/actions/setup-chrome/action.yml +5 -4
  2. package/.github/workflows/ci.yml +17 -3
  3. package/.github/workflows/e2e-headed.yml +16 -3
  4. package/CHANGELOG.md +23 -0
  5. package/PRIVACY.md +57 -0
  6. package/README.md +1 -1
  7. package/README.zh-CN.md +1 -1
  8. package/SKILL.md +101 -2
  9. package/dist/cli-manifest.json +720 -32
  10. package/dist/clis/apple-podcasts/search.js +2 -1
  11. package/dist/clis/arxiv/search.js +2 -2
  12. package/dist/clis/bbc/news.js +0 -1
  13. package/dist/clis/ctrip/search.js +0 -1
  14. package/dist/clis/douyin/_shared/browser-fetch.d.ts +10 -0
  15. package/dist/clis/douyin/_shared/browser-fetch.js +30 -0
  16. package/dist/clis/douyin/_shared/browser-fetch.test.d.ts +1 -0
  17. package/dist/clis/douyin/_shared/browser-fetch.test.js +31 -0
  18. package/dist/clis/douyin/_shared/creation-id.d.ts +1 -0
  19. package/dist/clis/douyin/_shared/creation-id.js +5 -0
  20. package/dist/clis/douyin/_shared/creation-id.test.d.ts +1 -0
  21. package/dist/clis/douyin/_shared/creation-id.test.js +22 -0
  22. package/dist/clis/douyin/_shared/imagex-upload.d.ts +20 -0
  23. package/dist/clis/douyin/_shared/imagex-upload.js +53 -0
  24. package/dist/clis/douyin/_shared/imagex-upload.test.d.ts +1 -0
  25. package/dist/clis/douyin/_shared/imagex-upload.test.js +87 -0
  26. package/dist/clis/douyin/_shared/sts2.d.ts +8 -0
  27. package/dist/clis/douyin/_shared/sts2.js +15 -0
  28. package/dist/clis/douyin/_shared/text-extra.d.ts +18 -0
  29. package/dist/clis/douyin/_shared/text-extra.js +15 -0
  30. package/dist/clis/douyin/_shared/text-extra.test.d.ts +1 -0
  31. package/dist/clis/douyin/_shared/text-extra.test.js +37 -0
  32. package/dist/clis/douyin/_shared/timing.d.ts +2 -0
  33. package/dist/clis/douyin/_shared/timing.js +22 -0
  34. package/dist/clis/douyin/_shared/timing.test.d.ts +1 -0
  35. package/dist/clis/douyin/_shared/timing.test.js +28 -0
  36. package/dist/clis/douyin/_shared/tos-upload-short-read.test.d.ts +11 -0
  37. package/dist/clis/douyin/_shared/tos-upload-short-read.test.js +83 -0
  38. package/dist/clis/douyin/_shared/tos-upload.d.ts +53 -0
  39. package/dist/clis/douyin/_shared/tos-upload.js +295 -0
  40. package/dist/clis/douyin/_shared/tos-upload.test.d.ts +1 -0
  41. package/dist/clis/douyin/_shared/tos-upload.test.js +229 -0
  42. package/dist/clis/douyin/_shared/transcode.d.ts +27 -0
  43. package/dist/clis/douyin/_shared/transcode.js +45 -0
  44. package/dist/clis/douyin/_shared/transcode.test.d.ts +1 -0
  45. package/dist/clis/douyin/_shared/transcode.test.js +93 -0
  46. package/dist/clis/douyin/_shared/types.d.ts +26 -0
  47. package/dist/clis/douyin/_shared/types.js +1 -0
  48. package/dist/clis/douyin/activities.d.ts +1 -0
  49. package/dist/clis/douyin/activities.js +20 -0
  50. package/dist/clis/douyin/activities.test.d.ts +1 -0
  51. package/dist/clis/douyin/activities.test.js +22 -0
  52. package/dist/clis/douyin/collections.d.ts +1 -0
  53. package/dist/clis/douyin/collections.js +22 -0
  54. package/dist/clis/douyin/collections.test.d.ts +1 -0
  55. package/dist/clis/douyin/collections.test.js +23 -0
  56. package/dist/clis/douyin/delete.d.ts +1 -0
  57. package/dist/clis/douyin/delete.js +18 -0
  58. package/dist/clis/douyin/delete.test.d.ts +1 -0
  59. package/dist/clis/douyin/delete.test.js +11 -0
  60. package/dist/clis/douyin/draft.d.ts +14 -0
  61. package/dist/clis/douyin/draft.js +237 -0
  62. package/dist/clis/douyin/draft.test.d.ts +1 -0
  63. package/dist/clis/douyin/draft.test.js +11 -0
  64. package/dist/clis/douyin/drafts.d.ts +1 -0
  65. package/dist/clis/douyin/drafts.js +23 -0
  66. package/dist/clis/douyin/drafts.test.d.ts +1 -0
  67. package/dist/clis/douyin/drafts.test.js +11 -0
  68. package/dist/clis/douyin/hashtag.d.ts +1 -0
  69. package/dist/clis/douyin/hashtag.js +45 -0
  70. package/dist/clis/douyin/hashtag.test.d.ts +1 -0
  71. package/dist/clis/douyin/hashtag.test.js +25 -0
  72. package/dist/clis/douyin/location.d.ts +1 -0
  73. package/dist/clis/douyin/location.js +24 -0
  74. package/dist/clis/douyin/location.test.d.ts +1 -0
  75. package/dist/clis/douyin/location.test.js +23 -0
  76. package/dist/clis/douyin/profile.d.ts +1 -0
  77. package/dist/clis/douyin/profile.js +28 -0
  78. package/dist/clis/douyin/profile.test.d.ts +1 -0
  79. package/dist/clis/douyin/profile.test.js +11 -0
  80. package/dist/clis/douyin/publish.d.ts +14 -0
  81. package/dist/clis/douyin/publish.js +288 -0
  82. package/dist/clis/douyin/publish.test.d.ts +1 -0
  83. package/dist/clis/douyin/publish.test.js +38 -0
  84. package/dist/clis/douyin/stats.d.ts +1 -0
  85. package/dist/clis/douyin/stats.js +27 -0
  86. package/dist/clis/douyin/stats.test.d.ts +1 -0
  87. package/dist/clis/douyin/stats.test.js +22 -0
  88. package/dist/clis/douyin/update.d.ts +1 -0
  89. package/dist/clis/douyin/update.js +31 -0
  90. package/dist/clis/douyin/update.test.d.ts +1 -0
  91. package/dist/clis/douyin/update.test.js +11 -0
  92. package/dist/clis/douyin/videos.d.ts +1 -0
  93. package/dist/clis/douyin/videos.js +34 -0
  94. package/dist/clis/douyin/videos.test.d.ts +1 -0
  95. package/dist/clis/douyin/videos.test.js +11 -0
  96. package/dist/clis/hackernews/search.yaml +1 -1
  97. package/dist/clis/instagram/search.yaml +2 -1
  98. package/dist/clis/linux-do/search.yaml +3 -1
  99. package/dist/clis/medium/search.js +1 -1
  100. package/dist/clis/reuters/search.js +0 -1
  101. package/dist/clis/twitter/search.js +5 -3
  102. package/dist/clis/twitter/search.test.js +54 -2
  103. package/dist/clis/weibo/comments.d.ts +1 -0
  104. package/dist/clis/weibo/comments.js +53 -0
  105. package/dist/clis/weibo/feed.d.ts +1 -0
  106. package/dist/clis/weibo/feed.js +56 -0
  107. package/dist/clis/weibo/hot.js +0 -1
  108. package/dist/clis/weibo/me.d.ts +1 -0
  109. package/dist/clis/weibo/me.js +76 -0
  110. package/dist/clis/weibo/post.d.ts +1 -0
  111. package/dist/clis/weibo/post.js +75 -0
  112. package/dist/clis/weibo/user.d.ts +1 -0
  113. package/dist/clis/weibo/user.js +63 -0
  114. package/dist/clis/weibo/utils.d.ts +6 -0
  115. package/dist/clis/weibo/utils.js +30 -0
  116. package/dist/clis/weread/search.js +3 -2
  117. package/dist/clis/xueqiu/search.yaml +2 -1
  118. package/dist/clis/yahoo-finance/quote.js +0 -1
  119. package/dist/clis/youtube/channel.d.ts +1 -0
  120. package/dist/clis/youtube/channel.js +150 -0
  121. package/dist/clis/youtube/comments.d.ts +1 -0
  122. package/dist/clis/youtube/comments.js +95 -0
  123. package/dist/clis/youtube/search.js +0 -1
  124. package/dist/clis/zhihu/search.yaml +2 -1
  125. package/dist/external-clis.yaml +0 -17
  126. package/dist/weread-search-regression.test.d.ts +1 -0
  127. package/dist/weread-search-regression.test.js +39 -0
  128. package/docs/.vitepress/config.mts +13 -0
  129. package/docs/adapters/browser/douyin.md +75 -0
  130. package/docs/adapters/browser/twitter.md +6 -0
  131. package/docs/adapters/index.md +6 -1
  132. package/extension/dist/background.js +508 -518
  133. package/extension/manifest.json +6 -2
  134. package/extension/package.json +1 -1
  135. package/extension/popup.html +84 -0
  136. package/extension/popup.js +25 -0
  137. package/extension/src/background.ts +20 -1
  138. package/package.json +1 -1
  139. package/src/clis/apple-podcasts/search.ts +2 -1
  140. package/src/clis/arxiv/search.ts +2 -2
  141. package/src/clis/bbc/news.ts +0 -1
  142. package/src/clis/ctrip/search.ts +0 -1
  143. package/src/clis/douyin/_shared/browser-fetch.test.ts +38 -0
  144. package/src/clis/douyin/_shared/browser-fetch.ts +45 -0
  145. package/src/clis/douyin/_shared/creation-id.test.ts +26 -0
  146. package/src/clis/douyin/_shared/creation-id.ts +8 -0
  147. package/src/clis/douyin/_shared/imagex-upload.test.ts +113 -0
  148. package/src/clis/douyin/_shared/imagex-upload.ts +76 -0
  149. package/src/clis/douyin/_shared/sts2.ts +20 -0
  150. package/src/clis/douyin/_shared/text-extra.test.ts +42 -0
  151. package/src/clis/douyin/_shared/text-extra.ts +33 -0
  152. package/src/clis/douyin/_shared/timing.test.ts +38 -0
  153. package/src/clis/douyin/_shared/timing.ts +22 -0
  154. package/src/clis/douyin/_shared/tos-upload-short-read.test.ts +102 -0
  155. package/src/clis/douyin/_shared/tos-upload.test.ts +281 -0
  156. package/src/clis/douyin/_shared/tos-upload.ts +444 -0
  157. package/src/clis/douyin/_shared/transcode.test.ts +117 -0
  158. package/src/clis/douyin/_shared/transcode.ts +78 -0
  159. package/src/clis/douyin/_shared/types.ts +29 -0
  160. package/src/clis/douyin/activities.test.ts +25 -0
  161. package/src/clis/douyin/activities.ts +23 -0
  162. package/src/clis/douyin/collections.test.ts +26 -0
  163. package/src/clis/douyin/collections.ts +25 -0
  164. package/src/clis/douyin/delete.test.ts +12 -0
  165. package/src/clis/douyin/delete.ts +20 -0
  166. package/src/clis/douyin/draft.test.ts +12 -0
  167. package/src/clis/douyin/draft.ts +282 -0
  168. package/src/clis/douyin/drafts.test.ts +12 -0
  169. package/src/clis/douyin/drafts.ts +27 -0
  170. package/src/clis/douyin/hashtag.test.ts +28 -0
  171. package/src/clis/douyin/hashtag.ts +56 -0
  172. package/src/clis/douyin/location.test.ts +26 -0
  173. package/src/clis/douyin/location.ts +27 -0
  174. package/src/clis/douyin/profile.test.ts +12 -0
  175. package/src/clis/douyin/profile.ts +37 -0
  176. package/src/clis/douyin/publish.test.ts +45 -0
  177. package/src/clis/douyin/publish.ts +340 -0
  178. package/src/clis/douyin/stats.test.ts +25 -0
  179. package/src/clis/douyin/stats.ts +30 -0
  180. package/src/clis/douyin/update.test.ts +12 -0
  181. package/src/clis/douyin/update.ts +43 -0
  182. package/src/clis/douyin/videos.test.ts +12 -0
  183. package/src/clis/douyin/videos.ts +49 -0
  184. package/src/clis/hackernews/search.yaml +1 -1
  185. package/src/clis/instagram/search.yaml +2 -1
  186. package/src/clis/linux-do/search.yaml +3 -1
  187. package/src/clis/medium/search.ts +1 -1
  188. package/src/clis/reuters/search.ts +0 -1
  189. package/src/clis/twitter/search.test.ts +69 -2
  190. package/src/clis/twitter/search.ts +5 -3
  191. package/src/clis/weibo/comments.ts +54 -0
  192. package/src/clis/weibo/feed.ts +57 -0
  193. package/src/clis/weibo/hot.ts +0 -1
  194. package/src/clis/weibo/me.ts +77 -0
  195. package/src/clis/weibo/post.ts +77 -0
  196. package/src/clis/weibo/user.ts +64 -0
  197. package/src/clis/weibo/utils.ts +32 -0
  198. package/src/clis/weread/search.ts +3 -2
  199. package/src/clis/xueqiu/search.yaml +2 -1
  200. package/src/clis/yahoo-finance/quote.ts +0 -1
  201. package/src/clis/youtube/channel.ts +155 -0
  202. package/src/clis/youtube/comments.ts +97 -0
  203. package/src/clis/youtube/search.ts +0 -1
  204. package/src/clis/zhihu/search.yaml +2 -1
  205. package/src/external-clis.yaml +0 -17
  206. package/src/weread-search-regression.test.ts +44 -0
  207. package/tests/e2e/browser-public-extended.test.ts +162 -0
  208. package/tests/e2e/browser-public.test.ts +7 -146
  209. package/vitest.config.ts +24 -17
@@ -1,582 +1,572 @@
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
- /** Check if a URL can be attached via CDP */
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
+ const BLANK_PAGE$1 = "data:text/html,<html></html>";
22
9
  function isDebuggableUrl$1(url) {
23
- if (!url) return true;
24
- return !url.startsWith("chrome://") && !url.startsWith("chrome-extension://");
10
+ if (!url) return true;
11
+ return url.startsWith("http://") || url.startsWith("https://") || url === BLANK_PAGE$1;
25
12
  }
26
13
  async function ensureAttached(tabId) {
27
- try {
28
- const tab = await chrome.tabs.get(tabId);
29
- if (!isDebuggableUrl$1(tab.url)) {
30
- attached.delete(tabId);
31
- throw new Error(`Cannot debug tab ${tabId}: URL is ${tab.url ?? "unknown"}`);
32
- }
33
- } catch (e) {
34
- if (e instanceof Error && e.message.startsWith("Cannot debug tab")) throw e;
35
- attached.delete(tabId);
36
- throw new Error(`Tab ${tabId} no longer exists`);
37
- }
38
- if (attached.has(tabId)) try {
39
- await chrome.debugger.sendCommand({ tabId }, "Runtime.evaluate", {
40
- expression: "1",
41
- returnByValue: true
42
- });
43
- return;
44
- } catch {
45
- attached.delete(tabId);
46
- }
47
- try {
48
- await chrome.debugger.attach({ tabId }, "1.3");
49
- } catch (e) {
50
- const msg = e instanceof Error ? e.message : String(e);
51
- const hint = msg.includes("chrome-extension://") ? ". Tip: another Chrome extension may be interfering — try disabling other extensions" : "";
52
- if (msg.includes("Another debugger is already attached")) {
53
- try {
54
- await chrome.debugger.detach({ tabId });
55
- } catch {}
56
- try {
57
- await chrome.debugger.attach({ tabId }, "1.3");
58
- } catch {
59
- throw new Error(`attach failed: ${msg}${hint}`);
60
- }
61
- } else throw new Error(`attach failed: ${msg}${hint}`);
62
- }
63
- attached.add(tabId);
64
- try {
65
- await chrome.debugger.sendCommand({ tabId }, "Runtime.enable");
66
- } catch {}
14
+ try {
15
+ const tab = await chrome.tabs.get(tabId);
16
+ if (!isDebuggableUrl$1(tab.url)) {
17
+ attached.delete(tabId);
18
+ throw new Error(`Cannot debug tab ${tabId}: URL is ${tab.url ?? "unknown"}`);
19
+ }
20
+ } catch (e) {
21
+ if (e instanceof Error && e.message.startsWith("Cannot debug tab")) throw e;
22
+ attached.delete(tabId);
23
+ throw new Error(`Tab ${tabId} no longer exists`);
24
+ }
25
+ if (attached.has(tabId)) {
26
+ try {
27
+ await chrome.debugger.sendCommand({ tabId }, "Runtime.evaluate", {
28
+ expression: "1",
29
+ returnByValue: true
30
+ });
31
+ return;
32
+ } catch {
33
+ attached.delete(tabId);
34
+ }
35
+ }
36
+ try {
37
+ await chrome.debugger.attach({ tabId }, "1.3");
38
+ } catch (e) {
39
+ const msg = e instanceof Error ? e.message : String(e);
40
+ const hint = msg.includes("chrome-extension://") ? ". Tip: another Chrome extension may be interfering — try disabling other extensions" : "";
41
+ if (msg.includes("Another debugger is already attached")) {
42
+ try {
43
+ await chrome.debugger.detach({ tabId });
44
+ } catch {
45
+ }
46
+ try {
47
+ await chrome.debugger.attach({ tabId }, "1.3");
48
+ } catch {
49
+ throw new Error(`attach failed: ${msg}${hint}`);
50
+ }
51
+ } else {
52
+ throw new Error(`attach failed: ${msg}${hint}`);
53
+ }
54
+ }
55
+ attached.add(tabId);
56
+ try {
57
+ await chrome.debugger.sendCommand({ tabId }, "Runtime.enable");
58
+ } catch {
59
+ }
67
60
  }
68
61
  async function evaluate(tabId, expression) {
69
- await ensureAttached(tabId);
70
- const result = await chrome.debugger.sendCommand({ tabId }, "Runtime.evaluate", {
71
- expression,
72
- returnByValue: true,
73
- awaitPromise: true
74
- });
75
- if (result.exceptionDetails) {
76
- const errMsg = result.exceptionDetails.exception?.description || result.exceptionDetails.text || "Eval error";
77
- throw new Error(errMsg);
78
- }
79
- return result.result?.value;
62
+ await ensureAttached(tabId);
63
+ const result = await chrome.debugger.sendCommand({ tabId }, "Runtime.evaluate", {
64
+ expression,
65
+ returnByValue: true,
66
+ awaitPromise: true
67
+ });
68
+ if (result.exceptionDetails) {
69
+ const errMsg = result.exceptionDetails.exception?.description || result.exceptionDetails.text || "Eval error";
70
+ throw new Error(errMsg);
71
+ }
72
+ return result.result?.value;
80
73
  }
81
- var evaluateAsync = evaluate;
82
- /**
83
- * Capture a screenshot via CDP Page.captureScreenshot.
84
- * Returns base64-encoded image data.
85
- */
74
+ const evaluateAsync = evaluate;
86
75
  async function screenshot(tabId, options = {}) {
87
- await ensureAttached(tabId);
88
- const format = options.format ?? "png";
89
- if (options.fullPage) {
90
- const metrics = await chrome.debugger.sendCommand({ tabId }, "Page.getLayoutMetrics");
91
- const size = metrics.cssContentSize || metrics.contentSize;
92
- if (size) await chrome.debugger.sendCommand({ tabId }, "Emulation.setDeviceMetricsOverride", {
93
- mobile: false,
94
- width: Math.ceil(size.width),
95
- height: Math.ceil(size.height),
96
- deviceScaleFactor: 1
97
- });
98
- }
99
- try {
100
- const params = { format };
101
- if (format === "jpeg" && options.quality !== void 0) params.quality = Math.max(0, Math.min(100, options.quality));
102
- return (await chrome.debugger.sendCommand({ tabId }, "Page.captureScreenshot", params)).data;
103
- } finally {
104
- if (options.fullPage) await chrome.debugger.sendCommand({ tabId }, "Emulation.clearDeviceMetricsOverride").catch(() => {});
105
- }
76
+ await ensureAttached(tabId);
77
+ const format = options.format ?? "png";
78
+ if (options.fullPage) {
79
+ const metrics = await chrome.debugger.sendCommand({ tabId }, "Page.getLayoutMetrics");
80
+ const size = metrics.cssContentSize || metrics.contentSize;
81
+ if (size) {
82
+ await chrome.debugger.sendCommand({ tabId }, "Emulation.setDeviceMetricsOverride", {
83
+ mobile: false,
84
+ width: Math.ceil(size.width),
85
+ height: Math.ceil(size.height),
86
+ deviceScaleFactor: 1
87
+ });
88
+ }
89
+ }
90
+ try {
91
+ const params = { format };
92
+ if (format === "jpeg" && options.quality !== void 0) {
93
+ params.quality = Math.max(0, Math.min(100, options.quality));
94
+ }
95
+ const result = await chrome.debugger.sendCommand({ tabId }, "Page.captureScreenshot", params);
96
+ return result.data;
97
+ } finally {
98
+ if (options.fullPage) {
99
+ await chrome.debugger.sendCommand({ tabId }, "Emulation.clearDeviceMetricsOverride").catch(() => {
100
+ });
101
+ }
102
+ }
106
103
  }
107
104
  async function detach(tabId) {
108
- if (!attached.has(tabId)) return;
109
- attached.delete(tabId);
110
- try {
111
- await chrome.debugger.detach({ tabId });
112
- } catch {}
105
+ if (!attached.has(tabId)) return;
106
+ attached.delete(tabId);
107
+ try {
108
+ await chrome.debugger.detach({ tabId });
109
+ } catch {
110
+ }
113
111
  }
114
112
  function registerListeners() {
115
- chrome.tabs.onRemoved.addListener((tabId) => {
116
- attached.delete(tabId);
117
- });
118
- chrome.debugger.onDetach.addListener((source) => {
119
- if (source.tabId) attached.delete(source.tabId);
120
- });
121
- chrome.tabs.onUpdated.addListener(async (tabId, info) => {
122
- if (info.url && !isDebuggableUrl$1(info.url)) await detach(tabId);
123
- });
113
+ chrome.tabs.onRemoved.addListener((tabId) => {
114
+ attached.delete(tabId);
115
+ });
116
+ chrome.debugger.onDetach.addListener((source) => {
117
+ if (source.tabId) attached.delete(source.tabId);
118
+ });
119
+ chrome.tabs.onUpdated.addListener(async (tabId, info) => {
120
+ if (info.url && !isDebuggableUrl$1(info.url)) {
121
+ await detach(tabId);
122
+ }
123
+ });
124
124
  }
125
- //#endregion
126
- //#region src/background.ts
127
- var ws = null;
128
- var reconnectTimer = null;
129
- var reconnectAttempts = 0;
130
- var _origLog = console.log.bind(console);
131
- var _origWarn = console.warn.bind(console);
132
- var _origError = console.error.bind(console);
125
+
126
+ let ws = null;
127
+ let reconnectTimer = null;
128
+ let reconnectAttempts = 0;
129
+ const _origLog = console.log.bind(console);
130
+ const _origWarn = console.warn.bind(console);
131
+ const _origError = console.error.bind(console);
133
132
  function forwardLog(level, args) {
134
- if (!ws || ws.readyState !== WebSocket.OPEN) return;
135
- try {
136
- const msg = args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
137
- ws.send(JSON.stringify({
138
- type: "log",
139
- level,
140
- msg,
141
- ts: Date.now()
142
- }));
143
- } catch {}
133
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
134
+ try {
135
+ const msg = args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
136
+ ws.send(JSON.stringify({ type: "log", level, msg, ts: Date.now() }));
137
+ } catch {
138
+ }
144
139
  }
145
140
  console.log = (...args) => {
146
- _origLog(...args);
147
- forwardLog("info", args);
141
+ _origLog(...args);
142
+ forwardLog("info", args);
148
143
  };
149
144
  console.warn = (...args) => {
150
- _origWarn(...args);
151
- forwardLog("warn", args);
145
+ _origWarn(...args);
146
+ forwardLog("warn", args);
152
147
  };
153
148
  console.error = (...args) => {
154
- _origError(...args);
155
- forwardLog("error", args);
149
+ _origError(...args);
150
+ forwardLog("error", args);
156
151
  };
157
152
  function connect() {
158
- if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING) return;
159
- try {
160
- ws = new WebSocket(DAEMON_WS_URL);
161
- } catch {
162
- scheduleReconnect();
163
- return;
164
- }
165
- ws.onopen = () => {
166
- console.log("[opencli] Connected to daemon");
167
- reconnectAttempts = 0;
168
- if (reconnectTimer) {
169
- clearTimeout(reconnectTimer);
170
- reconnectTimer = null;
171
- }
172
- };
173
- ws.onmessage = async (event) => {
174
- try {
175
- const result = await handleCommand(JSON.parse(event.data));
176
- ws?.send(JSON.stringify(result));
177
- } catch (err) {
178
- console.error("[opencli] Message handling error:", err);
179
- }
180
- };
181
- ws.onclose = () => {
182
- console.log("[opencli] Disconnected from daemon");
183
- ws = null;
184
- scheduleReconnect();
185
- };
186
- ws.onerror = () => {
187
- ws?.close();
188
- };
153
+ if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING) return;
154
+ try {
155
+ ws = new WebSocket(DAEMON_WS_URL);
156
+ } catch {
157
+ scheduleReconnect();
158
+ return;
159
+ }
160
+ ws.onopen = () => {
161
+ console.log("[opencli] Connected to daemon");
162
+ reconnectAttempts = 0;
163
+ if (reconnectTimer) {
164
+ clearTimeout(reconnectTimer);
165
+ reconnectTimer = null;
166
+ }
167
+ };
168
+ ws.onmessage = async (event) => {
169
+ try {
170
+ const command = JSON.parse(event.data);
171
+ const result = await handleCommand(command);
172
+ ws?.send(JSON.stringify(result));
173
+ } catch (err) {
174
+ console.error("[opencli] Message handling error:", err);
175
+ }
176
+ };
177
+ ws.onclose = () => {
178
+ console.log("[opencli] Disconnected from daemon");
179
+ ws = null;
180
+ scheduleReconnect();
181
+ };
182
+ ws.onerror = () => {
183
+ ws?.close();
184
+ };
189
185
  }
186
+ const MAX_EAGER_ATTEMPTS = 6;
190
187
  function scheduleReconnect() {
191
- if (reconnectTimer) return;
192
- reconnectAttempts++;
193
- const delay = Math.min(WS_RECONNECT_BASE_DELAY * Math.pow(2, reconnectAttempts - 1), WS_RECONNECT_MAX_DELAY);
194
- reconnectTimer = setTimeout(() => {
195
- reconnectTimer = null;
196
- connect();
197
- }, delay);
188
+ if (reconnectTimer) return;
189
+ reconnectAttempts++;
190
+ if (reconnectAttempts > MAX_EAGER_ATTEMPTS) return;
191
+ const delay = Math.min(WS_RECONNECT_BASE_DELAY * Math.pow(2, reconnectAttempts - 1), WS_RECONNECT_MAX_DELAY);
192
+ reconnectTimer = setTimeout(() => {
193
+ reconnectTimer = null;
194
+ connect();
195
+ }, delay);
198
196
  }
199
- var automationSessions = /* @__PURE__ */ new Map();
200
- var WINDOW_IDLE_TIMEOUT = 12e4;
197
+ const automationSessions = /* @__PURE__ */ new Map();
198
+ const WINDOW_IDLE_TIMEOUT = 12e4;
201
199
  function getWorkspaceKey(workspace) {
202
- return workspace?.trim() || "default";
200
+ return workspace?.trim() || "default";
203
201
  }
204
202
  function resetWindowIdleTimer(workspace) {
205
- const session = automationSessions.get(workspace);
206
- if (!session) return;
207
- if (session.idleTimer) clearTimeout(session.idleTimer);
208
- session.idleDeadlineAt = Date.now() + WINDOW_IDLE_TIMEOUT;
209
- session.idleTimer = setTimeout(async () => {
210
- const current = automationSessions.get(workspace);
211
- if (!current) return;
212
- try {
213
- await chrome.windows.remove(current.windowId);
214
- console.log(`[opencli] Automation window ${current.windowId} (${workspace}) closed (idle timeout)`);
215
- } catch {}
216
- automationSessions.delete(workspace);
217
- }, WINDOW_IDLE_TIMEOUT);
203
+ const session = automationSessions.get(workspace);
204
+ if (!session) return;
205
+ if (session.idleTimer) clearTimeout(session.idleTimer);
206
+ session.idleDeadlineAt = Date.now() + WINDOW_IDLE_TIMEOUT;
207
+ session.idleTimer = setTimeout(async () => {
208
+ const current = automationSessions.get(workspace);
209
+ if (!current) return;
210
+ try {
211
+ await chrome.windows.remove(current.windowId);
212
+ console.log(`[opencli] Automation window ${current.windowId} (${workspace}) closed (idle timeout)`);
213
+ } catch {
214
+ }
215
+ automationSessions.delete(workspace);
216
+ }, WINDOW_IDLE_TIMEOUT);
218
217
  }
219
- /** Get or create the dedicated automation window. */
220
218
  async function getAutomationWindow(workspace) {
221
- const existing = automationSessions.get(workspace);
222
- if (existing) try {
223
- await chrome.windows.get(existing.windowId);
224
- return existing.windowId;
225
- } catch {
226
- automationSessions.delete(workspace);
227
- }
228
- const session = {
229
- windowId: (await chrome.windows.create({
230
- url: "data:text/html,<html></html>",
231
- focused: false,
232
- width: 1280,
233
- height: 900,
234
- type: "normal"
235
- })).id,
236
- idleTimer: null,
237
- idleDeadlineAt: Date.now() + WINDOW_IDLE_TIMEOUT
238
- };
239
- automationSessions.set(workspace, session);
240
- console.log(`[opencli] Created automation window ${session.windowId} (${workspace})`);
241
- resetWindowIdleTimer(workspace);
242
- await new Promise((resolve) => setTimeout(resolve, 200));
243
- return session.windowId;
219
+ const existing = automationSessions.get(workspace);
220
+ if (existing) {
221
+ try {
222
+ await chrome.windows.get(existing.windowId);
223
+ return existing.windowId;
224
+ } catch {
225
+ automationSessions.delete(workspace);
226
+ }
227
+ }
228
+ const win = await chrome.windows.create({
229
+ url: BLANK_PAGE,
230
+ focused: false,
231
+ width: 1280,
232
+ height: 900,
233
+ type: "normal"
234
+ });
235
+ const session = {
236
+ windowId: win.id,
237
+ idleTimer: null,
238
+ idleDeadlineAt: Date.now() + WINDOW_IDLE_TIMEOUT
239
+ };
240
+ automationSessions.set(workspace, session);
241
+ console.log(`[opencli] Created automation window ${session.windowId} (${workspace})`);
242
+ resetWindowIdleTimer(workspace);
243
+ await new Promise((resolve) => setTimeout(resolve, 200));
244
+ return session.windowId;
244
245
  }
245
246
  chrome.windows.onRemoved.addListener((windowId) => {
246
- for (const [workspace, session] of automationSessions.entries()) if (session.windowId === windowId) {
247
- console.log(`[opencli] Automation window closed (${workspace})`);
248
- if (session.idleTimer) clearTimeout(session.idleTimer);
249
- automationSessions.delete(workspace);
250
- }
247
+ for (const [workspace, session] of automationSessions.entries()) {
248
+ if (session.windowId === windowId) {
249
+ console.log(`[opencli] Automation window closed (${workspace})`);
250
+ if (session.idleTimer) clearTimeout(session.idleTimer);
251
+ automationSessions.delete(workspace);
252
+ }
253
+ }
251
254
  });
252
- var initialized = false;
255
+ let initialized = false;
253
256
  function initialize() {
254
- if (initialized) return;
255
- initialized = true;
256
- chrome.alarms.create("keepalive", { periodInMinutes: .4 });
257
- registerListeners();
258
- connect();
259
- console.log("[opencli] OpenCLI extension initialized");
257
+ if (initialized) return;
258
+ initialized = true;
259
+ chrome.alarms.create("keepalive", { periodInMinutes: 0.4 });
260
+ registerListeners();
261
+ connect();
262
+ console.log("[opencli] OpenCLI extension initialized");
260
263
  }
261
264
  chrome.runtime.onInstalled.addListener(() => {
262
- initialize();
265
+ initialize();
263
266
  });
264
267
  chrome.runtime.onStartup.addListener(() => {
265
- initialize();
268
+ initialize();
266
269
  });
267
270
  chrome.alarms.onAlarm.addListener((alarm) => {
268
- if (alarm.name === "keepalive") connect();
271
+ if (alarm.name === "keepalive") connect();
272
+ });
273
+ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
274
+ if (msg?.type === "getStatus") {
275
+ sendResponse({
276
+ connected: ws?.readyState === WebSocket.OPEN,
277
+ reconnecting: reconnectTimer !== null
278
+ });
279
+ }
280
+ return false;
269
281
  });
270
282
  async function handleCommand(cmd) {
271
- const workspace = getWorkspaceKey(cmd.workspace);
272
- resetWindowIdleTimer(workspace);
273
- try {
274
- switch (cmd.action) {
275
- case "exec": return await handleExec(cmd, workspace);
276
- case "navigate": return await handleNavigate(cmd, workspace);
277
- case "tabs": return await handleTabs(cmd, workspace);
278
- case "cookies": return await handleCookies(cmd);
279
- case "screenshot": return await handleScreenshot(cmd, workspace);
280
- case "close-window": return await handleCloseWindow(cmd, workspace);
281
- case "sessions": return await handleSessions(cmd);
282
- default: return {
283
- id: cmd.id,
284
- ok: false,
285
- error: `Unknown action: ${cmd.action}`
286
- };
287
- }
288
- } catch (err) {
289
- return {
290
- id: cmd.id,
291
- ok: false,
292
- error: err instanceof Error ? err.message : String(err)
293
- };
294
- }
283
+ const workspace = getWorkspaceKey(cmd.workspace);
284
+ resetWindowIdleTimer(workspace);
285
+ try {
286
+ switch (cmd.action) {
287
+ case "exec":
288
+ return await handleExec(cmd, workspace);
289
+ case "navigate":
290
+ return await handleNavigate(cmd, workspace);
291
+ case "tabs":
292
+ return await handleTabs(cmd, workspace);
293
+ case "cookies":
294
+ return await handleCookies(cmd);
295
+ case "screenshot":
296
+ return await handleScreenshot(cmd, workspace);
297
+ case "close-window":
298
+ return await handleCloseWindow(cmd, workspace);
299
+ case "sessions":
300
+ return await handleSessions(cmd);
301
+ default:
302
+ return { id: cmd.id, ok: false, error: `Unknown action: ${cmd.action}` };
303
+ }
304
+ } catch (err) {
305
+ return {
306
+ id: cmd.id,
307
+ ok: false,
308
+ error: err instanceof Error ? err.message : String(err)
309
+ };
310
+ }
295
311
  }
296
- /** Check if a URL can be attached via CDP (not chrome:// or chrome-extension://) */
312
+ const BLANK_PAGE = "data:text/html,<html></html>";
297
313
  function isDebuggableUrl(url) {
298
- if (!url) return true;
299
- return !url.startsWith("chrome://") && !url.startsWith("chrome-extension://");
314
+ if (!url) return true;
315
+ return url.startsWith("http://") || url.startsWith("https://") || url === BLANK_PAGE;
316
+ }
317
+ function isSafeNavigationUrl(url) {
318
+ return url.startsWith("http://") || url.startsWith("https://");
319
+ }
320
+ function normalizeUrlForComparison(url) {
321
+ if (!url) return "";
322
+ try {
323
+ const parsed = new URL(url);
324
+ if (parsed.protocol === "https:" && parsed.port === "443" || parsed.protocol === "http:" && parsed.port === "80") {
325
+ parsed.port = "";
326
+ }
327
+ const pathname = parsed.pathname === "/" ? "" : parsed.pathname;
328
+ return `${parsed.protocol}//${parsed.host}${pathname}${parsed.search}${parsed.hash}`;
329
+ } catch {
330
+ return url;
331
+ }
332
+ }
333
+ function isTargetUrl(currentUrl, targetUrl) {
334
+ return normalizeUrlForComparison(currentUrl) === normalizeUrlForComparison(targetUrl);
300
335
  }
301
- /**
302
- * Resolve target tab in the automation window.
303
- * If explicit tabId is given, use that directly.
304
- * Otherwise, find or create a tab in the dedicated automation window.
305
- */
306
336
  async function resolveTabId(tabId, workspace) {
307
- if (tabId !== void 0) try {
308
- const tab = await chrome.tabs.get(tabId);
309
- if (isDebuggableUrl(tab.url)) return tabId;
310
- console.warn(`[opencli] Tab ${tabId} URL is not debuggable (${tab.url}), re-resolving`);
311
- } catch {
312
- console.warn(`[opencli] Tab ${tabId} no longer exists, re-resolving`);
313
- }
314
- const windowId = await getAutomationWindow(workspace);
315
- const tabs = await chrome.tabs.query({ windowId });
316
- const debuggableTab = tabs.find((t) => t.id && isDebuggableUrl(t.url));
317
- if (debuggableTab?.id) return debuggableTab.id;
318
- const reuseTab = tabs.find((t) => t.id);
319
- if (reuseTab?.id) {
320
- await chrome.tabs.update(reuseTab.id, { url: "data:text/html,<html></html>" });
321
- await new Promise((resolve) => setTimeout(resolve, 300));
322
- try {
323
- const updated = await chrome.tabs.get(reuseTab.id);
324
- if (isDebuggableUrl(updated.url)) return reuseTab.id;
325
- console.warn(`[opencli] data: URI was intercepted (${updated.url}), creating fresh tab`);
326
- } catch {}
327
- }
328
- const newTab = await chrome.tabs.create({
329
- windowId,
330
- url: "data:text/html,<html></html>",
331
- active: true
332
- });
333
- if (!newTab.id) throw new Error("Failed to create tab in automation window");
334
- return newTab.id;
337
+ if (tabId !== void 0) {
338
+ try {
339
+ const tab = await chrome.tabs.get(tabId);
340
+ const session = automationSessions.get(workspace);
341
+ if (isDebuggableUrl(tab.url) && session && tab.windowId === session.windowId) return tabId;
342
+ if (session && tab.windowId !== session.windowId) {
343
+ console.warn(`[opencli] Tab ${tabId} belongs to window ${tab.windowId}, not automation window ${session.windowId}, re-resolving`);
344
+ } else if (!isDebuggableUrl(tab.url)) {
345
+ console.warn(`[opencli] Tab ${tabId} URL is not debuggable (${tab.url}), re-resolving`);
346
+ }
347
+ } catch {
348
+ console.warn(`[opencli] Tab ${tabId} no longer exists, re-resolving`);
349
+ }
350
+ }
351
+ const windowId = await getAutomationWindow(workspace);
352
+ const tabs = await chrome.tabs.query({ windowId });
353
+ const debuggableTab = tabs.find((t) => t.id && isDebuggableUrl(t.url));
354
+ if (debuggableTab?.id) return debuggableTab.id;
355
+ const reuseTab = tabs.find((t) => t.id);
356
+ if (reuseTab?.id) {
357
+ await chrome.tabs.update(reuseTab.id, { url: BLANK_PAGE });
358
+ await new Promise((resolve) => setTimeout(resolve, 300));
359
+ try {
360
+ const updated = await chrome.tabs.get(reuseTab.id);
361
+ if (isDebuggableUrl(updated.url)) return reuseTab.id;
362
+ console.warn(`[opencli] data: URI was intercepted (${updated.url}), creating fresh tab`);
363
+ } catch {
364
+ }
365
+ }
366
+ const newTab = await chrome.tabs.create({ windowId, url: BLANK_PAGE, active: true });
367
+ if (!newTab.id) throw new Error("Failed to create tab in automation window");
368
+ return newTab.id;
335
369
  }
336
370
  async function listAutomationTabs(workspace) {
337
- const session = automationSessions.get(workspace);
338
- if (!session) return [];
339
- try {
340
- return await chrome.tabs.query({ windowId: session.windowId });
341
- } catch {
342
- automationSessions.delete(workspace);
343
- return [];
344
- }
371
+ const session = automationSessions.get(workspace);
372
+ if (!session) return [];
373
+ try {
374
+ return await chrome.tabs.query({ windowId: session.windowId });
375
+ } catch {
376
+ automationSessions.delete(workspace);
377
+ return [];
378
+ }
345
379
  }
346
380
  async function listAutomationWebTabs(workspace) {
347
- return (await listAutomationTabs(workspace)).filter((tab) => isDebuggableUrl(tab.url));
381
+ const tabs = await listAutomationTabs(workspace);
382
+ return tabs.filter((tab) => isDebuggableUrl(tab.url));
348
383
  }
349
384
  async function handleExec(cmd, workspace) {
350
- if (!cmd.code) return {
351
- id: cmd.id,
352
- ok: false,
353
- error: "Missing code"
354
- };
355
- const tabId = await resolveTabId(cmd.tabId, workspace);
356
- try {
357
- const data = await evaluateAsync(tabId, cmd.code);
358
- return {
359
- id: cmd.id,
360
- ok: true,
361
- data
362
- };
363
- } catch (err) {
364
- return {
365
- id: cmd.id,
366
- ok: false,
367
- error: err instanceof Error ? err.message : String(err)
368
- };
369
- }
385
+ if (!cmd.code) return { id: cmd.id, ok: false, error: "Missing code" };
386
+ const tabId = await resolveTabId(cmd.tabId, workspace);
387
+ try {
388
+ const data = await evaluateAsync(tabId, cmd.code);
389
+ return { id: cmd.id, ok: true, data };
390
+ } catch (err) {
391
+ return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) };
392
+ }
370
393
  }
371
394
  async function handleNavigate(cmd, workspace) {
372
- if (!cmd.url) return {
373
- id: cmd.id,
374
- ok: false,
375
- error: "Missing url"
376
- };
377
- const tabId = await resolveTabId(cmd.tabId, workspace);
378
- const beforeUrl = (await chrome.tabs.get(tabId)).url ?? "";
379
- const targetUrl = cmd.url;
380
- await detach(tabId);
381
- await chrome.tabs.update(tabId, { url: targetUrl });
382
- let timedOut = false;
383
- await new Promise((resolve) => {
384
- let urlChanged = false;
385
- const listener = (id, info, tab) => {
386
- if (id !== tabId) return;
387
- if (info.url && info.url !== beforeUrl) urlChanged = true;
388
- if (urlChanged && info.status === "complete") {
389
- chrome.tabs.onUpdated.removeListener(listener);
390
- resolve();
391
- }
392
- };
393
- chrome.tabs.onUpdated.addListener(listener);
394
- setTimeout(async () => {
395
- try {
396
- const currentTab = await chrome.tabs.get(tabId);
397
- if (currentTab.url !== beforeUrl && currentTab.status === "complete") {
398
- chrome.tabs.onUpdated.removeListener(listener);
399
- resolve();
400
- }
401
- } catch {}
402
- }, 100);
403
- setTimeout(() => {
404
- chrome.tabs.onUpdated.removeListener(listener);
405
- timedOut = true;
406
- console.warn(`[opencli] Navigate to ${targetUrl} timed out after 15s`);
407
- resolve();
408
- }, 15e3);
409
- });
410
- const tab = await chrome.tabs.get(tabId);
411
- return {
412
- id: cmd.id,
413
- ok: true,
414
- data: {
415
- title: tab.title,
416
- url: tab.url,
417
- tabId,
418
- timedOut
419
- }
420
- };
395
+ if (!cmd.url) return { id: cmd.id, ok: false, error: "Missing url" };
396
+ if (!isSafeNavigationUrl(cmd.url)) {
397
+ return { id: cmd.id, ok: false, error: "Blocked URL scheme -- only http:// and https:// are allowed" };
398
+ }
399
+ const tabId = await resolveTabId(cmd.tabId, workspace);
400
+ const beforeTab = await chrome.tabs.get(tabId);
401
+ const beforeNormalized = normalizeUrlForComparison(beforeTab.url);
402
+ const targetUrl = cmd.url;
403
+ if (beforeTab.status === "complete" && isTargetUrl(beforeTab.url, targetUrl)) {
404
+ return {
405
+ id: cmd.id,
406
+ ok: true,
407
+ data: { title: beforeTab.title, url: beforeTab.url, tabId, timedOut: false }
408
+ };
409
+ }
410
+ await detach(tabId);
411
+ await chrome.tabs.update(tabId, { url: targetUrl });
412
+ let timedOut = false;
413
+ await new Promise((resolve) => {
414
+ let settled = false;
415
+ let checkTimer = null;
416
+ let timeoutTimer = null;
417
+ const finish = () => {
418
+ if (settled) return;
419
+ settled = true;
420
+ chrome.tabs.onUpdated.removeListener(listener);
421
+ if (checkTimer) clearTimeout(checkTimer);
422
+ if (timeoutTimer) clearTimeout(timeoutTimer);
423
+ resolve();
424
+ };
425
+ const isNavigationDone = (url) => {
426
+ return isTargetUrl(url, targetUrl) || normalizeUrlForComparison(url) !== beforeNormalized;
427
+ };
428
+ const listener = (id, info, tab2) => {
429
+ if (id !== tabId) return;
430
+ if (info.status === "complete" && isNavigationDone(tab2.url ?? info.url)) {
431
+ finish();
432
+ }
433
+ };
434
+ chrome.tabs.onUpdated.addListener(listener);
435
+ checkTimer = setTimeout(async () => {
436
+ try {
437
+ const currentTab = await chrome.tabs.get(tabId);
438
+ if (currentTab.status === "complete" && isNavigationDone(currentTab.url)) {
439
+ finish();
440
+ }
441
+ } catch {
442
+ }
443
+ }, 100);
444
+ timeoutTimer = setTimeout(() => {
445
+ timedOut = true;
446
+ console.warn(`[opencli] Navigate to ${targetUrl} timed out after 15s`);
447
+ finish();
448
+ }, 15e3);
449
+ });
450
+ const tab = await chrome.tabs.get(tabId);
451
+ return {
452
+ id: cmd.id,
453
+ ok: true,
454
+ data: { title: tab.title, url: tab.url, tabId, timedOut }
455
+ };
421
456
  }
422
457
  async function handleTabs(cmd, workspace) {
423
- switch (cmd.op) {
424
- case "list": {
425
- const data = (await listAutomationWebTabs(workspace)).map((t, i) => ({
426
- index: i,
427
- tabId: t.id,
428
- url: t.url,
429
- title: t.title,
430
- active: t.active
431
- }));
432
- return {
433
- id: cmd.id,
434
- ok: true,
435
- data
436
- };
437
- }
438
- case "new": {
439
- const windowId = await getAutomationWindow(workspace);
440
- const tab = await chrome.tabs.create({
441
- windowId,
442
- url: cmd.url ?? "data:text/html,<html></html>",
443
- active: true
444
- });
445
- return {
446
- id: cmd.id,
447
- ok: true,
448
- data: {
449
- tabId: tab.id,
450
- url: tab.url
451
- }
452
- };
453
- }
454
- case "close": {
455
- if (cmd.index !== void 0) {
456
- const target = (await listAutomationWebTabs(workspace))[cmd.index];
457
- if (!target?.id) return {
458
- id: cmd.id,
459
- ok: false,
460
- error: `Tab index ${cmd.index} not found`
461
- };
462
- await chrome.tabs.remove(target.id);
463
- await detach(target.id);
464
- return {
465
- id: cmd.id,
466
- ok: true,
467
- data: { closed: target.id }
468
- };
469
- }
470
- const tabId = await resolveTabId(cmd.tabId, workspace);
471
- await chrome.tabs.remove(tabId);
472
- await detach(tabId);
473
- return {
474
- id: cmd.id,
475
- ok: true,
476
- data: { closed: tabId }
477
- };
478
- }
479
- case "select": {
480
- if (cmd.index === void 0 && cmd.tabId === void 0) return {
481
- id: cmd.id,
482
- ok: false,
483
- error: "Missing index or tabId"
484
- };
485
- if (cmd.tabId !== void 0) {
486
- await chrome.tabs.update(cmd.tabId, { active: true });
487
- return {
488
- id: cmd.id,
489
- ok: true,
490
- data: { selected: cmd.tabId }
491
- };
492
- }
493
- const target = (await listAutomationWebTabs(workspace))[cmd.index];
494
- if (!target?.id) return {
495
- id: cmd.id,
496
- ok: false,
497
- error: `Tab index ${cmd.index} not found`
498
- };
499
- await chrome.tabs.update(target.id, { active: true });
500
- return {
501
- id: cmd.id,
502
- ok: true,
503
- data: { selected: target.id }
504
- };
505
- }
506
- default: return {
507
- id: cmd.id,
508
- ok: false,
509
- error: `Unknown tabs op: ${cmd.op}`
510
- };
511
- }
458
+ switch (cmd.op) {
459
+ case "list": {
460
+ const tabs = await listAutomationWebTabs(workspace);
461
+ const data = tabs.map((t, i) => ({
462
+ index: i,
463
+ tabId: t.id,
464
+ url: t.url,
465
+ title: t.title,
466
+ active: t.active
467
+ }));
468
+ return { id: cmd.id, ok: true, data };
469
+ }
470
+ case "new": {
471
+ if (cmd.url && !isSafeNavigationUrl(cmd.url)) {
472
+ return { id: cmd.id, ok: false, error: "Blocked URL scheme -- only http:// and https:// are allowed" };
473
+ }
474
+ const windowId = await getAutomationWindow(workspace);
475
+ const tab = await chrome.tabs.create({ windowId, url: cmd.url ?? BLANK_PAGE, active: true });
476
+ return { id: cmd.id, ok: true, data: { tabId: tab.id, url: tab.url } };
477
+ }
478
+ case "close": {
479
+ if (cmd.index !== void 0) {
480
+ const tabs = await listAutomationWebTabs(workspace);
481
+ const target = tabs[cmd.index];
482
+ if (!target?.id) return { id: cmd.id, ok: false, error: `Tab index ${cmd.index} not found` };
483
+ await chrome.tabs.remove(target.id);
484
+ await detach(target.id);
485
+ return { id: cmd.id, ok: true, data: { closed: target.id } };
486
+ }
487
+ const tabId = await resolveTabId(cmd.tabId, workspace);
488
+ await chrome.tabs.remove(tabId);
489
+ await detach(tabId);
490
+ return { id: cmd.id, ok: true, data: { closed: tabId } };
491
+ }
492
+ case "select": {
493
+ if (cmd.index === void 0 && cmd.tabId === void 0)
494
+ return { id: cmd.id, ok: false, error: "Missing index or tabId" };
495
+ if (cmd.tabId !== void 0) {
496
+ const session = automationSessions.get(workspace);
497
+ let tab;
498
+ try {
499
+ tab = await chrome.tabs.get(cmd.tabId);
500
+ } catch {
501
+ return { id: cmd.id, ok: false, error: `Tab ${cmd.tabId} no longer exists` };
502
+ }
503
+ if (!session || tab.windowId !== session.windowId) {
504
+ return { id: cmd.id, ok: false, error: `Tab ${cmd.tabId} is not in the automation window` };
505
+ }
506
+ await chrome.tabs.update(cmd.tabId, { active: true });
507
+ return { id: cmd.id, ok: true, data: { selected: cmd.tabId } };
508
+ }
509
+ const tabs = await listAutomationWebTabs(workspace);
510
+ const target = tabs[cmd.index];
511
+ if (!target?.id) return { id: cmd.id, ok: false, error: `Tab index ${cmd.index} not found` };
512
+ await chrome.tabs.update(target.id, { active: true });
513
+ return { id: cmd.id, ok: true, data: { selected: target.id } };
514
+ }
515
+ default:
516
+ return { id: cmd.id, ok: false, error: `Unknown tabs op: ${cmd.op}` };
517
+ }
512
518
  }
513
519
  async function handleCookies(cmd) {
514
- const details = {};
515
- if (cmd.domain) details.domain = cmd.domain;
516
- if (cmd.url) details.url = cmd.url;
517
- const data = (await chrome.cookies.getAll(details)).map((c) => ({
518
- name: c.name,
519
- value: c.value,
520
- domain: c.domain,
521
- path: c.path,
522
- secure: c.secure,
523
- httpOnly: c.httpOnly,
524
- expirationDate: c.expirationDate
525
- }));
526
- return {
527
- id: cmd.id,
528
- ok: true,
529
- data
530
- };
520
+ if (!cmd.domain && !cmd.url) {
521
+ return { id: cmd.id, ok: false, error: "Cookie scope required: provide domain or url to avoid dumping all cookies" };
522
+ }
523
+ const details = {};
524
+ if (cmd.domain) details.domain = cmd.domain;
525
+ if (cmd.url) details.url = cmd.url;
526
+ const cookies = await chrome.cookies.getAll(details);
527
+ const data = cookies.map((c) => ({
528
+ name: c.name,
529
+ value: c.value,
530
+ domain: c.domain,
531
+ path: c.path,
532
+ secure: c.secure,
533
+ httpOnly: c.httpOnly,
534
+ expirationDate: c.expirationDate
535
+ }));
536
+ return { id: cmd.id, ok: true, data };
531
537
  }
532
538
  async function handleScreenshot(cmd, workspace) {
533
- const tabId = await resolveTabId(cmd.tabId, workspace);
534
- try {
535
- const data = await screenshot(tabId, {
536
- format: cmd.format,
537
- quality: cmd.quality,
538
- fullPage: cmd.fullPage
539
- });
540
- return {
541
- id: cmd.id,
542
- ok: true,
543
- data
544
- };
545
- } catch (err) {
546
- return {
547
- id: cmd.id,
548
- ok: false,
549
- error: err instanceof Error ? err.message : String(err)
550
- };
551
- }
539
+ const tabId = await resolveTabId(cmd.tabId, workspace);
540
+ try {
541
+ const data = await screenshot(tabId, {
542
+ format: cmd.format,
543
+ quality: cmd.quality,
544
+ fullPage: cmd.fullPage
545
+ });
546
+ return { id: cmd.id, ok: true, data };
547
+ } catch (err) {
548
+ return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) };
549
+ }
552
550
  }
553
551
  async function handleCloseWindow(cmd, workspace) {
554
- const session = automationSessions.get(workspace);
555
- if (session) {
556
- try {
557
- await chrome.windows.remove(session.windowId);
558
- } catch {}
559
- if (session.idleTimer) clearTimeout(session.idleTimer);
560
- automationSessions.delete(workspace);
561
- }
562
- return {
563
- id: cmd.id,
564
- ok: true,
565
- data: { closed: true }
566
- };
552
+ const session = automationSessions.get(workspace);
553
+ if (session) {
554
+ try {
555
+ await chrome.windows.remove(session.windowId);
556
+ } catch {
557
+ }
558
+ if (session.idleTimer) clearTimeout(session.idleTimer);
559
+ automationSessions.delete(workspace);
560
+ }
561
+ return { id: cmd.id, ok: true, data: { closed: true } };
567
562
  }
568
563
  async function handleSessions(cmd) {
569
- const now = Date.now();
570
- const data = await Promise.all([...automationSessions.entries()].map(async ([workspace, session]) => ({
571
- workspace,
572
- windowId: session.windowId,
573
- tabCount: (await chrome.tabs.query({ windowId: session.windowId })).filter((tab) => isDebuggableUrl(tab.url)).length,
574
- idleMsRemaining: Math.max(0, session.idleDeadlineAt - now)
575
- })));
576
- return {
577
- id: cmd.id,
578
- ok: true,
579
- data
580
- };
564
+ const now = Date.now();
565
+ const data = await Promise.all([...automationSessions.entries()].map(async ([workspace, session]) => ({
566
+ workspace,
567
+ windowId: session.windowId,
568
+ tabCount: (await chrome.tabs.query({ windowId: session.windowId })).filter((tab) => isDebuggableUrl(tab.url)).length,
569
+ idleMsRemaining: Math.max(0, session.idleDeadlineAt - now)
570
+ })));
571
+ return { id: cmd.id, ok: true, data };
581
572
  }
582
- //#endregion