@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
package/src/runtime.ts CHANGED
@@ -1,11 +1,7 @@
1
- /**
2
- * Runtime utilities: timeouts and browser session management.
3
- */
4
-
5
1
  import type { IPage } from './types.js';
6
2
 
7
3
  export const DEFAULT_BROWSER_CONNECT_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_CONNECT_TIMEOUT ?? '30', 10);
8
- export const DEFAULT_BROWSER_COMMAND_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_COMMAND_TIMEOUT ?? '45', 10);
4
+ export const DEFAULT_BROWSER_COMMAND_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_COMMAND_TIMEOUT ?? '60', 10);
9
5
  export const DEFAULT_BROWSER_EXPLORE_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_EXPLORE_TIMEOUT ?? '120', 10);
10
6
  export const DEFAULT_BROWSER_SMOKE_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_SMOKE_TIMEOUT ?? '60', 10);
11
7
 
@@ -32,7 +28,7 @@ export function withTimeoutMs<T>(promise: Promise<T>, timeoutMs: number, message
32
28
  });
33
29
  }
34
30
 
35
- /** Interface for browser factory (PlaywrightMCP or test mocks) */
31
+ /** Interface for browser factory (BrowserBridge or test mocks) */
36
32
  export interface IBrowserFactory {
37
33
  connect(opts?: { timeout?: number }): Promise<IPage>;
38
34
  close(): Promise<void>;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Injected script for detecting frontend frameworks (Vue, React, Next, Nuxt, etc.)
3
+ */
4
+ export function detectFramework() {
5
+ const r: Record<string, boolean> = {};
6
+ try {
7
+ const app = document.querySelector('#app') as any;
8
+ r.vue3 = !!(app && app.__vue_app__);
9
+ r.vue2 = !!(app && app.__vue__);
10
+ r.react = !!(window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__ || !!document.querySelector('[data-reactroot]');
11
+ r.nextjs = !!(window as any).__NEXT_DATA__;
12
+ r.nuxt = !!(window as any).__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
+ } catch {}
19
+ return r;
20
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Injected script for interactive fuzzing (clicking elements to trigger lazy loading)
3
+ */
4
+ export async function interactFuzz() {
5
+ const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
6
+ const clickables = Array.from(document.querySelectorAll(
7
+ 'button, [role="button"], [role="tab"], .tab, .btn, a[href="javascript:void(0)"], a[href="#"]'
8
+ )).slice(0, 15); // limit to a small number to avoid endless loops
9
+
10
+ let clicked = 0;
11
+ for (const el of clickables) {
12
+ try {
13
+ const rect = el.getBoundingClientRect();
14
+ if (rect.width > 0 && rect.height > 0) {
15
+ el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
16
+ clicked++;
17
+ await sleep(300); // give it time to trigger network
18
+ }
19
+ } catch {}
20
+ }
21
+ return clicked;
22
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Injected script for discovering Pinia or Vuex stores and their actions/state representations
3
+ */
4
+ export function discoverStores() {
5
+ const stores: Array<{ type: string; id: string; actions: string[]; stateKeys: string[] }> = [];
6
+ try {
7
+ const app = document.querySelector('#app') as any;
8
+ if (!app?.__vue_app__) return stores;
9
+ const gp = app.__vue_app__.config?.globalProperties;
10
+
11
+ // Pinia stores
12
+ const pinia = gp?.$pinia;
13
+ if (pinia?._s) {
14
+ pinia._s.forEach((store: any, id: string) => {
15
+ const actions: string[] = [];
16
+ const stateKeys: string[] = [];
17
+ for (const k in store) {
18
+ try {
19
+ if (k.startsWith('$') || k.startsWith('_')) continue;
20
+ if (typeof store[k] === 'function') actions.push(k);
21
+ else stateKeys.push(k);
22
+ } catch {}
23
+ }
24
+ stores.push({ type: 'pinia', id, actions: actions.slice(0, 20), stateKeys: stateKeys.slice(0, 15) });
25
+ });
26
+ }
27
+
28
+ // Vuex store modules
29
+ const vuex = gp?.$store;
30
+ if (vuex?._modules?.root?._children) {
31
+ const children = vuex._modules.root._children;
32
+ for (const [modName, mod] of Object.entries<any>(children)) {
33
+ const actions = Object.keys(mod._rawModule?.actions ?? {}).slice(0, 20);
34
+ const stateKeys = Object.keys(mod.state ?? {}).slice(0, 15);
35
+ stores.push({ type: 'vuex', id: modName, actions, stateKeys });
36
+ }
37
+ }
38
+ } catch {}
39
+ return stores;
40
+ }
package/src/setup.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  import chalk from 'chalk';
9
9
  import { checkDaemonStatus } from './browser/discover.js';
10
10
  import { checkConnectivity } from './doctor.js';
11
- import { PlaywrightMCP } from './browser/index.js';
11
+ import { BrowserBridge } from './browser/index.js';
12
12
 
13
13
  export async function runSetup(opts: { cliVersion?: string; token?: string } = {}) {
14
14
  console.log();
@@ -27,7 +27,7 @@ export async function runSetup(opts: { cliVersion?: string; token?: string } = {
27
27
  console.log(chalk.dim(' Starting daemon now...'));
28
28
 
29
29
  // Try to spawn daemon
30
- const mcp = new PlaywrightMCP();
30
+ const mcp = new BrowserBridge();
31
31
  try {
32
32
  await mcp.connect({ timeout: 5 });
33
33
  await mcp.close();
package/src/synthesize.ts CHANGED
@@ -116,7 +116,7 @@ function buildEvaluateScript(url: string, itemPath: string, endpoint: any): stri
116
116
 
117
117
  return [
118
118
  '(async () => {',
119
- ` const res = await fetch('${url}', {`,
119
+ ` const res = await fetch(${JSON.stringify(url)}, {`,
120
120
  ` credentials: 'include'`,
121
121
  ' });',
122
122
  ' const data = await res.json();',
@@ -6,12 +6,49 @@
6
6
  import { describe, it, expect } from 'vitest';
7
7
  import { runCli, parseJsonOutput } from './helpers.js';
8
8
 
9
- function isExpectedXiaoyuzhouRestriction(code: number, stderr: string): boolean {
9
+ function isExpectedChineseSiteRestriction(code: number, stderr: string): boolean {
10
10
  if (code === 0) return false;
11
11
  return /Error \[FETCH_ERROR\]: HTTP (403|429|451|503)\b/.test(stderr);
12
12
  }
13
13
 
14
+ // Keep old name as alias for existing tests
15
+ const isExpectedXiaoyuzhouRestriction = isExpectedChineseSiteRestriction;
16
+
14
17
  describe('public commands E2E', () => {
18
+ // ── apple-podcasts ──
19
+ it('apple-podcasts search returns structured podcast results', async () => {
20
+ const { stdout, code } = await runCli(['apple-podcasts', 'search', 'technology', '--limit', '3', '-f', 'json']);
21
+ expect(code).toBe(0);
22
+ const data = parseJsonOutput(stdout);
23
+ expect(Array.isArray(data)).toBe(true);
24
+ expect(data.length).toBeGreaterThanOrEqual(1);
25
+ expect(data[0]).toHaveProperty('id');
26
+ expect(data[0]).toHaveProperty('title');
27
+ expect(data[0]).toHaveProperty('author');
28
+ }, 30_000);
29
+
30
+ it('apple-podcasts episodes returns episode list from a known show', async () => {
31
+ const { stdout, code } = await runCli(['apple-podcasts', 'episodes', '275699983', '--limit', '3', '-f', 'json']);
32
+ expect(code).toBe(0);
33
+ const data = parseJsonOutput(stdout);
34
+ expect(Array.isArray(data)).toBe(true);
35
+ expect(data.length).toBeGreaterThanOrEqual(1);
36
+ expect(data[0]).toHaveProperty('title');
37
+ expect(data[0]).toHaveProperty('duration');
38
+ expect(data[0]).toHaveProperty('date');
39
+ }, 30_000);
40
+
41
+ it('apple-podcasts top returns ranked podcasts', async () => {
42
+ const { stdout, code } = await runCli(['apple-podcasts', 'top', '--limit', '3', '--country', 'us', '-f', 'json']);
43
+ expect(code).toBe(0);
44
+ const data = parseJsonOutput(stdout);
45
+ expect(Array.isArray(data)).toBe(true);
46
+ expect(data.length).toBe(3);
47
+ expect(data[0]).toHaveProperty('rank');
48
+ expect(data[0]).toHaveProperty('title');
49
+ expect(data[0]).toHaveProperty('id');
50
+ }, 30_000);
51
+
15
52
  // ── hackernews ──
16
53
  it('hackernews top returns structured data', async () => {
17
54
  const { stdout, code } = await runCli(['hackernews', 'top', '--limit', '3', '-f', 'json']);
@@ -115,4 +152,34 @@ describe('public commands E2E', () => {
115
152
  expect(code).not.toBe(0);
116
153
  expect(stderr).toMatch(/limit must be a positive integer|Argument "limit" must be a valid number/);
117
154
  }, 30_000);
155
+
156
+ // ── weread (Chinese site — may return empty on overseas CI runners) ──
157
+ it('weread search returns books', async () => {
158
+ const { stdout, stderr, code } = await runCli(['weread', 'search', 'python', '--limit', '3', '-f', 'json']);
159
+ if (isExpectedChineseSiteRestriction(code, stderr)) {
160
+ console.warn(`weread search skipped: ${stderr.trim()}`);
161
+ return;
162
+ }
163
+ expect(code).toBe(0);
164
+ const data = parseJsonOutput(stdout);
165
+ expect(Array.isArray(data)).toBe(true);
166
+ expect(data.length).toBeGreaterThanOrEqual(1);
167
+ expect(data[0]).toHaveProperty('title');
168
+ expect(data[0]).toHaveProperty('bookId');
169
+ }, 30_000);
170
+
171
+ it('weread ranking returns books', async () => {
172
+ const { stdout, stderr, code } = await runCli(['weread', 'ranking', 'all', '--limit', '3', '-f', 'json']);
173
+ if (isExpectedChineseSiteRestriction(code, stderr)) {
174
+ console.warn(`weread ranking skipped: ${stderr.trim()}`);
175
+ return;
176
+ }
177
+ expect(code).toBe(0);
178
+ const data = parseJsonOutput(stdout);
179
+ expect(Array.isArray(data)).toBe(true);
180
+ expect(data.length).toBeGreaterThanOrEqual(1);
181
+ expect(data[0]).toHaveProperty('title');
182
+ expect(data[0]).toHaveProperty('readingCount');
183
+ expect(data[0]).toHaveProperty('bookId');
184
+ }, 30_000);
118
185
  });
@@ -1 +0,0 @@
1
- export declare const debugCommand: import("../../registry.js").CliCommand;
@@ -1,45 +0,0 @@
1
- import { cli, Strategy } from '../../registry.js';
2
- export const debugCommand = cli({
3
- site: 'grok',
4
- name: 'debug',
5
- description: 'Debug grok page structure',
6
- domain: 'grok.com',
7
- strategy: Strategy.COOKIE,
8
- browser: true,
9
- columns: ['data'],
10
- func: async (page, _kwargs) => {
11
- await page.goto('https://grok.com');
12
- await page.wait(3);
13
- // Get all button-like elements near textarea
14
- const debug = await page.evaluate(`(() => {
15
- const ta = document.querySelector('textarea');
16
- if (!ta) return { error: 'no textarea' };
17
-
18
- // Get parent containers
19
- let parent = ta.parentElement;
20
- const parents = [];
21
- for (let i = 0; i < 5 && parent; i++) {
22
- parents.push({
23
- tag: parent.tagName,
24
- class: parent.className?.substring(0, 80),
25
- childCount: parent.children.length,
26
- });
27
- parent = parent.parentElement;
28
- }
29
-
30
- // Find buttons in the form/container near textarea
31
- const form = ta.closest('form') || ta.closest('[class*="composer"]') || ta.closest('[class*="input"]') || ta.parentElement?.parentElement;
32
- const buttons = form ? [...form.querySelectorAll('button')].map(b => ({
33
- testid: b.getAttribute('data-testid'),
34
- type: b.type,
35
- disabled: b.disabled,
36
- text: (b.textContent || '').substring(0, 30),
37
- html: b.outerHTML.substring(0, 200),
38
- rect: b.getBoundingClientRect().toJSON(),
39
- })) : [];
40
-
41
- return { parents, buttons, formTag: form?.tagName, formClass: form?.className?.substring(0, 80) };
42
- })()`);
43
- return [{ data: JSON.stringify(debug, null, 2) }];
44
- },
45
- });
@@ -1,49 +0,0 @@
1
- import { cli, Strategy } from '../../registry.js';
2
- import type { IPage } from '../../types.js';
3
-
4
- export const debugCommand = cli({
5
- site: 'grok',
6
- name: 'debug',
7
- description: 'Debug grok page structure',
8
- domain: 'grok.com',
9
- strategy: Strategy.COOKIE,
10
- browser: true,
11
- columns: ['data'],
12
- func: async (page: IPage, _kwargs: Record<string, any>) => {
13
- await page.goto('https://grok.com');
14
- await page.wait(3);
15
-
16
- // Get all button-like elements near textarea
17
- const debug = await page.evaluate(`(() => {
18
- const ta = document.querySelector('textarea');
19
- if (!ta) return { error: 'no textarea' };
20
-
21
- // Get parent containers
22
- let parent = ta.parentElement;
23
- const parents = [];
24
- for (let i = 0; i < 5 && parent; i++) {
25
- parents.push({
26
- tag: parent.tagName,
27
- class: parent.className?.substring(0, 80),
28
- childCount: parent.children.length,
29
- });
30
- parent = parent.parentElement;
31
- }
32
-
33
- // Find buttons in the form/container near textarea
34
- const form = ta.closest('form') || ta.closest('[class*="composer"]') || ta.closest('[class*="input"]') || ta.parentElement?.parentElement;
35
- const buttons = form ? [...form.querySelectorAll('button')].map(b => ({
36
- testid: b.getAttribute('data-testid'),
37
- type: b.type,
38
- disabled: b.disabled,
39
- text: (b.textContent || '').substring(0, 30),
40
- html: b.outerHTML.substring(0, 200),
41
- rect: b.getBoundingClientRect().toJSON(),
42
- })) : [];
43
-
44
- return { parents, buttons, formTag: form?.tagName, formClass: form?.className?.substring(0, 80) };
45
- })()`);
46
-
47
- return [{ data: JSON.stringify(debug, null, 2) }];
48
- },
49
- });