@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.
Files changed (43) hide show
  1. package/dist/index.d.mts +164 -38
  2. package/dist/index.d.ts +164 -38
  3. package/dist/index.js +4051 -2547
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +3696 -2192
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/{normalize-tKg8IiDk.d.mts → normalize-auJAPmKy.d.mts} +669 -629
  8. package/dist/{normalize-tKg8IiDk.d.ts → normalize-auJAPmKy.d.ts} +669 -629
  9. package/dist/status/normalize.d.mts +1 -1
  10. package/dist/status/normalize.d.ts +1 -1
  11. package/package.json +5 -1
  12. package/src/agent-stream/forward.ts +6 -0
  13. package/src/boot/daemon-lifecycle.ts +7 -4
  14. package/src/cli-adapter-types.ts +2 -0
  15. package/src/cli-adapters/provider-cli-adapter.ts +148 -11
  16. package/src/cli-adapters/pty-transport.ts +100 -0
  17. package/src/cli-adapters/session-host-transport.ts +392 -0
  18. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +126 -0
  19. package/src/cli-adapters/terminal-backends/types.ts +17 -0
  20. package/src/cli-adapters/terminal-backends/xterm-backend.ts +87 -0
  21. package/src/cli-adapters/terminal-screen.ts +40 -53
  22. package/src/commands/cli-manager.ts +184 -55
  23. package/src/config/config.d.ts +116 -0
  24. package/src/config/workspace-activity.d.ts +22 -0
  25. package/src/config/workspaces.d.ts +84 -0
  26. package/src/daemon/dev-auto-implement.ts +1087 -0
  27. package/src/daemon/dev-cdp-handlers.ts +1003 -0
  28. package/src/daemon/dev-cli-debug.ts +288 -0
  29. package/src/daemon/dev-server-types.ts +45 -0
  30. package/src/daemon/dev-server.ts +121 -1698
  31. package/src/index.ts +5 -1
  32. package/src/providers/cli-provider-instance.ts +13 -1
  33. package/src/providers/contracts.d.ts +408 -0
  34. package/src/providers/contracts.ts +9 -0
  35. package/src/providers/extension-provider-instance.ts +50 -10
  36. package/src/providers/provider-instance-manager.ts +48 -10
  37. package/src/providers/provider-instance.d.ts +142 -0
  38. package/src/providers/provider-instance.ts +23 -1
  39. package/src/shared-types.d.ts +157 -0
  40. package/src/shared-types.ts +14 -0
  41. package/src/status/builders.ts +6 -0
  42. package/src/status/normalize.d.ts +14 -0
  43. package/src/types.d.ts +127 -0
@@ -1,3 +1,4 @@
1
+ import type { DevServerContext } from './dev-server-types.js';
1
2
  /**
2
3
  * Dev Server — HTTP API for Provider debugging + script development
3
4
  *
@@ -27,15 +28,18 @@ import type { DaemonCliManager } from '../commands/cli-manager.js';
27
28
  import { generateTemplate as genScaffoldTemplate, generateFiles as genScaffoldFiles } from './scaffold-template.js';
28
29
  import { VersionArchive, detectAllVersions } from '../providers/version-archive.js';
29
30
  import { LOG } from '../logging/logger.js';
31
+ import { handleCdpEvaluate, handleCdpClick, handleCdpDomQuery, handleScreenshot, handleScriptsRun, handleTypeAndSend, handleTypeAndSendAt, handleScriptHints, handleCdpTargets, handleDomInspect, handleDomChildren, handleDomAnalyze, handleFindCommon, handleFindByText, handleDomContext } from './dev-cdp-handlers.js';
32
+ import { handleCliStatus, handleCliLaunch, handleCliSend, handleCliStop, handleCliDebug, handleCliResolve, handleCliRaw, handleCliSSE } from './dev-cli-debug.js';
33
+ import { handleAutoImplement, handleAutoImplCancel, handleAutoImplSSE } from './dev-auto-implement.js';
30
34
 
31
35
  export const DEV_SERVER_PORT = 19280;
32
36
 
33
- export class DevServer {
37
+ export class DevServer implements DevServerContext {
34
38
  private server: http.Server | null = null;
35
- private providerLoader: ProviderLoader;
36
- private cdpManagers: Map<string, DaemonCdpManager>;
37
- private instanceManager: ProviderInstanceManager | null;
38
- private cliManager: DaemonCliManager | null;
39
+ public providerLoader: ProviderLoader;
40
+ public cdpManagers: Map<string, DaemonCdpManager>;
41
+ public instanceManager: ProviderInstanceManager | null;
42
+ public cliManager: DaemonCliManager | null;
39
43
  private logFn: (msg: string) => void;
40
44
  private sseClients: http.ServerResponse[] = [];
41
45
  private watchScriptPath: string | null = null;
@@ -43,9 +47,9 @@ export class DevServer {
43
47
  private watchTimer: NodeJS.Timeout | null = null;
44
48
 
45
49
  // Auto-implement state
46
- private autoImplProcess: ChildProcess | null = null;
47
- private autoImplSSEClients: http.ServerResponse[] = [];
48
- private autoImplStatus: { running: boolean; type: string | null; progress: any[] } = { running: false, type: null, progress: [] };
50
+ public autoImplProcess: ChildProcess | null = null;
51
+ public autoImplSSEClients: http.ServerResponse[] = [];
52
+ public autoImplStatus: { running: boolean; type: string | null; progress: any[] } = { running: false, type: null, progress: [] };
49
53
 
50
54
  // CLI debug SSE
51
55
  private cliSSEClients: http.ServerResponse[] = [];
@@ -64,7 +68,7 @@ export class DevServer {
64
68
  this.logFn = options.logFn || LOG.forComponent('DevServer').asLogFn();
65
69
  }
66
70
 
67
- private log(msg: string): void {
71
+ public log(msg: string): void {
68
72
  this.logFn(`[DevServer] ${msg}`);
69
73
  }
70
74
 
@@ -392,147 +396,23 @@ export class DevServer {
392
396
  }
393
397
 
394
398
  private async handleCdpEvaluate(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
395
- const body = await this.readBody(req);
396
- const { expression, timeout, ideType } = body;
397
- if (!expression) {
398
- this.json(res, 400, { error: 'expression required' });
399
- return;
400
- }
401
-
402
- const cdp = this.getCdp(ideType);
403
- if (!cdp && !ideType) {
404
- LOG.warn('DevServer', 'CDP evaluate without ideType — picked first connected manager');
405
- }
406
- if (!cdp?.isConnected) {
407
- this.json(res, 503, { error: 'No CDP connection available' });
408
- return;
409
- }
410
-
411
- try {
412
- const raw = await cdp.evaluate(expression, timeout || 30000);
413
- let result = raw;
414
- if (typeof raw === 'string') {
415
- try { result = JSON.parse(raw); } catch { /* keep */ }
416
- }
417
- this.json(res, 200, { result });
418
- } catch (e: any) {
419
- this.json(res, 500, { error: e.message });
420
- }
399
+ return handleCdpEvaluate(this, req, res);
421
400
  }
422
401
 
423
402
  private async handleCdpClick(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
424
- const body = await this.readBody(req);
425
- const { ideType, x, y } = body;
426
- if (x == null || y == null) {
427
- this.json(res, 400, { error: 'x and y coordinates required' });
428
- return;
429
- }
430
-
431
- const cdp = this.getCdp(ideType);
432
- if (!cdp?.isConnected) {
433
- this.json(res, 503, { error: 'No CDP connection available' });
434
- return;
435
- }
436
-
437
- try {
438
- await cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', x, y, button: 'left', clickCount: 1 });
439
- await cdp.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x, y, button: 'left', clickCount: 1 });
440
- this.json(res, 200, { success: true, clicked: true, x, y });
441
- } catch (e: any) {
442
- this.json(res, 500, { error: e.message });
443
- }
403
+ return handleCdpClick(this, req, res);
444
404
  }
445
405
 
446
406
  private async handleCdpDomQuery(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
447
- const body = await this.readBody(req);
448
- const { selector, limit = 10, ideType } = body;
449
- if (!selector) {
450
- this.json(res, 400, { error: 'selector required' });
451
- return;
452
- }
453
-
454
- const cdp = this.getCdp(ideType as string);
455
- if (!cdp) {
456
- this.json(res, 503, { error: 'No CDP connection available' });
457
- return;
458
- }
459
-
460
- const expr = `(() => {
461
- try {
462
- const els = document.querySelectorAll('${selector.replace(/'/g, "\\'")}');
463
- const results = [];
464
- for (let i = 0; i < Math.min(els.length, ${limit}); i++) {
465
- const el = els[i];
466
- results.push({
467
- index: i,
468
- tag: el.tagName?.toLowerCase(),
469
- id: el.id || null,
470
- class: el.className && typeof el.className === 'string' ? el.className.trim().slice(0, 200) : null,
471
- role: el.getAttribute?.('role') || null,
472
- text: (el.textContent || '').trim().slice(0, 100),
473
- visible: el.offsetParent !== null || el.offsetWidth > 0,
474
- 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; } })()
475
- });
476
- }
477
- return JSON.stringify({ total: els.length, results });
478
- } catch (e) { return JSON.stringify({ error: e.message }); }
479
- })()`;
480
-
481
- try {
482
- const raw = await cdp.evaluate(expr, 10000);
483
- const result = typeof raw === 'string' ? JSON.parse(raw) : raw;
484
- this.json(res, 200, result);
485
- } catch (e: any) {
486
- this.json(res, 500, { error: e.message });
487
- }
407
+ return handleCdpDomQuery(this, req, res);
488
408
  }
489
409
 
490
410
  private async handleScreenshot(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
491
- const url = new URL(req.url || '/', 'http://localhost');
492
- const ideType = url.searchParams.get('ideType') || undefined;
493
- const cdp = this.getCdp(ideType);
494
- if (!cdp) {
495
- this.json(res, 503, { error: 'No CDP connection available' });
496
- return;
497
- }
498
-
499
- try {
500
- // Get viewport metrics before capturing
501
- let vpW = 0, vpH = 0;
502
- try {
503
- const metrics = await cdp.send('Page.getLayoutMetrics', {}, 3000);
504
- const vp = metrics?.cssVisualViewport || metrics?.visualViewport;
505
- if (vp) {
506
- vpW = Math.round(vp.clientWidth || vp.width || 0);
507
- vpH = Math.round(vp.clientHeight || vp.height || 0);
508
- }
509
- } catch { /* ignore */ }
510
-
511
- const buf = await cdp.captureScreenshot();
512
- if (buf) {
513
- res.writeHead(200, {
514
- 'Content-Type': 'image/webp',
515
- 'X-Viewport-Width': String(vpW),
516
- 'X-Viewport-Height': String(vpH),
517
- });
518
- res.end(buf);
519
- } else {
520
- this.json(res, 500, { error: 'Screenshot failed' });
521
- }
522
- } catch (e: any) {
523
- this.json(res, 500, { error: e.message });
524
- }
411
+ return handleScreenshot(this, req, res);
525
412
  }
526
413
 
527
414
  private async handleScriptsRun(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
528
- const body = await this.readBody(req);
529
- const { type, script: scriptName, params } = body;
530
- if (!type || !scriptName) {
531
- this.json(res, 400, { error: 'type and script required' });
532
- return;
533
- }
534
- // Delegate to handleRunScript
535
- await this.handleRunScript(type, req, res, body);
415
+ return handleScriptsRun(this, req, res);
536
416
  }
537
417
 
