@jackwener/opencli 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +20 -1
  2. package/README.zh-CN.md +20 -1
  3. package/dist/browser/daemon-client.d.ts +1 -1
  4. package/dist/browser/index.d.ts +1 -2
  5. package/dist/browser/index.js +1 -5
  6. package/dist/browser/mcp.d.ts +5 -8
  7. package/dist/browser/mcp.js +9 -10
  8. package/dist/browser/page.d.ts +8 -1
  9. package/dist/browser/page.js +23 -17
  10. package/dist/browser.test.js +6 -6
  11. package/dist/cli-manifest.json +394 -14
  12. package/dist/clis/apple-podcasts/episodes.d.ts +1 -0
  13. package/dist/clis/apple-podcasts/episodes.js +28 -0
  14. package/dist/clis/apple-podcasts/search.d.ts +1 -0
  15. package/dist/clis/apple-podcasts/search.js +29 -0
  16. package/dist/clis/apple-podcasts/top.d.ts +1 -0
  17. package/dist/clis/apple-podcasts/top.js +34 -0
  18. package/dist/clis/apple-podcasts/utils.d.ts +11 -0
  19. package/dist/clis/apple-podcasts/utils.js +30 -0
  20. package/dist/clis/apple-podcasts/utils.test.d.ts +1 -0
  21. package/dist/clis/apple-podcasts/utils.test.js +57 -0
  22. package/dist/clis/chatwise/history.js +18 -1
  23. package/dist/clis/discord-app/channels.js +33 -21
  24. package/dist/clis/twitter/accept.d.ts +1 -0
  25. package/dist/clis/twitter/accept.js +202 -0
  26. package/dist/clis/twitter/followers.js +30 -22
  27. package/dist/clis/twitter/following.js +19 -14
  28. package/dist/clis/twitter/notifications.js +29 -22
  29. package/dist/clis/twitter/reply-dm.d.ts +1 -0
  30. package/dist/clis/twitter/reply-dm.js +181 -0
  31. package/dist/clis/twitter/search.js +50 -12
  32. package/dist/clis/weread/book.d.ts +1 -0
  33. package/dist/clis/weread/book.js +26 -0
  34. package/dist/clis/weread/highlights.d.ts +1 -0
  35. package/dist/clis/weread/highlights.js +23 -0
  36. package/dist/clis/weread/notebooks.d.ts +1 -0
  37. package/dist/clis/weread/notebooks.js +21 -0
  38. package/dist/clis/weread/notes.d.ts +1 -0
  39. package/dist/clis/weread/notes.js +29 -0
  40. package/dist/clis/weread/ranking.d.ts +1 -0
  41. package/dist/clis/weread/ranking.js +28 -0
  42. package/dist/clis/weread/search.d.ts +1 -0
  43. package/dist/clis/weread/search.js +25 -0
  44. package/dist/clis/weread/shelf.d.ts +1 -0
  45. package/dist/clis/weread/shelf.js +24 -0
  46. package/dist/clis/weread/utils.d.ts +20 -0
  47. package/dist/clis/weread/utils.js +72 -0
  48. package/dist/clis/weread/utils.test.d.ts +1 -0
  49. package/dist/clis/weread/utils.test.js +85 -0
  50. package/dist/daemon.js +2 -2
  51. package/dist/doctor.d.ts +0 -21
  52. package/dist/doctor.js +2 -24
  53. package/dist/main.js +6 -16
  54. package/dist/runtime.d.ts +1 -4
  55. package/dist/runtime.js +1 -4
  56. package/dist/setup.js +2 -2
  57. package/extension/dist/background.js +484 -0
  58. package/extension/manifest.json +1 -1
  59. package/extension/package.json +1 -1
  60. package/extension/src/background.ts +99 -22
  61. package/extension/src/protocol.ts +1 -1
  62. package/package.json +1 -1
  63. package/src/browser/daemon-client.ts +1 -1
  64. package/src/browser/index.ts +1 -6
  65. package/src/browser/mcp.ts +14 -15
  66. package/src/browser/page.ts +23 -17
  67. package/src/browser.test.ts +6 -6
  68. package/src/clis/apple-podcasts/episodes.ts +28 -0
  69. package/src/clis/apple-podcasts/search.ts +29 -0
  70. package/src/clis/apple-podcasts/top.ts +34 -0
  71. package/src/clis/apple-podcasts/utils.test.ts +72 -0
  72. package/src/clis/apple-podcasts/utils.ts +37 -0
  73. package/src/clis/chatwise/history.ts +15 -1
  74. package/src/clis/discord-app/channels.ts +33 -21
  75. package/src/clis/twitter/accept.ts +213 -0
  76. package/src/clis/twitter/followers.ts +36 -29
  77. package/src/clis/twitter/following.ts +25 -20
  78. package/src/clis/twitter/notifications.ts +34 -27
  79. package/src/clis/twitter/reply-dm.ts +193 -0
  80. package/src/clis/twitter/search.ts +53 -13
  81. package/src/clis/weread/book.ts +28 -0
  82. package/src/clis/weread/highlights.ts +25 -0
  83. package/src/clis/weread/notebooks.ts +23 -0
  84. package/src/clis/weread/notes.ts +31 -0
  85. package/src/clis/weread/ranking.ts +29 -0
  86. package/src/clis/weread/search.ts +26 -0
  87. package/src/clis/weread/shelf.ts +26 -0
  88. package/src/clis/weread/utils.test.ts +104 -0
  89. package/src/clis/weread/utils.ts +74 -0
  90. package/src/daemon.ts +2 -2
  91. package/src/doctor.ts +2 -19
  92. package/src/main.ts +5 -11
  93. package/src/runtime.ts +2 -6
  94. package/src/setup.ts +2 -2
  95. package/tests/e2e/public-commands.test.ts +68 -1
  96. package/dist/clis/grok/debug.d.ts +0 -1
  97. package/dist/clis/grok/debug.js +0 -45
  98. package/src/clis/grok/debug.ts +0 -49
