assistme 0.1.10 → 0.1.12
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.js +218 -87
- package/package.json +1 -1
- package/src/tools/browser.ts +216 -95
- package/src/tools/index.ts +55 -16
package/dist/index.js
CHANGED
|
@@ -446,8 +446,19 @@ var BrowserController = class {
|
|
|
446
446
|
}
|
|
447
447
|
this.currentTabId = targetTab.id;
|
|
448
448
|
return new Promise((resolve2, reject) => {
|
|
449
|
+
let settled = false;
|
|
449
450
|
this.ws = new WebSocket(targetTab.webSocketDebuggerUrl);
|
|
451
|
+
const connectTimeout = setTimeout(() => {
|
|
452
|
+
if (!settled) {
|
|
453
|
+
settled = true;
|
|
454
|
+
this.ws?.close();
|
|
455
|
+
reject(new Error("Connection timeout (5s)"));
|
|
456
|
+
}
|
|
457
|
+
}, 5e3);
|
|
450
458
|
this.ws.on("open", () => {
|
|
459
|
+
if (settled) return;
|
|
460
|
+
settled = true;
|
|
461
|
+
clearTimeout(connectTimeout);
|
|
451
462
|
this.connected = true;
|
|
452
463
|
this.send("Page.enable").catch(() => {
|
|
453
464
|
});
|
|
@@ -469,13 +480,20 @@ var BrowserController = class {
|
|
|
469
480
|
});
|
|
470
481
|
this.ws.on("error", (err) => {
|
|
471
482
|
this.connected = false;
|
|
472
|
-
|
|
483
|
+
if (!settled) {
|
|
484
|
+
settled = true;
|
|
485
|
+
clearTimeout(connectTimeout);
|
|
486
|
+
reject(new Error(`WebSocket error: ${err.message}`));
|
|
487
|
+
}
|
|
473
488
|
});
|
|
474
489
|
this.ws.on("close", () => {
|
|
475
490
|
this.connected = false;
|
|
476
491
|
this.ws = null;
|
|
492
|
+
for (const [id, cb] of this.callbacks) {
|
|
493
|
+
cb({ id, error: { code: -1, message: "WebSocket closed" } });
|
|
494
|
+
}
|
|
495
|
+
this.callbacks.clear();
|
|
477
496
|
});
|
|
478
|
-
setTimeout(() => reject(new Error("Connection timeout")), 5e3);
|
|
479
497
|
});
|
|
480
498
|
}
|
|
481
499
|
async disconnect() {
|
|
@@ -577,12 +595,12 @@ URL: ${info.url}`;
|
|
|
577
595
|
}
|
|
578
596
|
async readElement(selector) {
|
|
579
597
|
this.ensureConnected();
|
|
580
|
-
const
|
|
598
|
+
const selectorJS = JSON.stringify(selector);
|
|
581
599
|
const result = await this.send("Runtime.evaluate", {
|
|
582
600
|
expression: `
|
|
583
601
|
(function() {
|
|
584
|
-
const el = document.querySelector(
|
|
585
|
-
if (!el) return 'Element not found: ${
|
|
602
|
+
const el = document.querySelector(${selectorJS});
|
|
603
|
+
if (!el) return 'Element not found: ' + ${selectorJS};
|
|
586
604
|
return el.innerText || el.textContent || el.value || '(empty)';
|
|
587
605
|
})()
|
|
588
606
|
`,
|
|
@@ -614,12 +632,12 @@ URL: ${info.url}`;
|
|
|
614
632
|
// ── Interactions ────────────────────────────────────────────────
|
|
615
633
|
async click(selector) {
|
|
616
634
|
this.ensureConnected();
|
|
617
|
-
const
|
|
635
|
+
const selectorJS = JSON.stringify(selector);
|
|
618
636
|
const result = await this.send("Runtime.evaluate", {
|
|
619
637
|
expression: `
|
|
620
638
|
(function() {
|
|
621
|
-
const el = document.querySelector(
|
|
622
|
-
if (!el) return 'Element not found: ${
|
|
639
|
+
const el = document.querySelector(${selectorJS});
|
|
640
|
+
if (!el) return 'Element not found: ' + ${selectorJS};
|
|
623
641
|
|
|
624
642
|
// Scroll into view
|
|
625
643
|
el.scrollIntoView({ block: 'center', behavior: 'instant' });
|
|
@@ -636,16 +654,16 @@ URL: ${info.url}`;
|
|
|
636
654
|
}
|
|
637
655
|
async typeText(selector, text) {
|
|
638
656
|
this.ensureConnected();
|
|
639
|
-
const
|
|
640
|
-
const
|
|
657
|
+
const selectorJS = JSON.stringify(selector);
|
|
658
|
+
const textJS = JSON.stringify(text);
|
|
641
659
|
const result = await this.send("Runtime.evaluate", {
|
|
642
660
|
expression: `
|
|
643
661
|
(function() {
|
|
644
|
-
const el = document.querySelector(
|
|
645
|
-
if (!el) return 'Element not found: ${
|
|
662
|
+
const el = document.querySelector(${selectorJS});
|
|
663
|
+
if (!el) return 'Element not found: ' + ${selectorJS};
|
|
646
664
|
|
|
647
665
|
el.focus();
|
|
648
|
-
el.value =
|
|
666
|
+
el.value = ${textJS};
|
|
649
667
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
650
668
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
651
669
|
return 'Typed into: ' + (el.tagName || '') + ' [' + (el.name || el.id || '') + ']';
|
|
@@ -784,12 +802,32 @@ URL: ${info.url}`;
|
|
|
784
802
|
(function() {
|
|
785
803
|
const elements = [];
|
|
786
804
|
const selectors = 'a, button, input, select, textarea, [role="button"], [onclick]';
|
|
787
|
-
document.querySelectorAll(selectors)
|
|
788
|
-
|
|
805
|
+
const all = document.querySelectorAll(selectors);
|
|
806
|
+
for (let i = 0; i < all.length && elements.length < 50; i++) {
|
|
807
|
+
const el = all[i];
|
|
789
808
|
const rect = el.getBoundingClientRect();
|
|
790
|
-
if (rect.width === 0 || rect.height === 0)
|
|
809
|
+
if (rect.width === 0 || rect.height === 0) continue; // Skip hidden
|
|
810
|
+
|
|
811
|
+
// Build a reliable CSS selector
|
|
812
|
+
let selector;
|
|
813
|
+
if (el.id) {
|
|
814
|
+
selector = '#' + CSS.escape(el.id);
|
|
815
|
+
} else if (el.getAttribute('data-testid')) {
|
|
816
|
+
selector = '[data-testid="' + el.getAttribute('data-testid') + '"]';
|
|
817
|
+
} else {
|
|
818
|
+
// Build a path-based selector: find nth-of-type among siblings
|
|
819
|
+
const tag = el.tagName.toLowerCase();
|
|
820
|
+
const parent = el.parentElement;
|
|
821
|
+
if (parent) {
|
|
822
|
+
const siblings = parent.querySelectorAll(':scope > ' + tag);
|
|
823
|
+
const idx = Array.from(siblings).indexOf(el) + 1;
|
|
824
|
+
selector = tag + ':nth-of-type(' + idx + ')';
|
|
825
|
+
} else {
|
|
826
|
+
selector = tag;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
791
829
|
|
|
792
|
-
|
|
830
|
+
elements.push({
|
|
793
831
|
tag: el.tagName.toLowerCase(),
|
|
794
832
|
text: (el.textContent || '').trim().slice(0, 80),
|
|
795
833
|
type: el.getAttribute('type') || '',
|
|
@@ -797,12 +835,9 @@ URL: ${info.url}`;
|
|
|
797
835
|
id: el.id || '',
|
|
798
836
|
href: el.getAttribute('href') || '',
|
|
799
837
|
placeholder: el.getAttribute('placeholder') || '',
|
|
800
|
-
selector:
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
};
|
|
804
|
-
elements.push(info);
|
|
805
|
-
});
|
|
838
|
+
selector: selector,
|
|
839
|
+
});
|
|
840
|
+
}
|
|
806
841
|
return JSON.stringify(elements, null, 2);
|
|
807
842
|
})()
|
|
808
843
|
`,
|
|
@@ -820,12 +855,23 @@ function findChromePath() {
|
|
|
820
855
|
const paths = [
|
|
821
856
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
822
857
|
"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
|
|
823
|
-
"/Applications/Chromium.app/Contents/MacOS/Chromium"
|
|
858
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
859
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
860
|
+
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
|
|
824
861
|
];
|
|
825
862
|
return paths.find((p) => existsSync(p)) ?? null;
|
|
826
863
|
}
|
|
827
864
|
if (os === "linux") {
|
|
828
|
-
|
|
865
|
+
const names = [
|
|
866
|
+
"google-chrome",
|
|
867
|
+
"google-chrome-stable",
|
|
868
|
+
"chromium-browser",
|
|
869
|
+
"chromium",
|
|
870
|
+
"microsoft-edge",
|
|
871
|
+
"microsoft-edge-stable",
|
|
872
|
+
"brave-browser"
|
|
873
|
+
];
|
|
874
|
+
for (const name of names) {
|
|
829
875
|
try {
|
|
830
876
|
return execSync(`which ${name}`, {
|
|
831
877
|
encoding: "utf-8",
|
|
@@ -842,31 +888,53 @@ function findChromePath() {
|
|
|
842
888
|
process.env["PROGRAMFILES(X86)"],
|
|
843
889
|
process.env.LOCALAPPDATA
|
|
844
890
|
].filter(Boolean);
|
|
891
|
+
const subPaths = [
|
|
892
|
+
"Google\\Chrome\\Application\\chrome.exe",
|
|
893
|
+
"Microsoft\\Edge\\Application\\msedge.exe",
|
|
894
|
+
"BraveSoftware\\Brave-Browser\\Application\\brave.exe"
|
|
895
|
+
];
|
|
845
896
|
for (const prefix of prefixes) {
|
|
846
|
-
const
|
|
847
|
-
|
|
897
|
+
for (const sub of subPaths) {
|
|
898
|
+
const p = `${prefix}\\${sub}`;
|
|
899
|
+
if (existsSync(p)) return p;
|
|
900
|
+
}
|
|
848
901
|
}
|
|
849
902
|
return null;
|
|
850
903
|
}
|
|
851
904
|
return null;
|
|
852
905
|
}
|
|
853
|
-
function isChromeRunning() {
|
|
906
|
+
function isChromeRunning(chromePath) {
|
|
854
907
|
try {
|
|
855
908
|
if (platform() === "win32") {
|
|
856
|
-
const out2 = execSync(
|
|
857
|
-
|
|
858
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
859
|
-
|
|
860
|
-
return
|
|
909
|
+
const out2 = execSync(
|
|
910
|
+
'tasklist /FI "IMAGENAME eq chrome.exe" /FI "IMAGENAME eq msedge.exe" /FI "IMAGENAME eq brave.exe" /NH',
|
|
911
|
+
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
912
|
+
);
|
|
913
|
+
return /chrome\.exe|msedge\.exe|brave\.exe/i.test(out2);
|
|
861
914
|
}
|
|
862
915
|
if (platform() === "darwin") {
|
|
863
|
-
|
|
916
|
+
if (chromePath) {
|
|
917
|
+
const appDir = chromePath.replace(/\/Contents\/MacOS\/.*$/, "");
|
|
918
|
+
const out3 = execSync(`pgrep -f ${JSON.stringify(appDir)}`, {
|
|
919
|
+
encoding: "utf-8",
|
|
920
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
921
|
+
});
|
|
922
|
+
return out3.trim().length > 0;
|
|
923
|
+
}
|
|
924
|
+
const out2 = execSync(
|
|
925
|
+
'pgrep -f "(Google Chrome|Microsoft Edge|Brave Browser|Chromium).app/Contents/MacOS/"',
|
|
926
|
+
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
927
|
+
);
|
|
928
|
+
return out2.trim().length > 0;
|
|
929
|
+
}
|
|
930
|
+
if (chromePath) {
|
|
931
|
+
const out2 = execSync(`pgrep -f ${JSON.stringify(chromePath)} 2>/dev/null || true`, {
|
|
864
932
|
encoding: "utf-8",
|
|
865
933
|
stdio: ["pipe", "pipe", "pipe"]
|
|
866
934
|
});
|
|
867
935
|
return out2.trim().length > 0;
|
|
868
936
|
}
|
|
869
|
-
const out = execSync("pgrep -f '(chrome|chromium)' 2>/dev/null || true", {
|
|
937
|
+
const out = execSync("pgrep -f '(chrome|chromium|msedge|brave)' 2>/dev/null || true", {
|
|
870
938
|
encoding: "utf-8",
|
|
871
939
|
stdio: ["pipe", "pipe", "pipe"]
|
|
872
940
|
});
|
|
@@ -875,21 +943,30 @@ function isChromeRunning() {
|
|
|
875
943
|
return false;
|
|
876
944
|
}
|
|
877
945
|
}
|
|
878
|
-
|
|
946
|
+
function macAppName(chromePath) {
|
|
947
|
+
if (chromePath.includes("Brave Browser")) return "Brave Browser";
|
|
948
|
+
if (chromePath.includes("Microsoft Edge")) return "Microsoft Edge";
|
|
949
|
+
if (chromePath.includes("Chromium")) return "Chromium";
|
|
950
|
+
if (chromePath.includes("Canary")) return "Google Chrome Canary";
|
|
951
|
+
return "Google Chrome";
|
|
952
|
+
}
|
|
953
|
+
async function killChromeGracefully(chromePath) {
|
|
879
954
|
const os = platform();
|
|
880
955
|
try {
|
|
881
956
|
if (os === "darwin") {
|
|
882
|
-
|
|
957
|
+
const app = macAppName(chromePath);
|
|
958
|
+
execSync(`osascript -e 'quit app "${app}"'`, {
|
|
883
959
|
timeout: 5e3,
|
|
884
960
|
stdio: ["pipe", "pipe", "pipe"]
|
|
885
961
|
});
|
|
886
962
|
} else if (os === "linux") {
|
|
887
|
-
execSync(
|
|
963
|
+
execSync(`pkill -TERM -f ${JSON.stringify(chromePath)}`, {
|
|
888
964
|
timeout: 5e3,
|
|
889
965
|
stdio: ["pipe", "pipe", "pipe"]
|
|
890
966
|
});
|
|
891
967
|
} else if (os === "win32") {
|
|
892
|
-
|
|
968
|
+
const exe = chromePath.split("\\").pop() || "chrome.exe";
|
|
969
|
+
execSync(`taskkill /IM "${exe}"`, {
|
|
893
970
|
timeout: 5e3,
|
|
894
971
|
stdio: ["pipe", "pipe", "pipe"]
|
|
895
972
|
});
|
|
@@ -898,20 +975,21 @@ async function killChromeGracefully() {
|
|
|
898
975
|
}
|
|
899
976
|
const start = Date.now();
|
|
900
977
|
while (Date.now() - start < 8e3) {
|
|
901
|
-
if (!isChromeRunning()) {
|
|
902
|
-
log.debug(`
|
|
978
|
+
if (!isChromeRunning(chromePath)) {
|
|
979
|
+
log.debug(`Browser exited after ${Date.now() - start}ms`);
|
|
903
980
|
return;
|
|
904
981
|
}
|
|
905
982
|
await new Promise((r) => setTimeout(r, 500));
|
|
906
983
|
}
|
|
907
|
-
log.debug("
|
|
984
|
+
log.debug("Browser still running after graceful quit, force-killing...");
|
|
908
985
|
try {
|
|
909
986
|
if (os === "win32") {
|
|
910
|
-
|
|
987
|
+
const exe = chromePath.split("\\").pop() || "chrome.exe";
|
|
988
|
+
execSync(`taskkill /F /IM "${exe}"`, {
|
|
911
989
|
stdio: ["pipe", "pipe", "pipe"]
|
|
912
990
|
});
|
|
913
991
|
} else {
|
|
914
|
-
execSync(
|
|
992
|
+
execSync(`pkill -9 -f ${JSON.stringify(chromePath)}`, {
|
|
915
993
|
stdio: ["pipe", "pipe", "pipe"]
|
|
916
994
|
});
|
|
917
995
|
}
|
|
@@ -921,25 +999,27 @@ async function killChromeGracefully() {
|
|
|
921
999
|
if (os !== "win32") {
|
|
922
1000
|
const home = process.env.HOME;
|
|
923
1001
|
if (home) {
|
|
924
|
-
const
|
|
925
|
-
|
|
926
|
-
`${home}/Library/Application Support/Google/Chrome
|
|
927
|
-
`${home}/Library/Application Support/
|
|
1002
|
+
const lockSuffixes = ["SingletonLock", "SingletonSocket", "SingletonCookie"];
|
|
1003
|
+
const profileDirs = os === "darwin" ? [
|
|
1004
|
+
`${home}/Library/Application Support/Google/Chrome`,
|
|
1005
|
+
`${home}/Library/Application Support/Microsoft Edge`,
|
|
1006
|
+
`${home}/Library/Application Support/BraveSoftware/Brave-Browser`
|
|
928
1007
|
] : [
|
|
929
|
-
`${home}/.config/google-chrome
|
|
930
|
-
`${home}/.config/
|
|
931
|
-
`${home}/.config/
|
|
932
|
-
`${home}/.config/
|
|
933
|
-
`${home}/.config/chromium/SingletonSocket`,
|
|
934
|
-
`${home}/.config/chromium/SingletonCookie`
|
|
1008
|
+
`${home}/.config/google-chrome`,
|
|
1009
|
+
`${home}/.config/chromium`,
|
|
1010
|
+
`${home}/.config/microsoft-edge`,
|
|
1011
|
+
`${home}/.config/BraveSoftware/Brave-Browser`
|
|
935
1012
|
];
|
|
936
|
-
for (const
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1013
|
+
for (const dir of profileDirs) {
|
|
1014
|
+
for (const suffix of lockSuffixes) {
|
|
1015
|
+
const lockPath = `${dir}/${suffix}`;
|
|
1016
|
+
try {
|
|
1017
|
+
if (existsSync(lockPath)) {
|
|
1018
|
+
unlinkSync(lockPath);
|
|
1019
|
+
log.debug(`Removed stale lock: ${lockPath}`);
|
|
1020
|
+
}
|
|
1021
|
+
} catch {
|
|
941
1022
|
}
|
|
942
|
-
} catch {
|
|
943
1023
|
}
|
|
944
1024
|
}
|
|
945
1025
|
}
|
|
@@ -947,11 +1027,24 @@ async function killChromeGracefully() {
|
|
|
947
1027
|
}
|
|
948
1028
|
function spawnChrome(chromePath, port) {
|
|
949
1029
|
const cdpFlag = `--remote-debugging-port=${port}`;
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1030
|
+
const os = platform();
|
|
1031
|
+
let child;
|
|
1032
|
+
if (os === "darwin") {
|
|
1033
|
+
const appName = macAppName(chromePath);
|
|
1034
|
+
log.debug(
|
|
1035
|
+
`Spawning browser via: open -a "${appName}" --args ${cdpFlag} --restore-last-session`
|
|
1036
|
+
);
|
|
1037
|
+
child = spawn("open", ["-a", appName, "--args", cdpFlag, "--restore-last-session"], {
|
|
1038
|
+
detached: true,
|
|
1039
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1040
|
+
});
|
|
1041
|
+
} else {
|
|
1042
|
+
log.debug(`Spawning Chrome: ${chromePath} ${cdpFlag} --restore-last-session`);
|
|
1043
|
+
child = spawn(chromePath, [cdpFlag, "--restore-last-session"], {
|
|
1044
|
+
detached: true,
|
|
1045
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
955
1048
|
let stderr = "";
|
|
956
1049
|
child.stderr?.on("data", (chunk) => {
|
|
957
1050
|
stderr += chunk.toString();
|
|
@@ -1018,11 +1111,20 @@ async function ensureBrowserAvailable(port = 9222) {
|
|
|
1018
1111
|
return { success: false, action: "chrome_not_found" };
|
|
1019
1112
|
}
|
|
1020
1113
|
log.debug(`Found Chrome at: ${chromePath}`);
|
|
1021
|
-
const running = isChromeRunning();
|
|
1022
|
-
log.debug(`
|
|
1114
|
+
const running = isChromeRunning(chromePath);
|
|
1115
|
+
log.debug(`Browser currently running: ${running}`);
|
|
1023
1116
|
if (running) {
|
|
1024
|
-
log.debug("Killing
|
|
1025
|
-
await killChromeGracefully();
|
|
1117
|
+
log.debug("Killing browser gracefully for restart with CDP...");
|
|
1118
|
+
await killChromeGracefully(chromePath);
|
|
1119
|
+
if (isChromeRunning(chromePath)) {
|
|
1120
|
+
log.debug("Browser still running after kill attempt \u2014 cannot restart with CDP");
|
|
1121
|
+
return {
|
|
1122
|
+
success: false,
|
|
1123
|
+
action: "launch_failed",
|
|
1124
|
+
chromePath,
|
|
1125
|
+
detail: "Could not stop the existing browser process. Please quit the browser manually and run assistme again."
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1026
1128
|
await new Promise((r) => setTimeout(r, 2e3));
|
|
1027
1129
|
const child2 = spawnChrome(chromePath, port);
|
|
1028
1130
|
if (await waitForCDP(browser)) {
|
|
@@ -1030,25 +1132,25 @@ async function ensureBrowserAvailable(port = 9222) {
|
|
|
1030
1132
|
}
|
|
1031
1133
|
if (child2.exitCode !== null) {
|
|
1032
1134
|
log.debug(
|
|
1033
|
-
`
|
|
1135
|
+
`Browser process already exited (code ${child2.exitCode}) \u2014 may have crashed or profile is locked`
|
|
1034
1136
|
);
|
|
1035
1137
|
return {
|
|
1036
1138
|
success: false,
|
|
1037
1139
|
action: "launch_failed",
|
|
1038
1140
|
chromePath,
|
|
1039
|
-
detail: `
|
|
1141
|
+
detail: `Browser exited immediately (code ${child2.exitCode}). The profile may be locked. Try closing all browser windows first, then run assistme again.`
|
|
1040
1142
|
};
|
|
1041
1143
|
}
|
|
1042
1144
|
log.debug("First CDP wait timed out after restart, retrying...");
|
|
1043
1145
|
if (await waitForCDP(browser, 15e3)) {
|
|
1044
1146
|
return { success: true, action: "restarted", chromePath };
|
|
1045
1147
|
}
|
|
1046
|
-
const stillRunning2 = isChromeRunning();
|
|
1148
|
+
const stillRunning2 = isChromeRunning(chromePath);
|
|
1047
1149
|
return {
|
|
1048
1150
|
success: false,
|
|
1049
1151
|
action: "launch_failed",
|
|
1050
1152
|
chromePath,
|
|
1051
|
-
detail: stillRunning2 ? "
|
|
1153
|
+
detail: stillRunning2 ? "Browser is running but CDP port is not responding. Try: 1) Quit the browser completely, 2) Run assistme again." : "Browser was restarted but exited unexpectedly. Try launching it manually to check for errors."
|
|
1052
1154
|
};
|
|
1053
1155
|
}
|
|
1054
1156
|
const child = spawnChrome(chromePath, port);
|
|
@@ -1056,19 +1158,19 @@ async function ensureBrowserAvailable(port = 9222) {
|
|
|
1056
1158
|
return { success: true, action: "launched", chromePath };
|
|
1057
1159
|
}
|
|
1058
1160
|
if (child.exitCode !== null) {
|
|
1059
|
-
log.debug(`
|
|
1161
|
+
log.debug(`Browser process already exited (code ${child.exitCode})`);
|
|
1060
1162
|
return {
|
|
1061
1163
|
success: false,
|
|
1062
1164
|
action: "launch_failed",
|
|
1063
1165
|
chromePath,
|
|
1064
|
-
detail: `
|
|
1166
|
+
detail: `Browser exited immediately (code ${child.exitCode}). Try launching it manually to see any error dialogs.`
|
|
1065
1167
|
};
|
|
1066
1168
|
}
|
|
1067
1169
|
log.debug("First CDP wait timed out after launch, retrying...");
|
|
1068
1170
|
if (await waitForCDP(browser, 15e3)) {
|
|
1069
1171
|
return { success: true, action: "launched", chromePath };
|
|
1070
1172
|
}
|
|
1071
|
-
const stillRunning = isChromeRunning();
|
|
1173
|
+
const stillRunning = isChromeRunning(chromePath);
|
|
1072
1174
|
return {
|
|
1073
1175
|
success: false,
|
|
1074
1176
|
action: "launch_failed",
|
|
@@ -2260,6 +2362,18 @@ ${stderr}` : "";
|
|
|
2260
2362
|
}
|
|
2261
2363
|
|
|
2262
2364
|
// src/tools/index.ts
|
|
2365
|
+
async function ensureConnected(browser, tabIndex) {
|
|
2366
|
+
if (browser.isConnected() && tabIndex === void 0) return;
|
|
2367
|
+
if (!await browser.isAvailable()) {
|
|
2368
|
+
const result = await ensureBrowserAvailable();
|
|
2369
|
+
if (!result.success) {
|
|
2370
|
+
throw new Error(
|
|
2371
|
+
`Chrome is not available (${result.action}). ${result.detail || "Please ensure Google Chrome is installed."}`
|
|
2372
|
+
);
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
await browser.connect(tabIndex);
|
|
2376
|
+
}
|
|
2263
2377
|
async function executeTool(name, input) {
|
|
2264
2378
|
const browser = getBrowser();
|
|
2265
2379
|
switch (name) {
|
|
@@ -2286,34 +2400,35 @@ async function executeTool(name, input) {
|
|
|
2286
2400
|
return executeShell(input.command, input.cwd);
|
|
2287
2401
|
// ── Browser (CDP) ───────────────────────────────────────
|
|
2288
2402
|
case "browser_connect": {
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
if (!result.success) {
|
|
2292
|
-
throw new Error(
|
|
2293
|
-
`Failed to auto-launch Chrome (${result.action}). Please ensure Google Chrome is installed.`
|
|
2294
|
-
);
|
|
2295
|
-
}
|
|
2296
|
-
}
|
|
2297
|
-
return browser.connect(input.tab_index);
|
|
2403
|
+
await ensureConnected(browser, input.tab_index);
|
|
2404
|
+
return browser.isConnected() ? "Connected to browser." : "Failed to connect.";
|
|
2298
2405
|
}
|
|
2299
2406
|
case "browser_navigate":
|
|
2300
|
-
|
|
2407
|
+
await ensureConnected(browser);
|
|
2301
2408
|
return browser.navigate(input.url);
|
|
2302
2409
|
case "browser_read_page":
|
|
2410
|
+
await ensureConnected(browser);
|
|
2303
2411
|
return browser.readPage();
|
|
2304
2412
|
case "browser_screenshot":
|
|
2413
|
+
await ensureConnected(browser);
|
|
2305
2414
|
return browser.screenshot();
|
|
2306
2415
|
case "browser_click":
|
|
2416
|
+
await ensureConnected(browser);
|
|
2307
2417
|
return browser.click(input.selector);
|
|
2308
2418
|
case "browser_type":
|
|
2419
|
+
await ensureConnected(browser);
|
|
2309
2420
|
return browser.typeText(input.selector, input.text);
|
|
2310
2421
|
case "browser_press_key":
|
|
2422
|
+
await ensureConnected(browser);
|
|
2311
2423
|
return browser.pressKey(input.key);
|
|
2312
2424
|
case "browser_scroll":
|
|
2425
|
+
await ensureConnected(browser);
|
|
2313
2426
|
return input.direction === "up" ? browser.scrollUp() : browser.scrollDown();
|
|
2314
2427
|
case "browser_get_elements":
|
|
2428
|
+
await ensureConnected(browser);
|
|
2315
2429
|
return browser.getInteractiveElements();
|
|
2316
2430
|
case "browser_evaluate":
|
|
2431
|
+
await ensureConnected(browser);
|
|
2317
2432
|
return browser.evaluate(input.expression);
|
|
2318
2433
|
case "browser_list_tabs":
|
|
2319
2434
|
return browser.listTabs();
|
|
@@ -2332,7 +2447,23 @@ async function executeTool(name, input) {
|
|
|
2332
2447
|
console.log(` (Waiting up to ${waitSeconds}s for you to complete this)`);
|
|
2333
2448
|
console.log("\u2501".repeat(60));
|
|
2334
2449
|
console.log("\n");
|
|
2335
|
-
|
|
2450
|
+
let initialUrl = "";
|
|
2451
|
+
try {
|
|
2452
|
+
const info = await browser.getPageInfo();
|
|
2453
|
+
initialUrl = info.url;
|
|
2454
|
+
} catch {
|
|
2455
|
+
}
|
|
2456
|
+
const deadline = Date.now() + waitSeconds * 1e3;
|
|
2457
|
+
while (Date.now() < deadline) {
|
|
2458
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
2459
|
+
try {
|
|
2460
|
+
const info = await browser.getPageInfo();
|
|
2461
|
+
if (initialUrl && info.url !== initialUrl) {
|
|
2462
|
+
break;
|
|
2463
|
+
}
|
|
2464
|
+
} catch {
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2336
2467
|
try {
|
|
2337
2468
|
const pageInfo = await browser.readPage();
|
|
2338
2469
|
return `User action wait completed. Current page state:
|
package/package.json
CHANGED
package/src/tools/browser.ts
CHANGED
|
@@ -110,9 +110,21 @@ export class BrowserController {
|
|
|
110
110
|
this.currentTabId = targetTab.id;
|
|
111
111
|
|
|
112
112
|
return new Promise((resolve, reject) => {
|
|
113
|
+
let settled = false;
|
|
113
114
|
this.ws = new WebSocket(targetTab.webSocketDebuggerUrl!);
|
|
114
115
|
|
|
116
|
+
const connectTimeout = setTimeout(() => {
|
|
117
|
+
if (!settled) {
|
|
118
|
+
settled = true;
|
|
119
|
+
this.ws?.close();
|
|
120
|
+
reject(new Error("Connection timeout (5s)"));
|
|
121
|
+
}
|
|
122
|
+
}, 5000);
|
|
123
|
+
|
|
115
124
|
this.ws.on("open", () => {
|
|
125
|
+
if (settled) return;
|
|
126
|
+
settled = true;
|
|
127
|
+
clearTimeout(connectTimeout);
|
|
116
128
|
this.connected = true;
|
|
117
129
|
// Enable required domains
|
|
118
130
|
this.send("Page.enable").catch(() => {});
|
|
@@ -135,15 +147,22 @@ export class BrowserController {
|
|
|
135
147
|
|
|
136
148
|
this.ws.on("error", (err) => {
|
|
137
149
|
this.connected = false;
|
|
138
|
-
|
|
150
|
+
if (!settled) {
|
|
151
|
+
settled = true;
|
|
152
|
+
clearTimeout(connectTimeout);
|
|
153
|
+
reject(new Error(`WebSocket error: ${err.message}`));
|
|
154
|
+
}
|
|
139
155
|
});
|
|
140
156
|
|
|
141
157
|
this.ws.on("close", () => {
|
|
142
158
|
this.connected = false;
|
|
143
159
|
this.ws = null;
|
|
160
|
+
// Reject pending CDP commands so they don't hang forever
|
|
161
|
+
for (const [id, cb] of this.callbacks) {
|
|
162
|
+
cb({ id, error: { code: -1, message: "WebSocket closed" } });
|
|
163
|
+
}
|
|
164
|
+
this.callbacks.clear();
|
|
144
165
|
});
|
|
145
|
-
|
|
146
|
-
setTimeout(() => reject(new Error("Connection timeout")), 5000);
|
|
147
166
|
});
|
|
148
167
|
}
|
|
149
168
|
|
|
@@ -261,12 +280,12 @@ export class BrowserController {
|
|
|
261
280
|
|
|
262
281
|
async readElement(selector: string): Promise<string> {
|
|
263
282
|
this.ensureConnected();
|
|
264
|
-
const
|
|
283
|
+
const selectorJS = JSON.stringify(selector);
|
|
265
284
|
const result = await this.send("Runtime.evaluate", {
|
|
266
285
|
expression: `
|
|
267
286
|
(function() {
|
|
268
|
-
const el = document.querySelector(
|
|
269
|
-
if (!el) return 'Element not found: ${
|
|
287
|
+
const el = document.querySelector(${selectorJS});
|
|
288
|
+
if (!el) return 'Element not found: ' + ${selectorJS};
|
|
270
289
|
return el.innerText || el.textContent || el.value || '(empty)';
|
|
271
290
|
})()
|
|
272
291
|
`,
|
|
@@ -305,13 +324,13 @@ export class BrowserController {
|
|
|
305
324
|
|
|
306
325
|
async click(selector: string): Promise<string> {
|
|
307
326
|
this.ensureConnected();
|
|
308
|
-
const
|
|
327
|
+
const selectorJS = JSON.stringify(selector);
|
|
309
328
|
|
|
310
329
|
const result = await this.send("Runtime.evaluate", {
|
|
311
330
|
expression: `
|
|
312
331
|
(function() {
|
|
313
|
-
const el = document.querySelector(
|
|
314
|
-
if (!el) return 'Element not found: ${
|
|
332
|
+
const el = document.querySelector(${selectorJS});
|
|
333
|
+
if (!el) return 'Element not found: ' + ${selectorJS};
|
|
315
334
|
|
|
316
335
|
// Scroll into view
|
|
317
336
|
el.scrollIntoView({ block: 'center', behavior: 'instant' });
|
|
@@ -331,17 +350,19 @@ export class BrowserController {
|
|
|
331
350
|
|
|
332
351
|
async typeText(selector: string, text: string): Promise<string> {
|
|
333
352
|
this.ensureConnected();
|
|
334
|
-
|
|
335
|
-
|
|
353
|
+
// Use JSON.stringify for safe string interpolation into JS — handles all
|
|
354
|
+
// special characters (quotes, backslashes, newlines, unicode) correctly.
|
|
355
|
+
const selectorJS = JSON.stringify(selector);
|
|
356
|
+
const textJS = JSON.stringify(text);
|
|
336
357
|
|
|
337
358
|
const result = await this.send("Runtime.evaluate", {
|
|
338
359
|
expression: `
|
|
339
360
|
(function() {
|
|
340
|
-
const el = document.querySelector(
|
|
341
|
-
if (!el) return 'Element not found: ${
|
|
361
|
+
const el = document.querySelector(${selectorJS});
|
|
362
|
+
if (!el) return 'Element not found: ' + ${selectorJS};
|
|
342
363
|
|
|
343
364
|
el.focus();
|
|
344
|
-
el.value =
|
|
365
|
+
el.value = ${textJS};
|
|
345
366
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
346
367
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
347
368
|
return 'Typed into: ' + (el.tagName || '') + ' [' + (el.name || el.id || '') + ']';
|
|
@@ -513,12 +534,32 @@ export class BrowserController {
|
|
|
513
534
|
(function() {
|
|
514
535
|
const elements = [];
|
|
515
536
|
const selectors = 'a, button, input, select, textarea, [role="button"], [onclick]';
|
|
516
|
-
document.querySelectorAll(selectors)
|
|
517
|
-
|
|
537
|
+
const all = document.querySelectorAll(selectors);
|
|
538
|
+
for (let i = 0; i < all.length && elements.length < 50; i++) {
|
|
539
|
+
const el = all[i];
|
|
518
540
|
const rect = el.getBoundingClientRect();
|
|
519
|
-
if (rect.width === 0 || rect.height === 0)
|
|
520
|
-
|
|
521
|
-
|
|
541
|
+
if (rect.width === 0 || rect.height === 0) continue; // Skip hidden
|
|
542
|
+
|
|
543
|
+
// Build a reliable CSS selector
|
|
544
|
+
let selector;
|
|
545
|
+
if (el.id) {
|
|
546
|
+
selector = '#' + CSS.escape(el.id);
|
|
547
|
+
} else if (el.getAttribute('data-testid')) {
|
|
548
|
+
selector = '[data-testid="' + el.getAttribute('data-testid') + '"]';
|
|
549
|
+
} else {
|
|
550
|
+
// Build a path-based selector: find nth-of-type among siblings
|
|
551
|
+
const tag = el.tagName.toLowerCase();
|
|
552
|
+
const parent = el.parentElement;
|
|
553
|
+
if (parent) {
|
|
554
|
+
const siblings = parent.querySelectorAll(':scope > ' + tag);
|
|
555
|
+
const idx = Array.from(siblings).indexOf(el) + 1;
|
|
556
|
+
selector = tag + ':nth-of-type(' + idx + ')';
|
|
557
|
+
} else {
|
|
558
|
+
selector = tag;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
elements.push({
|
|
522
563
|
tag: el.tagName.toLowerCase(),
|
|
523
564
|
text: (el.textContent || '').trim().slice(0, 80),
|
|
524
565
|
type: el.getAttribute('type') || '',
|
|
@@ -526,12 +567,9 @@ export class BrowserController {
|
|
|
526
567
|
id: el.id || '',
|
|
527
568
|
href: el.getAttribute('href') || '',
|
|
528
569
|
placeholder: el.getAttribute('placeholder') || '',
|
|
529
|
-
selector:
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
};
|
|
533
|
-
elements.push(info);
|
|
534
|
-
});
|
|
570
|
+
selector: selector,
|
|
571
|
+
});
|
|
572
|
+
}
|
|
535
573
|
return JSON.stringify(elements, null, 2);
|
|
536
574
|
})()
|
|
537
575
|
`,
|
|
@@ -559,12 +597,23 @@ export function findChromePath(): string | null {
|
|
|
559
597
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
560
598
|
"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
|
|
561
599
|
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
600
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
601
|
+
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
|
|
562
602
|
];
|
|
563
603
|
return paths.find((p) => existsSync(p)) ?? null;
|
|
564
604
|
}
|
|
565
605
|
|
|
566
606
|
if (os === "linux") {
|
|
567
|
-
|
|
607
|
+
const names = [
|
|
608
|
+
"google-chrome",
|
|
609
|
+
"google-chrome-stable",
|
|
610
|
+
"chromium-browser",
|
|
611
|
+
"chromium",
|
|
612
|
+
"microsoft-edge",
|
|
613
|
+
"microsoft-edge-stable",
|
|
614
|
+
"brave-browser",
|
|
615
|
+
];
|
|
616
|
+
for (const name of names) {
|
|
568
617
|
try {
|
|
569
618
|
return execSync(`which ${name}`, {
|
|
570
619
|
encoding: "utf-8",
|
|
@@ -584,9 +633,17 @@ export function findChromePath(): string | null {
|
|
|
584
633
|
process.env.LOCALAPPDATA,
|
|
585
634
|
].filter(Boolean) as string[];
|
|
586
635
|
|
|
636
|
+
const subPaths = [
|
|
637
|
+
"Google\\Chrome\\Application\\chrome.exe",
|
|
638
|
+
"Microsoft\\Edge\\Application\\msedge.exe",
|
|
639
|
+
"BraveSoftware\\Brave-Browser\\Application\\brave.exe",
|
|
640
|
+
];
|
|
641
|
+
|
|
587
642
|
for (const prefix of prefixes) {
|
|
588
|
-
const
|
|
589
|
-
|
|
643
|
+
for (const sub of subPaths) {
|
|
644
|
+
const p = `${prefix}\\${sub}`;
|
|
645
|
+
if (existsSync(p)) return p;
|
|
646
|
+
}
|
|
590
647
|
}
|
|
591
648
|
return null;
|
|
592
649
|
}
|
|
@@ -595,28 +652,45 @@ export function findChromePath(): string | null {
|
|
|
595
652
|
}
|
|
596
653
|
|
|
597
654
|
/**
|
|
598
|
-
* Check if
|
|
655
|
+
* Check if a Chromium-based browser is currently running.
|
|
656
|
+
* Optionally pass the specific browser binary path for precise matching.
|
|
599
657
|
*/
|
|
600
|
-
export function isChromeRunning(): boolean {
|
|
658
|
+
export function isChromeRunning(chromePath?: string): boolean {
|
|
601
659
|
try {
|
|
602
660
|
if (platform() === "win32") {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
661
|
+
// Check for any Chromium-based browser process
|
|
662
|
+
const out = execSync(
|
|
663
|
+
'tasklist /FI "IMAGENAME eq chrome.exe" /FI "IMAGENAME eq msedge.exe" /FI "IMAGENAME eq brave.exe" /NH',
|
|
664
|
+
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
665
|
+
);
|
|
666
|
+
return /chrome\.exe|msedge\.exe|brave\.exe/i.test(out);
|
|
608
667
|
}
|
|
609
668
|
if (platform() === "darwin") {
|
|
610
|
-
//
|
|
611
|
-
|
|
612
|
-
|
|
669
|
+
// If we know the exact binary, match it precisely
|
|
670
|
+
if (chromePath) {
|
|
671
|
+
const appDir = chromePath.replace(/\/Contents\/MacOS\/.*$/, "");
|
|
672
|
+
const out = execSync(`pgrep -f ${JSON.stringify(appDir)}`, {
|
|
673
|
+
encoding: "utf-8",
|
|
674
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
675
|
+
});
|
|
676
|
+
return out.trim().length > 0;
|
|
677
|
+
}
|
|
678
|
+
// Otherwise check all known Chromium browsers
|
|
679
|
+
const out = execSync(
|
|
680
|
+
'pgrep -f "(Google Chrome|Microsoft Edge|Brave Browser|Chromium).app/Contents/MacOS/"',
|
|
681
|
+
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
682
|
+
);
|
|
683
|
+
return out.trim().length > 0;
|
|
684
|
+
}
|
|
685
|
+
// Linux
|
|
686
|
+
if (chromePath) {
|
|
687
|
+
const out = execSync(`pgrep -f ${JSON.stringify(chromePath)} 2>/dev/null || true`, {
|
|
613
688
|
encoding: "utf-8",
|
|
614
689
|
stdio: ["pipe", "pipe", "pipe"],
|
|
615
690
|
});
|
|
616
691
|
return out.trim().length > 0;
|
|
617
692
|
}
|
|
618
|
-
|
|
619
|
-
const out = execSync("pgrep -f '(chrome|chromium)' 2>/dev/null || true", {
|
|
693
|
+
const out = execSync("pgrep -f '(chrome|chromium|msedge|brave)' 2>/dev/null || true", {
|
|
620
694
|
encoding: "utf-8",
|
|
621
695
|
stdio: ["pipe", "pipe", "pipe"],
|
|
622
696
|
});
|
|
@@ -627,24 +701,37 @@ export function isChromeRunning(): boolean {
|
|
|
627
701
|
}
|
|
628
702
|
|
|
629
703
|
/**
|
|
630
|
-
*
|
|
704
|
+
* Derive the macOS app name from a binary path inside a .app bundle.
|
|
631
705
|
*/
|
|
632
|
-
|
|
706
|
+
function macAppName(chromePath: string): string {
|
|
707
|
+
if (chromePath.includes("Brave Browser")) return "Brave Browser";
|
|
708
|
+
if (chromePath.includes("Microsoft Edge")) return "Microsoft Edge";
|
|
709
|
+
if (chromePath.includes("Chromium")) return "Chromium";
|
|
710
|
+
if (chromePath.includes("Canary")) return "Google Chrome Canary";
|
|
711
|
+
return "Google Chrome";
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Gracefully quit the browser, then force-kill if it doesn't exit in time.
|
|
716
|
+
*/
|
|
717
|
+
async function killChromeGracefully(chromePath: string): Promise<void> {
|
|
633
718
|
const os = platform();
|
|
634
719
|
try {
|
|
635
720
|
if (os === "darwin") {
|
|
636
|
-
|
|
637
|
-
execSync(
|
|
721
|
+
const app = macAppName(chromePath);
|
|
722
|
+
execSync(`osascript -e 'quit app "${app}"'`, {
|
|
638
723
|
timeout: 5000,
|
|
639
724
|
stdio: ["pipe", "pipe", "pipe"],
|
|
640
725
|
});
|
|
641
726
|
} else if (os === "linux") {
|
|
642
|
-
|
|
727
|
+
// Kill the specific browser binary, not all Chromium variants
|
|
728
|
+
execSync(`pkill -TERM -f ${JSON.stringify(chromePath)}`, {
|
|
643
729
|
timeout: 5000,
|
|
644
730
|
stdio: ["pipe", "pipe", "pipe"],
|
|
645
731
|
});
|
|
646
732
|
} else if (os === "win32") {
|
|
647
|
-
|
|
733
|
+
const exe = chromePath.split("\\").pop() || "chrome.exe";
|
|
734
|
+
execSync(`taskkill /IM "${exe}"`, {
|
|
648
735
|
timeout: 5000,
|
|
649
736
|
stdio: ["pipe", "pipe", "pipe"],
|
|
650
737
|
});
|
|
@@ -653,26 +740,27 @@ async function killChromeGracefully(): Promise<void> {
|
|
|
653
740
|
/* may already be closed */
|
|
654
741
|
}
|
|
655
742
|
|
|
656
|
-
// Wait for
|
|
743
|
+
// Wait for browser to fully exit (up to 8s)
|
|
657
744
|
const start = Date.now();
|
|
658
745
|
while (Date.now() - start < 8000) {
|
|
659
|
-
if (!isChromeRunning()) {
|
|
660
|
-
log.debug(`
|
|
746
|
+
if (!isChromeRunning(chromePath)) {
|
|
747
|
+
log.debug(`Browser exited after ${Date.now() - start}ms`);
|
|
661
748
|
return;
|
|
662
749
|
}
|
|
663
750
|
await new Promise((r) => setTimeout(r, 500));
|
|
664
751
|
}
|
|
665
752
|
|
|
666
|
-
log.debug("
|
|
753
|
+
log.debug("Browser still running after graceful quit, force-killing...");
|
|
667
754
|
|
|
668
755
|
// Force kill if still alive
|
|
669
756
|
try {
|
|
670
757
|
if (os === "win32") {
|
|
671
|
-
|
|
758
|
+
const exe = chromePath.split("\\").pop() || "chrome.exe";
|
|
759
|
+
execSync(`taskkill /F /IM "${exe}"`, {
|
|
672
760
|
stdio: ["pipe", "pipe", "pipe"],
|
|
673
761
|
});
|
|
674
762
|
} else {
|
|
675
|
-
execSync(
|
|
763
|
+
execSync(`pkill -9 -f ${JSON.stringify(chromePath)}`, {
|
|
676
764
|
stdio: ["pipe", "pipe", "pipe"],
|
|
677
765
|
});
|
|
678
766
|
}
|
|
@@ -683,34 +771,35 @@ async function killChromeGracefully(): Promise<void> {
|
|
|
683
771
|
// Wait for processes to fully terminate after SIGKILL
|
|
684
772
|
await new Promise((r) => setTimeout(r, 1000));
|
|
685
773
|
|
|
686
|
-
//
|
|
687
|
-
// after a force-kill, preventing the next Chrome instance from starting.
|
|
774
|
+
// Remove SingletonLock files that may linger after a force-kill
|
|
688
775
|
if (os !== "win32") {
|
|
689
776
|
const home = process.env.HOME;
|
|
690
777
|
if (home) {
|
|
691
|
-
const
|
|
778
|
+
const lockSuffixes = ["SingletonLock", "SingletonSocket", "SingletonCookie"];
|
|
779
|
+
const profileDirs =
|
|
692
780
|
os === "darwin"
|
|
693
781
|
? [
|
|
694
|
-
`${home}/Library/Application Support/Google/Chrome
|
|
695
|
-
`${home}/Library/Application Support/
|
|
696
|
-
`${home}/Library/Application Support/
|
|
782
|
+
`${home}/Library/Application Support/Google/Chrome`,
|
|
783
|
+
`${home}/Library/Application Support/Microsoft Edge`,
|
|
784
|
+
`${home}/Library/Application Support/BraveSoftware/Brave-Browser`,
|
|
697
785
|
]
|
|
698
786
|
: [
|
|
699
|
-
`${home}/.config/google-chrome
|
|
700
|
-
`${home}/.config/
|
|
701
|
-
`${home}/.config/
|
|
702
|
-
`${home}/.config/
|
|
703
|
-
`${home}/.config/chromium/SingletonSocket`,
|
|
704
|
-
`${home}/.config/chromium/SingletonCookie`,
|
|
787
|
+
`${home}/.config/google-chrome`,
|
|
788
|
+
`${home}/.config/chromium`,
|
|
789
|
+
`${home}/.config/microsoft-edge`,
|
|
790
|
+
`${home}/.config/BraveSoftware/Brave-Browser`,
|
|
705
791
|
];
|
|
706
|
-
for (const
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
792
|
+
for (const dir of profileDirs) {
|
|
793
|
+
for (const suffix of lockSuffixes) {
|
|
794
|
+
const lockPath = `${dir}/${suffix}`;
|
|
795
|
+
try {
|
|
796
|
+
if (existsSync(lockPath)) {
|
|
797
|
+
unlinkSync(lockPath);
|
|
798
|
+
log.debug(`Removed stale lock: ${lockPath}`);
|
|
799
|
+
}
|
|
800
|
+
} catch {
|
|
801
|
+
/* best effort */
|
|
711
802
|
}
|
|
712
|
-
} catch {
|
|
713
|
-
/* best effort */
|
|
714
803
|
}
|
|
715
804
|
}
|
|
716
805
|
}
|
|
@@ -720,18 +809,37 @@ async function killChromeGracefully(): Promise<void> {
|
|
|
720
809
|
/**
|
|
721
810
|
* Spawn Chrome with the remote-debugging-port flag.
|
|
722
811
|
* Returns the child process so callers can detect early failures.
|
|
812
|
+
*
|
|
813
|
+
* On macOS, uses `open -a` which goes through Launch Services — the correct
|
|
814
|
+
* way to launch a .app bundle and ensure flags reach the Chrome process.
|
|
815
|
+
* Direct binary invocation on macOS can fail because Chrome's Mach-O binary
|
|
816
|
+
* goes through framework wrappers that may drop command-line flags.
|
|
817
|
+
*
|
|
818
|
+
* IMPORTANT: On macOS, `open -a` ignores --args if Chrome is ALREADY running.
|
|
819
|
+
* Callers must ensure Chrome is fully quit before calling this function.
|
|
723
820
|
*/
|
|
724
821
|
function spawnChrome(chromePath: string, port: number): ChildProcess {
|
|
725
822
|
const cdpFlag = `--remote-debugging-port=${port}`;
|
|
823
|
+
const os = platform();
|
|
726
824
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
825
|
+
let child: ChildProcess;
|
|
826
|
+
|
|
827
|
+
if (os === "darwin") {
|
|
828
|
+
const appName = macAppName(chromePath);
|
|
829
|
+
log.debug(
|
|
830
|
+
`Spawning browser via: open -a "${appName}" --args ${cdpFlag} --restore-last-session`
|
|
831
|
+
);
|
|
832
|
+
child = spawn("open", ["-a", appName, "--args", cdpFlag, "--restore-last-session"], {
|
|
833
|
+
detached: true,
|
|
834
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
835
|
+
});
|
|
836
|
+
} else {
|
|
837
|
+
log.debug(`Spawning Chrome: ${chromePath} ${cdpFlag} --restore-last-session`);
|
|
838
|
+
child = spawn(chromePath, [cdpFlag, "--restore-last-session"], {
|
|
839
|
+
detached: true,
|
|
840
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
841
|
+
});
|
|
842
|
+
}
|
|
735
843
|
|
|
736
844
|
// Capture stderr for diagnostics — Chrome prints errors here
|
|
737
845
|
let stderr = "";
|
|
@@ -850,13 +958,26 @@ export async function ensureBrowserAvailable(port = 9222): Promise<AutoLaunchRes
|
|
|
850
958
|
|
|
851
959
|
log.debug(`Found Chrome at: ${chromePath}`);
|
|
852
960
|
|
|
853
|
-
const running = isChromeRunning();
|
|
854
|
-
log.debug(`
|
|
961
|
+
const running = isChromeRunning(chromePath);
|
|
962
|
+
log.debug(`Browser currently running: ${running}`);
|
|
855
963
|
|
|
856
|
-
// Case 3:
|
|
964
|
+
// Case 3: Browser running without CDP → restart
|
|
857
965
|
if (running) {
|
|
858
|
-
log.debug("Killing
|
|
859
|
-
await killChromeGracefully();
|
|
966
|
+
log.debug("Killing browser gracefully for restart with CDP...");
|
|
967
|
+
await killChromeGracefully(chromePath);
|
|
968
|
+
|
|
969
|
+
// Verify browser is fully dead — critical on macOS where `open -a`
|
|
970
|
+
// ignores --args if the app is still alive.
|
|
971
|
+
if (isChromeRunning(chromePath)) {
|
|
972
|
+
log.debug("Browser still running after kill attempt — cannot restart with CDP");
|
|
973
|
+
return {
|
|
974
|
+
success: false,
|
|
975
|
+
action: "launch_failed",
|
|
976
|
+
chromePath,
|
|
977
|
+
detail:
|
|
978
|
+
"Could not stop the existing browser process. Please quit the browser manually and run assistme again.",
|
|
979
|
+
};
|
|
980
|
+
}
|
|
860
981
|
|
|
861
982
|
// Extra wait for profile lock release after kill
|
|
862
983
|
await new Promise((r) => setTimeout(r, 2000));
|
|
@@ -867,37 +988,37 @@ export async function ensureBrowserAvailable(port = 9222): Promise<AutoLaunchRes
|
|
|
867
988
|
return { success: true, action: "restarted", chromePath };
|
|
868
989
|
}
|
|
869
990
|
|
|
870
|
-
// Check if
|
|
991
|
+
// Check if browser process exited immediately
|
|
871
992
|
if (child.exitCode !== null) {
|
|
872
993
|
log.debug(
|
|
873
|
-
`
|
|
994
|
+
`Browser process already exited (code ${child.exitCode}) — may have crashed or profile is locked`
|
|
874
995
|
);
|
|
875
996
|
return {
|
|
876
997
|
success: false,
|
|
877
998
|
action: "launch_failed",
|
|
878
999
|
chromePath,
|
|
879
|
-
detail: `
|
|
1000
|
+
detail: `Browser exited immediately (code ${child.exitCode}). The profile may be locked. Try closing all browser windows first, then run assistme again.`,
|
|
880
1001
|
};
|
|
881
1002
|
}
|
|
882
1003
|
|
|
883
|
-
// Retry once —
|
|
1004
|
+
// Retry once — browser can be slow to start (extensions, session restore)
|
|
884
1005
|
log.debug("First CDP wait timed out after restart, retrying...");
|
|
885
1006
|
if (await waitForCDP(browser, 15000)) {
|
|
886
1007
|
return { success: true, action: "restarted", chromePath };
|
|
887
1008
|
}
|
|
888
1009
|
|
|
889
|
-
const stillRunning = isChromeRunning();
|
|
1010
|
+
const stillRunning = isChromeRunning(chromePath);
|
|
890
1011
|
return {
|
|
891
1012
|
success: false,
|
|
892
1013
|
action: "launch_failed",
|
|
893
1014
|
chromePath,
|
|
894
1015
|
detail: stillRunning
|
|
895
|
-
? "
|
|
896
|
-
: "
|
|
1016
|
+
? "Browser is running but CDP port is not responding. Try: 1) Quit the browser completely, 2) Run assistme again."
|
|
1017
|
+
: "Browser was restarted but exited unexpectedly. Try launching it manually to check for errors.",
|
|
897
1018
|
};
|
|
898
1019
|
}
|
|
899
1020
|
|
|
900
|
-
// Case 4:
|
|
1021
|
+
// Case 4: Browser not running → launch
|
|
901
1022
|
const child = spawnChrome(chromePath, port);
|
|
902
1023
|
|
|
903
1024
|
if (await waitForCDP(browser)) {
|
|
@@ -905,12 +1026,12 @@ export async function ensureBrowserAvailable(port = 9222): Promise<AutoLaunchRes
|
|
|
905
1026
|
}
|
|
906
1027
|
|
|
907
1028
|
if (child.exitCode !== null) {
|
|
908
|
-
log.debug(`
|
|
1029
|
+
log.debug(`Browser process already exited (code ${child.exitCode})`);
|
|
909
1030
|
return {
|
|
910
1031
|
success: false,
|
|
911
1032
|
action: "launch_failed",
|
|
912
1033
|
chromePath,
|
|
913
|
-
detail: `
|
|
1034
|
+
detail: `Browser exited immediately (code ${child.exitCode}). Try launching it manually to see any error dialogs.`,
|
|
914
1035
|
};
|
|
915
1036
|
}
|
|
916
1037
|
|
|
@@ -920,7 +1041,7 @@ export async function ensureBrowserAvailable(port = 9222): Promise<AutoLaunchRes
|
|
|
920
1041
|
return { success: true, action: "launched", chromePath };
|
|
921
1042
|
}
|
|
922
1043
|
|
|
923
|
-
const stillRunning = isChromeRunning();
|
|
1044
|
+
const stillRunning = isChromeRunning(chromePath);
|
|
924
1045
|
return {
|
|
925
1046
|
success: false,
|
|
926
1047
|
action: "launch_failed",
|
package/src/tools/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getBrowser, ensureBrowserAvailable } from "./browser.js";
|
|
1
|
+
import { type BrowserController, getBrowser, ensureBrowserAvailable } from "./browser.js";
|
|
2
2
|
import {
|
|
3
3
|
readFileContent,
|
|
4
4
|
writeFileContent,
|
|
@@ -270,6 +270,25 @@ export function getToolDefinitions(): ToolDefinition[] {
|
|
|
270
270
|
];
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Ensure Chrome is running with CDP and we have an active WebSocket connection.
|
|
275
|
+
* Called lazily by every browser tool so the user never has to call browser_connect explicitly.
|
|
276
|
+
*/
|
|
277
|
+
async function ensureConnected(browser: BrowserController, tabIndex?: number): Promise<void> {
|
|
278
|
+
if (browser.isConnected() && tabIndex === undefined) return;
|
|
279
|
+
|
|
280
|
+
// Auto-launch Chrome if CDP is not reachable
|
|
281
|
+
if (!(await browser.isAvailable())) {
|
|
282
|
+
const result = await ensureBrowserAvailable();
|
|
283
|
+
if (!result.success) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
`Chrome is not available (${result.action}). ${result.detail || "Please ensure Google Chrome is installed."}`
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
await browser.connect(tabIndex);
|
|
290
|
+
}
|
|
291
|
+
|
|
273
292
|
export async function executeTool(name: string, input: Record<string, unknown>): Promise<string> {
|
|
274
293
|
const browser = getBrowser();
|
|
275
294
|
|
|
@@ -298,36 +317,35 @@ export async function executeTool(name: string, input: Record<string, unknown>):
|
|
|
298
317
|
|
|
299
318
|
// ── Browser (CDP) ───────────────────────────────────────
|
|
300
319
|
case "browser_connect": {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const result = await ensureBrowserAvailable();
|
|
304
|
-
if (!result.success) {
|
|
305
|
-
throw new Error(
|
|
306
|
-
`Failed to auto-launch Chrome (${result.action}). ` +
|
|
307
|
-
"Please ensure Google Chrome is installed."
|
|
308
|
-
);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
return browser.connect(input.tab_index as number | undefined);
|
|
320
|
+
await ensureConnected(browser, input.tab_index as number | undefined);
|
|
321
|
+
return browser.isConnected() ? "Connected to browser." : "Failed to connect.";
|
|
312
322
|
}
|
|
313
323
|
case "browser_navigate":
|
|
314
|
-
|
|
324
|
+
await ensureConnected(browser);
|
|
315
325
|
return browser.navigate(input.url as string);
|
|
316
326
|
case "browser_read_page":
|
|
327
|
+
await ensureConnected(browser);
|
|
317
328
|
return browser.readPage();
|
|
318
329
|
case "browser_screenshot":
|
|
330
|
+
await ensureConnected(browser);
|
|
319
331
|
return browser.screenshot();
|
|
320
332
|
case "browser_click":
|
|
333
|
+
await ensureConnected(browser);
|
|
321
334
|
return browser.click(input.selector as string);
|
|
322
335
|
case "browser_type":
|
|
336
|
+
await ensureConnected(browser);
|
|
323
337
|
return browser.typeText(input.selector as string, input.text as string);
|
|
324
338
|
case "browser_press_key":
|
|
339
|
+
await ensureConnected(browser);
|
|
325
340
|
return browser.pressKey(input.key as string);
|
|
326
341
|
case "browser_scroll":
|
|
342
|
+
await ensureConnected(browser);
|
|
327
343
|
return (input.direction as string) === "up" ? browser.scrollUp() : browser.scrollDown();
|
|
328
344
|
case "browser_get_elements":
|
|
345
|
+
await ensureConnected(browser);
|
|
329
346
|
return browser.getInteractiveElements();
|
|
330
347
|
case "browser_evaluate":
|
|
348
|
+
await ensureConnected(browser);
|
|
331
349
|
return browser.evaluate(input.expression as string);
|
|
332
350
|
case "browser_list_tabs":
|
|
333
351
|
return browser.listTabs();
|
|
@@ -350,10 +368,31 @@ export async function executeTool(name: string, input: Record<string, unknown>):
|
|
|
350
368
|
console.log("━".repeat(60));
|
|
351
369
|
console.log("\n");
|
|
352
370
|
|
|
353
|
-
//
|
|
354
|
-
|
|
371
|
+
// Capture current page URL to detect navigation (sign that user acted)
|
|
372
|
+
let initialUrl = "";
|
|
373
|
+
try {
|
|
374
|
+
const info = await browser.getPageInfo();
|
|
375
|
+
initialUrl = info.url;
|
|
376
|
+
} catch {
|
|
377
|
+
/* may not be connected */
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Poll every 3s instead of blind wait — detect URL changes early
|
|
381
|
+
const deadline = Date.now() + waitSeconds * 1000;
|
|
382
|
+
while (Date.now() < deadline) {
|
|
383
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
384
|
+
try {
|
|
385
|
+
const info = await browser.getPageInfo();
|
|
386
|
+
if (initialUrl && info.url !== initialUrl) {
|
|
387
|
+
// User navigated — action likely completed
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
} catch {
|
|
391
|
+
// Connection may have dropped during user action
|
|
392
|
+
}
|
|
393
|
+
}
|
|
355
394
|
|
|
356
|
-
//
|
|
395
|
+
// Read the page to see current state
|
|
357
396
|
try {
|
|
358
397
|
const pageInfo = await browser.readPage();
|
|
359
398
|
return `User action wait completed. Current page state:\n${pageInfo.slice(0, 3000)}`;
|