538
418
  private async handleStatus(_req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
@@ -730,7 +610,7 @@ export class DevServer {
730
610
  // ─── Provider File Explorer ───
731
611
 
732
612
  /** Find the provider directory on disk */
733
- private findProviderDir(type: string): string | null {
613
+ public findProviderDir(type: string): string | null {
734
614
  return this.providerLoader.findProviderDir(type);
735
615
  }
736
616
 
@@ -844,143 +724,15 @@ export class DevServer {
844
724
  }
845
725
 
846
726
  private async handleTypeAndSend(type: string, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
847
- const body = await this.readBody(req);
848
- const { selector, text } = body;
849
- if (!selector || typeof selector !== 'string' || !text || typeof text !== 'string') {
850
- this.json(res, 400, { error: 'selector and text strings required' }); return;
851
- }
852
- const cdp = this.getCdp(type);
853
- if (!cdp) {
854
- this.json(res, 503, { error: `CDP not connected for '${type}'` }); return;
855
- }
856
- try {
857
- const sent = await cdp.typeAndSend(selector, text);
858
- this.json(res, 200, { sent });
859
- } catch (e: any) {
860
- this.json(res, 500, { error: e.message });
861
- }
727
+ return handleTypeAndSend(this, type, req, res);
862
728
  }
863
729
 
864
730
  private async handleTypeAndSendAt(type: string, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
865
- const body = await this.readBody(req);
866
- const { x, y, text } = body;
867
- if (typeof x !== 'number' || typeof y !== 'number' || !text || typeof text !== 'string') {
868
- this.json(res, 400, { error: 'x, y numbers and text string required' }); return;
869
- }
870
- const cdp = this.getCdp(type);
871
- if (!cdp) {
872
- this.json(res, 503, { error: `CDP not connected for '${type}'` }); return;
873
- }
874
- try {
875
- const sent = await cdp.typeAndSendAt(x, y, text);
876
- this.json(res, 200, { sent });
877
- } catch (e: any) {
878
- this.json(res, 500, { error: e.message });
879
- }
731
+ return handleTypeAndSendAt(this, type, req, res);
880
732
  }
881
733
 
882
734
  private async handleScriptHints(type: string, _req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
883
- const dir = this.findProviderDir(type);
884
- if (!dir) { this.json(res, 404, { error: `Provider not found: ${type}` }); return; }
885
-
886
- // Find scripts.js in the provider dir (may be versioned)
887
- let scriptsPath = '';
888
- const directScripts = path.join(dir, 'scripts.js');
889
- if (fs.existsSync(directScripts)) {
890
- scriptsPath = directScripts;
891
- } else {
892
- // Check versioned scripts dirs
893
- const scriptsDir = path.join(dir, 'scripts');
894
- if (fs.existsSync(scriptsDir)) {
895
- const versions = fs.readdirSync(scriptsDir).filter(d => {
896
- return fs.statSync(path.join(scriptsDir, d)).isDirectory();
897
- }).sort().reverse();
898
- for (const ver of versions) {
899
- const p = path.join(scriptsDir, ver, 'scripts.js');
900
- if (fs.existsSync(p)) { scriptsPath = p; break; }
901
- }
902
- }
903
- }
904
-
905
- if (!scriptsPath) {
906
- this.json(res, 200, { hints: {} });
907
- return;
908
- }
909
-
910
- try {
911
- const source = fs.readFileSync(scriptsPath, 'utf-8');
912
- const hints: Record<string, { template: Record<string, any>; description: string }> = {};
913
-
914
- // Parse exported functions and extract param usage
915
- const funcRegex = /module\.exports\.(\w+)\s*=\s*function\s+\w+\s*\(params\)/g;
916
- let match;
917
- while ((match = funcRegex.exec(source)) !== null) {
918
- const name = match[1];
919
- // Find the function body (rough: from match to next module.exports or end)
920
- const startIdx = match.index;
921
- const nextFunc = source.indexOf('module.exports.', startIdx + 1);
922
- const funcBody = source.substring(startIdx, nextFunc > 0 ? nextFunc : source.length);
923
-
924
- const paramFields: Record<string, any> = {};
925
-
926
- // Pattern 1: params?.xxx or params.xxx
927
- const dotRegex = /params\?\.([a-zA-Z_]+)|params\.([a-zA-Z_]+)/g;
928
- let dm;
929
- while ((dm = dotRegex.exec(funcBody)) !== null) {
930
- const field = dm[1] || dm[2];
931
- if (field === 'length') continue;
932
- if (!(field in paramFields)) {
933
- // Infer type from context
934
- if (/index|count|port|timeout/i.test(field)) paramFields[field] = 0;
935
- else if (/action|text|title|message|model|mode|button|name|filter/i.test(field)) paramFields[field] = '';
936
- else paramFields[field] = '';
937
- }
938
- }
939
-
940
- // Pattern 2: typeof params === 'string' ? params : params?.xxx
941
- const typeofRegex = /typeof params === 'string' \? params : params\?\.([a-zA-Z_]+)/g;
942
- let tm;
943
- while ((tm = typeofRegex.exec(funcBody)) !== null) {
944
- const field = tm[1];
945
- if (!(field in paramFields)) paramFields[field] = '';
946
- }
947
-
948
- // Pattern 3: typeof params === 'number' ? params : params?.xxx
949
- const numRegex = /typeof params === 'number' \? params : params\?\.([a-zA-Z_]+)/g;
950
- let nm;
951
- while ((nm = numRegex.exec(funcBody)) !== null) {
952
- const field = nm[1];
953
- if (!(field in paramFields)) paramFields[field] = 0;
954
- }
955
-
956
- // Determine description from function name
957
- const descriptions: Record<string, string> = {
958
- readChat: 'No params required',
959
- sendMessage: 'Text to send to the chat',
960
- listSessions: 'No params required',
961
- switchSession: 'Switch by index or title',
962
- newSession: 'No params required',
963
- focusEditor: 'No params required',
964
- openPanel: 'No params required',
965
- resolveAction: 'Approve/reject action buttons',
966
- listNotifications: 'Optional message filter',
967
- dismissNotification: 'Dismiss by index, message, or button',
968
- listModels: 'No params required',
969
- setModel: 'Model name to select',
970
- listModes: 'No params required',
971
- setMode: 'Mode name to select',
972
- };
973
-
974
- hints[name] = {
975
- template: Object.keys(paramFields).length > 0 ? paramFields : {},
976
- description: descriptions[name] || (Object.keys(paramFields).length > 0 ? 'Params: ' + Object.keys(paramFields).join(', ') : 'No params'),
977
- };
978
- }
979
-
980
- this.json(res, 200, { hints });
981
- } catch (e: any) {
982
- this.json(res, 500, { error: e.message });
983
- }
735
+ return handleScriptHints(this, type, _req, res);
984
736
  }
985
737
 
986
738
  // ─── Validate provider.json ───
@@ -1089,11 +841,7 @@ export class DevServer {
1089
841
 
1090
842
 
1091
843
  private async handleCdpTargets(_req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
1092
- const targets: { ide: string; connected: boolean; port: number }[] = [];
1093
- for (const [ide, cdp] of this.cdpManagers.entries()) {
1094
- targets.push({ ide, connected: cdp.isConnected, port: cdp.getPort() });
1095
- }
1096
- this.json(res, 200, { targets });
844
+ return handleCdpTargets(this, _req, res);
1097
845
  }
1098
846
 
1099
847
  // ─── Scaffold ───
@@ -1161,728 +909,36 @@ export class DevServer {
1161
909
  // ─── DOM Inspector ───
1162
910
 
1163
911
  private async handleDomInspect(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
1164
- const body = await this.readBody(req);
1165
- const { x, y, selector, ideType } = body;
1166
- const cdp = this.getCdp(ideType);
1167
- if (!cdp) { this.json(res, 503, { error: 'No CDP connection' }); return; }
1168
-
1169
- const selectorArg = selector ? JSON.stringify(selector) : 'null';
1170
- const inspectScript = `(() => {
1171
- function gs(el) {
1172
- if (!el || el === document.body) return 'body';
1173
- if (el.id) return '#' + CSS.escape(el.id);
1174
- let s = el.tagName.toLowerCase();
1175
- if (el.className && typeof el.className === 'string') {
1176
- const cls = el.className.trim().split(/\\s+/).filter(c => c && !c.startsWith('_')).slice(0, 3);
1177
- if (cls.length) s += '.' + cls.map(c => CSS.escape(c)).join('.');
1178
- }
1179
- const p = el.parentElement;
1180
- if (p) {
1181
- const sibs = [...p.children].filter(c => c.tagName === el.tagName);
1182
- if (sibs.length > 1) s += ':nth-child(' + ([...p.children].indexOf(el) + 1) + ')';
1183
- }
1184
- return s;
1185
- }
1186
- function gp(el) {
1187
- const parts = [];
1188
- let c = el;
1189
- while (c && c !== document.documentElement) { parts.unshift(gs(c)); c = c.parentElement; }
1190
- return parts;
1191
- }
1192
- function ni(el) {
1193
- if (!el) return null;
1194
- const tag = el.tagName?.toLowerCase() || '#text';
1195
- const attrs = {};
1196
- if (el.attributes) for (const a of el.attributes) if (a.name !== 'class' && a.name !== 'style') attrs[a.name] = a.value?.substring(0, 200);
1197
- const cls = (el.className && typeof el.className === 'string') ? el.className.trim().split(/\\s+/).filter(Boolean).slice(0, 10) : [];
1198
- const text = el.textContent?.trim().substring(0, 150) || '';
1199
- const dt = [...(el.childNodes||[])].filter(n=>n.nodeType===3).map(n=>n.textContent.trim()).filter(Boolean).join(' ').substring(0,100);
1200
- const cc = el.children?.length || 0;
1201
- const r = el.getBoundingClientRect?.();
1202
- 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 };
1203
- }
1204
- const sel = ${selectorArg};
1205
- let el = sel ? document.querySelector(sel) : document.elementFromPoint(${x || 0}, ${y || 0});
1206
- if (!el) return JSON.stringify({ error: 'No element found' });
1207
- const info = ni(el);
1208
- const ancestors = [];
1209
- let pp = el.parentElement;
1210
- while (pp && pp !== document.documentElement) {
1211
- ancestors.push({ tag: pp.tagName.toLowerCase(), selector: gs(pp), cls: (pp.className && typeof pp.className === 'string') ? pp.className.trim().split(/\\s+/).slice(0,3) : [] });
1212
- pp = pp.parentElement;
1213
- }
1214
- const children = [...(el.children||[])].slice(0,50).map(c => ni(c));
1215
- return JSON.stringify({ element: info, ancestors: ancestors.reverse(), children });
1216
- })()`;
1217
-
1218
- try {
1219
- const raw = await cdp.evaluate(inspectScript, 10000);
1220
- let result = raw;
1221
- if (typeof raw === 'string') { try { result = JSON.parse(raw as string); } catch { } }
1222
- this.json(res, 200, result as Record<string, unknown>);
1223
- } catch (e: any) {
1224
- this.json(res, 500, { error: e.message });
1225
- }
912
+ return handleDomInspect(this, req, res);
1226
913
  }
1227
914
 
1228
915
  private async handleDomChildren(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
1229
- const body = await this.readBody(req);
1230
- const { selector, ideType } = body;
1231
- const cdp = this.getCdp(ideType);
1232
- if (!cdp) { this.json(res, 503, { error: 'No CDP connection' }); return; }
1233
- if (!selector) { this.json(res, 400, { error: 'selector required' }); return; }
1234
-
1235
- const script = `(() => {
1236
- function gs(el) {
1237
- if (!el || el === document.body) return 'body';
1238
- if (el.id) return '#' + CSS.escape(el.id);
1239
- let s = el.tagName.toLowerCase();
1240
- if (el.className && typeof el.className === 'string') {
1241
- const cls = el.className.trim().split(/\\s+/).filter(c => c && !c.startsWith('_')).slice(0, 3);
1242
- if (cls.length) s += '.' + cls.map(c => CSS.escape(c)).join('.');
1243
- }
1244
- const p = el.parentElement;
1245
- if (p) {
1246
- const sibs = [...p.children].filter(c => c.tagName === el.tagName);
1247
- if (sibs.length > 1) s += ':nth-child(' + ([...p.children].indexOf(el) + 1) + ')';
1248
- }
1249
- return s;
1250
- }
1251
- const el = document.querySelector(${JSON.stringify(selector)});
1252
- if (!el) return JSON.stringify({ error: 'Element not found' });
1253
- const children = [...(el.children||[])].slice(0,100).map(c => {
1254
- const tag = c.tagName?.toLowerCase();
1255
- const cls = (c.className && typeof c.className === 'string') ? c.className.trim().split(/\\s+/).filter(Boolean).slice(0,10) : [];
1256
- const attrs = {};
1257
- for (const a of c.attributes) if (a.name!=='class'&&a.name!=='style') attrs[a.name] = a.value?.substring(0,200);
1258
- const text = c.textContent?.trim().substring(0,150)||'';
1259
- const dt = [...c.childNodes].filter(n=>n.nodeType===3).map(n=>n.textContent.trim()).filter(Boolean).join(' ').substring(0,100);
1260
- return { tag, cls, attrs, text, directText: dt, childCount: c.children?.length||0, selector: gs(c) };
1261
- });
1262
- return JSON.stringify({ selector: ${JSON.stringify(selector)}, childCount: el.children?.length||0, children });
1263
- })()`;
1264
-
1265
- try {
1266
- const raw = await cdp.evaluate(script, 10000);
1267
- let result = raw;
1268
- if (typeof raw === 'string') { try { result = JSON.parse(raw as string); } catch { } }
1269
- this.json(res, 200, result as Record<string, unknown>);
1270
- } catch (e: any) {
1271
- this.json(res, 500, { error: e.message });
1272
- }
916
+ return handleDomChildren(this, req, res);
1273
917
  }
1274
918
 
1275
919
  private async handleDomAnalyze(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
1276
- const body = await this.readBody(req);
1277
- const { ideType, selector, x, y } = body;
1278
- const cdp = this.getCdp(ideType);
1279
- if (!cdp) { this.json(res, 503, { error: 'No CDP connection' }); return; }
1280
-
1281
- const selectorArg = selector ? JSON.stringify(selector) : 'null';
1282
- const analyzeScript = `(() => {
1283
- function gs(el) {
1284
- if (!el || el === document.body) return 'body';
1285
- if (el.id) return '#' + CSS.escape(el.id);
1286
- let s = el.tagName.toLowerCase();
1287
- if (el.className && typeof el.className === 'string') {
1288
- const cls = el.className.trim().split(/\\s+/).filter(c => c && !c.startsWith('_')).slice(0, 3);
1289
- if (cls.length) s += '.' + cls.map(c => CSS.escape(c)).join('.');
1290
- }
1291
- return s;
1292
- }
1293
- function fp(el) {
1294
- const parts = [];
1295
- let c = el;
1296
- while (c && c !== document.documentElement) { parts.unshift(gs(c)); c = c.parentElement; }
1297
- return parts.join(' > ');
1298
- }
1299
- function sigOf(el) {
1300
- return el.tagName + '|' + ((el.className && typeof el.className === 'string') ? el.className.trim().split(/\\s+/).filter(c => c && !c.startsWith('_')).sort().join('.') : '');
1301
- }
1302
-
1303
- // Find target element
1304
- const sel = ${selectorArg};
1305
- let target = sel ? document.querySelector(sel) : document.elementFromPoint(${x || 0}, ${y || 0});
1306
- if (!target) return JSON.stringify({ error: 'Element not found' });
1307
-
1308
- const result = {
1309
- target: { tag: target.tagName.toLowerCase(), selector: fp(target), text: (target.textContent||'').trim().substring(0, 200) },
1310
- siblingPattern: null,
1311
- ancestorAnalysis: [],
1312
- subtreeTexts: [],
1313
- };
1314
-
1315
- // 1. Walk UP parents — at each level, find sibling patterns
1316
- let el = target;
1317
- let depth = 0;
1318
- while (el && el !== document.body && depth < 15) {
1319
- const parent = el.parentElement;
1320
- if (!parent) break;
1321
-
1322
- const mySig = sigOf(el);
1323
- const siblings = [...parent.children].filter(c => sigOf(c) === mySig);
1324
- const totalChildren = parent.children.length;
1325
- const childSel = gs(el).replace(/:nth-child\\(\\d+\\)/, '');
1326
- const parentSel = fp(parent);
1327
-
1328
- result.ancestorAnalysis.push({
1329
- depth,
1330
- parentTag: parent.tagName.toLowerCase(),
1331
- parentSelector: parentSel,
1332
- totalChildren,
1333
- matchingSiblings: siblings.length,
1334
- childSelector: childSel,
1335
- fullSelector: parentSel + ' > ' + childSel,
1336
- });
1337
-
1338
- // Best sibling pattern: 3+ matching siblings with text
1339
- if (!result.siblingPattern && siblings.length >= 3) {
1340
- const siblingData = siblings.map((s, i) => {
1341
- const directText = [...s.childNodes].filter(n => n.nodeType === 3).map(n => n.textContent.trim()).filter(Boolean).join(' ').substring(0, 120);
1342
- const allText = (s.textContent || '').trim().substring(0, 200);
1343
- const childCount = s.children?.length || 0;
1344
- const cls = (s.className && typeof s.className === 'string') ? s.className.trim().split(/\\s+/).filter(Boolean) : [];
1345
- const attrs = {};
1346
- if (s.attributes) for (const a of s.attributes) {
1347
- if (a.name !== 'class' && a.name !== 'style' && a.value) attrs[a.name] = a.value.substring(0, 100);
1348
- }
1349
- return { index: i, directText, allText, childCount, cls, attrs, tag: s.tagName.toLowerCase() };
1350
- });
1351
-
1352
- // Find common attributes across siblings
1353
- const allAttrs = siblingData.map(s => Object.keys(s.attrs));
1354
- const commonAttrs = allAttrs[0]?.filter(attr => allAttrs.every(a => a.includes(attr))) || [];
1355
- // Find varying attributes (data-*, role, etc)
1356
- const varyingAttrs = {};
1357
- for (const attr of commonAttrs) {
1358
- const values = siblingData.map(s => s.attrs[attr]);
1359
- const unique = [...new Set(values)];
1360
- if (unique.length > 1) varyingAttrs[attr] = unique.slice(0, 5);
1361
- }
1362
-
1363
- result.siblingPattern = {
1364
- count: siblings.length,
1365
- selector: parentSel + ' > ' + childSel,
1366
- parentSelector: parentSel,
1367
- depthFromTarget: depth,
1368
- siblings: siblingData.slice(0, 30),
1369
- commonAttrs,
1370
- varyingAttrs,
1371
- };
1372
- }
1373
-
1374
- el = parent;
1375
- depth++;
1376
- }
1377
-
1378
- // 2. Collect subtree text nodes from target
1379
- const walker = document.createTreeWalker(target, NodeFilter.SHOW_TEXT, null);
1380
- let node;
1381
- while ((node = walker.nextNode()) && result.subtreeTexts.length < 30) {
1382
- const text = node.textContent.trim();
1383
- if (text.length > 2) {
1384
- const parentTag = node.parentElement?.tagName?.toLowerCase() || '';
1385
- const parentCls = (node.parentElement?.className && typeof node.parentElement.className === 'string')
1386
- ? node.parentElement.className.trim().split(/\\s+/).filter(Boolean).slice(0,3).join('.') : '';
1387
- result.subtreeTexts.push({
1388
- text: text.substring(0, 150),
1389
- parentTag,
1390
- parentCls,
1391
- parentSelector: gs(node.parentElement),
1392
- });
1393
- }
1394
- }
1395
-
1396
- return JSON.stringify(result);
1397
- })()`;
1398
-
1399
- try {
1400
- const raw = await cdp.evaluate(analyzeScript, 15000);
1401
- let result = raw;
1402
- if (typeof raw === 'string') { try { result = JSON.parse(raw as string); } catch { } }
1403
- this.json(res, 200, result as Record<string, unknown>);
1404
- } catch (e: any) {
1405
- this.json(res, 500, { error: e.message });
1406
- }
920
+ return handleDomAnalyze(this, req, res);
1407
921
  }
1408
922
 
1409
923
  private async handleFindCommon(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
1410
- const body = await this.readBody(req);
1411
- const { include, exclude, ideType } = body;
1412
- if (!Array.isArray(include) || include.length === 0) { this.json(res, 400, { error: 'include[] is required' }); return; }
1413
- const cdp = this.getCdp(ideType);
1414
- if (!cdp) { this.json(res, 503, { error: 'No CDP connection' }); return; }
1415
-
1416
- const script = `(() => {
1417
- const includes = ${JSON.stringify(include)};
1418
- const excludes = ${JSON.stringify(exclude || [])};
1419
-
1420
- function gs(el) {
1421
- if (!el || el === document.body) return 'body';
1422
- if (el.id) return '#' + CSS.escape(el.id);
1423
- let s = el.tagName.toLowerCase();
1424
- if (el.className && typeof el.className === 'string') {
1425
- const cls = el.className.trim().split(/\\s+/).filter(c => c && !c.startsWith('_')).slice(0, 3);
1426
- if (cls.length) s += '.' + cls.map(c => CSS.escape(c)).join('.');
1427
- }
1428
- return s;
1429
- }
1430
- function fp(el) {
1431
- const parts = [];
1432
- let c = el;
1433
- while (c && c !== document.documentElement) { parts.unshift(gs(c)); c = c.parentElement; }
1434
- return parts.join(' > ');
1435
- }
1436
- function sig(el) {
1437
- return el.tagName + '|' + ((el.className && typeof el.className === 'string') ? el.className.trim() : '');
1438
- }
1439
-
1440
- // Step 1: For each include, find all matching leaf elements
1441
- const includeMatches = includes.map(text => {
1442
- const lower = text.toLowerCase();
1443
- const found = [];
1444
- const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
1445
- acceptNode: n => n.textContent.toLowerCase().includes(lower) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT
1446
- });
1447
- let node;
1448
- while ((node = walker.nextNode()) && found.length < 5) {
1449
- if (node.parentElement) found.push(node.parentElement);
1450
- }
1451
- return found;
1452
- });
1453
-
1454
- if (includeMatches.some(m => m.length === 0)) {
1455
- const missing = includes.filter((_, i) => includeMatches[i].length === 0);
1456
- return JSON.stringify({ results: [], message: 'Text not found: ' + missing.join(', ') });
1457
- }
1458
-
1459
- // Step 2: Find LCA for each combination of include elements
1460
- // For each pair of include[0] element and include[1] element, find their LCA
1461
- // Then within the LCA, find the direct-child subtree branch for each
1462
- const containers = [];
1463
- const seen = new Set();
1464
-
1465
- function findLCA(el1, el2) {
1466
- const ancestors1 = new Set();
1467
- let c = el1;
1468
- while (c) { ancestors1.add(c); c = c.parentElement; }
1469
- c = el2;
1470
- while (c) { if (ancestors1.has(c)) return c; c = c.parentElement; }
1471
- return document.body;
1472
- }
1473
-
1474
- function findDirectChildContaining(parent, descendant) {
1475
- let c = descendant;
1476
- while (c && c.parentElement !== parent) c = c.parentElement;
1477
- return c;
1478
- }
1479
-
1480
- // Try all combinations (first 3 matches per include)
1481
- for (const el1 of includeMatches[0].slice(0, 3)) {
1482
- for (let ii = 1; ii < includeMatches.length; ii++) {
1483
- for (const el2 of includeMatches[ii].slice(0, 3)) {
1484
- if (el1 === el2) continue;
1485
- const lca = findLCA(el1, el2);
1486
- if (!lca || lca === document.body || lca === document.documentElement) continue;
1487
-
1488
- // Find which direct child of LCA contains each include element
1489
- const child1 = findDirectChildContaining(lca, el1);
1490
- const child2 = findDirectChildContaining(lca, el2);
1491
- if (!child1 || !child2 || child1 === child2) continue;
1492
-
1493
- const lcaSel = fp(lca);
1494
- if (seen.has(lcaSel)) continue;
1495
- seen.add(lcaSel);
1496
-
1497
- // Check exclude
1498
- if (excludes.length > 0) {
1499
- const lcaText = (lca.textContent || '').toLowerCase();
1500
- if (excludes.some(ex => lcaText.includes(ex.toLowerCase()))) continue;
1501
- }
1502
-
1503
- // Are child1 and child2 same tag? (relaxed — ignore classes)
1504
- const tag1 = child1.tagName;
1505
- const tag2 = child2.tagName;
1506
-
1507
- // Bubble up: walk up from LCA, find the best list container
1508
- // (the one with most repeating same-tag children)
1509
- let container = lca;
1510
- let bestContainer = lca;
1511
- let bestListCount = 0;
1512
- for (let up = 0; up < 10; up++) {
1513
- const p = container.parentElement;
1514
- if (!p || p === document.body || p === document.documentElement) break;
1515
- // Check how many same-tag siblings 'container' has in parent
1516
- const myTag = container.tagName;
1517
- const sibCount = [...p.children].filter(c => c.tagName === myTag).length;
1518
- if (sibCount > bestListCount) {
1519
- bestListCount = sibCount;
1520
- bestContainer = p;
1521
- }
1522
- container = p;
1523
- }
1524
- container = bestListCount >= 3 ? bestContainer : lca;
1525
-
1526
- const allChildren = [...container.children];
1527
- const childTag = tag1 === tag2 ? tag1 : (allChildren.length > 0 ? allChildren[0].tagName : '');
1528
- const sameTagCount = allChildren.filter(c => c.tagName === childTag).length;
1529
- const isList = sameTagCount >= 3 && sameTagCount >= allChildren.length * 0.4;
1530
-
1531
- // Gather all same-tag children as list items
1532
- const listItems = isList
1533
- ? allChildren.filter(c => c.tagName === childTag)
1534
- : allChildren;
1535
-
1536
- // Filter rendered items (skip virtual scroll placeholders)
1537
- const rendered = listItems.filter(c => (c.innerText || '').trim().length > 0);
1538
- const placeholderCount = listItems.length - rendered.length;
1539
-
1540
- const containerSel = fp(container);
1541
- if (seen.has(containerSel)) continue;
1542
- seen.add(containerSel);
1543
-
1544
- const r = container.getBoundingClientRect();
1545
- containers.push({
1546
- selector: containerSel,
1547
- tag: container.tagName.toLowerCase(),
1548
- childCount: allChildren.length,
1549
- listItemCount: listItems.length,
1550
- renderedCount: rendered.length,
1551
- placeholderCount,
1552
- isList,
1553
- rect: { w: Math.round(r.width), h: Math.round(r.height) },
1554
- depth: containerSel.split(' > ').length,
1555
- items: rendered.slice(0, 30).map((el, i) => {
1556
- const fullText = (el.innerText || el.textContent || '').trim();
1557
- // Find snippet around first matched include text
1558
- let text = fullText.substring(0, 200);
1559
- const matched = [];
1560
- for (const inc of includes) {
1561
- const idx = fullText.toLowerCase().indexOf(inc.toLowerCase());
1562
- if (idx >= 0) {
1563
- matched.push(inc);
1564
- if (matched.length === 1) {
1565
- // Show snippet around first match
1566
- const start = Math.max(0, idx - 30);
1567
- const end = Math.min(fullText.length, idx + inc.length + 80);
1568
- text = (start > 0 ? '...' : '') + fullText.substring(start, end) + (end < fullText.length ? '...' : '');
1569
- }
1570
- }
1571
- }
1572
- return {
1573
- index: i,
1574
- tag: el.tagName.toLowerCase(),
1575
- cls: (el.className && typeof el.className === 'string') ? el.className.trim().split(/\\s+/).slice(0, 2).join(' ') : '',
1576
- text,
1577
- matchedIncludes: matched,
1578
- childCount: el.children.length,
1579
- h: Math.round(el.getBoundingClientRect().height),
1580
- };
1581
- }),
1582
- });
1583
- }
1584
- }
1585
- }
1586
-
1587
- // Sort: list containers first (more items = better), then by depth
1588
- containers.sort((a, b) => {
1589
- if (a.isList !== b.isList) return a.isList ? -1 : 1;
1590
- return b.listItemCount - a.listItemCount || b.depth - a.depth;
1591
- });
1592
-
1593
- return JSON.stringify({
1594
- results: containers.slice(0, 10),
1595
- includeCount: includes.length,
1596
- excludeCount: excludes.length,
1597
- });
1598
- })()`;
1599
-
1600
- try {
1601
- const raw = await cdp.evaluate(script, 10000);
1602
- let result = raw;
1603
- if (typeof raw === 'string') { try { result = JSON.parse(raw as string); } catch { } }
1604
- this.json(res, 200, result as Record<string, unknown>);
1605
- } catch (e: any) {
1606
- this.json(res, 500, { error: e.message });
1607
- }
924
+ return handleFindCommon(this, req, res);
1608
925
  }
1609
926
 
1610
927
  private async handleFindByText(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
1611
- const body = await this.readBody(req);
1612
- const { text, ideType, containerSelector } = body;
1613
- if (!text || typeof text !== 'string') { this.json(res, 400, { error: 'text is required' }); return; }
1614
- const cdp = this.getCdp(ideType);
1615
- if (!cdp) { this.json(res, 503, { error: 'No CDP connection' }); return; }
1616
-
1617
- const containerArg = containerSelector ? JSON.stringify(containerSelector) : 'null';
1618
- const script = `(() => {
1619
- function gs(el) {
1620
- if (!el || el === document.body) return 'body';
1621
- if (el.id) return '#' + CSS.escape(el.id);
1622
- let s = el.tagName.toLowerCase();
1623
- if (el.className && typeof el.className === 'string') {
1624
- const cls = el.className.trim().split(/\\s+/).filter(c => c && !c.startsWith('_')).slice(0, 3);
1625
- if (cls.length) s += '.' + cls.map(c => CSS.escape(c)).join('.');
1626
- }
1627
- return s;
1628
- }
1629
- function fp(el) {
1630
- const parts = [];
1631
- let c = el;
1632
- while (c && c !== document.documentElement) { parts.unshift(gs(c)); c = c.parentElement; }
1633
- return parts.join(' > ');
1634
- }
1635
- function parentSig(el) {
1636
- // Signature: tag+class chain up 3 levels
1637
- const parts = [];
1638
- let c = el;
1639
- for (let i = 0; i < 3 && c; i++) { parts.push(gs(c)); c = c.parentElement; }
1640
- return parts.join(' < ');
1641
- }
1642
-
1643
- const searchText = ${JSON.stringify(text)}.toLowerCase();
1644
- const container = ${containerArg} ? document.querySelector(${containerArg}) : document.body;
1645
- if (!container) return JSON.stringify({ error: 'Container not found' });
1646
-
1647
- const matches = [];
1648
- const seen = new Set();
1649
-
1650
- // Find all text nodes containing the search text
1651
- const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, {
1652
- acceptNode: n => n.textContent.toLowerCase().includes(searchText) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT
1653
- });
1654
- let node;
1655
- while ((node = walker.nextNode()) && matches.length < 50) {
1656
- // Walk up to find the most specific visible element
1657
- let el = node.parentElement;
1658
- if (!el) continue;
1659
-
1660
- // Skip hidden elements
1661
- const r = el.getBoundingClientRect();
1662
- if (r.width === 0 && r.height === 0) continue;
1663
-
1664
- const selector = fp(el);
1665
- if (seen.has(selector)) continue;
1666
- seen.add(selector);
1667
-
1668
- // Walk up parent chain — record each level's selector + sibling count
1669
- const ancestors = [];
1670
- let cur = el;
1671
- let pLvl = cur.parentElement;
1672
- for (let lvl = 0; lvl < 10 && pLvl && pLvl !== document.body; lvl++) {
1673
- const mySig = cur.tagName + '|' + ((cur.className && typeof cur.className === 'string') ? cur.className.trim().split(/\\s+/).sort().join('.') : '');
1674
- const sibs = [...pLvl.children].filter(c => {
1675
- const sig = c.tagName + '|' + ((c.className && typeof c.className === 'string') ? c.className.trim().split(/\\s+/).sort().join('.') : '');
1676
- return sig === mySig;
1677
- });
1678
- const childSel = gs(cur).replace(/:nth-child\\(\\d+\\)/, '');
1679
- ancestors.push({
1680
- parentSelector: fp(pLvl),
1681
- childSelector: childSel,
1682
- fullSelector: fp(pLvl) + ' > ' + childSel,
1683
- siblingCount: sibs.length,
1684
- parentTag: pLvl.tagName.toLowerCase(),
1685
- });
1686
- cur = pLvl;
1687
- pLvl = pLvl.parentElement;
1688
- }
1689
-
1690
- const directText = (node.textContent || '').trim().substring(0, 200);
1691
- const allText = (node.parentElement.textContent || '').trim().substring(0, 300);
1692
- const tag = node.parentElement.tagName.toLowerCase();
1693
- const cls = (node.parentElement.className && typeof node.parentElement.className === 'string')
1694
- ? node.parentElement.className.trim().split(/\\s+/).filter(Boolean) : [];
1695
-
1696
- matches.push({
1697
- selector,
1698
- tag,
1699
- cls,
1700
- directText,
1701
- allText,
1702
- ancestors,
1703
- rect: { w: Math.round(r.width), h: Math.round(r.height) },
1704
- depth: selector.split(' > ').length,
1705
- });
1706
- }
1707
-
1708
- // Sort: prefer elements with more siblings in ancestry, then fewer depth
1709
- matches.sort((a, b) => {
1710
- const aMax = Math.max(1, ...a.ancestors.map(x => x.siblingCount));
1711
- const bMax = Math.max(1, ...b.ancestors.map(x => x.siblingCount));
1712
- return (bMax - aMax) || (a.depth - b.depth);
1713
- });
1714
-
1715
- return JSON.stringify({ query: ${JSON.stringify(text)}, matches, total: matches.length });
1716
- })()`;
1717
-
1718
- try {
1719
- const raw = await cdp.evaluate(script, 10000);
1720
- let result = raw;
1721
- if (typeof raw === 'string') { try { result = JSON.parse(raw as string); } catch { } }
1722
- this.json(res, 200, result as Record<string, unknown>);
1723
- } catch (e: any) {
1724
- this.json(res, 500, { error: e.message });
1725
- }
928
+ return handleFindByText(this, req, res);
1726
929
  }
1727
930
 
1728
931
  // ─── Phase 1: DOM Context API ───
1729
932
 
1730
933
  private async handleDomContext(type: string, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
1731
- const body = await this.readBody(req);
1732
- const { ideType } = body;
1733
- const provider = this.providerLoader.resolve(type);
1734
- if (!provider) { this.json(res, 404, { error: `Provider not found: ${type}` }); return; }
1735
-
1736
- const cdp = this.getCdp(ideType || type);
1737
- if (!cdp) { this.json(res, 503, { error: 'No CDP connection available. Target IDE must be running with CDP enabled.' }); return; }
1738
-
1739
- try {
1740
- // 1. Capture screenshot
1741
- let screenshot: string | null = null;
1742
- try {
1743
- const buf = await cdp.captureScreenshot();
1744
- if (buf) screenshot = buf.toString('base64');
1745
- } catch { /* screenshot optional */ }
1746
-
1747
- // 2. Collect DOM snapshot
1748
- const domScript = `(() => {
1749
- function gs(el) {
1750
- if (!el || el === document.body) return 'body';
1751
- if (el.id) return '#' + CSS.escape(el.id);
1752
- let s = el.tagName.toLowerCase();
1753
- if (el.className && typeof el.className === 'string') {
1754
- const cls = el.className.trim().split(/\\s+/).filter(c => c && !c.startsWith('_')).slice(0, 3);
1755
- if (cls.length) s += '.' + cls.map(c => CSS.escape(c)).join('.');
1756
- }
1757
- return s;
1758
- }
1759
- function fp(el) {
1760
- const parts = [];
1761
- let c = el;
1762
- while (c && c !== document.documentElement) { parts.unshift(gs(c)); c = c.parentElement; }
1763
- return parts.join(' > ');
1764
- }
1765
- function rect(el) {
1766
- 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) }; }
1767
- catch { return null; }
1768
- }
1769
-
1770
- const result = { contentEditables: [], chatContainers: [], buttons: [], sidebars: [], dropdowns: [], inputs: [] };
1771
-
1772
- // Content editables + textareas + inputs
1773
- document.querySelectorAll('[contenteditable], textarea, input[type="text"], input:not([type])').forEach(el => {
1774
- if (el.offsetWidth === 0 && el.offsetHeight === 0) return;
1775
- result.contentEditables.push({
1776
- selector: fp(el),
1777
- tag: el.tagName.toLowerCase(),
1778
- contenteditable: el.getAttribute('contenteditable'),
1779
- role: el.getAttribute('role'),
1780
- ariaLabel: el.getAttribute('aria-label'),
1781
- placeholder: el.getAttribute('placeholder'),
1782
- rect: rect(el),
1783
- visible: el.offsetParent !== null || el.offsetWidth > 0,
1784
- });
1785
- });
1786
-
1787
- // Chat containers — large divs with scroll
1788
- document.querySelectorAll('div, section, main').forEach(el => {
1789
- const style = getComputedStyle(el);
1790
- const isScrollable = style.overflowY === 'auto' || style.overflowY === 'scroll';
1791
- const r = el.getBoundingClientRect();
1792
- if (!isScrollable || r.height < 200 || r.width < 200) return;
1793
- const childCount = el.children.length;
1794
- if (childCount < 2) return;
1795
- result.chatContainers.push({
1796
- selector: fp(el),
1797
- childCount,
1798
- rect: { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) },
1799
- hasScrollable: true,
1800
- scrollTop: Math.round(el.scrollTop),
1801
- scrollHeight: Math.round(el.scrollHeight),
1802
- });
1803
- });
1804
-
1805
- // Buttons
1806
- document.querySelectorAll('button, [role="button"]').forEach(el => {
1807
- if (el.offsetWidth === 0 && el.offsetHeight === 0) return;
1808
- const text = (el.textContent || '').trim().substring(0, 80);
1809
- if (!text && !el.getAttribute('aria-label')) return;
1810
- result.buttons.push({
1811
- text,
1812
- ariaLabel: el.getAttribute('aria-label'),
1813
- selector: fp(el),
1814
- rect: rect(el),
1815
- disabled: el.disabled || el.getAttribute('aria-disabled') === 'true',
1816
- });
1817
- });
1818
-
1819
- // Sidebars — panels on left/right edges
1820
- document.querySelectorAll('[class*="sidebar"], [class*="side-bar"], [class*="panel"], [role="complementary"], [role="navigation"], aside').forEach(el => {
1821
- if (el.offsetWidth === 0 && el.offsetHeight === 0) return;
1822
- const r = el.getBoundingClientRect();
1823
- if (r.width < 50 || r.height < 200) return;
1824
- result.sidebars.push({
1825
- selector: fp(el),
1826
- position: r.x < window.innerWidth / 3 ? 'left' : r.x > window.innerWidth * 2 / 3 ? 'right' : 'center',
1827
- rect: { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) },
1828
- childCount: el.children.length,
1829
- });
1830
- });
1831
-
1832
- // Dropdowns — select, popover, menu patterns
1833
- document.querySelectorAll('select, [role="listbox"], [role="menu"], [role="combobox"], [class*="dropdown"], [class*="popover"]').forEach(el => {
1834
- result.dropdowns.push({
1835
- selector: fp(el),
1836
- tag: el.tagName.toLowerCase(),
1837
- role: el.getAttribute('role'),
1838
- visible: el.offsetParent !== null || el.offsetWidth > 0,
1839
- rect: rect(el),
1840
- });
1841
- });
1842
-
1843
- return JSON.stringify(result);
1844
- })()`;
1845
-
1846
- const raw = await cdp.evaluate(domScript, 15000);
1847
- let domSnapshot: any = {};
1848
- if (typeof raw === 'string') { try { domSnapshot = JSON.parse(raw); } catch { domSnapshot = { raw }; } }
1849
- else domSnapshot = raw;
1850
-
1851
- this.json(res, 200, {
1852
- screenshot: screenshot ? `base64:${screenshot}` : null,
1853
- domSnapshot,
1854
- pageTitle: await cdp.evaluate('document.title', 3000).catch(() => ''),
1855
- pageUrl: await cdp.evaluate('window.location.href', 3000).catch(() => ''),
1856
- providerType: type,
1857
- timestamp: new Date().toISOString(),
1858
- });
1859
- } catch (e: any) {
1860
- this.json(res, 500, { error: `DOM context collection failed: ${e.message}` });
1861
- }
934
+ return handleDomContext(this, type, req, res);
1862
935
  }
1863
936
 
1864
937
  // ─── Phase 2: Auto-Implement Backend ───
1865
938
 
1866
- private getDefaultAutoImplReference(category: string, type: string): string {
1867
- if (category === 'cli') {
1868
- return type === 'codex-cli' ? 'claude-cli' : 'codex-cli';
1869
- }
1870
- return 'antigravity';
1871
- }
1872
939
 
1873
- private resolveAutoImplReference(category: string, requestedReference: string | undefined, targetType: string): string | null {
1874
- const desired = requestedReference || this.getDefaultAutoImplReference(category, targetType);
1875
- const ref = this.providerLoader.resolve(desired) || this.providerLoader.getMeta(desired);
1876
- if (ref?.category === category) return desired;
1877
940
 
1878
- const all = this.providerLoader.getAll();
1879
- const fallback = all
1880
- .filter((p: any) => p.category === category && p.type !== targetType)
1881
- .sort((a: any, b: any) => String(a.type || '').localeCompare(String(b.type || ''), undefined, { numeric: true, sensitivity: 'base' }))[0];
1882
- return fallback?.type || null;
1883
- }
1884
-
1885
- private getLatestScriptVersionDir(scriptsDir: string): string | null {
941
+ public getLatestScriptVersionDir(scriptsDir: string): string | null {
1886
942
  if (!fs.existsSync(scriptsDir)) return null;
1887
943
 
1888
944
  const versions = fs.readdirSync(scriptsDir)
@@ -1943,432 +999,9 @@ export class DevServer {
1943
999
  return { dir: desiredDir };
1944
1000
  }
1945
1001
 
1946
- private loadAutoImplReferenceScripts(referenceType: string | null): Record<string, string> {
1947
- if (!referenceType) return {};
1948
-
1949
- const refDir = this.findProviderDir(referenceType);
1950
- if (!refDir || !fs.existsSync(refDir)) return {};
1951
-
1952
- const referenceScripts: Record<string, string> = {};
1953
- const scriptsDir = path.join(refDir, 'scripts');
1954
- const latestDir = this.getLatestScriptVersionDir(scriptsDir);
1955
- if (!latestDir) return referenceScripts;
1956
-
1957
- for (const file of fs.readdirSync(latestDir)) {
1958
- if (!file.endsWith('.js')) continue;
1959
- try {
1960
- referenceScripts[file] = fs.readFileSync(path.join(latestDir, file), 'utf-8');
1961
- } catch {
1962
- // ignore broken reference files
1963
- }
1964
- }
1965
- return referenceScripts;
1966
- }
1967
1002
 
1968
1003
  private async handleAutoImplement(type: string, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
1969
- const body = await this.readBody(req);
1970
- const { agent = 'claude-cli', functions, reference, model, comment, providerDir: requestedProviderDir } = body;
1971
- if (!functions || !Array.isArray(functions) || functions.length === 0) {
1972
- this.json(res, 400, { error: 'functions[] is required (e.g. ["readChat", "sendMessage"])' });
1973
- return;
1974
- }
1975
-
1976
- if (this.autoImplStatus.running) {
1977
- this.json(res, 409, { error: 'Auto-implement already in progress', type: this.autoImplStatus.type });
1978
- return;
1979
- }
1980
-
1981
- const provider = this.providerLoader.resolve(type);
1982
- if (!provider) { this.json(res, 404, { error: `Provider not found: ${type}` }); return; }
1983
-
1984
- const writableProvider = this.resolveAutoImplWritableProviderDir(provider.category, type, requestedProviderDir);
1985
- if (!writableProvider.dir) {
1986
- this.json(res, 409, {
1987
- error: writableProvider.reason || `Auto-implement only writes to the canonical user provider directory for '${type}'.`,
1988
- });
1989
- return;
1990
- }
1991
- const providerDir = writableProvider.dir;
1992
-
1993
- try {
1994
- // 1. Collect DOM context
1995
- // 1. Skip heavy DOM pre-parsing (Agent will use cURL to explore via CDP!)
1996
- const resolvedReference = this.resolveAutoImplReference(provider.category, reference, type);
1997
- this.sendAutoImplSSE({
1998
- event: 'progress',
1999
- data: {
2000
- function: '_init',
2001
- status: 'analyzing',
2002
- message: provider.category === 'cli'
2003
- ? 'Initializing agent (granting CLI PTY debug access)...'
2004
- : 'Initializing agent (granting DOM access)...'
2005
- }
2006
- });
2007
- const domContext = null;
2008
-
2009
- // 2. Load reference scripts
2010
- this.sendAutoImplSSE({
2011
- event: 'progress',
2012
- data: {
2013
- function: '_init',
2014
- status: 'loading_reference',
2015
- message: `Loading reference script (${resolvedReference || 'none'})...`
2016
- }
2017
- });
2018
-
2019
- const referenceScripts = this.loadAutoImplReferenceScripts(resolvedReference);
2020
-
2021
- // 3. Build the prompt
2022
- const prompt = this.buildAutoImplPrompt(type, provider, providerDir, functions, domContext, referenceScripts, comment, resolvedReference);
2023
-
2024
- // 4. Write prompt to temp file (avoids shell escaping issues with special chars)
2025
- const tmpDir = path.join(os.tmpdir(), 'adhdev-autoimpl');
2026
- if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
2027
- const promptFile = path.join(tmpDir, `prompt-${type}-${Date.now()}.md`);
2028
- fs.writeFileSync(promptFile, prompt, 'utf-8');
2029
- this.log(`Auto-implement prompt written to ${promptFile} (${prompt.length} chars)`);
2030
-
2031
- // 5. Determine agent command from provider spawn config
2032
- const agentProvider = this.providerLoader.resolve(agent) || this.providerLoader.getMeta(agent);
2033
- const spawn = (agentProvider as any)?.spawn;
2034
- if (!spawn?.command) {
2035
- try { fs.unlinkSync(promptFile); } catch { /* ignore */ }
2036
- this.json(res, 400, { error: `Agent '${agent}' has no spawn config. Select a CLI provider with a spawn configuration.` });
2037
- return;
2038
- }
2039
-
2040
- const agentCategory = (agentProvider as any)?.category;
2041
-
2042
- // ─── ACP Agent: use ACP SDK (JSON-RPC protocol) ───
2043
- if (agentCategory === 'acp') {
2044
- this.sendAutoImplSSE({ event: 'progress', data: { function: '_init', status: 'spawning', message: `Spawning ACP agent: ${spawn.command} ${(spawn.args || []).join(' ')}` } });
2045
- this.autoImplStatus = { running: true, type, progress: [] };
2046
-
2047
- // Dynamic import ACP SDK
2048
- const { ClientSideConnection, ndJsonStream, PROTOCOL_VERSION } = await import('@agentclientprotocol/sdk');
2049
- const { Readable, Writable } = await import('stream');
2050
- const { spawn: spawnFn } = await import('child_process');
2051
-
2052
- // Add model override to spawn args if specified
2053
- const acpArgs = [...(spawn.args || [])];
2054
- if (model) {
2055
- acpArgs.push('--model', model);
2056
- this.log(`Auto-implement ACP using model: ${model}`);
2057
- }
2058
-
2059
- const child = spawnFn(spawn.command, acpArgs, {
2060
- cwd: providerDir,
2061
- stdio: ['pipe', 'pipe', 'pipe'],
2062
- shell: spawn.shell ?? false,
2063
- env: { ...process.env, ...(spawn.env || {}) },
2064
- });
2065
- this.autoImplProcess = child;
2066
-
2067
- // stderr → stream to SSE
2068
- child.stderr?.on('data', (d: Buffer) => {
2069
- const chunk = d.toString();
2070
- this.sendAutoImplSSE({ event: 'output', data: { chunk, stream: 'stderr' } });
2071
- });
2072
-
2073
- // Setup ACP connection via SDK
2074
- const webStdin = Writable.toWeb(child.stdin!) as WritableStream<Uint8Array>;
2075
- const webStdout = Readable.toWeb(child.stdout!) as ReadableStream<Uint8Array>;
2076
- const stream = ndJsonStream(webStdin, webStdout);
2077
-
2078
- const connection = new ClientSideConnection((_agent: any) => ({
2079
- // Auto-approve all tool calls for auto-implement
2080
- requestPermission: async (params: any) => {
2081
- const allowOpt = params.options?.find((o: any) => o.kind === 'allow_once') || params.options?.[0];
2082
- this.sendAutoImplSSE({ event: 'output', data: { chunk: `[ACP] Auto-approved: ${params.toolCall?.title || 'tool call'}\n`, stream: 'stdout' } });
2083
- return { outcome: { outcome: 'selected', optionId: allowOpt?.optionId || '' } };
2084
- },
2085
- sessionUpdate: async (params: any) => {
2086
- const update = params?.update;
2087
- if (!update) return;
2088
- // Stream meaningful output only (skip thought chunks — they're too verbose)
2089
- switch (update.sessionUpdate) {
2090
- case 'agent_message_chunk':
2091
- if (update.content?.text) {
2092
- this.sendAutoImplSSE({ event: 'output', data: { chunk: update.content.text, stream: 'stdout' } });
2093
- }
2094
- break;
2095
- case 'tool_call':
2096
- this.sendAutoImplSSE({ event: 'output', data: { chunk: `\n🔧 [Tool] ${update.title || 'unknown'}\n`, stream: 'stdout' } });
2097
- break;
2098
- case 'tool_call_update':
2099
- if (update.status === 'completed' || update.status === 'failed') {
2100
- const label = update.status === 'completed' ? '✅' : '❌';
2101
- const out = update.rawOutput ? (typeof update.rawOutput === 'string' ? update.rawOutput : JSON.stringify(update.rawOutput)) : '';
2102
- this.sendAutoImplSSE({ event: 'output', data: { chunk: `${label} Result: ${out.slice(0, 1000)}\n`, stream: 'stdout' } });
2103
- }
2104
- break;
2105
- case 'agent_thought_chunk':
2106
- // Skip — too verbose for auto-implement UI
2107
- break;
2108
- default:
2109
- break;
2110
- }
2111
- },
2112
- // Not used for auto-implement
2113
- readTextFile: async () => { throw new Error('not supported'); },
2114
- writeTextFile: async () => { throw new Error('not supported'); },
2115
- createTerminal: async () => { throw new Error('not supported'); },
2116
- terminalOutput: async () => { throw new Error('not supported'); },
2117
- releaseTerminal: async () => { throw new Error('not supported'); },
2118
- waitForTerminalExit: async () => { throw new Error('not supported'); },
2119
- killTerminal: async () => { throw new Error('not supported'); },
2120
- }), stream);
2121
-
2122
- child.on('exit', (code) => {
2123
- this.autoImplProcess = null;
2124
- this.autoImplStatus.running = false;
2125
- const success = code === 0;
2126
- this.sendAutoImplSSE({ event: 'complete', data: { success, exitCode: code, functions, message: success ? '✅ ACP Auto-implement complete' : `❌ ACP agent exited (code: ${code})` } });
2127
- try { this.providerLoader.reload(); } catch { /* ignore */ }
2128
- try { fs.unlinkSync(promptFile); } catch { /* ignore */ }
2129
- this.log(`Auto-implement (ACP) ${success ? 'completed' : 'failed'}: ${type} (exit: ${code})`);
2130
- });
2131
-
2132
- // ACP handshake flow (async, runs in background)
2133
- (async () => {
2134
- try {
2135
- this.sendAutoImplSSE({ event: 'progress', data: { function: '_init', status: 'initializing', message: 'ACP initialize...' } });
2136
- await connection.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} });
2137
-
2138
- this.sendAutoImplSSE({ event: 'progress', data: { function: '_init', status: 'session', message: 'Creating ACP session...' } });
2139
- const session = await connection.newSession({ cwd: providerDir, mcpServers: [] });
2140
- const sessionId = session?.sessionId;
2141
- if (!sessionId) throw new Error('No sessionId returned from session/new');
2142
-
2143
- this.sendAutoImplSSE({ event: 'progress', data: { function: '_init', status: 'prompting', message: `Sending prompt (${prompt.length} chars)...` } });
2144
- await connection.prompt({
2145
- sessionId,
2146
- prompt: [{ type: 'text', text: prompt }],
2147
- });
2148
-
2149
- this.sendAutoImplSSE({ event: 'progress', data: { function: '_done', status: 'complete', message: '✅ ACP prompt processing complete' } });
2150
- } catch (e: any) {
2151
- this.sendAutoImplSSE({ event: 'output', data: { chunk: `[ACP Error] ${e.message}\n`, stream: 'stderr' } });
2152
- this.log(`Auto-implement ACP error: ${e.message}`);
2153
- // Process exit will trigger the 'complete' SSE event
2154
- if (child.exitCode === null) { child.kill('SIGTERM'); }
2155
- }
2156
- })();
2157
-
2158
- this.json(res, 202, {
2159
- started: true, type, agent: spawn.command, functions, providerDir,
2160
- message: 'ACP Auto-implement started. Connect to SSE for progress.',
2161
- sseUrl: `/api/providers/${type}/auto-implement/status`,
2162
- });
2163
- return;
2164
- }
2165
-
2166
- // ─── CLI Agent: stdin pipe approach ───
2167
- const command: string = spawn.command;
2168
- // Strip interactive-only flags for auto-implement (non-interactive mode)
2169
- const interactiveFlags = ['--yolo', '--interactive', '-i'];
2170
- const baseArgs: string[] = [...(spawn.args || [])].filter((a: string) => !interactiveFlags.includes(a));
2171
-
2172
- // 6. Construct the complete shell command per-agent
2173
- let shellCmd: string;
2174
-
2175
- if (command === 'claude') {
2176
- // Claude Code: autonomous agent mode (no --print), skip permissions, prompt via meta-prompt
2177
- const args = [...baseArgs, '--dangerously-skip-permissions'];
2178
- if (model) args.push('--model', model);
2179
- const escapedArgs = args.map(a => `'${a.replace(/'/g, "'\\''")}'`).join(' ');
2180
- const metaPrompt = `Read the file at ${promptFile} and follow ALL the instructions. Implement the specific function requested, then test it via CDP curl targeting 127.0.0.1:19280, wait for confirmation of success, and then close. DO NOT start working on other features not listed in the prompt constraint.`;
2181
- shellCmd = `${command} ${escapedArgs} -p "${metaPrompt}"`;
2182
- } else if (command === 'gemini') {
2183
- // Gemini CLI: non-interactive prompt mode
2184
- // We can't use @file syntax (causes Parts object parsing bug) or $(cat) (arg too long).
2185
- // Solution: meta-prompt that tells Gemini to read the instructions file itself.
2186
- const args = [...baseArgs, '-y', '-s', 'false'];
2187
- if (model) args.push('-m', model);
2188
- const escapedArgs = args.map(a => `'${a.replace(/'/g, "'\\''")}'`).join(' ');
2189
- shellCmd = `${command} ${escapedArgs} -p "Read the file at ${promptFile} and follow ALL the instructions in it exactly. Do not ask questions, just execute."`;
2190
-
2191
- } else if (command === 'codex') {
2192
- const args = ['exec', ...baseArgs];
2193
- if (!args.includes('--dangerously-bypass-approvals-and-sandbox')) {
2194
- args.push('--dangerously-bypass-approvals-and-sandbox');
2195
- }
2196
- if (!args.includes('--skip-git-repo-check')) {
2197
- args.push('--skip-git-repo-check');
2198
- }
2199
- if (model) args.push('--model', model);
2200
- const escapedArgs = args.map(a => `'${a.replace(/'/g, "'\\''")}'`).join(' ');
2201
- const metaPrompt = `Read the file at ${promptFile} and follow ALL instructions strictly. DO NOT spend time exploring the filesystem or other providers. You have full authority to implement ALL required script files and independently test them against 127.0.0.1:19280 via CDP CURL. Upon complete validation of ALL assigned files, print exactly "_PIPELINE_COMPLETE_SIGNAL_" to gracefully close the pipeline. DO NOT WAIT FOR APPROVAL, execute completely autonomously.`;
2202
- shellCmd = `${command} ${escapedArgs} "${metaPrompt}"`;
2203
- } else {
2204
- // Generic fallback: pipe prompt via stdin
2205
- const escapedArgs = baseArgs.map(a => `'${a.replace(/'/g, "'\\''")}'`).join(' ');
2206
- shellCmd = `cat '${promptFile}' | ${command} ${escapedArgs}`;
2207
- }
2208
-
2209
- this.sendAutoImplSSE({ event: 'progress', data: { function: '_init', status: 'spawning', message: `Spawning agent: ${shellCmd.substring(0, 200)}... (prompt: ${prompt.length} chars)` } });
2210
-
2211
- this.autoImplStatus = { running: true, type, progress: [] };
2212
- const spawnedAt = Date.now();
2213
-
2214
- let child: any;
2215
- let isPty = false;
2216
- const { spawn: spawnFn } = await import('child_process');
2217
-
2218
- try {
2219
- const pty = require('node-pty');
2220
- this.log(`Auto-implement spawn (PTY): ${shellCmd}`);
2221
- const isWin = os.platform() === 'win32';
2222
- child = pty.spawn(isWin ? 'cmd.exe' : (process.env.SHELL || '/bin/zsh'), [isWin ? '/c' : '-c', shellCmd], {
2223
- name: 'xterm-256color',
2224
- cols: 120,
2225
- rows: 40,
2226
- cwd: providerDir,
2227
- env: { ...process.env, ...(spawn.env || {}) },
2228
- });
2229
- isPty = true;
2230
- } catch (err: any) {
2231
- this.log(`PTY not available, using child_process: ${err.message}`);
2232
- child = spawnFn('sh', ['-c', shellCmd], {
2233
- cwd: providerDir,
2234
- shell: false,
2235
- timeout: 900000,
2236
- stdio: ['pipe', 'pipe', 'pipe'],
2237
- env: {
2238
- ...process.env,
2239
- ...(spawn.env || {}),
2240
- ...(command === 'gemini' ? { SANDBOX: '1', GEMINI_CLI_NO_RELAUNCH: '1' } : {}),
2241
- },
2242
- });
2243
- child.on('error', (err: Error) => {
2244
- this.log(`Auto-implement spawn error: ${err.message}`);
2245
- this.sendAutoImplSSE({ event: 'output', data: { chunk: `[Spawn Error] ${err.message}\n`, stream: 'stderr' } });
2246
- });
2247
- }
2248
-
2249
- this.autoImplProcess = child;
2250
- let stdout = '';
2251
- let stderr = '';
2252
-
2253
- let approvalPatterns: RegExp[] = [];
2254
- let approvalKeys: Record<number, string> = { 0: 'y\r' };
2255
- let approvalBuffer = '';
2256
- let lastApprovalTime = 0;
2257
-
2258
- try {
2259
- const { normalizeCliProviderForRuntime } = await import('../cli-adapters/provider-cli-adapter.js');
2260
- const normalized = normalizeCliProviderForRuntime(agentProvider);
2261
- approvalPatterns = normalized.patterns.approval;
2262
- approvalKeys = (agentProvider as any)?.approvalKeys || { 0: 'y\r', 1: 'a\r' };
2263
- } catch (err: any) {
2264
- this.log(`Failed to load approval patterns: ${err.message}`);
2265
- }
2266
-
2267
- const checkAutoApproval = (chunk: string, writeFn: (s: string) => void) => {
2268
- // Strip ANSI
2269
- const cleanData = chunk.replace(/\x1B\[\d*[A-HJKSTfG]/g, ' ')
2270
- .replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '')
2271
- .replace(/\x1B\][^\x07]*\x07/g, '')
2272
- .replace(/\x1B\][^\x1B]*\x1B\\/g, '')
2273
- .replace(/ +/g, ' ');
2274
-
2275
- approvalBuffer = (approvalBuffer + cleanData).slice(-1500);
2276
-
2277
- // Force exit on completion signal (check cleanData directly to avoid stale buffer echo matches)
2278
- const elapsed = Date.now() - spawnedAt;
2279
- if (elapsed > 15000 && cleanData.includes('_PIPELINE_COMPLETE_SIGNAL_')) {
2280
- this.log(`Agent finished task after ${Math.round(elapsed/1000)}s. Terminating interactive CLI session to unblock pipeline.`);
2281
- this.sendAutoImplSSE({ event: 'output', data: { chunk: `\n[🤖 ADHDev Pipeline] Completion token detected. Proceeding...\n`, stream: 'stdout' } });
2282
- approvalBuffer = '';
2283
-
2284
- try {
2285
- (this.autoImplProcess as any).kill('SIGINT');
2286
- } catch {
2287
- // ignore
2288
- }
2289
- return;
2290
- }
2291
-
2292
- // Use a cooldown to prevent overlapping approval submissions
2293
- if (Date.now() - lastApprovalTime < 2000) return;
2294
-
2295
- if (approvalPatterns.some(p => p.test(approvalBuffer))) {
2296
- // Use 'Always allow' (1) if available, otherwise 'Allow once' (0), otherwise hard fallback to 'a\r' for newer CLIs
2297
- const key = approvalKeys[1] || approvalKeys[0] || 'a\r';
2298
- writeFn(key);
2299
- this.log(`Auto-Implement auto-approved prompt! Sending: ${JSON.stringify(key)}`);
2300
- this.sendAutoImplSSE({ event: 'output', data: { chunk: `\n[🤖 ADHDev Auto-Approve] CLI Action Approved\n`, stream: 'stdout' } });
2301
- approvalBuffer = '';
2302
- lastApprovalTime = Date.now();
2303
- }
2304
- };
2305
-
2306
- if (isPty) {
2307
- child.onData((data: string) => {
2308
- stdout += data;
2309
- if (data.includes('\x1b[6n')) {
2310
- child.write('\x1b[12;1R');
2311
- this.log('Terminal CPR request (\\x1b[6n) intercepted in PTY, responding with dummy coordinates [12;1R]');
2312
- }
2313
- checkAutoApproval(data, (s) => child.write(s));
2314
- this.sendAutoImplSSE({ event: 'output', data: { chunk: data, stream: 'stdout' } });
2315
- });
2316
- child.onExit(({ exitCode: code }: { exitCode: number }) => {
2317
- this.autoImplProcess = null;
2318
- this.autoImplStatus.running = false;
2319
- const success = code === 0;
2320
- this.sendAutoImplSSE({
2321
- event: 'complete',
2322
- data: { success, exitCode: code, functions, message: success ? '✅ Auto-implement complete' : `❌ Agent exited (code: ${code})` },
2323
- });
2324
- try { this.providerLoader.reload(); } catch { /* ignore */ }
2325
- try { fs.unlinkSync(promptFile); } catch { /* ignore */ }
2326
- });
2327
- } else {
2328
- child.stdout?.on('data', (d: Buffer) => {
2329
- const chunk = d.toString();
2330
- stdout += chunk;
2331
- if (chunk.includes('\x1b[6n')) child.stdin?.write('\x1b[1;1R');
2332
- checkAutoApproval(chunk, (s) => child.stdin?.write(s));
2333
- this.sendAutoImplSSE({ event: 'output', data: { chunk, stream: 'stdout' } });
2334
- });
2335
- child.stderr?.on('data', (d: Buffer) => {
2336
- const chunk = d.toString();
2337
- stderr += chunk;
2338
- checkAutoApproval(chunk, (s) => child.stdin?.write(s));
2339
- this.sendAutoImplSSE({ event: 'output', data: { chunk, stream: 'stderr' } });
2340
- });
2341
- child.on('exit', (code: number) => {
2342
- this.autoImplProcess = null;
2343
- this.autoImplStatus.running = false;
2344
- const success = code === 0;
2345
- this.sendAutoImplSSE({
2346
- event: 'complete',
2347
- data: {
2348
- success,
2349
- exitCode: code,
2350
- functions,
2351
- message: success ? '✅ Auto-implement complete' : `❌ Agent exited (code: ${code})`,
2352
- },
2353
- });
2354
- try { this.providerLoader.reload(); } catch { /* ignore */ }
2355
- try { fs.unlinkSync(promptFile); } catch { /* ignore */ }
2356
- this.log(`Auto-implement ${success ? 'completed' : 'failed'}: ${type} (exit: ${code})`);
2357
- });
2358
- }
2359
- this.json(res, 202, {
2360
- started: true,
2361
- type,
2362
- agent: command,
2363
- functions,
2364
- providerDir,
2365
- message: 'Auto-implement started. Connect to SSE for progress.',
2366
- sseUrl: `/api/providers/${type}/auto-implement/status`,
2367
- });
2368
- } catch (e: any) {
2369
- this.autoImplStatus.running = false;
2370
- this.json(res, 500, { error: `Auto-implement failed: ${e.message}` });
2371
- }
1004
+ return handleAutoImplement(this, type, req, res);
2372
1005
  }
2373
1006
 
2374
1007
  private buildAutoImplPrompt(
@@ -2397,21 +1030,51 @@ export class DevServer {
2397
1030
  lines.push(`Provider directory: \`${providerDir}\``);
2398
1031
  lines.push('');
2399
1032
 
2400
- // ── Existing target files (inline, so no reading needed) ──
2401
- lines.push('## Current Target Files');
2402
- lines.push('These are the files you need to EDIT. They contain TODO stubs — replace them with working implementations.');
2403
- lines.push('');
1033
+ // ── funcToFile mapping (needed early for file classification) ──
1034
+ const funcToFile: Record<string, string> = {
1035
+ readChat: 'read_chat.js', sendMessage: 'send_message.js',
1036
+ resolveAction: 'resolve_action.js', listSessions: 'list_sessions.js',
1037
+ listChats: 'list_chats.js', switchSession: 'switch_session.js',
1038
+ newSession: 'new_session.js', focusEditor: 'focus_editor.js',
1039
+ openPanel: 'open_panel.js', listModels: 'list_models.js',
1040
+ listModes: 'list_modes.js', setModel: 'set_model.js', setMode: 'set_mode.js',
1041
+ };
1042
+ const targetFileNames = new Set(functions.map(fn => funcToFile[fn]).filter(Boolean));
2404
1043
 
1044
+ // ── Existing target files (inline, so no reading needed) ──
2405
1045
  const scriptsDir = path.join(providerDir, 'scripts');
2406
1046
  const latestScriptsDir = this.getLatestScriptVersionDir(scriptsDir);
2407
1047
  if (latestScriptsDir) {
2408
1048
  lines.push(`Scripts version directory: \`${latestScriptsDir}\``);
2409
1049
  lines.push('');
1050
+
1051
+ // Target files: editable
1052
+ lines.push('## ✏️ Target Files (EDIT THESE)');
1053
+ lines.push('These are the ONLY files you are allowed to modify. Replace the TODO stubs with working implementations.');
1054
+ lines.push('');
2410
1055
  for (const file of fs.readdirSync(latestScriptsDir)) {
2411
- if (file.endsWith('.js')) {
1056
+ if (file.endsWith('.js') && targetFileNames.has(file)) {
1057
+ try {
1058
+ const content = fs.readFileSync(path.join(latestScriptsDir, file), 'utf-8');
1059
+ lines.push(`### \`${file}\` ✏️ EDIT`);
1060
+ lines.push('```javascript');
1061
+ lines.push(content);
1062
+ lines.push('```');
1063
+ lines.push('');
1064
+ } catch { /* skip */ }
1065
+ }
1066
+ }
1067
+
1068
+ // Non-target files: reference only
1069
+ const refFiles = fs.readdirSync(latestScriptsDir).filter(f => f.endsWith('.js') && !targetFileNames.has(f));
1070
+ if (refFiles.length > 0) {
1071
+ lines.push('## 🔒 Other Scripts (REFERENCE ONLY — DO NOT EDIT)');
1072
+ lines.push('These files are shown for context only. Do NOT modify them under any circumstances.');
1073
+ lines.push('');
1074
+ for (const file of refFiles) {
2412
1075
  try {
2413
1076
  const content = fs.readFileSync(path.join(latestScriptsDir, file), 'utf-8');
2414
- lines.push(`### \`${file}\``);
1077
+ lines.push(`### \`${file}\` 🔒`);
2415
1078
  lines.push('```javascript');
2416
1079
  lines.push(content);
2417
1080
  lines.push('```');
@@ -2431,15 +1094,7 @@ export class DevServer {
2431
1094
  lines.push('');
2432
1095
  }
2433
1096
 
2434
- // ── Reference implementation ──
2435
- const funcToFile: Record<string, string> = {
2436
- readChat: 'read_chat.js', sendMessage: 'send_message.js',
2437
- resolveAction: 'resolve_action.js', listSessions: 'list_sessions.js',
2438
- listChats: 'list_chats.js', switchSession: 'switch_session.js',
2439
- newSession: 'new_session.js', focusEditor: 'focus_editor.js',
2440
- openPanel: 'open_panel.js', listModels: 'list_models.js',
2441
- listModes: 'list_modes.js', setModel: 'set_model.js', setMode: 'set_mode.js',
2442
- };
1097
+ // ── Reference implementation ── (funcToFile already defined above)
2443
1098
 
2444
1099
  if (Object.keys(referenceScripts).length > 0) {
2445
1100
  lines.push(`## Reference Implementation (from ${referenceType || 'antigravity'} provider)`);
@@ -2499,6 +1154,7 @@ export class DevServer {
2499
1154
 
2500
1155
  // ── Rules ──
2501
1156
  lines.push('## Rules');
1157
+ lines.push('0. **🚫 SCOPE CONSTRAINT**: You may ONLY edit files marked ✏️ EDIT above. ALL other files are READ-ONLY. Do NOT modify, rewrite, refactor, or "improve" any file not explicitly marked as editable — even if you notice bugs or improvements. No exceptions.');
2502
1158
  lines.push('1. **Scripts WITHOUT params** → IIFE: `(() => { ... })()`');
2503
1159
  lines.push('2. **Scripts WITH params** → arrow: `(params) => { ... }` — router calls `(${script})(${JSON.stringify(params)})`');
2504
1160
  lines.push('3. If live DOM analysis is included above, use it. Otherwise, discover selectors yourself via CDP before coding.');
@@ -2664,20 +1320,29 @@ export class DevServer {
2664
1320
  lines.push('Provider category: `cli`');
2665
1321
  lines.push('');
2666
1322
 
2667
- lines.push('## Current Target Files');
2668
- lines.push('These are the files you need to edit. Replace TODO or heuristic-only logic with working PTY-aware implementations.');
2669
- lines.push('');
1323
+ const funcToFile: Record<string, string> = {
1324
+ parseOutput: 'parse_output.js',
1325
+ detectStatus: 'detect_status.js',
1326
+ parseApproval: 'parse_approval.js',
1327
+ };
1328
+ const targetFileNames = new Set(functions.map(fn => funcToFile[fn]).filter(Boolean));
2670
1329
 
2671
1330
  const scriptsDir = path.join(providerDir, 'scripts');
2672
1331
  const latestScriptsDir = this.getLatestScriptVersionDir(scriptsDir);
2673
1332
  if (latestScriptsDir) {
2674
1333
  lines.push(`Scripts version directory: \`${latestScriptsDir}\``);
2675
1334
  lines.push('');
1335
+
1336
+ // Target files: editable
1337
+ lines.push('## ✏️ Target Files (EDIT THESE)');
1338
+ lines.push('These are the ONLY files you are allowed to modify. Replace TODO or heuristic-only logic with working PTY-aware implementations.');
1339
+ lines.push('');
2676
1340
  for (const file of fs.readdirSync(latestScriptsDir)) {
2677
1341
  if (!file.endsWith('.js')) continue;
1342
+ if (!targetFileNames.has(file)) continue;
2678
1343
  try {
2679
1344
  const content = fs.readFileSync(path.join(latestScriptsDir, file), 'utf-8');
2680
- lines.push(`### \`${file}\``);
1345
+ lines.push(`### \`${file}\` ✏️ EDIT`);
2681
1346
  lines.push('```javascript');
2682
1347
  lines.push(content);
2683
1348
  lines.push('```');
@@ -2686,13 +1351,29 @@ export class DevServer {
2686
1351
  // ignore
2687
1352
  }
2688
1353
  }
1354
+
1355
+ // Non-target files: reference only
1356
+ const refFiles = fs.readdirSync(latestScriptsDir).filter(f => f.endsWith('.js') && !targetFileNames.has(f));
1357
+ if (refFiles.length > 0) {
1358
+ lines.push('## 🔒 Other Scripts (REFERENCE ONLY — DO NOT EDIT)');
1359
+ lines.push('These files are shown for context only. Do NOT modify them under any circumstances.');
1360
+ lines.push('');
1361
+ for (const file of refFiles) {
1362
+ try {
1363
+ const content = fs.readFileSync(path.join(latestScriptsDir, file), 'utf-8');
1364
+ lines.push(`### \`${file}\` 🔒`);
1365
+ lines.push('```javascript');
1366
+ lines.push(content);
1367
+ lines.push('```');
1368
+ lines.push('');
1369
+ } catch {
1370
+ // ignore
1371
+ }
1372
+ }
1373
+ }
2689
1374
  }
2690
1375
 
2691
- const funcToFile: Record<string, string> = {
2692
- parseOutput: 'parse_output.js',
2693
- detectStatus: 'detect_status.js',
2694
- parseApproval: 'parse_approval.js',
2695
- };
1376
+
2696
1377
 
2697
1378
  if (Object.keys(referenceScripts).length > 0) {
2698
1379
  lines.push(`## Reference Implementation (from ${referenceType || 'another CLI'} provider)`);
@@ -2748,6 +1429,7 @@ export class DevServer {
2748
1429
  lines.push('');
2749
1430
 
2750
1431
  lines.push('## Rules');
1432
+ lines.push('0. **🚫 SCOPE CONSTRAINT**: You may ONLY edit files marked ✏️ EDIT above. ALL other files are READ-ONLY. Do NOT modify, rewrite, refactor, or "improve" any file not explicitly marked as editable — even if you notice bugs or improvements. No exceptions.');
2751
1433
  lines.push('1. These scripts run in Node.js CommonJS, not in the browser. Do NOT use DOM APIs.');
2752
1434
  lines.push('2. Prefer `screenText` for current visible UI state. That is the PTY equivalent of parsing the current IDE DOM.');
2753
1435
  lines.push('3. Use `messages` as prior transcript state so redraws do not duplicate old turns on every parse.');
@@ -2756,7 +1438,7 @@ export class DevServer {
2756
1438
  lines.push('6. `parseApproval` should understand the live approval area and return clean button labels.');
2757
1439
  lines.push('7. Use `rawBuffer` only when ANSI/control-sequence artifacts matter. Do not depend on raw escape noise unless necessary.');
2758
1440
  lines.push('8. Keep exports compatible with the existing `scripts.js` router (`module.exports = function ...`).');
2759
- lines.push('9. Do not rewrite unrelated provider config. Only touch the scripts needed for this task unless a tiny supporting change is required.');
1441
+ lines.push('9. Do NOT modify ANY file not explicitly marked ✏️ EDIT above. No exceptions no "tiny supporting changes" to other files.');
2760
1442
  lines.push('10. When the verification API returns `instanceId`, keep using that exact instance for follow-up `send`, `resolve`, `raw`, and `stop` calls. Do not assume type-only routing is safe if multiple sessions exist.');
2761
1443
  lines.push('11. Do NOT repeatedly dump the same target files. Read the target scripts once, reproduce the bug, then move directly to patching.');
2762
1444
  lines.push('12. If the user instructions include concrete screen text, raw PTY snippets, or a specific repro, treat that as the primary acceptance criteria.');
@@ -2845,40 +1527,14 @@ export class DevServer {
2845
1527
  }
2846
1528
 
2847
1529
  private handleAutoImplSSE(type: string, req: http.IncomingMessage, res: http.ServerResponse): void {
2848
- res.writeHead(200, {
2849
- 'Content-Type': 'text/event-stream',
2850
- 'Cache-Control': 'no-cache',
2851
- 'Connection': 'keep-alive',
2852
- 'Access-Control-Allow-Origin': '*',
2853
- });
2854
- res.write(`data: ${JSON.stringify({ type: 'connected', running: this.autoImplStatus.running, providerType: type })}\n\n`);
2855
-
2856
- // Replay existing progress
2857
- for (const p of this.autoImplStatus.progress) {
2858
- res.write(`event: ${p.event}\ndata: ${JSON.stringify(p.data)}\n\n`);
2859
- }
2860
-
2861
- this.autoImplSSEClients.push(res);
2862
- req.on('close', () => {
2863
- this.autoImplSSEClients = this.autoImplSSEClients.filter(c => c !== res);
2864
- });
1530
+ handleAutoImplSSE(this, type, req, res);
2865
1531
  }
2866
1532
 
2867
- private handleAutoImplCancel(_type: string, _req: http.IncomingMessage, res: http.ServerResponse): void {
2868
- if (this.autoImplProcess) {
2869
- this.autoImplProcess.kill('SIGTERM');
2870
- setTimeout(() => { if (this.autoImplProcess) this.autoImplProcess.kill('SIGKILL'); }, 3000);
2871
- this.sendAutoImplSSE({ event: 'complete', data: { success: false, exitCode: -1, message: '⛔ Aborted by user' } });
2872
- this.autoImplProcess = null;
2873
- this.autoImplStatus.running = false;
2874
- this.json(res, 200, { cancelled: true });
2875
- } else {
2876
- this.autoImplStatus.running = false;
2877
- this.json(res, 200, { cancelled: false, message: 'No running process' });
2878
- }
1533
+ private async handleAutoImplCancel(_type: string, _req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
1534
+ return handleAutoImplCancel(this, _type, _req, res);
2879
1535
  }
2880
1536
 
2881
- private sendAutoImplSSE(msg: { event: string; data: any }): void {
1537
+ public sendAutoImplSSE(msg: { event: string; data: any }): void {
2882
1538
  this.autoImplStatus.progress.push(msg);
2883
1539
  const payload = `event: ${msg.event}\ndata: ${JSON.stringify(msg.data)}\n\n`;
2884
1540
  for (const client of this.autoImplSSEClients) {
@@ -2889,7 +1545,7 @@ export class DevServer {
2889
1545
  /** Get CDP manager — matching IDE when ideType specified, first connected one otherwise.
2890
1546
  * DevServer is a debugging tool so first-connected fallback is acceptable,
2891
1547
  * but callers should pass ideType when possible. */
2892
- private getCdp(ideType?: string): DaemonCdpManager | null {
1548
+ public getCdp(ideType?: string): DaemonCdpManager | null {
2893
1549
  if (ideType) {
2894
1550
  const cdp = this.cdpManagers.get(ideType);
2895
1551
  if (cdp?.isConnected) return cdp;
@@ -2907,12 +1563,12 @@ export class DevServer {
2907
1563
  return null;
2908
1564
  }
2909
1565
 
2910
- private json(res: http.ServerResponse, status: number, data: any): void {
1566
+ public json(res: http.ServerResponse, status: number, data: any): void {
2911
1567
  res.writeHead(status, { 'Content-Type': 'application/json' });
2912
1568
  res.end(JSON.stringify(data, null, 2));
2913
1569
  }
2914
1570
 
2915
- private async readBody(req: http.IncomingMessage): Promise<any> {
1571
+ public async readBody(req: http.IncomingMessage): Promise<any> {
2916
1572
  return new Promise((resolve) => {
2917
1573
  let body = '';
2918
1574
  req.on('data', (chunk) => body += chunk);
@@ -2930,144 +1586,31 @@ export class DevServer {
2930
1586
 
2931
1587
  /** GET /api/cli/status — list all running CLI/ACP instances with state */
2932
1588
  private async handleCliStatus(_req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
2933
- if (!this.instanceManager) {
2934
- this.json(res, 503, { error: 'InstanceManager not available (daemon not fully initialized)' });
2935
- return;
2936
- }
2937
- const allStates = this.instanceManager.collectAllStates();
2938
- const cliStates = allStates.filter(s => s.category === 'cli' || s.category === 'acp');
2939
- const result = cliStates.map(s => ({
2940
- instanceId: s.instanceId,
2941
- type: s.type,
2942
- name: s.name,
2943
- category: s.category,
2944
- status: s.status,
2945
- mode: s.mode,
2946
- workspace: s.workspace,
2947
- messageCount: s.activeChat?.messages?.length || 0,
2948
- lastMessage: s.activeChat?.messages?.slice(-1)[0] || null,
2949
- activeModal: s.activeChat?.activeModal || null,
2950
- pendingEvents: s.pendingEvents || [],
2951
- currentModel: s.currentModel,
2952
- settings: s.settings,
2953
- }));
2954
- this.json(res, 200, { instances: result, count: result.length });
1589
+ return handleCliStatus(this, _req, res);
2955
1590
  }
2956
1591
 
2957
- private findCliTarget(type?: string, instanceId?: string): any | null {
2958
- if (!this.instanceManager) return null;
2959
- const cliStates = this.instanceManager
2960
- .collectAllStates()
2961
- .filter(s => s.category === 'cli' || s.category === 'acp');
2962
- if (instanceId) return cliStates.find(s => s.instanceId === instanceId) || null;
2963
- if (!type) return cliStates[cliStates.length - 1] || null;
2964
- const matches = cliStates.filter(s => s.type === type);
2965
- return matches[matches.length - 1] || null;
2966
- }
2967
1592
 
2968
1593
  /** POST /api/cli/launch — launch a CLI agent { type, workingDir?, args? } */
2969
1594
  private async handleCliLaunch(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
2970
- if (!this.cliManager) {
2971
- this.json(res, 503, { error: 'CliManager not available' });
2972
- return;
2973
- }
2974
- const body = await this.readBody(req);
2975
- const { type, workingDir, args } = body;
2976
- if (!type) {
2977
- this.json(res, 400, { error: 'type required (e.g. claude-cli, gemini-cli)' });
2978
- return;
2979
- }
2980
- try {
2981
- await this.cliManager.startSession(type, workingDir || process.cwd(), args || []);
2982
- this.json(res, 200, { launched: true, type, workspace: workingDir || process.cwd() });
2983
- } catch (e: any) {
2984
- this.json(res, 500, { error: `Launch failed: ${e.message}` });
2985
- }
1595
+ return handleCliLaunch(this, req, res);
2986
1596
  }
2987
1597
 
2988
1598
  /** POST /api/cli/send — send message to a running CLI { type, text } */
2989
1599
  private async handleCliSend(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
2990
- if (!this.instanceManager) {
2991
- this.json(res, 503, { error: 'InstanceManager not available' });
2992
- return;
2993
- }
2994
- const body = await this.readBody(req);
2995
- const { type, text, instanceId } = body;
2996
- if (!text) {
2997
- this.json(res, 400, { error: 'text required' });
2998
- return;
2999
- }
3000
-
3001
- const target = this.findCliTarget(type, instanceId);
3002
- if (!target) {
3003
- this.json(res, 404, { error: `No running instance found for: ${type || instanceId}` });
3004
- return;
3005
- }
3006
-
3007
- try {
3008
- this.instanceManager.sendEvent(target.instanceId, 'send_message', { text });
3009
- this.json(res, 200, { sent: true, type: target.type, instanceId: target.instanceId });
3010
- } catch (e: any) {
3011
- this.json(res, 500, { error: `Send failed: ${e.message}` });
3012
- }
1600
+ return handleCliSend(this, req, res);
3013
1601
  }
3014
1602
 
3015
1603
  /** POST /api/cli/stop — stop a running CLI { type } */
3016
1604
  private async handleCliStop(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
3017
- if (!this.instanceManager) {
3018
- this.json(res, 503, { error: 'InstanceManager not available' });
3019
- return;
3020
- }
3021
- const body = await this.readBody(req);
3022
- const { type, instanceId } = body;
3023
-
3024
- const target = this.findCliTarget(type, instanceId);
3025
- if (!target) {
3026
- this.json(res, 404, { error: `No running instance found for: ${type || instanceId}` });
3027
- return;
3028
- }
3029
-
3030
- try {
3031
- this.instanceManager.removeInstance(target.instanceId);
3032
- this.json(res, 200, { stopped: true, type: target.type, instanceId: target.instanceId });
3033
- } catch (e: any) {
3034
- this.json(res, 500, { error: `Stop failed: ${e.message}` });
3035
- }
1605
+ return handleCliStop(this, req, res);
3036
1606
  }
3037
1607
 
3038
1608
  /** GET /api/cli/events — SSE stream of CLI status events */
3039
1609
  private handleCliSSE(_req: http.IncomingMessage, res: http.ServerResponse): void {
3040
- res.writeHead(200, {
3041
- 'Content-Type': 'text/event-stream',
3042
- 'Cache-Control': 'no-cache',
3043
- 'Connection': 'keep-alive',
3044
- 'Access-Control-Allow-Origin': '*',
3045
- });
3046
- res.write('data: {"type":"connected"}\n\n');
3047
- this.cliSSEClients.push(res);
3048
-
3049
- // Register event listener if first client + instanceManager available
3050
- if (this.cliSSEClients.length === 1 && this.instanceManager) {
3051
- this.instanceManager.onEvent((event) => {
3052
- this.sendCliSSE(event);
3053
- });
3054
- }
3055
-
3056
- // Send current state snapshot immediately
3057
- if (this.instanceManager) {
3058
- const allStates = this.instanceManager.collectAllStates();
3059
- const cliStates = allStates.filter(s => s.category === 'cli' || s.category === 'acp');
3060
- for (const s of cliStates) {
3061
- this.sendCliSSE({ event: 'snapshot', providerType: s.type, status: s.status, instanceId: s.instanceId });
3062
- }
3063
- }
3064
-
3065
- _req.on('close', () => {
3066
- this.cliSSEClients = this.cliSSEClients.filter(c => c !== res);
3067
- });
1610
+ handleCliSSE(this, this.cliSSEClients, _req, res);
3068
1611
  }
3069
1612
 
3070
- private sendCliSSE(data: any): void {
1613
+ public sendCliSSE(data: any): void {
3071
1614
  const msg = `data: ${JSON.stringify({ ...data, timestamp: Date.now() })}\n\n`;
3072
1615
  for (const client of this.cliSSEClients) {
3073
1616
  try { client.write(msg); } catch { /* ignore */ }
@@ -3076,136 +1619,16 @@ export class DevServer {
3076
1619
 
3077
1620
  /** GET /api/cli/debug/:type — full internal debug state of a CLI adapter */
3078
1621
  private async handleCliDebug(type: string, _req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
3079
- if (!this.instanceManager) {
3080
- this.json(res, 503, { error: 'InstanceManager not available' });
3081
- return;
3082
- }
3083
-
3084
- const target = this.findCliTarget(type);
3085
- if (!target) {
3086
- const allStates = this.instanceManager.collectAllStates();
3087
- this.json(res, 404, { error: `No running instance for: ${type}`, available: allStates.filter(s => s.category === 'cli' || s.category === 'acp').map(s => s.type) });
3088
- return;
3089
- }
3090
-
3091
- // Get the ProviderInstance and access adapter debug state
3092
- const instance = this.instanceManager.getInstance(target.instanceId) as any;
3093
- if (!instance) {
3094
- this.json(res, 404, { error: `Instance not found: ${target.instanceId}` });
3095
- return;
3096
- }
3097
-
3098
- try {
3099
- const adapter = instance.getAdapter?.() || instance.adapter;
3100
- if (adapter && typeof adapter.getDebugState === 'function') {
3101
- const debugState = adapter.getDebugState();
3102
- this.json(res, 200, {
3103
- instanceId: target.instanceId,
3104
- providerState: {
3105
- type: target.type,
3106
- name: target.name,
3107
- status: target.status,
3108
- mode: 'mode' in target ? target.mode : undefined,
3109
- },
3110
- debug: debugState,
3111
- });
3112
- } else {
3113
- // Fallback: return what we can from the state
3114
- this.json(res, 200, {
3115
- instanceId: target.instanceId,
3116
- providerState: target,
3117
- debug: null,
3118
- message: 'No debug state available (adapter.getDebugState not found)',
3119
- });
3120
- }
3121
- } catch (e: any) {
3122
- this.json(res, 500, { error: `Debug state failed: ${e.message}` });
3123
- }
1622
+ return handleCliDebug(this, type, _req, res);
3124
1623
  }
3125
1624
 
3126
1625
  /** POST /api/cli/resolve — resolve an approval modal { type, buttonIndex } */
3127
1626
  private async handleCliResolve(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
3128
- const body = await this.readBody(req);
3129
- const { type, buttonIndex, instanceId } = body;
3130
- if (buttonIndex === undefined || buttonIndex === null) {
3131
- this.json(res, 400, { error: 'buttonIndex required (0=Yes, 1=Always, 2=Deny)' });
3132
- return;
3133
- }
3134
-
3135
- if (!this.cliManager) {
3136
- this.json(res, 503, { error: 'CliManager not available' });
3137
- return;
3138
- }
3139
- if (!this.instanceManager) {
3140
- this.json(res, 503, { error: 'InstanceManager not available' });
3141
- return;
3142
- }
3143
-
3144
- const target = this.findCliTarget(type, instanceId);
3145
- if (!target) {
3146
- this.json(res, 404, { error: `No running adapter for: ${type || instanceId}` });
3147
- return;
3148
- }
3149
-
3150
- const instance = this.instanceManager.getInstance(target.instanceId) as any;
3151
- const adapter = instance?.getAdapter?.() || instance?.adapter;
3152
- if (!adapter) {
3153
- this.json(res, 404, { error: `Adapter not found for instance: ${target.instanceId}` });
3154
- return;
3155
- }
3156
-
3157
- try {
3158
- if (typeof adapter.resolveModal === 'function') {
3159
- adapter.resolveModal(buttonIndex);
3160
- this.json(res, 200, { resolved: true, type: target.type, instanceId: target.instanceId, buttonIndex });
3161
- } else {
3162
- this.json(res, 400, { error: 'resolveModal not available on this adapter' });
3163
- }
3164
- } catch (e: any) {
3165
- this.json(res, 500, { error: `Resolve failed: ${e.message}` });
3166
- }
1627
+ return handleCliResolve(this, req, res);
3167
1628
  }
3168
1629
 
3169
1630
  /** POST /api/cli/raw — send raw keystrokes to PTY { type, keys } */
3170
1631
  private async handleCliRaw(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
3171
- const body = await this.readBody(req);
3172
- const { type, keys, instanceId } = body;
3173
- if (!keys) {
3174
- this.json(res, 400, { error: 'keys required (raw string to send to PTY)' });
3175
- return;
3176
- }
3177
-
3178
- if (!this.cliManager) {
3179
- this.json(res, 503, { error: 'CliManager not available' });
3180
- return;
3181
- }
3182
- if (!this.instanceManager) {
3183
- this.json(res, 503, { error: 'InstanceManager not available' });
3184
- return;
3185
- }
3186
-
3187
- const target = this.findCliTarget(type, instanceId);
3188
- if (!target) {
3189
- this.json(res, 404, { error: `No running adapter for: ${type || instanceId}` });
3190
- return;
3191
- }
3192
-
3193
- const instance = this.instanceManager.getInstance(target.instanceId) as any;
3194
- const adapter = instance?.getAdapter?.() || instance?.adapter;
3195
- if (!adapter) {
3196
- this.json(res, 404, { error: `Adapter not found for instance: ${target.instanceId}` });
3197
- return;
3198
- }
3199
-
3200
- try {
3201
- if (typeof adapter.writeRaw === 'function') {
3202
- adapter.writeRaw(keys);
3203
- this.json(res, 200, { sent: true, type: target.type, instanceId: target.instanceId, keysLength: keys.length });
3204
- } else {
3205
- this.json(res, 400, { error: 'writeRaw not available on this adapter' });
3206
- }
3207
- } catch (e: any) {
3208
- this.json(res, 500, { error: `Raw send failed: ${e.message}` });
3209
- }
1632
+ return handleCliRaw(this, req, res);
3210
1633
  }
3211
1634
  }