@adhdev/daemon-core 0.5.38 → 0.5.41

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.5.38",
3
+ "version": "0.5.41",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -86,7 +86,7 @@
86
86
  { "source": "Always\\s*allow", "flags": "i" },
87
87
  { "source": "\\(y/n\\)", "flags": "i" },
88
88
  { "source": "\\[Y/n\\]", "flags": "i" },
89
- { "source": "Run\\s+\\w+\\s+command", "flags": "i" },
89
+ { "source": "Run\\s*this\\s*command", "flags": "i" },
90
90
  { "source": "Allow\\s*tool", "flags": "i" },
91
91
  { "source": "Yes,?\\s*don'?t\\s*ask", "flags": "i" },
92
92
  { "source": "Deny", "flags": "i" },
@@ -45,17 +45,11 @@
45
45
 
46
46
  // 6. Check if we're on a task list or chat view
47
47
  const hasConversationList = document.querySelectorAll('[role="button"][class*="rounded-lg"]').length > 0;
48
- const headerEl = document.querySelector('[style*="view-transition-name: header-title"]');
49
- const headerText = (headerEl?.textContent || '').trim();
50
- const isTaskList = headerText === '작업' || headerText === 'Tasks';
51
-
52
- if (isTaskList) {
53
- return JSON.stringify({ __adhdev_skip_iframe: true, error: 'Found Tasks webview instead of Chat' });
54
- }
48
+ const headerText = document.querySelector('[style*="view-transition-name: header-title"]')?.textContent?.trim() || '';
55
49
 
