@adhdev/daemon-core 0.7.5 → 0.7.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +164 -38
- package/dist/index.d.ts +164 -38
- package/dist/index.js +4051 -2547
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3696 -2192
- package/dist/index.mjs.map +1 -1
- package/dist/{normalize-tKg8IiDk.d.mts → normalize-auJAPmKy.d.mts} +669 -629
- package/dist/{normalize-tKg8IiDk.d.ts → normalize-auJAPmKy.d.ts} +669 -629
- package/dist/status/normalize.d.mts +1 -1
- package/dist/status/normalize.d.ts +1 -1
- package/package.json +5 -1
- package/src/agent-stream/forward.ts +6 -0
- package/src/boot/daemon-lifecycle.ts +7 -4
- package/src/cli-adapter-types.ts +2 -0
- package/src/cli-adapters/provider-cli-adapter.ts +148 -11
- package/src/cli-adapters/pty-transport.ts +100 -0
- package/src/cli-adapters/session-host-transport.ts +392 -0
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +126 -0
- package/src/cli-adapters/terminal-backends/types.ts +17 -0
- package/src/cli-adapters/terminal-backends/xterm-backend.ts +87 -0
- package/src/cli-adapters/terminal-screen.ts +40 -53
- package/src/commands/cli-manager.ts +184 -55
- package/src/config/config.d.ts +116 -0
- package/src/config/workspace-activity.d.ts +22 -0
- package/src/config/workspaces.d.ts +84 -0
- package/src/daemon/dev-auto-implement.ts +1087 -0
- package/src/daemon/dev-cdp-handlers.ts +1003 -0
- package/src/daemon/dev-cli-debug.ts +288 -0
- package/src/daemon/dev-server-types.ts +45 -0
- package/src/daemon/dev-server.ts +121 -1698
- package/src/index.ts +5 -1
- package/src/providers/cli-provider-instance.ts +13 -1
- package/src/providers/contracts.d.ts +408 -0
- package/src/providers/contracts.ts +9 -0
- package/src/providers/extension-provider-instance.ts +50 -10
- package/src/providers/provider-instance-manager.ts +48 -10
- package/src/providers/provider-instance.d.ts +142 -0
- package/src/providers/provider-instance.ts +23 -1
- package/src/shared-types.d.ts +157 -0
- package/src/shared-types.ts +14 -0
- package/src/status/builders.ts +6 -0
- package/src/status/normalize.d.ts +14 -0
- package/src/types.d.ts +127 -0
package/src/daemon/dev-server.ts
CHANGED
|
@@ -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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
// ──
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
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
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|