@adhdev/daemon-core 0.7.5 → 0.7.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +164 -38
- package/dist/index.d.ts +164 -38
- package/dist/index.js +4051 -2547
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3696 -2192
- package/dist/index.mjs.map +1 -1
- package/dist/{normalize-tKg8IiDk.d.mts → normalize-auJAPmKy.d.mts} +669 -629
- package/dist/{normalize-tKg8IiDk.d.ts → normalize-auJAPmKy.d.ts} +669 -629
- package/dist/status/normalize.d.mts +1 -1
- package/dist/status/normalize.d.ts +1 -1
- package/package.json +5 -1
- package/src/agent-stream/forward.ts +6 -0
- package/src/boot/daemon-lifecycle.ts +7 -4
- package/src/cli-adapter-types.ts +2 -0
- package/src/cli-adapters/provider-cli-adapter.ts +148 -11
- package/src/cli-adapters/pty-transport.ts +100 -0
- package/src/cli-adapters/session-host-transport.ts +392 -0
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +126 -0
- package/src/cli-adapters/terminal-backends/types.ts +17 -0
- package/src/cli-adapters/terminal-backends/xterm-backend.ts +87 -0
- package/src/cli-adapters/terminal-screen.ts +40 -53
- package/src/commands/cli-manager.ts +184 -55
- package/src/config/config.d.ts +116 -0
- package/src/config/workspace-activity.d.ts +22 -0
- package/src/config/workspaces.d.ts +84 -0
- package/src/daemon/dev-auto-implement.ts +1087 -0
- package/src/daemon/dev-cdp-handlers.ts +1003 -0
- package/src/daemon/dev-cli-debug.ts +288 -0
- package/src/daemon/dev-server-types.ts +45 -0
- package/src/daemon/dev-server.ts +121 -1698
- package/src/index.ts +5 -1
- package/src/providers/cli-provider-instance.ts +13 -1
- package/src/providers/contracts.d.ts +408 -0
- package/src/providers/contracts.ts +9 -0
- package/src/providers/extension-provider-instance.ts +50 -10
- package/src/providers/provider-instance-manager.ts +48 -10
- package/src/providers/provider-instance.d.ts +142 -0
- package/src/providers/provider-instance.ts +23 -1
- package/src/shared-types.d.ts +157 -0
- package/src/shared-types.ts +14 -0
- package/src/status/builders.ts +6 -0
- package/src/status/normalize.d.ts +14 -0
- package/src/types.d.ts +127 -0
|
@@ -0,0 +1,1003 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DevServer — CDP & DOM Handlers
|
|
3
|
+
*
|
|
4
|
+
* Extracted from dev-server.ts for maintainability.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import type * as http from 'http';
|
|
10
|
+
import type { DevServerContext } from './dev-server-types.js';
|
|
11
|
+
import { LOG } from '../logging/logger.js';
|
|
12
|
+
|
|
13
|
+
export async function handleCdpEvaluate(ctx: DevServerContext, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
14
|
+
const body = await ctx.readBody(req);
|
|
15
|
+
const { expression, timeout, ideType } = body;
|
|
16
|
+
if (!expression) {
|
|
17
|
+
ctx.json(res, 400, { error: 'expression required' });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const cdp = ctx.getCdp(ideType);
|
|
22
|
+
if (!cdp && !ideType) {
|
|
23
|
+
LOG.warn('DevServer', 'CDP evaluate without ideType — picked first connected manager');
|
|
24
|
+
}
|
|
25
|
+
if (!cdp?.isConnected) {
|
|
26
|
+
ctx.json(res, 503, { error: 'No CDP connection available' });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const raw = await cdp.evaluate(expression, timeout || 30000);
|
|
32
|
+
let result = raw;
|
|
33
|
+
if (typeof raw === 'string') {
|
|
34
|
+
try { result = JSON.parse(raw); } catch { /* keep */ }
|
|
35
|
+
}
|
|
36
|
+
ctx.json(res, 200, { result });
|
|
37
|
+
} catch (e: any) {
|
|
38
|
+
ctx.json(res, 500, { error: e.message });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function handleCdpClick(ctx: DevServerContext, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
43
|
+
const body = await ctx.readBody(req);
|
|
44
|
+
const { ideType, x, y } = body;
|
|
45
|
+
if (x == null || y == null) {
|
|
46
|
+
ctx.json(res, 400, { error: 'x and y coordinates required' });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const cdp = ctx.getCdp(ideType);
|
|
51
|
+
if (!cdp?.isConnected) {
|
|
52
|
+
ctx.json(res, 503, { error: 'No CDP connection available' });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
await cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', x, y, button: 'left', clickCount: 1 });
|
|
58
|
+
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x, y, button: 'left', clickCount: 1 });
|
|
59
|
+
ctx.json(res, 200, { success: true, clicked: true, x, y });
|
|
60
|
+
} catch (e: any) {
|
|
61
|
+
ctx.json(res, 500, { error: e.message });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function handleCdpDomQuery(ctx: DevServerContext, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
66
|
+
const body = await ctx.readBody(req);
|
|
67
|
+
const { selector, limit = 10, ideType } = body;
|
|
68
|
+
if (!selector) {
|
|
69
|
+
ctx.json(res, 400, { error: 'selector required' });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const cdp = ctx.getCdp(ideType as string);
|
|
74
|
+
if (!cdp) {
|
|
75
|
+
ctx.json(res, 503, { error: 'No CDP connection available' });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const expr = `(() => {
|
|
80
|
+
try {
|
|
81
|
+
const els = document.querySelectorAll('${selector.replace(/'/g, "\\'")}');
|
|
82
|
+
const results = [];
|
|
83
|
+
for (let i = 0; i < Math.min(els.length, ${limit}); i++) {
|
|
84
|
+
const el = els[i];
|
|
85
|
+
results.push({
|
|
86
|
+
index: i,
|
|
87
|
+
tag: el.tagName?.toLowerCase(),
|
|
88
|
+
id: el.id || null,
|
|
89
|
+
class: el.className && typeof el.className === 'string' ? el.className.trim().slice(0, 200) : null,
|
|
90
|
+
role: el.getAttribute?.('role') || null,
|
|
91
|
+
text: (el.textContent || '').trim().slice(0, 100),
|
|
92
|
+
visible: el.offsetParent !== null || el.offsetWidth > 0,
|
|
93
|
+
rect: (() => { try { const r = el.getBoundingClientRect(); return { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) }; } catch { return null; } })()
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return JSON.stringify({ total: els.length, results });
|
|
97
|
+
} catch (e) { return JSON.stringify({ error: e.message }); }
|
|
98
|
+
})()`;
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const raw = await cdp.evaluate(expr, 10000);
|
|
102
|
+
const result = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
103
|
+
ctx.json(res, 200, result);
|
|
104
|
+
} catch (e: any) {
|
|
105
|
+
ctx.json(res, 500, { error: e.message });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function handleScreenshot(ctx: DevServerContext, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
110
|
+
const url = new URL(req.url || '/', 'http://localhost');
|
|
111
|
+
const ideType = url.searchParams.get('ideType') || undefined;
|
|
112
|
+
const cdp = ctx.getCdp(ideType);
|
|
113
|
+
if (!cdp) {
|
|
114
|
+
ctx.json(res, 503, { error: 'No CDP connection available' });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
// Get viewport metrics before capturing
|
|
120
|
+
let vpW = 0, vpH = 0;
|
|
121
|
+
try {
|
|
122
|
+
const metrics = await cdp.send('Page.getLayoutMetrics', {}, 3000);
|
|
123
|
+
const vp = metrics?.cssVisualViewport || metrics?.visualViewport;
|
|
124
|
+
if (vp) {
|
|
125
|
+
vpW = Math.round(vp.clientWidth || vp.width || 0);
|
|
126
|
+
vpH = Math.round(vp.clientHeight || vp.height || 0);
|
|
127
|
+
}
|
|
128
|
+
} catch { /* ignore */ }
|
|
129
|
+
|
|
130
|
+
const buf = await cdp.captureScreenshot();
|
|
131
|
+
if (buf) {
|
|
132
|
+
res.writeHead(200, {
|
|
133
|
+
'Content-Type': 'image/webp',
|
|
134
|
+
'X-Viewport-Width': String(vpW),
|
|
135
|
+
'X-Viewport-Height': String(vpH),
|
|
136
|
+
});
|
|
137
|
+
res.end(buf);
|
|
138
|
+
} else {
|
|
139
|
+
ctx.json(res, 500, { error: 'Screenshot failed' });
|
|
140
|
+
}
|
|
141
|
+
} catch (e: any) {
|
|
142
|
+
ctx.json(res, 500, { error: e.message });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function handleScriptsRun(ctx: DevServerContext, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
147
|
+
const body = await ctx.readBody(req);
|
|
148
|
+
const { type, script: scriptName, params } = body;
|
|
149
|
+
if (!type || !scriptName) {
|
|
150
|
+
ctx.json(res, 400, { error: 'type and script required' });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// Delegate to handleRunScript
|
|
154
|
+
await this.handleRunScript(type, req, res, body);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function handleTypeAndSend(ctx: DevServerContext, type: string, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
158
|
+
const body = await ctx.readBody(req);
|
|
159
|
+
const { selector, text } = body;
|
|
160
|
+
if (!selector || typeof selector !== 'string' || !text || typeof text !== 'string') {
|
|
161
|
+
ctx.json(res, 400, { error: 'selector and text strings required' }); return;
|
|
162
|
+
}
|
|
163
|
+
const cdp = ctx.getCdp(type);
|
|
164
|
+
if (!cdp) {
|
|
165
|
+
ctx.json(res, 503, { error: `CDP not connected for '${type}'` }); return;
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
const sent = await cdp.typeAndSend(selector, text);
|
|
169
|
+
ctx.json(res, 200, { sent });
|
|
170
|
+
} catch (e: any) {
|
|
171
|
+
ctx.json(res, 500, { error: e.message });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function handleTypeAndSendAt(ctx: DevServerContext, type: string, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
176
|
+
const body = await ctx.readBody(req);
|
|
177
|
+
const { x, y, text } = body;
|
|
178
|
+
if (typeof x !== 'number' || typeof y !== 'number' || !text || typeof text !== 'string') {
|
|
179
|
+
ctx.json(res, 400, { error: 'x, y numbers and text string required' }); return;
|
|
180
|
+
}
|
|
181
|
+
const cdp = ctx.getCdp(type);
|
|
182
|
+
if (!cdp) {
|
|
183
|
+
ctx.json(res, 503, { error: `CDP not connected for '${type}'` }); return;
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const sent = await cdp.typeAndSendAt(x, y, text);
|
|
187
|
+
ctx.json(res, 200, { sent });
|
|
188
|
+
} catch (e: any) {
|
|
189
|
+
ctx.json(res, 500, { error: e.message });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function handleScriptHints(ctx: DevServerContext, type: string, _req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
194
|
+
const dir = this.findProviderDir(type);
|
|
195
|
+
if (!dir) { ctx.json(res, 404, { error: `Provider not found: ${type}` }); return; }
|
|
196
|
+
|
|
197
|
+
// Find scripts.js in the provider dir (may be versioned)
|
|
198
|
+
let scriptsPath = '';
|
|
199
|
+
const directScripts = path.join(dir, 'scripts.js');
|
|
200
|
+
if (fs.existsSync(directScripts)) {
|
|
201
|
+
scriptsPath = directScripts;
|
|
202
|
+
} else {
|
|
203
|
+
// Check versioned scripts dirs
|
|
204
|
+
const scriptsDir = path.join(dir, 'scripts');
|
|
205
|
+
if (fs.existsSync(scriptsDir)) {
|
|
206
|
+
const versions = fs.readdirSync(scriptsDir).filter(d => {
|
|
207
|
+
return fs.statSync(path.join(scriptsDir, d)).isDirectory();
|
|
208
|
+
}).sort().reverse();
|
|
209
|
+
for (const ver of versions) {
|
|
210
|
+
const p = path.join(scriptsDir, ver, 'scripts.js');
|
|
211
|
+
if (fs.existsSync(p)) { scriptsPath = p; break; }
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!scriptsPath) {
|
|
217
|
+
ctx.json(res, 200, { hints: {} });
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const source = fs.readFileSync(scriptsPath, 'utf-8');
|
|
223
|
+
const hints: Record<string, { template: Record<string, any>; description: string }> = {};
|
|
224
|
+
|
|
225
|
+
// Parse exported functions and extract param usage
|
|
226
|
+
const funcRegex = /module\.exports\.(\w+)\s*=\s*function\s+\w+\s*\(params\)/g;
|
|
227
|
+
let match;
|
|
228
|
+
while ((match = funcRegex.exec(source)) !== null) {
|
|
229
|
+
const name = match[1];
|
|
230
|
+
// Find the function body (rough: from match to next module.exports or end)
|
|
231
|
+
const startIdx = match.index;
|
|
232
|
+
const nextFunc = source.indexOf('module.exports.', startIdx + 1);
|
|
233
|
+
const funcBody = source.substring(startIdx, nextFunc > 0 ? nextFunc : source.length);
|
|
234
|
+
|
|
235
|
+
const paramFields: Record<string, any> = {};
|
|
236
|
+
|
|
237
|
+
// Pattern 1: params?.xxx or params.xxx
|
|
238
|
+
const dotRegex = /params\?\.([a-zA-Z_]+)|params\.([a-zA-Z_]+)/g;
|
|
239
|
+
let dm;
|
|
240
|
+
while ((dm = dotRegex.exec(funcBody)) !== null) {
|
|
241
|
+
const field = dm[1] || dm[2];
|
|
242
|
+
if (field === 'length') continue;
|
|
243
|
+
if (!(field in paramFields)) {
|
|
244
|
+
// Infer type from context
|
|
245
|
+
if (/index|count|port|timeout/i.test(field)) paramFields[field] = 0;
|
|
246
|
+
else if (/action|text|title|message|model|mode|button|name|filter/i.test(field)) paramFields[field] = '';
|
|
247
|
+
else paramFields[field] = '';
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Pattern 2: typeof params === 'string' ? params : params?.xxx
|
|
252
|
+
const typeofRegex = /typeof params === 'string' \? params : params\?\.([a-zA-Z_]+)/g;
|
|
253
|
+
let tm;
|
|
254
|
+
while ((tm = typeofRegex.exec(funcBody)) !== null) {
|
|
255
|
+
const field = tm[1];
|
|
256
|
+
if (!(field in paramFields)) paramFields[field] = '';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Pattern 3: typeof params === 'number' ? params : params?.xxx
|
|
260
|
+
const numRegex = /typeof params === 'number' \? params : params\?\.([a-zA-Z_]+)/g;
|
|
261
|
+
let nm;
|
|
262
|
+
while ((nm = numRegex.exec(funcBody)) !== null) {
|
|
263
|
+
const field = nm[1];
|
|
264
|
+
if (!(field in paramFields)) paramFields[field] = 0;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Determine description from function name
|
|
268
|
+
const descriptions: Record<string, string> = {
|
|
269
|
+
readChat: 'No params required',
|
|
270
|
+
sendMessage: 'Text to send to the chat',
|
|
271
|
+
listSessions: 'No params required',
|
|
272
|
+
switchSession: 'Switch by index or title',
|
|
273
|
+
newSession: 'No params required',
|
|
274
|
+
focusEditor: 'No params required',
|
|
275
|
+
openPanel: 'No params required',
|
|
276
|
+
resolveAction: 'Approve/reject action buttons',
|
|
277
|
+
listNotifications: 'Optional message filter',
|
|
278
|
+
dismissNotification: 'Dismiss by index, message, or button',
|
|
279
|
+
listModels: 'No params required',
|
|
280
|
+
setModel: 'Model name to select',
|
|
281
|
+
listModes: 'No params required',
|
|
282
|
+
setMode: 'Mode name to select',
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
hints[name] = {
|
|
286
|
+
template: Object.keys(paramFields).length > 0 ? paramFields : {},
|
|
287
|
+
description: descriptions[name] || (Object.keys(paramFields).length > 0 ? 'Params: ' + Object.keys(paramFields).join(', ') : 'No params'),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
ctx.json(res, 200, { hints });
|
|
292
|
+
} catch (e: any) {
|
|
293
|
+
ctx.json(res, 500, { error: e.message });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export async function handleCdpTargets(ctx: DevServerContext, _req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
298
|
+
const targets: { ide: string; connected: boolean; port: number }[] = [];
|
|
299
|
+
for (const [ide, cdp] of ctx.cdpManagers.entries()) {
|
|
300
|
+
targets.push({ ide, connected: cdp.isConnected, port: cdp.getPort() });
|
|
301
|
+
}
|
|
302
|
+
ctx.json(res, 200, { targets });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export async function handleDomInspect(ctx: DevServerContext, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
306
|
+
const body = await ctx.readBody(req);
|
|
307
|
+
const { x, y, selector, ideType } = body;
|
|
308
|
+
const cdp = ctx.getCdp(ideType);
|
|
309
|
+
if (!cdp) { ctx.json(res, 503, { error: 'No CDP connection' }); return; }
|
|
310
|
+
|
|
311
|
+
const selectorArg = selector ? JSON.stringify(selector) : 'null';
|
|
312
|
+
const inspectScript = `(() => {
|
|
313
|
+
function gs(el) {
|
|
314
|
+
if (!el || el === document.body) return 'body';
|
|
315
|
+
if (el.id) return '#' + CSS.escape(el.id);
|
|
316
|
+
let s = el.tagName.toLowerCase();
|
|
317
|
+
if (el.className && typeof el.className === 'string') {
|
|
318
|
+
const cls = el.className.trim().split(/\\s+/).filter(c => c && !c.startsWith('_')).slice(0, 3);
|
|
319
|
+
if (cls.length) s += '.' + cls.map(c => CSS.escape(c)).join('.');
|
|
320
|
+
}
|
|
321
|
+
const p = el.parentElement;
|
|
322
|
+
if (p) {
|
|
323
|
+
const sibs = [...p.children].filter(c => c.tagName === el.tagName);
|
|
324
|
+
if (sibs.length > 1) s += ':nth-child(' + ([...p.children].indexOf(el) + 1) + ')';
|
|
325
|
+
}
|
|
326
|
+
return s;
|
|
327
|
+
}
|
|
328
|
+
function gp(el) {
|
|
329
|
+
const parts = [];
|
|
330
|
+
let c = el;
|
|
331
|
+
while (c && c !== document.documentElement) { parts.unshift(gs(c)); c = c.parentElement; }
|
|
332
|
+
return parts;
|
|
333
|
+
}
|
|
334
|
+
function ni(el) {
|
|
335
|
+
if (!el) return null;
|
|
336
|
+
const tag = el.tagName?.toLowerCase() || '#text';
|
|
337
|
+
const attrs = {};
|
|
338
|
+
if (el.attributes) for (const a of el.attributes) if (a.name !== 'class' && a.name !== 'style') attrs[a.name] = a.value?.substring(0, 200);
|
|
339
|
+
const cls = (el.className && typeof el.className === 'string') ? el.className.trim().split(/\\s+/).filter(Boolean).slice(0, 10) : [];
|
|
340
|
+
const text = el.textContent?.trim().substring(0, 150) || '';
|
|
341
|
+
const dt = [...(el.childNodes||[])].filter(n=>n.nodeType===3).map(n=>n.textContent.trim()).filter(Boolean).join(' ').substring(0,100);
|
|
342
|
+
const cc = el.children?.length || 0;
|
|
343
|
+
const r = el.getBoundingClientRect?.();
|
|
344
|
+
return { tag, cls, attrs, text, directText: dt, childCount: cc, selector: gs(el), fullSelector: gp(el).join(' > '), rect: r ? {x:Math.round(r.x),y:Math.round(r.y),w:Math.round(r.width),h:Math.round(r.height)} : null };
|
|
345
|
+
}
|
|
346
|
+
const sel = ${selectorArg};
|
|
347
|
+
let el = sel ? document.querySelector(sel) : document.elementFromPoint(${x || 0}, ${y || 0});
|
|
348
|
+
if (!el) return JSON.stringify({ error: 'No element found' });
|
|
349
|
+
const info = ni(el);
|
|
350
|
+
const ancestors = [];
|
|
351
|
+
let pp = el.parentElement;
|
|
352
|
+
while (pp && pp !== document.documentElement) {
|
|
353
|
+
ancestors.push({ tag: pp.tagName.toLowerCase(), selector: gs(pp), cls: (pp.className && typeof pp.className === 'string') ? pp.className.trim().split(/\\s+/).slice(0,3) : [] });
|
|
354
|
+
pp = pp.parentElement;
|
|
355
|
+
}
|
|
356
|
+
const children = [...(el.children||[])].slice(0,50).map(c => ni(c));
|
|
357
|
+
return JSON.stringify({ element: info, ancestors: ancestors.reverse(), children });
|
|
358
|
+
})()`;
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
const raw = await cdp.evaluate(inspectScript, 10000);
|
|
362
|
+
let result = raw;
|
|
363
|
+
if (typeof raw === 'string') { try { result = JSON.parse(raw as string); } catch { } }
|
|
364
|
+
ctx.json(res, 200, result as Record<string, unknown>);
|
|
365
|
+
} catch (e: any) {
|
|
366
|
+
ctx.json(res, 500, { error: e.message });
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export async function handleDomChildren(ctx: DevServerContext, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
371
|
+
const body = await ctx.readBody(req);
|
|
372
|
+
const { selector, ideType } = body;
|
|
373
|
+
const cdp = ctx.getCdp(ideType);
|
|
374
|
+
if (!cdp) { ctx.json(res, 503, { error: 'No CDP connection' }); return; }
|
|
375
|
+
if (!selector) { ctx.json(res, 400, { error: 'selector required' }); return; }
|
|
376
|
+
|
|
377
|
+
const script = `(() => {
|
|
378
|
+
function gs(el) {
|
|
379
|
+
if (!el || el === document.body) return 'body';
|
|
380
|
+
if (el.id) return '#' + CSS.escape(el.id);
|
|
381
|
+
let s = el.tagName.toLowerCase();
|
|
382
|
+
if (el.className && typeof el.className === 'string') {
|
|
383
|
+
const cls = el.className.trim().split(/\\s+/).filter(c => c && !c.startsWith('_')).slice(0, 3);
|
|
384
|
+
if (cls.length) s += '.' + cls.map(c => CSS.escape(c)).join('.');
|
|
385
|
+
}
|
|
386
|
+
const p = el.parentElement;
|
|
387
|
+
if (p) {
|
|
388
|
+
const sibs = [...p.children].filter(c => c.tagName === el.tagName);
|
|
389
|
+
if (sibs.length > 1) s += ':nth-child(' + ([...p.children].indexOf(el) + 1) + ')';
|
|
390
|
+
}
|
|
391
|
+
return s;
|
|
392
|
+
}
|
|
393
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
394
|
+
if (!el) return JSON.stringify({ error: 'Element not found' });
|
|
395
|
+
const children = [...(el.children||[])].slice(0,100).map(c => {
|
|
396
|
+
const tag = c.tagName?.toLowerCase();
|
|
397
|
+
const cls = (c.className && typeof c.className === 'string') ? c.className.trim().split(/\\s+/).filter(Boolean).slice(0,10) : [];
|
|
398
|
+
const attrs = {};
|
|
399
|
+
for (const a of c.attributes) if (a.name!=='class'&&a.name!=='style') attrs[a.name] = a.value?.substring(0,200);
|
|
400
|
+
const text = c.textContent?.trim().substring(0,150)||'';
|
|
401
|
+
const dt = [...c.childNodes].filter(n=>n.nodeType===3).map(n=>n.textContent.trim()).filter(Boolean).join(' ').substring(0,100);
|
|
402
|
+
return { tag, cls, attrs, text, directText: dt, childCount: c.children?.length||0, selector: gs(c) };
|
|
403
|
+
});
|
|
404
|
+
return JSON.stringify({ selector: ${JSON.stringify(selector)}, childCount: el.children?.length||0, children });
|
|
405
|
+
})()`;
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
const raw = await cdp.evaluate(script, 10000);
|
|
409
|
+
let result = raw;
|
|
410
|
+
if (typeof raw === 'string') { try { result = JSON.parse(raw as string); } catch { } }
|
|
411
|
+
ctx.json(res, 200, result as Record<string, unknown>);
|
|
412
|
+
} catch (e: any) {
|
|
413
|
+
ctx.json(res, 500, { error: e.message });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export async function handleDomAnalyze(ctx: DevServerContext, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
418
|
+
const body = await ctx.readBody(req);
|
|
419
|
+
const { ideType, selector, x, y } = body;
|
|
420
|
+
const cdp = ctx.getCdp(ideType);
|
|
421
|
+
if (!cdp) { ctx.json(res, 503, { error: 'No CDP connection' }); return; }
|
|
422
|
+
|
|
423
|
+
const selectorArg = selector ? JSON.stringify(selector) : 'null';
|
|
424
|
+
const analyzeScript = `(() => {
|
|
425
|
+
function gs(el) {
|
|
426
|
+
if (!el || el === document.body) return 'body';
|
|
427
|
+
if (el.id) return '#' + CSS.escape(el.id);
|
|
428
|
+
let s = el.tagName.toLowerCase();
|
|
429
|
+
if (el.className && typeof el.className === 'string') {
|
|
430
|
+
const cls = el.className.trim().split(/\\s+/).filter(c => c && !c.startsWith('_')).slice(0, 3);
|
|
431
|
+
if (cls.length) s += '.' + cls.map(c => CSS.escape(c)).join('.');
|
|
432
|
+
}
|
|
433
|
+
return s;
|
|
434
|
+
}
|
|
435
|
+
function fp(el) {
|
|
436
|
+
const parts = [];
|
|
437
|
+
let c = el;
|
|
438
|
+
while (c && c !== document.documentElement) { parts.unshift(gs(c)); c = c.parentElement; }
|
|
439
|
+
return parts.join(' > ');
|
|
440
|
+
}
|
|
441
|
+
function sigOf(el) {
|
|
442
|
+
return el.tagName + '|' + ((el.className && typeof el.className === 'string') ? el.className.trim().split(/\\s+/).filter(c => c && !c.startsWith('_')).sort().join('.') : '');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Find target element
|
|
446
|
+
const sel = ${selectorArg};
|
|
447
|
+
let target = sel ? document.querySelector(sel) : document.elementFromPoint(${x || 0}, ${y || 0});
|
|
448
|
+
if (!target) return JSON.stringify({ error: 'Element not found' });
|
|
449
|
+
|
|
450
|
+
const result = {
|
|
451
|
+
target: { tag: target.tagName.toLowerCase(), selector: fp(target), text: (target.textContent||'').trim().substring(0, 200) },
|
|
452
|
+
siblingPattern: null,
|
|
453
|
+
ancestorAnalysis: [],
|
|
454
|
+
subtreeTexts: [],
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
// 1. Walk UP parents — at each level, find sibling patterns
|
|
458
|
+
let el = target;
|
|
459
|
+
let depth = 0;
|
|
460
|
+
while (el && el !== document.body && depth < 15) {
|
|
461
|
+
const parent = el.parentElement;
|
|
462
|
+
if (!parent) break;
|
|
463
|
+
|
|
464
|
+
const mySig = sigOf(el);
|
|
465
|
+
const siblings = [...parent.children].filter(c => sigOf(c) === mySig);
|
|
466
|
+
const totalChildren = parent.children.length;
|
|
467
|
+
const childSel = gs(el).replace(/:nth-child\\(\\d+\\)/, '');
|
|
468
|
+
const parentSel = fp(parent);
|
|
469
|
+
|
|
470
|
+
result.ancestorAnalysis.push({
|
|
471
|
+
depth,
|
|
472
|
+
parentTag: parent.tagName.toLowerCase(),
|
|
473
|
+
parentSelector: parentSel,
|
|
474
|
+
totalChildren,
|
|
475
|
+
matchingSiblings: siblings.length,
|
|
476
|
+
childSelector: childSel,
|
|
477
|
+
fullSelector: parentSel + ' > ' + childSel,
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// Best sibling pattern: 3+ matching siblings with text
|
|
481
|
+
if (!result.siblingPattern && siblings.length >= 3) {
|
|
482
|
+
const siblingData = siblings.map((s, i) => {
|
|
483
|
+
const directText = [...s.childNodes].filter(n => n.nodeType === 3).map(n => n.textContent.trim()).filter(Boolean).join(' ').substring(0, 120);
|
|
484
|
+
const allText = (s.textContent || '').trim().substring(0, 200);
|
|
485
|
+
const childCount = s.children?.length || 0;
|
|
486
|
+
const cls = (s.className && typeof s.className === 'string') ? s.className.trim().split(/\\s+/).filter(Boolean) : [];
|
|
487
|
+
const attrs = {};
|
|
488
|
+
if (s.attributes) for (const a of s.attributes) {
|
|
489
|
+
if (a.name !== 'class' && a.name !== 'style' && a.value) attrs[a.name] = a.value.substring(0, 100);
|
|
490
|
+
}
|
|
491
|
+
return { index: i, directText, allText, childCount, cls, attrs, tag: s.tagName.toLowerCase() };
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// Find common attributes across siblings
|
|
495
|
+
const allAttrs = siblingData.map(s => Object.keys(s.attrs));
|
|
496
|
+
const commonAttrs = allAttrs[0]?.filter(attr => allAttrs.every(a => a.includes(attr))) || [];
|
|
497
|
+
// Find varying attributes (data-*, role, etc)
|
|
498
|
+
const varyingAttrs = {};
|
|
499
|
+
for (const attr of commonAttrs) {
|
|
500
|
+
const values = siblingData.map(s => s.attrs[attr]);
|
|
501
|
+
const unique = [...new Set(values)];
|
|
502
|
+
if (unique.length > 1) varyingAttrs[attr] = unique.slice(0, 5);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
result.siblingPattern = {
|
|
506
|
+
count: siblings.length,
|
|
507
|
+
selector: parentSel + ' > ' + childSel,
|
|
508
|
+
parentSelector: parentSel,
|
|
509
|
+
depthFromTarget: depth,
|
|
510
|
+
siblings: siblingData.slice(0, 30),
|
|
511
|
+
commonAttrs,
|
|
512
|
+
varyingAttrs,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
el = parent;
|
|
517
|
+
depth++;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// 2. Collect subtree text nodes from target
|
|
521
|
+
const walker = document.createTreeWalker(target, NodeFilter.SHOW_TEXT, null);
|
|
522
|
+
let node;
|
|
523
|
+
while ((node = walker.nextNode()) && result.subtreeTexts.length < 30) {
|
|
524
|
+
const text = node.textContent.trim();
|
|
525
|
+
if (text.length > 2) {
|
|
526
|
+
const parentTag = node.parentElement?.tagName?.toLowerCase() || '';
|
|
527
|
+
const parentCls = (node.parentElement?.className && typeof node.parentElement.className === 'string')
|
|
528
|
+
? node.parentElement.className.trim().split(/\\s+/).filter(Boolean).slice(0,3).join('.') : '';
|
|
529
|
+
result.subtreeTexts.push({
|
|
530
|
+
text: text.substring(0, 150),
|
|
531
|
+
parentTag,
|
|
532
|
+
parentCls,
|
|
533
|
+
parentSelector: gs(node.parentElement),
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return JSON.stringify(result);
|
|
539
|
+
})()`;
|
|
540
|
+
|
|
541
|
+
try {
|
|
542
|
+
const raw = await cdp.evaluate(analyzeScript, 15000);
|
|
543
|
+
let result = raw;
|
|
544
|
+
if (typeof raw === 'string') { try { result = JSON.parse(raw as string); } catch { } }
|
|
545
|
+
ctx.json(res, 200, result as Record<string, unknown>);
|
|
546
|
+
} catch (e: any) {
|
|
547
|
+
ctx.json(res, 500, { error: e.message });
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export async function handleFindCommon(ctx: DevServerContext, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
552
|
+
const body = await ctx.readBody(req);
|
|
553
|
+
const { include, exclude, ideType } = body;
|
|
554
|
+
if (!Array.isArray(include) || include.length === 0) { ctx.json(res, 400, { error: 'include[] is required' }); return; }
|
|
555
|
+
const cdp = ctx.getCdp(ideType);
|
|
556
|
+
if (!cdp) { ctx.json(res, 503, { error: 'No CDP connection' }); return; }
|
|
557
|
+
|
|
558
|
+
const script = `(() => {
|
|
559
|
+
const includes = ${JSON.stringify(include)};
|
|
560
|
+
const excludes = ${JSON.stringify(exclude || [])};
|
|
561
|
+
|
|
562
|
+
function gs(el) {
|
|
563
|
+
if (!el || el === document.body) return 'body';
|
|
564
|
+
if (el.id) return '#' + CSS.escape(el.id);
|
|
565
|
+
let s = el.tagName.toLowerCase();
|
|
566
|
+
if (el.className && typeof el.className === 'string') {
|
|
567
|
+
const cls = el.className.trim().split(/\\s+/).filter(c => c && !c.startsWith('_')).slice(0, 3);
|
|
568
|
+
if (cls.length) s += '.' + cls.map(c => CSS.escape(c)).join('.');
|
|
569
|
+
}
|
|
570
|
+
return s;
|
|
571
|
+
}
|
|
572
|
+
function fp(el) {
|
|
573
|
+
const parts = [];
|
|
574
|
+
let c = el;
|
|
575
|
+
while (c && c !== document.documentElement) { parts.unshift(gs(c)); c = c.parentElement; }
|
|
576
|
+
return parts.join(' > ');
|
|
577
|
+
}
|
|
578
|
+
function sig(el) {
|
|
579
|
+
return el.tagName + '|' + ((el.className && typeof el.className === 'string') ? el.className.trim() : '');
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Step 1: For each include, find all matching leaf elements
|
|
583
|
+
const includeMatches = includes.map(text => {
|
|
584
|
+
const lower = text.toLowerCase();
|
|
585
|
+
const found = [];
|
|
586
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
|
|
587
|
+
acceptNode: n => n.textContent.toLowerCase().includes(lower) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT
|
|
588
|
+
});
|
|
589
|
+
let node;
|
|
590
|
+
while ((node = walker.nextNode()) && found.length < 5) {
|
|
591
|
+
if (node.parentElement) found.push(node.parentElement);
|
|
592
|
+
}
|
|
593
|
+
return found;
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
if (includeMatches.some(m => m.length === 0)) {
|
|
597
|
+
const missing = includes.filter((_, i) => includeMatches[i].length === 0);
|
|
598
|
+
return JSON.stringify({ results: [], message: 'Text not found: ' + missing.join(', ') });
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Step 2: Find LCA for each combination of include elements
|
|
602
|
+
// For each pair of include[0] element and include[1] element, find their LCA
|
|
603
|
+
// Then within the LCA, find the direct-child subtree branch for each
|
|
604
|
+
const containers = [];
|
|
605
|
+
const seen = new Set();
|
|
606
|
+
|
|
607
|
+
function findLCA(el1, el2) {
|
|
608
|
+
const ancestors1 = new Set();
|
|
609
|
+
let c = el1;
|
|
610
|
+
while (c) { ancestors1.add(c); c = c.parentElement; }
|
|
611
|
+
c = el2;
|
|
612
|
+
while (c) { if (ancestors1.has(c)) return c; c = c.parentElement; }
|
|
613
|
+
return document.body;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function findDirectChildContaining(parent, descendant) {
|
|
617
|
+
let c = descendant;
|
|
618
|
+
while (c && c.parentElement !== parent) c = c.parentElement;
|
|
619
|
+
return c;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Try all combinations (first 3 matches per include)
|
|
623
|
+
for (const el1 of includeMatches[0].slice(0, 3)) {
|
|
624
|
+
for (let ii = 1; ii < includeMatches.length; ii++) {
|
|
625
|
+
for (const el2 of includeMatches[ii].slice(0, 3)) {
|
|
626
|
+
if (el1 === el2) continue;
|
|
627
|
+
const lca = findLCA(el1, el2);
|
|
628
|
+
if (!lca || lca === document.body || lca === document.documentElement) continue;
|
|
629
|
+
|
|
630
|
+
// Find which direct child of LCA contains each include element
|
|
631
|
+
const child1 = findDirectChildContaining(lca, el1);
|
|
632
|
+
const child2 = findDirectChildContaining(lca, el2);
|
|
633
|
+
if (!child1 || !child2 || child1 === child2) continue;
|
|
634
|
+
|
|
635
|
+
const lcaSel = fp(lca);
|
|
636
|
+
if (seen.has(lcaSel)) continue;
|
|
637
|
+
seen.add(lcaSel);
|
|
638
|
+
|
|
639
|
+
// Check exclude
|
|
640
|
+
if (excludes.length > 0) {
|
|
641
|
+
const lcaText = (lca.textContent || '').toLowerCase();
|
|
642
|
+
if (excludes.some(ex => lcaText.includes(ex.toLowerCase()))) continue;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Are child1 and child2 same tag? (relaxed — ignore classes)
|
|
646
|
+
const tag1 = child1.tagName;
|
|
647
|
+
const tag2 = child2.tagName;
|
|
648
|
+
|
|
649
|
+
// Bubble up: walk up from LCA, find the best list container
|
|
650
|
+
// (the one with most repeating same-tag children)
|
|
651
|
+
let container = lca;
|
|
652
|
+
let bestContainer = lca;
|
|
653
|
+
let bestListCount = 0;
|
|
654
|
+
for (let up = 0; up < 10; up++) {
|
|
655
|
+
const p = container.parentElement;
|
|
656
|
+
if (!p || p === document.body || p === document.documentElement) break;
|
|
657
|
+
// Check how many same-tag siblings 'container' has in parent
|
|
658
|
+
const myTag = container.tagName;
|
|
659
|
+
const sibCount = [...p.children].filter(c => c.tagName === myTag).length;
|
|
660
|
+
if (sibCount > bestListCount) {
|
|
661
|
+
bestListCount = sibCount;
|
|
662
|
+
bestContainer = p;
|
|
663
|
+
}
|
|
664
|
+
container = p;
|
|
665
|
+
}
|
|
666
|
+
container = bestListCount >= 3 ? bestContainer : lca;
|
|
667
|
+
|
|
668
|
+
const allChildren = [...container.children];
|
|
669
|
+
const childTag = tag1 === tag2 ? tag1 : (allChildren.length > 0 ? allChildren[0].tagName : '');
|
|
670
|
+
const sameTagCount = allChildren.filter(c => c.tagName === childTag).length;
|
|
671
|
+
const isList = sameTagCount >= 3 && sameTagCount >= allChildren.length * 0.4;
|
|
672
|
+
|
|
673
|
+
// Gather all same-tag children as list items
|
|
674
|
+
const listItems = isList
|
|
675
|
+
? allChildren.filter(c => c.tagName === childTag)
|
|
676
|
+
: allChildren;
|
|
677
|
+
|
|
678
|
+
// Filter rendered items (skip virtual scroll placeholders)
|
|
679
|
+
const rendered = listItems.filter(c => (c.innerText || '').trim().length > 0);
|
|
680
|
+
const placeholderCount = listItems.length - rendered.length;
|
|
681
|
+
|
|
682
|
+
const containerSel = fp(container);
|
|
683
|
+
if (seen.has(containerSel)) continue;
|
|
684
|
+
seen.add(containerSel);
|
|
685
|
+
|
|
686
|
+
const r = container.getBoundingClientRect();
|
|
687
|
+
containers.push({
|
|
688
|
+
selector: containerSel,
|
|
689
|
+
tag: container.tagName.toLowerCase(),
|
|
690
|
+
childCount: allChildren.length,
|
|
691
|
+
listItemCount: listItems.length,
|
|
692
|
+
renderedCount: rendered.length,
|
|
693
|
+
placeholderCount,
|
|
694
|
+
isList,
|
|
695
|
+
rect: { w: Math.round(r.width), h: Math.round(r.height) },
|
|
696
|
+
depth: containerSel.split(' > ').length,
|
|
697
|
+
items: rendered.slice(0, 30).map((el, i) => {
|
|
698
|
+
const fullText = (el.innerText || el.textContent || '').trim();
|
|
699
|
+
// Find snippet around first matched include text
|
|
700
|
+
let text = fullText.substring(0, 200);
|
|
701
|
+
const matched = [];
|
|
702
|
+
for (const inc of includes) {
|
|
703
|
+
const idx = fullText.toLowerCase().indexOf(inc.toLowerCase());
|
|
704
|
+
if (idx >= 0) {
|
|
705
|
+
matched.push(inc);
|
|
706
|
+
if (matched.length === 1) {
|
|
707
|
+
// Show snippet around first match
|
|
708
|
+
const start = Math.max(0, idx - 30);
|
|
709
|
+
const end = Math.min(fullText.length, idx + inc.length + 80);
|
|
710
|
+
text = (start > 0 ? '...' : '') + fullText.substring(start, end) + (end < fullText.length ? '...' : '');
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return {
|
|
715
|
+
index: i,
|
|
716
|
+
tag: el.tagName.toLowerCase(),
|
|
717
|
+
cls: (el.className && typeof el.className === 'string') ? el.className.trim().split(/\\s+/).slice(0, 2).join(' ') : '',
|
|
718
|
+
text,
|
|
719
|
+
matchedIncludes: matched,
|
|
720
|
+
childCount: el.children.length,
|
|
721
|
+
h: Math.round(el.getBoundingClientRect().height),
|
|
722
|
+
};
|
|
723
|
+
}),
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Sort: list containers first (more items = better), then by depth
|
|
730
|
+
containers.sort((a, b) => {
|
|
731
|
+
if (a.isList !== b.isList) return a.isList ? -1 : 1;
|
|
732
|
+
return b.listItemCount - a.listItemCount || b.depth - a.depth;
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
return JSON.stringify({
|
|
736
|
+
results: containers.slice(0, 10),
|
|
737
|
+
includeCount: includes.length,
|
|
738
|
+
excludeCount: excludes.length,
|
|
739
|
+
});
|
|
740
|
+
})()`;
|
|
741
|
+
|
|
742
|
+
try {
|
|
743
|
+
const raw = await cdp.evaluate(script, 10000);
|
|
744
|
+
let result = raw;
|
|
745
|
+
if (typeof raw === 'string') { try { result = JSON.parse(raw as string); } catch { } }
|
|
746
|
+
ctx.json(res, 200, result as Record<string, unknown>);
|
|
747
|
+
} catch (e: any) {
|
|
748
|
+
ctx.json(res, 500, { error: e.message });
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
export async function handleFindByText(ctx: DevServerContext, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
753
|
+
const body = await ctx.readBody(req);
|
|
754
|
+
const { text, ideType, containerSelector } = body;
|
|
755
|
+
if (!text || typeof text !== 'string') { ctx.json(res, 400, { error: 'text is required' }); return; }
|
|
756
|
+
const cdp = ctx.getCdp(ideType);
|
|
757
|
+
if (!cdp) { ctx.json(res, 503, { error: 'No CDP connection' }); return; }
|
|
758
|
+
|
|
759
|
+
const containerArg = containerSelector ? JSON.stringify(containerSelector) : 'null';
|
|
760
|
+
const script = `(() => {
|
|
761
|
+
function gs(el) {
|
|
762
|
+
if (!el || el === document.body) return 'body';
|
|
763
|
+
if (el.id) return '#' + CSS.escape(el.id);
|
|
764
|
+
let s = el.tagName.toLowerCase();
|
|
765
|
+
if (el.className && typeof el.className === 'string') {
|
|
766
|
+
const cls = el.className.trim().split(/\\s+/).filter(c => c && !c.startsWith('_')).slice(0, 3);
|
|
767
|
+
if (cls.length) s += '.' + cls.map(c => CSS.escape(c)).join('.');
|
|
768
|
+
}
|
|
769
|
+
return s;
|
|
770
|
+
}
|
|
771
|
+
function fp(el) {
|
|
772
|
+
const parts = [];
|
|
773
|
+
let c = el;
|
|
774
|
+
while (c && c !== document.documentElement) { parts.unshift(gs(c)); c = c.parentElement; }
|
|
775
|
+
return parts.join(' > ');
|
|
776
|
+
}
|
|
777
|
+
function parentSig(el) {
|
|
778
|
+
// Signature: tag+class chain up 3 levels
|
|
779
|
+
const parts = [];
|
|
780
|
+
let c = el;
|
|
781
|
+
for (let i = 0; i < 3 && c; i++) { parts.push(gs(c)); c = c.parentElement; }
|
|
782
|
+
return parts.join(' < ');
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const searchText = ${JSON.stringify(text)}.toLowerCase();
|
|
786
|
+
const container = ${containerArg} ? document.querySelector(${containerArg}) : document.body;
|
|
787
|
+
if (!container) return JSON.stringify({ error: 'Container not found' });
|
|
788
|
+
|
|
789
|
+
const matches = [];
|
|
790
|
+
const seen = new Set();
|
|
791
|
+
|
|
792
|
+
// Find all text nodes containing the search text
|
|
793
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, {
|
|
794
|
+
acceptNode: n => n.textContent.toLowerCase().includes(searchText) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT
|
|
795
|
+
});
|
|
796
|
+
let node;
|
|
797
|
+
while ((node = walker.nextNode()) && matches.length < 50) {
|
|
798
|
+
// Walk up to find the most specific visible element
|
|
799
|
+
let el = node.parentElement;
|
|
800
|
+
if (!el) continue;
|
|
801
|
+
|
|
802
|
+
// Skip hidden elements
|
|
803
|
+
const r = el.getBoundingClientRect();
|
|
804
|
+
if (r.width === 0 && r.height === 0) continue;
|
|
805
|
+
|
|
806
|
+
const selector = fp(el);
|
|
807
|
+
if (seen.has(selector)) continue;
|
|
808
|
+
seen.add(selector);
|
|
809
|
+
|
|
810
|
+
// Walk up parent chain — record each level's selector + sibling count
|
|
811
|
+
const ancestors = [];
|
|
812
|
+
let cur = el;
|
|
813
|
+
let pLvl = cur.parentElement;
|
|
814
|
+
for (let lvl = 0; lvl < 10 && pLvl && pLvl !== document.body; lvl++) {
|
|
815
|
+
const mySig = cur.tagName + '|' + ((cur.className && typeof cur.className === 'string') ? cur.className.trim().split(/\\s+/).sort().join('.') : '');
|
|
816
|
+
const sibs = [...pLvl.children].filter(c => {
|
|
817
|
+
const sig = c.tagName + '|' + ((c.className && typeof c.className === 'string') ? c.className.trim().split(/\\s+/).sort().join('.') : '');
|
|
818
|
+
return sig === mySig;
|
|
819
|
+
});
|
|
820
|
+
const childSel = gs(cur).replace(/:nth-child\\(\\d+\\)/, '');
|
|
821
|
+
ancestors.push({
|
|
822
|
+
parentSelector: fp(pLvl),
|
|
823
|
+
childSelector: childSel,
|
|
824
|
+
fullSelector: fp(pLvl) + ' > ' + childSel,
|
|
825
|
+
siblingCount: sibs.length,
|
|
826
|
+
parentTag: pLvl.tagName.toLowerCase(),
|
|
827
|
+
});
|
|
828
|
+
cur = pLvl;
|
|
829
|
+
pLvl = pLvl.parentElement;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const directText = (node.textContent || '').trim().substring(0, 200);
|
|
833
|
+
const allText = (node.parentElement.textContent || '').trim().substring(0, 300);
|
|
834
|
+
const tag = node.parentElement.tagName.toLowerCase();
|
|
835
|
+
const cls = (node.parentElement.className && typeof node.parentElement.className === 'string')
|
|
836
|
+
? node.parentElement.className.trim().split(/\\s+/).filter(Boolean) : [];
|
|
837
|
+
|
|
838
|
+
matches.push({
|
|
839
|
+
selector,
|
|
840
|
+
tag,
|
|
841
|
+
cls,
|
|
842
|
+
directText,
|
|
843
|
+
allText,
|
|
844
|
+
ancestors,
|
|
845
|
+
rect: { w: Math.round(r.width), h: Math.round(r.height) },
|
|
846
|
+
depth: selector.split(' > ').length,
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Sort: prefer elements with more siblings in ancestry, then fewer depth
|
|
851
|
+
matches.sort((a, b) => {
|
|
852
|
+
const aMax = Math.max(1, ...a.ancestors.map(x => x.siblingCount));
|
|
853
|
+
const bMax = Math.max(1, ...b.ancestors.map(x => x.siblingCount));
|
|
854
|
+
return (bMax - aMax) || (a.depth - b.depth);
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
return JSON.stringify({ query: ${JSON.stringify(text)}, matches, total: matches.length });
|
|
858
|
+
})()`;
|
|
859
|
+
|
|
860
|
+
try {
|
|
861
|
+
const raw = await cdp.evaluate(script, 10000);
|
|
862
|
+
let result = raw;
|
|
863
|
+
if (typeof raw === 'string') { try { result = JSON.parse(raw as string); } catch { } }
|
|
864
|
+
ctx.json(res, 200, result as Record<string, unknown>);
|
|
865
|
+
} catch (e: any) {
|
|
866
|
+
ctx.json(res, 500, { error: e.message });
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
export async function handleDomContext(ctx: DevServerContext, type: string, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
871
|
+
const body = await ctx.readBody(req);
|
|
872
|
+
const { ideType } = body;
|
|
873
|
+
const provider = ctx.providerLoader.resolve(type);
|
|
874
|
+
if (!provider) { ctx.json(res, 404, { error: `Provider not found: ${type}` }); return; }
|
|
875
|
+
|
|
876
|
+
const cdp = ctx.getCdp(ideType || type);
|
|
877
|
+
if (!cdp) { ctx.json(res, 503, { error: 'No CDP connection available. Target IDE must be running with CDP enabled.' }); return; }
|
|
878
|
+
|
|
879
|
+
try {
|
|
880
|
+
// 1. Capture screenshot
|
|
881
|
+
let screenshot: string | null = null;
|
|
882
|
+
try {
|
|
883
|
+
const buf = await cdp.captureScreenshot();
|
|
884
|
+
if (buf) screenshot = buf.toString('base64');
|
|
885
|
+
} catch { /* screenshot optional */ }
|
|
886
|
+
|
|
887
|
+
// 2. Collect DOM snapshot
|
|
888
|
+
const domScript = `(() => {
|
|
889
|
+
function gs(el) {
|
|
890
|
+
if (!el || el === document.body) return 'body';
|
|
891
|
+
if (el.id) return '#' + CSS.escape(el.id);
|
|
892
|
+
let s = el.tagName.toLowerCase();
|
|
893
|
+
if (el.className && typeof el.className === 'string') {
|
|
894
|
+
const cls = el.className.trim().split(/\\s+/).filter(c => c && !c.startsWith('_')).slice(0, 3);
|
|
895
|
+
if (cls.length) s += '.' + cls.map(c => CSS.escape(c)).join('.');
|
|
896
|
+
}
|
|
897
|
+
return s;
|
|
898
|
+
}
|
|
899
|
+
function fp(el) {
|
|
900
|
+
const parts = [];
|
|
901
|
+
let c = el;
|
|
902
|
+
while (c && c !== document.documentElement) { parts.unshift(gs(c)); c = c.parentElement; }
|
|
903
|
+
return parts.join(' > ');
|
|
904
|
+
}
|
|
905
|
+
function rect(el) {
|
|
906
|
+
try { const r = el.getBoundingClientRect(); return { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) }; }
|
|
907
|
+
catch { return null; }
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
const result = { contentEditables: [], chatContainers: [], buttons: [], sidebars: [], dropdowns: [], inputs: [] };
|
|
911
|
+
|
|
912
|
+
// Content editables + textareas + inputs
|
|
913
|
+
document.querySelectorAll('[contenteditable], textarea, input[type="text"], input:not([type])').forEach(el => {
|
|
914
|
+
if (el.offsetWidth === 0 && el.offsetHeight === 0) return;
|
|
915
|
+
result.contentEditables.push({
|
|
916
|
+
selector: fp(el),
|
|
917
|
+
tag: el.tagName.toLowerCase(),
|
|
918
|
+
contenteditable: el.getAttribute('contenteditable'),
|
|
919
|
+
role: el.getAttribute('role'),
|
|
920
|
+
ariaLabel: el.getAttribute('aria-label'),
|
|
921
|
+
placeholder: el.getAttribute('placeholder'),
|
|
922
|
+
rect: rect(el),
|
|
923
|
+
visible: el.offsetParent !== null || el.offsetWidth > 0,
|
|
924
|
+
});
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
// Chat containers — large divs with scroll
|
|
928
|
+
document.querySelectorAll('div, section, main').forEach(el => {
|
|
929
|
+
const style = getComputedStyle(el);
|
|
930
|
+
const isScrollable = style.overflowY === 'auto' || style.overflowY === 'scroll';
|
|
931
|
+
const r = el.getBoundingClientRect();
|
|
932
|
+
if (!isScrollable || r.height < 200 || r.width < 200) return;
|
|
933
|
+
const childCount = el.children.length;
|
|
934
|
+
if (childCount < 2) return;
|
|
935
|
+
result.chatContainers.push({
|
|
936
|
+
selector: fp(el),
|
|
937
|
+
childCount,
|
|
938
|
+
rect: { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) },
|
|
939
|
+
hasScrollable: true,
|
|
940
|
+
scrollTop: Math.round(el.scrollTop),
|
|
941
|
+
scrollHeight: Math.round(el.scrollHeight),
|
|
942
|
+
});
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
// Buttons
|
|
946
|
+
document.querySelectorAll('button, [role="button"]').forEach(el => {
|
|
947
|
+
if (el.offsetWidth === 0 && el.offsetHeight === 0) return;
|
|
948
|
+
const text = (el.textContent || '').trim().substring(0, 80);
|
|
949
|
+
if (!text && !el.getAttribute('aria-label')) return;
|
|
950
|
+
result.buttons.push({
|
|
951
|
+
text,
|
|
952
|
+
ariaLabel: el.getAttribute('aria-label'),
|
|
953
|
+
selector: fp(el),
|
|
954
|
+
rect: rect(el),
|
|
955
|
+
disabled: el.disabled || el.getAttribute('aria-disabled') === 'true',
|
|
956
|
+
});
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
// Sidebars — panels on left/right edges
|
|
960
|
+
document.querySelectorAll('[class*="sidebar"], [class*="side-bar"], [class*="panel"], [role="complementary"], [role="navigation"], aside').forEach(el => {
|
|
961
|
+
if (el.offsetWidth === 0 && el.offsetHeight === 0) return;
|
|
962
|
+
const r = el.getBoundingClientRect();
|
|
963
|
+
if (r.width < 50 || r.height < 200) return;
|
|
964
|
+
result.sidebars.push({
|
|
965
|
+
selector: fp(el),
|
|
966
|
+
position: r.x < window.innerWidth / 3 ? 'left' : r.x > window.innerWidth * 2 / 3 ? 'right' : 'center',
|
|
967
|
+
rect: { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) },
|
|
968
|
+
childCount: el.children.length,
|
|
969
|
+
});
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
// Dropdowns — select, popover, menu patterns
|
|
973
|
+
document.querySelectorAll('select, [role="listbox"], [role="menu"], [role="combobox"], [class*="dropdown"], [class*="popover"]').forEach(el => {
|
|
974
|
+
result.dropdowns.push({
|
|
975
|
+
selector: fp(el),
|
|
976
|
+
tag: el.tagName.toLowerCase(),
|
|
977
|
+
role: el.getAttribute('role'),
|
|
978
|
+
visible: el.offsetParent !== null || el.offsetWidth > 0,
|
|
979
|
+
rect: rect(el),
|
|
980
|
+
});
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
return JSON.stringify(result);
|
|
984
|
+
})()`;
|
|
985
|
+
|
|
986
|
+
const raw = await cdp.evaluate(domScript, 15000);
|
|
987
|
+
let domSnapshot: any = {};
|
|
988
|
+
if (typeof raw === 'string') { try { domSnapshot = JSON.parse(raw); } catch { domSnapshot = { raw }; } }
|
|
989
|
+
else domSnapshot = raw;
|
|
990
|
+
|
|
991
|
+
ctx.json(res, 200, {
|
|
992
|
+
screenshot: screenshot ? `base64:${screenshot}` : null,
|
|
993
|
+
domSnapshot,
|
|
994
|
+
pageTitle: await cdp.evaluate('document.title', 3000).catch(() => ''),
|
|
995
|
+
pageUrl: await cdp.evaluate('window.location.href', 3000).catch(() => ''),
|
|
996
|
+
providerType: type,
|
|
997
|
+
timestamp: new Date().toISOString(),
|
|
998
|
+
});
|
|
999
|
+
} catch (e: any) {
|
|
1000
|
+
ctx.json(res, 500, { error: `DOM context collection failed: ${e.message}` });
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|