56
50
  return JSON.stringify({
57
51
  headerText,
58
- isTaskList,
52
+ isTaskList: headerText === '작업' || headerText === 'Tasks',
59
53
  modelFound: modelMatch ? modelMatch[0] : null,
60
54
  textareaCount: textareas.length,
61
55
  textareas: Array.from(textareas).map(t => ({
@@ -52,11 +52,6 @@
52
52
  const headerEl = doc.querySelector('[style*="view-transition-name: header-title"]');
53
53
  const headerText = (headerEl?.textContent || '').trim();
54
54
  const isTaskList = headerText === '작업' || headerText === 'Tasks';
55
-
56
- // If we accidentally evaluated inside the Tasks webview instead of Chat, tell Daemon to try the next matching webview
57
- if (isTaskList) {
58
- return JSON.stringify({ __adhdev_skip_iframe: true, error: 'Found Tasks webview instead of Chat' });
59
- }
60
55
 
61
56
  // ─── Rich content extractor ───
62
57
  const BLOCK_TAGS = new Set(['DIV', 'P', 'BR', 'LI', 'TR', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE', 'HR', 'SECTION', 'ARTICLE']);
@@ -296,145 +291,24 @@
296
291
 
297
292
  // ─── 3. Status ───
298
293
  let status = 'idle';
299
- // Filter out disabled buttons to avoid matching old historical prompts
300
- // Include roles, inputs, custom vscode tags, and generic interactive lists used by multiple-choice forms
301
- const selectors = [
302
- 'button', '[role="radio"]', '[role="button"]', '[role="option"]', '[role="menuitem"]',
303
- 'input[type="radio"] + label', 'input[type="radio"] ~ span', 'input[type="checkbox"] + label',
304
- 'vscode-button', 'vscode-radio', 'vscode-checkbox', 'vscode-option', 'li'
305
- ].join(', ');
306
-
307
- const buttons = Array.from(doc.querySelectorAll(selectors))
308
- .filter(b => b.offsetWidth > 0 && !b.disabled && !b.closest('[inert]'));
309
-
310
- const getBtnLabel = (b) => {
311
- let t = (b.textContent || '').trim();
312
- return t || (b.getAttribute('aria-label') || '').trim();
313
- };
314
-
315
- const buttonLabels = buttons.map(getBtnLabel).map(t => t.toLowerCase());
316
-
317
- if (buttonLabels.some(l => l.includes('cancel') || l.includes('취소') || l.includes('stop') || l.includes('중지'))) {
294
+ const buttons = Array.from(doc.querySelectorAll('button'))
295
+ .filter(b => b.offsetWidth > 0);
296
+ const buttonTexts = buttons.map(b => (b.textContent || '').trim().toLowerCase());
297
+ const buttonLabels = buttons.map(b => (b.getAttribute('aria-label') || '').toLowerCase());
298
+
299
+ if (buttonTexts.includes('cancel') || buttonTexts.includes('취소') ||
300
+ buttonLabels.some(l => l.includes('cancel') || l.includes('취소') || l.includes('stop') || l.includes('중지'))) {
318
301
  status = 'generating';
319
302
  }
320
303
 
321
- // ─── 5. Universal Approval Modal Detection (Language-Agnostic) ───
322
- // The approval/interaction panel in Codex is rendered OUTSIDE the chat scroll area,
323
- // typically in a sibling "request-input-panel" region at the bottom.
324
- // We must scan the ENTIRE document body to find it.
325
- let activeModal = null;
326
-
327
- // Look for the request-input-panel area first (contains radio options + submit/skip)
328
- // The approval panel lives in the outer webview document, NOT inside the inner iframe (doc).
329
- // Use `document` (the webview frame root) to find it, same as explore_dom.js does.
330
- const searchDocs = [document, doc]; // outer first, then inner iframe
331
- let requestPanel = null;
332
- for (const d of searchDocs) {
333
- requestPanel = d.querySelector('[class*="request-input-panel"]');
334
- if (requestPanel) break;
335
- // Also search all textareas for the class
336
- const tas = d.querySelectorAll('textarea');
337
- for (const ta of tas) {
338
- if (ta.className && ta.className.includes('request-input-panel')) {
339
- requestPanel = ta;
340
- break;
341
- }
342
- }
343
- if (requestPanel) break;
304
+ // Codex approval buttons appear as distinct action buttons (e.g. "Approve", "Always approve", "Deny")
305
+ // They typically appear at the bottom of a tool-call/action block
306
+ const approvalSpecificPatterns = /^(approve|always approve|deny|reject|승인|항상 승인|거부)/i;
307
+ const hasApprovalButton = buttonTexts.some(b => approvalSpecificPatterns.test(b)) ||
308
+ buttonLabels.some(l => approvalSpecificPatterns.test(l));
309
+ if (hasApprovalButton) {
310
+ status = 'waiting_approval';
344
311
  }
345
- // Walk up from the request-input-panel to find the full approval card
346
- let approvalArea = null;
347
- if (requestPanel) {
348
- let p = requestPanel;
349
- for (let i = 0; i < 12 && p && p.parentElement; i++) {
350
- p = p.parentElement;
351
- const btns = p.querySelectorAll('button').length;
352
- const radios = p.querySelectorAll('[role="radio"], [role="option"]').length;
353
- const total = btns + radios;
354
- if (btns >= 4) {
355
- // Found the approval card with enough interactive elements
356
- approvalArea = p;
357
- break;
358
- }
359
- }
360
- }
361
-
362
-
363
-
364
-
365
- if (approvalArea) {
366
- // ─── Codex Approval Panel (request-input-panel based) ───
367
- // The approval form has:
368
- // - Option items as plain divs (e.g., "1.\n예", "2.\n네,...", "3.\n아니요,...")
369
- // - Action buttons (e.g., "건너뛰기", "제출⏎") as <button> elements
370
- // We need to extract BOTH for the dashboard.
371
-
372
- // Find parent container that has all options (level 3 from textarea, btns >= 4)
373
- // approvalArea is already set to this level.
374
-
375
- // Get the prompt message (from grandparent that starts with "Do you want...")
376
- let messageText = '';
377
- let msgParent = approvalArea.parentElement;
378
- for (let i = 0; i < 5 && msgParent; i++) {
379
- const t = (msgParent.innerText || '').trim();
380
- if (t.length > 20 && /[??]/.test(t.substring(0, 200))) {
381
- // Found a parent whose text starts with a question
382
- messageText = t.split('\n')[0].trim();
383
- break;
384
- }
385
- msgParent = msgParent.parentElement;
386
- }
387
- if (!messageText) {
388
- // Fallback: use the approvalArea parent's text, first sentence
389
- const parentText = (approvalArea.parentElement?.innerText || approvalArea.innerText || '').trim();
390
- const firstLine = parentText.split('\n')[0].trim();
391
- messageText = firstLine.length > 5 ? firstLine : 'Agent requires an interaction';
392
- }
393
-
394
- // Extract option labels from the panel's direct/nested children
395
- const allBtns = Array.from(approvalArea.querySelectorAll('button'))
396
- .filter(b => b.offsetWidth > 0 && !b.disabled)
397
- .map(b => (b.textContent || '').trim())
398
- .filter(t => t.length > 0 && t.length < 150);
399
-
400
- // Extract numbered option text items (the entire panel text, split by option numbering)
401
- const panelText = (approvalArea.innerText || '').trim();
402
- // Parse "1.\n예\n2.\n네,...\n3.\n아니요,...\n건너뛰기\n제출⏎" format
403
- const optionMatches = panelText.match(/\d+\.\n[^\n]+(?:\n[^\d][^\n]*)*/g) || [];
404
- let options = optionMatches.map(o => o.replace(/\n/g, '').trim()).filter(o => o.length > 0);
405
-
406
- // Clean: remove any trailing button labels that got captured in the last option
407
- const btnLabelsSet = new Set(allBtns);
408
- options = options.map(opt => {
409
- let changed = true;
410
- while (changed) {
411
- changed = false;
412
- for (const bl of btnLabelsSet) {
413
- if (opt.endsWith(bl)) {
414
- opt = opt.slice(0, -bl.length).trim();
415
- changed = true;
416
- }
417
- }
418
- }
419
- return opt;
420
- }).filter(o => o.length > 0);
421
-
422
- // Merge numbered options from both sources (buttons + text parsing)
423
- const allNumbered = [...new Set([...allBtns.filter(b => /^\d+\./.test(b)), ...options])];
424
- allNumbered.sort((a, b) => (parseInt(a) || 999) - (parseInt(b) || 999));
425
- const actionBtns = allBtns.filter(b => !/^\d+\./.test(b));
426
-
427
- // Combine: sorted numbered options first, then action buttons (deduped)
428
- const uniqueActions = [...new Set([...allNumbered, ...actionBtns])];
429
-
430
- if (uniqueActions.length > 0) {
431
- status = 'waiting_approval';
432
- activeModal = {
433
- message: messageText,
434
- buttons: uniqueActions
435
- };
436
- }
437
- } // end if (approvalArea)
438
312
 
439
313
  if (isTaskList) {
440
314
  status = messages.length === 0 ? 'idle' : status;
@@ -442,53 +316,30 @@
442
316
  if (!isVisible && messages.length === 0) status = 'panel_hidden';
443
317
 
444
318
  // ─── 4. Model / Mode ───
445
- // Language-agnostic detection via DOM structure.
446
- // Model: button text matching model name patterns (GPT-*, o1-*, claude-* — always English).
447
- // Mode: non-model aria-haspopup="menu" button in composer area (language-agnostic).
448
319
  let model = '';
449
320
  let mode = '';
450
- for (const d of [doc, document]) {
451
- // Search in composer area, or common footer containers
452
- const searchRoots = [
453
- d.querySelector('[class*="thread-composer-max-width"]'),
454
- d.querySelector('[class*="thread-composer"]'),
455
- d.querySelector('[class*="pb-2"]'),
456
- d.body,
457
- ].filter(Boolean);
458
-
459
- for (const searchRoot of searchRoots) {
460
- if (model && mode) break;
461
-
462
- // aria-haspopup="menu" buttons — dropdown triggers for model/mode
463
- if (!model || !mode) {
464
- const menuBtns = Array.from(searchRoot.querySelectorAll('button[aria-haspopup="menu"]'))
465
- .filter(b => b.offsetWidth > 0);
466
- for (const btn of menuBtns) {
467
- const text = (btn.textContent || '').trim();
468
- if (!model && /^(GPT-|gpt-|o\d|claude-|sonnet|opus)/i.test(text)) {
469
- model = text;
470
- } else if (!mode && text.length > 0 && text.length < 30) {
471
- mode = text;
472
- }
473
- }
474
- }
475
-
476
- // Fallback: any visible button with model-like text
477
- if (!model) {
478
- const allBtns = Array.from(searchRoot.querySelectorAll('button'))
479
- .filter(b => b.offsetWidth > 0);
480
- for (const btn of allBtns) {
481
- const text = (btn.textContent || '').trim();
482
- if (/^(GPT-|gpt-|o\d|claude-|sonnet|opus)/i.test(text)) {
483
- model = text;
484
- break;
485
- }
486
- }
487
- }
321
+ const footerButtons = doc.querySelectorAll(
322
+ '[class*="thread-composer-max-width"] button, [class*="pb-2"] button'
323
+ );
324
+ for (const btn of footerButtons) {
325
+ const text = (btn.textContent || '').trim();
326
+ if (/^(GPT-|gpt-|o\d|claude-)/i.test(text)) model = text;
327
+ if (/^(낮음|중간|높음|low|medium|high)$/i.test(text)) mode = text;
328
+ }
488
329
 
489
- if (model) break; // found model, no need to keep searching this scope
330
+ // ─── 5. Approval modal ───
331
+ let activeModal = null;
332
+ if (status === 'waiting_approval') {
333
+ const approvalBtns = buttons
334
+ .map(b => (b.textContent || '').trim())
335
+ .filter(t => t && t.length > 0 && t.length < 40 &&
336
+ /approve|accept|allow|confirm|run|proceed|cancel|deny|reject|승인|허용|실행|취소|거부/i.test(t));
337
+ if (approvalBtns.length > 0) {
338
+ activeModal = {
339
+ message: 'Codex wants to perform an action',
340
+ buttons: [...new Set(approvalBtns)],
341
+ };
490
342
  }
491
- if (model) break; // found in this frame
492
343
  }
493
344
 
494
345
  // ─── 6. Task info ───
@@ -2,79 +2,39 @@
2
2
  * Codex Extension — resolve_action
3
3
  *
4
4
  * Clicks approval/denial buttons in the Codex UI.
5
- * Actions: "approve", "deny", "cancel", or raw button text
5
+ * Actions: "approve", "deny", "cancel"
6
6
  *
7
- * Placeholders: ${action}, ${button}
7
+ * Placeholder: ${ACTION}
8
8
  */
9
9
  (() => {
10
10
  try {
11
- const action = ${action};
12
- const buttonText = ${button};
11
+ const action = ${ACTION};
13
12
 
14
- // Search in document (outer webview frame where Codex approval panel lives)
15
- const buttons = Array.from(document.querySelectorAll('button, [role="radio"], [role="button"], input[type="radio"] + label'))
16
- .filter(b => b.offsetWidth > 0 && !b.disabled && !b.closest('[inert]'));
13
+ const buttons = Array.from(document.querySelectorAll('button'))
14
+ .filter(b => b.offsetWidth > 0);
17
15
 
18
- const actionLower = (action || '').toLowerCase();
16
+ // Map action to button text patterns
19
17
  const patterns = {
20
18
  approve: /^(approve|accept|allow|confirm|run|proceed|yes|승인|허용|실행|확인)/i,
21
19
  deny: /^(deny|reject|no|거부|아니오)/i,
22
20
  cancel: /^(cancel|stop|취소|중지)/i,
23
21
  };
24
22
 
25
- // Determine what to search for: use buttonText if provided, otherwise match action pattern
26
- const searchText = buttonText || action || '';
27
- let targetBtn = null;
23
+ const pattern = patterns[action.toLowerCase()] || patterns.approve;
28
24
 
29
- const getBtnLabel = (b) => {
30
- let t = (b.textContent || '').trim();
31
- return t || (b.getAttribute('aria-label') || '').trim();
32
- };
33
-
34
- // 1. Try exact match on buttonText first
35
- if (buttonText) {
36
- targetBtn = buttons.find(b => getBtnLabel(b) === buttonText);
37
- // 2. Try startsWith match (for cases where button text has extra chars like ⏎)
38
- if (!targetBtn) {
39
- targetBtn = buttons.find(b => getBtnLabel(b).startsWith(buttonText));
40
- }
41
- // 3. Try includes match
42
- if (!targetBtn) {
43
- targetBtn = buttons.find(b => getBtnLabel(b).includes(buttonText));
44
- }
45
- }
46
-
47
- // 4. Fall back to pattern-based matching
48
- if (!targetBtn && patterns[actionLower]) {
49
- targetBtn = buttons.find(b => patterns[actionLower].test(getBtnLabel(b)));
50
- }
51
-
52
- // 5. Fall back to regex on raw action text
53
- if (!targetBtn) {
54
- const escapedAction = searchText.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
55
- const pattern = new RegExp(escapedAction, 'i');
56
- targetBtn = buttons.find(b => pattern.test(getBtnLabel(b)));
57
- }
58
-
59
- if (targetBtn) {
60
- targetBtn.click();
61
-
62
- // If this is a numbered option (1., 2., 3.), also click Submit after a short delay
63
- const clickedText = getBtnLabel(targetBtn);
64
- if (/^\d+\./.test(clickedText)) {
65
- setTimeout(() => {
66
- const submitBtn = buttons.find(b => /^(제출|submit)/i.test(getBtnLabel(b)));
67
- if (submitBtn && submitBtn !== targetBtn) submitBtn.click();
68
- }, 150);
25
+ for (const btn of buttons) {
26
+ const text = (btn.textContent || '').trim();
27
+ const label = btn.getAttribute('aria-label') || '';
28
+ if (pattern.test(text) || pattern.test(label)) {
29
+ btn.click();
30
+ return JSON.stringify({ success: true, action, clicked: text || label });
69
31
  }
70
-
71
- return JSON.stringify({ success: true, action, clicked: clickedText });
72
32
  }
73
33
 
74
34
  return JSON.stringify({
75
35
  success: false,
76
- error: `No button matching '${searchText}' found`,
77
- available: buttons.map(b => getBtnLabel(b)).filter(t => t.length > 0 && t.length < 80),
36
+ error: `No button matching action '${action}' found`,
37
+ available: buttons.map(b => (b.textContent || '').trim()).filter(t => t.length > 0 && t.length < 40),
78
38
  });
79
39
  } catch (e) {
80
40
  return JSON.stringify({ error: e.message || String(e) });
@@ -12,15 +12,35 @@
12
12
 
13
13
  // Find ProseMirror editor
14
14
  const editor = document.querySelector('.ProseMirror');
15
- if (!editor) return JSON.stringify({ error: 'Editor not found' });
15
+ if (!editor) return JSON.stringify({ error: 'ProseMirror editor not found' });
16
16
 
17
17
  // Focus the editor
18
18
  editor.focus();
19
19
 
20
- // Use execCommand to safely insert text. This avoids TrustedHTML errors
21
- // and naturally triggers ProseMirror's state updates and Keyboard/Input events.
22
- document.execCommand('selectAll', false, null);
23
- document.execCommand('insertText', false, message);
20
+ // Clear existing content
21
+ const existingP = editor.querySelector('p');
22
+ if (existingP) {
23
+ existingP.textContent = message;
24
+ // Dispatch input event for ProseMirror to detect the change
25
+ editor.dispatchEvent(new InputEvent('input', {
26
+ bubbles: true,
27
+ cancelable: true,
28
+ inputType: 'insertText',
29
+ data: message,
30
+ }));
31
+ } else {
32
+ // Fallback: create new paragraph
33
+ const p = document.createElement('p');
34
+ p.textContent = message;
35
+ editor.innerHTML = '';
36
+ editor.appendChild(p);
37
+ editor.dispatchEvent(new InputEvent('input', {
38
+ bubbles: true,
39
+ cancelable: true,
40
+ inputType: 'insertText',
41
+ data: message,
42
+ }));
43
+ }
24
44
 
25
45
  // Wait a tick then submit via Enter key
26
46
  setTimeout(() => {
@@ -229,7 +229,15 @@ export async function handleExtensionScript(h: CommandHelpers, args: any, script
229
229
  }
230
230
 
231
231
  const scriptFn = provider.scripts[actualScriptName as keyof typeof provider.scripts] as Function;
232
- const scriptCode = scriptFn(args);
232
+ // Normalize args: script placeholders use UPPERCASE (${MODE}, ${MODEL}, ${MESSAGE})
233
+ // but WebSocket args typically use lowercase. Add uppercase versions of common keys.
234
+ const normalizedArgs = { ...args };
235
+ for (const key of ['mode', 'model', 'message', 'action', 'button', 'text', 'sessionId']) {
236
+ if (key in normalizedArgs && !(key.toUpperCase() in normalizedArgs)) {
237
+ normalizedArgs[key.toUpperCase()] = normalizedArgs[key];
238
+ }
239
+ }
240
+ const scriptCode = scriptFn(normalizedArgs);
233
241
  if (!scriptCode) return { success: false, error: `Script '${actualScriptName}' returned null` };
234
242
 
235
243
  const cdpKey = provider.category === 'ide' ? (h.currentIdeType || agentType) : (h.currentIdeType || ideType);
@@ -249,10 +257,35 @@ export async function handleExtensionScript(h: CommandHelpers, args: any, script
249
257
  break;
250
258
  }
251
259
  }
252
- if (!targetSessionId) {
253
- return { success: false, error: `No active session found for ${agentType}` };
260
+
261
+ // IDE-level scripts (model/mode) try session frame first, fallback to main page
262
+ const IDE_LEVEL_SCRIPTS = ['listModes', 'setMode', 'listModels', 'setModel'];
263
+ if (IDE_LEVEL_SCRIPTS.includes(scriptName)) {
264
+ // Try session frame first (some extensions embed mode selector in their webview)
265
+ if (targetSessionId) {
266
+ try {
267
+ result = await cdp.evaluateInSessionFrame(targetSessionId, scriptCode);
268
+ // Check if result indicates "not found" — fallback to main page
269
+ const parsed = typeof result === 'string' ? JSON.parse(result) : result;
270
+ const notFound = parsed?.error?.includes('not found') || parsed?.error?.includes('no root');
271
+ if (notFound) {
272
+ LOG.info('Command', `[ExtScript] ${scriptName} not found in session frame → trying IDE main page`);
273
+ result = await cdp.evaluate(scriptCode, 30000);
274
+ }
275
+ } catch {
276
+ LOG.info('Command', `[ExtScript] ${scriptName} session frame failed → trying IDE main page`);
277
+ result = await cdp.evaluate(scriptCode, 30000);
278
+ }
279
+ } else {
280
+ LOG.info('Command', `[ExtScript] ${scriptName} no session → trying IDE main page`);
281
+ result = await cdp.evaluate(scriptCode, 30000);
282
+ }
283
+ } else {
284
+ if (!targetSessionId) {
285
+ return { success: false, error: `No active session found for ${agentType}` };
286
+ }
287
+ result = await cdp.evaluateInSessionFrame(targetSessionId, scriptCode);
254
288
  }
255
- result = await cdp.evaluateInSessionFrame(targetSessionId, scriptCode);
256
289
  } else if (hasWebviewScript && cdp.evaluateInWebviewFrame) {
257
290
  const matchText = provider.webviewMatchText;
258
291
  const matchFn = matchText ? (body: string) => body.includes(matchText) : undefined;
@@ -339,15 +339,29 @@ export class DevServer {
339
339
  }
340
340
  this.log(`Exec script length: ${scriptCode.length}, first 50 chars: ${scriptCode.slice(0, 50)}...`);
341
341
 
342
- // Execute webview script via evaluateInWebviewFrame
343
- const isWebviewScript = provider.category === 'extension' || scriptName.toLowerCase().includes('webview');
342
+ // Execute based on provider category
343
+ const isWebviewScript = scriptName.toLowerCase().includes('webview');
344
344
  let raw: any;
345
- if (isWebviewScript) {
345
+ if (provider.category === 'extension' && !isWebviewScript) {
346
+ // Extension scripts: prefer session frame (agent webview) — matching agent-stream poller behavior
347
+ const sessions = cdp.getAgentSessions();
348
+ let sessionId: string | null = null;
349
+ for (const [sid, target] of sessions) {
350
+ if (target.agentType === type) { sessionId = sid; break; }
351
+ }
352
+ if (sessionId) {
353
+ raw = await cdp.evaluateInSessionFrame(sessionId, scriptCode);
354
+ } else if (cdp.evaluateInWebviewFrame) {
355
+ // Fallback: try evaluateInWebviewFrame
356
+ const matchText = provider.webviewMatchText;
357
+ const matchFn = matchText ? (body: string) => body.includes(matchText) : undefined;
358
+ raw = await cdp.evaluateInWebviewFrame(scriptCode, matchFn);
359
+ } else {
360
+ raw = await cdp.evaluate(scriptCode, 30000);
361
+ }
362
+ } else if (isWebviewScript && cdp.evaluateInWebviewFrame) {
346
363
  const matchText = provider.webviewMatchText;
347
364
  const matchFn = matchText ? (body: string) => body.includes(matchText) : undefined;
348
- if (!cdp.evaluateInWebviewFrame) {
349
- throw new Error(`CDP manager does not support evaluateInWebviewFrame`);
350
- }
351
365
  raw = await cdp.evaluateInWebviewFrame(scriptCode, matchFn);
352
366
  } else {
353
367
  raw = await cdp.evaluate(scriptCode, 30000);
@@ -221,7 +221,7 @@ export class DaemonStatusReporter {
221
221
  LOG.debug('P2P', `sent (${JSON.stringify(payload).length} bytes)`);
222
222
  }
223
223
 
224
- // ═══ Server transmit (minimal routing meta only — sanitizeForRelay removes everything else) ═══
224
+ // ═══ Server transmit (minimal routing meta only) ═══
225
225
  if (opts?.p2pOnly) return;
226
226
  const wsPayload = {
227
227
  daemonMode: true,