package/src/doctor.ts CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  import chalk from 'chalk';
9
9
  import { checkDaemonStatus } from './browser/discover.js';
10
- import { PlaywrightMCP } from './browser/index.js';
10
+ import { BrowserBridge } from './browser/index.js';
11
11
  import { browserSession } from './runtime.js';
12
12
 
13
13
  export type DoctorOptions = {
@@ -37,7 +37,7 @@ export type DoctorReport = {
37
37
  export async function checkConnectivity(opts?: { timeout?: number }): Promise<ConnectivityResult> {
38
38
  const start = Date.now();
39
39
  try {
40
- const mcp = new PlaywrightMCP();
40
+ const mcp = new BrowserBridge();
41
41
  const page = await mcp.connect({ timeout: opts?.timeout ?? 8 });
42
42
  // Try a simple eval to verify end-to-end connectivity
43
43
  await page.evaluate('1 + 1');
@@ -116,20 +116,3 @@ export function renderBrowserDoctorReport(report: DoctorReport): string {
116
116
  return lines.join('\n');
117
117
  }
118
118
 
119
- // Backward compatibility exports (no-ops for things that no longer exist)
120
- export const PLAYWRIGHT_TOKEN_ENV = 'PLAYWRIGHT_MCP_EXTENSION_TOKEN';
121
- export function discoverExtensionToken(): string | null { return null; }
122
- export function checkExtensionInstalled(): { installed: boolean; browsers: string[] } { return { installed: false, browsers: [] }; }
123
- export function applyBrowserDoctorFix(): Promise<string[]> { return Promise.resolve([]); }
124
- export function getDefaultShellRcPath(): string { return ''; }
125
- export function getDefaultMcpConfigPaths(): string[] { return []; }
126
- export function readTokenFromShellContent(_content: string): string | null { return null; }
127
- export function upsertShellToken(content: string): string { return content; }
128
- export function upsertJsonConfigToken(content: string): string { return content; }
129
- export function readTomlConfigToken(_content: string): string | null { return null; }
130
- export function upsertTomlConfigToken(content: string): string { return content; }
131
- export function shortenPath(p: string): string { return p; }
132
- export function toolName(_p: string): string { return ''; }
133
- export function fileExists(filePath: string): boolean { try { return require('node:fs').existsSync(filePath); } catch { return false; } }
134
- export function writeFileWithMkdir(_p: string, _c: string): void {}
135
- export async function checkTokenConnectivity(opts?: { timeout?: number }): Promise<ConnectivityResult> { return checkConnectivity(opts); }
package/src/main.ts CHANGED
@@ -11,7 +11,7 @@ import chalk from 'chalk';
11
11
  import { discoverClis, executeCommand } from './engine.js';
12
12
  import { Strategy, type CliCommand, fullName, getRegistry, strategyLabel } from './registry.js';
13
13
  import { render as renderOutput } from './output.js';
14
- import { PlaywrightMCP } from './browser/index.js';
14
+ import { BrowserBridge } from './browser/index.js';
15
15
  import { browserSession, DEFAULT_BROWSER_COMMAND_TIMEOUT, runWithTimeout } from './runtime.js';
16
16
  import { PKG_VERSION } from './version.js';
17
17
  import { getCompletions, printCompletionScript } from './completion.js';
@@ -99,18 +99,18 @@ program.command('verify').description('Validate + smoke test').argument('[target
99
99
  });
100
100
 
101
101
  program.command('explore').alias('probe').description('Explore a website: discover APIs, stores, and recommend strategies').argument('<url>').option('--site <name>').option('--goal <text>').option('--wait <s>', '', '3').option('--auto', 'Enable interactive fuzzing (simulate clicks to trigger lazy APIs)').option('--click <labels>', 'Comma-separated labels to click before fuzzing (e.g. "字幕,CC,评论")')
102
- .action(async (url, opts) => { const { exploreUrl, renderExploreSummary } = await import('./explore.js'); const clickLabels = opts.click ? opts.click.split(',').map((s: string) => s.trim()) : undefined; console.log(renderExploreSummary(await exploreUrl(url, { BrowserFactory: PlaywrightMCP, site: opts.site, goal: opts.goal, waitSeconds: parseFloat(opts.wait), auto: opts.auto, clickLabels }))); });
102
+ .action(async (url, opts) => { const { exploreUrl, renderExploreSummary } = await import('./explore.js'); const clickLabels = opts.click ? opts.click.split(',').map((s: string) => s.trim()) : undefined; console.log(renderExploreSummary(await exploreUrl(url, { BrowserFactory: BrowserBridge, site: opts.site, goal: opts.goal, waitSeconds: parseFloat(opts.wait), auto: opts.auto, clickLabels }))); });
103
103
 
104
104
  program.command('synthesize').description('Synthesize CLIs from explore').argument('<target>').option('--top <n>', '', '3')
105
105
  .action(async (target, opts) => { const { synthesizeFromExplore, renderSynthesizeSummary } = await import('./synthesize.js'); console.log(renderSynthesizeSummary(synthesizeFromExplore(target, { top: parseInt(opts.top) }))); });
106
106
 
107
107
  program.command('generate').description('One-shot: explore → synthesize → register').argument('<url>').option('--goal <text>').option('--site <name>')
108
- .action(async (url, opts) => { const { generateCliFromUrl, renderGenerateSummary } = await import('./generate.js'); const r = await generateCliFromUrl({ url, BrowserFactory: PlaywrightMCP, builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, goal: opts.goal, site: opts.site }); console.log(renderGenerateSummary(r)); process.exitCode = r.ok ? 0 : 1; });
108
+ .action(async (url, opts) => { const { generateCliFromUrl, renderGenerateSummary } = await import('./generate.js'); const r = await generateCliFromUrl({ url, BrowserFactory: BrowserBridge, builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, goal: opts.goal, site: opts.site }); console.log(renderGenerateSummary(r)); process.exitCode = r.ok ? 0 : 1; });
109
109
 
110
110
  program.command('cascade').description('Strategy cascade: find simplest working strategy').argument('<url>').option('--site <name>')
111
111
  .action(async (url, opts) => {
112
112
  const { cascadeProbe, renderCascadeResult } = await import('./cascade.js');
113
- const result = await browserSession(PlaywrightMCP, async (page) => {
113
+ const result = await browserSession(BrowserBridge, async (page) => {
114
114
  // Navigate to the site first for cookie context
115
115
  try { const siteUrl = new URL(url); await page.goto(`${siteUrl.protocol}//${siteUrl.host}`); await page.wait(2); } catch {}
116
116
  return cascadeProbe(page, url);
@@ -192,13 +192,7 @@ for (const [, cmd] of registry) {
192
192
  if (actionOpts.verbose) process.env.OPENCLI_VERBOSE = '1';
193
193
  let result: any;
194
194
  if (cmd.browser) {
195
- result = await browserSession(PlaywrightMCP, async (page) => {
196
- // Cookie/header strategies require same-origin context for credentialed fetch.
197
- // In CDP mode the active tab may be on an unrelated domain, causing CORS failures.
198
- // Navigate to the command's domain first (mirrors cascade command behavior).
199
- if ((cmd.strategy === Strategy.COOKIE || cmd.strategy === Strategy.HEADER) && cmd.domain) {
200
- try { await page.goto(`https://${cmd.domain}`); await page.wait(2); } catch {}
201
- }
195
+ result = await browserSession(BrowserBridge, async (page) => {
202
196
  return runWithTimeout(executeCommand(cmd, page, kwargs, actionOpts.verbose), { timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT, label: fullName(cmd) });
203
197
  });
204
198
  } else { result = await executeCommand(cmd, null, kwargs, actionOpts.verbose); }
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>;
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();
@@ -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
- });